Agents: the uniform node, the tree, and the parent-less trunk

An exarch run is a fleet of structurally identical agents arranged in a tree, driven by the single shared drive loop; the only thing that distinguishes one agent from another is its position — whether it has a parent. A sub-agent is not a different machine: it is an Agent (agent) forked from a value-snapshot of its parent’s shell, with a narrowed capability ceiling and a parent: Option<AgentId> link. The thin Fleet holds what every node shares — the registry, the one event bus, and the transport engine.

One predicate, fixed at construction

There is no is_root, no spawns/returns axis pair. Whether an agent returns a value or converses with a human is a construction-fixed returns bit on the Agenttrue for a forked sub-agent, false for a /branch child, !interactive at the trunk. One bit is the single source of truth for every reader: returns(), parking’s conversing predicate, the desk’s reply refusal (builtins), and the per-agent builtin index resolved from the same bit at Agent::assemble — so reply availability, parking, and the advertised vocabulary cannot disagree (branch-minimal). Position still does two jobs — it fixes the registry edge (parent) and the signal path (only the trunk publishes the process cancel slots, per-agent-eval-cancel) — but it does not decide who returns.

  • A returning agent holds reply. A peer at any depth, and a headless trunk (parent = None, interactive = false) seeded once to produce one result, both advertise it and terminate at quiescence. This is reply-terminates-returning-agents’s reply gate, read off the construction-fixed bit rather than off is_root && interactive.
  • A conversing agent had reply withheld at construction and parks for a human instead of returning. Its reply call is refused at the desk, and the verb is dropped from its builtin index, both keyed on the same bit. The interactive trunk is one such agent — parent-less, its writer ever-present — but not the only one: a branch is interactive, reply-withheld, and parented (branch-minimal). “Parent-less trunk” and “converses” are not the same set, which is exactly why the property is a bit fixed at construction rather than a predicate on position.

“Returns a value” and “does not park for a human” remain the same fact, read in one place (agent). Parking is computed, not stored — a ParkMode (Held / Engaged / HeldByChildren / UntilCancelled / Quiesce) derived on every wake: a conversing agent parks Held while its registry entry lives, a returning agent a human has exchanged a message with parks Engaged bounded by the registry’s idle lease, and everyone else quiesces.

Uniform spawning: bounded by spawn fuel

Spawning is universal — every agent may spawn, so the spawn tree is not capped at one level. There is no spawns() axis: every agent holds the one agent spawn verb (builtins). Depth-N works structurally — a child registers in the fleet’s registry and fork snapshots the parent’s shell by value at any depth — but each fork hands its child one less unit of fuel than the parent holds (the parent’s own fuel is untouched, so fan-out itself is unbounded), and a fuel == 0 agent’s spawn call is refused at the desk with the exhaustion text: a delegation chain terminates by refusal a fixed number of generations down rather than recursing forever (uniform-agent-nodes, superseding the depth-1 cap of async-agent-tool; spawn-fuel-ceiling, bounding the depth that decision left open).

The agent spawn verb is launch-only and always asynchronous (async-agent-tool). Its argument is a single closed record [prompt: …, name: …, type: …, grant: …] — a record literal, so a missing or misspelled field is a static error naming it, while the type (`amnemon/`mnemon) and grant tags are checked at the runtime door that enumerates their legal labels (names-and-schedule-labels). One call:

  • forks a child Agent through Shell::fork_session (the flow matrix). The child snapshots the parent’s whole lexical scope, dynamic context (cwd, env, grants, handlers), and the installed builtin table, sets parent to the spawning agent’s id, takes the spawn’s name as its identity, and starts fresh in everything else — its own inbox, a fresh cancel token, an owned provider handle seeded from the parent’s current model, no terminal authority. This is a value snapshot: the child’s cd, env, and new bindings die with it; there is no flow-back, and the parent receives a string, not the child’s bindings. The isolation mirrors a byte-pipeline stage’s subshell;
  • runs it on a detached thread through the same drive loop, returning a receipt [name: Str, log-dir: Str] at once — a ral record the script can bind and fan out over. The child runs off the parent’s critical path — the one shape in-turn concurrency cannot express, the parent turn ending before the child does;
  • delivers the child’s single reply later as a marked Turn through the parent’s inbox — the parent edge parent names — rendered to prose at the consuming edge.

The spawn’s type field chooses the child’s model memory, not its shell isolation (subagent-memory-modes):

  • `amnemon is tabula rasa. The child starts with no conversation history; only the shell value-snapshot and the chosen prompt cross the edge.
  • `mnemon remembers. The child imports the parent’s model-visible context and appends the call’s prompt as a fresh final user prompt, while reusing the parent’s current provider selection so provider prompt caches can hit. If the parent is mid-tool-call, the unanswered assistant tool-call frame is not inherited; the child forks the request context, not a dangling protocol.

Returning: the deliberate reply

