Tools
The three tool kinds — client, background client, and server — plus schemas, validation, and the dispatch lifecycle.
Tools are how the agent acts on the world. The taxonomy is small and explicit:
| Kind | Runs where | You write | Typical use |
|---|---|---|---|
| Client tool | your app, in-process | a handler | update UI, read device state, hit your own APIs |
| Background client tool | your app, async | a handler that acks then finishes later | exports, long computations — anything slower than a beat of conversation |
| Server tool | Cosmo's backend | nothing — a typed, zero-config opt-in | web search, vision, call control |
All of these are declared in the agent's tools list and merged into one function-calling surface for the model. (A fourth kind, catalog — workspace-defined tools resolved by the server — is reserved on the wire but not executable yet; sending one is rejected at session start.)
Client tools
Declare a name, description, and JSON-Schema parameters; attach a handler. The SDK validates the model's arguments, runs your handler, and returns the result — with hooks able to deny or rewrite the call first.
Python — the @tool decorator derives the schema from a Pydantic model:
from typing import Any
from pydantic import BaseModel, Field
from cosmo_ai import CosmoRealtime, tool
class WeatherInput(BaseModel):
city: str = Field(description="City name")
@tool
async def get_weather(input: WeatherInput) -> dict[str, Any]:
"""Current weather for a city."""
return {"temp_c": 21.5}
agent = client.agent(instructions="…", tools=[get_weather])TypeScript — the tool() builder, optionally with Zod:
import { tool } from 'cosmo-ai/tool';
import { zodInput } from 'cosmo-ai/tool/zod';
import { z } from 'zod/v4';
const getWeather = tool({
name: 'get_weather',
description: 'Current weather for a city',
input: zodInput(z.object({ city: z.string().describe('City name') })),
handler: async ({ city }) => ({ tempC: 21.5 }),
});Swift — SessionConfig.Tool.define with a typed Decodable argument struct:
struct WeatherArgs: Decodable, Sendable { let city: String }
let getWeather = try SessionConfig.Tool.define(
name: "get_weather",
description: "Current weather for a city",
input: .object(properties: ["city": .string(description: "City name")], required: ["city"])
) { (args: WeatherArgs) in
["temp_c": .double(21.5)]
}Schemas and validation
Tool schemas use a restricted JSON-Schema dialect (no pattern/format, no oneOf, no recursive models; 8 KiB serialized cap). Violations throw ToolSchemaError at construction time — at import in Python, at tool() in TypeScript, at define in Swift — never mid-call.
When the model sends malformed arguments, the SDK rejects them before your handler runs and returns a sanitized INVALID_INPUT error to the model (paths and constraints, never the submitted values). Your handler only ever sees validated, typed input.
Background client tools
A regular client tool blocks the conversation until it returns. If the work takes longer than a beat, make it a background tool: the handler acks immediately (the agent can say "working on it…" and keep talking) and delivers the result later through a job handle.
@tool(background=True)
async def export_report(input: ExportInput, job: ClientToolJob) -> None:
"""Export the quarterly report."""
await job.ack(note="Starting the export")
url = await run_export(input) # takes a minute
await job.complete(result={"url": url})TypeScript uses tool({ background: true, handler: async (args, job) => … }); Swift uses SessionConfig.Tool.defineBackground. On failure, call job.fail(error=…) — the outcome is injected into the conversation whenever it lands.
Server tools
Server tools execute on Cosmo's backend; each built-in is its own typed, zero-config spec — the server owns the model-facing declaration, and you write no handler:
from cosmo_ai import WebSearchTool
agent = client.agent(instructions="…", tools=[WebSearchTool()])const agent = client.agent({ tools: [{ kind: 'web_search' }] });var config = SessionConfig()
config.tools = [.webSearch]| Kind | What it does |
|---|---|
web_search | Live web search |
examine_image | Examine the freshest published video frame at full resolution (image input) |
detect_objects | Locate a named object in the frame, returning a box per matching instance |
point_at_object | Locate a named object in the frame, returning points |
A few first-party tools have no typed kinds yet — cosmo.end_call (telephony) and cosmo.view_state / cosmo.set_state (session state). In TypeScript they remain reachable through the deprecated generic kind: "server" name reference until they graduate to typed kinds or retire. The Python SDK no longer ships the generic reference, so on Python these tools are unavailable until their typed kinds land.
An opt-in the deployment cannot run does not fail the session — it is dropped, and the model never calls it. The signal is rejected_tools on the ready event, with a reason per entry; check it first when a tool "does nothing".
Watching dispatch
Every tool invocation — client or server — emits an observability sequence sharing one tool_call_id:
tool-call— the model decided to call the tooltool-dispatch-started— the handler began executingtool-result— finished:okplus a shortsummary
Client-tool executions additionally arrive as tool-invocation (the actual dispatch to your handler). Render a live tool timeline from these events — see Handle tool calls.
Guarding execution
Pre-tool-use hooks run before every client-tool handler and can deny the call or rewrite its arguments — the right place for confirmation gates, argument clamps, and "never touch production" rules that must hold no matter what the model decides.