measure/export

Source: src/measure/export.ts

Exporting a run: runs/<timestamp>/ with one HTML and one JSONL per subagent, plus a usage.json.

Two rules govern this file.

We reimplement nothing. The HTML and the JSONL are pi’s own (AgentSession.exportToHtml / exportToJsonl); usage.json is the single artefact we produce ourselves, because it is the only one pi does not know about - time is measured here, and attribution per subagent is ours.

An export never breaks a run. Every failure comes back as a string in error, never as a throw: an export is an observer of the work, and an observer that takes the workflow down with it is a bug. That matters most on the interrupted path, where exporting is precisely what we are trying to rescue.

createRunDir

function

export function createRunDir(base = "runs", now = new Date()): string { /* … */ }

Creates <base>/<timestamp>/ and returns its path: a directory of its own for every call.

The timestamp is to the second, sortable and filesystem-safe, so ls shows the runs in order. Two runs started in the same second would share it, and a run directory holds one run: the second is <timestamp>-2, then -3, as the run’s branch is. Each candidate is created exclusively, so two processes never both take one. {@link newestRunFirst} orders the names.

<base> is given a .gitignore of its own, because the exports land inside the repository the run works on and git has no reason to know about them. Measured, both ways round: a delivery that gives its subtasks copies refuses to put them back, because runs/ alone makes the tree unclean; and /build’s commit is a git add -A, which would sweep a run’s transcripts into the user’s history. A file already there is left alone - it is their directory once they have said anything about it.

exportBaseName

function

export function exportBaseName(id: string): string { /* … */ }

Turns a subagent id into a file name: reviewer#2 → reviewer-2.

# is legal in a file name and unusable in a URL, and these files are meant to be opened in a browser and shared.

SessionExport

type

export type SessionExport = {
	/** The subagent this transcript belongs to, e.g. `reviewer#2`. */
	id: string;
	/** Path of the HTML page pi rendered. Absent when it could not be produced. */
	html?: string;
	/** Path of the JSONL transcript. Attempted separately from the HTML. */
	jsonl?: string;
	/** Why the export did not happen. Never thrown, always reported. */
	error?: string;
};

What one subagent left on disk. Both paths are absent when nothing could be written.

usageReport

function

export function usageReport(snapshot: RunSnapshot, wallMs: number, exports?: SessionExport[]): UsageReport { /* … */ }

Builds the report from a collected snapshot.

A fan-out aggregates, it never averages: tokens and cost are sums, busyMs is the sum of the branches, and wallMs is how long the run took. The ratio of the last two is the only honest measure of parallelism.

UsageReport

type

export type UsageReport = {
	/** When the report was written, ISO 8601. The run's own timestamp is the directory. */
	generatedAt: string;
	/** Wall time of the run itself, not the sum of the subagents. */
	wallMs: number;
	/** One entry per subagent, in tree order: a child follows the parent it hangs under. */
	subagents: UsageReportEntry[];
	/** The sum over every subagent - failures included, because they cost too. Its `wallMs` is the run's. */
	total: UsageTotal;
	/** Busy time over wall time: the parallelism actually achieved. */
	parallelism: number;
	/** Where each transcript landed, and why one is missing when it is. */
	exports?: SessionExport[];
	/** In a flow run: every visit of every life, each life's in plan order. */
	visits?: VisitUsage[];
	/** In a flow run: one entry per node address, in the order first visited. */
	nodes?: NodeUsage[];
	/** In a flow run with a journal: each life, whose sum `total` is. */
	lives?: LifeUsage[];
};

The whole usage.json document.

UsageReportEntry

type

export type UsageReportEntry = {
	/** The subagent, e.g. `scout#1` - what the transcript files are named after. */
	id: string;
	/** The agent it was spawned from. Several subagents may share one agent. */
	agent: string;
	/** The lifetime it actually ran with, not the agent's declared default. */
	lifetime: string;
	/** `provider/id` as pi resolved it, when pi could say. */
	model?: string;
	/** Its last known status: what it was doing when the run ended. */
	status: string;
	/** Whether its last turn succeeded. Absent while it is still running. */
	ok?: boolean;
	/** The failure, when there was one. A failed subagent keeps its usage. */
	error?: string;
	/** The last task it was given - a report of ids alone reads like nothing. */
	task: string;
	/** How many tools it called. The cheapest signal that a turn ran away. */
	toolCalls: number;
	/**
	 * The subagent that had this one spawned. Absent on a root.
	 *
	 * The list stays **flat** and carries the link, rather than nesting: the
	 * total is a sum over the whole tree either way, every reader written
	 * against the flat shape keeps working, and a tree is one pass away for
	 * whoever wants one. The rows are in tree order, so reading it top to bottom
	 * already shows the children under their parent.
	 */
	parentId?: string;
	/** Its {@link Usage}: time measured here, tokens as pi reported them. */
	usage: Usage;
	/** In a flow run: the folder of its transcript, relative to the run directory - its memory scope's path, else its visit's. */
	home?: string;
	/** In a flow run: the life it ran in, counting from 1. */
	life?: number;
	/** In a flow run: every visit it ran, in the order they ended. */
	visits?: string[];
};

One subagent’s line in usage.json.

UsageTotal

type

export type UsageTotal = Usage & {
	/** Subagents in the run, failures included. */
	subagents: number;
	/** Those that ended with `ok: false`. Counted apart: `2/3 done` hides a crash. */
	failed: number;
};

The sum over a whole run, and how many subagents it was spread over.

writeUsageReport

function

export function writeUsageReport(dir: string, report: UsageReport): string { /* … */ }

Writes usage.json into dir and returns its path.