Deuz SDK 2.0 est sorti — stores, guardrails, handoffs et MCP zéro-config. Nouveautés de la 2.0
Deuz SDK
Agents

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:

CapabilityagentToolHand-rolled agent-as-tool
Sub-agent stream visible in the parent streamLiveagentPath-tagged sub-agent parts on fullStreamYou write your own UI-writer plumbing
Tool approval inside the sub-agentInherited from the parent's approveToolCall, at every depthNot part of the pattern — you build it
Per-sub-agent usage attributionmeta.agentPath + folded into the parent totalManual bookkeeping
New runtime conceptNone — 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;
FieldTypeDefaultNotes
namestringIdentity used in agentPath. Use the same string for the tools map key and name — they should always match.
descriptionstringWhat the parent model reads to decide when to delegate. Write it like any tool description.
modelLanguageModelThe sub-agent's own model — can differ from the parent's (e.g. a cheaper model for a research sub-agent).
toolsToolSetThe sub-agent's own tools. Omit for a tool-less sub-agent (a single focused turn).
systemstringThe sub-agent's system prompt.
maxStepsnumber10Max 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.
maxDepthnumber2Nesting cap — see maxDepth guard below.
needsApprovalTool['needsApproval']Gates the sub-agent call itself in the parent's own approval flow (independent of what happens inside the sub-agent).
compactionCompactionOptionThe sub-agent's own compaction policy — useful for long-lived research agents.
stopWhenStopCondition | 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.

register-subagent.ts
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.

live-substream.ts
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:

approval-inheritance.ts
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/applyPatch tool with needsApproval decides 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, pass ctx.signal into long-running commands, capture stdout/stderr separately, 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 approveToolCall and breaking the loop into pendingApprovals — works inside a sub-agent when the parent call carries session (1.5): the sub-agent suspends into its own checkpoint, the parent suspends with agentPath-tagged pending approvals, and resuming the parent routes the verdicts back down the tree. See Durable runtime. Without a session, a gated sub-agent tool call with no inherited server-mode approver still returns a clear is_error explaining this (the parent model can react to it), rather than hanging or silently allowing the call. Server-mode approveToolCall on 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.

nested-agents.ts
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:

usage-attribution.ts
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 too

See also

  • Tool loop — the agentic loop agentTool runs one level down; every invariant (self-healing, immutable history, runaway guards) applies to sub-agents too.
  • ToolsprepareStep, 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 compaction policy, handy for long-lived research agents.
  • Client-side tools — the approval round-trip approveToolCall inherits into sub-agents.

Sur cette page