/**
 * BoundaryRecorder — unified domain event log for an agentfootprint run.
 *
 * The single source of truth Lens (and any other consumer) reads to
 * render a run. Every observable moment in a run is captured as one
 * `DomainEvent` in a single ordered stream:
 *
 *   - `run.entry` / `run.exit`              — top-level executor.run()
 *   - `subflow.entry` / `subflow.exit`      — every subflow boundary
 *   - `fork.branch`                         — one per parallel child
 *   - `decision.branch`                     — chosen branch of a Conditional
 *   - `loop.iteration`                      — one per back-edge traversal
 *   - `llm.start` / `llm.end`               — LLM provider call lifecycle
 *   - `tool.start` / `tool.end`             — tool execution lifecycle
 *   - `context.injected`                    — anything injected into a slot
 *
 * All events carry `runtimeStageId` (binds with footprintjs Trace view +
 * with each other), `subflowPath`, `depth`, and `ts` (wall-clock ms).
 * Subflow events are domain-tagged (`slotKind` / `primitiveKind` /
 * `isAgentInternal`) so consumers dispatch on tag without re-parsing.
 *
 * Architecture:
 *
 *   ┌──── footprintjs (domain-agnostic) ────┐
 *   │  FlowRecorder events (run/subflow/    │  ──┐
 *   │  fork/decision/loop)                  │    │
 *   └───────────────────────────────────────┘    │
 *                                                │
 *   ┌──── agentfootprint dispatcher ─────────┐   │  consumed by
 *   │  Typed events (llm/tool/context)       │  ──┤
 *   └────────────────────────────────────────┘   │
 *                                                ▼
 *                                  ┌─── BoundaryRecorder ───┐
 *                                  │  one tagged stream of  │
 *                                  │  DomainEvent           │
 *                                  └────────────────────────┘
 *                                                │
 *                                                ▼  consumed by
 *                                  ┌────── Lens (UI) ──────┐
 *                                  │  Slider / RunFlow /   │
 *                                  │  NodeDetail / etc.    │
 *                                  └───────────────────────┘
 *
 * Why ONE recorder: Lens scrub axis, run-flow graph, slot rows inside
 * the LLM card, right-pane detail panel, commentary panel — every UI
 * surface reads from the SAME stream. Adding a new domain event = one
 * tagged emit + one render shape. No state machines spread across
 * renderers, no merging of multiple sources, no name-based filter lists.
 *
 * Naming: `runtimeStageId` is footprintjs's primitive (path-prefixed +
 * `#executionIndex`). `subflowPath` is rooted under the synthetic
 * `'__root__'`. `slotKind` / `primitiveKind` are agent-domain. The
 * design follows the React Fiber + OpenTelemetry pattern:
 * **producers self-describe; consumers dispatch on type**.
 *
 * WIRING — THREE PARTS, ALL AT RECORD TIME
 * ────────────────────────────────────────
 * This recorder hears half a run per connection, and the half it misses
 * cannot be reconstructed afterwards. All three go on BEFORE `run()`:
 *
 *   1. `runner.attach(boundary)` — the footprintjs control-flow channel:
 *      run / subflow entry and exit, forks, decisions, loops. Without it
 *      there are no boundaries at all.
 *   2. `boundary.subscribe(runner)` — the agentfootprint typed stream:
 *      `llm.*`, `tool.*`, `context.injected`. Without it the boundaries
 *      have no content: no model calls, no tool calls, no injections.
 *   3. `{ getCommitCount }` — where each boundary sits on the commit
 *      axis. Without it every event is stamped `commitIdxBefore: 0` and
 *      the step strip cannot be rebuilt from the recording later. This
 *      one fails SILENTLY — the events all look fine.
 *
 * There is no after-the-fact fix for any of the three: a completed run
 * leaves a snapshot and a commit log, and neither says where a boundary
 * opened relative to the commits. `recordRun(runner)` does all three and
 * hands back `{ snapshot, events, structure }` — reach for it first, and
 * wire by hand only when you need a shape it doesn't produce.
 *
 * @example
 * ```typescript
 * import { boundaryRecorder, EventDispatcher } from 'agentfootprint';
 *
 * const boundary = boundaryRecorder({
 *   getCommitCount: () => executor.getCommitCount(),  // the commit axis
 * });
 * const dispatcher = new EventDispatcher();
 * executor.attachCombinedRecorder(boundary);   // wires FlowRecorder side
 * boundary.subscribe(dispatcher);              // wires typed-event side
 *
 * await executor.run({ input });
 *
 * for (const e of boundary.getEvents()) {
 *   switch (e.type) {
 *     case 'run.entry':       renderRoot(e); break;
 *     case 'subflow.entry':   if (e.slotKind) renderSlotRow(e);
 *                              else if (e.primitiveKind) renderPrimitive(e);
 *                              break;
 *     case 'llm.start':       renderLLMCall(e); break;
 *     // ...
 *   }
 * }
 * ```
 */
