agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
211 lines (210 loc) • 11.2 kB
TypeScript
/**
* epochs — where one epoch's committed pieces live, in either chart shape.
*
* Role: Fold. It reads a finished run's recording and answers one question:
* "for iteration k, which commit log holds the call, at which index,
* and against which fold base?" It executes nothing, records nothing
* and stores nothing beside the log.
* Reads: a recording (`commitLog`, `initialState`, `subflowResults`), through
* footprintjs's own id parsers and this folder's `keyedFold.ts`.
* Emits: N/A.
*
* ── WHY THIS FILE EXISTS ───────────────────────────────────────────────────
* An agent's iteration — an EPOCH — commits to a different place depending on
* how the chart was built. Under `reactMode: 'dynamic'` the `call-llm` stage is
* a parent-level stage and its bundle is in the run's own log. Under
* `'dynamic-grouped'` each turn runs inside an `sf-llm-call` subflow, which has
* its own isolated runtime and its own log, retained per iteration under
* `subflowResults['sf-llm-call#k'].treeContext.history`.
*
* That fork was written out THREE times — `contextLedger.ts` had a private
* `llmCallMountKeys`, `trajectory.ts` had a byte-twin of it, and a rebuild of
* what the model was served would have been the third. Three copies of "which
* log is this key in?" is three chances to disagree about which iteration a
* value belonged to, on the one question the whole family exists to answer.
* So the fork lives here once, and every reader asks it.
*
* ── WHAT AN EPOCH NUMBER IS ────────────────────────────────────────────────
* The committed `iteration` value in effect at the call — the same number
* `callLLM` put on the receipt's `basis.epoch` and on every `iteration_start`
* event. It starts at 1. It is READ, not counted: a run that pauses and
* resumes, or one whose first turn is not its first stage, still names its
* epochs the way the run itself named them.
*
* ── EVERY READ FOLDS FROM THE BASE ─────────────────────────────────────────
* A location carries the FOLD SOURCE its log was recorded against, not just
* the log, and every read below goes through `keyedFold.ts`. A resumed run is
* a fresh executor seeded from `checkpoint.sharedState`, so most of what the
* call read is base rather than log; a fold that could not see the base
* answered "absent" for the whole pre-pause world and said nothing about it.
* `EpochLocation.basis` is that fold's own verdict, carried out to the reader
* so a recording that travelled without its base declares a gap instead of a
* confident empty view.
*/
import type { CommitBundle } from 'foottrace';
import { type FoldBasis, type FoldSourceLike } from './keyedFold.js';
/** The shape a recording has to have for any of this to work — a snapshot, or
* anything that hands one over. */
export interface RecordingLike {
readonly commitLog?: readonly CommitBundle[];
/** The log's fold base (`RuntimeSnapshot.initialState`, footprintjs 9.17+).
* Absent on a recording that travelled without it — see
* {@link EpochLocation.basis}. */
readonly initialState?: Record<string, unknown>;
readonly executionTree?: unknown;
readonly subflowResults?: Record<string, unknown>;
}
/** One epoch, located: which log holds its call, where in that log, and what
* that log folds against. */
export interface EpochLocation {
/**
* The committed `iteration` at the call. 1-based, and READ rather than
* counted — a run that pauses and resumes still names its epochs the way the
* run itself named them.
*
* ONE fallback, and it is a count: when the fold cannot read `iteration` at
* all (a recording whose base did not travel, on a run that never re-`set`
* it), this is the location's position in run order instead. The tell is
* {@link EpochLocation.basis} / {@link EpochLocation.runBasis} reading
* `'log-only'`, which is also what makes `servedView.ts` raise
* `no-fold-base`. A view must have some number to be addressed by; this one
* is the honest second choice, not a claim about the record.
*/
readonly epoch: number;
/** The `call-llm` stage's `runtimeStageId` — the llm-turn stop's own id. */
readonly callRuntimeStageId: string;
/** The log the call committed to: the run's own under `'dynamic'`, the
* turn's inner history under `'dynamic-grouped'`. */
readonly log: readonly CommitBundle[];
/** The call's ARRAY index in {@link EpochLocation.log}. */
readonly callIdx: number;
/** The RUN's log, always. Run constants (a build-time fact seeded once) are
* read here, because a grouped turn's inner log never sees them unless the
* boundary happens to carry them. */
readonly runLog: readonly CommitBundle[];
/** The `sf-llm-call` mount this epoch was projected from — set under
* `'dynamic-grouped'` only, and the tell that `callIdx` is inner-relative. */
readonly subflowScope?: string;
/**
* The fold source {@link EpochLocation.log} belongs to — the log PLUS the
* base it was recorded against. Reads go through it, never through the bare
* array, so a value seeded before the run (every key of a resumed run) folds
* from where it actually came from.
*/
readonly source: FoldSourceLike;
/** The same, for {@link EpochLocation.runLog}. */
readonly runSource: FoldSourceLike;
/**
* How {@link EpochLocation.source} folds. `'log-only'` means the recording
* travelled without its fold base, so anything the log never `set` reads as
* absent — `servedView.ts` turns that into a declared gap rather than an
* empty view.
*/
readonly basis: FoldBasis;
/** The same, for {@link EpochLocation.runSource}. */
readonly runBasis: FoldBasis;
/** `false` when this recording carries no RUN log at all — a subtree handed
* in on its own. Every run constant is then unreadable, which
* `servedView.ts` declares rather than reads as absent. */
readonly hasRunLog: boolean;
}
/**
* A snapshot from whatever the caller had: a runner (`Agent`, `LLMCall`), or
* the snapshot itself. Duck-typed on purpose — a recording read back from JSON
* is a plain object and must work exactly like a live one.
*/
export declare function recordingOf(source: unknown): RecordingLike | undefined;
/**
* The `sf-llm-call` mount keys in `subflowResults`, in loop order.
*
* Non-empty ⇒ the run is GROUPED and every turn's pieces are in its own inner
* log. Empty ⇒ flat, and the run's own log holds everything.
*
* @example
* ```ts
* llmCallMountKeys(snapshot.subflowResults); // ['sf-llm-call#4', 'sf-llm-call#9']
* ```
*/
export declare function llmCallMountKeys(subflowResults: Record<string, unknown> | undefined): string[];
/**
* Every epoch in a recording, in run order — the ONE owner of the flat /
* grouped fork.
*
* Returns `[]` for a recording with no LLM call in it at all (an empty log, a
* chart with no `call-llm` stage). That is a different fact from "the epoch you
* asked for is not here", which is what {@link epochAt} says with `undefined`.
*
* MEMOIZED on the recording object, because the cost of locating epochs is a
* pass over the log and a per-epoch reader asks for one epoch at a time: before
* this, scrubbing a 600-turn run epoch by epoch relocated every epoch on every
* call and took 20.8 s. Holding the snapshot and asking it repeatedly is the
* shape a reader's UI actually has, so the memo is keyed on exactly that
* object; a caller that hands in a runner and gets a fresh snapshot each time
* pays the pass each time, which is the cost of asking a different question.
*
* FROZEN, because it is memoized: the array and each location on it are the
* very objects the next caller gets, so an edit here would be an edit to
* everybody's answer. The same law `keyedFold.ts` · `freezeDeep` states for a
* folded value. What is NOT frozen is what a location POINTS AT — `log`,
* `source`, `runSource` are the caller's own recording, and this file does not
* get to lock down an object it was merely handed.
*
* @example
* ```ts
* import { epochLocations } from 'agentfootprint';
*
* epochLocations(agent.getSnapshot()!).map((e) => [e.epoch, e.callRuntimeStageId]);
* // dynamic: [[1, 'call-llm#12'], [2, 'call-llm#31']]
* // dynamic-grouped: [[1, 'sf-llm-call/call-llm#9'], [2, 'sf-llm-call/call-llm#28']]
* ```
*/
export declare function epochLocations(source: unknown): readonly EpochLocation[];
/**
* One epoch by number, or `undefined` when the run has no such iteration.
*
* @example
* ```ts
* epochAt(agent.getSnapshot()!, 2)?.callRuntimeStageId; // 'call-llm#31'
* ```
*/
export declare function epochAt(source: unknown, epoch: number): EpochLocation | undefined;
/**
* The value of `key` as the epoch's call READ it — folded to just BEFORE the
* call's own bundle, so a key the call itself writes back (`history` gains the
* assistant turn; `receipt` is minted) reads as it was on the way in.
*
* Folded from the run's base, not from the log alone: on a resumed run the
* call read values the log never wrote, and reading those as absent is the
* fiction `keyedFold.ts` exists to end. A call that is its log's FIRST commit
* (a grouped turn, or the first stage after a resume) therefore folds to the
* base rather than to nothing.
*
* @example
* ```ts
* const epoch = epochAt(snapshot, 1)!;
* (readAtCall(epoch, 'history') as unknown[]).length; // the turns that went out
* ```
*/
export declare function readAtCall(location: EpochLocation, key: string): unknown;
/**
* The value of `key` the epoch's call itself COMMITTED — folded through the
* call's own bundle. The receipt is read this way, and only the receipt: it is
* the one value the call writes that describes the call.
*/
export declare function readAfterCall(location: EpochLocation, key: string): unknown;
/**
* A RUN CONSTANT: a build-time fact `seed` put on the record once, read from
* the run's own log.
*
* It is read from the run log rather than the epoch's log because that is where
* `seed` runs in both chart shapes. Under `'dynamic-grouped'` the turn's inner
* log only ever holds what the boundary's `inputMapper` carried in, and a
* constant nobody inside the turn reads is not carried — so asking the inner
* log would answer "absent" for a fact the run plainly recorded.
*
* `undefined` has TWO causes and a caller must not blur them: the run never
* recorded this constant, or this recording carries no run log to read
* (`EpochLocation.hasRunLog` is `false` — a subtree handed in on its own).
* `servedView.ts` branches on that flag and declares a gap for the second.
*/
export declare function readRunConstant(location: EpochLocation, key: string): unknown;