Upgrade Swift to 0.8
Every breaking change in `cosmo-swift-sdk` 0.8.0, with the replacement for each.
Breaking changes when moving cosmo-swift-sdk from v0.7.0 to v0.8.0. The changelog has the full release notes; more than one version behind, chain the pages.
-
audio.noiseCancellationtakes a mode instead of a boolean —'off','denoise'or'voice_focus'. The new one is'denoise': it removes non-speech noise and keeps every voice, which is what a microphone several people share needs.'voice_focus'is the previous behaviour, and keeps only the speaker it judges primary — on a shared microphone that treats the second person as background and filters them out.trueandfalseremain valid on the wire, so a session started by an already-published SDK version is unaffected.truebecomes'voice_focus',falsebecomes'off'. A two-person setup that was passingtrueand losing the quieter speaker wants'denoise':AudioConfig(noiseCancellation: .denoise) -
Tools are built by calling a constructor on
AgentTool, and every constructor returnsAgentTool, so atools:literal reads[.webSearchTool(), .drawBoxTool(onDraw:)]. The enum cases are internal:.webSearch,.examineImage,.detectObjects,.pointAtObject,.client(...),.backgroundClient(...)and.screenLocate(...)are replaced bywebSearchTool(),examineImageTool(),detectObjectsTool(),pointAtObjectTool(),clientTool(...),backgroundClientTool(...)andscreenLocateTool(capture:).AgentTool.define/defineBackgroundare renamedclientTool/backgroundClientTool, each overloading on aToolSchemaor rawparameters.AgentToolis now a struct;nameandclientToolHandlerstay readable on the built value.// before let agent = try client.agent(tools: [.webSearch, .client(name: "lookup", description: "…", parameters: schema, handler: handle)]) // after let agent = try client.agent(tools: [.webSearchTool(), .clientTool(name: "lookup", description: "…", parameters: schema, handler: handle)]) -
The session-event types move to the top level under the cross-SDK names — the same symbols Python and TypeScript publish, with the
Eventpostfix the event union's members carry everywhere else.RealtimeSession.Event→RealtimeSessionEvent, and the payload typealiases follow:RealtimeSession.Ready→ReadyEvent,.TranscriptDelta→TranscriptDeltaEvent,.ModelText→ModelTextEvent,.TurnComplete→TurnCompleteEvent,.ToolCall→ToolCallEvent,.ToolDispatchStarted→ToolDispatchStartedEvent,.ToolResult→ToolResultEvent,.ToolInvocation→ToolInvocationEvent,.Reconnecting→ReconnectingEvent,.UserSpeechTimeout→UserSpeechTimeoutEvent,.SessionEnded→SessionEndedEvent,.ErrorEvent→ErrorEvent,.ErrorCode→ErrorCode,.RejectedTool→RejectedTool,.ResolvedAgent→ResolvedAgent. Case names and field shapes are unchanged — only the type spellings move. -
Passing a workspace API key (
cosmo_…) astokenis refused at construction in every SDK. That parameter takes a minted end-user token; a key there authenticates anyway, so the mistake used to work — and shipped the key with whatever app carried it. Pass the key as the API-key parameter, or mint a token for the user withmintTokenand pass that. Acts-as-user tokens (cosmo_pat_…) are unaffected.// before RealtimeClient(.init(token: apiKey)) // after RealtimeClient(apiKey: apiKey) -
The
.cosmo(CosmoEvent)wrapper case is gone: wirecosmo.usagenow surfaces directly as.usage(UsageEvent)(RealtimeSession.CosmoUsage→UsageEvent), matching the event's place in the Python and TypeScript unions.if case .cosmo(.usage(let u))becomesif case .usage(let u). -
ToolInvocationEvent.argsis now a plain[String: JSONValue](empty when the wire omits it), replacing the generator's opaqueArgsPayloadcontainer — read arguments directly instead of digging throughadditionalProperties.originis aToolInvocationOriginenum (.realtime/.server), keeping the wire's closed set exhaustively switchable. -
ToolOutcome's payloads are labeled:case ok(result:),case error(message:),case denied(reason:). The declaration now names what each case carries, matching the field names Python and TypeScript already publish. Reading one is unaffected —case .ok(let result)reads the same — but constructing one positionally is not:ToolOutcome.error(text)becomesToolOutcome.error(message: text).PostToolUseContext.initis public and takes an outcome, so a hook's own unit tests construct these and need the labels. -
RealtimeClient.Transport.webrtcis the canonical/default room transport case, replacing the vendor-named.livekit. The deprecated.livekitalias still connects through WebRTC, but exhaustive switches must add.webrtc. -
A client tool carries the handler that runs it:
AgentTool.clientTool(name:description:parameters:handler:)no longer defaultshandlertonil, and neither does the.clientcase. Declaring a tool this client cannot execute advertised one that failed on every invocation, which the transport layer already said it would. A tool the server invokes over RPC without ever listing it to the agent is unchanged and unaffected — that isRealtimeAgent.start(…rpcHandlers:), the register-only complement, and it is now the only way to express it. -
model_optionsis gone; its provider block moves ontomodel, which now takes either the model string it always took or one provider block naming the provider once — a model that disagrees with its knobs is unrepresentable. The provider types drop theOptionssuffix (GeminiModelOptions→GeminiModel, and likewise for OpenAI, OpenAI-mini and Grok), each block'sturn_detectionaccepts only the detectors its provider offers (Cosmo-VAD tuning moves toCosmoVadConfigon the block'scosmoVadfield), and theModelOptionsenum becomesRealtimeModel, whose.id("…")case is the string form and whose block cases carry the provider's knobs. A block with no model id runs the provider's default, and.id("gemini")still selects a provider by name. The server keeps acceptingmodel_optionsfrom existing releases — the old pair folds intomodelserver-side — so upgrading the SDK is not coupled to a backend deploy.Move the block to
modeland fold the old model string into itsmodel_id:// before try client.agent(model: "gemini-live", modelOptions: .gemini(temperature: 0.7)) // after — the case is the provider try client.agent(model: .gemini(GeminiModel(modelId: "gemini-live", temperature: 0.7))) -
Client settings are the initializer's parameters, so
RealtimeClient.Optionsis gone.RealtimeClient(.init(apiKey: key))becomesRealtimeClient(apiKey: key), and the same fortoken:andtokenSource:;try RealtimeClient()is unchanged. Every parameter —baseURL,connectTimeout,requestTimeout,verifyTLS— keeps its name and default, one level up.Options.Credentialgoes with it: the four initializers cover the same three credential forms, so nothing is lost. -
RealtimeClient.canMintis removed. It reported whether a credential was an API key but gated nothing —mintTokenalways let the server rule on the credential, and it still does. A client that cannot mint raisesMintTokenErrorwithcode == .missingApiKey, before the request goes out. -
The
CosmoRealtimeMintproduct is removed andmintToken(externalUserId:ttlSeconds:)now ships inCosmoRealtime. Drop the product from yourPackage.swiftdependencies and deleteimport CosmoRealtimeMint; the method is on the sameRealtimeClientand its signature is unchanged. Minting still requires an api-key credential — a client holding a minted token or aTokenSourceraisesMintTokenErrorwithcode == .missingApiKeybefore any request goes out — and it matches how the Python and TypeScript SDKs expose the same call. -
RealtimeErroris now a protocol every error in the SDK conforms to, socatch let error as RealtimeErrorcatches them as one family — matchingexcept RealtimeErrorin Python andinstanceof RealtimeErrorin TypeScript. It replaces the enum of the same name, whose cases were unreachable:.connectTimeoutwas converted internally before any caller saw it, and.sessionStartFailed,.notConnected,.alreadyConnected,.screenShareUnavailableand.invalidWirePayloadwere never thrown at all. Catch the equivalent SDK error instead —SessionStartErrorfor a failed start,SessionStateErrorfor a call the session cannot serve.Changed: Failures that used to surface as raw LiveKit or Foundation errors now arrive as SDK errors, so the catch above covers them. A failed data publish, byte stream, or microphone toggle raises
SessionStartErrorcodedtransport; unreadable or non-JSON.mcp.jsonraisesMcpError; and aSKILL.mdthat cannot be read or decoded, or a skills directory that cannot be listed, raisesSkillError— matching what the Python SDK already did. Code matching on the underlying framework error types needs to read the SDK error instead. -
SkillParseErroris nowSkillErrorand carries acodenaming which failure it was, so a caller can tell them apart without reading the message. The codes arenot_a_directory,cannot_read,missing_frontmatter,unterminated_frontmatter,malformed_frontmatter_line,duplicate_frontmatter_key,missing_descriptionandduplicate_skill_name— aSkillErrorCodeenum in Python and Swift, a union of the same values in TypeScript. The old name described only the five parsing failures, while the type has always also covered a bad path and a duplicate skill name. Match onerr.code;err.messageis the sentence alone, and an unreadable skills directory now raisesSkillError(cannot_read)where it used to escape as a barePermissionError.Breaking: Attaching skills reads the same in every SDK. Swift takes a directory through the
skills:argument itself —client.agent(skills: .directory(skillsURL))— instead ofloadSkills(fromDirectory:), which is no longer public;.directory(_:)is a factory on[Skill], so inline skills are unchanged and the two compose with+. Swift'sskillsis optional rather than defaulting to an empty array.parseSkillMdtakesdefaultNamedirectly in TypeScript rather than wrapped in an options object, andparse_skill_mdis now public in Python for SKILL.md text you already hold. The skill-assembly internals —resolveSkills,skillsMenuText,buildLoadSkillTool,LoadSkillWiring,loadSkillToolName,UnknownSkillError— are no longer public in Swift; the wire name the tool registers under is unchanged, so a hook matchingcosmo_sdk_load_skillkeeps working. -
Every MCP failure now raises
McpError, carrying acodenaming which one it was, so a caller can tell them apart without reading the message. The codes arenot_a_file,cannot_read,invalid_json,missing_servers,invalid_server_entry,missing_command,invalid_args,invalid_env,invalid_cwd,duplicate_server_name,connection_failed,invalid_response,server_errorandtool_error, — anMcpErrorCodeenum. It replacesMCPConfigErrorandMCPError, and covers connection and tool-call failures as well as config, so onecatch let error as McpErrorspans the whole concept. Match onerror.code; the message is the sentence alone.McpErroris no longer aValueError— a dead subprocess is no ValueError. Two classes fold into codes:McpToolError, a bareRuntimeErroroutside the error family, becomestool_error, andMcpExtraNotInstalledErrorbecomesextra_not_installed. The second is breaking for anyone catchingImportErrororExtraNotInstalledErroraround a missing[mcp]install — catchMcpErrorand match the code instead.ExtraNotInstalledErroris removed with it: MCP was the only extra that raised it, so it was a base class for a family of none.Breaking: Attaching MCP servers reads the same in both SDKs. Swift takes servers through the
mcp:argument itself —client.agent(mcp: .configFile(configURL))— instead ofMcpRegistry, which is no longer public;.configFile(_:)is a factory on[McpStdioServer], so inline servers are unchanged and the two compose with+.catalogAgentnow throws, since duplicate server names are rejected when the agent is built rather than mid-call. The MCP internals —McpRegistry,ConnectedMcp,SkippedTool,MCPToolInfo,MCPCallResult,MCPTransport,MCPTransportFactory,defaultMCPTransportFactoryandparseMcpConfig— are no longer public in Swift.Fixed: Swift now rejects malformed
.mcp.jsonfields it previously accepted in silence.argsthat is not an array, or holds a boolean or an object, raisesinvalid_argsinstead of being coerced through string conversion — atruebecame the argument"1". Anenvthat is not an object of strings raisesinvalid_envrather than being dropped, which had launched the server without the variables it was configured with; a non-stringcwdraisesinvalid_cwdon the same footing. Duplicate server names are now rejected in Swift as they already were in Python. In Python, an unreadable config file raisesMcpError(cannot_read)where it used to escape as a barePermissionError, an invalid document isinvalid_jsonrather than sharing one message with an unreadable one, and"args": nullmeans absent, as it already did forenvandcwd. Both SDKs run the samemcp-config-vectors.jsonconformance file.Fixed: The runtime codes now say what actually failed. A dead subprocess reports
connection_failedin Python where it used to reportserver_error, and a reply the SDK cannot decode reportsinvalid_response, which nothing raised before — the three are read from the exception themcppackage raises rather than collapsed into one. In Swift, a well-formed.mcp.jsonwhose root is not an object reportsmissing_serversinstead of claiming the text is not valid JSON, and a config inside a directory the process cannot traverse reportscannot_readinstead ofnot_a_file—fileExistsanswers false for a permission wall exactly as it does for an absent file, so the read classifies it now.Breaking: A number in
argsis accepted only when it is whole and fits in a signed 64-bit integer, and is written in decimal.1.0and1are indistinguishable once decoded and a larger integer reached the process in scientific notation, so neither had a spelling both SDKs agreed on; quote the value instead. Both SDKs also walk a document's entries in name order now, which fixes the order servers are attached in and which malformed entry is reported when more than one is bad — Python previously followed document order.Fixed: A
.mcp.jsonwhose bytes are not UTF-8 now raisesMcpErrorcodedcannot_readin both SDKs. Python raised a bareUnicodeDecodeError, which is aValueErrorrather than anOSErrorand so escaped the error family entirely; Swift reportednot_a_fileabout a file that is there. -
MintTokenError.codeis now a closedMintTokenErrorCodenaming what the SDK saw —request_failed,invalid_response,request_rejectedormissing_api_key— and the server's own rejection slug moves toserverCode, set only when the code isrequest_rejected. The two were previously the same field, socodecould hold either the SDK's category or anything the server sent, down to a synthetichttp_<status>, with no way to tell which. Match onerr.codefor what happened to the request and readerr.serverCodefor why the server refused. Handlers comparingcodeagainst"transport_error"or against a server slug such as"auth_failed"need updating;str(err)is now the message alone, without thecode:prefix.Breaking: A
TokenSourcethat cannot produce a token now raisesTokenSourceErrorrather thanMintTokenError, with its ownTokenSourceErrorCode—request_failed,request_rejected,invalid_responseorfetcher_failed. Resolving a token source is not part ofmintToken: it happens beneath every authenticated call —verify,mintToken, session start, dial and usage reads all resolve it first, and it re-resolves on expiry and after a 401 — so the failure surfaced under the name of one operation it mostly had nothing to do with.token_source_failedis gone fromMintTokenErrorCodeaccordingly, and the four new codes say which part failed where one bucket said only that something did. A refused redirect isrequest_failedin every SDK — it never reached a token endpoint, so there is no rejection to report; Python previously reported it as anhttp_<status>rejection.Breaking: TypeScript gets the same two errors.
MintTokenErrorCodeandTokenSourceErrorCodeare literal unions rather than aliases ofstring, both errors take{ code, message, serverCode }, and a malformedTokenSource.endpointURL now throws aTypeErrorrather than an SDK error — argument validation is not part of the error family, which is what the Python SDK already did.Breaking: Swift gets the same two errors, as structs replacing the
MintTokenErrorenum.MintTokenError.rejected(code:detail:),.transport(message:)and.invalidResponse(message:)are gone; construct or matchMintTokenError(code:message:serverCode:)with aMintTokenErrorCodeinstead, and expectTokenSourceErrorwhere aTokenSourcefetch used to raise a mint error.mintTokennow refuses a client built with a minted token or a token source before the request goes out, with codemissingApiKey, rather than letting the server answer 401 — andTokenSource.customrejects a fetcher returning an emptyjwtinstead of sending it as a bearer.// before — the enum's rejected case carried the server's slug if case .rejected(let code, _) = error, code == "auth_failed" { reauthenticate() } // after if error.code == .requestRejected, error.serverCode == "auth_failed" { reauthenticate() } -
pushAudioBuffer(_:)must be called from a capture queue rather than an audio render callback because transports may synchronously convert and copy the buffer. Caller-owned audio stream start and stop operations are now serialized, so a rapid stop cannot be overtaken by an older start. -
mintTokentakes its subject unlabeled —mintToken("user-123", ttlSeconds: 3600)— matching Python and TypeScript, which pass the external user id positionally. The labeledmintToken(externalUserId:)spelling is removed; delete the label at each call site. -
AmbienceConfigand the agent'saudio.ambiencefield are removed from every SDK, The field never produced audible ambience on any session started through this API — it was accepted and validated, then dropped — so removing it changes no behavior. Deleteambiencefrom your agent'saudioblock; the rest of the block is unchanged. -
The five backend calls share an
ApiErrorbase —MintTokenError,TokenSourceError,VerifyError,UsageErrorandDialErrorall descend from it, so one catch covers any of them while catching a specific one still says which call it was.serverCodemoves to the base, because a rejection slug belongs to whichever backend answered rather than to the call that asked.VerifyError,UsageErrorandDialErrorgain closed code enums in place of a bare string:request_failed,request_rejected,invalid_response, plusinvalid_requeston dial and usage for a call the SDK refuses to make. Where a server slug was thecode, it is nowserverCodeandcodeisrequest_rejected. Swift gainsDialError, which it did not have, and itsUsageErrorandVerifyErrorbecome structs carryingcoderather than case-carrying enums. -
AudioUnavailableError.codeis the closedAudioUnavailableErrorCoderather than aString—micDenied,micNotFound,micInUseandaudioUnavailable, the same four values every SDK already reported. Aswitchover it is exhaustive. Comparisons against the slug no longer compile: match the case (error.code == .micDenied), and readerror.code.rawValuewhere the string itself is wanted. -
Hooks no longer fire for the screen-capture RPC or for caller-registered RPC methods — hooks fire for tool calls, and wire plumbing is not one. A
PreToolUsehook that matchedscreen_capturepreviously observed, denied, or rewrote captures on Swift only; Python and TypeScript already behaved this way, and all three SDKs now pin the contract. -
One
CredentialsErrorwith the closedCredentialsErrorCodereplaces six spellings of the same failure — TypeScript'sCredentialError(singular), Python'sCredentialsErrorplus itsNotFound,File,ExpiredandMismatchsubclasses, and Swift's case-carrying enum. The five resolution codes are the slugs the cross-SDK vectors already pinned;conflicting_credentials,api_key_in_token_slotandinsecure_base_urlcover the construction-time guards, which previously threw an untyped error. -
Every error carries
message, soexcept RealtimeError as e/catch let e as RealtimeErrorcan read it without narrowing to a concrete type first. In Swiftmessageis now aRealtimeErrorrequirement, which a type conforming to the protocol outside the SDK must add. In PythonRealtimeErrorand its argument-less subclasses —NotConnectedError,VideoPublishAlreadyActiveError— now take the message as their one positional argument.str(error)is unchanged, including the"code: message"form the session, dial, usage, verify and tool-schema errors render. -
Registering a hook that cannot work now throws
HookErrorin every SDK, with the closedHookErrorCode—malformed_matcher,invalid_hook,server_hook_not_allowed. Python raisedValueErrorandTypeErrorand TypeScript a bareErrorfor these, so neither was catchable asRealtimeError; in Python it is exported fromcosmo_ai.hooks, beside the hooks it describes; Swift'sMalformedHookMatcherErroris replaced. In PythonHookErroris aValueError, so anexcept ValueErroraround hook declaration keeps firing; the two cases that raisedTypeErrorno longer do. -
The screen-capture handler has one shape — it always receives the
ScreenCaptureRequest. The zero-argument form is gone: in Pythonscreen_locate_tool's handler must accept the request (lambda request: ...; ignore it if unneeded), and in Swift the handler type is the top-levelScreenCaptureHandler— the request-taking signature ({ request in ... }or{ _ in ... }), matching the Python and TypeScript name — with the nestedScreenLocateTool.HandlerandRequestHandlernames removed. TypeScript already had this shape and is unchanged. Migrate deliberately: a zero-argument handler now fails at call time, and in Python the arity error's text would reach the model as the locator's spoken reason. -
ScreenCaptureRequestno longer carrieswantsElements(wants_elementsin Python); the request is an empty envelope, and in Swift its initializer isinit(). Capture handlers should always collect elements — the server has always requested them, so nothing changes at runtime. A handler that read the flag can simply drop the check. -
Declaring
screen_locateon the websocket transport now refuses at session start withSessionStartError(code: "config",server_code: "screen_locate_unsupported") — the locator's capture payload travels as a byte stream, a channel the single-socket carrier does not have. Previously TypeScript silently skipped registration and Python and Swift captured the screen and then failed to deliver it; now the capture handler never runs. The Python background-tools refusal gains the matchingserver_code: "background_tools_unsupported". -
ErrorCode,SessionStatus,UsageStatusandCredentialKindnow accept a value the server added after your package shipped, instead of failing the payload that carried it — previously an unrecognized error code cost the whole error event, and an unrecognized status failed the usage request along with the counters you asked for. Swift switches over these enums need adefaultor@unknown defaultnow that they carry anunknown(String)case and are no longer@frozen; in Python both kinds are members of the enum, so usevalue in list(TheEnum)rather thanisinstanceto tell them apart.TranscriptRoleis unchanged.// before — the enums were frozen; an exhaustive switch compiled switch event.code { case .authFailed: signIn() case .versionMismatch: promptUpgrade() // …every declared case } // after — a value the server added arrives as .unknown(String) switch event.code { case .authFailed: signIn() case .versionMismatch: promptUpgrade() default: banner(event.code.rawValue) }In Python a server-added value is an enum member like any other, so declared versus added is a list check:
-
RealtimeSessionEventgains a case. The session now owns the coalesced transcript — readsession.transcript(oneTranscriptItemper turn,Identifiableby its stableid, with anisFinalflag) instead of folding the raw delta stream yourself, and the new.transcriptUpdated(TranscriptUpdatedEvent)is yielded onsession.eventswith the complete updated list after every change, session-synthesized like.sessionEnded. Aswitchover the event union without adefault:arm needs a new case (the forward-compatibility posture already calls fordefault:alongside.unknown).send(text:)now surfaces the sent text on the stream as its own closed user.transcriptfinal (plus the update event) unlesstranscript: falseis passed; an in-progress speech turn is unaffected. The raw.transcriptdelta events are otherwise unchanged. -
Every way a start can fail now raises one
SessionStartError, whose closedSessionStartErrorCodenames how far the attempt got —transport,invalid_response,join_failed,config,busy,entitlement,version_mismatch,voice_disabled,rejected,handshake_failed,ready_timeout. Switch oncodewhere you used to branch on a type or read an HTTP status, and read the server's own rejection slug fromserverCodebeside it. It replaces theRealtimeSessionErrorenum. In Python the base error'scode— previously open, carrying the server's own slug or a synthetichttp_<status>— closes to the enum, with the slug moving toserverCode.// before catch let error as RealtimeSessionError { … } // after catch let error as SessionStartError where error.code == .readyTimeout { retry() } -
SessionStartError.detailis aSessionStartRejectionin every SDK — the server's structured reason for refusing a start, which no SDK carried in full before. Each group of fields belongs to one server code:limit/activeforconcurrent_session_limit,granted_minutes/used_minutesforfree_minutes_exhausted,balance_cents/top_up_pathforinsufficient_credits,meter/included/used/reset_atforquota_exceeded, andprovider/allowed_providers/plan/upgrade_pathforprovider_not_entitled. A field the server adds that the SDK does not name is kept rather than dropped. -
Calling a session method the session cannot serve now throws
SessionStateErrorwith the closedSessionStateErrorCode—not_connected,already_started,audio_publish_already_active,video_publish_already_active,screen_share_unavailable,invalid_payload. It replaces six cases of the session error enum. A send issued beforereadyand one issued after the session ended both reportnot_connected. -
AgentTool.name,AgentTool.clientToolHandler, andAgentTool.sdkToolNamePrefixare removed — anAgentToolis construction-only. Keep the name and handler you pass at construction; the SDK registers the handler from the declaration. The reservedcosmo_sdk_prefix is still enforced at session start, with no caller decision attached. -
Audio that will not open throws
AudioUnavailableError, whosecodenames the failure, where the session's own error type was thrown before. A refused microphone takes this path — the defaultstart()publishes the mic as it joins — and so does an audio engine that will not start: no usable input format, or a converter that will not build. It reaches you the same way on both carriers, fromstart()and fromsetMuted. It is its own type, so a catch written for a start failure no longer matches it — catchRealtimeErrorfor both, or add a second catch. Swift namesmic_deniedwhere the platform reports a refused permission andmic_not_foundwhere it reports no usable input; an audio fault it cannot attribute isaudio_unavailablerather than a transport failure. -
The six turn-taking and reasoning enums —
InterruptionSensitivity,GrokReasoningEffort,ThinkingLevel,EndOfSpeechSensitivity,SemanticEagernessandTurnDetectionMode— are declared by the SDK rather than aliased to its generated internals. Reading a case or arawValueoff one previously needed a secondimport CosmoRealtimeAPI, a module the package does not publish; importingCosmoRealtimealone is now enough. Case names and wire values are unchanged, so code that spells them by name compiles as before. Code that reached into the generated module — importingCosmoRealtimeAPI, or namingComponents.Schemas.InterruptionSensitivityand its siblings explicitly — drops that import and uses the SDK's own type of the same name. -
The deprecated
String-returningdial(phoneNumber:callerNumber:)overload is removed;dialreturnsDialResultonly. Read the id fromresult.dialId— code that used the returned string gets the identical value fromresult.dialId.uuidString.lowercased(). -
ErrorEvent.fatalis a plainBoolinstead ofBool?. A frame that omits the field decodes asfalse, matching the wire default and the other SDKs. Read it directly — remove any unwrapping,?? false, or== truearound it. -
ReadyEvent.rejectedToolsis a plain[RejectedTool]instead of[RejectedTool]?. The server always reports the list — empty means nothing was rejected — and a frame that omits the field decodes as[], matching the other SDKs. Read it directly and drop any nil-handling or?? []. -
The screen tools' machinery leaves the public surface. The
cache:overloads ofscreenLocateTool,screenClickElementToolandscreenHighlightElementToolare removed, andScreenCaptureCache, theScreenLocateToolclass and itsrpcMethod/byteStreamTopicconstants are internal — migrate by dropping thecache:argument; every screen tool shares the SDK's store automatically.ScreenCapture.contextis now opaque and optional ((any Sendable)?, withelementsandcontextdefaulted in the initializer) andScreenCaptureContextis removed — stash your own context type at capture time and cast it back in your click/highlight handler. -
Server-sent types — the session events,
CredentialInfo,SessionUsage,SessionTokenUsageandRealtimeSessionStartTimings— no longer expose member-wise initializers. They are decoded, never constructed: build a test fixture by decoding the wire JSON the server would send (JSONDecoder().decode(ReadyEvent.self, from: json)), which also validates the fixture against the wire shape. Types you construct yourself —SilenceTimeout,Say,EndCalland all agent and session configuration — are unchanged. -
RealtimeSessionEventgains a case —.sessionEndingSoon(SessionEndingSoonEvent), the server's session-limit warning withsecondsRemainingand a stablereasonslug, previously surfaced through the unknown-event fallthrough. Aswitchover the event union without adefault:arm needs the new case (the forward-compatibility posture already calls fordefault:alongside.unknown). The session keeps running until.sessionEnded. -
agent.start(...)now returns when the session is ready — the server's handshake has landed — instead of at transport join, so every session method works the moment it returns. A session whose ready handshake never arrives within 40 seconds is torn down and the start throwsSessionStartErrorcodedreadyTimeout; a room that closes before ready throws it codedhandshakeFailedwith a synthetic status of0, carrying the server's boot-failureerrorframe code and message when one preceded the close, elsehandshake_disconnect. Cancelling the task that awaits a start tears the session down and throwsCancellationError, soTask.cancel()and SwiftUI's.taskteardown abort a start cleanly. Readiness is also read from the agent'scosmo.readyparticipant attribute — at join and after a reconnect — so a session that joins after the agent came up still observes it. -
Session state observation now matches the other Cosmo SDKs. Read the current value as
await session.state, and pass anonStateChange:handler toagent.startto observe every transition from.idleon — thesession.statesstream is removed. The state vocabulary is the shared five-state machine: the distinct.reconnectedcase is gone (a completed recovery re-enters.connected), the type is namedSessionState, and its terminal case is.disconnected(reason:detail:)carrying the same five-slugDisconnectReasonthe SessionEnd hook context uses, with the server's end slug or transport message indetail.// before Task { for await state in session.states { render(state) } } // after let session = try await agent.start(onStateChange: { state in render(state) }) let current = await session.state -
The token counters on
UsageEventandSessionTokenUsageare plainIntinstead ofInt?. A payload that omits a counter decodes as0, matching the wire default and the other SDKs. Read them directly — remove any unwrapping,?? 0, or== nilaround them.SessionUsage.tokensitself stays optional: a provider that reports no token usage still yields no breakdown. -
ToolSchemaErroris nowToolDefinitionError, and it covers the whole declaration — a bad tool name and a missing or overlong description throw it too, where Python raised a bareValueErrorand TypeScript a bareError. Both are now catchable asRealtimeErrorlike every other SDK error; in PythonToolDefinitionErroris still aValueError, so existing handling keeps working.codeis the closedToolDefinitionErrorCoderather than a string. Swift'sToolDefinitionErrorandToolSchemaConsistencyCheck.Failureare folded into it, the latter as codeschema_type_mismatch. -
A tool-call validation failure reports its issues as
ToolInputIssuein every SDK —path,code,constraint— where Python had raw dictionaries keyedloc/type/ctx, Swift nested the type inside the error, and TypeScript carriedpathas an array of segments.pathis now the dotted form (address.city,items[2].sku) everywhere, the same string theINVALID_INPUTmessage renders.TypeScript exports
ToolInputIssuefromcosmo-ai/tool: it is the typeToolInputValidationError.issuescarries, so a caller reading them has to be able to name it.