import { CommitRangeIndex } from 'foottrace';
import type { CombinedRecorder, FlowDecisionEvent, FlowForkEvent, FlowLoopEvent, FlowSubflowEvent, TraversalContext } from 'footprintjs';
interface FlowRunEvent {
    readonly payload?: unknown;
    readonly traversalContext?: TraversalContext;
}
interface FlowRunFailedEvent {
    readonly structuredError: {
        readonly message: string;
    };
    readonly traversalContext?: TraversalContext;
}
import type { AgentfootprintEvent } from '../../events/registry.js';
import type { ToolEndPayload, ToolStartPayload } from '../../events/payloads.js';
import type { Unsubscribe } from '../../events/dispatcher.js';
import type { ContextSlot } from '../../events/types.js';
/** Fields every domain event carries. */
interface DomainEventBase {
    /** Stable per-execution key (footprintjs primitive). For run events it
     *  is `'__root__#0'`; subflow events use the parent stage's runtimeStageId
     *  at mount; typed events use the firing stage's runtimeStageId. */
    readonly runtimeStageId: string;
    /** Decomposition of `subflowId` into segments, rooted under `'__root__'`. */
    readonly subflowPath: readonly string[];
    /** Depth in the run tree — root = 0, top-level subflow = 1, etc. */
    readonly depth: number;
    /** Wall-clock ms at capture time. */
    readonly ts: number;
    /** Commit count when this event fired. 0 if the recorder was
     *  constructed without `getCommitCount` (legacy mode). The boundary
     *  RANGE for an (entry, exit) pair is `[entry.commitIdxBefore,
     *  exit.commitIdxBefore]`. Phase 5 Layer 2 — see
     *  `docs/design/boundary-commit-ranges.md`. */
    readonly commitIdxBefore: number;
    /** RESERVED for future event types that trigger engine writes.
     *  CURRENT BEHAVIOR: always equals `commitIdxBefore` for every event
     *  emitted by today's BoundaryRecorder. Observer events don't write
     *  to scope, so the executor's commit count doesn't change between
     *  the moment the event is sampled and the moment it's recorded.
     *  Consumers should currently treat this as identical to
     *  `commitIdxBefore`; do NOT rely on it being strictly greater.
     *  The field exists for forward compatibility — if a future
     *  observer pattern triggers commits during its handler, this is
     *  where the post-effect count will land. */
    readonly commitIdxAfter: number;
}
export interface DomainRunEvent extends DomainEventBase {
    readonly type: 'run.entry' | 'run.exit';
    readonly payload?: unknown;
    /** Always `true` for run events — convenience flag for filter callers. */
    readonly isRoot: true;
}
export interface DomainSubflowEvent extends DomainEventBase {
    readonly type: 'subflow.entry' | 'subflow.exit';
    /** Path-prefixed engine id (matches `FlowSubflowEvent.subflowId`). */
    readonly subflowId: string;
    /** Last segment of `subflowId` — convenience for leaf-name grouping. */
    readonly localSubflowId: string;
    readonly subflowName: string;
    /** Build-time description from the subflow root (`'<Kind>: <detail>'`). */
    readonly description?: string;
    /** Parsed `'<Kind>:'` prefix — `'Agent'`, `'LLMCall'`, `'Sequence'`, etc. */
    readonly primitiveKind?: string;
    /** Set ONLY for the 3 input-slot subflows (sf-system-prompt / sf-messages / sf-tools). */
    readonly slotKind?: ContextSlot;
    /** True for Agent state-machine routing/wrapper subflows (route, tool-calls, final, merge). */
    readonly isAgentInternal: boolean;
    /** `inputMapper` result on entry; subflow shared state on exit. */
    readonly payload?: unknown;
}
export interface DomainForkBranchEvent extends DomainEventBase {
    readonly type: 'fork.branch';
    readonly parentSubflowId: string;
    readonly childName: string;
}
export interface DomainDecisionBranchEvent extends DomainEventBase {
    readonly type: 'decision.branch';
    readonly decider: string;
    readonly chosen: string;
    readonly rationale?: string;
    /**
     * `true` when this decision comes from one of the Agent's internal
     * routing stages (e.g., the ReAct `Route` decider that picks
     * `tool-calls` vs `final`). Filtered out of the timeline by
     * `buildStepGraph` — the actor arrows that follow already encode
     * the routing observably (`llm→tool` vs `llm→user`).
     *
     * `false` when the decision comes from a consumer-defined
     * `Conditional` primitive — those ARE meaningful timeline steps.
     */
    readonly isAgentInternal: boolean;
}
export interface DomainLoopIterationEvent extends DomainEventBase {
    readonly type: 'loop.iteration';
    readonly target: string;
    readonly iteration: number;
}
/**
 * Composition boundary event — fired for every composition primitive
 * (Parallel / Sequence / Loop / Conditional). Mirrors `subflow.entry/exit`
 * but for the COMPOSITION wrapper itself (the box that contains the
 * branches / steps / iterations / chosen-branch).
 *
 * This pair OPENS and CLOSES a boundary range in `boundaryIndex`. Child
 * subflows that fire between the pair nest naturally inside the
 * composition's range.
 *
 * The `runtimeStageId` is the composition's own per-execution id —
 * SAME format as any other runtimeStageId, with `#executionIndex`. The
 * `kind` discriminates which composition primitive this is.
 *
 * For the Lens compound time axis, this group is what collapses
 * parallel branches into ONE slider position at the parent's drill
 * level. Drill into the composition to see its children as positions.
 */
