Upgrade Python to 0.6
Every breaking change in `cosmo-ai-sdk` 0.6.0, with the replacement for each.
Breaking changes when moving cosmo-ai-sdk from v0.5.1 to v0.6.0. The changelog has the full release notes; more than one version behind, chain the pages.
-
Tools are built by calling a constructor, and every constructor returns
AgentTool.WebSearchTool()and its siblings are replaced byweb_search_tool(),examine_image_tool(),detect_objects_tool(),point_at_object_tool(),end_call_tool();draw_box,draw_point,screen_locate,screen_click_element,screen_highlight_elementandscreen_highlight_boxgain a_toolsuffix. The tool models are no longer exported — declare a hand-written JSON Schema withclient_tool(...)/background_client_tool(...)instead of constructingClientTooldirectly, and annotate withAgentTool, which is now the single discriminated union (RealtimeToolSpecis gone).# before client.agent(tools=[WebSearchTool(), ClientTool(name="lookup", description="…", parameters=schema, handler=handle)]) # after client.agent(tools=[web_search_tool(), client_tool(name="lookup", description="…", parameters=schema, handler=handle)]) -
client_tool(...)andbackground_client_tool(...)validate at construction, matching@tool: the name grammar, the reservedcosmo_sdk_prefix, the description, and the schema dialect. A declaration the server would refuse now fails where you wrote it instead of arriving as aready.rejected_toolsentry at connect. -
Server-event models no longer carry the
typeandidfields.typewas a constant restating the class (isinstanceis the idiom, and decode never read the field), andidwas a client-generated UUID that correlated with nothing — the wire defines neither on server events. Events now expose exactly the wire payload, matching the TypeScript and Swift SDKs field-for-field. Code branching onevent.type == "…"switches toisinstance(event, …Event); code loggingevent.iddrops it.# before async for event in session: if event.type == "transcript": print(event.text) # after async for event in session: if isinstance(event, TranscriptDeltaEvent): print(event.text) -
audio.noise_cancellationtakes 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(noise_cancellation=NoiseCancellation.DENOISE) -
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 withmint_tokenand pass that. Acts-as-user tokens (cosmo_pat_…) are unaffected.# before RealtimeClient(token=api_key) # after RealtimeClient(api_key=api_key) -
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'scosmo_vadfield), and the union is namedRealtimeModel:RealtimeModelOptionsis renamed, taking either the string or the block, withRealtimeModelBlockfor the block alone. A block with no model id runs the provider's default, andmodel: "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 client.agent(model="gemini-live", model_options=GeminiModelOptions(temperature=0.7)) # after client.agent(model=GeminiModel(model_id="gemini-live", temperature=0.7)) -
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, plusextra_not_installed— anMcpErrorCodeenum. It replacesMcpConfigError, and covers connection and tool-call failures as well as config, so oneexcept 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 toserver_code, 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.server_codefor 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 ofmint_token: it happens beneath every authenticated call —verify,mint_token, 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.mint_tokennow refuses a client built with a minted token or a token source before the request goes out, with codemissing_api_key, rather than letting the server answer 401 — andTokenSource.customrejects a fetcher returning an emptyjwtinstead of sending it as a bearer.# before if err.code == "auth_failed": reauthenticate() # after if err.code == "request_rejected" and err.server_code == "auth_failed": reauthenticate() -
A
TokenSource.customfetcher now resolves with the same shapemint_tokenreturns — aMintedToken— in every SDK. Python no longer accepts a plain{jwt, expires_at}mapping (constructMintedToken, which still parses an RFC 3339expires_atstring), and TypeScript no longer accepts a stringexpiresAt(pass aDate; theFetchedTokentype is removed — annotate withMintedToken). Python's directTokenSource(...)construction now validates identically tocustominstead of bypassing it. Swift is unchanged.# before async def fetch(): return {"jwt": jwt, "expires_at": expires_at} # after — construct MintedToken (still parses an RFC 3339 string) async def fetch(): return MintedToken(jwt=jwt, expires_at=expires_at) -
AmbienceConfigand the agent'saudio.ambiencefield are removed from every SDK, along withAmbienceTrack. 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.server_codemoves 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 nowserver_codeandcodeisrequest_rejected. Swift gainsDialError, which it did not have, and itsUsageErrorandVerifyErrorbecome structs carryingcoderather than case-carrying enums. -
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 threwValueErrororTypeError.CredentialsErroris also aValueError, so existing handling keeps working. -
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. These previously raisedValueErrorandTypeError, so neither was catchable asRealtimeError.HookErroris exported fromcosmo_ai.hooks, beside the hooks it describes.HookErroris aValueError, so anexcept ValueErroraround hook declaration keeps firing; the two cases that raisedTypeErrorno longer do. -
AudioUnavailableErrorandAudioPublishAlreadyActiveErrornow require the message they always carried, soAudioUnavailableError()with no arguments raisesTypeErrorwhere it used to build an error with no text. Pass the message positionally, as every raise site already did. -
session-ending-soonnow decodes to the typedSessionEndingSoonEvent, carryingseconds_remainingand a stablereasonslug, instead of surfacing through the unknown-event fallthrough. Code that matchedUnknownEventwithraw_type == "session-ending-soon"and read the raw payload must matchSessionEndingSoonEventinstead — the unknown-event arm no longer fires for this frame. The session keeps running untilsession-ended, so use the warning to have the agent wrap up or show a countdown. -
agent.start()now resolves 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. This closes a silent-loss window: a send in the join→ready gap previously reached a data channel no agent was subscribed to yet, and was dropped with no error. A session whose ready handshake never arrives within 40 seconds is torn down andstart()raisesSessionStartErrorcodedready_timeout; a room that closes before ready raises it codedhandshake_failed, carrying the server's boot-failureerrorframe code and message when one preceded the close. Cancelling a pending start tears the session down and re-raisesCancelledError, soasyncio.timeoutand task cancellation abort a start cleanly. Readiness is also read from the agent'scosmo.readyparticipant attribute, so a session that joins after the agent came up — a mid-call observer, a reconnect — still observes it. -
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.In Python a server-added value is an enum member like any other, so declared versus added is a list check:
if event.code not in list(ErrorCode): log_unknown(event.code) -
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 fromserver_codebeside it. It replacesVersionMismatchError. 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 toserver_code.# before — code carried the server's own slug try: session = await agent.start() except SessionStartError as e: if e.code == "concurrent_session_limit": wait_and_retry() else: raise # after — code is the closed SessionStartErrorCode; the slug is server_code try: session = await agent.start() except SessionStartError as e: if e.code == "busy": wait_and_retry() else: raise -
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 replacesNotConnectedError,AudioPublishAlreadyActiveErrorandVideoPublishAlreadyActiveError. A send issued beforereadyand one issued after the session ended both reportnot_connected. -
The session state machine's value type has one name in every SDK —
SessionState.RealtimeSessionStateis renamed; fields, kinds, and behaviour are unchanged, so the migration is the rename alone. -
ToolSchemaErroris nowToolDefinitionError, and it covers the whole declaration — a bad tool name and a missing or overlong description throw it too, where a bareValueErrorwas raised before. It is now catchable asRealtimeErrorlike every other SDK error, and is still aValueError, so existing handling keeps working.codeis the closedToolDefinitionErrorCoderather than a string. -
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.