Overview
Use spec.agents to declare the other agents this agent may call. Each entry names a target by ref, an optional wire name, and the shape of the result returned to the model. The parent model sees a normal tool; the runtime dispatches the call to a fresh child session running the pinned target snapshot.
| Topic | Chapter |
|---|---|
Multi-agent packaging | |
Worker- and MCP-backed tools | |
Dispatch and recovery | |
Run-wide caps | |
Inherited credentials |
Minimal example
This orchestrator lives inside a Bundle and delegates to a vendored specialist by local path:
apiVersion: phrony.com/v1
kind: Agent
metadata:
name: orchestrator
namespace: support
version: 1.0.0
secrets:
anthropic:
fromEnv: ANTHROPIC_API_KEY
spec:
purpose: Route customer questions and delegate billing to a specialist.
instructions:
text: You triage support requests and delegate billing questions.
model:
provider: anthropic
name: claude-sonnet-4-5
agents:
- ref: ./specialists/billing.yaml
as: ask_billing
description: Delegate billing questions; returns a resolved answer.The parent model now has an ask_billing tool. When it calls that tool, the runtime starts a nested session on the pinned billing snapshot from the bundle closure and returns the specialist's final answer as the tool result.
spec.agents
Each entry declares one target agent this agent may delegate to.
| Field | Required | Description |
|---|---|---|
ref | Yes | Ref kind—local path, pinned external id, or floating external with late_bound |
as | No | Wire name shown to the parent model; defaults to a sanitized form of ref |
description | No | Tool description presented to the parent model |
input_schema | No | ref or inline JSON Schema for the call arguments; defaults to a single { "task": string } contract |
result | No | |
policies | No | Policy documents gating the delegation call (logical refs or bundle file refs) |
late_bound | No | Opt in to live active-deployment resolution at call time; excluded from bundle closure |
Ref kinds
ref classifies into three forms. Unpinned external refs are rejected unless late_bound: true.
| Form | Example | Closure | Runtime dispatch |
|---|---|---|---|
Local path | ./specialists/billing.yaml | Included; vendored member | Frozen agent_version_id from bundle publish |
Pinned external | billing.refunds@1.4.0 | Included; external pin | Frozen agent_version_id from bundle publish |
Late-bound external | billing.refunds with late_bound: true | Excluded from closure | Active deployment at call time |
Local paths must resolve inside the bundle root. External refs must include @semver unless late_bound is set.
Default input schema
When you omit input_schema, the binding presents a single required string argument:
input_schema:
inline:
type: object
properties:
task: { type: string }
required: [task]Supply your own ref or inline schema to give the parent model a richer, typed call contract. The parent's tool-call arguments become the child session's input.
Result shape
result controls what the runtime returns to the parent model as the tool result:
| Value | Returned payload |
|---|---|
summary (default) | { "output": <child final message> }—just the specialist's final answer |
full | The child's entire session output: final message, token usage, and per-turn trace |
Compilation
At bundle publish, spec.agents is sugar that compiles into spec.tools. Each entry expands into a normal tool binding with an agent block and the authoring spec.agents list is cleared from the resolved snapshot (the same way policy attachments become compiled-only). The expanded binding:
- Routes dispatch to a nested child agent session instead of a worker or MCP server
- Carries a frozen
agent_version_id(andchild_namefor vendored targets) set during the closure walk - Is assigned
side_effect_class: non_idempotent_write, so recovery never blindly re-dispatches a side-effecting child after an indeterminate outcome - Flows through the standard pipeline: model tool definitions, policy gates, HITL, limits, tracing, and recovery
# Authoring (spec.agents)
spec:
agents:
- ref: ./specialists/billing.yaml
as: ask_billing
# Resolved snapshot (compiled into spec.tools)
spec:
tools:
- ref: support.billing
as: ask_billing
side_effect_class: non_idempotent_write
input_schema:
inline:
type: object
properties:
task: { type: string }
required: [task]
agent:
namespace: support
name: billing
version: <content-hash>
child_name: billing
agent_version_id: <frozen-uuid>Nested session execution
When the parent model calls a delegation tool, the runtime:
- Enforces the depth cap—
depth + 1must not exceedmax_subagent_depth. Over the cap returns a tool error to the model rather than failing the run. - Resolves the target: uses the compiled
agent_version_idfor pinned bindings, or the active deployment forlate_boundbindings only. - Creates a durable child session linked to the parent (
parent_session_id) atdepth + 1, inheriting secrets by name and mapping the tool-call arguments to the child input. - Drives the child to completion synchronously, bounded by the parent's remaining wall-clock budget.
- Returns the child's final output as the tool result, per the binding's
resultshape.
A child agent with its own spec.agents recurses naturally at depth + 2, since it runs through the same path.
Secrets for child sessions
Each agent declares only the secrets it uses. For bundle runs, the runtime unions requirements across the frozen closure, validates the full union on the root session, and stores values in a bundle secret pool on the root session. Child sessions inherit by name from that pool (not hop-by-hop from the immediate parent), so a specialist can declare anthropic even when the root orchestrator only declares openai.
For single-agent runs (no bundle), child inheritance still copies from the parent session encrypted store; the root run must supply every secret name used in that tree.
See Secrets for how secrets are declared and resolved. Use phrony bundles secret-requirements to inspect the deployed bundle union before running.
Limits across the tree
Delegation limits span the whole tree, not a single session:
| Limit | Behavior across delegation |
|---|---|
Depth | spec.limits.max_subagent_depth caps nesting; see below |
Wall-clock | The parent's remaining max_wall_clock_seconds budget bounds each child, so a child cannot outlive the parent |
Tokens | Child token usage is returned to the parent and counts toward the parent's max_tokens_per_run accounting |
Depth limit
Set the maximum delegation nesting on spec.limits:
spec:
limits:
max_subagent_depth: 3 # A -> B -> C; D would be rejected| Rule | Notes |
|---|---|
max_subagent_depth | Integer >= 1; defaults to 5 when unset |
Over the cap | The delegation call returns a tool error to the parent model; the run continues |
Policies and human-in-the-loop
Because a delegation compiles to a tool binding, the entire Policy surface applies. Attach policies on the spec.agents entry (or agent-wide via default_policies) to require approval, escalate, or deny a delegation before the child session starts:
spec:
agents:
- ref: ./specialists/billing.yaml
as: ask_billing
policies:
- support.delegation-approvalApproval, escalate, and deny decisions evaluate before any child session is created—see Policy and Human-in-the-loop approvals.
Tracing and recovery
- Tracing: the parent emits
tool_callandtool_resultevents for the delegation exactly like any other tool. The child is a full session with its own events, linked to the parent byparent_session_id, so the trace spans the whole tree with no new vocabulary. - Recovery: the delegation is recorded in the tool-invocation ledger and the child session is durable, keyed by the originating call id. On restart, a completed child returns its stored output; an in-flight child is resumed rather than duplicated. Since agent bindings are
non_idempotent_write, the parent will not auto-redispatch a child whose outcome is indeterminate—it routes to side-effect recovery instead. See Tool dispatch.
Validation rules
| Rule | Error when violated |
|---|---|
Standalone publish | spec.agents present on an agent published outside a Bundle |
agents[].ref | Missing, or not a local path or namespace.name[@version] |
agents[].ref local | Path resolves outside bundle root |
agents[].ref external | Missing @version unless late_bound: true |
agents[].ref version | @version constraint is not valid semver |
agents[].ref uniqueness | The same target is declared twice |
Self-reference | ref names the declaring agent (namespace.name) |
Wire name | as (or the derived name) must match [a-zA-Z0-9_-]{1,64} and not collide with a spec.tools binding or another delegation |
agents[].result | Value other than summary or full |
agents[].input_schema | Invalid schema object |
spec.limits.max_subagent_depth | Less than 1 |
spec.tools[].agent | Set on an authoring manifest (compiled-only field) |
phrony bundles validate checks these locally before publish. When bundle.lock.json is present, validation also compares the committed lock to the recomputed closure; use --require-lock in CI to reject missing locks.
Up next