export interface DomainCompositionEvent extends DomainEventBase {
    readonly type: 'composition.start' | 'composition.end';
    readonly kind: 'Parallel' | 'Sequence' | 'Loop' | 'Conditional';
    readonly compositionId: string;
    readonly name: string;
    /** On `composition.end`, the exit status reported by the composition. */
    readonly status?: 'ok' | 'err' | 'break' | 'budget_exhausted';
    readonly durationMs?: number;
}
/**
 * The 4 actor arrows of a ReAct cycle. Tagged on `llm.start` / `llm.end`
 * at capture time so consumers (slider, run-flow renderer) dispatch by
 * `event.actorArrow` instead of running their own state machine.
 *
 *   - `'user→llm'` — first LLM call, or any LLM call NOT preceded by a
 *     tool result (assembled-context delivery to the model).
 *   - `'tool→llm'` — LLM call that follows a tool's result (the next
 *     iteration of a ReAct loop).
 *   - `'llm→tool'` — `llm.end` whose `toolCallCount > 0` (the LLM is
 *     requesting tool execution).
 *   - `'llm→user'` — `llm.end` with `toolCallCount === 0` (terminal
 *     response delivered to the user).
 */
export type ActorArrow = 'user→llm' | 'tool→llm' | 'llm→tool' | 'llm→user';
export interface DomainLLMStartEvent extends DomainEventBase {
    readonly type: 'llm.start';
    readonly model: string;
    readonly provider: string;
    readonly systemPromptChars?: number;
    /**
     * The ASSEMBLED system prompt, verbatim as sent (9.88.0) — carried through
     * from `stream.llm_start`, so present under exactly the same condition:
     * the run was built with `recordSystemPrompt: true`. It used to be dropped
     * here, which meant the one opt-in that puts the prompt on the record
     * reached every sink EXCEPT the ordered boundary stream a replay reads.
     *
     * PRIVACY: it carries whatever its pieces carried. See
     * `payloads.ts` · `LLMStartPayload`.
     */
    readonly systemPromptText?: string;
    readonly messagesCount?: number;
    readonly toolsCount?: number;
    /**
     * The tool CATALOG the model saw for this call — `{ name, description }` per
     * tool, in request order (9.88.0, carried through from `stream.llm_start`;
     * absent when the call had no tools). `toolsCount` above says HOW MANY, and
     * a count is not an answer to "which tools was it choosing between?".
     */
    readonly tools?: readonly {
        readonly name: string;
        readonly description?: string;
    }[];
    /** Capture-time classification: `'user→llm'` for the first call or any
     *  call not preceded by a tool result; `'tool→llm'` after a tool result. */
    readonly actorArrow: 'user→llm' | 'tool→llm';
}
export interface DomainLLMEndEvent extends DomainEventBase {
    readonly type: 'llm.end';
    readonly content: string;
    readonly toolCallCount: number;
    readonly usage: {
        readonly input: number;
        readonly output: number;
    };
    readonly stopReason?: string;
    /** Capture-time classification: `'llm→tool'` when the LLM requested
     *  tools (`toolCallCount > 0`); `'llm→user'` for terminal delivery. */
    readonly actorArrow: 'llm→tool' | 'llm→user';
}
export interface DomainToolStartEvent extends DomainEventBase {
    readonly type: 'tool.start';
    readonly toolName: string;
    readonly toolCallId: string;
    readonly args?: unknown;
    /** 9.113.0 — copied from `stream.tool_start`: a call a paused batch never
     *  dispatched. The step graph draws no tool step for it and a boundary's
     *  rollup does not count it. Absent on every other tool event. */
    readonly notDispatched?: ToolStartPayload['notDispatched'];
}
export interface DomainToolEndEvent extends DomainEventBase {
    readonly type: 'tool.end';
    readonly toolCallId: string;
    readonly result?: unknown;
    readonly durationMs?: number;
    readonly error?: boolean;
    /** 9.113.0 — copied from `stream.tool_end`: the settled call's bracket
     *  closing. Its `result` is the library's sentence, not a tool's. */
    readonly notDispatched?: ToolEndPayload['notDispatched'];
}
export interface DomainContextInjectedEvent extends DomainEventBase {
    readonly type: 'context.injected';
    readonly slot: ContextSlot;
    readonly source: string;
    readonly sourceId?: string;
    readonly asRole?: 'system' | 'user' | 'assistant' | 'tool';
    readonly contentSummary?: string;
    readonly reason?: string;
    readonly sectionTag?: string;
    readonly upstreamRef?: string;
    readonly retrievalScore?: number;
    readonly rankPosition?: number;
    /** Tokens consumed by this injection (from `budgetSpent.tokens`). */
    readonly budgetTokens?: number;
    /** Fraction of slot cap consumed (from `budgetSpent.fractionOfCap`). */
    readonly budgetFraction?: number;
}
/** Discriminated union covering every observable moment in a run. */
export type DomainEvent = DomainRunEvent | DomainSubflowEvent | DomainCompositionEvent | DomainForkBranchEvent | DomainDecisionBranchEvent | DomainLoopIterationEvent | DomainLLMStartEvent | DomainLLMEndEvent | DomainToolStartEvent | DomainToolEndEvent | DomainContextInjectedEvent;
/**
 * The `DomainEvent` fields that carry captured run CONTENT — what the
 * agent read, produced, and handed across a boundary — as opposed to
 * run STRUCTURE (where a boundary was, when, of what kind). These are
 * the fields `snapshot: 'lean'` drops.
 *
 * They are also exactly the fields `buildStepGraphFromEvents` turns
 * into StepNode content (`entryPayload` / `exitPayload` /
 * `contentSummary` / `assistantText` / `toolArgs` / `toolResult`) —
 * which is why dropping them is opt-in rather than the default.
 *
 * When you add a field to a `Domain*Event`, decide:
 *   - Does it hold data the agent handled (unbounded, redaction-
 *     relevant)?                                        → add it HERE
 *   - Does it say where/when/what-kind the boundary was? → leave it out
 *
 * Forgetting to add one means a lean snapshot still ships that content
 * — silently, since nothing else references the field.
 *
 * Two free-text fields deliberately stay OUT of this set: `rationale`
 * on `decision.branch` and `reason` on `context.injected`. They are
 * short capture-time annotations saying WHY a boundary happened, and a
 * lean bundle loses most of its explanatory value without them — but a
 * decider that interpolates run values into its rationale does put that
 * text in a lean artifact. Lean means "no captured payloads", not "no
 * free text at all"; say so wherever lean is offered as a redaction
 * story.
 */
