defineTool declares one tool, tools.sync updates an AgentDefinition draft, and tools.serve follows explicitly named Sessions.
Define one source of truth
jsonSchema as an object or a zero-argument function. The SDK validates through ~standard.validate and declares the rendered JSON Schema. No schema package becomes a runtime dependency of the SDK.For a raw JSON Schema object, the dependency-free fallback deliberately supports only type, enum, const, required, properties, additionalProperties, and items, plus annotation keywords such as description. An unsupported validation keyword is rejected when the tool is defined rather than silently ignored. Reach for a Standard Schema adapter when you need pattern, numeric ranges, unions, or references.New declarations must be object-root JSON Schemas and stay within 32 KiB, depth 32, and 2,000 JSON values. An invalid declaration throws before sync, and the API applies the same fence on create, patch, import, and publish. Published versions stay readable and immutable.Sync the mutable draft
sync preserves catalog tools, replaces the draft’s Custom-tool set, and stops there. It never publishes, so a running Session keeps using its admitted immutable AgentDefinition version. Review and publish the draft through the ordinary Agent workflow.For needsActionApproval: true, sync also creates or verifies a real require_approval ActionPolicy on the AgentDefinition permission use_tool:<tool-name>. It creates the replacement before deleting a stale SDK-managed rule. SDK ownership requires the current generated name grammar and a matching custom-tool-<name> rule identity, so a legacy or hand-named ActionPolicy is never deleted. The API key used for sync therefore needs authority to edit the AgentDefinition and manage ActionPolicies. This is platform enforcement, not a client-side prompt, and a definition-wide ActionPolicy on use_tool still applies to every Custom tool.ActionPolicy is live authority, separate from the immutable admitted definition. Changing needsActionApproval can govern a matching call in a Session that is already running, even though the draft declaration itself applies only after publication and later admission.Serve the Sessions your app owns
waitForTurnSettlement is your own helper: watch the event log for the terminal event you care about, not a timer.The serving loop first rebuilds from the Session log, then tails with cursor resume. For each parked call it correlates run.requires_action or run.action_authorized with the preceding agent.tool_use, validates the recorded arguments, runs the matching handler, and posts the attributed user.custom_tool_result through the ordinary drive endpoint.Serving is persistent by default, whether or not you pass a cancellation signal. Set persistent: false only when you want each Session loop to stop after one clean stream pass.Handlers must return JSON-representable values. The SDK checks the result before posting and rejects undefined, functions, BigInt, cyclic objects, and anything JSON serialization would silently change.The handler context includes the serving group’s signal. Pass it to fetch or your dependency client so caller cancellation can stop in-flight work before serve() settles. The context also carries sessionId and the replay-stable toolUseId.ActionApproval is a separate durable decision. A needsActionApproval call first parks on action.kind: "action_approval", and your review UI answers it through the ActionApproval API. ToolServing waits. Only run.action_authorized lets the handler run.Failure and replay behavior
Argument validation failure becomes an ordinary in-band tool result:serve() rejects with an AggregateError; each entry in its errors array is a { sessionId, cause } record. With persistent serving, healthy sibling loops continue until they settle or the caller aborts the shared signal.
After a crash, restart the same serve call. ToolServing re-reads the log and does not invoke a handler whose result is already recorded. If a result post loses its response and a retry conflicts, the worker treats that as success only after a fresh log read finds a structurally identical result. A different recorded result stays a conflict.
A process can still crash after your handler performs its side effect but before the result reaches the Session log. Make mutating handlers idempotent on context.toolUseId, the second handler argument, or fence that identity in your own store before performing the side effect.
Next steps
Handle an approval
Build the human decision surface for
needsActionApproval calls.Resume a stream
Understand the cursor and replay rules ToolServing follows.