/** * FlowchartRecorder — StepGraph projection over `BoundaryRecorder`. * * Pattern: Pure projection. `attachFlowchart` wires a `BoundaryRecorder` * to the executor + dispatcher; `buildStepGraph` is a fold over * `boundary.getEvents()` that produces the renderer-friendly * `StepGraph` shape Lens (and any other consumer) renders. * * ZERO state machine in this file. ZERO event subscription. * ZERO name-based filters. Everything that decides "what's a * step" lives in BoundaryRecorder via capture-time tags * (`actorArrow`, `slotKind`, `primitiveKind`, `isAgentInternal`). * * Role: Tier 3 observability. Enabled via * `runner.enable.flowchart({ onUpdate })`. The handle exposes: * * handle.getSnapshot() → derived StepGraph (back-compat) * handle.boundary → the underlying BoundaryRecorder * (Lens reads it directly for richer * queries: getSlotBoundaries(), * getEventsByType, etc.) * * Event → StepNode mapping (the entire policy): * * run.entry → StepNode kind='subflow', primitiveKind='Run' * subflow.entry → StepNode kind='subflow' (skipped if isAgentInternal * or slotKind set — * those are sub-components * of the actor arrows) * fork.branch → StepNode kind='fork-branch' * decision.branch → StepNode kind='decision-branch' * llm.start → StepNode kind=actorArrow ('user→llm' | 'tool→llm') * tool.start → StepNode kind='llm->tool' * llm.end terminal → StepNode kind='llm->user' (delivery marker) * loop.iteration → loop-iteration StepEdge * context.injected → attached to NEXT user→llm / tool→llm StepNode * * Result: a one-to-one correspondence between visible scrubbable steps * and DomainEvents. Adding a new event type adds one mapping line here * (or in the pure projection); no state machine, no merging. */ import type { CombinedRecorder } from 'footprintjs'; import type { EventDispatcher } from '../../events/dispatcher.js'; import { BoundaryRecorder, type DomainEvent } from './BoundaryRecorder.js'; /** * One node in the step-level flowchart. Node kind drives rendering * (actor icon, color). ReAct steps carry token + tool details; topology * nodes (subflow / fork-branch / decision-branch) mirror the footprintjs * composition events and exist so composition structure (Loop, Parallel, * Conditional, Swarm) stays visible in the graph. */ export interface StepNode { readonly id: string; readonly kind: 'user->llm' | 'llm->tool' | 'tool->llm' | 'llm->user' | 'subflow' | 'fork-branch' | 'decision-branch'; readonly label: string; readonly startOffsetMs: number; readonly endOffsetMs?: number; /** LLM step: token usage of the call that bounded this step. */ readonly tokens?: { readonly in: number; readonly out: number; }; /** llm->tool / tool->llm: the tool name. */ readonly toolName?: string; /** user->llm / tool->llm: the model that was invoked. */ readonly llmModel?: string; /** Decomposition of the underlying subflowId (rooted under '__root__'). */ readonly subflowPath: readonly string[]; /** Context injections attributed to this step (LLM steps only). */ readonly injections?: readonly ContextInjection[]; /** 1-based ReAct iteration this step belongs to. Undefined for * topology / composition nodes. */ readonly iterationIndex?: number; /** Which slot the step's input updated. ReAct steps only. */ readonly slotUpdated?: 'system-prompt' | 'messages' | 'tools'; /** True ONLY for `subflow` StepNodes whose primitiveKind is `'Agent'`. * Narrow flag for callers that distinguish ReAct agents from other * composition primitives (cost / iteration / token attribution). */ readonly isAgentBoundary?: boolean; /** Primitive kind from the subflow root description prefix * (`'Agent'` / `'LLMCall'` / `'Sequence'` / etc.). */ readonly primitiveKind?: string; /** True for `subflow` StepNodes representing any KNOWN primitive * (Agent / LLMCall / Sequence / Parallel / Conditional / Loop) — * drives Lens's drill-in container treatment. */ readonly isPrimitiveBoundary?: boolean; /** `inputMapper` payload at the subflow's entry. Subflow nodes only. */ readonly entryPayload?: unknown; /** Subflow shared state at exit. Subflow nodes only. * Undefined for in-progress / paused subflows. */ readonly exitPayload?: unknown; /** LLM's text content. Set on `llm->tool` (the reasoning emitted with the * tool_use blocks) and on `llm->user` (the terminal answer). */ readonly assistantText?: string; /** Tool input arguments the LLM produced. Set on `llm->tool` from the * matching `tool.start` event payload. */ readonly toolArgs?: unknown; /** Tool result returned to the LLM. Set on `tool->llm` from the * preceding `tool.end` event payload. */ readonly toolResult?: unknown; /** Stable per-execution key — same `runtimeStageId` Trace view uses. */ readonly runtimeStageId?: string; /** * Slot boundary payloads composed for THIS LLM step. * * Set ONLY for `kind === 'user->llm'` and `kind === 'tool->llm'` * StepNodes — the moments where context flows INTO the LLM. Each * entry carries the slot subflow's `inputMapper` result (entryPayload) * and rendered slot output (exitPayload). * * Attribution: any slot subflow that fired BETWEEN the previous LLM * end (or run start) and THIS LLM start is attributed to this call. * Done at projection time over `boundary.getEvents()`; no consumer- * side correlation required. * * Lens uses this to make the 3 slot rows inside the LLM card * clickable — clicking a slot reveals its entry/exit payloads in * the right-pane detail panel without needing direct BoundaryRecorder * access. */ readonly slotBoundaries?: { readonly systemPrompt?: SlotBoundary; readonly messages?: SlotBoundary; readonly tools?: SlotBoundary; }; } /** One slot's boundary pair attributed to a specific LLM step. */ export interface SlotBoundary { /** runtimeStageId of the slot subflow execution. */ readonly runtimeStageId: string; /** `inputMapper` payload — the data the slot was COMPOSED FROM * (RAG hits, skill content, user message, tool result, etc). */ readonly entryPayload?: unknown; /** Subflow shared state at exit — the rendered slot content * (system prompt string, messages array, tools array). */ readonly exitPayload?: unknown; } /** Consumer-facing context injection (5 axes of context engineering). */ export interface ContextInjection { readonly slot: 'system-prompt' | 'messages' | 'tools'; readonly asRole?: 'system' | 'user' | 'assistant' | 'tool'; readonly source: string; readonly sourceId?: string; readonly contentSummary?: string; readonly reason?: string; readonly sectionTag?: string; readonly upstreamRef?: string; readonly retrievalScore?: number; readonly rankPosition?: number; readonly budgetTokens?: number; readonly budgetFraction?: number; } export interface StepEdge { readonly id: string; readonly from: string; readonly to: string; readonly kind: 'next' | 'loop-iteration' | 'fork-branch' | 'decision-branch'; readonly iteration?: number; } export interface StepGraph { readonly nodes: readonly StepNode[]; readonly edges: readonly StepEdge[]; readonly activeNodeId?: string; } export interface FlowchartOptions { /** Called each time the graph changes; fires synchronously on the * driving event so the UI updates the moment the structure changes. */ readonly onUpdate?: (graph: StepGraph) => void; } export interface FlowchartHandle { /** Current step graph (derived from boundary events). Safe during or * after a run. */ readonly getSnapshot: () => StepGraph; /** Underlying BoundaryRecorder. Use for richer queries — slot data, * full event log, type-narrowed lookups. The single source of truth * Lens reads. */ readonly boundary: BoundaryRecorder; /** Detach from executor + dispatcher. Subsequent events ignored. */ readonly unsubscribe: () => void; } /** * Attach a live FlowchartRecorder to a runner. * * 1. Creates a `BoundaryRecorder` (the unified domain event log) with * commit tracking, so every boundary records WHERE on the commit * axis it happened. * 2. Attaches it to the executor's FlowRecorder channel via * `runnerAttach` — captures run / subflow / fork / decision / loop. * 3. Subscribes it to the dispatcher — captures llm.* / tool.* / * context.injected. * 4. Wires `onUpdate` so the consumer sees a fresh derived StepGraph * on every event. * * @param getCommitCount where each boundary sits on the run's commit * axis — the runner's `getCommitCount()`, sampled live. Optional only * because a caller may have no runner to sample; omitting it stamps * every boundary `commitIdxBefore: 0`, which reads as "all boundaries * opened at once" and leaves an offline step strip with nothing to * place. Nothing downstream can repair it — the commit log records * what stages wrote, never when a boundary was crossed — so the * runner always passes it. * * @internal Called from `RunnerBase.enable.flowchart`. */ export declare function attachFlowchart(runnerAttach: (recorder: CombinedRecorder) => () => void, dispatcher: EventDispatcher, options?: FlowchartOptions, getCommitCount?: () => number): FlowchartHandle; /** * Project a `BoundaryRecorder`'s event stream into a `StepGraph`. * * Pure function — no side effects, no recorder mutation, deterministic. * Called on every snapshot request and on every `onUpdate` fire. O(N) * over the event stream; consumer-side memoization (e.g., React's * `useMemo`) is straightforward when needed. * * The mapping is local to each event type: see the mapping table in * the file header. State carried across the fold: * - `iter`: 1-based ReAct iteration counter, incremented on each * `llm.start`. ReAct nodes inherit the current value. * - `pendingInjections`: context.injected events buffered between * LLM calls; flushed onto the next user→llm or tool→llm StepNode. * - `prevReActId`: id of the previous ReAct StepNode for `next`-edge * wiring within an iteration. * - `runStartTs`: wall-clock at run start, for relative offsets. */ export declare function buildStepGraph(boundary: BoundaryRecorder): StepGraph; /** * Pure events → StepGraph fold. Same projection as `buildStepGraph`, but from a * flat `DomainEvent[]` rather than a live `BoundaryRecorder` — so an offline * `Trace` (which stores only events) can be rebuilt into a graph for `` * without re-running the agent. The graph is always derived, never stored. */ export declare function buildStepGraphFromEvents(events: readonly DomainEvent[]): StepGraph;