Sub-Agents
agentTool wraps a focused agentic loop as a callable Tool — with a live-forwarded stream and inherited tool approval, and no new runtime.
Le contenu des pages de documentation est en anglais. La navigation, la recherche et l’interface suivent la langue choisie.
agentTool turns a model + its own tools into a Tool the parent loop can call. The parent model delegates a sub-task ("research X", "write and test this function") to a nested agentic loop that runs to completion and returns its final text — the same tool loop machinery, one level down, with no new runtime concept to learn.
What's first-class here
The common way to compose agents is the "agent-as-tool" pattern: a tool whose execute calls another agent and returns its final text. agentTool is that pattern, but with three things provided out of the box instead of hand-wired:
| Capability | agentTool | Hand-rolled agent-as-tool |
|---|---|---|
| Sub-agent stream visible in the parent stream | Live — agentPath-tagged sub-agent parts on fullStream | You write your own UI-writer plumbing |
| Tool approval inside the sub-agent | Inherited from the parent's approveToolCall, at every depth | Not part of the pattern — you build it |
| Per-sub-agent usage attribution | meta.agentPath + folded into the parent total | Manual bookkeeping |
| New runtime concept | None — the same streamChat loop, one level down | — |
All three come for free because execute just drives the same tool loop internally — there's no separate agent runtime to adopt.
Defining a sub-agent
interface AgentToolDef {
/** Sub-agent identity — used in `agentPath`; use the same string as the tool key. */
name: string;
/** Shown to the parent model so it knows when to delegate. */
description: string;
model: LanguageModel;
tools?: ToolSet;
system?: string;
maxSteps?: number; // default 10
maxDepth?: number; // default 2
needsApproval?: Tool['needsApproval'];
compaction?: CompactionOption;
stopWhen?: StopCondition | StopCondition[];
subAgentStream?: 'full' | 'none'; // default 'full'
}
function agentTool(def: AgentToolDef): Tool;| Field | Type | Default | Notes |
|---|---|---|---|
name | string | — | Identity used in agentPath. Use the same string for the tools map key and name — they should always match. |
description | string | — | What the parent model reads to decide when to delegate. Write it like any tool description. |
model | LanguageModel | — | The sub-agent's own model — can differ from the parent's (e.g. a cheaper model for a research sub-agent). |
tools | ToolSet | — | The sub-agent's own tools. Omit for a tool-less sub-agent (a single focused turn). |
system | string | — | The sub-agent's system prompt. |
maxSteps | number | 10 | Max turns of the sub-agent's own loop — sub-agents are inherently multi-step, so the default is higher than the root loop's 1. |
maxDepth | number | 2 | Nesting cap — see maxDepth guard below. |
needsApproval | Tool['needsApproval'] | — | Gates the sub-agent call itself in the parent's own approval flow (independent of what happens inside the sub-agent). |
compaction | CompactionOption | — | The sub-agent's own compaction policy — useful for long-lived research agents. |
stopWhen | StopCondition | StopCondition[] | — | The sub-agent's own stop condition(s), OR-ed with its maxSteps. |
subAgentStream | 'full' | 'none' | 'full' | 'full' forwards the sub-agent's live stream into the parent; 'none' runs it silently. |
agentTool returns a plain Tool — register it in tools exactly like any other tool. The tool's input schema is fixed to { prompt: string }; the model passes the sub-task as prompt, and the tool's return value is the sub-agent's final text.
import { generateText, agentTool } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const haiku = anthropic('claude-haiku-5');
const opus = anthropic('claude-opus-4-8');
const { text } = await generateText({
model: opus,
messages: [{ role: 'user', content: 'Research the latest Anthropic pricing and summarize it.' }],
maxSteps: 5,
tools: {
researcher: agentTool({
name: 'researcher', // same string as the map key
description: 'Delegate research tasks to a focused sub-agent with web search.',
model: haiku,
tools: { webSearch: /* … a Tool with execute … */ },
maxSteps: 6,
}),
},
});
console.log(text);A buffered generateText parent always runs sub-agents silently — there's no stream to forward into — and just gets the returned text back as a normal tool_result.
Live visibility: sub-agent stream parts
When the parent is streaming, the sub-agent's entire canonical stream — every text-delta, tool-call, tool-result, step-start, everything — forwards live into the parent's fullStream, wrapped as a sub-agent part tagged with the full nesting path:
interface SubAgentPart {
type: 'sub-agent';
agentPath: string[]; // ['researcher'], or ['researcher', 'coder'] one level deeper
part: StreamPart; // the sub-agent's own canonical part, unwrapped
}A second-level sub-agent's parts ride agentPath: ['researcher', 'coder'] — single-wrapped, never a sub-agent part nested inside another sub-agent part.
import { streamChat, agentTool } from '@deuz-sdk/core';
const result = streamChat({
model: opus,
messages: [{ role: 'user', content: 'Research X and write me a summary.' }],
maxSteps: 5,
tools: {
researcher: agentTool({
name: 'researcher',
description: 'Delegate research tasks to a focused sub-agent.',
model: haiku,
tools: { webSearch: /* … */ },
}),
},
});
for await (const part of result.fullStream) {
if (part.type === 'sub-agent') {
const path = part.agentPath.join(' > ');
if (part.part.type === 'text-delta') {
process.stdout.write(`[${path}] ${part.part.text}`);
} else if (part.part.type === 'tool-call') {
console.log(`\n[${path}] calling ${part.part.toolName}`);
}
continue;
}
// the PARENT's own parts (text-delta, tool-call for `researcher` itself, …) arrive un-wrapped
}Pass subAgentStream: 'none' on the definition to run a sub-agent silently even from a streaming parent — the parent still gets the final text as a normal tool-result, it just skips forwarding the internals.
sub-agent parts also cross the Deuz UI wire and, since 1.9, applyUIPart folds each one into turn.subAgents (one frame per agentPath, each carrying the child's own full AssistantTurnState) — so a delegated run is renderable in a useChat UI without reading the wire by hand. See rendering a sub-agent run.
Approval inheritance
The parent's server-mode approveToolCall is inherited to every nesting depth — a sub-agent's own tool calls stay gated by the exact same policy the parent uses, with no extra wiring:
import { generateText, agentTool } from '@deuz-sdk/core';
import { z } from 'zod';
const result = await generateText({
model: opus,
messages: [{ role: 'user', content: 'Have the coder agent fix the failing test.' }],
maxSteps: 5,
tools: {
coder: agentTool({
name: 'coder',
description: 'Writes and runs code changes to fix a failing test.',
model: opus,
tools: {
runShellCommand: {
description: 'Run a shell command.',
parameters: z.object({ cmd: z.string() }),
needsApproval: true, // gated even though it's called from INSIDE the sub-agent
execute: async ({ cmd }) => runShell(cmd),
},
},
}),
},
// Inherited down: coder's runShellCommand calls are decided here too.
approveToolCall: async (call) => policyAllows(call),
});Approval is not sandboxing. Gating a
runShellCommand/writeFile/applyPatchtool withneedsApprovaldecides whether it runs — it does nothing to contain what it does. For shell/file/code tools, also: run inside a sandbox or a restricted workspace root, enforce allow/deny lists, passctx.signalinto long-running commands, capturestdout/stderrseparately, cap output size before feeding it back to the model, and never expose host secrets or unrestricted filesystem access. See the coding-agent cookbook for the full pattern.
Client-mode approval needs a durable session. Client-mode approval — omitting
approveToolCalland breaking the loop intopendingApprovals— works inside a sub-agent when the parent call carriessession(1.5): the sub-agent suspends into its own checkpoint, the parent suspends withagentPath-tagged pending approvals, and resuming the parent routes the verdicts back down the tree. See Durable runtime. Without asession, a gated sub-agent tool call with no inherited server-mode approver still returns a clearis_errorexplaining this (the parent model can react to it), rather than hanging or silently allowing the call. Server-modeapproveToolCallon the parent call works at any depth either way.
Nesting sub-agents (orchestrator → worker)
agentTool inside agentTool composes: an orchestrator delegates to a researcher, and the researcher can itself delegate to a coder. Each level is a normal tool inside the level above it; agentPath records the full chain (['researcher', 'coder']), approval and usage inherit straight down, and a streaming parent sees every level's parts live.
import { generateText, agentTool } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { z } from 'zod';
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const opus = anthropic('claude-opus-4-8');
const haiku = anthropic('claude-haiku-5');
// A leaf coding sub-agent with a gated shell tool.
const coder = agentTool({
name: 'coder',
description: 'Writes and runs code changes to satisfy a spec.',
model: opus,
maxSteps: 8,
tools: {
runShellCommand: {
description: 'Run a shell command.',
parameters: z.object({ cmd: z.string() }),
needsApproval: true, // still gated when reached two levels deep
execute: async ({ cmd }) => runShell(cmd),
},
},
});
// A research sub-agent that can itself delegate coding to `coder`.
const researcher = agentTool({
name: 'researcher',
description: 'Researches a task and delegates any code changes to the coder.',
model: haiku,
maxSteps: 6,
tools: { webSearch: /* … a Tool with execute … */, coder },
});
// The orchestrator only knows about `researcher`.
const { text, usage } = await generateText({
model: opus,
messages: [{ role: 'user', content: 'Investigate the failing build and fix it.' }],
maxSteps: 5,
tools: { researcher },
// Inherited all the way down: coder.runShellCommand calls are decided here.
approveToolCall: async (call) => policyAllows(call),
});
// usage.totalTokens includes the orchestrator + researcher + coder.The maxDepth guard
Nesting is capped by maxDepth (default 2) to stop runaway self-recursion. Each agentTool checks its own maxDepth against how deep its agentPath already is when it runs; exceeding it throws, which the loop's self-healing turns into an is_error tool result — the parent model can recover (e.g. try the task itself) instead of the whole call failing.
Abort
The parent call's signal propagates down into every sub-agent's own loop, exactly like it propagates into a plain tool's execute — aborting the parent tears down sub-agents at any depth too.
Usage attribution
A sub-agent's cumulative usage folds into the parent's total: result.usage, budget stops (totalTokensExceed/costExceeds), and cost calculations all include every sub-agent's tokens, at every depth. Per-call onUsage events from inside a sub-agent are additionally tagged with meta.agentPath, so you can attribute usage to a specific sub-agent if you need finer-grained accounting:
const result = await generateText({
model: opus,
messages,
maxSteps: 5,
tools: { researcher: agentTool({ name: 'researcher', description: '…', model: haiku, tools: {} }) },
onUsage: (usage, meta) => {
// meta.agentPath is ['researcher'] for usage produced inside that sub-agent,
// undefined for the parent's own steps.
console.log(meta.model, meta.agentPath?.join(' > ') ?? 'root', usage.totalTokens);
},
});
console.log(result.usage.totalTokens); // includes every sub-agent's tokens tooSee also
- Tool loop — the agentic loop
agentToolruns one level down; every invariant (self-healing, immutable history, runaway guards) applies to sub-agents too. - Tools —
prepareStep,activeTools, and budget stop conditions, all usable inside a sub-agent's own loop via its own call options. - Context compaction — a sub-agent accepts its own
compactionpolicy, handy for long-lived research agents. - Client-side tools — the approval round-trip
approveToolCallinherits into sub-agents.
Provider-Executed Tools
Provider-native tools (Anthropic web search, OpenAI web search, Gemini Google Search) that run on the provider's own infrastructure and never touch your local execute.
Handoffs
handoff() — transfer the whole run to another agent, history and all. The counterpart to agentTool, which delegates and comes back.