Orchestration¶
Multi-agent orchestration for complex conversational workflows. See the Multi-Agent Orchestration guide for usage examples.
Strategies¶
Orchestration ¶
Bases: ABC
Abstract base for orchestration strategies.
Orchestration strategies compose existing primitives
(ConversationPipeline, ConversationRouter, HandoffHandler)
into declarative patterns that can be passed to RoomKit or
create_room.
Subclasses must implement:
- :meth:
agents— which agents participate in the room. - :meth:
install— wire hooks, tools, and state into a room.
agents
abstractmethod
¶
Return agents to register and attach to the room.
The framework calls this to determine which agents should be registered on the kit and attached to the room at creation time. An external agent over ACP is one where the strategy takes it (a discussion does).
install
abstractmethod
async
¶
Wire hooks, tools, and state into the room.
Called after agents are registered and attached. Implementations
should install room-scoped hooks, set up handoff tools, and
initialise conversation state. Install through
:meth:RoomKit.install_strategy (or create_room(orchestration=...)),
which claims the room for one strategy and records what the install
adds, so :meth:RoomKit.uninstall_strategy can take it back.
uninstall
async
¶
Undo what :meth:install set up that the kit does not record.
:meth:RoomKit.uninstall_strategy calls it first, then removes what it
recorded of the install: the room hooks, the tools and turn runners set
up on the agents for the room, the agents the install attached, and the
room metadata the install wrote (RFC §19.7). A strategy with state of
its own elsewhere (a discussion's turns) stops it here. Default: nothing.
Pipeline ¶
Pipeline(agents, routing=None, *, supervisor_id=None, voice_channel_id=None, greet_on_handoff=False, greeting_prompt=None, farewell_prompt=None)
Bases: Orchestration
Linear pipeline orchestration strategy.
Agents are chained in order: the first agent is the entry point, each subsequent agent is reachable via handoff from its predecessor.
Example::
kit = RoomKit(
orchestration=Pipeline(
agents=[triage, support, billing],
),
)
room = await kit.create_room()
Initialise the pipeline strategy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agents
|
list[Agent]
|
Ordered list of agents. First agent is the entry point. Stages chain linearly (each agent hands off to the next). |
required |
routing
|
dict[str, list[str]] | None
|
Optional keyword hints injected into agent system prompts (not runtime rules). Keys are agent channel IDs, values are keyword lists describing when to route there. |
None
|
supervisor_id
|
str | None
|
Optional supervisor agent ID that receives all events for monitoring. |
None
|
voice_channel_id
|
str | None
|
Voice channel ID for voice-aware handoffs. |
None
|
greet_on_handoff
|
bool
|
Whether agents greet on handoff. |
False
|
greeting_prompt
|
str | None
|
Custom greeting prompt template. |
None
|
farewell_prompt
|
str | None
|
Custom farewell prompt template. |
None
|
Swarm ¶
Bases: Orchestration
Swarm orchestration strategy.
Every agent can hand off to every other agent with no phase constraints. Routing uses sticky agent affinity — once an agent is active, it keeps handling until it hands off.
Example::
kit = RoomKit(
orchestration=Swarm(
agents=[sales, support, billing],
entry="sales",
),
)
room = await kit.create_room()
Initialise the swarm strategy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agents
|
list[Agent]
|
All participating agents. |
required |
entry
|
str | None
|
Channel ID of the entry-point agent. Defaults to the first agent's channel ID. |
None
|
Supervisor ¶
Supervisor(supervisor, workers, *, strategy=None, auto_delegate=False, async_delivery=False, refine_task=True, refine_instruction=None, delegation_message="I'm dispatching my team to work on this.", wait_for_result=True, share_channels=None, task_timeout=_DEFAULT_TASK_TIMEOUT_SECONDS, max_revisions=_DEFAULT_MAX_REVISIONS)
Bases: _AutoDelegateInstallMixin, _StrategyToolMixin, _PerWorkerToolMixin, Orchestration
Supervisor orchestration strategy.
The supervisor handles all user interaction. Workers are registered
on the kit (so delegate() can find them) but are NOT attached
to the parent room — they run in child rooms.
Examples::
# Framework-driven: auto-delegate with task refinement
Supervisor(
supervisor=coordinator,
workers=[researcher, writer],
strategy="sequential",
auto_delegate=True,
)
# Framework-driven: workers get raw user message
Supervisor(
supervisor=coordinator,
workers=[technical, business],
strategy="parallel",
auto_delegate=True,
refine_task=False,
)
# Tool-based: AI decides when to delegate
Supervisor(
supervisor=coordinator,
workers=[researcher, writer],
strategy="sequential",
)
# Manual: per-worker tools, AI decides everything
Supervisor(
supervisor=coordinator,
workers=[researcher, writer],
)
Initialise the supervisor strategy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
supervisor
|
Agent
|
The agent that handles user interaction and delegates tasks. |
required |
workers
|
list[Agent]
|
Agents that run delegated tasks in child rooms. |
required |
strategy
|
WorkerStrategy | str | None
|
Deterministic execution pattern for workers.
|
None
|
auto_delegate
|
bool
|
If |
False
|
async_delivery
|
bool
|
If
Lifecycle events ( |
False
|
refine_task
|
bool
|
Controls whether the framework extracts a clean topic from the user's message before sending to workers. |
True
|
refine_instruction
|
str | None
|
Custom instruction for topic extraction. Overrides the default. |
None
|
delegation_message
|
str | None
|
Message injected into the conversation
when workers are dispatched (async mode only). Set to
|
"I'm dispatching my team to work on this."
|
wait_for_result
|
bool
|
When strategy is |
True
|
share_channels
|
list[str] | None
|
Channel IDs from the parent room to share
with every child room created during delegation. For
example, passing |
None
|
task_timeout
|
float
|
Per-worker delegation budget in seconds (default 120). Each delegated task is bounded individually so the chain is governed per-task rather than by a single global timeout that can never be coherent across team sizes. A worker that exceeds it is recorded as failed and the run continues. |
_DEFAULT_TASK_TIMEOUT_SECONDS
|
max_revisions
|
int
|
In supervised sequential mode, the maximum number of supervisor→worker rework round-trips per step (default 3). The supervisor validates each worker's output; if unacceptable it sends feedback for a rework, up to this many times. On exhaustion the step is delivered flagged as unvalidated rather than looping. |
_DEFAULT_MAX_REVISIONS
|
agents ¶
Return agents to attach to the room.
async_delivery removes the supervisor from the room ONLY
in voice auto_delegate mode, where a
RealtimeVoiceChannel handles user interaction directly.
In strategy-tool or per-worker-tool modes the supervisor
still drives the conversation via its tool loop; background
dispatch only affects worker delegation, not user routing.
Loop ¶
Loop(agent, reviewers=None, reviewer=None, max_iterations=3, *, strategy=None, async_delivery=False)
Bases: Orchestration
Loop orchestration strategy.
The producing agent generates output, then reviewers evaluate it. If all reviewers approve, the loop ends. Otherwise, feedback is routed back to the producer for revision.
Examples::
# Single reviewer
Loop(agent=writer, reviewers=[editor], max_iterations=3)
# Multiple reviewers — sequential (chained)
Loop(
agent=coder,
reviewers=[security, perf, style],
strategy="sequential",
)
# Multiple reviewers — parallel (fan-out)
Loop(
agent=coder,
reviewers=[security, perf, style],
strategy="parallel",
)
# Voice — async delivery
Loop(
agent=writer,
reviewers=[editor],
async_delivery=True,
)
Initialise the loop strategy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent
|
Agent
|
The producing agent. |
required |
reviewers
|
list[Agent] | None
|
List of reviewing agents. For multiple reviewers, use strategy to control execution order. |
None
|
reviewer
|
Agent | None
|
Single reviewer (convenience, same as
|
None
|
max_iterations
|
int
|
Maximum number of produce-review cycles. |
3
|
strategy
|
WorkerStrategy | str | None
|
How reviewers execute when there are multiple:
|
None
|
async_delivery
|
bool
|
If |
False
|
Discussion ¶
Discussion(agents, *, people=None, addressed_only=False, everyone=None, max_turns=None, max_depth=None, done=None, silent_token='(silent)', dispatch=None, dispatch_timeout=5.0)
Bases: Orchestration
Several agents and people hold one conversation; one agent speaks at a time.
Any agent may address any other by @channel_id, a person may address
any agent, and who speaks next follows from who was addressed: the
strategy queues the agents each message asks for and gives them their
turns one at a time, each turn reading the room as it is when it starts.
The room is the discussion's: its intelligence channels are the
discussion's agents and no other, and no router or other strategy shares
it. Text only in this version.
Example::
kit = RoomKit()
room = await kit.create_room(
orchestration=Discussion(agents=[investigator, dev, sre]),
)
# "@dev can you check the deploy?" asks dev; dev's "@sre please
# roll back" asks sre once dev's turn has ended.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agents
|
Sequence[AIChannel | ACPChannel]
|
The agents of the discussion: AI channels, Agents among them, with distinct channel ids. |
required |
people
|
Sequence[str] | None
|
The names agents address people by. |
None
|
addressed_only
|
bool
|
A person's message asks only the agents it names; one that names nobody asks no agent. |
False
|
everyone
|
Sequence[str] | None
|
The agents a person's message that answers no one asks, in
this order (the one that should open first). |
None
|
max_turns
|
int | None
|
Turns given in the room's lifetime, after which the
discussion is over. |
None
|
max_depth
|
int | None
|
The depth limit of the turns it gives, in place of the
kit's |
None
|
done
|
DoneFn | None
|
|
None
|
silent_token
|
str
|
The answer of an agent with nothing to add. |
'(silent)'
|
dispatch
|
DispatchPolicy | None
|
Decides which agents take a person's message that names
none and answers none, in place of asking |
None
|
dispatch_timeout
|
float
|
Seconds a decision may take; past them, or when the
policy fails, the message asks |
5.0
|
install
async
¶
Give the room to the discussion: its agents attached, its stored queue read, its turns started.
Raises:
| Type | Description |
|---|---|
ValueError
|
the room binds another intelligence channel or a voice or realtime channel, an agent thinks while it listens, or a router, another strategy or a discussion is installed. |
uninstall
async
¶
Give the room back to its agent_response_policy: the turns
stopped, the queue dropped.
Discussion's speak queue¶
SpeakQueue
dataclass
¶
A room's speak queue as the host reads it (RFC §19.7.5).
asked
instance-attribute
¶
(agent, person): an agent that asked a person and waits for the answer.
SpeakQueueChange ¶
Bases: StrEnum
What changed in a room's speak queue, as ON_SPEAK_QUEUE reports it.
SpeakQueueEvent
dataclass
¶
Carried to ON_SPEAK_QUEUE hooks: one change of a room's speak queue.
Discussion's dispatch policy¶
DispatchPolicy ¶
Bases: ABC
Decides which agents of a discussion take a person's unaddressed message.
A policy that raises, or does not decide within the discussion's
dispatch_timeout, does not silence the room: the message asks what it
asks with no policy (the agents that asked its author, else every
candidate), and the decision reported carries the reason fallback. A
message that names an agent never reaches a policy: a name is the
person's word.
decide
abstractmethod
async
¶
The decision on turn: agents among its candidates, in order.
DispatchTurn
dataclass
¶
What a policy judges: the message, the conversation before it, the candidates.
recent
class-attribute
instance-attribute
¶
The conversation before it, oldest first: the room's messages a candidate may read, without tool records.
speakers
class-attribute
instance-attribute
¶
Who said event and each of recent, by event id: a person by the
label the transcript gives them (RFC §6.4), an agent as @channel_id.
candidates
class-attribute
instance-attribute
¶
The agents the policy may pick: those that asked the message's author,
then everyone in its order; only those the message reaches, less
those that only listen.
asked
class-attribute
instance-attribute
¶
The candidates that asked the message's author a question and wait for the answer (RFC §19.7.5 rule 10): the message may answer them, or not.
DispatchCandidate
dataclass
¶
An agent a policy may pick, with the identity the host gave it.
description
class-attribute
instance-attribute
¶
What the agent does, as a turn's notes tell the others (rule 14).
DispatchDecision
dataclass
¶
Which agents take a message, in the order they answer, and why.
agents
class-attribute
instance-attribute
¶
Their channel ids; empty, no agent takes the message.
reason
class-attribute
instance-attribute
¶
Why, in a few words, for logs and ON_DISPATCH_DECISION.
judgments
class-attribute
instance-attribute
¶
What the policy weighed, by name: what makes a decision measurable.
DispatchDecisionEvent
dataclass
¶
Carried to ON_DISPATCH_DECISION hooks: one decision on one message.
duration_ms
class-attribute
instance-attribute
¶
How long the policy took: the discussion's bound when it did not decide in time.
ClassifierDispatchPolicy ¶
Bases: DispatchPolicy
Picks the candidates a classifier judges the message is for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
classifier
|
Classifier
|
Answers the questions, all candidates in one call
( |
required |
threshold
|
float
|
The probability of yes a candidate needs. None reaching it: no agent takes the message (thanks, small talk). |
0.5
|
max_agents
|
int
|
How many candidates take it at most, likeliest first. |
2
|
recent
|
int
|
How many earlier messages the classifier reads. |
8
|
MockDispatchPolicy ¶
Bases: DispatchPolicy
Decides from a script: each call takes the next decision, given as a
:class:DispatchDecision or as the agents' channel ids, and the last one
repeats. With no script, the message asks what it asks with no policy:
the candidates that asked its author, else every candidate. delay
holds each decision back, error is raised instead. Records every turn.
Agents¶
Agent ¶
Agent(channel_id, *, provider=None, name=None, role=None, description=None, purpose=None, scope=None, voice=None, greeting=None, language=None, auto_greet=True, identity_in_prompt=True, **kwargs)
Bases: AIChannel
AI agent with structured identity metadata.
Extends :class:AIChannel with role, description, scope,
voice, and greeting fields. The first three are auto-injected
into the system prompt as an identity block, unless
identity_in_prompt=False (a host that renders identity in its own
prompt keeps the fields readable without the block); voice is read by
:meth:ConversationPipeline.install to auto-wire the voice map;
greeting is spoken directly via TTS when a new voice session
becomes ready (controlled by auto_greet).
When provider is omitted the agent is config-only — it holds
identity and prompt data for speech-to-speech orchestration via
:class:RealtimeVoiceChannel but cannot generate responses itself.
Example::
# Full agent (STT → LLM → TTS)
triage = Agent(
"agent-triage",
provider=GeminiAIProvider(config),
role="Triage receptionist",
description="Routes callers to the right specialist",
voice="21m00Tcm4TlvDq8ikWAM",
system_prompt="Greet callers warmly.",
)
# Config-only agent (speech-to-speech)
triage = Agent(
"agent-triage",
role="Triage receptionist",
description="Routes callers to the right specialist",
voice="Aoede",
system_prompt="Greet callers warmly.",
)
build_identity_block ¶
Public access to :meth:_build_identity_block for orchestration callers.
ConversationRouter ¶
Routes events to the appropriate agent.
Installed as a BEFORE_BROADCAST sync hook (priority -100) that stamps routing metadata on events. The EventRouter reads this metadata to skip non-targeted intelligence channels.
Usage::
router = ConversationRouter(
rules=[...],
default_agent_id="agent-triage",
supervisor_id="agent-supervisor",
)
kit.hook(
HookTrigger.BEFORE_BROADCAST,
execution=HookExecution.SYNC,
priority=-100,
)(router.as_hook())
Initialize the router.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rules
|
list[RoutingRule] | None
|
Routing rules evaluated in ascending priority order (lower number = evaluated first, i.e. higher precedence). |
None
|
default_agent_id
|
str | None
|
Fallback agent when no rule matches. |
None
|
supervisor_id
|
str | None
|
Agent that receives escalations. |
None
|
select_agent ¶
Determine which agent should handle this event.
Answers for unaddressed events only. An addressed event never
reaches here — :meth:as_hook returns before calling this, because
an address outranks every rule below (RFC §19.4, step 0).
Priority order: 1. If active_agent_id is set and the agent is still in the room -> sticky affinity (agent keeps handling) 2. Evaluate rules in priority order -> first match wins 3. Fall back to default_agent_id
as_hook ¶
Return a BEFORE_BROADCAST sync hook function.
The hook stamps event.metadata with routing information
that EventRouter uses to filter intelligence channels — except on an
addressed event, which the router leaves alone (RFC §19.4, step 0).
install ¶
Wire routing and handoff in one call.
Registers this router as a BEFORE_BROADCAST sync hook,
builds a HandoffHandler, and calls setup_handoff
on every agent.
Returns the HandoffHandler for further customisation.
RoutingRule ¶
Bases: BaseModel
A routing rule mapping conditions to an agent.
Rules are evaluated in ascending priority order (lower values first).
The first matching rule wins. Use negative priorities to ensure a rule
is evaluated before the default 0, or positive values to defer it.
priority
class-attribute
instance-attribute
¶
Evaluation order — lower values are checked first (default 0).
RoutingConditions ¶
Bases: BaseModel
Conditions for a routing rule to match.
All specified conditions are ANDed — every non-None field must match.
Conversation State¶
ConversationState ¶
Bases: BaseModel
Tracks conversation progress within a room.
All fields are optional with sensible defaults so rooms without orchestration have zero overhead.
transition ¶
Create a new state with a phase transition recorded.
Returns a new instance (immutable pattern) — the original is never modified.
ConversationPhase ¶
Bases: StrEnum
Built-in conversation phases.
Users can use custom string values — routing and state do not restrict phases to this enum.
PhaseTransition ¶
Bases: BaseModel
Audit record for a phase change.
get_conversation_state ¶
Extract typed ConversationState from room metadata.
Returns a fresh default state if no orchestration state exists.
set_conversation_state ¶
Return a room copy with updated conversation state.
Does NOT persist: :func:save_conversation_state does, without a full
room write.
save_conversation_state
async
¶
Persist state as room_id's conversation state, writing its metadata key alone: a full room write from a room read earlier would undo what was written since, the room's register of authors among it (RFC §10.1 step 12).
Raises:
| Type | Description |
|---|---|
RoomNotFoundError
|
room_id does not exist. |
Pipeline¶
ConversationPipeline ¶
Generates routing rules for sequential agent workflows.
Example::
pipeline = ConversationPipeline(
stages=[
PipelineStage(phase="analysis", agent_id="agent-discuss", next="coding"),
PipelineStage(phase="coding", agent_id="agent-coder", next="review"),
PipelineStage(phase="review", agent_id="agent-reviewer",
next="report", can_return_to={"coding"}),
PipelineStage(phase="report", agent_id="agent-writer", next=None),
],
supervisor_id="agent-supervisor",
)
router = pipeline.to_router()
get_allowed_transitions ¶
Return phase -> allowed next phases for validation.
install ¶
install(kit, agents, *, agent_aliases=None, hook_priority=-100, greet_on_handoff=False, voice_channel_id=None, greeting_prompt=None, farewell_prompt=None)
Wire routing and handoff in one call.
Creates a router from this pipeline, registers it as a
BEFORE_BROADCAST sync hook, builds a HandoffHandler
with the pipeline's phase map and transition constraints,
and calls setup_handoff on every agent.
When greet_on_handoff is True, two extra hooks are
registered:
- ON_HANDOFF (async): blocks the old agent's farewell via
a
BEFORE_TTSflag, then sends anINSTRUCTIONaddressed to the new agent on voice_channel_id to prompt it to greet — never stored as the caller's words (RFC §10.1.1). - BEFORE_TTS (sync): blocks TTS while a handoff is pending.
Returns (router, handler) for further customisation.
PipelineStage ¶
Bases: BaseModel
A stage in a conversation pipeline.
Handoff¶
HANDOFF_TOOL
module-attribute
¶
HANDOFF_TOOL = AITool(name=HANDOFF_TOOL_NAME, description="Transfer this conversation to another agent or specialist. Use when: the user asks to speak with someone else, the user wants to be transferred or go back to a previous agent, the conversation needs expertise you don't have, or your task is complete and the next step requires a different agent. Always provide a clear summary of the conversation so far.", parameters={'type': 'object', 'properties': {'target': {'type': 'string', 'description': "Target agent ID or alias (e.g., 'agent-advisor', 'agent-coder', 'human')"}, 'reason': {'type': 'string', 'description': 'Why the handoff is needed'}, 'summary': {'type': 'string', 'description': 'Summary of the conversation so far and what the next agent needs to know to continue effectively'}, 'next_phase': {'type': 'string', 'description': "Optional: conversation phase to transition to (e.g., 'handling', 'review', 'escalation')"}, 'channel_escalation': {'type': 'string', 'enum': ['same', 'voice', 'email', 'sms'], 'description': "Whether to escalate to a different channel. 'same' keeps the current channel."}}, 'required': ['target', 'reason', 'summary']})
build_handoff_tool ¶
Build a handoff tool with constrained target enum.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
targets
|
list[tuple[str, str | None]]
|
List of |
required |
Returns:
| Name | Type | Description |
|---|---|---|
An |
AITool
|
class: |
AITool
|
restricting the AI to only the listed agent IDs. |
HandoffHandler ¶
HandoffHandler(kit, router, agent_aliases=None, phase_map=None, allowed_transitions=None, known_agents=None, on_handoff_complete=None, event_channel_id=None)
Processes handoff tool calls.
Intercepts the handoff_conversation tool, updates conversation
state, emits a system event, and returns the result to the agent.
on_handoff_complete
property
writable
¶
Callback invoked after a successful handoff.
get_room_language ¶
Get effective language: room override > agent default.
handle
async
¶
Process a handoff request.
Steps: 1. Resolve target agent (alias -> channel_id) 2. Validate target exists in room 3. Update ConversationState 4. Persist state to room metadata 5. Emit system event in room 6. Return result to calling agent
send_greeting
async
¶
Send the initial agent greeting for a room.
Looks up the current active agent from conversation state and has it
say its greeting through :meth:RoomKit.send_greeting, which
stores it as the agent's line and delivers it as one: injected with
the assistant intent into each :class:RealtimeVoiceChannel
session (RFC §12.4), otherwise broadcast to the room so a voice
channel speaks it. A greeting is never sent as something the user
said, which the agent would answer. The room's language, when set,
reaches a realtime session first as a silent instruction.
Call this after setting the initial conversation state::
await save_conversation_state(kit.store, room_id, ConversationState(...))
await handler.send_greeting(room_id, channel_id="voice")
Does nothing when the active agent has no greeting configured.
set_language
async
¶
Change the conversation language for a room.
Stores the language in conversation state and, for realtime sessions, reconfigures the active agent's session with an updated system prompt that includes the language instruction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
The room to update. |
required |
language
|
str
|
Language name or code (e.g. |
required |
channel_id
|
str | None
|
Voice channel ID. Falls back to the event channel
configured by |
None
|
HandoffRequest ¶
Bases: BaseModel
Parsed from agent's handoff tool call arguments.
HandoffResult ¶
Bases: BaseModel
Result returned to the calling agent.
HandoffMemoryProvider ¶
Bases: _MemoryWrapper
Injects handoff context (summary, reason) into agent prompts.
Wraps an inner MemoryProvider and prepends handoff information when the conversation state indicates a recent handoff. Its window, ingestion and lifecycle are the inner provider's.
setup_handoff ¶
Serve the handoff tool on an AIChannel.
- Declares the handoff tool in the channel's turns: in every room it serves, or in room_id's only (a strategy installed in that room)
- Serves
handoff_conversationcalls with handler - Hands off in the room of the call, read from the tool call context (RFC §19.6): one agent serves every room it is attached to
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
channel
|
AIChannel
|
The AI channel to wire handoff into. |
required |
handler
|
HandoffHandler
|
The handoff handler that processes tool calls. |
required |
tool
|
AITool | None
|
Optional custom handoff tool (e.g. from :func: |
None
|
room_id
|
str | None
|
The one room the handoff serves, when a strategy installs
it for that room (RFC §19.7). |
None
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
The handoff is already set up where it would be declared. |
Structured results¶
Delegated workers can hand their result back through the submit_result tool
instead of a free-text message scraped from the room — a forced, parseable
handoff. See Structured results
in the Agent Delegation guide. Enabled per call via
kit.delegate(require_structured_result=True), and used internally by the
supervised sequential flow.
kit.delegate(result_tool=...) forces another tool than submit_result, by
passing a ResultTool: the supervised flow's verdict comes back through
submit_verdict this way. The capture is scoped to the child room, so two
delegations to the same agent at once never read each other's result.
ResultTool
dataclass
¶
The tool a delegated agent must call to hand its work back, and how it is read.
A delegation run with require_structured_result injects :attr:tool,
captures its call, reminds the agent with :attr:reminder when a turn ends
without it, and gives up after the retries with :attr:on_missing. The call
is how the structure is forced, so it works with any provider that calls
tools, whatever its support for a response schema. :data:SUBMIT_RESULT (a
worker's result) is the default; the supervised flow hands its verdict back
through its own tool.
Attributes:
| Name | Type | Description |
|---|---|---|
tool |
AITool
|
The tool injected into the agent for the delegation. |
normalize |
Callable[[dict[str, Any]], dict[str, Any]]
|
Turns the call's arguments into the payload returned. |
on_missing |
MissingResult
|
Builds the payload returned when the agent never called the
tool, from |
reminder |
str
|
The message sent when a turn ends without the call. |
MissingResult ¶
Bases: Protocol
Builds the payload returned when a delegated agent never called its tool.
SUBMIT_RESULT
module-attribute
¶
SUBMIT_RESULT = ResultTool(tool=SUBMIT_RESULT_TOOL, normalize=normalize_result, on_missing=orchestration_fail, reminder='You did not submit a result. You MUST now call the `submit_result` tool with your final structured result. Do NOT reply with plain text or a question — call submit_result.')
SUBMIT_RESULT_TOOL
module-attribute
¶
SUBMIT_RESULT_TOOL = AITool(name=SUBMIT_RESULT_TOOL_NAME, description='Submit your FINAL result for this task. You MUST call this exactly once, when your work is done — it is the ONLY way to hand your work to the next step. Do NOT end your turn with a plain message or a question back to the user; call submit_result with your structured result.', parameters={'type': 'object', 'properties': {'status': {'type': 'string', 'enum': ['completed', 'failed'], 'description': 'completed if you did the task; failed only if you genuinely could not.'}, 'summary': {'type': 'string', 'description': 'One or two sentences summarizing what you produced.'}, 'data': {'type': 'object', 'description': 'Your structured result, for the next step to build on.'}, 'deliverables': {'type': 'array', 'items': {'type': 'object', 'properties': {'title': {'type': 'string'}, 'url': {'type': 'string'}}}, 'description': 'Concrete artifacts you produced (e.g. a published report URL).'}, 'reason': {'type': 'string', 'description': 'If status is failed, explain why.'}}, 'required': ['status', 'summary']})
is_submit_result ¶
Whether tool_name is a submit_result call.
A function-calling provider calls the injected tool by its bare name; a
claude_code worker calls the gateway-exposed tool, which the sandbox surfaces
with an MCP prefix (mcp__<server>__submit_result). Match both so
the capture is delivery-agnostic.
normalize_result ¶
Coerce a submit_result tool-call payload into the canonical shape.
orchestration_fail ¶
Fail payload the orchestration submits on a worker's behalf when it never
called submit_result after attempts tries.
Distinct from a worker self-reporting a task failure (status="failed" via
the tool): by="orchestration" marks a MECHANISM-level failure, carrying
the worker's last raw output as explanatory context so the next step and the
supervisor understand precisely what went wrong.