/** * FlowChartExecutor — Public API for executing a compiled FlowChart. * * Wraps FlowchartTraverser. Build a chart with flowChart() and pass the result here: * * const chart = flowChart('entry', entryFn).addFunction('process', processFn).build(); * * // No-options form (uses auto-detected TypedScope factory from the chart): * const executor = new FlowChartExecutor(chart); * * // Options-object form (preferred when you need to customize behavior): * const executor = new FlowChartExecutor(chart, { scopeFactory: myFactory }); * * // 2-param form (accepts a ScopeFactory directly, for backward compatibility): * const executor = new FlowChartExecutor(chart, myFactory); * * const result = await executor.run({ input: data, env: { traceId: 'req-123' } }); */ import type { FlowChart } from '../builder/types.js'; import type { CombinedNarrativeRecorderOptions } from '../engine/narrative/CombinedNarrativeRecorder.js'; import type { CombinedNarrativeEntry } from '../engine/narrative/narrativeTypes.js'; import type { ManifestEntry } from '../engine/narrative/recorders/ManifestFlowRecorder.js'; import type { FlowRecorder } from '../engine/narrative/types.js'; import { type ExecutorResult, type RunOptions, type ScopeFactory, type SerializedPipelineStructure, type StageNode, type StreamHandlers, type SubflowResult } from '../engine/types.js'; import type { CommitValuesMode, ReadTrackingMode, WriteTrackingMode } from '../memory/types.js'; import type { FlowchartCheckpoint } from '../pause/types.js'; import type { CombinedRecorder } from '../recorder/CombinedRecorder.js'; import type { EmitRecorder } from '../recorder/EmitRecorder.js'; import type { ScopeProtectionMode } from '../scope/protection/types.js'; import type { RedactionPolicy, RedactionReport, ScopeRecorder } from '../scope/types.js'; import { type AttachRecorderOptions, type ObserverDrainResult } from './DeferredObserverTier.js'; import { type RuntimeSnapshot } from './ExecutionRuntime.js'; /** * Options object for `FlowChartExecutor` — preferred over positional params. * * ```typescript * const ex = new FlowChartExecutor(chart, { * scopeFactory: myFactory, * defaultValuesForContext: { ... }, * }); * ``` * * **Sync note for maintainers:** Every field added here must also appear in the * `flowChartArgs` private field type and in the constructor's options-resolution * block (the `else if` branch that reads from `opts`). Missing any one of the * three causes silent omission — the option is accepted but never applied. * * **TScope inference note:** When using the options-object form with a custom scope, * TypeScript cannot infer `TScope` through the options object. Pass the type * explicitly: `new FlowChartExecutor(chart, { scopeFactory })`. */ export interface FlowChartExecutorOptions { /** Custom scope factory. Defaults to TypedScope or ScopeFacade auto-detection. */ scopeFactory?: ScopeFactory; /** * Default values pre-populated into the shared context before **each** stage * (re-applied every stage, acting as baseline defaults). */ defaultValuesForContext?: unknown; /** * Initial context values merged into the shared context **once** at startup * (applied before the first stage, not repeated on subsequent stages). * Distinct from `defaultValuesForContext`, which is re-applied every stage. */ initialContext?: unknown; /** Read-only input accessible via `scope.getArgs()` — never tracked or written. */ readOnlyContext?: unknown; /** * Policy for `StageSnapshot.stageReads` (#14). Default `'full'` — every * tracked read `structuredClone`s the value into the stage's read view * (the historical behavior; what lens/agentfootprint snapshots show). * `'summary'` records a cheap type/size/preview marker per read; `'off'` * records nothing — zero per-read clone cost (reads of large values become * ~free). Narrative and `ScopeRecorder.onRead` are identical in every mode. * Caveat: under `'off'` a stage's snapshot is indistinguishable from one * that read nothing — auditing consumers that need "did it read?" without * the value cost should prefer `'summary'`. * Equivalent to calling `executor.setReadTracking(mode)` before `run()`. */ readTracking?: ReadTrackingMode; /** * Policy for `StageSnapshot.stageWrites` (#13c-A) — the sibling of * {@link readTracking}; the two dials are independent. Default `'full'` — * every tracked write `structuredClone`s the value into the stage's write * view (the historical behavior). `'summary'` records a cheap * `WriteSummaryMarker` (type/size/preview) per write; `'off'` records * nothing — `stageWrites` is absent from the snapshot. * * Observable consequences — what the policy DOES govern: * - `StageSnapshot.stageWrites` (markers under `'summary'`, absent under * `'off'`). * - The commit observer payload: `ScopeRecorder.onCommit(mutations)` * receives the retained `_stageWrites` entries, so it carries the same * markers under `'summary'` and an empty mutations bag under `'off'` — * deferred/observer consumers see exactly what retention stored. * * What it does NOT govern: * - The writes themselves: shared state, the transaction buffer, and the * COMMIT LOG are identical in every mode (commitLog values keep their * full payloads — the lossless linear-cost fix for those is the * {@link commitValues} dial, #13c-B). * - Per-op `ScopeRecorder.onWrite` events — they fire with live values * regardless (delivery tier, RFC-001's concern), so narrative output is * identical in every mode. * - Redaction: a policy/per-call-redacted write stores `'[REDACTED]'` * under `'full'` AND `'summary'` (redaction takes precedence over the * dial; a marker would leak size/preview), and nothing under `'off'`. * * Caveat: under `'off'` a stage's SNAPSHOT is indistinguishable from one * that wrote nothing — but unlike `readTracking: 'off'`, the commit log * still records every net change, so "did it write?" stays answerable. * Equivalent to calling `executor.setWriteTracking(mode)` before `run()`. */ writeTracking?: WriteTrackingMode; /** * Encoding policy for COMMIT LOG values (#13c-B) — the third dial of the * family, and unlike its siblings it is **lossless in both modes** (it * changes the log's encoding, never its information). * * - `'full'` (default) — every surviving `set` path stores the full final * value; byte-identical to the historical behavior. * - `'delta'` — array net-changes that are "base plus a tail" commit as an * `append` trace verb storing ONLY the tail (the growing-history commit * log becomes linear instead of O(N²) retained); `deleteValue()` commits * as a real `delete` verb (replay removes the key instead of leaving * `key: undefined`); bundles carry exactly ONE trace entry per surviving * path. Replay (`applySmartMerge` — live state, `materialise()`, the * redacted mirror) reconstructs every step's full state exactly. * * Consumers that read `bundle.overwrite[key]` as "the full value written" * must switch to `commitValueAt(commitLog, idx, key)` from * `footprintjs/trace` — under `'delta'` that value is verb-qualified (an * `append` bundle holds only the tail). Path-tier consumers * (`findLastWriter`, `causalChain`, narrative, lens highlights) are * unaffected. The active mode is surfaced as * `getSnapshot().commitValues`. * * Honest cost note: append detection is new wall work — an O(|base array|) * structural prefix compare per array-set path per commit. On a hit the * commit gets cheaper in both wall and heap; on a miss (prefix diverges) * it pays compare + full clone. `'full'` pays zero. * Equivalent to calling `executor.setCommitValues(mode)` before `run()`. */ commitValues?: CommitValuesMode; /** * Custom error classifier for throttling detection. Return `true` if the * error represents a rate-limit or backpressure condition (the executor will * treat it differently from hard failures). Defaults to no throttling classification. */ throttlingErrorChecker?: (error: unknown) => boolean; /** Handlers for streaming stage lifecycle events (see `addStreamingFunction`). */ streamHandlers?: StreamHandlers; /** Scope protection mode for TypedScope direct-assignment detection. */ scopeProtectionMode?: ScopeProtectionMode; } export declare class FlowChartExecutor { private traverser; /** Shared execution counter — survives pause/resume. Reset on fresh run(). */ private _executionCounter; /** Shared per-run visit counts (by stageId) driving TraversalContext.loopIteration. * Twin of _executionCounter: survives pause/resume, reset on fresh run(). */ private _visitCounts; /** Per-`run()` identifier — generated fresh per run + per resume. Threaded * through every TraversalContext so recorders can scope state to a single * run. See `runId.ts`. */ private _currentRunId; private narrativeEnabled; private narrativeOptions?; private combinedRecorder; private flowRecorders; private scopeRecorders; /** * RFC-001 deferred-observer wiring — created LAZILY on the first * `delivery: 'deferred'` attach. `undefined` for every executor that never * opts in: zero allocation, zero per-event cost, byte-identical behavior * (the emit fast-path precedent). */ private deferredTier?; private redactionPolicy; private sharedRedactedKeys; private sharedRedactedFieldsByKey; private lastCheckpoint; /** * `true` once `run()` (or a previous `resume()`) has executed on * this instance. `resume()` branches on it: * * • true → reuse the constructor-time runtime (same-executor * continuity: execution tree, recorders, narrative * accumulate across pause/resume cycles) * • false → seed a fresh runtime from `checkpoint.sharedState` * (cross-executor / cross-process resume: new instance * reconstructed from a serialized checkpoint) * * Without this flag, fresh executors silently discarded the * checkpoint's sharedState and resume handlers couldn't read pre-pause * scope. See `test/lib/pause/cross-executor-resume.test.ts`. */ private _hasRunBefore; /** * Re-entrancy guard. `run()` and `resume()` mutate per-run instance state * (traverser, runId, execution counter, checkpoint) and clear attached * recorders — a second concurrent entry on the SAME executor would * interleave runIds and cross-contaminate recorder/narrative state, and * `getCheckpoint()` would return whichever run paused last. One executor = * one in-flight execution; create an executor per concurrent run. * See docs/guides/execution-model.md. */ private _isExecuting; private readonly flowChartArgs; /** * Create a FlowChartExecutor. * * **Options object form** (preferred): * ```typescript * new FlowChartExecutor(chart, { scopeFactory, defaultValuesForContext }) * ``` * * **2-param form** (also supported): * ```typescript * new FlowChartExecutor(chart, scopeFactory) * ``` * * @param flowChart - The compiled FlowChart returned by `flowChart(...).build()` * @param factoryOrOptions - A `ScopeFactory` OR a `FlowChartExecutorOptions` options object. */ constructor(flowChart: FlowChart, factoryOrOptions?: ScopeFactory | FlowChartExecutorOptions); private createTraverser; enableNarrative(options?: CombinedNarrativeRecorderOptions): void; /** * Set a declarative redaction policy that applies to all stages. * Must be called before run(). */ setRedactionPolicy(policy: RedactionPolicy): void; /** * Set the read-tracking policy for `StageSnapshot.stageReads` (#14). * Must be called before run(). Equivalent to the `readTracking` * constructor option — see {@link FlowChartExecutorOptions.readTracking} * for the mode semantics ('full' default / 'summary' / 'off'). */ setReadTracking(mode: ReadTrackingMode): void; /** * Set the write-tracking policy for `StageSnapshot.stageWrites` (#13c-A). * Must be called before run(). Equivalent to the `writeTracking` * constructor option — see {@link FlowChartExecutorOptions.writeTracking} * for the mode semantics ('full' default / 'summary' / 'off'), the * onCommit-payload consequence, and the redaction-precedence rule. */ setWriteTracking(mode: WriteTrackingMode): void; /** * Set the commit-values encoding policy for the commit log (#13c-B). * Must be called before run(). Equivalent to the `commitValues` * constructor option — see {@link FlowChartExecutorOptions.commitValues} * for the mode semantics ('full' default / 'delta'), the verb-qualified * `overwrite` consequence, and the `commitValueAt` migration helper. */ setCommitValues(mode: CommitValuesMode): void; /** * Returns a compliance-friendly report of all redaction activity from the * most recent run. Never includes actual values. */ getRedactionReport(): RedactionReport; /** * Returns the checkpoint from the most recent paused execution, or `undefined` * if the last run completed without pausing. * * The checkpoint is JSON-serializable — store it in Redis, Postgres, localStorage, etc. * * It is fully DETACHED from engine state: every field was deep-copied at * pause time (see `buildPauseCheckpoint`). Holding, mutating, or persisting * it cannot affect the executor, and a later same-executor resume cannot * mutate a checkpoint you already stored. * * @example * ```typescript * const result = await executor.run({ input }); * if (executor.isPaused()) { * const checkpoint = executor.getCheckpoint()!; * await redis.set(`session:${id}`, JSON.stringify(checkpoint)); * } * ``` */ getCheckpoint(): FlowchartCheckpoint | undefined; /** Returns `true` if the most recent run() was paused (checkpoint available). */ isPaused(): boolean; /** * Number of commits in the run's commit log. O(1) — direct length * read, no snapshot materialization. Use this to stamp commit * indices on observer events (e.g., `BoundaryRecorder` storing * `commitIdxBefore` / `commitIdxAfter` per domain event for * `CommitRangeIndex` queries — see `footprintjs/trace`). * * Returns 0 before any run; after, returns the cumulative commit * count across the executor's lifetime (including resumes). * * IMPLEMENTATION NOTE: this returns `runtime.executionHistory.length`, * which is the same value as `getSnapshot().commitLog.length`. The * naming asymmetry is historical — the underlying `EventLog` field * is named `executionHistory` but stores the `CommitBundle[]` that * `commitLog` exposes. They are the SAME array (verified by the * "matches commitLog.length" integration test). */ getCommitCount(): number; /** * Resume a paused flowchart from a checkpoint. * * Restores the scope state, calls the paused stage's `resumeFn` with the * provided input, then continues traversal from the next stage. * * The checkpoint can come from `getCheckpoint()` on a previous run, or from * a serialized checkpoint stored in Redis/Postgres/localStorage. * * **Narrative/recorder state is reset on resume.** To keep a unified narrative * across pause/resume cycles, collect it before calling resume. * * @example * ```typescript * // Process A — after a pause, persist the checkpoint: * const checkpoint = executor.getCheckpoint()!; * await redis.set(`session:${id}`, JSON.stringify(checkpoint)); * * // Process B (possibly different server, same chart) — restore and resume: * const restored = JSON.parse(await redis.get(`session:${id}`)); * const executor = new FlowChartExecutor(chart); * const result = await executor.resume(restored, { approved: true }); * ``` */ resume(checkpoint: FlowchartCheckpoint, resumeInput?: unknown, options?: Pick): Promise; /** * Build a fully DETACHED checkpoint from a caught PauseSignal. * * Every field is deep-copied via one `structuredClone` of the assembled * checkpoint, because the raw pieces alias live engine state: * * - `sharedState` IS `SharedMemory`'s internal context object — the alias * only detaches at the next commit (`applySmartMerge` rebuilds it), and * after a pause there is no next commit until resume. * - `executionTree` nodes are fresh, but their `logs`/`errors`/`metrics`/ * `evals`/`stageReads`/`flowMessages` fields reference live * `DiagnosticCollector` bags that keep accumulating on same-executor * resume. * - `subflowStates` values are shallow copies whose NESTED objects alias * subflow memory, and they get seeded back into live runtimes on resume. * - `subflowResults` values stay referenced by the traverser's results map. * * The checkpoint is persisted by contract ("store in Redis/Postgres") — it * must never share structure with the engine. Pause is not a hot path; the * clone cost is irrelevant. * * The JSON-safe checkpoint contract (no functions, no class instances) * governs CONSUMER-owned data — but the executionTree's diagnostic bags * accept ANY value at write time without cloning ($debug/$error/$metric/ * $eval store raw references), so a contract-compliant run can still carry * a non-cloneable diagnostic. Observability side-bags never abort traversal * anywhere else in the library, so they must not abort the pause either: * on clone failure we sanitize the diagnostic bags (non-cloneable values * become '[non-serializable: …]' markers — the live engine bags are never * touched) and retry. If the retry STILL fails, the violation is in * consumer-owned data (realistically `pauseData` — a function can never * reach shared state in the first place: TransactionBuffer clones every * written value at write time, so the offending write already rejected) * and we throw a DESCRIPTIVE contract error naming the offending * checkpoint field(s). A naked DataCloneError never escapes. * * Subflow scope capture (`subflowStates`) survives ONLY on the signal — the * nested runtimes are GC'd as the stack unwinds. Promoting it onto the * checkpoint here lets cross-executor resume restore pre-pause subflow * scope (e.g. an Agent's `scope.history`). Empty `{}` for root-level pauses. */ private buildPauseCheckpoint; /** * Find a StageNode in the compiled graph by ID. * Handles subflow paths by drilling into registered subflows. */ private findNodeInGraph; /** * Find the mount node (the node that mounts a subflow boundary) * for a given subflowId, by DFS from `start`. Used by `resume()` to * locate the OUTER node we have to enter through so the subflow's * outputMapper and parent continuation execute. * * Cycle-safe via visited set. Returns the first match (DFS order). */ private findMountInGraph; /** DFS search for a node by ID in the StageNode graph. Cycle-safe via visited set. */ private dfsFind; /** * Attach a scope ScopeRecorder to observe data operations (reads, writes, commits). * Automatically attached to every ScopeFacade created during traversal. * Must be called before run(). * * **Idempotent by ID:** If a recorder with the same `id` is already attached, * it is replaced (not duplicated). This prevents double-counting when both * a framework and the user attach the same recorder type. * * Built-in recorders use auto-increment IDs (`metrics-1`, `debug-1`, ...) by * default, so multiple instances with different configs coexist. To override * a framework-attached recorder, pass the same well-known ID. * * @example * ```typescript * // Multiple recorders with different configs — each gets a unique ID * executor.attachScopeRecorder(new MetricRecorder()); * executor.attachScopeRecorder(new DebugRecorder({ verbosity: 'minimal' })); * * // Override a framework-attached recorder by passing its well-known ID * executor.attachScopeRecorder(new MetricRecorder('metrics')); * * // Attaching twice with same ID replaces (no double-counting) * executor.attachScopeRecorder(new MetricRecorder('my-metrics')); * executor.attachScopeRecorder(new MetricRecorder('my-metrics')); // replaces previous * ``` * * **Delivery tier (RFC-001):** pass `{ delivery: 'deferred' }` to take the * recorder out of the engine's hot path — events are captured into a * bounded queue and delivered at the next microtask checkpoint ("one beat * behind"). Omitting `delivery` keeps the historical synchronous call, * byte-identical to previous releases. Re-attaching the same `id` with a * different tier SWAPS tiers cleanly — never double delivery. See * `docs/guides/observers-deferred.md`. */ attachScopeRecorder(recorder: ScopeRecorder, options?: AttachRecorderOptions): void; /** * Lazily create the executor's ONE deferred-observer tier (one merged * queue, total event order across all three channels). The FIRST deferred * attach's options configure the dispatcher; later differing options are * dev-warned and ignored (see `AttachRecorderOptions`). */ private ensureDeferredTier; /** * Detach a child flowchart on the given driver and return a `DetachHandle` * the caller can `wait()` on (Promise) or read `.status` from (sync). * * The driver is a REQUIRED first argument — there is no library-default, * to keep the engine free of driver imports and to make the choice of * scheduling algorithm explicit at the call site. * * @example * ```typescript * import { microtaskBatchDriver } from 'footprintjs/detach'; * * const exec = new FlowChartExecutor(parentChart); * const handle = exec.detachAndJoinLater(microtaskBatchDriver, telemetryChart, { event: 'x' }); * await handle.wait(); // optional * ``` */ detachAndJoinLater(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): import('../detach/types.js').DetachHandle; /** * Detach a child flowchart on the given driver and DISCARD the handle. * Use for telemetry exports / fire-and-forget side effects where the * caller doesn't care about the result. * * Errors raised by the child still land on the (discarded) handle — they * go silent unless surfaced through a recorder. For observable detach, * prefer `detachAndJoinLater` and surface failures via `.wait().catch()`. */ detachAndForget(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): void; /** Detach all scope Recorders with the given ID — both delivery tiers. */ detachScopeRecorder(id: string): void; /** Returns a defensive copy of attached scope Recorders (both tiers). */ getScopeRecorders(): ScopeRecorder[]; /** * Attach a FlowRecorder to observe control flow events. * Automatically enables narrative if not already enabled. * Must be called before run() — recorders are passed to the traverser at creation time. * * **Idempotent by ID:** replaces existing recorder with same `id`. * * **Delivery tier (RFC-001):** pass `{ delivery: 'deferred' }` for * next-checkpoint delivery off the hot path — see `attachScopeRecorder`. */ attachFlowRecorder(recorder: FlowRecorder, options?: AttachRecorderOptions): void; /** Detach all FlowRecorders with the given ID — both delivery tiers. */ detachFlowRecorder(id: string): void; /** Returns a defensive copy of attached FlowRecorders (both tiers). */ getFlowRecorders(): FlowRecorder[]; /** * Attach a recorder that may observe multiple event streams (scope * data-flow, control-flow, or both). Detects at runtime which streams the * recorder has methods for and routes it to the correct internal channels. * * Preferred over calling `attachScopeRecorder` and `attachFlowRecorder` * separately, because forgetting one of the two is a silent foot-gun — * half your events never fire and there is no runtime warning. With * `attachCombinedRecorder` the library guarantees the recorder's declared * methods all fire, and adds no overhead versus two explicit calls. * * ## Idempotency * * Idempotent by `id` across ALL channels — re-attaching with the same `id` * replaces the previous instance everywhere it was registered. Mixing * `attachCombinedRecorder(x)` with a prior `attachScopeRecorder(y)` or * `attachFlowRecorder(y)` that share `x.id === y.id` is also safe: the * combined attach replaces the single-channel registration on whichever * channel(s) `x` has methods for. No duplicate firings occur. * * ## Narrative activation * * If the recorder has any control-flow methods, `enableNarrative()` is * called as a side effect (the narrative subsystem is required to emit * control-flow events). Data-flow-only recorders do NOT activate the * narrative. * * ## Detection rule * * Only **own** event methods count (see `hasRecorderMethods`). Methods * inherited via the prototype chain are ignored — this protects against * accidental `Object.prototype` pollution attaching handlers you never * declared. A recorder that provides only `clear`/`toSnapshot` is a * no-op and emits a dev-mode warning to surface the likely mistake. * * @example * ```typescript * const audit: CombinedRecorder = { * id: 'audit', * onWrite: (e) => log('scope write', e.key), * onDecision: (e) => log('routed to', e.chosen), * }; * executor.attachCombinedRecorder(audit); * ``` */ attachCombinedRecorder(recorder: CombinedRecorder, options?: AttachRecorderOptions): void; /** * Detach a combined recorder from all channels it was attached to. * Safe to call if the recorder was only on one channel or never attached. */ detachCombinedRecorder(id: string): void; /** * Attach an `EmitRecorder` — an observer for consumer-emitted structured * events fired via `scope.$emit(name, payload)`. * * Internally, emit recorders share the scope-recorder channel because * emit events fire from inside `ScopeFacade` during stage execution, * same timing as `onRead`/`onWrite`. This method is a convenience that * delegates to `attachScopeRecorder` — consumers can also use * `attachScopeRecorder` directly for a recorder that implements BOTH * `onWrite` and `onEmit`. Either approach places the recorder on the * same underlying list, so `onEmit` fires exactly once per event. * * **Idempotent by `id`:** replaces existing recorder with same `id`. * * @example * ```typescript * executor.attachEmitRecorder({ * id: 'token-meter', * onEmit: (e) => { * if (e.name === 'agentfootprint.llm.tokens') trackTokens(e.payload); * }, * }); * ``` */ attachEmitRecorder(recorder: EmitRecorder, options?: AttachRecorderOptions): void; /** Detach an `EmitRecorder` by id. Safe to call if never attached. */ detachEmitRecorder(id: string): void; /** * Returns a defensive copy of attached recorders (both delivery tiers) * filtered to those that implement `onEmit`. Useful for inspection during * testing. */ getEmitRecorders(): EmitRecorder[]; /** * Returns structured narrative entries — the single public narrative API. * Each entry has a type (stage, step, condition, fork, etc.), text, and * depth. Consumers render however they want; call `.map(e => e.text)` * if a flat `string[]` is needed locally. */ getNarrativeEntries(): CombinedNarrativeEntry[]; /** * Returns the combined FlowRecorders list. When narrative is enabled, * includes the CombinedNarrativeRecorder (which builds merged flow+data * entries inline). Plus any user-attached recorders. */ private buildFlowRecordersList; run(options?: RunOptions): Promise; /** * Flush the deferred-observer backlog, then await async listener * completions under a deadline (RFC-001 Block 8 — the serverless / * graceful-shutdown pattern: call before the process freezes or exits so * "one beat behind" work is not lost). Resolves immediately with zeros * when no deferred observer was ever attached. `pending === 0` means a * full drain; a non-zero `pending` reports continuations (plus any queued * events) still outstanding at the deadline — honest, never silent. */ drainObservers(opts?: { timeoutMs?: number; }): Promise; /** * Returns the runtime snapshot. * * @param options.redact When `true`, `sharedState` comes from the parallel * redacted mirror (if maintained — see `setRedactionPolicy`). This is * the safe view for exporting traces externally (paste into a viewer, * share with support). When no redaction policy is configured the * redacted mirror is not maintained, so this flag is a no-op — * `sharedState` is the raw working memory either way. Default `false`. * * The commit log is already redacted at write-time regardless of this * flag, and the execution tree carries only structural metadata. * * **Treat `sharedState` as READ-ONLY.** In production it is a live view of * the engine's working memory (zero copy cost) — mutating it corrupts * engine state. In dev mode (`enableDevMode()`) it is a deep-frozen CLONE, * so any consumer mutation throws loudly instead of corrupting silently. */ getSnapshot(options?: { redact?: boolean; }): RuntimeSnapshot; /** @internal */ getRuntime(): import("../engine/types.js").IExecutionRuntime; /** @internal */ setRootObject(path: string[], key: string, value: unknown): void; /** @internal */ getBranchIds(): string[]; /** @internal */ getRuntimeRoot(): StageNode; /** @internal */ getRuntimeStructure(): SerializedPipelineStructure | undefined; /** @internal */ getSubflowResults(): Map; /** * Returns the subflow manifest from an attached ManifestFlowRecorder. * Returns empty array if no ManifestFlowRecorder is attached. */ getSubflowManifest(): ManifestEntry[]; /** * Returns the full spec for a dynamically-registered subflow. * Requires an attached ManifestFlowRecorder that observed the registration. */ getSubflowSpec(subflowId: string): unknown | undefined; }