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
Agent — true 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 offis_root && interactive. - A conversing agent had
replywithheld at construction and parks for a human instead of returning. Itsreplycall 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 childAgentthroughShell::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, setsparentto the spawning agent’s id, takes the spawn’snameas 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’scd, 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
driveloop, 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
Turnthrough the parent’s inbox — the parent edgeparentnames — 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):
`amnemonis tabula rasa. The child starts with no conversation history; only the shell value-snapshot and the chosen prompt cross the edge.`mnemonremembers. The child imports the parent’s model-visible context and appends the call’spromptas 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
confinedparent stays offline even underminimal, sincefalse ⊓ true = false; dangerousresolves 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).