declare const CONTENT_FIELDS: readonly ["payload", "args", "result", "content", "contentSummary"];
type ContentField = (typeof CONTENT_FIELDS)[number];
/**
 * A `DomainEvent` with its content fields removed — the shape
 * `toSnapshot()` emits under `snapshot: 'lean'`.
 *
 * Distributive by construction (`T extends unknown ? … : never`) so the
 * union survives the `Omit` and `type` still narrows for consumers.
 */
type StripContent<T> = T extends unknown ? Omit<T, ContentField> : never;
export type LeanDomainEvent = StripContent<DomainEvent>;
/**
 * Per-boundary rollup returned by
 * `BoundaryRecorder.aggregateForBoundary` and
 * `BoundaryRecorder.aggregateAllBoundaries`. Same shape regardless of
 * primitive kind — UIs render the same chip set for every Agent /
 * LLMCall / Sequence / Parallel / Conditional / Loop.
 *
 * Events count toward this rollup when their `subflowPath` is a
 * prefix-match of the boundary's `subflowPath`. Nested boundaries
 * (e.g., LLMCall inside an Agent) contribute to BOTH rollups.
 *
 * In-flight boundaries (no `subflow.exit` yet) get partial values;
 * `endedAtMs` and `durationMs` are undefined until close.
 */
