Multi-Agent Patterns
Awaken supports multiple patterns for composing agents. This page describes delegation, remote agents, background agents, agent-to-agent communication, sub-agent execution, and handoff.
Purpose
Section titled “Purpose”Use this page to choose the composition model before writing code. The best pattern is the one that makes ownership, return paths, state movement, and cancellation explicit; that is safer than treating every specialist as another prompt in the same hidden context.
| Pattern | Purpose | Why this is better |
|---|---|---|
| Delegate agent as a tool | Run a specialist and return a bounded result to the parent | The parent sees a normal tool result and keeps final control. |
| Programmatic sub-agent | Let custom Rust decide seed/export, streaming, and status policy | State flow is typed and auditable instead of implicit. |
| Background task or background agent | Keep long work alive across step boundaries | The loop can wait, cancel, resume, and ingest inbox events. |
send_message communication | Let independent agents exchange messages | Routing is explicit: live child inbox or durable mailbox. |
| Handoff | Let another agent take over the current thread | Conversation and state stay continuous without spawning a parallel run. |
Agent Delegation via AgentSpec.delegates
Section titled “Agent Delegation via AgentSpec.delegates”An agent can declare sub-agents it is allowed to delegate to:
{ "id": "orchestrator", "model_id": "gpt-4o", "system_prompt": "You coordinate tasks across specialized agents.", "delegates": ["researcher", "writer", "reviewer"]}Each ID in delegates must be a registered agent in the AgentSpecRegistry. During resolution, the runtime creates an AgentTool for each delegate. From the LLM’s perspective, each sub-agent appears as a regular tool named agent_run_{delegate_id}.
The AgentTool holds an Arc<dyn AgentResolver>, not a pre-selected backend. When the LLM calls the tool, execute() invokes resolver.resolve_execution(&agent_id) at call time, and the resolver decides per call:
- Local agents (no
endpointfield) resolve to a localResolvedAgentand execute inline within the same runtime. - Remote agents (with
endpointfield) resolve toResolvedBackendAgentand execute through the configuredExecutionBackend(today: A2A) —message:sendrequest, then poll the resulting task for completion.
Because resolution is deferred to call time, mutating the delegate’s AgentSpec via the config API (e.g. flipping its endpoint) takes effect on the next tool call without rebuilding the parent agent.
Remote Agents via A2A
Section titled “Remote Agents via A2A”Remote agents are declared with an endpoint in AgentSpec:
{ "id": "remote-analyst", "model_id": "unused-for-remote", "system_prompt": "", "endpoint": { "backend": "a2a", "base_url": "https://analyst.example.com/v1/a2a", "auth": { "type": "bearer", "token": "token-abc" }, "target": "analyst", "timeout_ms": 300000, "options": { "poll_interval_ms": 1000 } }}The A2aBackend handles the A2A protocol lifecycle:
- Sends a
message:sendrequest with the user message. - Receives a task wrapper, extracts
task.id, and polls/tasks/:task_idat the configured interval. - Returns the completed response as a
BackendRunResult. - The result is formatted as a
ToolResultand returned to the parent agent’s LLM context.
If the remote agent times out or fails, the BackendRunStatus reflects the failure and the parent agent receives an error tool result.
Programmatic Sub-Agent Invocation
Section titled “Programmatic Sub-Agent Invocation”When you are writing a custom Tool that needs to delegate to another agent — and especially when you need parent ↔ child state to flow with strict control — use run_child_agent from awaken_runtime::child_agent. It and the auto-generated AgentTool are siblings — both build a BackendDelegateRunRequest and call the same execute_resolved_delegate_execution dispatch — while run_streaming_subagent is a thin wrapper around run_child_agent.
run_child_agent accepts initial_state_seed: Option<PersistedState> for parent → child seeding and returns the child’s BackendRunResult.state (a PersistedState) for the parent tool to decode and surface as a StateCommand on its ToolOutput. State flows back through the same ToolOutput.command channel any other tool uses — there is no separate “sub-agent export” mechanism.
State seeding is Local-backend only. It is not a BackendProfile
capability flag: validate_delegate_execution_request rejects
BackendDelegateRunRequest.state_seed whenever the resolved ExecutionPlan is
non-local. A2A and custom remote backends have no seed-passing wire protocol,
so seeded delegate requests fail with ExecutionBackendError instead of
silently dropping the seed. The child’s BackendRunResult.state is still
available for read-back when the backend returns a result.
Backend implementors should use BackendProfile for typed capabilities such as
continuation, persistence, waits, transcript shape, and output shape. Parent →
child state seed remains a local-execution rule outside that profile.
Background Tasks and Background Agents
Section titled “Background Tasks and Background Agents”Use a background task when work should continue outside the current model step: polling an external job, watching an event stream, running long analysis, or waiting for human/system input. Register BackgroundTaskPlugin and use BackgroundTaskManager::spawn(...) from a tool. The manager persists task metadata in BackgroundTaskStateKey, exposes cancellation, and emits completion/custom events into the owning thread inbox.
Use a background agent when the long-running task is itself an agent loop that should stay addressable. BackgroundTaskManager::spawn_agent_with_context(...) creates a sub_agent task with an inbox, so the parent can send follow-up messages while the child is still running. This is better than synchronous delegation when the child may need multiple turns, late data, or cancellation after the parent has already moved on.
Background work is not a substitute for domain state transfer. Treat task status as lifecycle metadata; pass business state through typed StateKey seed/export policies when the parent and child must share structured state.
Agent-to-Agent Communication
Section titled “Agent-to-Agent Communication”Expose SendMessageTool when agents need to communicate without merging their execution contexts. The tool uses one schema and selects the transport by recipient:
childsends to a live background child task inbox by name or task ID.parentsends to the parent thread through the host’s durable message sink.agentsends to another thread/agent through the same durable sink.
This is better than sharing mutable memory because each message has an explicit recipient, sender, receipt, and failure mode. Use live child messaging for low-latency in-process coordination; use mailbox-backed durable messaging when the recipient may be on another thread, process, or worker.
Sub-Agent Patterns
Section titled “Sub-Agent Patterns”Sequential Delegation
Section titled “Sequential Delegation”The orchestrator calls sub-agents one at a time, using each result to decide the next step:
Orchestrator -> researcher (tool call) -> result -> writer (tool call, using researcher output) -> result -> reviewer (tool call, using writer output) -> resultEach delegation is a tool call within the orchestrator’s step loop. The orchestrator sees tool results and decides whether to delegate further or respond directly.
Parallel Delegation
Section titled “Parallel Delegation”When the LLM emits multiple delegate tool calls in a single inference response,
they use the same ToolExecutor as any other tool call. The built-in resolver
installs SequentialToolExecutor, so delegations run one at a time by default.
Install ParallelToolExecutor with a custom resolver or
ResolvedAgent::with_tool_executor(...) when delegate calls are independent and
should execute concurrently.
Nested Delegation
Section titled “Nested Delegation”Sub-agents can themselves have delegates, creating hierarchies:
orchestrator -> team_lead (delegates: [dev_a, dev_b]) -> dev_a -> dev_bEach level resolves independently through the AgentResolver. There is no hard depth limit, but each level adds latency and token cost.
Agent Handoff
Section titled “Agent Handoff”Handoff transfers control from one agent to another mid-run without stopping the loop. The mechanism:
- A plugin (or the handoff extension) writes a new agent ID to the
ActiveAgentKeystate key. - At the next step boundary, the loop runner detects the changed key.
- The loop re-resolves the agent from the
AgentResolver— new config, new model, new tools, new system prompt. - Execution continues in the same run with the new agent’s configuration.
Handoff is a re-resolve, not a loop restart. Thread history is preserved. The new agent sees all prior messages and can continue the conversation seamlessly.
Handoff vs Delegation
Section titled “Handoff vs Delegation”| Aspect | Delegation | Handoff |
|---|---|---|
| Control flow | Parent calls sub-agent as tool, gets result back | Control transfers entirely to new agent |
| Thread continuity | Sub-agent may use a separate thread context | Same thread, same message history |
| Return path | Result flows back to parent LLM | No return — new agent owns the run |
| Use case | Task decomposition, specialized subtasks | Role switching, escalation, routing |
ExecutionBackend Trait
Section titled “ExecutionBackend Trait”Root execution and delegation both use the canonical ExecutionBackend trait:
pub trait ExecutionBackend: Send + Sync { fn capabilities(&self) -> BackendProfile;
async fn abort(&self, request: BackendAbortRequest<'_>) -> Result<(), ExecutionBackendError>;
async fn execute_root( &self, request: BackendRootRunRequest<'_>, ) -> Result<BackendRunResult, ExecutionBackendError>;
async fn execute_delegate( &self, request: BackendDelegateRunRequest<'_>, ) -> Result<BackendRunResult, ExecutionBackendError>;}BackendRunResult carries the agent ID, status, termination reason, optional response text, structured output, run ID, inbox, run-scoped persisted state, and optional thread-scoped persisted state. BackendRunStatus variants include Completed, WaitingInput, WaitingAuth, Suspended, Failed, Cancelled, and Timeout.
This trait is the extension point for custom local or remote execution backends beyond the built-in local and A2A implementations. awaken_runtime::extensions::a2a still re-exports AgentBackend, AgentBackendFactory, and DelegateRunResult as compatibility aliases, but new code should use the ExecutionBackend names.
See Also
Section titled “See Also”- Invoke a Sub-Agent from a Tool — the operational guide for
run_child_agent - A2A Protocol Reference — wire protocol details
- Architecture — runtime and resolver layers
- Tool and Plugin Boundary — where delegation tools fit