subagent¶
Source: src/subagent.ts
A subagent: a live session, a memory, a state.
This is the heart of the library. Everything else - run, chain,
fanOut - is built on the three methods below: ask, usage, close.
The rule that governs this file: whoever opens, closes. A Subagent is
an explicit object with an explicit owner. There is no global session cache
hidden anywhere.
AskOptions¶
type
export type AskOptions = {
/**
* Cancels this turn. pi's `prompt()` takes no signal, so we bridge it to
* `session.abort()`.
*
* The turn fails with `"aborted"`, unless the signal's reason is a
* `TimeoutError`, as `AbortSignal.timeout` gives: then it is a deadline,
* and fails with the reason's message, as {@link AskOptions.timeoutMs} does.
*/
signal?: AbortSignal;
/**
* Deadline for this turn, in milliseconds. No default: an `ask` waits
* forever unless you say otherwise.
*
* This matters more than it looks. One `ask` is one `session.prompt()`, and
* pi's agent loop is a `while (true)` that runs as long as the model keeps
* requesting tools - there is no step cap in pi. A model that hallucinates a
* tool name, gets "unknown tool" back and asks again will loop until
* something stops it. Nothing will, unless it is this.
*/
timeoutMs?: number;
};
What governs one turn: how it can be stopped, and when it must be.
CustomToolsFor¶
type
export type CustomToolsFor = (id: string) => ToolDefinition[] | undefined;
Tools built once the subagent’s id is known.
The one thing a caller cannot decide before spawn: a tool that spawns
children has to name their parent, and the parent does not exist yet.
spawn¶
function
export async function spawn(agent: Agent, options: SpawnOptions = {}): Promise<Subagent> { /* … */ }
Brings an agent to life.
The lifetime is explicit and local: the argument wins over the agent’s
frontmatter, which itself wins over the "task" default. Persistence is
asked for; it is never obtained by accident.
The caller owns the returned object and must close() it, ideally in a
finally.
SpawnOptions¶
type
export type SpawnOptions = {
/** Overrides the lifetime declared by the agent. Absent from both, `"task"`. */
lifetime?: Lifetime;
/** Working directory the subagent's tools act in. Defaults to the process's own. */
cwd?: string;
/** Dedicated session directory, required for the session to be exportable. */
sessionDir?: string;
/**
* Where this subagent writes its HTML and JSONL when it closes.
*
* Setting it **implies a session directory** (`<exportDir>/.sessions`),
* because pi cannot render an in-memory session to HTML. That is one
* decision, not two: asking for an export is asking for the session to be
* kept long enough to export it. Pass {@link SpawnOptions.sessionDir}
* explicitly to put it somewhere else.
*
* Still opt-in: with no `exportDir`, a subagent stays in memory and leaves
* nothing behind - not in `~/.pi`, not in the working directory.
*/
exportDir?: string;
/** The name its files take in `exportDir`, without the extension. Defaults to its id's, `reviewer-2`. */
exportName?: string;
/** Subscribed to the event stream for the subagent's whole life. */
onEvent?: EventListener;
/** Shared bus, when several subagents must report to the same place. */
bus?: EventBus;
/** Session factory. Injection point for tests - defaults to a real pi session. */
createSession?: CreateSession;
/**
* Tools combo defines, offered to this subagent.
*
* Offered, not granted: the agent's `tools:` is an allowlist and covers these
* too, so one it does not name is not enabled. See
* {@link CreateSessionOptions.customTools}.
*
* A function receives the id this subagent is about to get, before its
* session opens. That is what a tool spawning children needs in order to
* name their parent, and it is the only way to have it: the id is minted
* here, after the caller has built everything it could.
*/
customTools?: ToolDefinition[] | CustomToolsFor;
/**
* The subagent that had this one spawned, when one did.
*
* Set by `delegateTool`, never guessed: a name is ambiguous the moment two
* explorers run at once, so the link is an id or it is nothing. It reaches
* the reporters on the `spawn` event and nothing else reads it.
*/
parentId?: string;
/** The flow visit this subagent is spawned for. It reaches the reporters on the `spawn` event. */
visit?: string;
/** Where a flow keeps it: its memory scope's path, else its visit's. It reaches the reporters on the `spawn` event. */
home?: string;
/**
* Model pattern for this subagent, e.g. `"anthropic/claude-sonnet-5"`.
*
* An override, not a default: the argument wins over the agent's
* frontmatter, which wins over pi's own settings - the same rule as
* {@link SpawnOptions.lifetime}. It exists so one workflow can run against
* different models without editing a single agent file; a frontmatter model
* surviving a sweep would make an experiment measure a mixture.
*
* A pattern that resolves to nothing throws at spawn: better than running
* a whole workflow on the wrong model.
*/
model?: string;
/**
* Give this subagent its own herdr split, when running inside herdr.
*
* Opt-in per subagent, like {@link SpawnOptions.lifetime}, and resolved the
* same way: this argument wins over the agent's frontmatter, which wins over
* `false`. A fan-out of twenty branches must not carpet the screen unless
* someone asked for it. Outside herdr it is simply ignored.
*/
openInHerdr?: boolean;
};
Everything that can be decided about a subagent before it exists.
Subagent¶
type
export type Subagent = {
/** Unique for the process, e.g. `reviewer#2`. Names its transcript files. */
readonly id: string;
/** The definition it was spawned from. Inert data - it is not re-read. */
readonly agent: Agent;
/** Resolved once, at spawn: the argument, then the frontmatter, then `"task"`. */
readonly lifetime: Lifetime;
/** `provider/id` as pi resolved it, which only the session knows. Absent when pi could not say. */
readonly model?: string;
/** Cumulative measurements since spawn. Read `Result.usage` for a single turn. */
readonly usage: Usage;
/** Runs one turn of work. Never throws on a model failure - returns `ok: false`. */
ask(task: string, options?: AskOptions): Promise<Result>;
/**
* Stops this subagent, for good: the turn in flight is cut short, and any
* later `ask` fails at once with `"stopped"`.
*
* One-way and idempotent, because that is what a person pressing a key
* means. It is **not** `close()`: the session is still there, so the
* transcript of what it did before it was stopped is still exportable, and
* whoever opened it still owes it a `close()`.
*/
stop(): void;
/**
* Writes this subagent's transcript into `dir` - HTML and JSONL, pi's own.
*
* Callable at any moment while the subagent lives, not only at the end: an
* interrupted workflow must still be able to export what it did. After
* `close()` the session is gone, so this reports an error instead of
* throwing - losing an export must never be worse than losing the run.
*/
export(dir?: string): Promise<SessionExport>;
/** Releases the session. Idempotent. */
close(): Promise<void>;
};
A living subagent. Its owner is whoever called {@link spawn}.