export interface BoundaryAggregate {
    readonly runtimeStageId: string;
    readonly subflowId: string;
    readonly subflowPath: readonly string[];
    /** `'Agent'` / `'LLMCall'` / `'Sequence'` / `'Parallel'` /
     *  `'Conditional'` / `'Loop'`. Always set on rollups returned by
     *  `aggregateAllBoundaries` (which filters to primitive boundaries).
     *  Optional on `aggregateForBoundary` results because the caller may
     *  request rollup for a non-primitive subflow (rare). */
    readonly primitiveKind?: string;
    /** Subflow display name (e.g., 'Triage', 'Billing'). */
    readonly label: string;
    /** Token usage summed across every `llm.end` inside this boundary. */
    readonly tokens: {
        readonly input: number;
        readonly output: number;
    };
    /** Count of `llm.start` events inside this boundary. */
    readonly llmCalls: number;
    /** Count of `tool.start` events inside this boundary — a settled call's
     *  (`notDispatched`, 9.113.0) excluded: it never ran. */
    readonly toolCalls: number;
    /** Count of `agent.iteration_start` events scoped to this boundary —
     *  ReAct-loop iterations. Always `0` for non-Agent primitives. */
    readonly iterations: number;
    /** Wall-clock ms of `subflow.entry`. */
    readonly startedAtMs: number;
    /** Wall-clock ms of `subflow.exit`. Undefined while in flight. */
    readonly endedAtMs?: number;
    /** `endedAtMs - startedAtMs`. Undefined while in flight. */
    readonly durationMs?: number;
}
/**
 * Anything that can hand out every typed event as it fires — the one
 * capability `BoundaryRecorder.subscribe()` needs.
 *
 * A `Runner` satisfies this (`runner.on('*', …)`) and so does the
 * internal `EventDispatcher`, which is the point: wiring the recorder's
 * typed half never requires reaching past the public runner for a
 * dispatcher it doesn't hand out.
 */
export interface TypedEventSource {
    on(type: '*', listener: (event: AgentfootprintEvent) => void): Unsubscribe;
}
export interface BoundaryRecorderOptions {
    readonly id?: string;
    /**
     * Live commit-count accessor — `() => runner.getCommitCount()` on an
     * agentfootprint runner, or `() => executor.getCommitCount()` on a raw
     * footprintjs executor (5.1+). When provided:
     *   - Every DomainEvent gains `commitIdxBefore` / `commitIdxAfter`.
     *   - `recorder.boundaryIndex` is populated with open/close ranges
     *     keyed on each subflow's entry event.
     *
     * **Omitting it produces a recording whose step strip cannot be
     * rebuilt.** Every event is stamped `commitIdxBefore: 0`, so every
     * boundary claims to have opened at the same instant; `boundaryIndex`
     * is deliberately left EMPTY rather than filled with zero-width
     * `[0, 0]` ranges that would read as real. Offline, a consumer
     * replaying this bundle sees no positions to place, so the strip stays
     * quiet — the honest outcome, and an unrecoverable one: the commit log
     * records what each stage WROTE, never when a boundary was crossed
     * relative to it, so nothing downstream can derive the axis after the
     * fact. There is no error and the events otherwise look complete,
     * which is exactly why this is called out here.
     *
     * Sample it live — a closure over the runner, not a captured number.
     * The count is meaningless before the run starts and must be read at
     * the moment each boundary fires. Phase 5 Layer 2.
     */
    readonly getCommitCount?: () => number;
    /**
     * How much of each event `toSnapshot()` carries into
     * `runtimeSnapshot.recorders`.
     *
     *   - `'full'` (default) — every field, content included. Required by
     *     `buildStepGraphFromEvents()`: an offline `<Replay>` restores its
     *     entry/exit payloads, assistant text, tool args and tool results
     *     from this bundle and nowhere else.
     *   - `'lean'` — boundary STRUCTURE only. Drops the five content
     *     fields (`payload`, `args`, `result`, `content`,
     *     `contentSummary`); keeps every field that says WHERE, WHEN and
     *     OF WHAT KIND each boundary was, so the commit-range index
     *     rebuilds range for range offline. Sized on one demo turn (4
     *     ReAct iterations, 4 LLM calls, 3 tool calls, run end to end on
     *     a mock provider): the bundle this method returned was 69.7 KB
     *     raw / 4.6 KB gzipped, its lean projection 21.7 KB / 1.9 KB —
     *     content was 69% of the raw bytes, and a consumer rebuilding
     *     only the index reads none of it. Same run measured twice; that
     *     turn's content was the entry/exit payloads, since only the
     *     boundary side of this recorder was attached — a run that also
     *     `subscribe()`s the typed-event side carries tool args, tool
     *     results and assistant text in there too. (The replay fixture
     *     the suite measures reports a much larger full bundle — it
     *     stands in for each subflow's missing entry seed with that
     *     subflow's final state, so it serializes the state twice per
     *     boundary. Don't quote it.)
     *
     * Same reasoning as `BoundaryRangeLabel`: a stored artifact shouldn't
     * become a second, redaction-bypassing copy of everything the agent
     * read and wrote. Consumers that DO want content join back on
     * `runtimeStageId` against the run's own snapshot.
     *
     * `'full'` stays the default because this bundle has carried content
     * since it shipped (v2.x, 2026-04-27) and offline consumers depend on
     * that silently — a lean default would quietly empty their replays.
     */
    readonly snapshot?: 'full' | 'lean';
    /**
     * Keep raw embedding vectors in captured payloads (8.20.0). Default
     * `false`: every `embedding` / `embeddings` field in a boundary payload is
     * replaced with its `{ dims, norm }` summary at capture time.
     *
     * The default exists because of a field measurement: the memory-read
     * subflow's boundary output carries each retrieved entry's full vector,
     * and one retrieval turn's recording weighed 2.76 MB — ~1.1 MB of it
     * embedding floats no consumer of a recording reads. The retrieval
     * evidence a debugger needs (score, passage, document, rejected
     * candidates) does not live in those floats and is untouched.
     *
     * Set `true` only when a consumer genuinely replays vectors (say, to
     * re-rank offline) — the recording grows by the full vector bytes again.
     */
    readonly recordEmbeddings?: boolean;
}
/**
 * Stripped projection used as the LABEL for the commit-range index.
 * Intentionally OMITS `payload` (security panel review YELLOW #1):
 * `boundaryIndex.enclosing()` queries should not bypass redaction by
 * exposing raw scope payloads through the range index. Consumers
 * needing payload can join on `runtimeStageId` with the full event
 * stream via `getEvents()` (which IS subject to redaction policy).
 */
