Skip to content

Tools

Tools are explicit host functions that the model can request.

tool is exported by the SDK. It is not a global helper, so import it next to createAgent before defining custom tools.

import { createAgent, tool } from "agent-lattice";
import { z } from "zod/v4";
const calculator = tool(
"calculator",
"Evaluate a simple arithmetic expression",
z.object({
expr: z.string(),
}),
async input => {
return { content: String(Function(`return ${input.expr}`)()) };
},
);
const agent = createAgent({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.deepseek.com/anthropic",
model: "deepseek-v4-flash",
tools: [calculator],
});

The SDK converts Zod schemas to JSON Schema for the model and parses tool input before calling your handler.

type ToolResult = {
content: string | ContentBlock[];
/** End the run after this tool batch: finish with subtype "success" instead of calling the model again. */
endTurn?: boolean;
/** Structured payload carried to `SDKResultMessage.structuredResult` when this tool also sets `endTurn: true`. Ignored otherwise. */
structuredResult?: unknown;
};

If a tool handler throws, the SDK returns an error tool_result to the model instead of crashing the entire loop.

Requires 0.16.0 or later.

A tool that has the final answer can end the run itself by returning endTurn: true. The SDK finishes with subtype: "success" and uses that tool’s text content as the result, without calling the model again:

const finish = tool(
"finish",
"Submit the final answer and end the run",
z.object({ answer: z.string() }),
async ({ answer }) => ({ content: answer, endTurn: true }),
);

endTurn does not cancel the other tools of the same batch — they already started concurrently, their tool_result blocks still enter the history, and onToolResult hooks and trace events run for them as usual. Only the next model call is skipped. When several tools in a batch set endTurn, the first one’s content becomes the result text.

A tool that ends the run can also return structuredResult next to endTurn: true; the payload lands on SDKResultMessage.structuredResult. Without endTurn, structuredResult is ignored. Requires 0.20.0 or later.

Requires 0.9.25 or later.

The model asks to run tools together by returning multiple tool_use blocks in one assistant message. The SDK still decides which handlers may overlap. Mark a tool as concurrency-safe when its parsed input does not cause conflicting side effects:

const search = tool(
"search",
"Search documents",
z.object({ query: z.string() }),
async ({ query }) => {
// App code: replace with your database or search client.
return { content: await documentIndex.search(query) };
},
{ isConcurrencySafe: () => true },
);
const agent = createAgent({
model: "claude-sonnet-4-6",
tools: [search],
toolConcurrency: {
mode: "safe",
maxConcurrency: 8,
},
});

mode: "safe" is the default. It concurrently executes consecutive calls whose tools return true from isConcurrencySafe(input). A missing declaration, a false result, invalid input, or an exception from the safety check makes that call sequential. The built-in Read, LS, Glob, and Grep tools are marked safe; Write, Edit, and Bash are not.

When concurrency is available, the SDK tells the model to place independent calls in one assistant response and to use separate responses when a later call needs an earlier result. The model cannot override isConcurrencySafe, maxConcurrency, or toolBatchPolicy.

Use mode: "all" only when every tool registered on that Agent is safe to overlap. Use mode: "sequential" to force every call to run one at a time. maxConcurrency defaults to 10 and must be a positive integer.

The SDK waits for the whole batch before requesting the model again. Handlers may finish in any order, but tool_result blocks are sent back in the original tool_use order. One handler failure does not remove successful results from the same batch. On abort, running handlers receive the query’s AbortSignal, calls still waiting for a concurrency slot do not start, and the SDK waits for handlers that already started to settle.

Requires 0.9.25 or later.

Use AgentOptions.toolBatchPolicy when specific tools must run in separate model responses. The policy receives every tool call from the current response before any handler runs.

const agent = createAgent({
model: "claude-sonnet-4-6",
tools: [incrementRevision],
toolBatchPolicy: {
validate({ toolCalls }) {
const update = toolCalls.find(call => call.name === "incrementRevision");
const handoff = toolCalls.find(
call => call.kind === "agent_tool" &&
(call.input as { mode?: string }).mode === "handoff",
);
if (update && handoff) {
return {
allowed: false,
code: "invalid_tool_batch",
message: "Update the revision before delegating dependent work.",
conflictingToolCallIds: [update.id, handoff.id],
suggestedNextStep: "Run the update first, then hand off the new revision.",
};
}
return { allowed: true };
},
},
});

When rejected, no handler runs and no handoff enters the mailbox. The SDK returns an is_error: true result for every tool call in the batch. If the policy throws, the SDK uses tool_batch_policy_error and still executes no tools. When configured, the policy always checks the complete batch before concurrency scheduling begins.

This check prevents known conflicts inside one model response. Keep database transactions and revision checks for changes made by other requests or processes.

Requires 0.22.0 or later.

ToolOptions.metadata accepts a Record<string, unknown> that is passed through to ToolDefinition.metadata. The SDK never reads or interprets it, and it is never shown to the model — it is host-owned, machine-readable annotation (for example a contract version). When not passed, the key is absent from the ToolDefinition. AgentToolOptions.metadata works the same way for agentTool() tools.

Requires 0.22.0 or later.

The option objects of createAgent()/createBareAgent()/defineAgent() (AgentOptions), agentTool() (AgentToolOptions), delegateTool() (DelegateToolOptions), and tool() (ToolOptions) are validated strictly: an unknown key throws at assembly time with AgentOptions: unknown option "bogusOption". Check for a typo, or upgrade the SDK if this option was added in a newer version. (agentTool()/delegateTool() prefix the message with agentTool("<name>"): / delegateTool("<name>"):). This fails fast on the old-SDK + new-API combination, which previously succeeded with the feature silently absent. Behavior change: extra keys that used to be silently ignored — such as host fields spread into an options object — now throw, so strip them when upgrading.