Cosmo Realtime SDK
Capabilities

Hooks

Five lifecycle seams — inject context, deny or rewrite tool calls, observe outcomes — with identical semantics in every SDK.

Hooks are deterministic callbacks at fixed seams of the session lifecycle. Where instructions ask the model to behave, hooks guarantee behavior in code: a delete_* tool stays blocked no matter what the model decides.

There are four seams. Semantics are identical across TypeScript, Python, and Swift — pinned by shared conformance vectors that all three SDKs run in CI, so a hook you design in one language behaves the same in the others.

SeamFiresCan
SessionStartonce, before the session opensinject extra context into the instructions
PreToolUsebefore each client-tool handlerdeny the call or rewrite its arguments
PostToolUseafter each client-tool outcomeobserve (log, meter, alert)
SessionEndexactly once at teardown, any exit pathobserve

Registering hooks

Hooks ride on the agent config as an ordered list — list order is fold order.

Python — decorators from cosmo_ai.hooks:

from cosmo_ai import CosmoRealtime
from cosmo_ai.hooks import (
    PreToolUseResult,
    SessionStartResult,
    post_tool_use,
    pre_tool_use,
    session_start,
)

@session_start
def add_context(ctx) -> SessionStartResult:
    return SessionStartResult(additional_context="The user is on the premium plan.")

@pre_tool_use(matcher="delete_*")
def block_deletes(ctx) -> PreToolUseResult:
    return PreToolUseResult(permission="deny", reason="destructive tools are disabled")

@post_tool_use
def log_outcome(ctx) -> None:
    print(ctx.tool_name, type(ctx.outcome).__name__)

agent = client.agent(instructions="…", hooks=[add_context, block_deletes, log_outcome])

TypeScript — seam factories:

import { sessionStart, preToolUse, postToolUse, sessionEnd } from 'cosmo-ai';

const agent = client.agent({
  instructions: '…',
  hooks: [
    sessionStart(() => ({ additionalContext: 'The user is on the premium plan.' })),
    preToolUse(() => ({ permission: 'deny', reason: 'destructive tools are disabled' }),
               { matcher: 'delete_*' }),
    postToolUse((ctx) => console.log(ctx.toolName, ctx.outcome)),
  ],
});

Swift — factory functions on SessionConfig.hooks:

var config = SessionConfig()
config.hooks = [
    sessionStart { _ in
        SessionStartResult(additionalContext: "The user is on the premium plan.")
    },
    try preToolUse(matcher: "delete_*") { _ in
        PreToolUseResult(permission: .deny, reason: "destructive tools are disabled")
    },
    try postToolUse { ctx in print(ctx.toolName, ctx.outcome) },
]

Matchers

PreToolUse and PostToolUse accept an optional glob matcher on the tool name: * (any run), ? (one char), [seq], [!seq]. Matching is case-sensitive, against the full tool name, with no implicit substring — delete does not match delete_file; delete* does. No matcher means every tool. Malformed patterns (an unterminated [) are rejected at registration, not silently ignored.

Fold rules

When several hooks share a seam, they fold in list order:

  • SessionStart — every hook runs; the additional_context strings are concatenated (blank-line separated) and appended to the instructions. All-none injects nothing.
  • PreToolUse — hooks run in order. The first deny wins and stops the fold (an empty reason becomes "denied by hook"). Argument rewrites chain — each hook sees the previous hook's rewritten arguments.
  • PostToolUse / SessionEnd — all observers run in order.

A hook that throws is logged and skipped; later hooks still run and the session survives. The exception is a throwing SessionStart in TypeScript, which aborts the not-yet-opened session with handshake_failed.

The PostToolUse context carries the final outcome as a typed value: ToolOk(result), ToolError(message), or ToolDenied(reason) — so your audit log records denials the same way it records failures.

Server hooks

Client hooks live in your process; if the process dies mid-call, they die with it. Server hooks are declarative config the backend executes on your behalf — today, the silence timeout:

from cosmo_ai.hooks import SilenceTimeout, Say, EndCall

agent = client.agent(
    instructions="…",
    hooks=[
        SilenceTimeout(timeout_seconds=30,
                       action=Say(prompt="Gently check whether the caller is still there."),
                       max_count=2, reset_mode="on_user_speech"),
        SilenceTimeout(timeout_seconds=90,
                       action=EndCall(farewell="I'll let you go — call back anytime.")),
    ],
)

Say speaks exact text or generates from a prompt; EndCall says an optional farewell and hangs up. max_count (1–10) caps how many times the hook fires; reset_mode="on_user_speech" restarts the count whenever the user talks. Up to 16 server hooks per session.

Each firing reaches the client as an ordinary user-speech-timeout session event on the event stream (RealtimeUserSpeechTimeout in Python, user_speech_timeout in TypeScript, .userSpeechTimeout in Swift) — a fired server hook surfaces as an event, never as a client hook. In TypeScript and Swift, server hooks share the same hooks list as client hooks (SilenceTimeout objects in TS, Hook.server(...) in Swift); the SDK serializes only the server hooks onto the wire — client callbacks never leave your process.

What hooks are not

Hooks guard client tools. Server tools execute on the backend and are governed by which names you opt into, not by PreToolUse. And hooks are not a place for conversation logic — steering what the agent says belongs in instructions and skills; hooks are for guarantees.

On this page