Cosmo Realtime SDK
ReleasesMigration guidesTypeScript

Upgrade TypeScript to 0.7

Every breaking change in `cosmo-ai` 0.7.0, with the replacement for each.

Breaking changes when moving cosmo-ai from v0.6.0 to v0.7.0. The changelog has the full release notes; more than one version behind, chain the pages.

  • TranscriptDeltaEvent is now the wire shape — { role, text, isFinal }. The client-derived id, turnId, and append fields are removed: they were rendering instructions, and the session's coalesced transcript — session.transcript, one TranscriptItem per turn with a stable id — is the render-ready form.

  • tool() is renamed clientTool(), and the background form is its own constructor rather than a flag: tool({ background: true, ... }) becomes backgroundClientTool({ ... }), and background is gone from the option types — the constructor decides the form, so a caller can no longer set it to the value contradicting the one they called. The typed, raw and unsafe input forms are unchanged; they stay overloads of each constructor.

    // before
    const lookup = tool({ name: 'lookup', description: '…', parameters, handler });
    const index  = tool({ name: 'index', description: '…', parameters, handler, background: true });
    // after — the constructor decides the form
    const lookup = clientTool({ name: 'lookup', description: '…', parameters, handler });
    const index  = backgroundClientTool({ name: 'index', description: '…', parameters, handler });
  • Tools are built by calling a constructor, and every constructor returns AgentTool. { kind: 'web_search' } and the other hand-written literals are no longer the documented form — AgentTool is a structural union, so an existing literal still compiles, but it is unsupported and gains none of the constructors' checks. Use webSearchTool(), examineImageTool(), detectObjectsTool(), pointAtObjectTool(), endCallTool(), screenLocateTool(capture). The SDK-shipped renderers gained the same suffix: drawBox → drawBoxTool, drawPoint → drawPointTool, screenClickElement → screenClickElementTool, screenHighlightElement → screenHighlightElementTool, screenHighlightBox → screenHighlightBoxTool. The per-tool types (ClientToolSpec, WebSearchToolSpec, and the rest) are no longer exported — annotate with AgentTool, which RealtimeTool is also renamed to.

    // before
    client.agent({ tools: [{ kind: 'web_search' }, { kind: 'examine_image' }] });
    // after
    client.agent({ tools: [webSearchTool(), examineImageTool()] });
  • The agent's output level is metered from its audio track instead of the <audio> element playing it, so a custom RealtimeTransport must implement the new optional getOutputStream and onOutputStreamChanged to report an output level. It keeps compiling and stays audible without them; only useOutputLevel goes quiet. The element tap it replaces claimed the element for the life of the page and routed its sound through the audio graph, so ending a session left the element wired to a closed graph and the next session played silently.

    A custom transport adds the two optional members:

    getOutputStream?(): MediaStream | null;              // the remote agent audio
    onOutputStreamChanged?(cb: () => void): Unsubscribe; // fires when it is replaced
  • audio.noiseCancellation takes 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.

    true and false remain valid on the wire, so a session started by an already-published SDK version is unaffected.

    true becomes 'voice_focus', false becomes 'off'. A two-person setup that was passing true and losing the quieter speaker wants 'denoise':

    // before
    client.agent({ audio: { noiseCancellation: true } });
    // after — same behaviour
    client.agent({ audio: { noiseCancellation: 'voice_focus' } });
    // after — noise goes, both voices stay
    client.agent({ audio: { noiseCancellation: 'denoise' } });
  • Passing a workspace API key (cosmo_…) as token is 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 with mintToken and pass that. Acts-as-user tokens (cosmo_pat_…) are unaffected.

    // before
    new RealtimeClient({ token: apiKey });
    // after
    new RealtimeClient({ apiKey });
  • The client-tool option types are named and exported: ClientToolOptions and RawClientToolOptions, each taking the handler as a type parameter — the handler is the only thing the immediate and background constructors differ by, so there is one type per input form rather than one per form per constructor. ToolInput stops being exported — a converter mints it, so it is not a type a caller writes.

  • The Standard Schema input form is removed: clientTool and backgroundClientTool no longer take { input: <validator>, unsafeParameters }, and StandardSchemaV1, ToolInput and ToolInputParseResult are no longer exported. It existed so a validator the SDK ships no converter for could still validate handler arguments, at the cost of publishing the interop types and a third call shape nothing used. A validator without a converter goes through { parameters } — the hand-written schema form — and is called inside the handler. The two remaining forms are the two Python and Swift take.

    The typed input form (a converter-minted schema) is unchanged; this is the removed unsafe form's migration:

    // before — the unsafe form: a Standard Schema validator beside a hand-written schema
    tool({ name: 'lookup', description: '…', input: lookupSchema, unsafeParameters: lookupJsonSchema, handler });
    // after — keep the hand-written parameters; run the validator inside the handler
    clientTool({
      name: 'lookup',
      description: '…',
      parameters: lookupJsonSchema,
      handler: async (args) => handle(lookupSchema.parse(args)),
    });
  • handler is required on the raw form of clientTool({ parameters }) and backgroundClientTool({ parameters }), matching the typed and Standard Schema forms, which always required it. A tool declared without one was advertised to the agent and then failed every time it was called.

  • model_options is gone; its provider block moves onto model, 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 the Options suffix (GeminiModelOptions → GeminiModel, and likewise for OpenAI, OpenAI-mini and Grok), each block's turn_detection accepts only the detectors its provider offers (Cosmo-VAD tuning moves to CosmoVadConfig on the block's cosmoVad field), and the union is named RealtimeModel: ModelOptions is renamed, taking either the string or the block, with RealtimeModelBlock for the block alone. A block with no model id runs the provider's default, and model: "gemini" still selects a provider by name. The server keeps accepting model_options from existing releases — the old pair folds into model server-side — so upgrading the SDK is not coupled to a backend deploy.

    Move the block to model and fold the old model string into its model_id:

    // before
    client.agent({ model: 'gemini-live', modelOptions: { provider: 'gemini', temperature: 0.7 } });
    // after — the constructor stamps the provider tag
    client.agent({ model: GeminiModel({ modelId: 'gemini-live', temperature: 0.7 }) });
  • SkillParseError is now SkillError and carries a code naming which failure it was, so a caller can tell them apart without reading the message. The codes are not_a_directory, cannot_read, missing_frontmatter, unterminated_frontmatter, malformed_frontmatter_line, duplicate_frontmatter_key, missing_description and duplicate_skill_name — a SkillErrorCode enum 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 on err.code; err.message is the sentence alone, and an unreadable skills directory now raises SkillError(cannot_read) where it used to escape as a bare PermissionError.

    Breaking: Attaching skills reads the same in every SDK. Swift takes a directory through the skills: argument itself — client.agent(skills: .directory(skillsURL)) — instead of loadSkills(fromDirectory:), which is no longer public; .directory(_:) is a factory on [Skill], so inline skills are unchanged and the two compose with +. Swift's skills is optional rather than defaulting to an empty array. parseSkillMd takes defaultName directly in TypeScript rather than wrapped in an options object, and parse_skill_md is 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 matching cosmo_sdk_load_skill keeps working.

  • RealtimeAgent exposes its resolved persona as fields — agent.instructions, agent.skills, agent.voice, agent.tools, agent.model, agent.audio, agent.greeting, agent.hooks, agent.interruptionSensitivity, agent.name, agent.inputs — instead of nesting them under agent.config, which is removed. Read agent.instructions where you read agent.config.instructions. Building a persona is unchanged: client.agent({ instructions, voice }) still takes an options object, since JavaScript has no named arguments. This matches the Python and Swift SDKs, which have always flattened the same fields onto the agent.

  • MintTokenError.code is now a closed MintTokenErrorCode naming what the SDK saw — request_failed, invalid_response, request_rejected or missing_api_key — and the server's own rejection slug moves to server_code, set only when the code is request_rejected. The two were previously the same field, so code could hold either the SDK's category or anything the server sent, down to a synthetic http_<status>, with no way to tell which. Match on err.code for what happened to the request and read err.server_code for why the server refused. Handlers comparing code against "transport_error" or against a server slug such as "auth_failed" need updating; str(err) is now the message alone, without the code: prefix.

    Breaking: A TokenSource that cannot produce a token now raises TokenSourceError rather than MintTokenError, with its own TokenSourceErrorCode — request_failed, request_rejected, invalid_response or fetcher_failed. Resolving a token source is not part of mint_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_failed is gone from MintTokenErrorCode accordingly, and the four new codes say which part failed where one bucket said only that something did. A refused redirect is request_failed in every SDK — it never reached a token endpoint, so there is no rejection to report; Python previously reported it as an http_<status> rejection.

    Breaking: TypeScript gets the same two errors. MintTokenErrorCode and TokenSourceErrorCode are literal unions rather than aliases of string, both errors take { code, message, serverCode }, and a malformed TokenSource.endpoint URL now throws a TypeError rather 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 MintTokenError enum. MintTokenError.rejected(code:detail:), .transport(message:) and .invalidResponse(message:) are gone; construct or match MintTokenError(code:message:serverCode:) with a MintTokenErrorCode instead, and expect TokenSourceError where a TokenSource fetch used to raise a mint error. mintToken now refuses a client built with a minted token or a token source before the request goes out, with code missingApiKey, rather than letting the server answer 401 — and TokenSource.custom rejects a fetcher returning an empty jwt instead of sending it as a bearer.

    // before — code could be the SDK's category or the server's slug
    if (err.code === 'auth_failed') reauthenticate();
    // after — code is the SDK's closed set; the server's slug is serverCode
    if (err.code === 'request_rejected' && err.serverCode === 'auth_failed') reauthenticate();
  • The unused ScreenShareOptions type is removed. No API ever accepted it — startScreenShare() takes no options — so the only migration is deleting the import; annotate with your own shape if you referenced it.

  • The deprecated CosmoRealtimeProvider and CosmoRealtimeProviderProps aliases are removed. Import RealtimeProvider and RealtimeProviderProps instead — the same component and props type under the names the rename already established.

  • A TokenSource.custom fetcher now resolves with the same shape mintToken returns — a MintedToken — in every SDK. Python no longer accepts a plain {jwt, expires_at} mapping (construct MintedToken, which still parses an RFC 3339 expires_at string), and TypeScript no longer accepts a string expiresAt (pass a Date; the FetchedToken type is removed — annotate with MintedToken). Python's direct TokenSource(...) construction now validates identically to custom instead of bypassing it. Swift is unchanged.

    // before
    TokenSource.custom(async () => ({ jwt, expiresAt: '2026-09-14T00:00:00Z' }));
    // after — expiresAt is a Date; annotate with MintedToken
    TokenSource.custom(async (): Promise<MintedToken> => ({ jwt, expiresAt: new Date(expiry) }));
  • AmbienceConfig and the agent's audio.ambience field 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. Delete ambience from your agent's audio block; the rest of the block is unchanged.

  • The five backend calls share an ApiError base — MintTokenError, TokenSourceError, VerifyError, UsageError and DialError all descend from it, so one catch covers any of them while catching a specific one still says which call it was. serverCode moves to the base, because a rejection slug belongs to whichever backend answered rather than to the call that asked.

    VerifyError, UsageError and DialError gain closed code enums in place of a bare string: request_failed, request_rejected, invalid_response, plus invalid_request on dial and usage for a call the SDK refuses to make. Where a server slug was the code, it is now serverCode and code is request_rejected. Swift gains DialError, which it did not have, and its UsageError and VerifyError become structs carrying code rather than case-carrying enums.

  • One CredentialsError with the closed CredentialsErrorCode replaces six spellings of the same failure — TypeScript's CredentialError (singular), Python's CredentialsError plus its NotFound, File, Expired and Mismatch subclasses, 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_slot and insecure_base_url cover the construction-time guards, which previously threw a bare Error.

  • The session's error event and useRealtimeError() now deliver the error itself instead of a summary of it. The value is the SessionStartError or AudioUnavailableError that start() rejected with, or an ErrorEvent carrying the server's own code and fatal — so a banner and a catch block see the same object, and a server code is no longer collapsed into a client bucket. ErrorCode is now the server's error enum, the same one Python and Swift publish, and the client-side union that held the name is gone.

    Code reading error.code and error.message keeps working. Code matching the old client codes changes: auth_error becomes auth_failed or workspace_forbidden, server_error becomes the server's real code, and mic_denied and the other device failures arrive as AudioUnavailableError with an AudioUnavailableErrorCode. Branch with instanceof RealtimeError and name for the typed errors; the remaining case is the server's event. ErrorEvent gains fatal, always present — false for an error the session survives.

    A mid-session transport drop no longer latches on this axis — read session.state (disconnectReason: 'transport_error') or the session_ended event for it — and a failed mic toggle reaches only the caller that awaited it. A start failure that the transport raised untyped is now wrapped in SessionStartError with code: 'join_failed', carrying the original as its cause. A non-fatal error no longer moves the transport to failed.

    // before — every failure arrived as a summary with a client bucket
    session.on('error', (e) => { if (e.code === 'auth_error') signIn(); });
    // after — the event delivers the error itself, or null on the clear
    session.on('error', (error) => {
      if (error === null) return hideBanner(); // healthy again — e.g. the next start
      if (error instanceof SessionStartError) {
        if (error.serverCode === 'auth_failed') signIn();
        else showStartFailure(error.code); // busy, entitlement, config, …
      } else if (error instanceof AudioUnavailableError) {
        askForMicrophone();
      } else {
        banner(error.code, error.fatal); // the server's ErrorEvent
      }
    });
  • Registering a hook that cannot work now throws HookError in every SDK, with the closed HookErrorCode — malformed_matcher, invalid_hook, server_hook_not_allowed. These previously threw a bare Error, so none was catchable as RealtimeError.

  • A microphone that will not open now throws AudioUnavailableError, whose code says which failure it was — mic_denied, mic_not_found, mic_in_use, or audio_unavailable — where the browser's own exception was raised before, and MicState gains 'in-use' to match. Code that switches exhaustively over MicState, or that read the browser exception off a failed start, needs the new member and the new type; Python and Swift raise the same error with the same codes.

  • ScreenCaptureRequest no longer carries wantsElements (wants_elements in Python); the request is an empty envelope, and in Swift its initializer is init(). 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_locate on the websocket transport now refuses at session start with SessionStartError (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 matching server_code: "background_tools_unsupported".

  • ErrorCode, SessionStatus, UsageStatus and CredentialKind now 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 a default or @unknown default now that they carry an unknown(String) case and are no longer @frozen; in Python both kinds are members of the enum, so use value in list(TheEnum) rather than isinstance to tell them apart. TranscriptRole is unchanged.

    In Python a server-added value is an enum member like any other, so declared versus added is a list check:

  • The session now owns the coalesced transcript, and the do-it-yourself folding surface is removed. Read session.transcript (one TranscriptItem per turn, with a stable id and an isFinal flag) or subscribe to transcript_updated, which carries the full updated list and replays the current value on subscribe; useTranscript() now reads this state and no longer caps at 12 items. Removed: reduceTranscript, RealtimeTranscriptItem, the core/transcript_fold and core/transcript_reducer modules, and RealtimeProvider's maxTranscriptLength prop — render session.transcript (or pass useTranscript({ limit })) instead. sendText now lands the sent text in the transcript as its own closed user turn (an in-progress speech turn is unaffected) unless transcript: false is passed. The raw transcript delta events are unchanged and remain available.

    // before — fold the deltas yourself
    let items: RealtimeTranscriptItem[] = [];
    session.on('transcript', (delta) => { items = reduceTranscript(items, delta, 100); });
    // after — the session holds the folded transcript
    session.on('transcript_updated', ({ items }) => render(items));
    render(session.transcript); // same value, readable any time
  • Every way a start can fail now raises one SessionStartError, whose closed SessionStartErrorCode names how far the attempt got — transport, invalid_response, join_failed, config, busy, entitlement, version_mismatch, voice_disabled, rejected, handshake_failed, ready_timeout. Switch on code where you used to branch on a type or read an HTTP status, and read the server's own rejection slug from serverCode beside it. It replaces SessionBusyError, SessionEntitlementError, SessionConfigError, VersionMismatchError and SessionStartTransportError. In Python the base error's code — previously open, carrying the server's own slug or a synthetic http_<status> — closes to the enum, with the slug moving to server_code.

    // before
    try { await agent.start(); } catch (e) {
      if (e instanceof SessionBusyError) retryIn(e.retryAfterSeconds);
      else if (e instanceof SessionEntitlementError) showUpgrade();
      else throw e;
    }
    // after
    try { await agent.start(); } catch (e) {
      if (!(e instanceof SessionStartError)) throw e;
      if (e.code === 'busy') retryIn(e.retryAfterSeconds);
      else if (e.code === 'entitlement') showUpgrade();
      else throw e;
    }
  • SessionStartError.detail is a SessionStartRejection in 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 / active for concurrent_session_limit, granted_minutes / used_minutes for free_minutes_exhausted, balance_cents / top_up_path for insufficient_credits, meter / included / used / reset_at for quota_exceeded, and provider / allowed_providers / plan / upgrade_path for provider_not_entitled. A field the server adds that the SDK does not name is kept rather than dropped. RealtimeSessionStartDetail is renamed to SessionStartRejection and gains the ten fields it never declared.

  • Calling a session method the session cannot serve now throws SessionStateError with the closed SessionStateErrorCode — not_connected, already_started, audio_publish_already_active, video_publish_already_active, screen_share_unavailable, invalid_payload. It replaces NotReadyError and AudioPublishAlreadyActiveError. A send issued before ready and one issued after the session ended both report not_connected.

  • The session state machine's value type has one name in every SDK — SessionState. TypeScript's SessionLifecycleState and Python's RealtimeSessionState are renamed; fields, kinds, and behavior are unchanged, so the migration is the rename alone. TypeScript's kind field also gains the named SessionStateKind type (the same name Python exports); the values are unchanged, so existing code is unaffected.

  • 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 the promise settles. The naive await start(); sendText(...) sequence is now correct as written, and waitUntilReady() is no longer needed on the golden path (it remains, and resolves instantly after a resolved start). A session whose ready handshake never arrives within 40 seconds is torn down and start() rejects with SessionStartError coded ready_timeout; a room that closes before ready rejects with it coded handshake_failed, carrying the server's boot-failure error frame (code and message) when one preceded the close — where previously such failures surfaced only as an error event after the fact. A pre-ready error frame on its own never rejects start(); the close that follows carries its detail. SessionStateError coded not_connected from a session method now means the session has ended or never started. Readiness is also published as room state (a participant attribute on the agent), so a client that joins after the agent came up — a mid-call observer, a reconnect — still observes ready.

  • ToolSchemaError is now ToolDefinitionError, and it covers the whole declaration — a bad tool name and a missing or overlong description throw it too, where a bare Error was thrown before. It is now catchable as RealtimeError like every other SDK error. code is the closed ToolDefinitionErrorCode rather than a string.

  • A tool-call validation failure reports its issues as ToolInputIssue in every SDK — path, code, constraint — where Python had raw dictionaries keyed loc/type/ctx, Swift nested the type inside the error, and TypeScript carried path as an array of segments. path is now the dotted form (address.city, items[2].sku) everywhere, the same string the INVALID_INPUT message renders.

    TypeScript exports ToolInputIssue from cosmo-ai/tool: it is the type ToolInputValidationError.issues carries, so a caller reading them has to be able to name it.

  • AgentTool is now opaque — a tool is built by calling its constructor, and a hand-written object literal no longer type-checks in tools. The wire discriminant and the per-tool shapes are internal, as they are in the Python and Swift SDKs. Migrate each literal to the constructor that builds it: { kind: 'web_search' } becomes webSearchTool() (likewise examineImageTool(), detectObjectsTool(), pointAtObjectTool(), endCallTool(), speakerLogTool(), and screenLocateTool(capture)), and a kind: 'client' spec becomes clientTool({ name, description, parameters, handler }) — or backgroundClientTool(...) for the background form — with a typed input or raw parameters, exactly as before.

  • A client tool carries the handler that runs it — handler is now required on the specs clientTool and backgroundClientTool build, so a hand-built kind: 'client' literal without one no longer type-checks. A handler-less spec advertised a tool that failed on every invocation; attach the handler that runs the tool. A method the server invokes over RPC without advertising it to the model is unchanged — that is session.registerRpcMethod, the register-only complement.

  • A client constructed with no credential and no getAuthHeaders now throws CredentialError (code: "no_credential") at its first authenticated call, before any request is sent, instead of sending unauthenticated requests the server rejects with 401. On such a client mintToken also throws this instead of MintTokenError (missing_api_key), which remains the error for token- and getAuthHeaders-credentialed clients. This matches what the Python and Swift constructors already do. Pass apiKey or token, set COSMO_API_KEY, sign in with cosmo login, or supply getAuthHeaders.

  • The session stream now yields the SDK's own event types instead of raw wire frames, so the type you can import is the type you receive. A consumer iterating for await (const event of session) reads camelCase fields — event.toolCallId where it was event.tool_call_id, event.isFinal where it was event.is_final, plus event.sessionId, event.secondsRemaining and event.updatedKeys — and ReadyEvent, ToolCallEvent, UsageEvent and the rest now annotate a stream item as well as an on() payload.

    event.type is the SDK's own name for the event, not the wire's: 'tool_call' rather than 'tool-call', 'model_text' rather than 'model-text', 'usage' and 'session_state' rather than 'cosmo.usage' and 'cosmo.session-state'. Where an event also has a callback, that is the name on() takes, so one vocabulary names it on either surface; the bot_* and user_*_speaking markers and tool_invocation reach the stream only. Callback payloads are unchanged.

    Nine events that only ever reached the stream are now published types: BotLlmStartedEvent, BotLlmStoppedEvent, BotStartedSpeakingEvent, BotStoppedSpeakingEvent, BotTtsStartedEvent, BotTtsStoppedEvent, UserStartedSpeakingEvent, UserStoppedSpeakingEvent and ToolInvocationEvent, alongside ToolInvocationOrigin. Optional wire fields read as settled values rather than undefined: a missing summary is null, missing counters are 0, and args, state, updatedKeys and rejectedTools are empty rather than absent.

    // before — the stream yielded raw wire frames
    for await (const event of session) {
      if (event.type === 'tool-call') handle(event.tool_call_id);
    }
    // after — the stream yields the SDK's own event types, camelCased
    for await (const event of session) {
      if (event.type === 'tool_call') handle(event.toolCallId);
    }
  • cosmo-ai/tool/screen stops exporting the ScreenLocateTool member type and the capture plumbing (SCREEN_CAPTURE_RPC_METHOD, screenCaptureRpc, screenCapturePayload, ScreenCaptureCache). The member type was the wire model behind screenLocateTool(capture) — annotate tool values with AgentTool — and the plumbing was the wiring behind it, never consumer API; the constructor and the capture-handler contract are unchanged.

  • The React bindings are no longer re-exported from the package root — import them from cosmo-ai/react, which carries the whole surface. import { RealtimeProvider, useTranscript } from 'cosmo-ai' becomes import { RealtimeProvider, useTranscript } from 'cosmo-ai/react'; nothing else moves, and every name keeps its spelling. In exchange react and react-dom become optional peer dependencies, so a headless Node app no longer installs React to use the SDK, and cosmo-ai/server is now a narrower surface by choice rather than a workaround for the root pulling React into the react-server graph.