export interface BoundaryRangeLabel {
    readonly type: 'subflow.entry' | 'run.entry' | 'composition.start';
    readonly runtimeStageId: string;
    readonly subflowPath: readonly string[];
    readonly depth: number;
    readonly ts: number;
    /** Set on subflow entries; undefined on the synthetic run-root entry. */
    readonly subflowId?: string;
    readonly localSubflowId?: string;
    readonly subflowName?: string;
    readonly description?: string;
    readonly primitiveKind?: string;
    readonly slotKind?: ContextSlot;
    readonly isAgentInternal?: boolean;
    /** Composition primitive (Parallel/Sequence/Loop/Conditional) when the
     *  range was opened by a `composition.start` event. */
    readonly compositionKind?: 'Parallel' | 'Sequence' | 'Loop' | 'Conditional';
    readonly compositionName?: string;
}
/** Factory — matches the `inOutRecorder()` / `topologyRecorder()` style. */
export declare function boundaryRecorder(options?: BoundaryRecorderOptions): BoundaryRecorder;
/**
 * Unified domain event recorder. Implements `CombinedRecorder` so it can
 * attach to the executor's FlowRecorder channel; exposes `subscribe()`
 * to wire to the agentfootprint typed-event dispatcher.
 *
 * v5: composes a `SequenceStore<DomainEvent>` (storage) instead of
 * extending the deprecated `SequenceStore<T>` base. Time-travel
 * utilities (`getEntryRanges`, `accumulate`) are accessed through the
 * store via the public read API on this class.
 */
