milestoneStops
Function: milestoneStops()
milestoneStops(
commitLog,executionTree?):Stop[]
Defined in: src/lib/time-travel/milestoneStops.ts:129
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. This function calls commitStops and filters
its result. A second implementation of that collapsing 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'. 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 the wrong
one to assume from kind: 'start' alone, so it is said out loud here, in the
folder README, on the docs page and in a test.
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).
Parameters
commitLog
readonly CommitBundle[]
the run's commitLog, or a subflow's own history.
executionTree?
StageSnapshot
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.
Returns
Stop[]
Example
import { timeTravel } from 'footprintjs/trace';
import { milestoneStopsStrategy, milestoneOf } from 'agentfootprint';
const cursor = timeTravel(agent.getSnapshot()!, { strategy: milestoneStopsStrategy });
cursor.stops.map((s) => [s.label, milestoneOf(s)?.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]]