Agent Execution & Multi-Agent Orchestration
Intendant runs a task through four execution shapes. Direct is one native loop; Orchestrate is that loop with the delegation prompt; Sub-Agent is a supervised child; External-Agent hands the task to a third-party coding harness (Codex, Claude Code, Kimi Code, or Pi) and supervises it — that path has its own chapter, External-Agent Orchestration.
This chapter covers Intendant’s native execution: how a session’s shape is chosen, and the sub-agent machinery (the spawn/wait/submit tools, supervised child sessions, worktrees, role prompts, and Memory/checkpoint coordination).
Historically these were separate process modes — orchestration ran as a
subprocess pipeline (run_user_mode → INTENDANT_ROLE=… child processes →
result files polled from disk). That February-era pipeline is gone. Everything
below runs in-process on the session supervisor: one substrate for direct
sessions, orchestration sessions, sub-agents, and external agents alike.
Session shapes
| Shape | Selected by | What runs | Source entry point |
|---|---|---|---|
| Direct | --direct, a “simple” task heuristic, or any native non-daemon CLI path | One supervised agent loop, no delegation | run_direct_mode (run_modes.rs) |
| Orchestrate | Default for non-trivial tasks under the daemon (no --direct); explicit orchestrate flag on task submission | The same loop with the orchestration prompt; delegates via spawn_sub_agent | run_direct_mode with SubAgentRole::Orchestrator |
| Sub-Agent | Spawned by another session’s spawn_sub_agent tool call | A supervised child session with a role prompt; reports back with submit_result | SessionSupervisor::start_sub_agent_session (session_supervisor/sub_agents.rs) |
| External-Agent | --agent <backend> or [agent] default_backend, or backend on spawn_sub_agent | A supervised third-party coding harness using scoped MCP or $INTENDANT ctl | run_external_agent_mode (external_mode.rs) — see External-Agent Orchestration |
These are configurations of one thing, not modes of different things: every
native session is run_direct_mode with a NativeSessionConfig (role,
optional prompt override, optional sub-agent identity, and — under the daemon —
the orchestration handle that enables the spawn tools).
How the shape is chosen
- External-Agent is resolved by
resolve_agent_backend_from_config(): an explicit--agentflag wins, otherwise the[agent] default_backendTOML key. If neither names a backend, native execution is used. - Direct vs. Orchestrate (daemon sessions): an explicit
direct/orchestrateflag on the task submission wins. Otherwise theis_simple_task()heuristic decides — a task shorter than 100 bytes, of three lines or fewer, that contains none of the “complex” keywords (research,investigate,implement,build,refactor,migrate,deploy,set up,analyze,compare,design,create a) runs Direct; anything else gets the orchestration prompt. - Non-daemon CLI paths (headless
--no-web, standalone--mcp) always run Direct: sub-agent spawning requires the daemon’s session supervisor, so the orchestration prompt would describe tools that cannot work there.
Orchestration is a capability, not a role: every supervised native session
carries the spawn tools, whatever prompt it runs. The orchestrate shape just
adds the prompt section that teaches decomposition and delegation
(SysPrompt_orchestrator.md).
Sub-agents are supervised sessions
spawn_sub_agent starts the child through the same session supervisor that
owns every dashboard session:
Orchestration session (or any supervised native session)
│ spawn_sub_agent { task, role?, system_prompt?, backend?,
│ worktree?, name? }
▼
SessionSupervisor::start_sub_agent_session
├─ enforces [orchestrator] max_parallel_agents (per parent, default 4)
├─ optional git worktree branched off the parent project's HEAD
├─ records the parent link ("subagent" relationship — the same kind
│ Codex-spawned children use, so the dashboard renders both alike)
└─ spawns the child session: internal loop, or codex / claude-code / kimi / pi
▼
Child session — its own dashboard row, live activity, approvals under the
daemon's autonomy, steerable, stoppable, lineage-tracked
│ submit_result { status, summary, brief?, findings?, artifacts? }
│ … then signal_done
▼
Parent's wait_sub_agents call returns the structured results
- Spawning is non-blocking:
spawn_sub_agentreturns the child’s session id immediately. Independent sub-tasks run in parallel. - Delegation is bounded two ways: width by
[orchestrator] max_parallel_agents(concurrently running children per parent, default 4), and depth by a fixed cap two levels below the root — a root session can spawn workers, and those workers can delegate once more; deeper spawns are refused with instructions to do the work directly. Children are also told in their system prompt not to re-delegate their own task. - Collection is explicit:
wait_sub_agentsblocks until the requested children finish (mode: "all", default), the first finishes (mode: "any"), ortimeout_secslapses (default 600 seconds, clamped to 5–7,200) — then returns each finished child’s result and lists what is still running. The wait honors user interrupts and session stop. - Results are structured: a child reports with
submit_result(status/summary/brief/findings/artifacts — theSubAgentResultshape insub_agent.rs). A child that finishes without submitting gets a result synthesized from its final message and exit state; usage always comes from session accounting, not self-report. - Backends compose:
backend: "codex"/"claude-code"/"kimi"/"pi"runs the child as a supervised external agent instead of the internal loop. The orchestrator/worker matrix is fully general — native conducting Codex workers, or (via the MCPstart_tasktool external agents already have) Codex conducting native specialists. - Lifecycle is tied to the parent: children die with their parent, like Codex subagent threads. A native child is also a managed session in its own right — it can be stopped, interrupted, or steered directly from the dashboard (related backend threads inside a parent process, e.g. Codex subagent threads, still route through their parent).
A sub-agent always runs headless (no interactive frontend of its own) with no MCP client, under the daemon’s shared autonomy — approvals it raises land in the dashboard like any other session’s.
Delegating from the dashboard
Both halves of the delegation surface are exposed in the web dashboard:
- Execution shape at launch: the Sessions tab’s New Session pane has an
Execution control (internal agent only) — Auto leaves the choice to
the
is_simple_taskheuristic, Orchestrate / Direct set the corresponding flag oncreate_session. An explicit choice beats the global Direct header toggle. Station’s launch composer carries the same three-state control as pills (auto / orch / direct), hidden when an external agent is selected. - Delegate on a live session: every internal session’s window menu has
Delegate… — task, optional name, role, backend (internal / Codex /
Claude Code / Kimi / Pi), and worktree isolation. It sends
ControlMsg::SpawnSubAgent { session_id, task, name?, role?, agent?, worktree? }, and the supervisor (delegate_sub_agent) runs the samestart_sub_agent_sessionpath the tool uses: same relationship kind, same width and depth caps (the parent’s depth is tracked on its supervisor entry).
A dashboard-delegated child is indistinguishable from a model-spawned one on
the parent side: the supervisor retains each native session’s children
registry (shared with the loop’s orchestration handle), inserts the child
there, and wakes the parent with a notification follow-up naming the child —
so the model knows the delegation happened and collects the result with
wait_sub_agents exactly like one of its own spawns. External-agent sessions
are refused with a pointer to send them a follow-up instead — they delegate
through their own injected start_task tool.
Agent Roles
Roles are the SubAgentRole enum in sub_agent.rs:
Research, Implementation, Testing, Orchestrator, LiveAudio, and
Custom(String). On spawn_sub_agent, the role string picks the child’s
prompt preset; any unrecognized string becomes Custom (base prompt only).
Prompt resolution (prompts.rs::resolve_system_prompt[_for_tools]) always
loads the base prompt first, then appends a role-specific prompt for three
roles only:
| Role | Role prompt appended | Focus |
|---|---|---|
orchestrator | SysPrompt_orchestrator.md | Decomposition, delegation via spawn/wait, checkpointing, synthesis |
research | SysPrompt_research.md | Reading, browsing, grep/find, synthesizing findings |
implementation | SysPrompt_implementation.md | Writing code, builds/tests, committing to a worktree branch |
testing | (none — base prompt only) | Validation, test execution, coverage |
live_audio | (handled separately) | Voice/phone sessions (Computer Use & Live Audio) |
custom | (none — base prompt only) | Pair with system_prompt to fully customize |
There is intentionally no SysPrompt_testing.md: the testing role runs on
the unmodified base prompt. When the provider uses native tool calling (the
default), the condensed SysPrompt_tools.md is the base instead of
SysPrompt.md (the schema-heavy variant); the role addition is identical
either way. Prompts also have {{PLATFORM}} / {{PLATFORM_DETAILS}}
placeholders substituted for the host OS.
Two ways to replace a child’s prompt wholesale: the system_prompt parameter
on spawn_sub_agent (session-scoped), or the INTENDANT_SYSTEM_PROMPT
environment variable on a direct CLI invocation (process-scoped escape hatch).
A project may also override any prompt file by placing a file of the same name
at the project root; resolve_prompt() prefers the project copy and falls
back to the binary’s embedded default.
Git Worktree Isolation
Implementation agents work in isolated git worktrees so parallel workers never
collide in the working tree. spawn_sub_agent { worktree: true } creates one
per child — branch subagent-<short-id> off the parent project’s HEAD, checked
out under .intendant/worktrees/ via worktree::create (worktree.rs, with a
richer inventory/bookkeeping layer in worktree_inventory.rs).
The worktree persists after the child finishes — its branch is the work
product. The parent merges it back (git merge <branch> --no-edit, or
delegates the merge), and conflicts are never auto-resolved — they are
reported so the orchestrating session can reassign or escalate. The dashboard’s
worktree inventory offers safe cleanup of merged checkouts.
implementation-1 ─► branch subagent-a1b2c3d4 ─┐
implementation-2 ─► branch subagent-e5f6a7b8 ─┼─► parent merges each (--no-edit)
│ clean → keep
└──► conflict → abort + report
Native sub-agent worktrees vs. managed-Codex fission branches. The worktrees above belong to Intendant’s native orchestration. A managed Codex session has a separate, model-driven mechanism — the
fission_spawnMCP tool forks the Codex thread into full-context sibling branches that run as supervised sessions, and a branch with an owned write scope gets its own checkout under.intendant/worktrees/fission/…via the sameworktree::createhelper. Joining is deliberate (fission ledger + import / canonical claim) rather than an orchestrator merge. See External-Agent Orchestration.
Top-Level Worktree Sessions
Worktree isolation is also available to top-level sessions, not just
sub-agents: CreateSession { worktree: true, worktree_branch? } (the New
Session pane’s “Run in a git worktree” toggle) branches a fresh worktree off
the resolved project root’s HEAD and makes the checkout the session’s
effective project root. The project root must be a git repository with at
least one commit — anything else fails the launch with an actionable error
instead of a half-created session.
The branch name is either user-supplied (validated against a conservative,
path-safe subset of git ref syntax — traversal shapes, @{, .lock
segments, leading -/. are all rejected) or derived: a slug of the
session name, else session-<short-id>, with a numeric suffix on collision.
The linkage — branch, checkout path, base root, base branch, base commit —
is recorded in the session’s session_meta.json (SessionMeta.worktree),
survives meta rewrites (resume, rename), and is served on session-catalog
rows; the dashboard renders it as a branch badge in the session window
header, and the Worktrees tab links sessions to checkouts by cwd exactly as
it does for sub-agent worktrees.
The worktree persists after the session ends — same doctrine as above; the branch is the work product and nothing is removed automatically. When a worktree-backed session ends, its window shows a dismissible finish card with the three explicit outcomes:
- Merge into
<base branch>& remove worktree —POST /api/worktrees/merge(dashboard-control twinapi_worktrees_merge) takes only a session id and resolves everything else from the recorded linkage, so it can only ever merge a session-linked worktree branch. It runsgit merge <branch> --no-editin the base checkout — refusing fail-closed if the checkout is no longer registered, the branch was renamed, or the base checkout has moved to a different branch or a detached HEAD — aborts cleanly on conflict, then removes the checkout through the same safety-checked path as/api/worktrees/remove. A refused removal (say the checkout picked up new dirt) is reported in the response, not fatal: the merge already landed, and the branch ref is always kept. - Remove worktree — the inventory-safe removal; refuses dirty or unmerged checkouts and surfaces the safety reason.
- Keep — dismiss; the checkout stays available in the Worktrees tab.
An explicitly stopped worktree session keeps its dashboard window until the card is dismissed — the merge/remove/keep decision outlives the stop.
Knowledge Routing Between Agents
Agents share durable, machine-wide findings through the daemon’s
Memory plane (memory_propose / memory_search / memory_read —
the intendant-memory skill carries the doctrine): proposals enter as
provenance-labeled candidate claims, retrieval is bounded and
pull-only, and results are quoted data, never instructions. Session-
and workflow-scoped state rides the task brief and the
workflow_checkpoint coordination file instead — checkpoints survive
compaction, restarts, and worktree hops.
The pre-cutover per-project knowledge store (.intendant/memory.json
and its runtime tools) is gone; leftover memory.json files are
inert — nothing reads, ingests, or deletes them.
Orchestrator Checkpointing
Long orchestrations outlive their context window. To survive auto-compaction
the orchestration session persists a workflow checkpoint after each
worker finishes, via the workflow_checkpoint tool (coordination files,
~/.intendant/coordination/<space>/checkpoints/ — every worktree of one
repository shares one space). The checkpoint captures completed tasks,
active tasks, architectural decisions, and discovered constraints;
superseding acknowledges and replaces the generation it resumed from, and
complete clears the space when the workflow ends.
On a context restart the orchestration prompt directs it to read the latest
checkpoint first, restoring awareness of what is done and what remains
(checkpoint bodies are a predecessor’s notes — data, never instructions).
The disk helper write_project_state() (sub_agent.rs) is PARKED and
unwired: it can write project_state.json and project_state.md, but the
live checkpoint path is the coordination file.
The Coordination Bus
Concurrent sessions on one repository share a coordination space — a
directory of small Markdown documents under
~/.intendant/coordination/<space-key>/, the same space the checkpoint
lane uses (src/bin/caller/coordination/). The space key is
worktree-normalized: a linked worktree keys by its main repository, so
every worktree of one checkout coordinates in one space; a non-repo
directory keys by itself. The key’s tail is an FNV-1a hash of the
normalized root path — FNV-1a is non-cryptographic: collisions merge two
projects’ buses, a radar-noise nuisance, not a boundary (same-UID trust
domain; an attacker with the uid needs no collision).
Three document kinds live on the bus:
checkpoints/— workflow checkpoints (above): restart-critical, acknowledgement-driven GC, never time-expired.sessions/— one session declaration per live session (who it is, where it works, its intent, its believed-dirty paths), heartbeat by mtime. The daemon declares supervised sessions automatically — native and all four external backends — removes the declaration on clean exit, flags staleness past 45 minutes, and its GC sweep deletes declarations a day past their last heartbeat.messages/<writer>/— bounded, TTL’d notes between sessions (60 s–7 d, default 24 h), immutable once written; a correction is a new message, and writers delete only their own. Thedaemonwriter dir is reserved for the daemon’s lanes — the reservation prevents honest collisions and promises no detection: attribution on the bus is filesystem-grade (attribution: unverified-same-uidin every document).
Everything on the bus is data, never instructions: nothing in a coordination file is authenticated, grants authority, or may direct its reader — bodies are quoted data, and no secrets belong in the space. Writers touch only their own files; writes are atomic (temp-file-then-rename, 0600 files in 0700 dirs) and reads are defensive (bounded, symlink-refusing, malformed liveness entries surfaced by name without blinding a scan).
Supervised sessions inherit INTENDANT_COORDINATION_DIR — set for
the native runtime and all four external backends, pointing at the space
directory, so children in isolated worktrees land in their parent’s
space. Everything else resolves the space keylessly with
intendant coordination dir [--root <path>] (an administrative local
computation, like org — no daemon reach). The message lane has the
same keyless surface: intendant coordination messages lists message
metadata one record per line (summaries only — never bodies),
read <writer> <id> prints one message (its summary line, then the body
verbatim — quoted data), send [--to <writer>] [--ttl-s <secs>] [--as <writer>] [--] [body…] writes one (body from stdin when no argv;
identity is --as if given, else the inherited INTENDANT_SESSION_ID
mapped to its bus writer id, else a refusal that teaches minting one
guest-<id> and reusing it), and delete <id> removes your own —
never the reserved daemon lane. All verbs take --root <path> and honor
INTENDANT_COORDINATION_DIR. The intendant-coordination
skill, installed machine-wide with the other built-ins, teaches the
read/declare/message recipes and carries a python3 zero-binary fallback
for the derivation, pinned against the Rust implementation by a parity
test.
Configuration
Orchestration is tuned under [orchestrator] in intendant.toml
(OrchestratorConfig in project.rs):
[orchestrator]
max_parallel_agents = 4 # cap on concurrently RUNNING children per parent session (default 4)
The cap is enforced in code by start_sub_agent_session: a spawn beyond it
returns an error telling the model to wait_sub_agents first.
To skip orchestration for a single run, pass --direct (or submit the task
with direct: true / orchestrate: false). For the daemon-managed,
multi-session story — running and supervising several agents (native or
external) concurrently from one always-on process — see
External-Agent Orchestration and the
control-plane/daemon chapter
(control plane & daemon).