import type { LedgerEntry } from '../runtime/telemetry/exporters/local-ledger.js'; import type { RuntimeStateSnapshot } from '../runtime/diagnostics/types.js'; /** * Category of a mismatch between expected and replayed state. * * - `missing_event` : an event expected at this revision was absent. * - `extra_event` : an event appeared that was not in the recording. * - `payload_mismatch` : event name matched but payload differed. * - `ordering` : events arrived in different order than recorded. * - `state_divergence` : domain state diverged after applying event. */ export type MismatchClass = 'missing_event' | 'extra_event' | 'payload_mismatch' | 'ordering' | 'state_divergence'; export type ReplayMismatchOwnerDomain = 'turn' | 'tasks' | 'tools' | 'providers' | 'session' | 'conversation' | 'agents' | 'workflows' | 'permissions' | 'transport' | 'unknown'; export type ReplayMismatchFailureMode = 'missing_event' | 'extra_event' | 'ordering_violation' | 'payload_schema_mismatch' | 'payload_type_mismatch' | 'payload_value_mismatch' | 'missing_terminal_summary' | 'terminal_outcome_diverged' | 'stop_reason_diverged'; /** * A single actionable mismatch entry produced by diff mode. */ export interface ReplayMismatch { /** The revision at which the mismatch was detected. */ readonly rev: number; /** Mismatch classifier. */ readonly kind: MismatchClass; /** Human-readable description, sufficient to act on without raw dumps. */ readonly description: string; /** The event name involved, if applicable. */ readonly eventName?: string | undefined; /** Key fields from the recorded payload, if applicable. */ readonly recordedSummary?: string | undefined; /** Key fields from the replayed payload, if applicable. */ readonly replayedSummary?: string | undefined; /** Likely owning runtime domain for the divergence. */ readonly ownerDomain?: ReplayMismatchOwnerDomain | undefined; /** Narrower replay failure mode for operator triage. */ readonly failureMode?: ReplayMismatchFailureMode | undefined; /** Related turn ID when the divergence can be tied to a single turn. */ readonly relatedTurnId?: string | undefined; } export type ReplayTurnOutcome = 'completed' | 'failed' | 'cancelled'; export interface ReplayTurnSummary { readonly turnId: string; readonly outcome: ReplayTurnOutcome; readonly terminalEvent: 'PREFLIGHT_FAIL' | 'TURN_COMPLETED' | 'TURN_ERROR' | 'TURN_CANCEL'; readonly startedRev?: number | undefined; readonly terminalRev: number; readonly stopReason?: string | undefined; readonly message?: string | undefined; } /** * The replay-local state tree at a given revision. * * Built by folding ledger entries over the initial snapshot; each step * produces a new immutable frame. */ export interface ReplayFrame { /** The revision this frame represents (0 = initial snapshot). */ readonly rev: number; /** The event that produced this frame (absent for the initial snapshot). */ readonly entry?: LedgerEntry | undefined; /** Domain state at this revision, merged from snapshot + events applied so far. */ readonly domains: Record>; } export type ReplayStatus = 'idle' | 'loaded' | 'running' | 'exhausted'; /** * Snapshot of engine state for the Replay panel. */ export interface ReplayEngineSnapshot { readonly status: ReplayStatus; readonly runId: string | null; readonly currentRev: number; readonly totalRevisions: number; readonly currentFrame: ReplayFrame | null; readonly mismatches: readonly ReplayMismatch[]; readonly turnSummaries: readonly ReplayTurnSummary[]; } /** * DeterministicReplayEngine. * * Usage: * ```ts * const engine = new DeterministicReplayEngine('/path/to/project'); * engine.load(runId, snapshot, ledgerEntries); * engine.step(); // advance one event * engine.step(5); // advance five events * engine.seek(10); // jump to rev 10 * const report = engine.diff(); // compare current to recorded * engine.export('replay.json'); // write report inside the project root * ``` */ export declare class DeterministicReplayEngine { private readonly _projectRoot; private _status; private _runId; private _snapshot; /** * Returns the initial snapshot that was loaded, or null if no run is loaded. */ getInitialSnapshot(): RuntimeStateSnapshot | null; private _entries; private _frames; private _currentFrameIndex; private _mismatches; private _turnSummaries; private readonly _subscribers; constructor(projectRoot: string); private _isInsideRoot; /** * Load a run for replay. * * Replaces any currently loaded run. The engine is positioned at rev 0 * (the initial snapshot) after loading. * * @param runId - The run identifier. * @param snapshot - The initial state snapshot captured at run start. * @param entries - All ledger entries for this run, in any order (sorted internally). */ load(runId: string, snapshot: RuntimeStateSnapshot, entries: LedgerEntry[]): void; /** * Advance the replay cursor by `n` steps (default: 1). * * Returns the frames that were stepped over. * If fewer than `n` events remain, steps to the end. * * @param n - Number of steps to advance. * @returns The frames produced by the steps. */ step(n?: number): ReplayFrame[]; /** * Seek to a specific revision. * * Valid revisions are 0 (initial snapshot) through `totalRevisions`. * Clamped to valid range. * * @param targetRev - Target revision number. */ seek(targetRev: number): void; /** * Run diff mode: compare each replayed frame against the recorded sequence. * * Produces a list of `ReplayMismatch` entries that identify divergences * with actionable classifiers and descriptions, not raw payload dumps. * * Diff analysis covers: * - Missing events (recorded entry has no corresponding replayed frame) * - Extra events (frame exists past the recorded sequence) * - Payload mismatches (event name matches, but key payload fields differ) * - Ordering violations (same events, different rev sequence) * * @returns Ordered list of mismatches (by rev). */ diff(): ReplayMismatch[]; /** * Export the current replay report (frames + mismatches) to a JSON file. * * The exported object contains: * - `runId` * - `exportedAt` (epoch ms) * - `totalRevisions` * - `currentRev` * - `mismatches` * - `frames` (condensed: rev, eventName, domainNames only, no full state) * * @param filePath - Absolute path to write the JSON report. * @returns A promise that resolves when the file is written. */ export(filePath: string): Promise; /** * Get a snapshot of engine state for the Replay panel. */ getSnapshot(): ReplayEngineSnapshot; /** * Register a callback invoked when engine state changes. * @returns An unsubscribe function. */ subscribe(callback: () => void): () => void; /** Reset to idle, clears all loaded state. */ reset(): void; /** * Convert a RuntimeStateSnapshot into a flat domain map. */ private _snapshotToDomains; /** * Apply a single ledger entry to the previous frame, producing a new frame. * * Event payloads are merged into the domain state by convention: * the payload is treated as a partial update to the event's implied domain * (derived from the event name prefix, e.g. "turn:" → "turn" domain). * Unknown domain prefixes are collected into a synthetic "_events" domain. */ private _applyEntry; /** * Compare two payloads at a key-level and return a mismatch if they diverge. * * Reports only the first differing key to keep the output actionable. */ private _diffPayloads; private _deriveTurnSummaries; private _diffTurnSummaries; private _extractTurnId; private _inferOwnerDomain; private _notify; } //# sourceMappingURL=deterministic-replay.d.ts.map