events¶
Source: src/events.ts
The event stream: one core, many reporters.
Display is an observer, never a participant. No workflow may depend on a UI being there: unplug every reporter and the result is identical. Which is why nothing here ever writes to the terminal.
EventBus¶
type
export type EventBus = {
/** Delivers to every listener; a listener that throws is swallowed. */
emit(event: SubagentEvent): void;
/** Returns the unsubscribe function. */
subscribe(listener: EventListener): () => void;
};
The one channel between the core and every reporter.
EventListener¶
type
export type EventListener = (event: SubagentEvent) => void;
A subscriber. Throwing from here must never break the workflow.
isVisit¶
function
export function isVisit(event: SubagentEvent): event is VisitEvent { /* … */ }
Whether an event is a flow’s visit rather than a subagent’s.
A reader that follows subagents by id skips these: a visit is the plan’s business, and the subagent it ran on reports on its own events.
SubagentEvent¶
type
export type SubagentEvent =
| {
type: "spawn";
id: string;
agent: string;
lifetime: Lifetime;
/**
* Whether this subagent asked for its own herdr split.
*
* It travels on the event rather than being read back from the core,
* because a reporter is a pure observer: it never queries anything,
* it only listens.
*/
openInHerdr: boolean;
/** `provider/id` as pi resolved it. Absent when pi could not say. */
model?: string;
/**
* Where this subagent came in the launch, counting from 1.
*
* The event cannot be emitted until the session exists, because it
* carries the model pi resolved - and sessions come up in whatever
* order they come up in. Measured: a fan-out of three drew as
* `scout#2, scout#1, scout#3`. A reader that wants the order the
* branches were launched in sorts on this.
*/
order: number;
/**
* The subagent that had this one spawned, when one did.
*
* Absent at the top level, which is what makes a root a root. It
* travels on `spawn` alone: every later event about this subagent
* carries its `id`, and a reporter that saw the spawn already knows
* where to hang it. Sending it again would be a second copy of one
* fact, and two copies drift.
*/
parentId?: string;
/**
* The flow visit it was spawned for, `deliver#2/work[1]/code`, when a
* flow's runner spawned it. A subagent a memory scope keeps names the
* first visit that asked for it. It is how a plan line finds its
* subagents.
*/
visit?: string;
/**
* Where a flow keeps it: its memory scope's path when it has one,
* its visit's otherwise, `""` for `memory: flow`. It is what a
* herdr split is named after.
*/
home?: string;
/**
* Where its transcript is written when it closes, without the
* extension: `<exportDir>/<name>`. Absent when it exports nothing.
*/
transcript?: string;
}
| {
type: "status";
id: string;
status: SubagentStatus;
/**
* The task this turn is about, on the `"working"` transition only.
*
* A reporter has no other way to learn it: `spawn` happens before
* anyone knows what the subagent will be asked, and a persistent
* subagent is asked several different things over its life.
*/
task?: string;
}
| { type: "text"; id: string; delta: string }
/** A tool call; `call` is pi's id for it, when pi gave one. */
| { type: "tool"; id: string; name: string; args: unknown; call?: string }
/**
* A tool call that came back an error: refused before it ran, as a tool
* the agent does not have is, or failed while running. `error` is the first
* line pi said, which names the refusal.
*/
| { type: "tool_error"; id: string; name: string; error: string; call?: string }
/**
* A member said something on the board.
*
* `id` is the member, as on every other event; the post carries who it was
* for and what kind of thing it was. `record.ts` writes it down, which is
* what makes the traffic of a run readable afterwards instead of
* reconstructed, and the console reporter prints it as it happens.
*/
| { type: "post"; id: string; post: Post }
/**
* A member was handed what it had not seen.
*
* The posts alone say who said what, and that is the smaller half: what an
* investigation asks is who *knew* what, and knowing comes from being handed
* something. Measured on three members dividing one job: the claims were
* spread over two seconds, so the later ones could have read the earlier,
* and nothing in the record could say whether they had.
*
* Only the ids: the text is already in the record, under the `post` that put
* it there. A read that was handed nothing is recorded too, and is the
* strongest thing the record holds about what a member could not have known.
*/
| { type: "read"; id: string; posts: readonly string[]; waiting: number }
/**
* A member asked for a thing, or gave one back.
*
* `ok` is whether it got what it asked for, and `heldBy` names the holder
* when a take was refused. Recorded for the same reason a read is: the
* question afterwards is who held what and when, and a refusal is as much a
* fact of the run as a grant.
*/
| { type: "claim"; id: string; key: string; action: "take" | "release"; ok: boolean; heldBy?: string }
/**
* A person spoke to a working subagent, through its pane.
*
* On the stream so the record holds it: a run somebody steered is not the
* run they would have got by watching, and two identical `events.jsonl`
* must not describe two different runs.
*/
| { type: "steer"; id: string; text: string }
| { type: "usage"; id: string; usage: Usage }
| { type: "close"; id: string; result: Result }
| VisitEvent;
Everything the core emits. Reporters subscribe, and only read.
SubagentStatus¶
type
export type SubagentStatus = "working" | "idle" | "blocked" | "done";
What a subagent is doing right now, as seen from the outside.
VisitEvent¶
type
export type VisitEvent =
| {
type: "visit_start";
/** The visit: `deliver#2/work[1]/code`. */
path: string;
/** The node's address, without iterations: `deliver/work/code`. */
node: string;
kind: CheckedNode["kind"];
}
| {
type: "visit_end";
path: string;
/** As on its `visit_start`, so an end read alone, from the journal, says what it was. */
node: string;
kind: CheckedNode["kind"];
ok: boolean;
/** What the node handed on, when it ran. */
output?: unknown;
/** Why not, when it did not. */
error?: FlowError;
/** The case a `choice` ran: `"1"` for the first, or `"default"`. */
case?: string;
/** Whether a `loop` that ended stopped on its condition. */
converged?: boolean;
/** The agent an `agent` visit ran, the one `agent-from:` picked included. */
agent?: string;
/** The subagent an `agent` visit ran on: the last one, when a timeout renewed it. */
subagent?: string;
/** The model its subagent ran on, as pi resolved it. */
model?: string;
wallMs: number;
/** Every attempt's tokens, and every nested visit's: the delta of pi's cumulative stats. */
usage: Usage;
};
A flow’s runner entering and leaving one visit of a node.
They carry no subagent id: a visit is a node’s, and a choice has no
subagent at all. A visit_end is also what the journal writes down, so a
reader folds the journal and the stream alike.