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.