# AgentLattice — full documentation > TypeScript framework for building coordinated agent systems with tools, skills, tracing, and teams. Every English documentation page, in sidebar order. Install with `npm install agent-lattice zod`. Individual pages are also available as Markdown at their own URLs; see llms.txt for the index. This documentation describes agent-lattice v0.23.0. If the package is already installed, check the installed version before writing code against these pages, and upgrade if it is older. Unknown options are ignored rather than rejected, so calling a newer API on an older install succeeds with the feature silently doing nothing — there is no error to catch. Install or upgrade with `npm install agent-lattice@0.23.0`. --- # Quickstart > Install AgentLattice and run a minimal agent. ## Install ```bash npm install agent-lattice zod ``` ## Ask a question The examples use DeepSeek's Anthropic-compatible endpoint. ```ts import { createAgent } from "agent-lattice"; const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", }); for await (const message of agent.query("Say hello in one sentence.")) { console.log(message); } ``` `agent.query()` returns an async iterator of SDK events. Use it when you want to observe the agent as it runs: initialization, assistant messages, streaming deltas, tool calls, tool results, and the final result. ## Return only the final result `agent.prompt()` runs the same agent loop and resolves with only the final `result` message. It is the easiest API when your application does not need intermediate events. ```ts const result = await agent.prompt("What is 2+2?"); console.log(result.result); ``` ## Send multimodal input For image or document input, pass a content block array instead of embedding media in a string. The SDK forwards Anthropic-compatible `image` and `document` blocks through the agent loop. ```ts const result = await agent.prompt([ { type: "text", text: "Summarize this screenshot." }, { type: "image", source: { type: "base64", media_type: "image/png", data: imageBase64, }, }, ]); console.log(result.result); ``` ## Choose streaming behavior `query()` and `prompt()` describe how your app consumes SDK output. The `stream` option controls whether the underlying model request uses streaming. ```ts // Model streaming + SDK event streaming. for await (const message of agent.query("Write a short haiku.", { stream: true, })) { console.log(message); } ``` ```ts // Non-streaming model request, while still consuming SDK events. for await (const message of agent.query("Write a short haiku.", { stream: false, })) { console.log(message); } ``` ```ts // Non-streaming model request + final result only. const result = await agent.prompt("Write a short haiku.", { stream: false, }); ``` Use `outputFormat` for schema-constrained JSON output: ```ts const jsonResult = await agent.prompt("Return JSON only.", { outputFormat: "json", }); const result = await agent.prompt("Return the answer to 2 + 2.", { outputFormat: { type: "json_schema", schema: { type: "object", properties: { answer: { type: "number" }, }, required: ["answer"], additionalProperties: false, }, }, }); ``` When `outputFormat` is set, the SDK sends structured output parameters to the provider and returns the final text unchanged. Parse or validate the returned JSON in your application when you need a typed value. In short: `query()` is the event-stream API, `prompt()` is the final-result API, `{ stream: true | false }` controls model streaming, and `outputFormat` requests JSON or provider-supported schema output. ## Next steps When you add custom tools, import the `tool` helper explicitly: ```ts import { createAgent, tool } from "agent-lattice"; ``` Then continue with [Tools](/concepts/tools.md). - For one-shot child agents, read [Supervisor delegation](/concepts/supervisor-delegation.md). - For persistent team coordination, read [Mailbox Team](/concepts/mailbox-team.md). --- # Configuration > Configure models, providers, turn limits, and request controls. ## DeepSeek ```ts import { createAgent } from "agent-lattice"; const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", systemPrompt: "You are a concise engineering assistant.", maxTokens: 16384, maxTurns: 50, }); ``` DeepSeek exposes an Anthropic-compatible endpoint, so configure `baseURL` and use a DeepSeek model name. ## Anthropic Use Anthropic directly by omitting `baseURL` and passing an Anthropic model name. ```ts const agent = createAgent({ apiKey: process.env.ANTHROPIC_API_KEY, model: "claude-sonnet-4-6", systemPrompt: "You are a concise engineering assistant.", }); ``` ## System prompt Use `systemPrompt` to define an agent's stable role, responsibility, and boundaries. This is especially important in team setups where each working `Agent` needs a different job description. ```ts const backendAgent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", systemPrompt: [ "You are the backend executor agent.", "Handle APIs, data models, storage, integrations, and server-side correctness.", "Escalate product or UI decisions instead of guessing.", ].join("\n"), }); ``` ## Workspace Every agent gets a private workspace for durable files, code, reports, logs, and test evidence. The workspace belongs to that agent, not to a `Team` or `teamMember()` definition. By default, the SDK stores agent work under `~/.agent/workspaces/`. If the agent has no `name`, it uses `~/.agent/workspaces/agent-`. ```ts const backendAgent = createAgent({ apiKey, name: "backend", model, systemPrompt: "You are the backend executor agent.", }); ``` Use `workspace` only when the host wants to override the default location: ```ts const backendAgent = createAgent({ apiKey, name: "backend", model, workspace: ".agent-workspaces/backend", }); ``` The SDK automatically adds workspace tools and instructions telling the agent to report important paths and verification notes in natural language. Pass `workspace: false` to opt out of the built-in workspace entirely: no built-in file/shell tools, no workspace prompt section — equivalent to `createBareAgent()`. *Requires 0.23.0 or later.* Unlike `createBareAgent()`, the option also works through `defineAgent()`, so `defineAgent({ workspace: false })` spawns bare sessions. This is useful for typed-delegation specialists that should have no filesystem or shell surface. ## Bare agents Use `createBareAgent()` when the host wants a plain model loop with no default prompt, no default tools, and no workspace directory creation. This is useful for hosted applications that provide their own sandbox, tests that need exact tool lists, or wrappers that assemble capabilities themselves. ```ts import { createBareAgent, createBuiltinTools } from "agent-lattice"; const agent = createBareAgent({ apiKey, model, systemPrompt: "You are a concise engineering assistant.", tools: createBuiltinTools({ cwd: process.cwd(), allowedDirectories: [process.cwd()], }), }); ``` Most agents that produce durable files should use `createAgent()`. Choose `createBareAgent()` only when your application owns every capability explicitly. ## Abort a request ```ts const controller = new AbortController(); setTimeout(() => controller.abort(), 5000); const result = await agent.prompt("Summarize this repository.", { signal: controller.signal, }); ``` ## Query options | Option | Type | Default | Description | | --- | --- | --- | --- | | `stream` | `boolean` | `true` | Enables model streaming and `stream_event` SDK messages. | | `signal` | `AbortSignal` | `undefined` | Cancels the active model request and tool loop. | ## Turn limits `maxTurns` counts model requests, not tool executions. The default is `50`, which gives multi-agent and tool-heavy flows room to complete while still protecting the host from infinite loops. Set a lower value for small one-shot tools or tests: ```ts const agent = createAgent({ apiKey, model, maxTurns: 8, }); ``` --- # Agent Loop > How the SDK turns model responses and tool calls into an agent session. The agent stores conversation state in memory for the lifetime of the `Agent` instance. For each prompt, it: 1. Adds the prompt as a user message. 2. Calls the configured model client. 3. Emits the assistant response. 4. Executes any `tool_use` blocks. 5. Adds `tool_result` blocks as a user message. 6. Repeats until the assistant returns no tool calls. 7. Emits a final `result` message. ```ts const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", maxTurns: 50, }); await agent.prompt("Remember that my project is an SDK."); const result = await agent.prompt("What kind of project is this?"); ``` `maxTurns` defaults to `50` and protects the host application from infinite tool loops. It counts model requests. Tool executions, permission callbacks, and stream events do not increment it. When the limit is reached, the SDK emits a result with `subtype: "error_max_turns"`. A tool can also end the loop itself: when any tool in a batch returns `endTurn: true`, the SDK still records every `tool_result` of that batch and then finishes with `subtype: "success"` — step 6 stops without another model request, and the tool's content becomes the result text. *Requires 0.16.0 or later.* ## Structured output with submit_output *Requires 0.20.0 or later.* Set `AgentOptions.outputSchema` when a run must end with a validated, typed payload instead of free text. The SDK injects a built-in `submit_output` tool (its name is exported as `SUBMIT_OUTPUT_TOOL_NAME`) whose input schema is your schema converted to JSON Schema — `OutputSchema` is just `{ parse(input: unknown): T }`, so a zod schema works directly. This changes how the loop is allowed to end: - The model finishes by calling `submit_output`. The payload is validated against the schema first; a validation failure goes back into the loop as an error `tool_result`, so the model can fix it and retry. A valid submission ends the run with `subtype: "success"` and the payload on `SDKResultMessage.structuredResult`. - Ending the turn without calling `submit_output` fails the run with `subtype: "error_missing_output"` and a `MissingOutputError`. There is no fallback that parses the final text as JSON — submission is an explicit act. - `submit_output` must be the only tool call in its batch: a batch that mixes it with other calls, or contains two submissions, is rejected with code `submit_output_exclusive_batch` and the loop continues. - The name is reserved while `outputSchema` is set — registering your own `submit_output` tool throws from `createAgent`/`addTools`. The structure is enforced by the harness, not by prompt discipline, and only requires a model that can call tools — unlike `outputFormat`, it does not depend on provider `response_format`/`json_schema` support (see [Provider Compatibility](/reference/provider-compatibility.md)). ## One Agent holds one conversation *Requires 0.10.0 or later.* An `Agent` is a conversation, not a reusable client. Its message history is instance state, so a second query started while one is still running would interleave turns from both. The SDK rejects that second query with `ConcurrentQueryError` rather than corrupting either conversation. Create one Agent per concurrent conversation. In a server, that means constructing an Agent per request or per user session, not sharing a module-level instance: ```ts // Wrong: every request appends to the same history. const shared = createAgent({ model: "claude-sonnet-4-6" }); app.post("/ask", async req => shared.prompt(req.body.question)); // Right: one conversation per caller. app.post("/ask", async req => { const agent = createAgent({ model: "claude-sonnet-4-6" }); return agent.prompt(req.body.question); }); ``` Sequential reuse is fine and is how multi-turn conversations work: await one query before starting the next, as in the example above. The guard releases when a query finishes, including on error or abort. ## Persistent history and resume *Requires 0.16.0 or later.* By default the history lives only in memory. Attach a `HistoryStore` to seed it from durable storage and mirror every write back: ```ts import { createAgent, createJsonlHistoryStore } from "agent-lattice"; const agent = createAgent({ model: "claude-sonnet-4-6", historyStore: createJsonlHistoryStore({ path: ".agent-sessions/ada.jsonl" }), }); // The first query lazily loads whatever the store holds and continues from it. await agent.prompt("What is my name?"); // A deep copy of the live history; mutating it is safe. const transcript = await agent.getHistory(); ``` `load()` runs once per Agent lifetime, before the first query. Resuming a conversation in another process is a new Agent over the same store — the one-Agent-one-conversation rule above is unchanged. After loading, `append(message)` follows every message added to the history, and `replace(messages)` follows compaction, which rewrites the history wholesale, so a store must support full replacement. `createJsonlHistoryStore()` writes one JSON message per line and skips malformed lines on load, so a torn final write does not lose the transcript. By default a failing store is swallowed and the conversation continues in memory only; bind `failOnError: true` when the store is created (via `createJsonlHistoryStore()` options or `defineHistoryStore()`) to propagate store errors out of `query()`. The host can also rewrite the history directly with `agent.replaceHistory(messages)`. *Requires 0.17.0 or later.* It is idle-only — calling it while a query is running throws `ConcurrentQueryError` — and when a `historyStore` is configured the store is replaced too, so persistence stays in sync. The SDK does not validate the content: the host owns it, and the replacement must be a well-formed history (e.g. no dangling `tool_use` without its matching `tool_result`). ## Deadlines *`requestTimeoutMs` requires 0.11.0 or later.* Two limits apply at different scopes, and long-running agents usually need both. `QueryOptions.signal` bounds the whole query — every model request, tool call, and turn together: ```ts const result = await agent.prompt("Audit this repository.", { signal: AbortSignal.timeout(600_000), }); // result.subtype === "error_abort" ``` `requestTimeoutMs` bounds each single model request instead, so an agent that legitimately runs twenty tool-using turns is not forced to share one budget across all of them: ```ts const agent = createAgent({ model: "claude-sonnet-4-6", requestTimeoutMs: 120_000, }); // Or per query, which overrides the agent default. await agent.prompt("Answer quickly.", { requestTimeoutMs: 15_000 }); ``` Exceeding it produces `subtype: "error_timeout"` with a `TimeoutError`, distinct from the `"error_abort"` a caller-initiated cancellation produces, so a host can retry timeouts without retrying deliberate cancellations. The value must be a positive integer number of milliseconds. When unset, the SDK applies no deadline of its own and the provider client's default applies. Both limits are enforced by the SDK itself rather than delegated. `ModelRequest` carries `signal` and `timeoutMs` so a client can cancel its own work, but the agent loop also races the call: a `ModelClient` that ignores both cannot stall the loop indefinitely. Losing that race abandons the call rather than cancelling it, so a non-cooperative client may keep working in the background while the loop moves on. ## Interrupting a query *Requires 0.16.0 or later.* `QueryOptions.signal` terminates the query: it ends with `subtype: "error_abort"` and is meant for cancellation. When the host instead wants to take the conversation back — the user typed a correction, a higher-priority instruction arrived — `agent.interrupt()` ends the current query with `subtype: "interrupted"`, which is normal control flow rather than an error (`is_error` stays `false`): ```ts const pending = agent.prompt("Draft the release notes."); agent.interrupt(); const result = await pending; // result.subtype === "interrupted" // Same Agent, same history: inject the new message and continue. await agent.prompt("Actually, skip 0.15.x and cover 0.16.0 only."); ``` Only the in-flight turn is lost: the partial assistant message is dropped exactly as on abort, while every completed turn stays in the history, so the follow-up query sends the model the full prior conversation. An interrupt that lands while a tool batch is executing takes effect once the batch completes — its tool results are written to history first, and the query ends `"interrupted"` before the next model call. `interrupt()` is a no-op when no query is running. ## Token usage and truncation *Requires 0.10.0 or later.* Every `result` message carries `usage`, summed over the model requests in that query, and `stop_reason` from the last response: ```ts const result = await agent.prompt("Summarize this file."); console.log(result.usage); // { input_tokens, output_tokens, cache_read_input_tokens?, ... } if (result.stop_reason === "max_tokens") { // subtype is still "success", but result is a fragment, not an answer. } ``` `stop_reason: "max_tokens"` means the model ran out of output budget mid-response. The SDK does not treat that as an error, so checking it is the only way to tell a complete answer from a truncated one. Raise `maxTokens` or ask for less output. When a response containing tool calls is truncated at `max_tokens`, the SDK does not execute those calls: the last `tool_use` input may be incomplete, and a truncated value can even survive JSON parsing with its meaning changed. Every call in the batch receives an error `tool_result` explaining the truncation and asking the model to reissue the call with a shorter output, then the loop continues. *Requires 0.18.0 or later.* Usage is reported by the model client. The built-in Anthropic client fills it in; a custom `ModelClient` that omits `usage` yields zeroed counts rather than an error. Assistant messages also carry provider response metadata: `providerResponseId` (the provider-assigned response id) and `model` (the model that actually served the response, which may differ from the requested one). The built-in Anthropic client fills both in on streaming and non-streaming requests; a custom `ModelClient` may set them on the `AssistantModelMessage` it returns. Both are absent when the client does not report them. *Requires 0.16.0 or later.* ## Automatic context compaction *Requires 0.13.0 or later; recovery after an overflow requires 0.14.0.* History only grows, so a long-running agent eventually exceeds the model's context window. Enable `autoCompact` to have the SDK replace the older part of the conversation with a model-written summary: ```ts const agent = createAgent({ model: "claude-sonnet-4-6", autoCompact: true, // or { thresholdTokens: 150_000, keepRecentMessages: 8 } }); ``` Compaction runs **between turns**, once a response has reported more input tokens than `thresholdTokens` (default `100000`). The SDK summarizes everything except the last `keepRecentMessages` messages (default `6`), then rebuilds the history as the summary followed by those retained messages. The summary is wrapped in an instruction telling the model that compaction just happened and to continue from it, so the next turn resumes the task rather than restarting it. Unlike the `onModelRequest` hook, which only shapes one outgoing request, **this rewrites the stored conversation**. That is the point — the saving has to persist across turns — but it means the replaced turns are gone from the agent. The cut point is chosen so the retained messages stand on their own: a `tool_result` is never separated from the `tool_use` that produced it, since the model API rejects that pairing. If no safe cut leaves anything to summarize, compaction is skipped. Compaction costs a model call. Its tokens are folded into `result.usage`, so the total stays honest, and a `system` message with `subtype: "compaction"` is emitted when it happens: ```ts for await (const message of agent.query("Refactor this module.")) { if (message.type === "system" && message.subtype === "compaction") { console.log(`compacted ${message.compacted_messages} messages`, message.usage); } } ``` The trigger depends on reported usage, so an agent using a custom `ModelClient` that omits `usage` never compacts. Override the summarization instruction with `prompt`, or read the built-in one from `DEFAULT_COMPACTION_PROMPT`. ### Recovering from an overflow that already happened The threshold is a forecast, so a single large tool result can still carry a request past the window. When that happens, compaction also runs as a recovery: the SDK summarizes, then retries the same turn rather than failing the query. Two signals trigger it: | Signal | Meaning | | --- | --- | | `stop_reason: "model_context_window_exceeded"` | The window ran out mid-request. The response is unusable and is never added to the history. | | An API error naming a too-long prompt | The input already exceeded the window, so the request was rejected before generation and there is no stop reason at all. | Recovery is attempted once per query. If summarizing fails, or there is nothing left to summarize, the original failure surfaces unchanged rather than being replaced by a second, derived one. **`stop_reason: "max_tokens"` deliberately does not trigger compaction.** It means the *output* hit `maxTokens`, not that the input was too large — the model had plenty of room to read and ran out of room to write. Compacting the history would not make the answer complete. Raise `maxTokens` or ask for less output instead. --- # Streaming Events > Consume stable SDK messages and raw model stream events. `agent.query()` returns an async generator of SDK messages. ```ts for await (const message of agent.query("What is 2+2?")) { switch (message.type) { case "system": console.log("session", message.session_id); break; case "stream_event": console.log("raw stream event", message.event); break; case "assistant": console.log("assistant message", message.message); break; case "user": console.log("tool result", message.tool_use_result); break; case "result": console.log("final", message.result); break; } } ``` ## Message types | Type | Description | | --- | --- | | `system` | Session initialization metadata. | | `stream_event` | Raw provider stream events, emitted when streaming is enabled. | | `assistant` | Assistant message after model assembly. | | `user` | Tool result message sent back to the model. | | `result` | Final success, API error, abort, or max-turn result. | ## Delivery timing `stream_event` messages arrive while the model is still responding rather than in a burst after the turn ends, so they can drive incremental UI directly. `assistant` arrives once the model response for that turn is assembled, and `user` arrives after the whole tool batch has finished. The SDK produces the next event only after the consumer takes the current one. Slow work inside the `for await` body delays later events without dropping or reordering them, so keep expensive handling off the loop itself. ## Per-event guarantees The event stream is built for live UI. These guarantees are part of the public contract: - **The prompt is not echoed.** `agent.query("...")` never yields a `user` event containing your prompt; the prompt goes into the internal history only. If you need the full transcript — the prompt, tool results, and messages injected by a team runtime — subscribe a [ContextTracer](/concepts/context-tracing.md) instead. Its `user_message` / `assistant_message` events record everything; the event stream does not. - **`assistant`** fires once per model turn, after that turn's response is fully assembled. `message` is an `AssistantModelMessage`: `content` holds text and `tool_use` blocks, `stopReason` says why the model stopped, and `providerResponseId` / `model` carry provider metadata when the model client reports them. - **`user`** appears only after a whole tool batch has finished executing — one event per batch, never one per tool. `message.content` is always `ToolResultBlock[]`. `tool_use_result` is a convenience view over the same results: with a single tool it is that result's `content` (`string | ContentBlock[]`); with several tools it is the array of result blocks. The first tool error, if any, is also surfaced on `error`, and an aborted batch still yields a `user` event with `error` set. - **`result`** is the terminal event, emitted exactly once per query. `subtype` is `"success"` for a normal end, `"interrupted"` when `Agent.interrupt()` ends the query cleanly (completed turns stay in history and `is_error` remains `false`), or one of `"error"`, `"error_max_turns"`, `"error_abort"`, `"error_timeout"`. Even on success, check `stop_reason` for `"max_tokens"` — the text may be a truncated fragment. --- # Context Tracing > Record context traces for agent runs, tool calls, and delegated team work. Context tracing lets a host observe an agent run without changing the agent loop. The SDK emits structured trace events through a small `ContextTracer` interface. Built-in tracers can write JSONL locally or project those events into LangSmith or Langfuse. Tracing is append-only observability. It is not resume support: agents still keep conversation state in memory for the lifetime of the `Agent` instance. ## JSONL tracer Use `createJsonlContextTracer()` when you want local trace files for debugging, audit logs, or test artifacts. ```ts import { createAgent, createJsonlContextTracer } from "agent-lattice"; const tracer = createJsonlContextTracer({ path: ".agent-runs/session.jsonl", }); const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", tracer, }); await agent.prompt("Remember that my name is Ada.", { stream: false, }); ``` Each JSONL row is an independent object: ```json { "version": 1, "timestamp": "2026-06-28T12:00:00.000Z", "session_id": "session-id", "run_id": "run-id", "seq": 1, "source": { "kind": "agent", "name": "agent" }, "type": "user_message", "data": { "message": { "role": "user", "content": "Remember that my name is Ada." } } } ``` If you pass `dir` instead of `path`, the tracer writes one file per session: ```ts const tracer = createJsonlContextTracer({ dir: ".agent-runs", }); ``` ## LangSmith tracer Use `createLangSmithContextTracer()` when you want SDK runs to appear as LangSmith traces. The SDK depends on `langsmith` directly and reuses its official `RunTree` types, so the tracer works out of the box. *Since 0.17.0 the bundled `RunTree` is the default; pass `RunTree` or `runTree` only to inject a custom runtime or a test fake.* Configure LangSmith with its standard environment variables: ```bash LANGSMITH_TRACING=true LANGSMITH_ENDPOINT=https://api.smith.langchain.com LANGSMITH_API_KEY= LANGSMITH_PROJECT= # Required only for org-scoped or multi-workspace API keys. LANGSMITH_WORKSPACE_ID= ``` ```ts import { createAgent, createLangSmithContextTracer } from "agent-lattice"; const tracer = createLangSmithContextTracer({ projectName: process.env.LANGSMITH_PROJECT, workspaceId: process.env.LANGSMITH_WORKSPACE_ID, tags: ["local-debug"], }); const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", tracer, }); try { await agent.prompt("Trace this run.", { stream: false }); } finally { await tracer.close?.(); } ``` Close or flush the LangSmith tracer before a short-lived test process exits. This waits for LangSmith's internal trace batch queue, including the final root run patch that marks the run completed. If you prefer explicit values over environment variables, pass them to the SDK tracer. `workspaceId` is optional and only selects a LangSmith workspace; it is not the tracing project name. ```ts import { createLangSmithContextTracer } from "agent-lattice"; const tracer = createLangSmithContextTracer({ apiKey: process.env.LANGSMITH_API_KEY, apiUrl: process.env.LANGSMITH_ENDPOINT, projectName: process.env.LANGSMITH_PROJECT, // Optional: only when LangSmith requires an explicit workspace. workspaceId: process.env.LANGSMITH_WORKSPACE_ID, }); ``` The LangSmith tracer implements the same `ContextTracer` interface as JSONL, so the agent loop stays closed to provider-specific changes. LangSmith receives: | SDK event sequence | LangSmith projection | | --- | --- | | `run_start` to `result` | `chain` run; it is a root unless `parent_run_id` links it to another chain. | | `model_request` to `assistant_message` | Child `llm` run. | | `tool_use` to `tool_result` | Child `tool` run. | | `team_message` and other auxiliary events | Run events on the active chain. | Each run's duration reflects the work itself. A `tool` run starts when that handler starts, so sequential calls appear one after another and concurrent calls overlap. An `llm` run ends when the model request settles, not when the host finishes consuming the events yielded from it. Tool runs carry the tool's `description` as `tool_description` metadata, so a trace shows what the tool was advertised to do, not only its input and output. Use `createCompositeContextTracer()` to send the same trace stream to multiple sinks: ```ts const tracer = createCompositeContextTracer([ createJsonlContextTracer({ path: ".agent-runs/session.jsonl" }), createLangSmithContextTracer({ projectName: process.env.LANGSMITH_PROJECT, workspaceId: process.env.LANGSMITH_WORKSPACE_ID, }), ]); ``` When a composite tracer contains LangSmith, close the composite tracer in the same `finally` block; it forwards `close()` to every child tracer. ## Langfuse tracer *Requires 0.19.0 or later.* Use `createLangfuseContextTracer()` when you want SDK runs to appear as Langfuse traces. The adapter targets the current Langfuse JS SDK generation (`@langfuse/tracing` v5), which is OpenTelemetry-based: register the `LangfuseSpanProcessor` once at process startup, and the tracer works out of the box from then on. Configure Langfuse with its standard environment variables: ```bash LANGFUSE_PUBLIC_KEY= LANGFUSE_SECRET_KEY= LANGFUSE_BASE_URL=https://us.cloud.langfuse.com # or your self-hosted host ``` ```bash npm install @langfuse/otel @opentelemetry/sdk-trace-node ``` ```ts // instrumentation: register the span processor before agents run. import { LangfuseSpanProcessor } from "@langfuse/otel"; import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; export const langfuseSpanProcessor = new LangfuseSpanProcessor(); const tracerProvider = new NodeTracerProvider({ spanProcessors: [langfuseSpanProcessor], }); tracerProvider.register(); ``` ```ts import { createAgent, createLangfuseContextTracer } from "agent-lattice"; import { langfuseSpanProcessor } from "./instrumentation"; const tracer = createLangfuseContextTracer({ // Drained by tracer.flush()/close() so spans reach Langfuse before a // short-lived process exits. spanProcessor: langfuseSpanProcessor, tags: ["local-debug"], }); const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", tracer, }); try { await agent.prompt("Trace this run.", { stream: false }); } finally { await tracer.close?.(); } ``` `startObservation` defaults to the bundled `@langfuse/tracing` function; pass `startObservation` only to inject a custom runtime or a test fake. Langfuse receives one trace per SDK query: | SDK event sequence | Langfuse projection | | --- | --- | | `run_start` to `result` | `chain` observation; it is the trace root unless `parent_run_id` nests it under another chain. | | `model_request` to `assistant_message` | Child `generation` observation with the `model` attribute. | | `tool_use` to `tool_result` | Child `tool` observation. | | `team_message` and other auxiliary events | Auto-ended `event` observations on the active chain. | The trace root observation carries the trace name, `session.id`, and `langfuse.trace.tags` attributes — the same attributes `propagateAttributes` writes on the active span — so traces stay grouped and filterable in the Langfuse UI. Error results and tool errors set the observation `level` to `ERROR` with a `statusMessage`. ## Event types | Type | Description | | --- | --- | | `run_start` | Agent run metadata, including model and tool names. | | `user_message` | User prompt or tool-result message added to the model context. | | `model_request` | Model request context, including selected messages, tools, and stream mode. | | `assistant_message` | Assistant message returned by the model client. | | `tool_use` | Tool call requested by the assistant, including the tool's `description` (since 0.17.0). | | `tool_result` | Tool result produced by the SDK and sent back to the model. | | `result` | Final query result, including success and error subtypes. | The tracer uses a monotonically increasing `seq` per tracer instance. Use `session_id`, `run_id`, and `source` to group entries in UIs or log pipelines. ## Team tracing Pass a tracer in `team.query()` or `createTeamRunner().query()` options to propagate the same trace sink into delegated agents. ```ts for await (const event of team.query("Ask engineering to inspect tracing.", { tracer, })) { console.log(event); } ``` One `team.query()` or `team.prompt()` call creates one Team-level root run. The Lead's initial work, each delegated Member run, and every later Lead run are children of that root. They share one trace `session_id`, so LangSmith displays the complete handoff as one trace instead of separate top-level traces. The shared trace session does not replace an Agent's own SDK session. Trace metadata keeps that original value as `agent_session_id`, and emitted SDK messages continue to use the Agent's existing `session_id`. Delegated agents include their runtime source: ```json { "source": { "kind": "team_member", "name": "engineering", "member": "engineering", "mailbox": "engineering" }, "type": "result", "data": { "subtype": "success", "result": "Engineering result" } } ``` This keeps the JSONL stream readable even when a root agent delegates through teams or nested teams. ## Custom sinks `ContextTracer` is intentionally small so hosts can write to their own storage or observability pipeline. The port object exposes methods only — `failOnError` is bound when the factory creates the tracer. Implement a custom sink with `defineContextTracer()`. *Requires 0.17.0 or later.* ```ts import { defineContextTracer } from "agent-lattice"; const tracer = defineContextTracer({ async onEvent(event) { // TODO: Replace with your own database insert. }, async flush() { // TODO: Replace with your own buffered-write flush. }, }); ``` Pass `failOnError: true` to the factory only when trace durability is part of your product contract. By default, tracing should be observational and should not make agent runs fail. ## Redaction Trace entries can contain prompts, model context, tool inputs, and tool outputs. Use `redact` for local policy before writing JSONL. `redact` is a callback that you provide. Before the JSONL tracer writes each trace event, the SDK passes the event to this callback. The callback can return the original event, return a modified copy, or return `undefined` to skip the event. This only changes what is written to the trace file; it does not change the request sent to the model. ```ts const tracer = createJsonlContextTracer({ path: ".agent-runs/session.jsonl", redact(event) { if (event.type === "model_request") { return { ...event, data: { ...event.data, messages: "[redacted]", }, }; } return event; }, }); ``` In this example, `model_request.data.messages` is replaced with `"[redacted]"` before persistence. That field can contain the user prompt, conversation history, and tool results, so it is often the first place to apply a local privacy policy. Other event types are returned unchanged. Return `undefined` from `redact` to skip an event entirely: ```ts const tracer = createJsonlContextTracer({ path: ".agent-runs/session.jsonl", redact(event) { if (event.type === "tool_result") { return undefined; } return event; }, }); ``` --- # Tools > Define custom tools with schemas, parsed input, and typed handlers. 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. ```ts 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. ## Tool result ```ts 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. ## End the run from a tool *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: ```ts 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.* ## Run independent tools concurrently *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: ```ts 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. ## Validate a tool batch before execution *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. ```ts 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. ## Tool metadata *Requires 0.22.0 or later.* `ToolOptions.metadata` accepts a `Record` 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. ## Strict option validation *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(""):` / `delegateTool(""):`). 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. --- # Hooks > Rewrite tool results and outgoing model requests as they cross the agent loop. *Requires 0.12.0 or later.* `permission` and `toolBatchPolicy` decide whether something runs. Hooks decide what it looks like. Use them to redact tool output, trim context before a request, or inject retrieved documents. ```ts const agent = createAgent({ model: "claude-sonnet-4-6", hooks: { async onToolResult({ toolName, result }) { if (toolName !== "queryDatabase") return; // undefined: leave unchanged return { ...result, content: await redact(result.content) }; }, onModelRequest({ messages }) { if (messages.length < 40) return; return { messages: compact(messages) }; }, }, }); ``` ## onToolResult Runs for every tool result on its way back to the model — successful handlers, failed ones, calls cancelled by an abort, and calls blocked by `toolBatchPolicy`. The context carries `toolName`, `toolUseId`, the raw `input` the model sent, the `result` block the SDK would return, and `error` when the call failed. Return a replacement `tool_result` block, or nothing to keep the original. A hook can turn a failure into a success, or a success into an error, by setting `is_error` on what it returns. A hook cannot add or remove tool calls. Each `tool_use` must be answered by exactly one `tool_result`, and breaking that pairing is rejected by the model API. ## onModelRequest Runs before each model request, receiving the `messages` and `systemPrompt` that would be sent plus the 1-based `turn`. Return `{ messages }`, `{ systemPrompt }`, or both. This shapes **one request only**. The agent's stored conversation is untouched, so trimming context for a long turn does not destroy history — the next turn's hook still sees everything. This matches how skills are injected. ## Rules **Return, do not mutate.** A hook receives the value the SDK is about to use. Return a replacement instead of editing it in place; returning nothing means no change. **A hook that throws propagates out of `query()`.** It does not become an error `result`, and it is not swallowed the way a tracer error is. A hook failure is host code failing, and a redaction hook that failed quietly would leak exactly the data it exists to protect. Handle recoverable errors inside the hook. **Hooks run before the matching trace event.** A trace therefore records what was actually sent, not the value before rewriting. **Hooks belong to one Agent.** They are set on `createAgent()` and are not inherited by delegated agents or team members; give those their own. There is no per-query override — a redaction policy that varied by call site would not be auditable. ## Composing ```ts import { createCompositeAgentHooks } from "agent-lattice"; const hooks = createCompositeAgentHooks([redactionHooks, truncationHooks, auditHooks]); ``` Hooks are chained in array order, and each one receives the previous one's output, so separate concerns can be written independently and combined. `undefined` and `null` entries are skipped, which makes conditional composition straightforward. --- # MCP > Connect MCP server tools to the agent loop. MCP support lets the SDK expose tools from a Model Context Protocol server as normal agent tools. The SDK supports stdio MCP servers, remote Streamable HTTP servers, OAuth providers, and a generic `MCPClient` adapter. ## Connect a stdio MCP server ```ts import { connectMCPStdioServer, createAgent, } from "agent-lattice"; const mcp = await connectMCPStdioServer( { command: "node", args: ["./mcp-server.js"], }, { namePrefix: "docs", }, ); const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", tools: mcp.tools, }); try { const result = await agent.prompt("Search the docs for installation steps."); console.log(result.result); } finally { await mcp.close(); } ``` ## Use an existing MCP client If your application already owns the MCP connection lifecycle, map it into SDK tools: ```ts import { createAgent, createMCPTools } from "agent-lattice"; const tools = await createMCPTools(existingMCPClient, { namePrefix: "repo", }); const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", tools, }); ``` ## Tool names MCP tool names are sanitized before being sent to the model. Use `namePrefix` when connecting multiple MCP servers so tools do not collide. For example, an MCP tool named `search` with `namePrefix: "docs"` becomes `docs_search`. ## Connect a remote Streamable HTTP server ```ts import { connectMCPStreamableHTTPServer, createAgent, } from "agent-lattice"; const mcp = await connectMCPStreamableHTTPServer("https://mcp.example.com/mcp", { namePrefix: "remote", requestInit: { headers: { "X-Workspace": "demo", }, }, }); const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", tools: mcp.tools, }); ``` The connection exposes: | Property | Description | | --- | --- | | `tools` | SDK tool definitions mapped from the remote MCP server. | | `client` | Minimal MCP client adapter. | | `close()` | Closes the MCP connection. | | `finishAuth(code)` | Completes an OAuth authorization-code flow. | | `terminateSession()` | Sends MCP session termination when the server supports it. | | `sessionId` | Current Streamable HTTP MCP session ID, when available. | ## OAuth Pass an official MCP `OAuthClientProvider` when the server requires OAuth: ```ts const mcp = await connectMCPStreamableHTTPServer("https://mcp.example.com/mcp", { authProvider, }); ``` If the transport needs user authorization, the official MCP SDK calls `authProvider.redirectToAuthorization(url)`. After the user returns with an authorization code, call: ```ts await mcp.finishAuth(code); ``` --- # Skills > Reuse instruction bundles without depending on the Claude Code runtime. Skills are reusable instruction bundles. They help the agent adopt domain-specific behavior for matching prompts. AgentLattice supports a lightweight Skill API. It does not require the Claude Code runtime, and it does not load Claude Code plugins. ## Create a skill in code ```ts import { createAgent, skill } from "agent-lattice"; const codeReview = skill({ name: "code-review", description: "Review code changes and pull requests", instructions: "Always list bugs and risks before summaries.", }); const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", skills: [codeReview], }); ``` ## Load a skill from disk ```ts import { createAgent, loadSkill } from "agent-lattice"; const pdf = await loadSkill("./skills/pdf"); const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", skills: [pdf], }); ``` `loadSkill(path)` expects a `SKILL.md` file: ```md --- name: pdf description: Read and inspect PDF documents --- Render pages before claiming layout is correct. ``` ## Matching behavior The agent compares the user's prompt with each skill's `name` and `description`. When a skill matches, the SDK injects that skill's instructions into the model request for that query. When it does not match, the skill is left out. This keeps prompts smaller while still allowing applications to register multiple reusable skills. --- # Agent delegation and teams > Choose between one-shot supervisor delegation and persistent mailbox teams. AgentLattice has two related but different coordination patterns. Keep them separate when you design an application: - [Supervisor delegation](/concepts/supervisor-delegation.md) is for one-shot child agents. A supervisor exposes another `AgentLike` as a model tool, waits for the child result in the tool call, and continues its own turn. - [Mailbox team](/concepts/mailbox-team.md) is for persistent teams. A lead coordinates named members through mailbox-backed work items, replies, status, persistence, and optional manual draining. Both patterns use the same core invariant: every callable boundary is an `AgentLike`. The difference is lifecycle. | Pattern | Use it when | Main API | | --- | --- | --- | | Supervisor delegation | The parent needs a temporary specialist such as explore, plan, review, or implementation help for this turn. | `agentTool()` with `mode: "ask"` | | Mailbox team | You want a durable organization with named members, inboxes, replies, nested teams, and resumable work. | `createTeam()` and `createTeamRunner()` | If you only need the main agent to ask a temporary helper for one result, start with supervisor delegation. If work should live as routed messages that can be claimed, replied to, inspected, or persisted, use a mailbox team. --- # Supervisor delegation > Use one-shot AgentLike child agents as tools. Supervisor delegation is for "ask a specialist once." The user keeps talking only to the supervisor. When useful, the supervisor calls a temporary child agent through a tool, gives it one clear task, and receives the child agent's final answer as the `tool_result`. Use this for short-lived child agents such as: - an explore agent that reads the codebase and reports findings - a planner that proposes an implementation path - a reviewer that checks a patch - an implementer that writes one artifact and reports what changed This pattern does not require a team mailbox. The child `AgentLike` runs for one request, returns one result, and the supervisor decides what to do next. ## Basic shape ```ts import { agentTool, createAgent, } from "agent-lattice"; const explorer = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", name: "explorer", systemPrompt: [ "You are an explore subagent.", "Inspect the repository, identify relevant files, and report concise findings.", "Do not make code changes.", ].join("\n"), workspace: ".agent-workspaces/explorer", }); const supervisor = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", name: "supervisor", systemPrompt: "You supervise the task and call specialist subagents when useful.", tools: [ agentTool("explore", explorer, { description: "Ask a temporary explore subagent to inspect the codebase and report findings.", }), ], }); const result = await supervisor.prompt("Find the modules involved in SDK team delegation."); console.log(result.result); ``` When the supervisor agent calls the `explore` tool with `mode: "ask"`, the SDK calls `explorer.prompt(...)`. The explorer's final result becomes the supervisor's `tool_result`, and then the supervisor continues its own agent loop. ## Tool input `agentTool()` wraps a child agent into a **plain tool** — it returns a `ToolDefinition` you drop into `createAgent`'s `tools` just like any custom tool. To the supervisor model it's indistinguishable from any other tool; the only difference is internal: when the model calls it, the SDK runs the wrapped child agent instead of running code, and returns that as the tool result. That tool exposes a standard model-facing input: ```ts { mode: "ask", task: "Inspect the SDK team runtime and summarize the relevant files.", expectedOutput: "A concise file map and implementation notes", acceptanceCriteria: [ "Mention the runner entry point", "Mention mailbox state transitions" ] } ``` For one-shot supervisor delegation, prefer `mode: "ask"`. | Mode | Meaning in supervisor delegation | | --- | --- | | `ask` | Run the child `AgentLike` now and return its final answer as this tool result. | | `handoff` | Requires a team runtime. Without a runtime, the tool returns a clear error. Use mailbox teams for async handoff. | | `observe` | Reserved for observable long-running work. It currently reports unsupported unless a host runtime provides it. | > The **team runtime** here is the runtime that backs a persistent mailbox team — it owns the message queue, holds task state, and drives member lifecycle after a `handoff`. This one-shot pattern has no runtime by default, so `handoff` / `observe` are rarely useful here; when you need them, use a [persistent mailbox team](/concepts/mailbox-team.md). `AgentToolOptions` also accepts `metadata`, a host-owned `Record` passed through to the generated `ToolDefinition.metadata` (for example a contract version). The SDK never reads or interprets it and never shows it to the model. *Requires 0.22.0 or later.* ## Spec or session target *Requires 0.15.0 or later.* `agentTool()` accepts either a live `AgentLike` session (as above) or an `AgentSpec` template created by `defineAgent()`: ```ts import { agentTool, defineAgent } from "agent-lattice"; const explorerSpec = defineAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", name: "explorer", systemPrompt: "You are an explore subagent. Inspect and report; do not make code changes.", }); const supervisor = createAgent({ // ... tools: [ agentTool("explore", explorerSpec, { description: "Ask a temporary explore subagent to inspect the codebase and report findings.", }), ], }); ``` A spec target spawns a **fresh session on every tool call** — no history leaks between unrelated tasks, and concurrent calls never collide on a shared conversation. A session target keeps its history across calls, so earlier tasks can influence later answers. Prefer a spec for supervisor delegation; register `spec.spawn()` only when the specialist should remember previous tasks. The generated tool description states which semantics the target has, so the supervisor model knows whether each task must be self-contained. ## Structured child output *Requires 0.20.0 or later.* When the child's deliverable should be typed data rather than prose, declare an `OutputSchema` on the child (a zod schema works directly): the child agent must then submit its answer through the injected `submit_output` tool, and the parent side validates what comes back. Since 0.21.0 `agentTool()` inherits the target's declared schema — from `spec.options.outputSchema` for an `AgentSpec`, or from the live Agent's `outputSchema` getter — so the parent-side copy can be omitted: ```ts import { agentTool, createAgent } from "agent-lattice"; import { z } from "zod/v4"; const findingsSchema = z.object({ files: z.array(z.string()), notes: z.string(), }); const explorer = createAgent({ model: "deepseek-v4-flash", name: "explorer", systemPrompt: "You are an explore subagent. Inspect and report; do not make code changes.", outputSchema: findingsSchema, }); const supervisor = createAgent({ model: "deepseek-v4-flash", name: "supervisor", tools: [ agentTool("explore", explorer, { description: "Ask a temporary explore subagent to inspect the codebase and report findings.", // outputSchema is inherited from the target (0.21.0+). }), ], }); ``` With `mode: "ask"` the tool result is the child's validated structured output as a JSON string, and the generated tool description tells the supervisor model to expect that. If the child ends without submitting, or its submission fails the schema, the tool returns an `is_error` `tool_result` starting with `child_output_invalid:` — a normal tool error in the supervisor's loop, so the supervisor can retry or rephrase the task. The same applies on the runtime delegate path: a failed child surfaces as `child_output_invalid` carrying the original failure code and message. The structure is enforced by the harness rather than by prompt discipline, and unlike `outputFormat` it does not depend on provider `response_format`/`json_schema` support — any tool-capable provider works (see [Provider Compatibility](/reference/provider-compatibility.md)). ### Contract inheritance and assembly-time checks *Requires 0.21.0 or later.* Passing `AgentToolOptions.outputSchema` explicitly is still allowed, but it must match the target's declaration — compared by reference first, then by derived JSON Schema structure. A mismatch throws from `agentTool()` at assembly time; the error suggests sharing one schema instance between `createAgent`/`defineAgent` and `agentTool()`, or omitting the `agentTool()` copy to inherit it. Host-defined `AgentLike` adapters expose no readable declaration, so nothing is inherited or cross-checked for them — an explicit `outputSchema` still applies. One combination is pinned down since 0.23.0: when the **target declares no schema and `agentTool()` declares one explicitly**, assembly allows it (there is nothing to cross-check against), and an `ask` call validates the child's `structuredResult` against the parent-side schema and returns it as JSON. This fits children that end through a custom `endTurn` + `structuredResult` tool performing domain validation beyond the schema (for example reference truthfulness), with the contract declared by the parent alone. *Behavior change in 0.21.0:* where the child declares an `outputSchema` and the parent does not, the `ask` tool result changed from the fixed text `"Structured output submitted."` to the validated JSON. That is the intended fix and ships in a minor under 0.x. ### Structured result passthrough without a schema *Requires 0.23.0 or later.* When neither side declares an `outputSchema` and the child ends with a `structuredResult` (via a custom `endTurn` tool), the `ask` tool result is that payload's JSON as-is — unvalidated — instead of falling back to the text content and dropping it. The trust level is the same as the text result; the schema's job is validation only, not gating the structured channel. This applies on both the direct path and the team runtime delegate path. *Behavior change in 0.23.0:* existing code where the child ends with `endTurn` + `structuredResult` and the parent declares no schema now receives the structured JSON from `ask` instead of the text content. ## Typed delegation *Requires 0.21.0 or later.* `AgentToolOptions.inputSchema` replaces the default `{mode, task, expectedOutput, acceptanceCriteria, workspaceGrants}` input shape with your own schema, and `mapInput` projects the validated input into the child prompt: ```ts const judge = createAgent({ model: "deepseek-v4-flash", name: "judge", systemPrompt: "You judge a case and submit a structured verdict.", outputSchema: verdictSchema, }); const judgeTool = agentTool("judge", judge, { description: "Judge a case from its summary and documents.", inputSchema: z.object({ caseSummary: z.string(), documents: z.array(z.object({ title: z.string(), content: z.string() })), }), mapInput: input => renderJudgeTask(input.caseSummary, input.documents), }); ``` The parent's arguments are parsed against `inputSchema` before anything runs; invalid input is rejected as an error `tool_result` in the supervisor's loop — the same semantics as a plain `tool()` call — and the child is never invoked. `inputSchema` and `mapInput` must come as a pair: `agentTool()` throws at assembly time when one is missing. Typed delegation is ask-only — there is no `mode` field and no `workspaceGrants`. `mapInput` may return a string or `ContentBlock[]`, but `ContentBlock[]` is only supported for direct `ask` calls; inside a team runtime the projected prompt must be a string, or the call fails at runtime. ## Workspaces Each child agent has its own workspace. Ask child agents to write durable reports, logs, generated files, or code under their workspace and mention important paths in their final text. `workspace` is optional. When omitted, the SDK assigns a default workspace keyed by the agent's `name` at `~/.agent/workspaces//` (`agent-` if there's no `name`) and creates it on first run. **The default is fine for most cases** — it's already isolated per agent and lives outside your repo, so it won't clutter the project. Pass `workspace` explicitly only when you want artifacts in a specific location (e.g. a directory inside the repo), as below: ```ts const reviewer = createAgent({ model: "deepseek-v4-flash", apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", systemPrompt: "Review the patch and write notes under your workspace.", workspace: ".agent-workspaces/reviewer", }); ``` ## When to use a mailbox team instead In this pattern the child's deliverable is just a natural-language `tool_result` (what it did, where artifacts live under its workspace, how to verify) — no durable state, message queue, or structured-event protocol. Use a [mailbox team](/concepts/mailbox-team.md) when the work needs to **stay in a team**: durable status, named member inboxes, replies, follow-ups, persistence, or nested teams. In one line: **supervisor delegation** is "call this helper now and get a result"; a **mailbox team** is "put the task in the mailbox and let members complete it through the message flow," emitting `team_message` / `team_agent` structured events for tracing and UI along the way. --- # Mailbox team > Build persistent teams with lead agents, member inboxes, replies, and resumable work. Mailbox teams are for work that should stay inside a team until it is completed. The lead drops a task into a member inbox; the member claims the message, does the work, and sends the final result or progress back to the lead. That task state stays in the mailbox — ready to be claimed, answered, or followed up on — until it is marked done. A team is not a new kind of agent — it still satisfies the `AgentLike` interface (anything with `prompt()` / `query()`). Calling a team means talking to the lead agent; behind that lead, member tools and the mailbox runtime store message state and keep accepted handoffs moving. ## Create a team Use `createTeam()` when you want to talk to a lead directly while that lead delegates work to a handful of named members (e.g. `researcher`, `reviewer`) behind the scenes. ```ts import { createAgent, createMemoryMailbox, createTeam, teamMember, } from "agent-lattice"; const researcher = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", systemPrompt: "You research agent SDK architecture and report concise findings.", workspace: ".agent-workspaces/researcher", }); const team = createTeam({ name: "engineering", lead: createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", systemPrompt: "You lead engineering work. Delegate research tasks to researcher.", workspace: ".agent-workspaces/engineering-lead", }), members: [ teamMember({ name: "researcher", role: "executor", focus: "Research agent architecture", agent: researcher, }), ], mailbox: createMemoryMailbox(), }); const result = await team.prompt("Ask the researcher to inspect the SDK design."); console.log(result.result); ``` `team.query()` streams the lead's normal SDK messages plus team runtime events such as `team_message`, `team_agent`, and nested `agent_message` events. `team.prompt()` consumes that stream and returns only the root lead's final result. ## Modes for calling a member `createTeam()` injects member `AgentLike` tools into the lead. The lead picks an explicit mode each time it calls a member: | Mode | Meaning | | --- | --- | | `ask` | Ask the member and wait for the final answer in the current tool call. | | `handoff` | Assign work and get an acceptance receipt as the tool result. In `team.query()` or `team.prompt()`, the runtime continues the accepted mailbox work, waits for the member's upstream reply, resumes the lead, and returns the root final result. | | `observe` | Start observable long-running work if the host runtime supports it. | The receipt from `handoff` is not a final delivery. It is only proof that the work was accepted into the mailbox. For compatibility, the receipt keeps `status: "accepted"`; `phase: "queued"` and `completion_pending: true` make the current state explicit. It also includes `message_id`, `work_item_id`, and `thread_id`. After the lead queues every handoff in the current tool batch, the SDK stops calling the lead model. The TeamRunner runs those members first. When every member has completed or failed, the SDK passes their reports to the lead and calls the lead model again. The lead therefore cannot start another model turn and poll dependent tools while those handoffs are still pending. ## Controlled handoff concurrency Handoff work is serial by default. Set `runner.maxConcurrentWorkItems` when independent members should run at the same time: ```ts const team = createTeam({ name: "research", lead, members, runner: { maxConcurrentWorkItems: 4 }, }); ``` Advanced callers can pass the same option directly to `createTeamRunner()`. The value must be a positive integer and defaults to `1`. The limit applies across different member mailboxes. Multiple work items addressed to the same mailbox remain serial because an Agent may keep mutable conversation state. Runtime events follow actual start and completion order; the mailbox reports passed back to the lead stay in the original handoff order and retain their message, thread, and work-item identifiers. ## Handoff failure isolation Accepted handoffs are isolated work items by default. If one member returns an agent error such as `MaxTurnsError` or `APIError`, the runtime: 1. marks that member's work item `failed`; 2. emits a `team_message` event with `subtype: "failed"` and a normalized error; 3. sends an upstream failure report to the lead with the same error in `message.metadata`; 4. continues the other accepted handoffs; and 5. resumes the lead with both successful and failed reports. The lead then decides whether to retry, revise the task, accept a partial result, or finish. `team.prompt()` does not reject only because one member work item failed. A run-wide abort is different: `AbortError` stops the runner. Before propagating the abort, the runtime marks the current and remaining accepted work `cancelled`, so accepted work does not silently remain `pending`. ## Mailbox tools for members Member agents that can accept tools receive mailbox tools for assigned work: | Tool | Purpose | | --- | --- | | `team_send` | Send an asynchronous message to another member. | | `team_inbox` | List mailbox messages. | | `team_read` | Read and claim a message. | | `team_reply` | Send a final reply and close the original message. | | `team_followup` | Send progress without closing the original message. | | `team_status` | Summarize mailbox status counts. | The lead does not receive raw mailbox tools by default. Use `exposeLeadMailboxTools: true` only when the lead should manually operate the team mailbox. ## Workspaces Workspaces belong to agents, not teams or member definitions. Every agent gets a default workspace under `~/.agent/workspaces`; pass `workspace` only when the host wants to override that location. If a member creates files, code, logs, or other durable deliverables, it should write them in its own workspace and report the relevant paths in natural-language `team_reply` or `team_followup` text. ## Durable mailbox storage The default `createMemoryMailbox()` adapter is in-memory. For durable local storage, use `createSQLiteMailbox()` with a SQLite-like database: ```ts import Database from "better-sqlite3"; import { createAgent, createSQLiteMailbox, createTeam, } from "agent-lattice"; const mailbox = createSQLiteMailbox({ database: new Database("team-mailbox.db"), }); const team = createTeam({ name: "engineering", lead: createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", }), members: [], mailbox, }); ``` The SDK does not install a SQLite driver for you. Hosts can choose `better-sqlite3`, Cloudflare D1 wrappers, Durable Objects, Redis, or another compatible `TeamMailbox` adapter. The SDK ships only two mailbox factories: `createMemoryMailbox()` (in-memory, the default) and `createSQLiteMailbox()` (SQLite). For D1, Durable Objects, Redis, or any other backend, implement the `TeamMailbox` interface yourself — it is just five message-centric methods. Don't be put off by the states: **the team runtime drives the state transitions; your adapter only persists them**. The message state machine: ```text send() writes -> pending --claimNext()--> processing --updateStatus()--> done | failed (also: cancelled / read) ``` How a handoff actually plays out: ```text 1. lead --handoff--> runtime --send()--> adapter (msg = pending) 2. runtime --claimNext()--> adapter (pending -> processing), handed to member 3. member --team_reply--> runtime --updateStatus()--> adapter (-> done) ``` > Progress-only uses `team_followup` (original stays open); if member ends with no reply -> `failed` + a diagnostic message. What you implement are the five storage methods behind that flow. Here is a minimal, runnable in-memory implementation (real logic for every method); to target Redis / D1 / Durable Objects, swap the storage calls and make `claimNext` atomic under concurrency: ```ts import type { TeamMailbox, TeamMessage } from "agent-lattice"; const byId = new Map(); const order: string[] = []; // ids in send order, for FIFO claiming let seq = 0; const myMailbox: TeamMailbox = { async send(from, to, content, options = {}) { const id = `msg_${++seq}`; const msg: TeamMessage = { id, from, to, content, status: "pending", createdAt: Date.now(), threadId: options.threadId ?? id, ...options, }; byId.set(id, msg); order.push(id); return { ...msg }; }, async inbox(mailboxId, options = {}) { const status = options.status ?? "pending"; return order .map(id => byId.get(id)!) .filter(m => m.to === mailboxId && (status === "all" || m.status === status)) .map(m => ({ ...m })); }, async get(messageId) { const m = byId.get(messageId); return m ? { ...m } : undefined; }, async claimNext(mailboxId) { for (const id of order) { const m = byId.get(id); if (m && m.to === mailboxId && m.status === "pending") { m.status = "processing"; // safe in a single process; use a Lua script / transaction on a distributed backend return { ...m }; } } return undefined; }, async updateStatus(messageId, status) { const m = byId.get(messageId); if (!m) return false; m.status = status; return true; }, }; const team = createTeam({ /* name, lead, members, … */ mailbox: myMailbox }); ``` > The SDK's built-in `createMemoryMailbox()` is the production version of exactly this; the snippet above just strips it down so you can adapt it to your own backend. Two things matter: `claimNext` must be atomic on a distributed backend (several members may claim the same mailbox concurrently) — it flips a message from `pending` to `processing`; `updateStatus` moves it to a terminal state (`done` / `failed` / `cancelled` / `read`), and the runtime relies on it to advance handoffs. ## Nested teams Because `teamMember().agent` accepts any `AgentLike`, a team wrapper can be a member of another team wrapper. ```ts const engineeringTeam = createTeam({ name: "engineering", lead: engineeringHeadAgent, members: [ teamMember({ name: "backend", role: "executor", agent: backendAgent }), teamMember({ name: "frontend", role: "executor", agent: frontendAgent }), ], }); const companyTeam = createTeam({ name: "company", lead: ceoAgent, members: [ teamMember({ name: "engineering", role: "head", focus: "Own engineering delivery", agent: engineeringTeam, }), ], }); ``` The CEO sees `engineeringTeam` as one member. Internally, that member is still a callable wrapper around the engineering lead, and the engineering lead can route work to backend or frontend. ## Manual draining `team.prompt()` and `team.query()` are the default path. They drive accepted handoffs until the root lead returns a final result. Use lower-level mailbox controls only when you need explicit ownership over routing, persistence, or worker loops: ```ts const message = await team.mailbox.claimNext("engineering::researcher"); const result = await team.drain({ maxRounds: 5, maxMessages: 20, }); ``` `drain()` iterates members, claims messages from each member's own mailbox, and prompts that member agent. It does not choose a different member or reroute work. Each member must close the loop with `team_reply` for a final result or `team_followup` for progress. If a member ends without either tool, `drain()` marks the original message `failed` and sends a diagnostic follow-up to the upstream mailbox. --- # Built-in Tools > Agent workspace tools and manual workspace tool construction. `createAgent()` gives each agent private workspace tools by default. If no `workspace` is provided, files are stored under `~/.agent/workspaces/` when you pass `name`, otherwise under `~/.agent/workspaces/agent-`. Pass `workspace` only to override that default location: ```ts import { createAgent } from "agent-lattice"; const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", name: "backend", model: "deepseek-v4-flash", workspace: { cwd: process.cwd(), allowedDirectories: [process.cwd()], bashTimeoutMs: 30_000, }, }); ``` `createBareAgent()` starts with no tools, no workspace instructions, and no default workspace directory. Use `createBuiltinTools()` when you want a bare agent but still want to opt into the SDK's built-in file and shell tools: ```ts import { createBareAgent, createBuiltinTools } from "agent-lattice"; const agent = createBareAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", systemPrompt: "Use the configured project directory for file work.", tools: createBuiltinTools({ cwd: process.cwd(), allowedDirectories: [process.cwd()], bashTimeoutMs: 30_000, }), }); ``` `createBuiltinTools()` and `createAgentWorkspaceTools()` return the same `ToolDefinition[]` shown below. `createAgentWorkspaceTools()` remains available for code that prefers the explicit workspace name. ## Included tools | Tool | Capability | | --- | --- | | `Read` | Read a workspace file, optionally with 1-based line offset and limit. | | `Write` | Write a file and create parent directories. | | `Edit` | Replace text in an existing file. | | `LS` | List a directory. | | `Glob` | Find files by glob pattern. | | `Grep` | Search file contents by regular expression. | | `Bash` | Run a shell command with a timeout. | ## Security boundary These tools can read and write files and run shell commands, so in production pair them with a `permission` callback: ```ts permission: async request => { if (request.toolName === "Bash" || request.toolName === "Write") { return { behavior: "deny", message: "This host did not approve shell or write access.", }; } return { behavior: "allow" }; } ``` `allowedDirectories` restricts write roots for `Write`, `Edit`, and obvious shell writes. Read-only tools can inspect any path the host process can read. These checks do not replace the host application's own permission policy. --- # Permissions > Intercept tool calls before execution and apply host policy. Permission callbacks let the host application decide whether a tool call should run. Use them for file writes, shell commands, external APIs, payments, deletion, or any operation where the host owns the policy. The callback runs after the model asks to call a registered tool and before the tool input is parsed or the tool handler executes. Return `{ behavior: "allow" }` to run the tool, or return `{ behavior: "deny", message }` to block it. ```ts import { createAgent } from "agent-lattice"; const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", workspace: { cwd: process.cwd(), allowedDirectories: [process.cwd()], }, permission: async request => { if (request.toolName === "Bash") { return { behavior: "deny", message: "Shell access is disabled for this session.", }; } return { behavior: "allow" }; }, }); ``` ## Allowing Tools If you do not pass a `permission` callback, every registered tool call is allowed. When you do pass one, returning `{ behavior: "allow" }` approves the current tool call. The example above is a deny-list policy: it blocks `Bash`, then allows everything else with the final `return { behavior: "allow" }`. For an allow-list policy, invert the condition: ```ts import type { AgentOptions } from "agent-lattice"; const readOnlyTools = new Set(["Read", "LS", "Glob", "Grep"]); const permission: AgentOptions["permission"] = async request => { if (readOnlyTools.has(request.toolName)) { return { behavior: "allow" }; } return { behavior: "deny", message: "Only read-only tools are allowed for this session.", }; }; ``` ## Deny Is Not A Crash Denied tools do not throw by default. The SDK sends the model a `tool_result` with `is_error: true` and the denial message as its content. That lets the model: - Explain that the operation was blocked by host policy. - Try another allowed tool. - Ask the user for a different path or additional approval. ## Request Shape The permission callback receives: | Field | Meaning | | --- | --- | | `toolName` | The registered tool name the model wants to call. | | `input` | The raw tool input from the model. Treat it as untrusted data. | | `toolUseId` | The id for this tool call. | ## Input-Aware Policies You can inspect `request.input` before allowing a tool. Keep the policy in your application code; the SDK only calls it. ```ts import type { AgentOptions } from "agent-lattice"; const permission: AgentOptions["permission"] = async request => { if (request.toolName === "Bash") { const command = String(request.input.command ?? ""); if (command.includes("rm -rf")) { return { behavior: "deny", message: "Destructive shell commands are disabled for this session.", }; } } return { behavior: "allow" }; }; ``` ## Boundaries Permissions are an approval layer, not a full sandbox. They do not rewrite model output, hide prompt content, or validate every custom tool argument for you. For built-in file tools, combine permissions with `allowedDirectories` so file access stays inside the workspace you configured. For `Bash` and custom tools that call external systems, enforce the real safety boundary in the host environment as well: expose only the tools you need, validate inputs, use scoped credentials, and redact sensitive values before logging. --- # Public API > Stable exports exposed by the SDK package. ## Functions | Export | Description | | --- | --- | | `createAgent(options)` | Creates an in-memory `Agent` with a private workspace and default workspace tools. | | `createBareAgent(options)` | Creates an in-memory `Agent` without default tools, workspace instructions, or workspace directory creation. | | `defineAgent(options)` | Creates an `AgentSpec` template from agent options. The spec is stateless; `spawn()` creates an independent session with its own history and workspace. | | `isAgentSpec(target)` | Type guard distinguishing an `AgentSpec` template from a live `AgentLike` session. | | `query(params)` | One-shot async generator helper. | | `tool(name, description, inputSchema, handler)` | Creates a custom tool definition. | | `agentTool(name, target, metadata)` | Exposes another `AgentLike` or `AgentSpec` as a model tool with explicit `ask`, `handoff`, or `observe` modes. A spec target spawns a fresh session per call; a session target keeps its history across calls. With `inputSchema` + `mapInput` (requires 0.21.0) the tool takes a custom typed input shape instead (ask-only) and the return type is `ToolDefinition`. | | `delegateTool(name, description, agent, options)` | Advanced helper that exposes an `AgentLike` or `AgentSpec` as a mailbox-backed delegate tool. | | `skill(options)` | Creates an in-memory skill definition. | | `loadSkill(path)` | Loads a skill from a directory containing `SKILL.md`. | | `createMCPTools(client, options)` | Maps an MCP client into SDK tool definitions. | | `connectMCPStdioServer(server, options)` | Connects a stdio MCP server and returns SDK tools plus a close handle. | | `connectMCPStreamableHTTPServer(url, options)` | Connects a remote Streamable HTTP MCP server, with optional OAuth support. | | `createJsonlContextTracer(options)` | Creates a JSONL trace sink for agent run context events. | | `createLangSmithContextTracer(options)` | Creates a LangSmith trace sink from the same provider-agnostic context events. | | `createLangfuseContextTracer(options)` | Creates a Langfuse trace sink (built on `@langfuse/tracing` v5) from the same provider-agnostic context events. | | `createCompositeContextTracer(tracers)` | Fans context trace events out to multiple trace sinks. | | `defineContextTracer(impl)` | Creates a custom trace sink from an implementation object, validating `onEvent` and binding `failOnError` at creation time. | | `createCompositeAgentHooks(hooks)` | Chains hooks in array order; each receives the previous one's output. | | `createJsonlHistoryStore(options)` | Creates a JSONL history store that persists an Agent's conversation as one message per line. | | `defineHistoryStore(impl)` | Creates a custom history store from an implementation object, validating `load`/`append`/`replace` and binding `failOnError` at creation time. | | `DEFAULT_COMPACTION_PROMPT` | The built-in summarization instruction used by `autoCompact`. | | `SUBMIT_OUTPUT_TOOL_NAME` | Name of the built-in tool (`"submit_output"`) injected when `AgentOptions.outputSchema` is set. The name is reserved in that mode. | | `teamMember(options)` | Creates a named team member with role, focus, and agent. | | `createTeam(options)` | Creates a callable team wrapper around a lead, members, mailbox, and built-in runtime. After the lead queues a handoff batch, members run before the lead model is called again. Set `runner.maxConcurrentWorkItems` for bounded cross-member concurrency; the default is `1`. | | `createTeamRunner(options)` | Advanced runtime for manually running a root `AgentLike` with mailbox-backed delegate tools. Accepts `maxConcurrentWorkItems`; member failures stay isolated and a run-wide abort cancels remaining work. | | `createMemoryMailbox()` | Creates the default in-memory team mailbox adapter. | | `createSQLiteMailbox(options)` | Creates a durable team mailbox adapter from a SQLite-like database. | | `createBuiltinTools(options)` | Creates the built-in file and shell tools for manual tool assembly. | | `createAgentWorkspaceTools(options)` | Creates opt-in file and shell workspace tools for agents. | ## Agent `Agent` is a type-only export (breaking in 0.17.0: the constructor is no longer exported). Create instances with `createAgent()`, `createBareAgent()`, or `AgentSpec.spawn()` — the factories also generate the session id and install the workspace. ```ts const agent = createAgent(options); for await (const message of agent.query(prompt)) { console.log(message); } const result = await agent.prompt(prompt); // A deep copy of the conversation history, including messages seeded from // AgentOptions.historyStore. Mutating it cannot corrupt the live history. const history = await agent.getHistory(); // Replace the whole conversation history. Idle only (throws // ConcurrentQueryError while a query runs); a configured historyStore is // replaced too, so persistence stays in sync. The SDK does not validate the // content — the host owns it. await agent.replaceHistory(messages); // End the running query with subtype "interrupted", keeping completed turns // in history so a follow-up query can continue the conversation. Returns // true when a query was interrupted, false when idle. agent.interrupt(); // The structured output contract declared via AgentOptions.outputSchema, // if any. Read-only; used by agentTool() for contract inheritance. // Requires 0.21.0 or later. agent.outputSchema; ``` `interrupt()` is the cooperative counterpart of `QueryOptions.signal`: the signal terminates the query with `error_abort`, while an interrupt finishes it with `interrupted` (not an error) after dropping only the in-flight turn. It returns `false` when no query is running, so the host knows it can send its next query directly. Use `createAgent()` when the agent should have a private workspace for durable files and test evidence. Use `createBareAgent()` when the host wants a plain model loop and will pass every system prompt and tool explicitly: ```ts import { createBareAgent, createBuiltinTools } from "agent-lattice"; const agent = createBareAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", systemPrompt: "You are a concise engineering assistant.", tools: createBuiltinTools({ cwd: process.cwd(), allowedDirectories: [process.cwd()], }), }); ``` ### AgentOptions | Option | Type | Description | | --- | --- | --- | | `apiKey` | `string` | Provider API key. | | `baseURL` | `string` | Optional Anthropic-compatible endpoint, such as DeepSeek. | | `name` | `string` | Optional source name used in context trace events. | | `model` | `string` | Model name. | | `systemPrompt` | `string` | Stable role and responsibility instructions sent as the provider system prompt. | | `maxTokens` | `number` | Maximum output tokens per model request. | | `maxTurns` | `number` | Maximum agent loop turns before returning `error_max_turns`. | | `thinkingConfig` | `ThinkingConfig` | Default reasoning configuration. Accepts `{ type: "adaptive" }`, `{ type: "enabled", budgetTokens }`, or `{ type: "disabled" }`. `disabled` is sent to the provider explicitly (as `thinking: { "type": "disabled" }`) because some Anthropic-compatible providers, such as DeepSeek, default thinking to on. When omitted, the SDK sends no thinking configuration. See [Provider Compatibility](../provider-compatibility/) for what each provider actually honors. | | `reasoningEffort` | `ReasoningEffort` | Default provider reasoning effort. Accepts `"low"`, `"high"`, or `"max"` and is sent as `reasoning_effort` (a Kimi convention — ignored by DeepSeek; see [Provider Compatibility](../provider-compatibility/)). When omitted, the provider applies its default. | | `requestTimeoutMs` | `number` | Deadline for each single model request, in milliseconds. Enforced by the SDK even when a model client ignores it. Unset means no SDK-side limit. | | `outputSchema` | `OutputSchema` | Requires the run to end with a validated structured payload. The SDK injects a built-in `submit_output` tool; a valid submission ends the run with `subtype: "success"` and the payload on `SDKResultMessage.structuredResult`, while ending the turn without submitting fails with `error_missing_output` (`MissingOutputError`). Unlike `QueryOptions.outputFormat`, it only requires a tool-capable provider. | | `tools` | `ToolDefinition[]` | Custom tools available to the model. | | `toolBatchPolicy` | `ToolBatchPolicy` | Optional pre-execution hook that accepts or rejects the complete tool-call batch before any handler runs. | | `hooks` | `AgentHooks` | Lifecycle callbacks that rewrite tool results and outgoing model requests. Not inherited by delegated agents. | | `autoCompact` | `boolean \| AutoCompactOptions` | Replaces older history with a model-written summary past a token threshold. Off unless set; `true` uses the defaults. | | `toolConcurrency` | `ToolConcurrencyOptions` | Tool-call scheduling. Defaults to `{ mode: "safe", maxConcurrency: 10 }`; undeclared tools stay sequential. | | `skills` | `SkillDefinition[]` | Reusable instruction bundles selected per query. | | `workspace` | `string \| false \| { cwd: string; allowedDirectories?: string[]; bashTimeoutMs?: number }` | Optional override for the private agent workspace. Defaults to `~/.agent/workspaces/` when `name` is set, otherwise `~/.agent/workspaces/agent-`. `false` (requires 0.23.0) opts out of the built-in workspace entirely — no built-in file/shell tools, no workspace prompt section — equivalent to `createBareAgent()` and also effective through `defineAgent()`. | | `permission` | `function` | Callback that can allow or deny tool execution. | | `modelClient` | `ModelClient` | Custom model client for tests or alternate providers. | | `tracer` | `ContextTracer` | Optional trace sink for agent context events. | | `historyStore` | `HistoryStore` | Optional persistence adapter for the conversation history. Loaded once before the first query; `append()` follows every history write and `replace()` follows compaction. | ### BareAgentOptions `createBareAgent()` accepts `BareAgentOptions`, which is `AgentOptions` without `workspace`. It does not add workspace instructions, create a default workspace directory, or register built-in tools unless you pass them in `tools`. ### QueryOptions | Option | Type | Description | | --- | --- | --- | | `stream` | `boolean` | Enables raw provider stream events. Defaults to `true`. | | `outputFormat` | `OutputFormat` | Requests structured output via `"json"` or JSON Schema output via `{ type: "json_schema", schema }`; the final text is returned unchanged. | | `thinkingConfig` | `ThinkingConfig` | Overrides the agent's reasoning configuration for this query. A fixed budget is capped at `maxTokens - 1`. | | `reasoningEffort` | `ReasoningEffort` | Overrides the agent's provider reasoning effort for this query. | | `requestTimeoutMs` | `number` | Overrides the agent's per-request deadline for this query. | | `signal` | `AbortSignal` | Cancels the running query, ending it with subtype `error_abort`. Compare `Agent.interrupt()`, which ends it with `interrupted` and keeps the conversation. | | `runtime` | `AgentRuntimeContext` | Runtime context supplied by team runners and delegate tools. | | `tracer` | `ContextTracer` | Query-scoped trace sink. Overrides or supplies tracing for this query. | ## Context tracing ```ts import { createAgent, createCompositeContextTracer, createJsonlContextTracer, createLangSmithContextTracer, } from "agent-lattice"; const tracer = createCompositeContextTracer([ createJsonlContextTracer({ path: ".agent-runs/session.jsonl" }), createLangSmithContextTracer({ projectName: process.env.LANGSMITH_PROJECT, workspaceId: process.env.LANGSMITH_WORKSPACE_ID, }), ]); const agent = createAgent({ model: "deepseek-v4-flash", apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", tracer, }); try { await agent.prompt("Trace this run.", { stream: false }); } finally { await tracer.close?.(); } ``` `createJsonlContextTracer()` accepts either `path` for one explicit file or `dir` for one file per `session_id`. It also accepts `redact(event)` for local filtering before persistence and `failOnError` for hosts that want trace write failures to fail the agent run. `createLangSmithContextTracer()` uses the bundled langsmith `RunTree` by default (since 0.17.0); pass a LangSmith-compatible `RunTree` constructor or `runTree(config)` factory only to inject a custom runtime or a test fake. The SDK depends on `langsmith` directly and uses the official `RunTree` / `RunTreeConfig` types from `langsmith/run_trees`. Configure LangSmith with its standard environment variables: `LANGSMITH_TRACING`, `LANGSMITH_ENDPOINT`, `LANGSMITH_API_KEY`, and `LANGSMITH_PROJECT`. `LANGSMITH_WORKSPACE_ID` is required only for org-scoped or multi-workspace API keys. You can pass `apiKey`, `apiUrl`, and `workspaceId` directly to `createLangSmithContextTracer()`; `workspaceId` is only for selecting a LangSmith workspace and is not the project name. If you need full LangSmith client customization, pass an explicit `Client` as `client`. In short-lived tests or scripts, close or flush the tracer in `finally` before the process exits so LangSmith receives the final root run patch. `createLangfuseContextTracer()` (requires 0.19.0) maps the same events onto Langfuse observations through the current `@langfuse/tracing` (v5) SDK: a `chain` observation per agent run, a `generation` per model turn, a `tool` per tool call, and auto-ended `event` observations for auxiliary events. The Langfuse SDK is OpenTelemetry-based — register a `LangfuseSpanProcessor` (from `@langfuse/otel`) with a tracer provider at process startup and configure the standard `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` / `LANGFUSE_BASE_URL` environment variables. Pass the registered processor as `spanProcessor` so `flush()`/`close()` drains pending spans before a short-lived process exits. `startObservation` defaults to the bundled `@langfuse/tracing` function; pass it only to inject a custom runtime or a test fake. When the tracer is passed to `team.query()` or `team.prompt()`, one Team call creates one root chain. Lead runs, delegated Member runs, and later Lead runs share its trace session and appear as child chains. Their original Agent session IDs remain available as `agent_session_id` metadata. ## Feature availability Everything below works on the version shown and later. Before 0.22.0 the SDK ignored unknown options rather than rejecting them, so calling a newer API on an older install succeeded with the feature silently absent. Since 0.22.0 the main factories (`createAgent()`/`createBareAgent()`/`defineAgent()`, `agentTool()`, `delegateTool()`, `tool()`) throw on unknown option keys, but other options objects still ignore them — check the installed version rather than relying on an error. | Feature | Requires | | --- | --- | | `toolConcurrency`, `isConcurrencySafe`, `toolBatchPolicy`, `reasoningEffort` | 0.9.25 | | `SDKResultMessage.usage`, `stop_reason`, `TokenUsage`, `ConcurrentQueryError` | 0.10.0 | | `requestTimeoutMs`, `TimeoutError`, result subtype `error_timeout` | 0.11.0 | | `hooks`, `AgentHooks`, `createCompositeAgentHooks` | 0.12.0 | | `autoCompact`, `DEFAULT_COMPACTION_PROMPT`, `SDKSystemCompactionMessage` | 0.13.0 | | Compaction recovery after an overflow, `model_context_window_exceeded` | 0.14.0 | | `defineAgent()`, `AgentSpec`, `AgentToolTarget`, `isAgentSpec()`, `AgentSpec` targets for `agentTool()`/`delegateTool()` | 0.15.0 | | `AssistantModelMessage.providerResponseId`, `AssistantModelMessage.model`, `ToolResult.endTurn`, `Agent.interrupt()` (also on the `AgentLike` interface — breaking for custom implementers), result subtype `interrupted`, `AgentOptions.historyStore`, `HistoryStore`, `createJsonlHistoryStore`, `Agent.getHistory()` | 0.16.0 | | `Agent.replaceHistory()`, `AgentLike.interrupt()` returns `boolean` (breaking for custom implementers), `defineContextTracer()`, `defineHistoryStore()` (breaking: `failOnError` removed from the `ContextTracer`/`HistoryStore` port types — custom implementations must use the `defineXxx` factories), `Agent` becomes a type-only export (breaking: `new Agent()` no longer available), `createLangSmithContextTracer()` defaults to the bundled `RunTree`, `tool_use` trace events carry the tool `description` | 0.17.0 | | `thinkingConfig: { type: "disabled" }` is sent to the provider explicitly as `thinking: { "type": "disabled" }` instead of being omitted (fixes providers that default thinking to on, e.g. DeepSeek) | 0.17.1 | | Tool calls in a response truncated at `max_tokens` are not executed; each call gets an error `tool_result` asking the model to reissue it with shorter output | 0.18.0 | | `createLangfuseContextTracer()`, `LangfuseContextTracerOptions`, `LangfuseObservationLike`, `LangfuseChainLike`, `LangfuseGenerationLike`, `LangfuseToolLike`, `LangfuseStartObservation`, `LangfuseFlushableSpanProcessor`, `LangfuseKVMap` | 0.19.0 | | `AgentOptions.outputSchema`, `OutputSchema`, `SUBMIT_OUTPUT_TOOL_NAME`, `MissingOutputError`, `SDKResultMessage.structuredResult`, result subtype `error_missing_output`, `AgentRuntimeFailure` code `missing_output`, `ToolResult.structuredResult`, `AgentToolOptions.outputSchema` | 0.20.0 | | `AgentToolOptions.inputSchema` + `mapInput` (typed ask-only delegation), `agentTool()` inherits `outputSchema` from the target and cross-checks explicit copies at assembly time, `Agent.outputSchema` getter, `agentTool()` returns `ToolDefinition` (behavior change: `ask` with an inherited schema now returns the validated JSON instead of the fixed `"Structured output submitted."` text) | 0.21.0 | | Strict option validation: `createAgent()`/`createBareAgent()`/`defineAgent()` (`AgentOptions`), `agentTool()` (`AgentToolOptions`), `delegateTool()` (`DelegateToolOptions`), and `tool()` (`ToolOptions`) throw on unknown option keys at assembly time (behavior change: extra keys that used to be silently ignored, e.g. spread host fields, now throw). `ToolOptions.metadata` / `AgentToolOptions.metadata` pass through to `ToolDefinition.metadata` (host-owned, never shown to the model) | 0.22.0 | | `AgentOptions.workspace: false` (bare mode, also via `defineAgent()`), `agentTool()` contract pinned: explicit `outputSchema` with no target declaration validates the child's `structuredResult`, and with no schema on either side `ask` passes the child's `structuredResult` through as unvalidated JSON (behavior change: previously the `ask` result fell back to the text content and dropped it) | 0.23.0 | Read the installed version from `node_modules/agent-lattice/package.json`; the package does not export it as an importable path. ## Types | Export | Description | | --- | --- | | `SDKMessage` | Union of all emitted SDK events. | | `AgentLikeEvent` | Union emitted by `AgentLike.query()`, including plain SDK messages and team runtime events. | | `AgentLike` | Minimal callable interface implemented by `Agent` and the callable `Team` wrapper: `query()`, `prompt()`, and `interrupt()` (the latter requires 0.16.0; its `boolean` return requires 0.17.0). | | `AgentSpec` | Agent template created by `defineAgent()`: a stateless identity (name, model, prompt, tools, workspace policy) plus `spawn()` for creating independent sessions. | | `AgentToolTarget` | Union accepted by `agentTool()` and `delegateTool()`: a live `AgentLike` session or an `AgentSpec` template. | | `AgentOptions` | Agent construction options. | | `BareAgentOptions` | Construction options for `createBareAgent()`, equivalent to `AgentOptions` without `workspace`. | | `AgentWorkspaceOptions` | String path or workspace configuration object accepted by `AgentOptions.workspace` to override the default workspace. | | `AgentWorkspaceToolsOptions` | `cwd`, write-root, and shell timeout options accepted by `createBuiltinTools()` and `createAgentWorkspaceTools()`. | | `QueryOptions` | Per-query options for streaming, cancellation, runtime, and tracing. | | `OutputFormat` | Structured output format accepted by `QueryOptions.outputFormat`: `"json"` or `{ type: "json_schema", schema }`. | | `OutputSchema` | Validation shape accepted by `AgentOptions.outputSchema` and `AgentToolOptions.outputSchema`: `{ parse(input: unknown): T }`. A zod schema satisfies it directly (requires 0.20.0). | | `ThinkingConfig` | Reasoning configuration accepted by `AgentOptions.thinkingConfig` and `QueryOptions.thinkingConfig`. | | `ReasoningEffort` | Provider reasoning effort accepted by `AgentOptions.reasoningEffort` and `QueryOptions.reasoningEffort`: `"low"`, `"high"`, or `"max"`. | | `TokenUsage` | Token counts reported on `SDKResultMessage.usage` and `AssistantModelMessage.usage`. | | `AssistantModelMessage` | One assistant turn returned by a `ModelClient`: `content`, optional `usage` and `stopReason`, plus optional provider metadata `providerResponseId` (the provider-assigned response id) and `model` (the model that actually served the response, which may differ from the requested one). | | `StopReason` | Why the model stopped. `"max_tokens"` means the output was truncated; `"model_context_window_exceeded"` means the context window ran out and is what `autoCompact` recovers from. | | `AgentRuntimeFailure` | Normalized member failure stored in failure events and upstream report metadata. | | `TeamRunnerConfig` | Team runtime limits: `maxDelegateDepth` and `maxConcurrentWorkItems`. Concurrency defaults to `1`, and the same target mailbox remains serial. | | `ContextTracer` | Trace sink interface with `onEvent`, optional `flush`, and optional `close`. Methods only — `failOnError` is bound by the creating factory (`createXxxContextTracer` options or `defineContextTracer()`). | | `ContextTraceEvent` | Structured trace event envelope emitted by agents. | | `ContextTraceEventType` | Union of trace event type strings. | | `JsonlContextTracerOptions` | Options for the built-in JSONL tracer. | | `LangSmithContextTracerOptions` | Options for the LangSmith tracer adapter. | | `LangfuseContextTracerOptions` | Options for the Langfuse tracer adapter (requires 0.19.0). | | `LangfuseObservationLike` | Alias for the `@langfuse/tracing` `LangfuseObservation` union; `LangfuseChainLike`, `LangfuseGenerationLike`, and `LangfuseToolLike` alias the concrete observation classes (requires 0.19.0). | | `LangfuseStartObservation` | Alias for the `@langfuse/tracing` `startObservation` function type, accepted by `LangfuseContextTracerOptions.startObservation` (requires 0.19.0). | | `LangfuseFlushableSpanProcessor` | Minimal `forceFlush()` shape a `LangfuseSpanProcessor` satisfies, accepted by `LangfuseContextTracerOptions.spanProcessor` (requires 0.19.0). | | `HistoryStore` | Persistence adapter interface for an Agent's conversation history: `load()`, `append(message)`, `replace(messages)`. Compaction calls `replace()`, so stores must support full replacement. Methods only — `failOnError` is bound by the creating factory (`createJsonlHistoryStore` options or `defineHistoryStore()`). | | `JsonlHistoryStoreOptions` | Options for the built-in JSONL history store: `path` and optional `failOnError`. | | `LangSmithRunTreeLike` | Alias for LangSmith's official `RunTree` type. | | `LangSmithRunTreeConfig` | Alias for LangSmith's official `RunTreeConfig` type. | | `ToolDefinition` | Tool shape passed to an agent. Carries optional host-owned `metadata` passed through from `ToolOptions.metadata`/`AgentToolOptions.metadata` (requires 0.22.0); never shown to the model. | | `ToolResult` | Tool handler return value: `content` plus optional `endTurn`, which ends the run with `subtype: "success"` after the batch instead of calling the model again, and optional `structuredResult` (requires 0.20.0), a structured payload carried to `SDKResultMessage.structuredResult` when combined with `endTurn: true` and ignored otherwise. | | `ToolOptions` | Optional `tool()` settings, including the input-aware `isConcurrencySafe` check and host-owned `metadata` (requires 0.22.0) passed through to `ToolDefinition.metadata`. | | `ToolConcurrencyMode` | Tool scheduling mode: `"safe"`, `"all"`, or `"sequential"`. | | `ToolConcurrencyOptions` | Agent tool scheduling mode and positive `maxConcurrency` limit. | | `AutoCompactOptions` | Compaction settings: `thresholdTokens` (100000), `keepRecentMessages` (6), `prompt`, `model`, `maxTokens`. | | `SDKSystemCompactionMessage` | `system` message with `subtype: "compaction"`, emitted when history is summarized. Reports message counts and the summary's own usage. | | `AgentHooks` | Lifecycle callbacks accepted by `AgentOptions.hooks`: `onToolResult` and `onModelRequest`. A hook that throws propagates out of `query()`. | | `ToolResultHookContext` | `onToolResult` input: tool name, raw input, the result block, and the error when the call failed. | | `ModelRequestHookContext` | `onModelRequest` input: the messages and system prompt about to be sent, plus the turn number. | | `ModelRequestHookResult` | Replacement `messages` and/or `systemPrompt` for one request; the stored history is untouched. | | `ToolBatchPolicy` | Optional Agent policy that validates a complete tool-call batch before execution. | | `ToolBatchPolicyContext` | Policy input containing source, host context, signal, and tool calls with `tool` or `agent_tool` kind. | | `ToolBatchPolicyResult` | Allows a batch or rejects it with a code, message, conflicting call IDs, and suggested next step. | | `AgentToolInput` | Standard input shape for `agentTool()`. | | `AgentToolOptions` | Description and target mailbox options for `agentTool()`, plus `outputSchema` (requires 0.20.0): in `ask` mode the tool result is the child's validated structured output as a JSON string, and a missing or invalid child submission surfaces as a `child_output_invalid` tool error. Since 0.21.0 an omitted `outputSchema` is inherited from the target's own declaration (`spec.options.outputSchema` or the live Agent's `outputSchema` getter; host-defined `AgentLike` adapters carry no declaration, so nothing is inherited or cross-checked), and an explicit copy that does not match the target's declaration throws at assembly time. Also since 0.21.0, `inputSchema` + `mapInput` (required as a pair) enable typed delegation: the tool takes the custom schema as input — ask-only, no `mode`/`workspaceGrants` — validates the parent's arguments before the child runs, and projects the validated input into the child prompt. Also accepts host-owned `metadata` (requires 0.22.0) passed through to `ToolDefinition.metadata`; never shown to the model. Since 0.23.0 two more contract combinations are pinned: an explicit `outputSchema` with no target declaration is allowed and validates the child's `structuredResult`; and with no schema on either side, an `ask` result passes the child's `structuredResult` through as unvalidated JSON instead of falling back to the text content. | | `DelegateToolOptions` | Options for mailbox-backed AgentLike delegation. | | `SkillDefinition` | Reusable instruction bundle passed to an agent. | | `MCPClient` | Minimal MCP client interface used by `createMCPTools()`. | | `MCPStdioConnection` | Connected stdio MCP server with mapped tools. | | `MCPStreamableHTTPConnection` | Connected remote Streamable HTTP MCP server with auth/session helpers. | | `Team` | Callable wrapper around a lead with members, member AgentLike tools, and advanced mailbox controls. Its default `prompt()` result is the root lead's final answer, not a handoff receipt. | | `TeamRunner` | Advanced runtime that drives mailbox-backed AgentLike delegate tools, including accepted handoffs that need mailbox follow-up before final delivery. | | `TeamRunnerMessage` | Union of root SDK messages and team runner activity events. | | `TeamMemberDefinition` | Named team member with role and `AgentLike`. | | `TeamMailbox` | Storage adapter interface for team messages, including member-scoped `claimNext()`. | | `TeamMessage` | Message record with thread and work-item context. | | `TeamDrainOptions` | Limits and abort signal for `team.drain()`. | | `TeamDrainResult` | Processed, failed, and round counts from `team.drain()`. | | `SQLiteDatabaseLike` | Minimal database interface accepted by `createSQLiteMailbox()`. | | `SQLiteMailboxOptions` | SQLite mailbox adapter options. | | `PermissionDecision` | Allow or deny decision returned by a permission callback. | | `ModelClient` | Interface for custom or mocked model clients. | ## Errors | Error | Description | | --- | --- | | `APIError` | Provider request or response failure. | | `ToolExecutionError` | Tool handler failure. | | `MaxTurnsError` | Agent loop reached `maxTurns`. | | `AbortError` | Request was aborted. | | `TimeoutError` | A model request exceeded `requestTimeoutMs`; the result subtype is `error_timeout`. | | `ToolBatchRejectedError` | Tool batch was rejected before any tool executed. | | `ConcurrentQueryError` | A second query was started while the Agent was still running one. An Agent holds one conversation. | | `MissingOutputError` | A run with `AgentOptions.outputSchema` ended without the model calling `submit_output`; the result subtype is `error_missing_output`. | --- # Provider Compatibility > How SDK thinking, reasoning-effort, and structured-output options behave on Anthropic-compatible providers such as DeepSeek and Kimi. The SDK's built-in model client speaks the Anthropic Messages format and works against any Anthropic-compatible endpoint, but each provider honors a different subset of the thinking-related parameters. This page records what actually works, verified against the provider's official compatibility documentation and live requests. ## DeepSeek Endpoint: `https://api.deepseek.com/anthropic`. DeepSeek publishes an [Anthropic API compatibility table](https://api-docs.deepseek.com/zh-cn/guides/anthropic_api); the rows below map SDK options onto it. | SDK option | Wire format | DeepSeek behavior | | --- | --- | --- | | `thinkingConfig: { type: "disabled" }` | `thinking: { "type": "disabled" }` | Honored. Thinking blocks disappear from the response. | | `thinkingConfig: { type: "enabled", budgetTokens }` | `thinking: { "type": "enabled", "budget_tokens": N }` | Thinking turns on, but DeepSeek **ignores `budget_tokens`** — the model may think far beyond the budget. Do not rely on it for latency control. | | `thinkingConfig: { type: "adaptive" }` | `thinking: { "type": "adaptive" }` | Accepted without an error and the model still thinks; DeepSeek documents no `adaptive` value, so treat it as `enabled` with no guarantees. | | `reasoningEffort` | top-level `reasoning_effort` | Not in DeepSeek's compatibility table; no effect. `reasoning_effort` is a Kimi convention — see below. | | `outputFormat` | `output_config.format` | Ignored. DeepSeek supports only `effort` inside `output_config`, so SDK structured output does not apply. | DeepSeek's own thinking-strength knob is `output_config.effort`. The SDK does not expose it yet, so on DeepSeek the only reliable thinking control today is the on/off switch via `thinkingConfig`. Other DeepSeek specifics worth knowing: - Unknown model names are silently mapped to `deepseek-v4-flash`; pass an explicit `deepseek-*` model name. - `thinking` content blocks in message history are accepted; `redacted_thinking` blocks are not. - `max_tokens`, `system`, `stream`, `temperature`, `top_p`, `stop_sequences`, and tool use are fully supported. `cache_control` is ignored everywhere. ## Kimi Kimi K3 through an Anthropic-compatible endpoint uses the inverse convention: it accepts the top-level `reasoning_effort` parameter that the SDK sends from `reasoningEffort` (`"low"`, `"high"`, `"max"`), and it does not accept `thinkingConfig`. When `reasoningEffort` is omitted the SDK sends nothing, so the provider default applies (`"max"` for Kimi K3). The Kimi Open Platform endpoint at `https://api.moonshot.cn/v1` uses the OpenAI Chat Completions protocol and is not a valid `baseURL` for the SDK's built-in Anthropic client. ## Anthropic `thinkingConfig` targets the Anthropic API first: `adaptive`, fixed `budgetTokens` (capped at `maxTokens - 1`), and `disabled` all serialize to the official Anthropic `thinking` wire format, and `outputFormat` maps to `output_config.format`. `reasoningEffort` is the exception — it sends Kimi's top-level `reasoning_effort` parameter, not an Anthropic field.