/** * RunStepRecorder — slider-ready ordered list of RunSteps, BUILT * INCREMENTALLY during traversal. Real-time recorder, not a walker. * * Pattern: extends `SequenceStore` (shared storage shelf) * and implements `CombinedRecorder` (FlowRecorder hooks). * Subscribes to the agentfootprint typed-event dispatcher * for actor-arrow events. Each event handler decides whether * to emit a step; state lives on the instance and persists * across the run. * Role: The single source of truth for "what slider positions * exist in this run, and what transitions does each light * up." Lens consumers attach the recorder once and read * `getSteps()` — no per-render re-derivation. * * Why this matters: the older `buildRunSteps(events)` walker violated * footprintjs's core principle ("collect during traversal, never * post-process"). Each call walked the full event log multiple times; * the playground triggered a full walk on every flowchart update, * yielding O(N²) total work for a streaming run. The recorder pattern * is O(N) — one handler call per event — and matches BoundaryRecorder / * FlowchartRecorder / KeyedStore idioms throughout the library. * * The `buildRunSteps(...)` function is RETAINED as a thin compatibility * shim that constructs a fresh recorder, replays events through it, * and returns the resulting entries. Useful for snapshot-from-saved- * events use cases (replay, testing, post-hoc analysis). Live consumers * should attach the recorder directly via `runner.attach(rec)`. */ import type { CombinedRecorder, FlowDecisionEvent, FlowForkEvent, FlowLoopEvent, FlowSubflowEvent, TraversalContext } from 'footprintjs'; interface FlowRunEvent { readonly payload?: unknown; readonly traversalContext?: TraversalContext; } import type { AgentfootprintEvent } from '../../events/registry.js'; import type { EventDispatcher, Unsubscribe } from '../../events/dispatcher.js'; import type { BoundaryRecorder, DomainEvent } from './BoundaryRecorder.js'; /** * One slider position. The smallest scrubable unit of the run. * * `transitions` is 1+ — fan-out / merge steps light up multiple * transitions at once; sequential / decide / react steps light up * exactly one. Renderers iterate `transitions` to highlight edges; * details panels read `anchor.runtimeStageId`. */ export interface RunStep { /** 0-based slider index (matches array position in `getSteps()`). */ readonly seq: number; readonly kind: RunStepKind; readonly transitions: readonly RunStepTransition[]; /** * Per-step key — required by `SequenceStore` for time-travel * utilities (`getEntriesForStep`, `getEntryRanges`). Mirrors * `anchor.runtimeStageId`; both fields point at the same value. * Top-level placement satisfies the recorder's storage contract. */ readonly runtimeStageId: string; /** Anchor for commentary highlight + details pane lookup. */ readonly anchor: { readonly runtimeStageId: string; readonly subflowPath: readonly string[]; }; /** Human label — short, kind-specific. */ readonly label: string; /** Wall-clock ms at which this step occurred. */ readonly tsMs: number; /** Kind-specific decoration. Discriminate on `kind`. */ readonly meta?: RunStepMeta; } export type RunStepKind = 'sequential' | 'fork' | 'merge' | 'decide' | 'iteration' | 'iteration-exit' | 'react'; export interface RunStepTransition { readonly from: string; readonly to: string; readonly via: 'next' | 'fork-branch' | 'decision-branch' | 'loop-iteration' | 'actor-arrow'; readonly label?: string; } export type RunStepMeta = { readonly kind: 'decide'; readonly chosen: string; readonly rationale?: string; } | { readonly kind: 'iteration'; readonly index: number; readonly target: string; } | { readonly kind: 'iteration-exit'; readonly index: number; readonly reason?: string; } | { readonly kind: 'fork'; readonly parentSubflowId: string; } | { readonly kind: 'merge'; readonly mergedCount: number; } | { readonly kind: 'react'; readonly actorArrow: 'user→llm' | 'tool→llm' | 'llm→tool' | 'llm→user'; }; export interface RunStepRecorderOptions { readonly id?: string; } /** Factory — matches the `boundaryRecorder()` / `topologyRecorder()` style. */ export declare function runStepRecorder(options?: RunStepRecorderOptions): RunStepRecorder; /** * Real-time slider-step recorder. Emits a `RunStep` whenever an event * marks a meaningful slider transition. State persists on the instance * so successive events update bookkeeping in O(1). * * Attach via `runner.attach(rec)` for FlowRecorder events; call * `rec.subscribe(runner.dispatcher)` for actor-arrow events. The * `getSteps(drillPath?)` method returns the already-built list (no * walking) with optional drill-scope filtering. */ export declare class RunStepRecorder implements CombinedRecorder { readonly id: string; /** Composition: storage shelf for the slider-step sequence. */ private readonly store; /** Run-boundary observer — fires this.clear() when traversalContext.runId * changes between events. THIS IS THE FIX for the Parallel multi-run * aliasing bug — without it `forkKey = ${parent}@${rid}` collides * because rid resets to `seed#0` on each run. */ private readonly runIdGuard; /** Stack of currently-open boundaries. The recorder owns this * directly because it's a simple stack and frames are recorder- * shaped. */ private boundaryStack; /** Fork-emission coalescing + branch-exit tally. */ private readonly forks; /** Tracks the most-recent leaf exit per depth → "forwards" handoff. */ private readonly siblings; /** Buffers a "this MIGHT be the answer" leaf until onRunEnd. */ private readonly answerBuffer; /** Run-root inference state machine (leaf vs composition). */ private readonly rootInferrer; /** llm.start / llm.end actor-arrow classifier. */ private readonly actorArrows; /** Has the first `asks` step fired? */ private asksEmitted; constructor(options?: RunStepRecorderOptions); /** * Emit a RunStep, auto-mirroring `anchor.runtimeStageId` to the * top-level `runtimeStageId` field that the keyed index uses. Single * source of truth (the anchor) — never inconsistent with the storage * key. */ private push; /** Internal seq-numbering helper — mirrors the store size so each * RunStep gets a unique 0-based index in emit order. */ private get entryCount(); clear(): void; /** Internal — wipe all per-run state WITHOUT resetting the runIdGuard * itself. Called by `clear()` (which then resets the guard) AND by * the runIdGuard's onNewRun callback (where the guard is mid-update * and must NOT be reset, only the recorder's data should be). * * Note: each sub-tracker owns its OWN clear; the orchestrator just * fans out. Adding new state to a sub-tracker requires no edit here. */ private resetForNewRun; private observeRunId; onRunStart(event: FlowRunEvent): void; onRunEnd(event: FlowRunEvent): void; onSubflowEntry(event: FlowSubflowEvent): void; onSubflowExit(event: FlowSubflowEvent): void; onFork(event: FlowForkEvent): void; onDecision(event: FlowDecisionEvent): void; onLoop(event: FlowLoopEvent): void; /** * Subscribe to the runner's typed-event dispatcher and emit a * `react` RunStep on every `llm.start` / `llm.end`. The recorder * classifies `actorArrow` locally (mirrors BoundaryRecorder's * pattern) so consumers don't have to depend on BoundaryRecorder's * own subscription order. */ subscribe(dispatcher: EventDispatcher): Unsubscribe; /** Internal — also called by `ingestDomainEvent` for shim replay. * * NOTE: deliberately does NOT call observeRunId(event.meta.runId). * The agentfootprint dispatcher's runId is a DIFFERENT generator * than footprintjs's traversalContext.runId — mixing them would * toggle lastRunId on every event and trigger a false reset. * Run-boundary detection happens reliably on the FlowRecorder side * (onRunStart fires FIRST in any new run before any typed event). */ protected ingestTypedEvent(event: AgentfootprintEvent): void; /** * Feed a single recorded `DomainEvent` (from BoundaryRecorder) into * this recorder as if it had fired live. Used by `buildRunSteps` * for snapshot replay; tests use it for fixture-driven projection. * * Live consumers should use `runner.attach(rec)` + * `rec.subscribe(dispatcher)` instead — the recorder's hooks fire * naturally during traversal. */ ingestDomainEvent(e: DomainEvent): void; /** * Read-only query — returns the already-built step list filtered to * `drillPath` scope. O(1) per call when scope is empty; O(N) filter * otherwise. Composition-vs-leaf root filter is applied so the * slider semantics match the user's mental model: * * - **Leaf root** (single Agent / LLMCall): show `react` steps only. * - **Composition root** (Sequence / Parallel / Conditional / Loop): * show composition steps; hide intra-leaf `react` steps. * * Drill-down filters by `anchor.subflowPath` prefix and re-applies * the leaf-vs-composition rule for the drilled scope. */ getSteps(drillPath?: readonly string[]): readonly RunStep[]; /** Flush any deferred answer-candidate from the buffer. Called by * `onRunEnd` so a single `answers` step appears for runs that end * on a leaf exit (no further leaf entries followed). */ private flushCandidateAnswer; } export interface RunStepGraph { readonly steps: readonly RunStep[]; } export interface BuildRunStepsOptions { readonly drillPath?: readonly string[]; } /** * Compatibility shim for snapshot-from-events use cases (replay, * post-hoc analysis, tests). For LIVE use, prefer attaching a * `RunStepRecorder` directly via `runner.attach(rec)` — * `buildRunSteps(events)` constructs a fresh recorder, replays the * events through its handlers, and returns the resulting entries. * * @deprecated Prefer `runStepRecorder()` + `runner.attach(rec)` for * live consumers. This shim remains for offline / testing * scenarios where only a recorded event list is available. */ export declare function buildRunSteps(source: BoundaryRecorder | readonly DomainEvent[], options?: BuildRunStepsOptions): RunStep[]; export {};