# Callable Methods Callable methods let clients invoke agent methods over WebSocket using RPC (Remote Procedure Call). Mark methods with `@callable()` to expose them to external clients like browsers, mobile apps, or other services. ## Overview ```typescript import { Agent, callable } from "agents"; export class MyAgent extends Agent { @callable() async greet(name: string): Promise { return `Hello, ${name}!`; } } ``` ```typescript // Client const result = await agent.stub.greet("World"); console.log(result); // "Hello, World!" ``` ### How It Works ``` ┌─────────┐ ┌─────────┐ │ Client │ │ Agent │ └────┬────┘ └────┬────┘ │ │ │ agent.stub.greet("World") │ │ ──────────────────────────────────▶ │ │ WebSocket RPC message │ │ │ │ Check @callable │ Execute method │ │ │ ◀──────────────────────────────── │ │ "Hello, World!" │ │ │ ``` ### When to Use @callable | Scenario | Use | | ------------------------------------ | ----------------------------- | | Browser/mobile calling agent | `@callable()` | | External service calling agent | `@callable()` | | Worker calling agent (same codebase) | DO RPC (no decorator needed) | | Agent calling another agent | DO RPC via `getAgentByName()` | The `@callable()` decorator is specifically for WebSocket-based RPC from external clients. When calling from within the same Worker or another agent, use standard [Durable Object RPC](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/) directly. ## TypeScript Configuration The `@callable()` decorator requires TypeScript's decorator support. Set `"target"` to `"ES2021"` or later in your `tsconfig.json`: ```json { "compilerOptions": { "target": "ES2021" } } ``` Without this, your dev server will fail with `SyntaxError: Invalid or unexpected token`. Setting the target to `ES2021` ensures that Vite's esbuild transpiler downlevels TC39 decorators instead of passing them through as native syntax. > **Warning:** Do not set `"experimentalDecorators": true` in your `tsconfig.json`. The Agents SDK uses [TC39 standard decorators](https://github.com/tc39/proposal-decorators), not TypeScript legacy decorators. Enabling `experimentalDecorators` applies an incompatible transform that silently breaks `@callable()` at runtime. ## Basic Usage ### Defining Callable Methods Add the `@callable()` decorator to any method you want to expose: ```typescript import { Agent, callable } from "agents"; type State = { count: number; items: string[]; }; export class CounterAgent extends Agent { initialState: State = { count: 0, items: [] }; @callable() increment(): number { this.setState({ ...this.state, count: this.state.count + 1 }); return this.state.count; } @callable() decrement(): number { this.setState({ ...this.state, count: this.state.count - 1 }); return this.state.count; } @callable() async addItem(item: string): Promise { this.setState({ ...this.state, items: [...this.state.items, item] }); return this.state.items; } @callable() getStats(): { count: number; itemCount: number } { return { count: this.state.count, itemCount: this.state.items.length }; } } ``` ### Calling from the Client There are two ways to call methods from the client: **Using `agent.stub` (recommended):** ```typescript // Clean, typed syntax const count = await agent.stub.increment(); const items = await agent.stub.addItem("new item"); const stats = await agent.stub.getStats(); ``` **Using `agent.call()`:** ```typescript // Explicit method name as string const count = await agent.call("increment"); const items = await agent.call("addItem", ["new item"]); const stats = await agent.call("getStats"); ``` The `stub` proxy provides better ergonomics and TypeScript support. ## Method Signatures ### Serializable Types Arguments and return values must be JSON-serializable: ```typescript // ✅ Valid - primitives and plain objects @callable() processData(input: { name: string; count: number }): { result: boolean } { return { result: true }; } // ✅ Valid - arrays @callable() processItems(items: string[]): number[] { return items.map(item => item.length); } // ❌ Invalid - non-serializable types @callable() badMethod(fn: Function, date: Date): Map { // Functions, Dates, Maps, Sets, etc. cannot be serialized } ``` ### Async Methods Both sync and async methods work: ```typescript // Sync method @callable() add(a: number, b: number): number { return a + b; } // Async method @callable() async fetchUser(id: string): Promise { const user = await this.sql`SELECT * FROM users WHERE id = ${id}`; return user[0]; } ``` ### Void Methods Methods that don't return a value: ```typescript @callable() async logEvent(event: string): Promise { await this.sql`INSERT INTO events (name) VALUES (${event})`; } ``` On the client, these still return a Promise that resolves when the method completes: ```typescript await agent.stub.logEvent("user-clicked"); // Resolves when the server confirms execution ``` ## Streaming Responses For methods that produce data over time (like AI text generation), use streaming: ### Defining a Streaming Method ```typescript import { Agent, callable, type StreamingResponse } from "agents"; export class AIAgent extends Agent { @callable({ streaming: true }) async generateText(stream: StreamingResponse, prompt: string) { // First parameter is always StreamingResponse for streaming methods for await (const chunk of this.llm.stream(prompt)) { stream.send(chunk); // Send each chunk to the client } stream.end(); // Signal completion } @callable({ streaming: true }) async streamNumbers(stream: StreamingResponse, count: number) { for (let i = 0; i < count; i++) { stream.send(i); await new Promise((resolve) => setTimeout(resolve, 100)); } stream.end(count); // Optional final value } } ``` ### Consuming Streams on the Client ```typescript // Preferred format (supports timeout and other options) await agent.call("generateText", [prompt], { stream: { onChunk: (chunk) => { // Called for each chunk appendToOutput(chunk); }, onDone: (finalValue) => { // Called when stream ends console.log("Stream complete", finalValue); }, onError: (error) => { // Called if an error occurs console.error("Stream error:", error); } } }); // Legacy format (still supported for backward compatibility) await agent.call("generateText", [prompt], { onChunk: (chunk) => appendToOutput(chunk), onDone: (finalValue) => console.log("Done", finalValue), onError: (error) => console.error("Error:", error) }); ``` ### StreamingResponse API | Method | Description | | ------------------ | ------------------------------------------------ | | `send(chunk)` | Send a chunk to the client | | `end(finalChunk?)` | End the stream, optionally with a final value | | `error(message)` | Send an error to the client and close the stream | ```typescript @callable({ streaming: true }) async processWithProgress(stream: StreamingResponse, items: string[]) { for (let i = 0; i < items.length; i++) { await this.process(items[i]); stream.send({ progress: (i + 1) / items.length, item: items[i] }); } stream.end({ completed: true, total: items.length }); } ``` ## TypeScript Integration ### Typed Client Calls Pass your agent class as a type parameter for full type safety: ```typescript import { useAgent } from "agents/react"; import type { MyAgent } from "./server"; function App() { const agent = useAgent({ agent: "MyAgent", name: "default" }); // ✅ TypeScript knows the method signature const result = await agent.stub.greet("World"); // ^? string // ✅ TypeScript catches errors await agent.stub.greet(123); // Error: Argument of type 'number' is not assignable await agent.stub.nonExistent(); // Error: Property 'nonExistent' does not exist } ``` ### Excluding Non-Callable Methods If you have methods that aren't decorated with `@callable()`, you can exclude them from the type: ```typescript class MyAgent extends Agent { @callable() publicMethod(): string { return "public"; } // Not callable from clients internalMethod(): void { // internal logic } } // Exclude internal methods from the client type const agent = useAgent, {}>({ agent: "MyAgent" }); agent.stub.publicMethod(); // ✅ Works agent.stub.internalMethod(); // ✅ TypeScript error ``` ### Type Inference for State When methods return `this.state`, TypeScript correctly infers the type: ```typescript type MyState = { count: number; name: string }; class MyAgent extends Agent { @callable() async getState(): Promise { return this.state; } } // Client const state = await agent.stub.getState(); // ^? MyState ``` ## Error Handling ### Throwing Errors in Callable Methods Errors thrown in callable methods are propagated to the client: ```typescript @callable() async riskyOperation(data: unknown): Promise { if (!isValid(data)) { throw new Error("Invalid data format"); } try { await this.processData(data); } catch (e) { throw new Error("Processing failed: " + e.message); } } ``` ### Client-Side Error Handling ```typescript try { const result = await agent.stub.riskyOperation(data); } catch (error) { // Error thrown by the agent method console.error("RPC failed:", error.message); } ``` ### Streaming Error Handling For streaming methods, use the `onError` callback: ```typescript await agent.call("streamData", [input], { stream: { onChunk: (chunk) => handleChunk(chunk), onError: (errorMessage) => { console.error("Stream error:", errorMessage); showErrorUI(errorMessage); }, onDone: (result) => handleComplete(result) } }); ``` Server-side, you can use `stream.error()` to gracefully send an error mid-stream: ```typescript @callable({ streaming: true }) async processItems(stream: StreamingResponse, items: string[]) { for (const item of items) { try { const result = await this.process(item); stream.send(result); } catch (e) { stream.error(`Failed to process ${item}: ${e.message}`); return; // Stream is now closed } } stream.end(); } ``` ### Connection Errors If the WebSocket connection closes while RPC calls are pending, they automatically reject with a "Connection closed" error: ```typescript try { const result = await agent.call("longRunningMethod", []); } catch (error) { if (error.message === "Connection closed") { // Handle disconnection console.log("Lost connection to agent"); } } ``` #### Retrying After Reconnection PartySocket automatically reconnects after disconnection. To retry a failed call after reconnection, await `agent.ready` before retrying: ```typescript async function callWithRetry( agent: AgentClient, method: string, args: unknown[] = [] ): Promise { try { return await agent.call(method, args); } catch (error) { if (error.message === "Connection closed") { await agent.ready; // Wait for reconnection return await agent.call(method, args); // Retry once } throw error; } } // Usage const result = await callWithRetry(agent, "processData", [data]); ``` > **Note:** Only retry idempotent operations. If the server received the request but the connection dropped before the response arrived, retrying could cause duplicate execution. ## When NOT to Use @callable ### Worker-to-Agent Calls When calling an agent from the same Worker (e.g., in your `fetch` handler), use Durable Object RPC directly: ```typescript import { getAgentByName } from "agents"; export default { async fetch(request: Request, env: Env) { // Get the agent stub const agent = await getAgentByName(env.MyAgent, "instance-name"); // Call methods directly - no @callable needed const result = await agent.processData(data); return Response.json(result); } }; ``` ### Agent-to-Agent Calls When one agent needs to call another: ```typescript class OrchestratorAgent extends Agent { async delegateWork(taskId: string) { // Get another agent const worker = await getAgentByName(this.env.WorkerAgent, taskId); // Call its methods directly const result = await worker.doWork(); return result; } } ``` ### Why the Distinction? | RPC Type | Transport | Use Case | | ----------- | --------- | --------------------------------- | | `@callable` | WebSocket | External clients (browsers, apps) | | DO RPC | Internal | Worker ↔ Agent, Agent ↔ Agent | DO RPC is more efficient for internal calls since it doesn't go through WebSocket serialization. The `@callable` decorator adds the necessary WebSocket RPC handling for external clients. ## API Reference ### `@callable(metadata?)` Decorator Marks a method as callable from external clients. ```typescript import { callable } from "agents"; @callable() method(): void {} @callable({ streaming: true }) streamingMethod(stream: StreamingResponse): void {} @callable({ description: "Fetches user data" }) getUser(id: string): User {} ``` ### `CallableMetadata` Type ```typescript type CallableMetadata = { /** Optional description of what the method does */ description?: string; /** Whether the method supports streaming responses */ streaming?: boolean; }; ``` ### `StreamingResponse` Class Used in streaming callable methods to send data to the client. ```typescript import { type StreamingResponse } from "agents"; @callable({ streaming: true }) async streamData(stream: StreamingResponse, input: string) { stream.send("chunk 1"); stream.send("chunk 2"); stream.end("final"); } ``` | Method | Signature | Description | | ------- | -------------------------------- | ---------------------------------- | | `send` | `(chunk: unknown) => void` | Send a chunk to the client | | `end` | `(finalChunk?: unknown) => void` | End the stream | | `error` | `(message: string) => void` | Send an error and close the stream | ### Client Methods | Method | Signature | Description | | ------------ | -------------------------------------- | --------------------- | | `agent.call` | `(method, args?, options?) => Promise` | Call a method by name | | `agent.stub` | `Proxy` | Typed method calls | ```typescript // Using call() await agent.call("methodName", [arg1, arg2]); await agent.call("streamMethod", [arg], { stream: { onChunk, onDone, onError } }); // With timeout (rejects if call doesn't complete in time) await agent.call("slowMethod", [], { timeout: 5000 }); // Using stub await agent.stub.methodName(arg1, arg2); ``` ### `CallOptions` Type ```typescript type CallOptions = { /** Timeout in milliseconds. Rejects if call doesn't complete in time. */ timeout?: number; /** Streaming options */ stream?: { onChunk?: (chunk: unknown) => void; onDone?: (finalChunk: unknown) => void; onError?: (error: string) => void; }; }; ``` > **Backward Compatibility**: The legacy format `{ onChunk, onDone, onError }` (without nesting under `stream`) is still supported. The client auto-detects which format you're using. ### `getCallableMethods()` Method Returns a map of all callable methods on the agent with their metadata. Useful for introspection and auto-documentation. ```typescript const methods = agent.getCallableMethods(); // Map for (const [name, meta] of methods) { console.log(`${name}: ${meta.description || "(no description)"}`); if (meta.streaming) console.log(" (streaming)"); } ```