export declare class BoundaryRecorder implements CombinedRecorder {
    readonly id: string;
    /** Composition: storage shelf. */
    private readonly store;
    /**
     * Phase 5 Layer 2 — interval index over commit indices, populated
     * live as boundary entry/exit pairs fire. Consumers (Lens) read
     * `enclosing(commitIdx)` for breadcrumbs and `overlapping(slice)`
     * for time-range queries. Empty when `getCommitCount` is not
     * injected. See `docs/design/boundary-commit-ranges.md`.
     */
    readonly boundaryIndex: CommitRangeIndex<BoundaryRangeLabel>;
    /** Open-range tokens keyed by `runtimeStageId` so the matching exit
     *  can close the correct range. Pure side-table; cleared on runId
     *  reset. Not exposed externally. */
    private readonly openTokens;
    /** Live commit-count accessor injected by the runner. Sanitized
     *  (NaN/Infinity/negative → 0) before use. */
    private readonly getCommitCount;
    /** True when `getCommitCount` was explicitly injected. In LEGACY
     *  MODE (false), `boundaryIndex` is intentionally NOT populated —
     *  zero-width [0,0] ranges would mislead consumers querying the
     *  index. Multi-panel review flagged this footgun. */
    private readonly hasCommitTracking;
    /** Snapshot verbosity — see `BoundaryRecorderOptions.snapshot`. Only
     *  `toSnapshot()` reads it; the live event stream from `getEvents()`
     *  is always full, so in-process consumers are never affected. */
    private readonly snapshotMode;
    /** See `BoundaryRecorderOptions.recordEmbeddings`. Applied at CAPTURE
     *  time, so the summary is what every projection (live stream, snapshot,
     *  lean) sees — one truth, not per-reader rewrites. */
    private readonly keepEmbeddings;
    /**
     * Tracks whether the most recent `llm.end` had toolCalls. Used to
     * classify the NEXT `llm.start` as `'tool→llm'` (vs `'user→llm'` if
     * there's no pending tool result). Reset on `clear()` and on every
     * `llm.start` event after the classification is applied.
     */
    private prevLLMEndHadTools;
    /**
     * Run-boundary observer — fires resetForNewRun() when
     * traversalContext.runId changes between events AND no boundary is
     * currently open. The "no open boundary" gate distinguishes:
     *
     *   - **Legitimate new run** — consumer reuses one recorder across
     *     sequential `executor.run()` calls. All prior boundaries closed
     *     before the second run began; openTokens is empty when the new
     *     runId arrives → safe to wipe state so the second run doesn't
     *     alias with the first.
     *   - **Composition sub-run** — primitives like `LLMCall`, `Sequence`,
     *     and `Parallel` internally spawn their own `FlowChartExecutor`
     *     instances. Each sub-executor mints a NEW runId. When that
     *     sub-executor fires events on the SHARED recorder, the recorder
     *     is still inside the parent run — `openTokens` is non-empty.
     *     Resetting here would wipe the parent's boundary index mid-run
     *     (the bug Layer 4 surfaced in agentfootprint-lens fanout).
     *
     * The `openTokens.size === 0` check is the cleanest semantic signal:
     * if nothing is in-flight, a runId change means "the consumer started
     * fresh"; if something is open, the new runId is from a sub-executor
     * nested inside the still-ongoing parent.
     */
    private readonly runIdGuard;
    constructor(options?: BoundaryRecorderOptions);
    /** Boundary payloads keep a vector's shape, not its bytes — see
     *  `recordEmbeddings`. Copy-on-write: payloads without embeddings pass
     *  through by reference. */
    private capturePayload;
    /**
     * Reset all transient state.
     *
     * **Composition-safe gate (Phase 5 Layer 4):** if `openTokens.size > 0`
     * the call is a no-op. Rationale: `FlowChartExecutor.run()` calls
     * `r.clear?.()` on every attached recorder during its pre-run loop.
     * When agentfootprint composition primitives (LLMCall, Sequence,
     * Parallel, etc.) propagate the parent's recorders to nested
     * sub-executors, EACH sub-executor's pre-run clear loop calls
     * `clear()` on the SHARED parent recorder mid-run — wiping live
     * parent state. The `openTokens.size > 0` check distinguishes:
     *
     *   - **Legitimate reset** — consumer or executor calls `clear()`
     *     when no boundary is in-flight (`openTokens` empty). Safe to
     *     wipe; the recorder is idle.
     *   - **Composition wipe** — sub-executor's pre-run clear fires
     *     while the parent has open boundaries (`openTokens` non-empty).
     *     Skip the wipe; the parent's state must be preserved.
     *
     * If a consumer needs to forcibly wipe state even with open tokens
     * (e.g., manual recovery after a crashed run), pair `clear()` with
     * an explicit `forceClear()` (TODO — add when the use case shows up;
     * today the recorder lifecycle pattern is "one recorder per logical
     * run" so leaked tokens shouldn't occur).
     */
    clear(): void;
    private observeRunId;
    onRunStart(event: FlowRunEvent): void;
    onRunEnd(event: FlowRunEvent): void;
    onRunFailed(event: FlowRunFailedEvent): void;
    onSubflowEntry(event: FlowSubflowEvent): void;
    onSubflowExit(event: FlowSubflowEvent): void;
    onFork(event: FlowForkEvent): void;
    onDecision(event: FlowDecisionEvent): void;
    onLoop(event: FlowLoopEvent): void;
    /**
     * Subscribe to the run's typed events and emit a domain event for each
     * `llm.*` / `tool.*` / `context.injected` one. This is the recorder's
     * SECOND half — `runner.attach(...)` supplies the boundaries, this
     * supplies what happened inside them. Wire both, before the run.
     *
     * Takes anything that offers a wildcard subscription, which a public
     * `Runner` does as readily as the internal `EventDispatcher` — so a
     * consumer never has to reach past `runner.on` for a dispatcher it
     * isn't given.
     *
     * Returns an unsubscribe function; safe to call multiple times (each
     * call adds a new subscription). Most consumers call this once at
     * recorder construction and dispose with the returned function.
     */
    subscribe(source: TypedEventSource): Unsubscribe;
    private ingestTypedEvent;
    /** All events in capture order (the canonical projection). */
    getEvents(): DomainEvent[];
    /** Type-narrowed lookup: all events of one kind. */
    getEventsByType<T extends DomainEvent['type']>(type: T): Extract<DomainEvent, {
        type: T;
    }>[];
    /** All boundary events (run + subflow, entry + exit interleaved). */
    getBoundaries(): (DomainRunEvent | DomainSubflowEvent)[];
    /** Just the entry-phase boundary events — the "step list" timeline. */
    getSteps(): (DomainRunEvent | DomainSubflowEvent)[];
    /** Subset of `getSteps()` excluding agent-internal routing subflows. */
    getVisibleSteps(): (DomainRunEvent | DomainSubflowEvent)[];
    /** Entry/exit pair for one chart execution by `runtimeStageId`. */
    getBoundary(runtimeStageId: string): {
        entry?: DomainRunEvent | DomainSubflowEvent;
        exit?: DomainRunEvent | DomainSubflowEvent;
    };
    /** Convenience for the outermost `__root__` pair. */
    getRootBoundary(): {
        entry?: DomainRunEvent;
        exit?: DomainRunEvent;
    };
    /** Subflow events grouped by the 3 input slots — for slot-row rendering. */
    getSlotBoundaries(): {
        systemPrompt: DomainSubflowEvent[];
        messages: DomainSubflowEvent[];
        tools: DomainSubflowEvent[];
    };
    /**
     * Roll up the event stream for ONE primitive boundary (Agent /
     * LLMCall / Sequence / Parallel / Conditional / Loop) into per-
     * boundary totals — tokens, llm calls, tool calls, iterations,
     * cache hits, duration.
     *
     * Pure projection over `getEvents()`. Events are attributed to a
     * boundary when their `subflowPath` is a **prefix-match** of the
     * boundary's path — so a nested `LLMCall` inside an `Agent` rolls
     * up into BOTH (LLMCall total + Agent total).
     *
     * Works mid-run (the boundary's `subflow.exit` may not have fired
     * yet — `endedAtMs` / `durationMs` are undefined in that case).
     * Works post-run.
     *
     * Multi-consumer story: this is the single source of rollup truth
     * for Lens, CLI live monitors, Sentry breadcrumbs, OTel exporters,
     * dashboards. Domain math (what counts as an "iteration"? does
     * cache hit count separately from llmCalls?) lives HERE — every
     * consumer hooks up; nobody re-implements.
     *
     * @param runtimeStageId The boundary's runtimeStageId (the same id
     *   carried by `StepNode.runtimeStageId` for primitive subflows).
     * @returns The rollup, or `undefined` if no `subflow.entry` event
     *   matches `runtimeStageId`.
     */
    aggregateForBoundary(runtimeStageId: string): BoundaryAggregate | undefined;
    /**
     * Roll up every primitive boundary in the run into one rollup each,
     * in the order their `subflow.entry` events fired. Top-level multi-
     * agent UIs call this once per render to populate per-agent chips.
     *
     * Filters to `primitiveKind`-tagged subflows ONLY (Agent / LLMCall /
     * Sequence / Parallel / Conditional / Loop). Slot subflows
     * (`sf-system-prompt` / `sf-messages` / `sf-tools`) are NOT
     * boundaries in this sense — they're context-engineering machinery,
     * not user-facing rollup units.
     */
    aggregateAllBoundaries(): readonly BoundaryAggregate[];
    /** Snapshot bundle — included in `executor.getSnapshot()` if the
     *  executor implements the snapshot extension protocol.
     *
     *  Under `snapshot: 'lean'` the bundle carries structure only, and
     *  `meta.mode` says so. That field is the whole point: an offline
     *  consumer holding a lean bundle can then SAY "this recording carries
     *  structure only" instead of drawing empty detail panels, because
     *  `buildStepGraphFromEvents()` restores step content from this bundle
     *  and nowhere else. `description` still carries the same fact in
     *  prose for a human reading the JSON — but prose is not something
     *  code branches on, and the readers that had to string-match it
     *  didn't.
     *
     *  `meta` reaches `getSnapshot().recorders[i].meta` on footprintjs
     *  9.12+, which copies it through; older engines rebuild the row
     *  without it and the field is dropped in transit. It is always on the
     *  value THIS method returns, so a producer holding the recorder
     *  (`recordRun`) can read it either way.
     *
     *  `data` is annotated rather than inferred: a lean event is a
     *  structural SUPERtype of a full one, so inference collapses the
     *  union to `LeanDomainEvent[]` and full-mode callers silently lose
     *  `payload` / `content` / … from the type. Narrow on the mode you
     *  configured. */
    toSnapshot(): {
        name: string;
        description: string;
        preferredOperation: 'translate';
        data: DomainEvent[] | LeanDomainEvent[];
        meta: {
            readonly mode: 'full' | 'lean';
        };
    };
}
export {};
