agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
150 lines • 8.72 kB
TypeScript
/**
* milestoneStops — the agent's own scrub stops, derived from its commit log.
*
* Role: Fold. It reads a finished run's commit log and answers one question —
* "which positions in this run is a reader allowed to stop at?" It
* executes nothing, records nothing, and stores nothing beside the log.
* Reads: a recorded commit log (plus the execution tree when the caller has
* it), and `conventions.ts` · `milestoneFor` for the domain vocabulary.
* Emits: N/A.
*
* WHY THIS FILE EXISTS. footprintjs ships the reader's cursor (`timeTravel`)
* and one stop grammar — `commitStops`, one stop per executed stage — because
* that is the only grammar the substrate itself knows. It does NOT know what an
* LLM turn is. agentfootprint does: `milestoneFor` has classified a local stage
* id into an iteration / slot / llm-turn / tool-call / decision since 9.x, and
* until 9.87 every consumer that wanted a milestone slider mapped that
* classifier onto commits by hand. This is that mapping, once, on the strategy
* seam the port opened — so the agent SUPPLIES the stops for its own runs and
* every reader gets the same axis.
*
* WHAT IT IS (9.89.0). A FILTER over the port's own axis. footprintjs 9.18
* shipped `filterStops(stops, keep)` — the bookend guard, the re-partition and
* a `meta` slot on the stop — because two consumers had each re-derived all
* three by hand against 9.17, this file among them. The hand-rolled copies are
* gone: the one owner of the `[start, …stages, end]` contract is the library
* that returns it, and this strategy is one expression over it. The axis is
* byte-for-byte the 9.88.0 axis (`test/time-travel/milestone-stops-equivalence.test.ts` pins
* that against a verbatim copy of the 9.88.0 implementation); what is new is that the milestone now RIDES on the
* stop as `meta`, and that a start which absorbed pre-milestone stages says so
* with `prologue: true`.
*
* WHAT IT IS NOT. Not a second cursor. A strategy only says where the one
* cursor may rest; `timeTravel` still owns the position, the folds and the
* marks. And not a re-walk: a milestone that was never committed gets no stop,
* because a cursor that stops where no evidence exists is telling a story
* rather than reading one.
*/
import type { StageSnapshot } from 'footprintjs/advanced';
import type { CommitBundle, Stop, TimeTravelStrategy } from 'foottrace';
import { type Milestone } from '../lib/conventions.js';
/**
* The milestone a stop stands for, or `null` when it stands for none (the
* `'start'` / `'end'` bookends).
*
* A stop that {@link milestoneStops} made carries its milestone as
* `stop.meta`, and that is what is read — the classification the strategy
* made when it put the stop on the axis, not a second run of the classifier.
* A stop from ANOTHER strategy — the port's own `commitStops`, or a consumer's
* filter that kept the stop without a milestone meta — has none, so the answer
* falls back to where it always came from: `milestoneFor` over the stop's
* `runtimeStageId`. Same classifier, same answer; a `meta` of some other
* vocabulary is not mistaken for ours.
*
* @example
* ```ts
* const stop = cursor.at()!;
* milestoneOf(stop)?.kind; // 'llm-turn'
* ```
*/
export declare function milestoneOf(stop: Stop<unknown>): Milestone | null;
/**
* Derive one stop per committed MILESTONE from a recorded commit log.
*
* The rule, in one sentence: take footprintjs's own per-stage axis, keep the
* stages `milestoneFor` classifies, and give the survivors the log back.
*
* **Composed, never re-derived.** The hard parts of the per-stage axis are
* already solved upstream and are not repeated here: one stop per
* `runtimeStageId` at its FIRST commit (a subflow mount commits twice, a
* parallel fork child commits twice and siblings interleave — every repeat is
* an empty bundle), the authoritative mount set read off the execution tree,
* the `'start'` / `'end'` bookends, and the id-less leading commit that carries
* a subflow's `inputMapper` seed. And since 9.89.0 the COMPOSITION itself is
* the port's too: `filterStops` checks the `[start, …stages, end]` shape,
* keeps what `keep` keeps, and re-partitions the log. A second implementation
* of any of that would be a second chance to disagree with the library about
* what a stage is.
*
* **The survivors re-partition the log.** A stage that classifies `null` is not
* a stop, but its commits still happened, so they are folded into the stop that
* PRECEDES them: every kept stop's `lastCommitIdx` is extended to just before
* the next kept stop begins. Nothing is orphaned, and `stateAt(stop)` remains
* exactly "the state that existed when the next milestone's stage started".
* Commits before the FIRST milestone belong to `'start'` for the same reason —
* so on a drilled cursor `'start'` reads as "what this subflow began with,
* after its plumbing ran".
*
* **What that costs `'start'`, and the flag that says so.** On footprintjs's
* own axis `'start'` is the fold base: the state before ANY stage ran. Here it
* absorbs every stage that ran before the first milestone, so `stateAt(start)`
* is the state the first milestone READ, not the run's raw base — a real agent
* seeds a couple of dozen keys in `seed` before anything a reader would scrub
* to. That is the right answer for this axis (the stops must still partition
* the log), and since 9.18 the port marks it: the returned start carries
* `prologue: true` whenever it absorbed a stage, so a renderer that means
* "before anything ran" checks `kind === 'start' && !prologue` instead of
* assuming it from the kind.
*
* **The milestone rides on the stop.** Every kept stop carries its
* classification as `meta` — `Stop<Milestone>` — so a reader asks
* `stop.meta?.kind` (or {@link milestoneOf}, which reads the same slot) rather
* than re-running the classifier over the id. The bookends carry none.
*
* **A log with no milestones in it at all.** A non-empty log the classifier
* recognises nothing in — a non-agent chart handed this strategy — yields the
* two bookends and nothing between them: `'start'` folds the whole log,
* `'end'` folds the whole log, and `jumpTo` any stage id refuses with
* `reason: 'miss'`. An axis with nowhere meaningful to stand, which is the
* truthful shape for a run that has no milestones — not an error, and not the
* empty `[]` that says the log itself was empty.
*
* **It does not know which log it was given.** The classifier reads the LOCAL
* segment of a stage id, so `sf-llm-call#3` on a run's outer log and
* `sf-llm-call/call-llm#7` on that mount's drilled inner history are both
* classified without the strategy being told which one it is holding. That is
* what lets one strategy serve `reactMode: 'dynamic'` (the `call-llm` bundle is
* in the outer log → an llm-turn stop on the outer cursor) and
* `'dynamic-grouped'` (the outer log holds `sf-llm-call` mounts → iteration
* stops; `drill()` gives the inner cursor, where the same strategy finds the
* llm-turn, tool-call and decision stops).
*
* @param commitLog the run's `commitLog`, or a subflow's own `history`.
* @param executionTree the run's `executionTree`, when the caller has it — it
* is what makes a mount stop say `kind: 'mount'` rather than being guessed
* from bundle shape.
*
* @example
* ```ts
* import { timeTravel } from 'foottrace';
* import { milestoneStopsStrategy } from 'agentfootprint';
*
* const cursor = timeTravel(agent.getSnapshot()!, { strategy: milestoneStopsStrategy });
* cursor.stops.map((s) => [s.label, s.meta?.kind]);
* // measured on a two-turn `dynamic` run — 42 commits, 15 stops:
* // [['Run start', undefined], ['Iteration', 'iteration'],
* // ['System prompt', 'slot'], ['Messages', 'slot'], ['Tools', 'slot'],
* // ['LLM turn', 'llm-turn'], ['Route', 'decision'], ['Tool call', 'tool-call'],
* // … the second turn …, ['Run end', undefined]]
* cursor.stops[0].prologue; // true — `seed` and the plumbing ran before the first Iteration
* ```
*/
export declare function milestoneStops(commitLog: readonly CommitBundle[], executionTree?: StageSnapshot): Stop<Milestone>[];
/**
* `milestoneStops` as a footprintjs `TimeTravelStrategy` — what you hand
* `timeTravel(snapshot, { strategy })`. Typed over {@link Milestone}, so the
* cursor's stops are `Stop<Milestone>` and `cursor.at()?.meta?.kind` is typed;
* it is still assignable wherever a bare `TimeTravelStrategy` is expected.
*/
export declare const milestoneStopsStrategy: TimeTravelStrategy<Milestone>;
//# sourceMappingURL=milestoneStops.d.ts.map