Orchestration¶
Intent and scope¶
The Blueprint's Data Director is one agent a person converses with, which hands work to specialist agents (§9.3). This spec covers how that works here: conversations made of turns, an orchestrator that delegates to other agents only through the harness, and how an orchestrator places agents in the researcher's work by the Blueprint's entry points (§8.1) and process flow (§8.2). The step-by-step mechanics of a delegated run are in the conductor and Architecture §4.
An orchestrator is an ordinary agent whose grounding mode is delegation. Two exist and share one
specification (Message in, Reply out): orchestrator.stub, a fixed, model-free reference that
routes by rules, and orchestrator.model, which lets a model decide. The contract admits either
without change.
Out of scope here: the grounding mode's linter rules and the invocation: canonicalisation
(Evidence and grounding); the grant's wire keys, the callback's envelope encoding
and the child's trace link (Transport and tracing); the registry, manifest and
roster (Agents); which agent the app opens on and how the browser shows a
conversation (The app); a conversation's project and who may see it
(Projects). Governance as an entry point (open, restricted or unshared paths) is
deferred by the Blueprint to v2.0 (Appendix G, comment 1). Creating, reviewing and updating a Data
Management Plan are out of scope for the initial phase, so the figures' steps for them are left
out of the process flow.
Invariants¶
- A turn is one invocation. A conversation is the set of top-level invocations sharing a
conversation_id; a delegated child belongs to the conversation but is not a turn. Each turn is held to every rule any invocation is. (harness/tests/test_conversations.py) - Conversations are derived from the run store, never stored apart from it. Turn order is the order in which the conductor stored the turns.
- The history is part of the input. What an agent was shown of earlier turns travels inside
Message, so the input hash covers it and every transport gives the agent the same document. (agents/orchestrator-stub/tests/test_orchestrator_stub.py,test_history_is_part_of_the_input_hash) - A turn follows the latest. A turn names the turn it follows, and one that does not follow
the conversation's latest stored turn, or that arrives while another turn of the conversation
is running, is refused before anything runs. So a message resent after a dropped connection
never runs twice, two windows cannot fork a conversation, and each turn's history is exactly
the conversation as it stood. The app enforces this at its turn endpoint, where it also builds
the history from the stored turns rather than taking one from the caller. Not yet on
main: TODO(#337, #390). - An orchestrator reaches other agents only through the harness, under a grant. The grant is
live only while the orchestrator's own invocation runs; the harness refuses a callback with an
unknown or expired grant, and a delegation to the orchestrator itself. Each child is an
ordinary invocation: gated by the conductor's profile, input-checked, linted, stored and given
its own trace. (
harness/tests/test_delegation.py) - Delegation is one level deep. A child is never given a grant, and
orchestrator.modeloffers no delegation-mode agent, itself included, as a tool. (test_self_delegation_and_a_second_level_are_refused;agents/orchestrator-model/tests/test_orchestrator.py,test_delegation_agents_and_itself_are_not_tools) - Only the conductor sets lineage. A caller's request carrying
parent_invocation_idis refused. A child's conversation, project and the human it acts for come from the grant, never from whoever sent the callback. The parent'sdelegationsis the conductor's record of the children it ran under that grant, kept when the parent later fails and including a child that finishes after the parent returns. (test_a_caller_may_not_set_lineage_or_use_an_unknown_or_expired_token,test_a_child_acts_for_its_parents_principal_not_its_callers,test_delegations_are_kept_when_the_parent_fails_after_delegating,test_a_child_that_finishes_after_its_parent_is_still_recorded) - An orchestrator has no tools except its delegations. A model-backed orchestrator is offered
one tool per input class of each agent in the harness's manifest and nothing else: no MCP
server, no server tool (web search, web fetch, code execution), no Agent Skills; its package
depends on and imports nothing that could provide one.
(
agents/orchestrator-model/tests/test_orchestrator_only_delegates.py) - An orchestrator learns what it may invoke from the harness's manifest, read through its
grant, and keeps no list of agents of its own. The manifest lists every registered agent; the
policy gate still refuses a child the profile does not enable, and the orchestrator relays that
refusal like any other outcome.
(
test_a_delegation_agent_reads_what_it_may_invoke_from_the_harness_card,test_a_child_refused_by_policy_is_recorded_and_relayed) - An orchestrator succeeds once it has replied, whatever its children's outcomes. A failed
child stays failed on its own envelope and in the parent's
delegations, and the reply says what went wrong and what to try next. The parent fails only when it cannot reply at all: for a model, when the API fails or the model declines. (test_a_failed_or_refused_child_is_reported_and_the_parent_succeeds,test_a_declined_turn_fails_the_run) - The process flow is Blueprint data in the SDK and names no agent. It holds the three entry
points and six phases; each step has a stable id
<phase>.<step>and the requirements its figure caption names, often none. Step ids are unique and never a requirement ID. (sdk/tests/test_process_flow.py) - An agent's
servesnames process-flow steps only. A requirement ID, or any unknown name, is aSpecError. A description withoutservesserves nothing. The requirements an agent bears on are derived from its steps, never declared. (sdk/tests/test_agent_spec.py,test_serving_anything_but_a_step_is_refused) - A step's agents are exactly the agents whose manifest entry serves its id. The SDK's
planand the app shell's phase tracker make the same join; no orchestrator or page keeps a map of its own. (test_an_agent_is_planned_at_the_steps_it_names_and_no_other;app/web/src/flow/serving.test.ts) - A step with no agent is named, with its requirement ("no agent yet", and for example R5),
or with the note that the Blueprint names none. An orchestrator never skips such a step or
does it itself. (
test_entry_two_lists_phases_two_to_six_with_the_agents_and_the_gaps) - On a first turn that names no task, an orchestrator asks which entry point applies, then
follows the phases from that entry. The stub's question and its list of steps are templates
that delegate nothing and rest on the input alone. The model's prompt carries the whole flow
with step ids, tells the model not to show ids to the researcher, names no agent, and is the
same on every run so that it caches.
(
test_the_prompt_carries_every_entry_point_and_phase_and_names_no_agent)
Boundaries and contracts¶
- The harness promises an orchestrator: a grant on every invocation run where it knows its own
A2A address, the manifest of registered agents, and each child's stored envelope to cite. Where
there is no address, as under
workbench invoke, there is no grant andorchestrator.modelabstains. (test_a_delegation_agent_run_without_a_callback_gets_no_delegate) - The orchestrator promises the harness: a reply that cites only its input and the delegations the
conductor recorded (Evidence and grounding), checked against the conductor's
record, not against the
delegatespans the agent reports. - The harness's A2A routes in the app accept a callback under a live grant and nothing else (Transport and tracing).
- An external source such as a registry, a PID service or an MCP server is reached by an agent the orchestrator delegates to, never by the orchestrator.
- Neither the harness nor a browser shell names an orchestrator in code. The deployment's profile names the one the app's conversations go to (Policy and identity, The app).
servesis the agent author's routing hint, "ask me at this step". It is not a conformance claim: whether an agent meets a requirement is recorded indocs/requirements.yamlanddocs/reviews.yamlwith its evidence (Evaluation and conformance).- A step id is a published name once an agent serves it. Renaming or splitting a step breaks the
agents that serve it and is a breaking change to the SDK. Editing
dd_sdk/process_flow.pyalso changes the model orchestrator's prompt, and so its behaviour: treat it as a change to both orchestrators. - Cross-cutting requirements need no step. R10 (provenance) is the harness's work on every run and is never dispatched; R12 has its step. No requirement in §7.1 needs an agent placed outside the flow.
Rationale¶
Governance must not thin out in conversation. The policy gate, the input check, the linter, the store and provenance exist per invocation, so a turn is an invocation and so is every hand-off. A reviewer can open each child on its own, and the parent names each with its agent and version.
Agents are separate services, so an orchestrator cannot reach the conductor in-process; the grant
and callback carry its hand-offs back across the network to the one place that governs them.
The linter trusts only what the harness observed, which is why lineage and delegations are the
conductor's and the child's acting-for comes from the grant.
Two facts place an agent in the researcher's work: the process flow, which is the Blueprint's and the same everywhere, and which agent serves which step, which depends on what a harness has registered. The first is SDK data; the second belongs on the manifest, which is already the only list of agents an orchestrator reads. Agents name steps rather than requirements because requirements are capabilities, not places: R3 applies in Phase 1 and Phase 4, so serving R3 put an agent at both whether or not it helped there, and most steps name no requirement, so an agent written for one had nothing true to declare. A figure step that joins two acts different agents may do is split (sensitive data: identify, then handle), so that an agent can flag without deciding treatment.
An orchestrator succeeds whatever its children's outcomes because mirroring a child's failure on the parent would lose the reply's explanation, while the failure stays visible where it occurred.
Rejected alternatives¶
- The orchestrator calling agents directly over A2A. The children would be ungoverned and unstored, and the parent could cite anything.
- Tools of the orchestrator's own: MCP servers, web search, Agent Skills. A lookup made that way passes no policy gate, leaves no evidence or stored run, and meets no source check. Skills would also need the code-execution container, which adds egress and does not carry over to a local model.
- Grounding mode
noneorinput_onlyfor an orchestrator. It could cite only its input, so a relayed answer would be ungrounded. Moderetrievalrequires a retrieval before any model call, but the orchestrator decides what to delegate before it delegates. - Checking delegations against the agent's
delegatespans. Those are the agent's account; the conductor has its own record. - History in AG-UI
messagesonly. A2A and the CLI would not carry it, and the input hash would not cover it. - A conversation store. It would duplicate what the invocation store holds.
- Routing from the orchestrator's own configuration, or an orchestrator's own map of agents to
steps. It drifts from the registry and duplicates it. (The stub's
routing.yamlremains only as fixed example rules for a model-free reference.) - The manifest in the grant's metadata. Every delegation-mode request would carry every agent's schemas, and the wire keys would change.
servesnaming Blueprint requirements. It misplaced agents at every step naming a requirement, left agents for the many steps that name none with nothing to declare, and merged a routing hint with a conformance claim that nothing checked.
Non-functional requirements¶
- Security: the grant token is a bearer capability. Agent and harness ports stay on the host or a private network until calls between them are authenticated, TODO(#17).
- Content an orchestrator did not write (a researcher's message, a child's reply) is data, not instructions; the model's prompt says so.
- Cost and latency:
orchestrator.modelcaps the delegations in one turn and sends the model only the last turns of the history; the input hash still covers the whole history. An orchestrator's registry timeout must exceed those of the agents it delegates to; nothing checks this. - Turns to one conversation run one at a time; turns to different conversations, and parents with
their children, run side by side.
(
test_async_callers_run_parents_side_by_side_and_their_children_never_wait_on_them) - Performance: conversations are found by scanning the run store, TODO(#141).
Open questions¶
- Mapping A2A
context_idtoconversation_id; delegation more than one level deep; delegation from the CLI; whether children inheritrequirement_ids; checking a caller's history against the store on transports other than the app, TODO(#253). - Whether an orchestrator keeps the researcher's place in the flow across turns. The stub reads only the current message and lists the steps once, TODO(#203).
- The steps come from the figure captions in §8.2; the figures may show more, TODO(#245).
- Step ids as the vocabulary in which a deployment's policy names the steps that need a person's sign-off, TODO(#308); proposing the ids to the RDA working group, TODO(#310).
- The orchestrator's context about its deployment and the researcher's project, to be supplied by the harness rather than as a tool, TODO(#341, #160).
- A read-only check of a deposit against an existing Data Management Plan, TODO(#284).
- Streaming replies, TODO(#159).