Tool calls

Updated Aug 28, 2026View as Markdown

A tool is a function the model can call. Your worker declares it and runs it.

When the model calls a tool, the engine sends your worker a tool.execute trigger. The worker answers with a tool.result or a tool.error. The engine records the result and prompts the model again.

Example

A tool with an input schema. The engine validates the model's arguments.

server.mjs
const forecasts = { "San Francisco": "foggy", Tokyo: "clear" };

const tools = [
    {
        name: "get_weather",
        description: "Get the weather for a city.",
        input: {
            type: "object",
            properties: { city: { type: "string" } },
            required: ["city"]
        },
        exec: ({ city }) => {
            const sky = forecasts[city];
            if (!sky) throw new Error(`No forecast for ${city}.`);
            return `It's ${sky} in ${city}.`;
        }
    }
];

function decide({ trigger, proposed }) {
    if (trigger.type === "session.start") {
        return {
            agent: {
                model: "claude-haiku-4-5",
                tools: tools.map(({ name, description, input }) =>
                    ({ name, description, input }))
            }
        };
    }

    // The model called a tool. Run it, or say why it failed.
    if (trigger.type === "tool.execute") {
        const tool = tools.find((t) => t.name === trigger.name);
        try {
            const text = tool.exec(trigger.input.value);
            return {
                actions: [
                    { type: "tool.result", result: { content: [{ type: "text", text }] } },
                ],
            };
        } catch (err) {
            return { actions: [{ type: "tool.error", error: err.message }] };
        }
    }

    return proposed;
}

Declare a tool

Tools go in the agent config. The model sees name, description, and input.

type AgentTool = {
    name: string
    description?: string
    input?: unknown             // JSON Schema for the arguments
    output?: unknown            // JSON Schema the result must match
    handler?: "worker" | "client"  // where it runs. default worker
    defer?: boolean             // keep it out of the request. default: the agent's defer_tools
}

Keep a large tool set out of the request

A model chooses worse as the tool list grows, and worst between tools that look alike. Each definition also sits at the front of the request, where the provider keeps its cache.

Set defer on the tools an agent seldom needs. The request leaves them out, and the agent gets tool_search and call_tool in their place. The model searches for a tool and names it to call_tool. Your worker receives an ordinary tool.execute, under the tool's own name.

See Deferred tools.

Schemas

The engine checks a tool's schemas in both directions. It only validates. It converts no types and adds no defaults, so the value your worker receives is the value that came in.

The engine does not check a tool with no schema.

Input validation

Before tool.execute reaches your worker, the engine checks the raw arguments against input. It reports the result in trigger.input.

statusMeaning
validThe arguments are an object and match the schema. value holds them.
invalidThe arguments are an object. They do not match the schema.
malformedThe arguments are not a JSON object.

Validation never stops a call. All three reach your worker, so you decide whether to run the tool, correct the arguments, or refuse.

For invalid and malformed, the engine puts a tool.error in proposed. You can return it unchanged.

Output validation

When a call ends with a result, the engine checks the result against output. A result that does not match never reaches the model. The call ends with a tool.error that cannot be retried.

The engine reads the result as JSON if it parses, and as a string if it does not.

Tool triggers

Your worker answers tool.execute. It usually accepts the proposal for tool.finished.

tool.execute

The model called a tool. Run it.

For a valid call, proposed is empty. Answer with a tool.result or a tool.error. If validation failed, or the model named a tool you did not declare, proposed holds a tool.error you can return unchanged.

{
    type: "tool.execute"
    id: string
    name: string
    arguments: string           // the raw argument string
    input: ToolInput            // the engine's validation
    attempt: number
    deadline?: string
}

tool.finished

A tool call ended, after its result and after any retries.

proposed records the result as a tool message and prompts the model again. If other calls are still in flight, proposed waits. Return it to continue.

{
    type: "tool.finished"
    id: string
    ok: boolean
    name: string
    result?: StoredResult       // bytes are stored; blocks name them
    error?: ErrorInfo
}

Tool actions

tool.call

Start a tool call. The engine proposes one for each call the model makes. Your worker can also send one.

The tool's name decides where the call runs. A tool declared with handler: "client" runs on the client. Everything else runs on your worker.

tool.result

End a call with a result.

{
    type: "tool.result"
    id?: string                 // id and attempt default to those of the
    attempt?: number            // tool.execute you answer
    result: ToolResult
}

A result is blocks of content, in the shape that MCP defines. Text is the common case:

{ "type": "tool.result", "result": { "content": [{ "type": "text", "text": "sunny" }] } }

You can also put the blocks straight on the action as content, instead of inside result. Naming both is an error.

{ "type": "tool.result", "content": [{ "type": "text", "text": "sunny" }] }

A result that is not a ToolResult becomes one text block. A string becomes its own text, and any other JSON value is stringified.

A tool that answers with an image or a file sends the bytes inline. The engine stores them and records a reference, so nothing large enters the log.

type ToolResult = {
    content: ToolContent[]
    structuredContent?: unknown   // preferred where the tool declares an output
    isError?: boolean             // the tool ran and reported failure
}

// The blocks MCP defines. Bytes ride inline; the engine stores them and
// records a `blob` in their place.
type ToolContent =
    | { type: "text"; text: string }
    | { type: "image"; data: string; mimeType: string }        // data is base64
    | { type: "audio"; data: string; mimeType: string }        // data is base64
    | { type: "resource"; resource: ResourceContents }
    | { type: "resource_link"; uri: string; name?: string; mimeType?: string }

type ResourceContents = {
    uri: string
    mimeType?: string
    text?: string
    blob?: string                 // base64
}

// What the engine recorded. Bytes are never here: `blob` names them.
type StoredContent =
    | { type: "text"; text: string }
    | { type: "blob"; uri: string }
    | { type: "link"; uri: string; name?: string; mimeType?: string }

type StoredResult = {
    content: StoredContent[]
    structuredContent?: unknown
    isError?: boolean
}

tool.error

End a call with a failure.

{
    type: "tool.error"
    id?: string
    attempt?: number
    error: string
    retryable?: boolean         // default false
    code?: ErrorCode
    detail?: unknown
}

The engine does not retry by default. Set retryable: true to retry under the call's policy. See Retries.

The model reads error as the tool's result. Write it for the model to read.

Where tools run

SourceRuns onDeclared in
Your codeYour workerThe config the worker returns
A connectorThe enginemcp on the agent
The browserThe clienttools, with handler = "client"

Next steps