Tool calls

A tool is a function the model can call. You declare it in the agent config. When the model calls one, the engine sends your worker a tool.execute trigger; the worker runs it and answers with a tool.result or tool.error. The engine settles the call, fires a tool.finished trigger, and re-prompts the model with the outcome.

Example

A tool with an input schema. The engine validates the model's arguments; the worker returns a tool.error when the tool itself fails.

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-20251001",
                stream: true,
                tools: tools.map(({ name, description, input }) =>
                    ({ name, description, input }))
            }
        };
    }

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

    return proposed;
}

Declaration

Tools live in the agent config. The model sees name, description, and input; the rest is for the engine.

type AgentTool = {
    name: string
    description?: string
    input?: unknown             // JSON Schema for arguments; omitted: none
    output?: unknown            // JSON Schema the result must satisfy
    handler?: "worker" | "client"  // where it runs; default worker
}

input and output are optional JSON Schemas; the engine enforces them (see Schemas).

Schemas

The engine enforces a tool's declared schemas in both directions. It only validates and never mutates: a passing value is exactly what came in, with no coercion or default-filling. Tools with no schema pass through unchecked.

input

Before the tool.execute trigger reaches your worker, the engine classifies the raw arguments against input and reports the outcome in trigger.input:

statusMeaning
validParsed to an object and conforms. value holds the arguments.
invalidParsed to an object but violates the schema.
malformedNot a JSON object at all.

Validation never blocks the call. Every case reaches your worker, so you decide whether to run, coerce, or reject it. When the arguments are invalid or malformed, the engine sets proposed to a ready-made tool.error you can return as-is.

output

When a call settles with a result, the engine checks it against output. A result that violates the schema never reaches the model. It settles as a terminal tool.error instead. The result is read as JSON when it parses, else as a plain string.

Triggers

Two triggers concern a tool call. Your worker answers tool.execute; it usually accepts the proposal on tool.finished.

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

type ToolInput =
    | { status: "valid"; value: unknown }
    | { status: "invalid"; value: unknown; error: string }
    | { status: "malformed"; error: string }

type ToolFinished = {
    type: "tool.finished"
    id: string
    ok: boolean
    name: string
    result?: string
    error?: string
}

tool.execute

The model called a tool; run it. input carries the engine's validation of the raw arguments (see Schemas). proposed is empty on a clean call, because only your worker can run the tool, so you answer with a tool.result or tool.error. When validation fails or the model named a tool you never declared, proposed is instead a ready-made tool.error to return as-is.

tool.finished

A tool call settled, after its result or error and any retries. proposed records the outcome as a tool message, then re-prompts the model, or holds when sibling calls are still in flight. Return it to continue the loop.

Actions

type ToolCall = {
    type: "tool.call"
    id?: string                 // omitted: the engine mints one
    name: string
    arguments: unknown
    handler?: "worker" | "client"  // default worker
    retry?: RetryPolicy         // default: no retry
}

type ToolResult = {
    type: "tool.result"
    id?: string                 // id and attempt default to the answered
    attempt?: number            // tool.execute trigger
    result: unknown
}

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

tool.call

Dispatch a tool call. The engine proposes these for the model's calls, and your worker can issue one directly. handler routes it to your worker or the client; retry bounds the attempts.

tool.result

Settle a call with a result. A result checked against a declared output schema may settle as a terminal error instead (see Schemas).

tool.error

Settle a call with a failure. It's terminal by default; set retryable: true to retry under the call's retry policy. code and detail carry structured info. The error text reaches the model as the tool's result, so write it to be read.

Full types in Protocol.

Next