A returning agent hands back the argument of an explicit, hard-terminating reply call (builtins) — never a scrape of whatever prose ended the run (agent-reply-tool). reply is the sole return path: a returning agent that finishes without it is re-nudged within budget, then fails honestly rather than handing up a trailing fragment that masquerades as the answer. reply hard-terminates the agent regardless of focus — the conversation ends out from under a human who had TABbed to it, and focus falls back to its parent — though the run ends only once the enclosing ral call’s batch finishes draining, never mid-batch. The payload is the faithful first-order ral value the model passed (FOValue), rendered at each consuming edge (shell-eval) — ral surface syntax for a model parent, ordinary JSON for the headless harness through the user_json projection (reply-terminates-returning-agents). Before the returning node disappears, reply cancels and reaps its proper descendants: a parent may choose to abandon unfinished children, but it cannot leave live agents registered beneath a settled node. This is not a /clear; unrelated siblings keep their generation and may still settle normally.

Focus is presentation; the idle lease is lifecycle

The human’s TAB cursor is a plain AgentId the TUI moves — purely presentational, read by neither the registry nor park_mode. TABbing to a tab lets it receive the human’s typed lines and own Esc, but looking at a tab keeps nothing alive. What keeps a non-conversing returning child alive past quiescence is a renewable idle lease the registry arms at birth (Registration::lease, one hour), not the human’s attention: the one thing that renews it is a delivered human message (AgentRegistry::steer), so a child a human is actually steering parks Engaged and keeps its lease fresh, while a lease that is never renewed fires at exactly its birth-seeded hour. A /branch child and the trunk carry no lease at all and so never idle-reap. Neither TAB, nor the model-facing message builtin, nor a /resources probe renews anything — enumeration and attention alone can never immortalise a child.

A leased child that is parked waiting for input and has sat idle for five minutes demotes out of the TAB cycle and the tab bar into a compact matrix strip, carrying its idle age — a per-frame projection off the registry’s exchange clock, never stored state, and root is never a candidate. /focus <name> reaches a demoted tab directly and re-promotes it into the cycle; the gesture is presentation only and never touches the lease. When the lease itself runs out, the reaper cancels the whole subtree at the bound whether or not a human happens to be looking at it — mere focus was never immunity, and there is no TAB-driven reap to begin with.

Peer messages: marked notes, not shared memory

Live agents may send one another a marked message by name through the message builtin. The registry resolves the name to the recipient’s inbox and posts an AgentMessage; the recipient sees it at the next tool boundary as a marked note naming the sender, not as human input. This is coordination, not a return edge: it does not share shell state, does not grant authority, and does not wait for an answer. The durable result path remains reply; the durable cancellation path remains agent-cancel, addressed by name the same way.

Cancellation: a key interrupts one turn; a terminator cascades the subtree

Esc and Ctrl-C are a per-tab turn interrupt — they unwind only the focused tab’s current turn, never a subtree and never an agent (cancel-per-tab). The subtree cascade survives, but only behind the lifecycle terminators: the agent-cancel builtin, the per-agent idle-lease reaper, and /clear. They share one registry cascade — the registry is the spawn tree (AgentRegistry::Entry carries the parent link), so terminating a mid-tree agent reaps everything below it; /clear additionally bumps the generation, dropping a late result or deferred surface batch from a cleared generation. A settling reply still cancels only the returning node’s proper descendants — a parent may abandon unfinished children, but never leave live agents registered beneath a settled node. This refines per-root-turn-cancel: the per-focus turn token now interrupts one turn in place, while the subtree cascade is the terminators’ alone.

Self-scheduling is inherited

A peer may arm its own wakeups (a cron expression or after <dur>) into its own inbox when the trunk was launched --allow-schedule: the grant is inherited by a fork, so it flows down the spawn tree. Scheduling is gated by that authority — refused at the desk without it, and the schedule family is dropped from an ungranted agent’s builtin index (scheduled-wakeups). A live self-schedule is one of the three reasons an agent parks (ParkMode::UntilCancelled).

Permissions: the child’s ceiling is parent ⊓ base

Every spawn states the child’s ceiling explicitly, through a mandatory grant field naming one of the six capability bake-ins — the same vocabulary as the --base CLI flag (confined, minimal, read-only, edit-only, reasonable, dangerous; ordered loosest-to-tightest in policy). An unknown tag is refused at the runtime door, naming all six. The child is born with

  child = parent ⊓ resolve_base(grant)

computed by policy::narrow, the meet-sibling of the root’s policy::for_invocation (policy). Because meet only ever removes authority and the result is ≤ both operands, the base can narrow the child below the parent but can never escalate it past the parent’s reach (the grant lattice):

  • naming a base looser than the parent simply changes nothing — a network-off confined parent stays offline even under minimal, since false ⊓ true = false;
  • dangerous resolves to the lattice top (Capabilities::root), so it means no narrowing — inherit the parent’s authority verbatim.

The base is frozen against the child’s working directory as it resolves, so the meet runs on already-resolved capabilities. The ceiling is non-escalating by construction: the spawn site, not the child, owns the authority decision, because `Agent::fork_with` takes the child’s Capabilities as an argument rather than cloning the parent’s.

See also

exarch-architecture (the agent as a provider loop over one shell tool), names-and-schedule-labels (the one record-spec agent verb, names as fleet-unique identity, schedule labels, commitments retired), grant (the capability lattice the meet runs in), tools, agent, policy, async-agent-tool, agent-reply-tool, reply-terminates-returning-agents, spawn-fuel-ceiling, cancel-per-tab (Esc/Ctrl-C are a per-tab turn interrupt, not a subtree cascade), branch-minimal (the conversing parented child whose returns bit is fixed false at construction).