Sub-Agent Orchestration
Sub-agents enable hierarchical agent systems where a parent agent can delegate specialized tasks to child agents. This is powerful for complex workflows that benefit from separation of concerns.
What Are Sub-Agents?
A sub-agent is an agent invoked by another agent as if it were a tool. The parent agent decides when to delegate, the sub-agent executes to completion, and the result flows back to the parent.
graph TB
Parent["Parent Agent"]
Parent --> Tools["Uses regular tools"]
Parent --> SubAgent["Delegates to Sub-Agent"]
SubAgent --> Run["Sub-agent runs to completion"]
SubAgent --> Result["Result returned to parent"]Creating Sub-Agent Tools
Use createSubAgentTool() to turn an agent into a tool:
import { defineAgent, createSubAgentTool } from '@helix-agents/sdk';
import { z } from 'zod';
// Define a specialized agent
const AnalyzerAgent = defineAgent({
name: 'text-analyzer',
description: 'Analyzes text for sentiment and topics',
systemPrompt: 'You analyze text. Determine sentiment and extract key topics.',
outputSchema: z.object({
sentiment: z.enum(['positive', 'negative', 'neutral']),
confidence: z.number(),
topics: z.array(z.string()),
}),
llmConfig: { model: openai('gpt-4o-mini') },
});
// Create a tool that invokes this agent
const analyzeTool = createSubAgentTool(
AnalyzerAgent,
z.object({
text: z.string().describe('Text to analyze'),
}),
{
description: 'Analyze text for sentiment and key topics',
timeoutMs: 60_000, // Optional: per-tool timeout in ms
}
);
// Use in parent agent
const OrchestratorAgent = defineAgent({
name: 'orchestrator',
systemPrompt: 'You coordinate research. Use the analyzer for sentiment analysis.',
tools: [searchTool, analyzeTool], // Mix regular tools and sub-agents
llmConfig: { model: openai('gpt-4o') },
});Requirements
Sub-agents must have outputSchema:
// ✓ Valid sub-agent - has outputSchema
const ValidSubAgent = defineAgent({
name: 'valid',
outputSchema: z.object({ result: z.string() }), // Required!
// ...
});
// ✗ Invalid sub-agent - no outputSchema
const InvalidSubAgent = defineAgent({
name: 'invalid',
// No outputSchema - will throw error when used as sub-agent
// ...
});The outputSchema defines the contract between parent and child - what the parent receives as the tool result.
Cloudflare Durable Objects Runtime
In the DO runtime, createSubAgentTool() works transparently — no changes to your agent definitions are needed. Internally, each sub-agent is routed to a sibling DO instance with its own isolated SQLite state. You only need to add subAgentNamespace to your createAgentServer config, and register all sub-agents in the same AgentRegistry. See Sub-Agents in the DO Runtime for setup details.
How Sub-Agent Execution Works
When the parent LLM calls a sub-agent tool:
- Tool Call Detection - Framework identifies the tool as a sub-agent (prefixed with
subagent__) - Sub-Agent Initialization - New agent run created with:
- Fresh
runId(derived from parent's ID) - Same
streamIdas parent (for unified streaming) - Input converted to user message
- Fresh
- Execution - Sub-agent runs its full loop until completion
- Result Return - Sub-agent's
outputbecomes the tool result
// Parent LLM calls:
{
name: 'subagent__text-analyzer',
arguments: { text: 'This product is amazing!' }
}
// Framework:
// 1. Creates sub-agent run
// 2. Converts input to message: "This product is amazing!"
// 3. Runs AnalyzerAgent loop
// 4. Returns output: { sentiment: 'positive', confidence: 0.95, topics: ['product'] }Input Mapping
The input schema defines what arguments the parent provides. These are converted to a user message for the sub-agent:
const subAgentTool = createSubAgentTool(
SubAgent,
z.object({
query: z.string(), // Common field names are recognized
context: z.string().optional(),
})
);
// When called with { query: "analyze this", context: "..." }
// Sub-agent receives user message: '{"query":"analyze this","context":"..."}'The full tool input is JSON-serialized and sent as the user message. The remote agent server is responsible for parsing the JSON and constructing the appropriate context for its agent.
Streaming Integration
Sub-agent events stream alongside parent events on the same stream:
for await (const chunk of stream) {
switch (chunk.type) {
case 'text_delta':
// Could be from parent or sub-agent
console.log(`[${chunk.agentType}]`, chunk.delta);
break;
case 'subagent_start':
console.log(`Starting sub-agent: ${chunk.subAgentType}`);
break;
case 'subagent_end':
console.log(`Sub-agent ${chunk.subAgentType} result:`, chunk.result);
break;
case 'tool_start':
// Includes sub-agent tool calls from within sub-agents
console.log(`[${chunk.agentType}] Tool: ${chunk.toolName}`);
break;
}
}Sub-agent streaming events:
tool_startfor the parent'ssubagent__<name>tool callsubagent_startwhen the sub-agent begins- All proxied sub-agent chunks (
text_delta,tool_start/tool_endfor inner tools, etc.) subagent_endwhen the sub-agent completestool_endfor the parent'ssubagent__<name>tool call
The tool_start/tool_end pair on the parent is what closes the parent's dynamic-tool UI part for AI SDK consumers (transitions 'input-available' → 'output-available'). See Sub-agent chunk ordering for the full semantics, including failure-path behavior and the subagent_end → tool_end ordering invariant.
This enables real-time visibility into nested execution.
State Isolation
Sub-agents have completely isolated state:
const ParentAgent = defineAgent({
name: 'parent',
stateSchema: z.object({
parentCounter: z.number().default(0),
}),
// ...
});
const SubAgent = defineAgent({
name: 'child',
stateSchema: z.object({
childCounter: z.number().default(0), // Separate from parent
}),
// ...
});Key points:
- Sub-agent cannot read parent's custom state
- Parent cannot read sub-agent's custom state
- Each has its own
messages,stepCount, etc. - Sub-agent output is the only communication channel
To share data, pass it through the input and receive it in the output.
Error Handling
Sub-Agent Failures
If a sub-agent fails, the error becomes the tool result:
// Sub-agent throws error
throw new Error('Analysis failed: text too short');
// Parent receives tool result:
{
success: false,
error: 'Analysis failed: text too short'
}The parent LLM sees the error and can decide how to proceed (retry, try different approach, etc.).
Handling Errors
Check for failures in parent's tools or logic:
const processResultTool = defineTool({
name: 'process_analysis',
execute: async (input, context) => {
// The sub-agent result may have succeeded or failed
if (!input.analysisResult.success) {
// Handle sub-agent failure
return {
processed: false,
reason: input.analysisResult.error,
};
}
// Process successful result
const analysis = input.analysisResult.result;
// ...
},
});Nested Sub-Agents
Sub-agents can themselves have sub-agents:
// Level 3: Leaf agent
const SentimentAnalyzer = defineAgent({
name: 'sentiment',
outputSchema: z.object({ sentiment: z.string() }),
// ...
});
// Level 2: Uses sentiment analyzer
const TextProcessor = defineAgent({
name: 'processor',
tools: [createSubAgentTool(SentimentAnalyzer /* ... */)],
outputSchema: z.object({ processed: z.string() }),
// ...
});
// Level 1: Uses text processor
const Orchestrator = defineAgent({
name: 'orchestrator',
tools: [createSubAgentTool(TextProcessor /* ... */)],
// ...
});Stream events include all levels:
[orchestrator] text_delta: "Let me analyze..."
[orchestrator] subagent_start: processor
[processor] text_delta: "Processing..."
[processor] subagent_start: sentiment
[sentiment] text_delta: "Analyzing..."
[sentiment] output: { sentiment: "positive" }
[processor] subagent_end: sentiment
[processor] output: { processed: "..." }
[orchestrator] subagent_end: processor
[orchestrator] text_delta: "Based on the analysis..."Patterns
Specialist Pattern
Delegate specific tasks to specialists:
const ResearchAgent = defineAgent({
name: 'researcher',
tools: [
searchTool,
createSubAgentTool(FactCheckerAgent /* ... */),
createSubAgentTool(SummarizerAgent /* ... */),
],
systemPrompt: `You are a research coordinator.
1. Search for information
2. Send claims to the fact-checker
3. Send findings to the summarizer
4. Compile final report`,
});Pipeline Pattern
Chain agents in a processing pipeline:
// Each agent processes and passes to next
const ExtractorAgent = defineAgent({
name: 'extractor',
outputSchema: z.object({ entities: z.array(z.string()) }),
});
const EnricherAgent = defineAgent({
name: 'enricher',
outputSchema: z.object({
enrichedEntities: z.array(
z.object({
/* ... */
})
),
}),
});
const FormatterAgent = defineAgent({
name: 'formatter',
outputSchema: z.object({ formatted: z.string() }),
});
// Coordinator runs the pipeline
const PipelineAgent = defineAgent({
name: 'pipeline',
tools: [
createSubAgentTool(ExtractorAgent, z.object({ text: z.string() })),
createSubAgentTool(EnricherAgent, z.object({ entities: z.array(z.string()) })),
createSubAgentTool(FormatterAgent, z.object({ data: z.unknown() })),
],
systemPrompt: `Process text through the pipeline:
1. Extract entities
2. Enrich each entity
3. Format the output`,
});Parallel Delegation Pattern
Delegate multiple tasks simultaneously:
const MultiAnalyzerAgent = defineAgent({
name: 'multi-analyzer',
tools: [
createSubAgentTool(SentimentAgent, z.object({ text: z.string() })),
createSubAgentTool(TopicAgent, z.object({ text: z.string() })),
createSubAgentTool(EntityAgent, z.object({ text: z.string() })),
],
systemPrompt: `Analyze text from multiple angles.
You can run multiple analyses in parallel.
Combine results into a comprehensive report.`,
});The framework executes parallel tool calls concurrently when the LLM requests them.
Conditional Delegation Pattern
Delegate based on input characteristics:
const RouterAgent = defineAgent({
name: 'router',
tools: [
createSubAgentTool(SimpleQAAgent, z.object({ question: z.string() }), {
description: 'For simple factual questions',
}),
createSubAgentTool(ResearchAgent, z.object({ topic: z.string() }), {
description: 'For topics requiring deep research',
}),
createSubAgentTool(MathAgent, z.object({ problem: z.string() }), {
description: 'For mathematical calculations',
}),
],
systemPrompt: `Route questions to the appropriate specialist:
- Simple facts → SimpleQA
- Complex topics → Research
- Math problems → Math
Choose the best agent for each request.`,
});Remote Sub-Agents
For agents running on a separate HTTP service, use createRemoteSubAgentTool() instead of createSubAgentTool(). This enables cross-service and cross-runtime delegation via HTTP + SSE.
import {
defineAgent,
createRemoteSubAgentTool,
HttpRemoteAgentTransport,
} from '@helix-agents/core';
import { z } from 'zod';
const transport = new HttpRemoteAgentTransport({
url: 'http://localhost:4000',
});
const researcherTool = createRemoteSubAgentTool('researcher', {
description: 'Delegate research to a remote specialist agent',
inputSchema: z.object({ query: z.string() }),
outputSchema: z.object({
findings: z.array(z.object({ title: z.string(), snippet: z.string() })),
}),
transport,
remoteAgentType: 'researcher',
timeoutMs: 120_000,
});
const OrchestratorAgent = defineAgent({
name: 'orchestrator',
tools: [researcherTool], // Works like any other tool
// ...
});Remote sub-agents stream events using the same subagent_start/subagent_end protocol as local sub-agents, so frontends don't need to distinguish between them.
For the full guide — including server setup, transport configuration, and production considerations — see Remote Agents.
Persistent Sub-Agents
Overview
Persistent sub-agents are long-lived child agents that maintain state across multiple interactions. Unlike ephemeral sub-agents (created with createSubAgentTool()), persistent children can receive follow-up messages and be managed throughout the parent's lifecycle.
Configure persistent sub-agents via the persistentAgents field on AgentConfig:
import { defineAgent } from '@helix-agents/core';
import { z } from 'zod';
const ResearcherAgent = defineAgent({
name: 'researcher',
systemPrompt: 'You research topics.',
outputSchema: z.object({ findings: z.string() }),
llmConfig: { model: openai('gpt-4o-mini') },
});
const OrchestratorAgent = defineAgent({
name: 'orchestrator',
systemPrompt: 'You coordinate research tasks using your persistent children.',
outputSchema: z.object({ summary: z.string() }),
persistentAgents: [{ agent: ResearcherAgent, mode: 'blocking' }],
llmConfig: { model: openai('gpt-4o') },
});Two Modes
Blocking (mode: 'blocking'): Parent waits for the child to complete before continuing. Use when you need the child's result before making further decisions.
Non-blocking (mode: 'non-blocking'): Parent continues immediately after spawning. Child runs concurrently and the parent receives a completion notification later. Use for fire-and-forget background tasks.
persistentAgents: [
{ agent: ResearcherAgent, mode: 'blocking' }, // Parent waits
{ agent: BackgroundWorker, mode: 'non-blocking' }, // Fire-and-forget
],Note: Each agent type can only appear once in
persistentAgents.defineAgent()will throw if the same agent type appears multiple times. To use the same agent logic in both modes, create two separate agent definitions with distinct names.
Companion Tools
When persistentAgents is configured, six companion tools are auto-injected into the parent agent (prefixed with companion__):
| Tool | Description |
|---|---|
companion__spawnAgent | Create and start a new persistent child. Args: { agent: string, initialMessage: string, name?: string } |
companion__sendMessage | Send a follow-up message to an active child. Args: { name: string, message: string } |
companion__listChildren | List all persistent children and their current statuses |
companion__getChildStatus | Get detailed status of a specific child by name. Args: { name: string } |
companion__waitForResult | Block until a specific child completes (only available when at least one blocking agent is configured). Args: { name: string } |
companion__terminateChild | Terminate a running child. Args: { name: string } |
The parent's LLM decides when and how to use these tools based on the system prompt and conversation context.
Child Naming
Children can be named explicitly via the name argument in companion__spawnAgent, or auto-named using the pattern {agentType}-{counter} (e.g., researcher-1, researcher-2).
// Explicit naming
spawnAgent({
agent: 'researcher',
initialMessage: 'Research AI safety',
name: 'safety-researcher',
});
// Auto-naming (uses counter)
spawnAgent({ agent: 'researcher', initialMessage: 'Research quantum computing' });
// -> named 'researcher-1'
spawnAgent({ agent: 'researcher', initialMessage: 'Research fusion energy' });
// -> named 'researcher-2'Session IDs
Each persistent child gets a deterministic session ID: {parentSessionId}-agent-{name}. This enables:
- Stable references across parent restarts
- Predictable state store lookups
- Clean cleanup on parent completion
Re-spawning
If a child with the same name is spawned after a previous one completed, the old session is cleaned up and a new one starts fresh. The SubSessionRef is updated to point to the new session.
State Tracking
Persistent children are tracked via SubSessionRef entries with mode: 'persistent':
interface SubSessionRef {
subSessionId: string;
agentType: string;
parentToolCallId: string;
status: 'running' | 'completed' | 'failed' | 'terminated';
startedAt: number;
completedAt?: number;
mode: 'ephemeral' | 'persistent'; // 'persistent' for companion-managed children
name?: string; // The child's name
}Ephemeral vs Persistent Comparison
| Feature | Ephemeral (createSubAgentTool) | Persistent (persistentAgents) |
|---|---|---|
| Created by | Parent's tool call to subagent__ | companion__spawnAgent tool |
| Lifecycle | Runs to completion, result returned | Long-lived, can receive messages |
| Follow-up messages | Not supported | Via companion__sendMessage |
| Result access | Immediate (tool result) | Via companion__getChildStatus or companion__waitForResult |
| Naming | Auto-generated | Explicit or auto-incremented |
| Mode | Always blocking | Blocking or non-blocking |
| Session ID | {parentSessionId}-sub-{callId} | {parentSessionId}-agent-{name} |
| SubSessionRef mode | 'ephemeral' | 'persistent' |
Example: Research Coordinator
const ResearcherAgent = defineAgent({
name: 'researcher',
systemPrompt: 'You research topics thoroughly and return findings.',
outputSchema: z.object({
findings: z.string(),
sources: z.array(z.string()),
}),
tools: [searchTool],
llmConfig: { model: openai('gpt-4o-mini') },
});
const CoordinatorAgent = defineAgent({
name: 'coordinator',
systemPrompt: `You coordinate research tasks.
You have persistent researcher children that you can spawn, send messages to, and check results.
1. Spawn researchers for different topics
2. Wait for their results
3. Compile a final summary`,
outputSchema: z.object({ summary: z.string() }),
persistentAgents: [
{
agent: ResearcherAgent,
mode: 'blocking',
description: 'Spawns researcher agents for deep dives',
},
],
llmConfig: { model: openai('gpt-4o') },
});The coordinator can then:
- Spawn
researcher-1for topic A - Spawn
researcher-2for topic B - Check status or wait for results
- Terminate if needed
- Compile final output
Runtime Support
Persistent sub-agents work across all runtimes:
| Runtime | Blocking | Non-blocking | Notes |
|---|---|---|---|
| JS | Yes | Yes | In-process execution |
| Temporal | Yes | Yes | Child workflows with signal support |
| Cloudflare Workflows | Yes | Yes | Nested workflow instances |
| Cloudflare DO | Yes | Yes | Sibling DO instances |
Workspaces
Persistent sub-agents can use workspaces in two modes:
Per-invocation (default)
Each companion__spawnAgent and companion__sendMessage cycle opens the child's workspaces fresh and closes them when the child exits. This is the safe default, but cost-bearing providers (Cloudflare sandboxes, R2 namespaces) pay the open() cost N times for N sends.
State persistence across invocations depends on the provider:
InMemoryWorkspaceProvider— state is LOST between invocations (in-memory state has no backing store).CloudflareFileStoreWorkspace/CloudflareSandboxWorkspace— state persists via the underlying Durable Object storage; close+reopen cycles reattach to the same R2 prefix / sandbox container.LocalBashWorkspace— state is LOST (per-cycle tmpdir).
Persistent
Set workspaceLifetime: 'persistent' on the persistentAgents entry to keep workspaces open across the child's lifetime. They close only when the child is terminated (via companion__terminateChild) or the parent shuts down.
persistentAgents: [
{
agent: ResearcherAgent,
mode: 'blocking',
workspaceLifetime: 'persistent', // workspaces open once, reused across sends
},
],Use 'persistent' when:
- The child's workspaces have meaningful open-cost (sandboxes, network-backed FS).
- The child receives many
sendMessagecalls in quick succession.
Use 'per-invocation' (default) when:
- The child is short-lived or rarely re-invoked.
- Workspace state needs to be reset between invocations.
- The provider already handles open-cost cheaply (in-memory).
Inheriting parent workspaces
Set inheritWorkspaces: true on a persistentAgents entry to share the parent's workspaces with the child. Same semantics as the ephemeral createSubAgentTool({ inheritWorkspaces: true }) flag — the child sees the parent's workspaces under their parent-side names AND can declare its own, which are layered on top via addEntries() (collisions throw).
persistentAgents: [
{
agent: ResearcherAgent,
mode: 'blocking',
inheritWorkspaces: true, // child shares the parent's WorkspaceRegistry
},
],When inheritWorkspaces: true, the workspaceLifetime field has no effect — the child uses the parent's registry, whose lifetime is bounded by the parent's runLoop.
Status of
workspaceLifetime(round-5 D10). TheworkspaceLifetimefield on apersistentAgentsentry is reserved for future use. As of v1, all values behave as'per-invocation'— workspaces open at sub-agent spawn/resume and close at sub-agent exit. The'persistent'lifetime (workspaces stay open across multiplecompanion__sendMessagecalls) is filed as a known follow-up. Until support lands, prefer providers whoseresolve()reattaches efficiently (CloudflareFileStoreWorkspace,CloudflareSandboxWorkspace) to minimize per-invocation cost.
addEntries persistence semantics (round-5 D2)
When a sub-agent inherits AND declares its own additional workspaces, the framework calls registry.addEntries() to layer those workspaces onto the parent's registry. The added entries' persistRef callback is the SAME one bound to the parent's registry — so refs for the child's "own" workspaces are persisted under the PARENT's session ID, NOT the child's.
This matters when sub-agents need their workspace state isolated from the parent's session. A sub-agent inheriting a parent's registry and adding its own workspaces leaves refs in the parent's session state — visible to anyone with parent-session access regardless of which child created them.
Practical guidance. Use inheritWorkspaces: true for shared workspaces that are part of the parent's session (a working notes workspace, a shared cache). For sub-agent operations whose workspace state must be isolated from the parent's session, prefer fully-isolated workspaces declared on the child (no inheritance) so the child's runLoop owns its own registry and persistRef writes to the child's session state.
Sibling workspace visibility (round-5 D17)
When TWO sibling sub-agents both opt into inheritWorkspaces: true, they share the SAME physical workspace storage via the parent's registry. Concretely:
- Sibling A writes
/notes/idea.md. Sibling B can read it back. - Sibling B's writes are visible to Sibling A and to the parent.
- All three (parent + A + B) operate against the same
WorkspaceRegistryinstance.
This is the natural consequence of registry sharing — the registry is a per-session singleton, and inheritWorkspaces: true means "use the parent's registry directly." There is no per-sub-agent isolation when inheriting.
If sibling sub-agents need workspace isolation from each other, do NOT set inheritWorkspaces: true on either; declare each sub-agent's workspaces on the child config so each gets its own isolated registry.
Reserved tool prefixes
The companion__ prefix is reserved by the framework for the auto-injected companion tools. User-defined tools whose name starts with companion__ cause defineAgent() to throw at build time, regardless of whether the agent declares any persistentAgents. This is enforced unconditionally so the prefix's reserved status is a stable contract — your agent code keeps working when you add a persistent sub-agent later. Use any other naming pattern (e.g. helper__listChildren, myCompanion) for your own tools.
The workspace__ prefix is similarly reserved (see Workspaces — reserved prefix).
Best Practices
1. Clear Output Schemas
Define precise output schemas for clear contracts:
// Good: Specific schema
const agent = defineAgent({
outputSchema: z.object({
sentiment: z.enum(['positive', 'negative', 'neutral']),
confidence: z.number().min(0).max(1),
reasoning: z.string(),
}),
});
// Avoid: Vague schema
const agent = defineAgent({
outputSchema: z.object({
result: z.unknown(), // What is this?
}),
});2. Descriptive Sub-Agent Tools
Help the parent LLM choose correctly:
const tool = createSubAgentTool(AnalyzerAgent, z.object({ text: z.string() }), {
description: `Analyze text for sentiment and extract key topics.
Use when you need:
- Sentiment classification (positive/negative/neutral)
- Topic extraction from text
- Confidence scores for analysis
Returns: { sentiment, confidence, topics }`,
});3. Appropriate Granularity
Balance specialization vs. overhead:
// Good: Meaningful specialization
const FactCheckerAgent = defineAgent({
/* verifies claims */
});
const SummarizerAgent = defineAgent({
/* creates summaries */
});
// Avoid: Over-granular
const CapitalizerAgent = defineAgent({
/* just capitalizes text */
});
// ^ This is better as a regular tool or string method4. Handle Sub-Agent Limits
Set appropriate maxSteps for sub-agents, and use timeoutMs to enforce wall-clock limits:
const SubAgent = defineAgent({
name: 'focused-task',
maxSteps: 5, // Sub-agents should complete quickly
// ...
});
const subAgentTool = createSubAgentTool(SubAgent, inputSchema, {
timeoutMs: 30_000, // 30-second wall-clock limit (important in DO runtime)
});5. Test Sub-Agents Independently
Sub-agents are full agents - test them alone first:
// Test sub-agent directly
const subHandle = await executor.execute(AnalyzerAgent, 'Test text');
const subResult = await subHandle.result();
expect(subResult.status).toBe('completed');
// Then test in orchestration
const parentHandle = await executor.execute(OrchestratorAgent, 'Analyze this');
const parentResult = await parentHandle.result();Limitations
No Shared State
Sub-agents cannot access parent state. Design inputs/outputs to carry needed context:
// Pass context through input
const tool = createSubAgentTool(
SubAgent,
z.object({
query: z.string(),
context: z.object({
previousFindings: z.array(z.string()),
constraints: z.array(z.string()),
}),
})
);Sequential by Default
Multiple sub-agent calls in one LLM response may execute in parallel, but the parent waits for all before continuing.
Overhead
Each sub-agent invocation includes:
- State initialization
- Full agent loop (potentially multiple LLM calls)
- State persistence
For simple transformations, prefer regular tools.
Lifecycle Hook Guarantees
Sub-agents fire their own lifecycle hooks (onAgentStart, onAgentComplete, onAgentFail) independently from the parent agent. This is important for tracing integrations that need to emit spans for each sub-agent. The parent's stream is not closed when a sub-agent completes — only its hooks fire. This behavior is consistent across all runtimes. See Sub-Agent Execution Internals for implementation details.
Next Steps
- Remote Agents - Delegate to agents on separate HTTP services
- Streaming - Handle sub-agent stream events
- Interrupt and Resume - How interrupts propagate through sub-agent hierarchies
- Runtimes - How different runtimes handle sub-agents
- Examples - Real-world orchestration examples
- Hooks - Observe sub-agent execution with
beforeSubAgentandafterSubAgenthooks