Lifetime¶
A subagent is a session. Keeping it open keeps a context, a memory, and a token cost that accumulates. Closing it starts clean but amnesic. Both are legitimate, which is why the choice is explicit and local and never inferred.
|
The subagent |
Cost and context |
When |
|---|---|---|---|
|
is born and dies with each task |
minimal context, reproducible |
exploration, fan-out, independent tasks |
|
lives for the workflow |
remembers iterations, context grows |
coding/review loop, iterative refinement |
|
lives as long as the pi session |
long memory, watch it |
a companion agent consulted several times |
Resolution order, always: the explicit argument, then the agent’s frontmatter,
then "task". Persistence is asked for. It is never obtained by accident.
The order holds inside a workflow too, and it is decided agent by agent. In a
loop that names no lifetime, the shipped coder and reviewer both declare
lifetime: workflow, so each keeps one subagent for the whole loop, while a
task agent beside them gets a fresh one every turn. Two places set the
lifetime themselves: run forces "task", and a flow node runs as "task"
unless its memory: names a scope, whatever the frontmatter says.
The two regimes, on one workflow¶
Same code, one parameter, two different behaviours.
// "Team": the coder and the reviewer remember the previous turns. The reviewer
// does not repeat its remarks, the coder knows what it was told.
await loop({
steps: [coder, reviewer],
input: task,
lifetime: "workflow",
until: (step) => saysWord(step.output, "LGTM"),
maxIterations: 5,
});
// "Fresh eyes": brand new subagents at every iteration. No accumulated bias,
// every review starts from the code alone. More expensive in re-reading, more
// honest about the result.
await loop({ steps: [coder, reviewer], input: task, lifetime: "task" });
Neither is better. "workflow" converges faster and drifts; "task" costs more
and cannot fool itself.
The rules¶
Whoever opens, closes. The owner of a
Subagentis whoeverspawn()ed it. A workflow closes what it created in afinally, cancellation included, and never closes a live subagent it merely received.Persistent subagents do not share their history. “Working together” means passing
Results along, not merging contexts. If an agent must know what another one did, you tell it in the task.No shared mutable state between fan-out branches, whatever the lifetime. In
"workflow", each branch gets its own persistent subagent.Context growth is visible.
subagent.usage.contextTokensis reported to the display. A"workflow"agent approaching its limit must either compact or fail cleanly, never truncate silently.Lifetime does not change the shape of every combinator.
reduceis one agent and one turn, so"task"and"workflow"both spawn once. What differs is only what is observable.
Closing, and what it triggers¶
const coder = await spawn(coderAgent, { lifetime: "workflow", exportDir: dir });
try {
await coder.ask("Implement the parser");
} finally {
await coder.close(); // exports first, disposes after
}
close() writes the transcript before releasing the session, so the finally
of a workflow is already the “export what was done” path, interruptions
included. See Export.
Who opens and closes inside a combinator¶
No combinator spawns or closes anything itself. Each one plays its turns
through a SubagentPool built from the options it was handed, and the rule
above lives there, in one place. The lifetime is the workflow’s when it names
one, otherwise each agent’s own:
"task": a fresh subagent per turn, closed as soon as the turn is over.anything else: one subagent per
key, reused, closed bycloseAll().
key is who shares a memory, and it defaults to the agent’s name. A chain keys
by name, so in "workflow" the reviewer of the third step is the one that
reviewed the first. A fan-out keys by branch, because two branches must never
share a context.
const pool = new SubagentPool(options);
try {
const plan = await pool.turn(planner, task);
return await pool.turn(coder, plan.output);
} finally {
await pool.closeAll(); // whoever opens, closes - cancellation included
}
A turn asked for after the signal was aborted spawns nothing. It comes back as
a failed Result and is recorded anyway, so a workflow that was called off
says so where its answer would have been.
A held subagent outlives the turn¶
pool.hold() is for a conversation rather than a task: it hands back an id
and an ask, and what it holds lives until closeAll() whatever the
lifetime. interview holds its interviewer, swarm holds its members. An
interviewer that forgot the previous question, or a member renamed at every
round, would not be a conversation.
That is the one case where "task" does not mean one subagent per turn, and
the caller asked for it by calling hold instead of turn.
closeAll() ends the subagents, not the record: what their turns produced is
on the pool’s trail, which is read after the closing and not before. Writing
a combinator of your own is Workflows.
Stopping is not closing¶
subagent.stop() cuts the turn in flight short and makes every later ask
fail at once with "stopped". It changes nothing about ownership: the session
is still there, still exportable, and still owed the close() its opener
promised it. A stopped subagent inside a workflow is therefore closed by the
same finally as any other.
A whole run has a signal for that, but a signal cannot single one branch out -
it is shared, and a combinator hands out no handles. stopSwitch is the pair
that can: give the workflow its signal and its spawn, and stop what you like
from outside.
const stop = stopSwitch({ signal: ctx.signal });
const running = fanOut({ agent: scout, tasks, spawn: stop.spawn, signal: stop.signal });
stop.one("scout#2"); // that branch fails with "stopped", the others carry on
stop.all(); // the turns in flight, and the ones not started yet
This is what esc, ctrl+del and /stop are wired to in the pi extension -
see Display.
Reference¶
subagent-spawn,Subagent,SpawnOptions.run- the disposable form, where the lifetime is forced to"task".stop-stopSwitch, the run’s two halves.agent-Lifetime.workflows/pool-SubagentPool,turn,hold,closeAll.workflows/trail-Trail, what the turns add up to.