footprintjs 4.12.2 → 4.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +111 -0
- package/README.md +29 -1
- package/dist/esm/index.js +16 -3
- package/dist/esm/lib/engine/graph/StageNode.js +2 -3
- package/dist/esm/lib/engine/handlers/SubflowExecutor.js +29 -1
- package/dist/esm/lib/engine/handlers/types.js +1 -1
- package/dist/esm/lib/engine/index.js +1 -1
- package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +176 -41
- package/dist/esm/lib/engine/narrative/FlowRecorderDispatcher.js +8 -3
- package/dist/esm/lib/engine/narrative/index.js +1 -1
- package/dist/esm/lib/engine/narrative/narrativeTypes.js +1 -1
- package/dist/esm/lib/engine/narrative/types.js +1 -1
- package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +60 -36
- package/dist/esm/lib/engine/types.js +1 -1
- package/dist/esm/lib/reactive/createTypedScope.js +6 -3
- package/dist/esm/lib/reactive/types.js +2 -1
- package/dist/esm/lib/recorder/CombinedRecorder.js +211 -0
- package/dist/esm/lib/recorder/EmitRecorder.js +62 -0
- package/dist/esm/lib/recorder/index.js +2 -1
- package/dist/esm/lib/runner/FlowChartExecutor.js +123 -1
- package/dist/esm/lib/scope/ScopeFacade.js +117 -1
- package/dist/esm/lib/scope/detectCircular.js +74 -5
- package/dist/esm/lib/scope/types.js +1 -1
- package/dist/esm/recorders.js +1 -1
- package/dist/index.js +50 -32
- package/dist/lib/engine/graph/StageNode.js +2 -3
- package/dist/lib/engine/handlers/SubflowExecutor.js +29 -1
- package/dist/lib/engine/handlers/types.js +1 -1
- package/dist/lib/engine/index.js +1 -1
- package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +176 -41
- package/dist/lib/engine/narrative/FlowRecorderDispatcher.js +8 -3
- package/dist/lib/engine/narrative/index.js +1 -1
- package/dist/lib/engine/narrative/narrativeTypes.js +1 -1
- package/dist/lib/engine/narrative/types.js +1 -1
- package/dist/lib/engine/traversal/FlowchartTraverser.js +60 -36
- package/dist/lib/engine/types.js +1 -1
- package/dist/lib/reactive/createTypedScope.js +6 -3
- package/dist/lib/reactive/types.js +2 -1
- package/dist/lib/recorder/CombinedRecorder.js +218 -0
- package/dist/lib/recorder/EmitRecorder.js +63 -0
- package/dist/lib/recorder/index.js +7 -2
- package/dist/lib/runner/FlowChartExecutor.js +123 -1
- package/dist/lib/scope/ScopeFacade.js +117 -1
- package/dist/lib/scope/detectCircular.js +74 -5
- package/dist/lib/scope/types.js +1 -1
- package/dist/recorders.js +1 -1
- package/dist/types/index.d.ts +36 -2
- package/dist/types/lib/engine/graph/StageNode.d.ts +3 -7
- package/dist/types/lib/engine/handlers/SubflowExecutor.d.ts +2 -3
- package/dist/types/lib/engine/handlers/types.d.ts +19 -3
- package/dist/types/lib/engine/index.d.ts +1 -1
- package/dist/types/lib/engine/narrative/CombinedNarrativeRecorder.d.ts +38 -17
- package/dist/types/lib/engine/narrative/FlowRecorderDispatcher.d.ts +1 -1
- package/dist/types/lib/engine/narrative/index.d.ts +1 -1
- package/dist/types/lib/engine/narrative/narrativeTypes.d.ts +70 -6
- package/dist/types/lib/engine/narrative/types.d.ts +24 -2
- package/dist/types/lib/engine/traversal/FlowchartTraverser.d.ts +18 -0
- package/dist/types/lib/engine/types.d.ts +66 -0
- package/dist/types/lib/reactive/types.d.ts +61 -3
- package/dist/types/lib/recorder/CombinedRecorder.d.ts +173 -0
- package/dist/types/lib/recorder/EmitRecorder.d.ts +135 -0
- package/dist/types/lib/recorder/index.d.ts +3 -0
- package/dist/types/lib/runner/FlowChartExecutor.d.ts +85 -0
- package/dist/types/lib/scope/ScopeFacade.d.ts +40 -0
- package/dist/types/lib/scope/detectCircular.d.ts +30 -3
- package/dist/types/lib/scope/types.d.ts +21 -0
- package/dist/types/recorders.d.ts +3 -2
- package/package.json +1 -2
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CombinedRecorder — a single observer that can hook into MULTIPLE event
|
|
3
|
+
* streams (currently: scope data-flow + control-flow). One object, one `id`,
|
|
4
|
+
* one consistent view of the execution.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this exists
|
|
7
|
+
*
|
|
8
|
+
* Before `CombinedRecorder`, a consumer who wanted to observe both streams
|
|
9
|
+
* had to:
|
|
10
|
+
* 1. Implement both `Recorder` (8 methods) and `FlowRecorder` (12 methods)
|
|
11
|
+
* fully — stubbing every event they didn't care about.
|
|
12
|
+
* 2. Remember to call BOTH `attachRecorder(r)` AND `attachFlowRecorder(r)`.
|
|
13
|
+
* Forgetting the second call silently dropped half their events — no
|
|
14
|
+
* warning, no runtime error.
|
|
15
|
+
* 3. Re-implement coordination logic (buffering, ordering) themselves.
|
|
16
|
+
*
|
|
17
|
+
* `CombinedRecorder` collapses that into a single type and a single attach
|
|
18
|
+
* call (`executor.attachCombinedRecorder(r)`), with the library handling the
|
|
19
|
+
* routing internally. Consumers implement only the events they care about
|
|
20
|
+
* (`Partial<...>`) and the attach method duck-types at runtime to dispatch
|
|
21
|
+
* to the relevant channels.
|
|
22
|
+
*
|
|
23
|
+
* ## Forward compatibility
|
|
24
|
+
*
|
|
25
|
+
* When a third observer type is added (e.g. an `OperationRecorder`), the
|
|
26
|
+
* `CombinedRecorder` type gains an `& Partial<OperationRecorder>` clause and
|
|
27
|
+
* `attachCombinedRecorder` gains one more runtime branch in its dispatch.
|
|
28
|
+
* Consumers writing a `CombinedRecorder` today are NOT affected — their
|
|
29
|
+
* code keeps compiling and attaching correctly, because every new layer is
|
|
30
|
+
* optional.
|
|
31
|
+
*
|
|
32
|
+
* ## Example
|
|
33
|
+
*
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import type { CombinedRecorder } from 'footprintjs';
|
|
36
|
+
*
|
|
37
|
+
* const audit: CombinedRecorder = {
|
|
38
|
+
* id: 'audit',
|
|
39
|
+
* onWrite: (e) => logWrite(e.key, e.value), // Recorder method
|
|
40
|
+
* onDecision: (e) => logDecision(e.chosen), // FlowRecorder method
|
|
41
|
+
* };
|
|
42
|
+
*
|
|
43
|
+
* executor.attachCombinedRecorder(audit);
|
|
44
|
+
* // ^ internally: detects both sets of methods, routes to both channels.
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* ## Contract with existing APIs
|
|
48
|
+
*
|
|
49
|
+
* - `attachRecorder(r)` and `attachFlowRecorder(r)` remain unchanged.
|
|
50
|
+
* Consumers who want only ONE channel keep using them — explicit is good.
|
|
51
|
+
* - `attachCombinedRecorder(r)` is the ONLY way to guarantee an object is
|
|
52
|
+
* hooked into every stream it has methods for, without maintaining two
|
|
53
|
+
* attach calls at the call site.
|
|
54
|
+
* - Idempotency by `id` is preserved across channels: re-attaching with the
|
|
55
|
+
* same `id` replaces the previous instance on BOTH channels, not just one.
|
|
56
|
+
*/
|
|
57
|
+
import type { FlowErrorEvent, FlowPauseEvent, FlowRecorder, FlowResumeEvent } from '../engine/narrative/types.js';
|
|
58
|
+
import type { ErrorEvent, PauseEvent, Recorder, ResumeEvent } from '../scope/types.js';
|
|
59
|
+
import type { EmitRecorder } from './EmitRecorder.js';
|
|
60
|
+
/**
|
|
61
|
+
* Method names that appear on BOTH `Recorder` and `FlowRecorder` but with
|
|
62
|
+
* different event payload types. For these, a `CombinedRecorder` declares
|
|
63
|
+
* ONE handler that receives the union of both payloads — consumers
|
|
64
|
+
* discriminate on `traversalContext`, which only control-flow events carry.
|
|
65
|
+
*/
|
|
66
|
+
type SharedLifecycleOverlap = 'onError' | 'onPause' | 'onResume';
|
|
67
|
+
/** Lifecycle hooks (not event-specific) that both interfaces share identically. */
|
|
68
|
+
type SharedLifecycle = 'id' | 'clear' | 'toSnapshot';
|
|
69
|
+
/**
|
|
70
|
+
* A recorder that MAY observe any combination of supported event streams.
|
|
71
|
+
*
|
|
72
|
+
* Today's streams:
|
|
73
|
+
* - Scope data-flow (`Recorder`: onRead/onWrite/onCommit/onStageStart/…)
|
|
74
|
+
* - Control-flow (`FlowRecorder`: onDecision/onSubflowEntry/onLoop/…)
|
|
75
|
+
*
|
|
76
|
+
* All event handlers are optional — implement only what you care about.
|
|
77
|
+
* `id` is required so the library can deduplicate re-attaches.
|
|
78
|
+
*
|
|
79
|
+
* ## Shared method names (onError / onPause / onResume)
|
|
80
|
+
*
|
|
81
|
+
* Both `Recorder` and `FlowRecorder` declare these with DIFFERENT payload
|
|
82
|
+
* shapes. In a combined recorder, each such handler is called by BOTH
|
|
83
|
+
* channels with its own variant. The parameter type is a union — consumers
|
|
84
|
+
* can either handle both variants uniformly, or discriminate (control-flow
|
|
85
|
+
* variants carry a `traversalContext` field that data-flow variants lack).
|
|
86
|
+
*
|
|
87
|
+
* ## Forward compatibility
|
|
88
|
+
*
|
|
89
|
+
* When a third observer type ships (e.g. `OperationRecorder`), the type
|
|
90
|
+
* gains another `& Partial<…>` clause. Because every clause is `Partial`,
|
|
91
|
+
* existing `CombinedRecorder` implementations remain type-valid.
|
|
92
|
+
*/
|
|
93
|
+
export type CombinedRecorder = Partial<Omit<Recorder, SharedLifecycleOverlap | SharedLifecycle>> & Partial<Omit<FlowRecorder, SharedLifecycleOverlap | SharedLifecycle>> & Partial<Omit<EmitRecorder, SharedLifecycle>> & {
|
|
94
|
+
readonly id: string;
|
|
95
|
+
clear?(): void;
|
|
96
|
+
toSnapshot?(): {
|
|
97
|
+
name: string;
|
|
98
|
+
description?: string;
|
|
99
|
+
preferredOperation?: 'translate' | 'accumulate' | 'aggregate';
|
|
100
|
+
data: unknown;
|
|
101
|
+
};
|
|
102
|
+
onError?(event: ErrorEvent | FlowErrorEvent): void;
|
|
103
|
+
onPause?(event: PauseEvent | FlowPauseEvent): void;
|
|
104
|
+
onResume?(event: ResumeEvent | FlowResumeEvent): void;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Discriminator for the union payload types on `CombinedRecorder`'s shared
|
|
108
|
+
* methods (`onError`, `onPause`, `onResume`). Returns `true` if the event
|
|
109
|
+
* was emitted from the control-flow channel (FlowRecorder).
|
|
110
|
+
*
|
|
111
|
+
* ## How it discriminates
|
|
112
|
+
*
|
|
113
|
+
* Scope-channel events extend `RecorderContext`, which carries `pipelineId`.
|
|
114
|
+
* Flow-channel events do not carry `pipelineId` (they carry an optional
|
|
115
|
+
* `traversalContext` instead, but that field is optional and cannot be
|
|
116
|
+
* relied on as a positive signal). We detect the flow variant by the
|
|
117
|
+
* ABSENCE of `pipelineId` — this is schema-stable as long as scope events
|
|
118
|
+
* continue to extend `RecorderContext`.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* onError: (e) => {
|
|
123
|
+
* if (isFlowEvent(e)) {
|
|
124
|
+
* // Narrowed to FlowErrorEvent: has stageName, message, structuredError
|
|
125
|
+
* } else {
|
|
126
|
+
* // Narrowed to ErrorEvent: has error, operation, key?
|
|
127
|
+
* }
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export declare function isFlowEvent<T>(event: T): event is Exclude<T, {
|
|
132
|
+
pipelineId: string;
|
|
133
|
+
}>;
|
|
134
|
+
/**
|
|
135
|
+
* True iff the recorder implements at least one data-flow event method.
|
|
136
|
+
* Used by `executor.attachCombinedRecorder` to decide whether to hook into
|
|
137
|
+
* the scope data-flow channel.
|
|
138
|
+
*
|
|
139
|
+
* ## Detection rule
|
|
140
|
+
*
|
|
141
|
+
* Accepts both object-literal recorders (own-property handlers) AND class
|
|
142
|
+
* instances (handlers declared on the class prototype). Rejects handlers
|
|
143
|
+
* inherited from `Object.prototype` — that's always accidental or
|
|
144
|
+
* malicious pollution, never a legitimate recorder.
|
|
145
|
+
*
|
|
146
|
+
* ## Lifecycle exclusions
|
|
147
|
+
*
|
|
148
|
+
* `clear` and `toSnapshot` are NOT counted as event methods — a recorder
|
|
149
|
+
* that only implements those has nothing to observe and would be a no-op
|
|
150
|
+
* on either channel.
|
|
151
|
+
*
|
|
152
|
+
* ## Return type
|
|
153
|
+
*
|
|
154
|
+
* Returns plain `boolean` (not a type predicate). The full `Recorder`
|
|
155
|
+
* interface has payload types that diverge from `CombinedRecorder`'s union
|
|
156
|
+
* variants for shared methods, so a narrowing predicate would be unsound.
|
|
157
|
+
* Callers that need to treat the recorder as a `Recorder` do so explicitly
|
|
158
|
+
* at the attach site — the cast is the contract that each channel passes
|
|
159
|
+
* its own payload variant.
|
|
160
|
+
*/
|
|
161
|
+
export declare function hasRecorderMethods(r: CombinedRecorder): boolean;
|
|
162
|
+
/**
|
|
163
|
+
* True iff the recorder implements at least one control-flow event method.
|
|
164
|
+
* See `hasRecorderMethods`.
|
|
165
|
+
*/
|
|
166
|
+
export declare function hasFlowRecorderMethods(r: CombinedRecorder): boolean;
|
|
167
|
+
/**
|
|
168
|
+
* True iff the recorder implements at least one emit-channel event method.
|
|
169
|
+
* See `hasRecorderMethods` for the ownership-detection rules (own or
|
|
170
|
+
* class-prototype methods count; `Object.prototype` pollution does not).
|
|
171
|
+
*/
|
|
172
|
+
export declare function hasEmitRecorderMethods(r: CombinedRecorder): boolean;
|
|
173
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmitRecorder — the third observer channel, alongside `Recorder`
|
|
3
|
+
* (scope data-flow) and `FlowRecorder` (control-flow).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* The first two channels capture what the LIBRARY knows about — scope
|
|
8
|
+
* reads/writes fired by `setValue`, and control-flow events fired by the
|
|
9
|
+
* traverser. But **user-emitted structured events** — things a stage
|
|
10
|
+
* function wants to surface for observability (LLM tokens, billing metrics,
|
|
11
|
+
* auth decisions, domain milestones) — have nowhere to go today. They land
|
|
12
|
+
* in unobserved `DiagnosticCollector` side bags that no recorder watches.
|
|
13
|
+
*
|
|
14
|
+
* This channel closes that gap. Consumer calls `scope.$emit(name, payload)`
|
|
15
|
+
* from inside a stage; the library enriches the event with stage context
|
|
16
|
+
* and dispatches it synchronously to every attached `EmitRecorder`.
|
|
17
|
+
*
|
|
18
|
+
* ## Design properties
|
|
19
|
+
*
|
|
20
|
+
* - **Pass-through, not buffered.** Zero allocation when no recorder is
|
|
21
|
+
* attached. Events delivered synchronously, in-order, at call time.
|
|
22
|
+
* Same semantics as `onRead`/`onWrite` already use.
|
|
23
|
+
* - **Library-agnostic vocabulary.** Event names are consumer-chosen strings.
|
|
24
|
+
* Convention: hierarchical dotted names, e.g. `'agentfootprint.llm.tokens'`,
|
|
25
|
+
* `'myapp.billing.spend'`. Library enforces no registry.
|
|
26
|
+
* - **Auto-enriched context.** Every event carries `stageName`,
|
|
27
|
+
* `runtimeStageId`, `subflowPath`, `pipelineId`, `timestamp`. Consumers
|
|
28
|
+
* never need to thread execution context through their emit payloads.
|
|
29
|
+
* - **Redaction-aware.** `RedactionPolicy.emitPatterns` matches event names
|
|
30
|
+
* before dispatch — matched events have their payload replaced with
|
|
31
|
+
* `'[REDACTED]'` so secrets can't leak via emit.
|
|
32
|
+
*
|
|
33
|
+
* ## Relationship to existing channels
|
|
34
|
+
*
|
|
35
|
+
* | Channel | Fires when | Built-in consumers |
|
|
36
|
+
* |-----------------|------------------------------------|------------------------|
|
|
37
|
+
* | `Recorder` | scope read/write/commit | DebugRecorder, MetricRecorder |
|
|
38
|
+
* | `FlowRecorder` | traversal transitions | NarrativeFlowRecorder, etc. |
|
|
39
|
+
* | `EmitRecorder` | consumer calls `scope.$emit(...)` | (none ships today; Phase 3.X adds MemoryEmitRecorder) |
|
|
40
|
+
*
|
|
41
|
+
* `CombinedRecorder` (from `./CombinedRecorder.ts`) intersects all three
|
|
42
|
+
* via `Partial<...>`. A consumer can write ONE object implementing any
|
|
43
|
+
* combination; `executor.attachCombinedRecorder(r)` duck-types and routes
|
|
44
|
+
* to the right channel(s).
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const tokenMeter: EmitRecorder = {
|
|
49
|
+
* id: 'token-meter',
|
|
50
|
+
* onEmit(event) {
|
|
51
|
+
* if (event.name === 'agentfootprint.llm.tokens') {
|
|
52
|
+
* this.total += (event.payload as { input: number; output: number }).input;
|
|
53
|
+
* }
|
|
54
|
+
* },
|
|
55
|
+
* total: 0,
|
|
56
|
+
* } as any;
|
|
57
|
+
*
|
|
58
|
+
* executor.attachEmitRecorder(tokenMeter);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
import type { RecorderOperation } from './RecorderOperation.js';
|
|
62
|
+
/**
|
|
63
|
+
* Event delivered to `EmitRecorder.onEmit`.
|
|
64
|
+
*
|
|
65
|
+
* Name + payload are consumer-supplied via `scope.$emit(name, payload)`.
|
|
66
|
+
* Everything else is library-enriched at dispatch time from the current
|
|
67
|
+
* stage's execution context.
|
|
68
|
+
*/
|
|
69
|
+
export interface EmitEvent {
|
|
70
|
+
/**
|
|
71
|
+
* Consumer-supplied event name. Convention: hierarchical dotted namespace
|
|
72
|
+
* (e.g. `'agentfootprint.llm.tokens'`, `'myapp.billing.spend'`). Keeps
|
|
73
|
+
* vocabularies collision-free across libraries/apps without requiring a
|
|
74
|
+
* central registry.
|
|
75
|
+
*/
|
|
76
|
+
readonly name: string;
|
|
77
|
+
/**
|
|
78
|
+
* Consumer-supplied payload. Shape is up to the consumer and their
|
|
79
|
+
* convention; library treats it as opaque and passes through unchanged
|
|
80
|
+
* (modulo redaction — see `RedactionPolicy.emitPatterns`).
|
|
81
|
+
*
|
|
82
|
+
* When redacted, replaced with the string `'[REDACTED]'`.
|
|
83
|
+
*/
|
|
84
|
+
readonly payload: unknown;
|
|
85
|
+
/** Name of the stage that emitted this event. */
|
|
86
|
+
readonly stageName: string;
|
|
87
|
+
/**
|
|
88
|
+
* Unique per-execution-step identifier — the same value recorder events
|
|
89
|
+
* and commit-log entries carry. See `runtimeStageId.ts` for format.
|
|
90
|
+
*/
|
|
91
|
+
readonly runtimeStageId: string;
|
|
92
|
+
/**
|
|
93
|
+
* Subflow path from the outermost parent down to the subflow that emitted
|
|
94
|
+
* this event. Empty array when the emit came from the root flowchart.
|
|
95
|
+
* Matches the convention used by `FlowPauseEvent.subflowPath`,
|
|
96
|
+
* `FlowchartCheckpoint.subflowPath`, etc.
|
|
97
|
+
*/
|
|
98
|
+
readonly subflowPath: readonly string[];
|
|
99
|
+
/** Pipeline/run identifier (matches `RecorderContext.pipelineId`). */
|
|
100
|
+
readonly pipelineId: string;
|
|
101
|
+
/** Emission timestamp in milliseconds since epoch (`Date.now()`). */
|
|
102
|
+
readonly timestamp: number;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Pluggable observer for consumer-emitted structured events.
|
|
106
|
+
*
|
|
107
|
+
* All methods are optional; implement only what you care about. Recorders
|
|
108
|
+
* are invoked synchronously in attachment order. If a recorder throws, the
|
|
109
|
+
* error is caught and isolated — other recorders continue to receive the
|
|
110
|
+
* event and the emitting stage is unaffected.
|
|
111
|
+
*/
|
|
112
|
+
export interface EmitRecorder {
|
|
113
|
+
/**
|
|
114
|
+
* Stable identifier for idempotent attach/detach. Re-attaching with the
|
|
115
|
+
* same id replaces the previous registration on the executor.
|
|
116
|
+
*/
|
|
117
|
+
readonly id: string;
|
|
118
|
+
/** Called for every `scope.$emit(name, payload)` call in any stage. */
|
|
119
|
+
onEmit?(event: EmitEvent): void;
|
|
120
|
+
/**
|
|
121
|
+
* Optional: reset recorder-internal state between runs. Called by the
|
|
122
|
+
* executor before each `run()`.
|
|
123
|
+
*/
|
|
124
|
+
clear?(): void;
|
|
125
|
+
/**
|
|
126
|
+
* Optional: expose collected data for inclusion in
|
|
127
|
+
* `executor.getSnapshot().recorders`.
|
|
128
|
+
*/
|
|
129
|
+
toSnapshot?(): {
|
|
130
|
+
name: string;
|
|
131
|
+
description?: string;
|
|
132
|
+
preferredOperation?: RecorderOperation;
|
|
133
|
+
data: unknown;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
export type { CombinedRecorder } from './CombinedRecorder.js';
|
|
2
|
+
export { hasEmitRecorderMethods, hasFlowRecorderMethods, hasRecorderMethods, isFlowEvent } from './CombinedRecorder.js';
|
|
1
3
|
export type { CompositeSnapshot } from './CompositeRecorder.js';
|
|
2
4
|
export { CompositeRecorder } from './CompositeRecorder.js';
|
|
5
|
+
export type { EmitEvent, EmitRecorder } from './EmitRecorder.js';
|
|
3
6
|
export { KeyedRecorder } from './KeyedRecorder.js';
|
|
4
7
|
export { RecorderOperation } from './RecorderOperation.js';
|
|
5
8
|
export { SequenceRecorder } from './SequenceRecorder.js';
|
|
@@ -22,6 +22,8 @@ import type { ManifestEntry } from '../engine/narrative/recorders/ManifestFlowRe
|
|
|
22
22
|
import type { FlowRecorder } from '../engine/narrative/types.js';
|
|
23
23
|
import { type ExecutorResult, type ExtractorError, type FlowChart, type RunOptions, type ScopeFactory, type SerializedPipelineStructure, type StageNode, type StreamHandlers, type SubflowResult } from '../engine/types.js';
|
|
24
24
|
import type { FlowchartCheckpoint } from '../pause/types.js';
|
|
25
|
+
import type { CombinedRecorder } from '../recorder/CombinedRecorder.js';
|
|
26
|
+
import type { EmitRecorder } from '../recorder/EmitRecorder.js';
|
|
25
27
|
import type { ScopeProtectionMode } from '../scope/protection/types.js';
|
|
26
28
|
import type { Recorder, RedactionPolicy, RedactionReport } from '../scope/types.js';
|
|
27
29
|
import { type RuntimeSnapshot } from './ExecutionRuntime.js';
|
|
@@ -216,6 +218,89 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
216
218
|
detachFlowRecorder(id: string): void;
|
|
217
219
|
/** Returns a defensive copy of attached FlowRecorders. */
|
|
218
220
|
getFlowRecorders(): FlowRecorder[];
|
|
221
|
+
/**
|
|
222
|
+
* Attach a recorder that may observe multiple event streams (scope
|
|
223
|
+
* data-flow, control-flow, or both). Detects at runtime which streams the
|
|
224
|
+
* recorder has methods for and routes it to the correct internal channels.
|
|
225
|
+
*
|
|
226
|
+
* Preferred over calling `attachRecorder` and `attachFlowRecorder`
|
|
227
|
+
* separately, because forgetting one of the two is a silent foot-gun —
|
|
228
|
+
* half your events never fire and there is no runtime warning. With
|
|
229
|
+
* `attachCombinedRecorder` the library guarantees the recorder's declared
|
|
230
|
+
* methods all fire, and adds no overhead versus two explicit calls.
|
|
231
|
+
*
|
|
232
|
+
* ## Idempotency
|
|
233
|
+
*
|
|
234
|
+
* Idempotent by `id` across ALL channels — re-attaching with the same `id`
|
|
235
|
+
* replaces the previous instance everywhere it was registered. Mixing
|
|
236
|
+
* `attachCombinedRecorder(x)` with a prior `attachRecorder(y)` or
|
|
237
|
+
* `attachFlowRecorder(y)` that share `x.id === y.id` is also safe: the
|
|
238
|
+
* combined attach replaces the single-channel registration on whichever
|
|
239
|
+
* channel(s) `x` has methods for. No duplicate firings occur.
|
|
240
|
+
*
|
|
241
|
+
* ## Narrative activation
|
|
242
|
+
*
|
|
243
|
+
* If the recorder has any control-flow methods, `enableNarrative()` is
|
|
244
|
+
* called as a side effect (the narrative subsystem is required to emit
|
|
245
|
+
* control-flow events). Data-flow-only recorders do NOT activate the
|
|
246
|
+
* narrative.
|
|
247
|
+
*
|
|
248
|
+
* ## Detection rule
|
|
249
|
+
*
|
|
250
|
+
* Only **own** event methods count (see `hasRecorderMethods`). Methods
|
|
251
|
+
* inherited via the prototype chain are ignored — this protects against
|
|
252
|
+
* accidental `Object.prototype` pollution attaching handlers you never
|
|
253
|
+
* declared. A recorder that provides only `clear`/`toSnapshot` is a
|
|
254
|
+
* no-op and emits a dev-mode warning to surface the likely mistake.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```typescript
|
|
258
|
+
* const audit: CombinedRecorder = {
|
|
259
|
+
* id: 'audit',
|
|
260
|
+
* onWrite: (e) => log('scope write', e.key),
|
|
261
|
+
* onDecision: (e) => log('routed to', e.chosen),
|
|
262
|
+
* };
|
|
263
|
+
* executor.attachCombinedRecorder(audit);
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
attachCombinedRecorder(recorder: CombinedRecorder): void;
|
|
267
|
+
/**
|
|
268
|
+
* Detach a combined recorder from all channels it was attached to.
|
|
269
|
+
* Safe to call if the recorder was only on one channel or never attached.
|
|
270
|
+
*/
|
|
271
|
+
detachCombinedRecorder(id: string): void;
|
|
272
|
+
/**
|
|
273
|
+
* Attach an `EmitRecorder` — an observer for consumer-emitted structured
|
|
274
|
+
* events fired via `scope.$emit(name, payload)`.
|
|
275
|
+
*
|
|
276
|
+
* Internally, emit recorders share the scope-recorder channel because
|
|
277
|
+
* emit events fire from inside `ScopeFacade` during stage execution,
|
|
278
|
+
* same timing as `onRead`/`onWrite`. This method is a convenience that
|
|
279
|
+
* delegates to `attachRecorder` — consumers can also use
|
|
280
|
+
* `attachRecorder` directly for a recorder that implements BOTH
|
|
281
|
+
* `onWrite` and `onEmit`. Either approach places the recorder on the
|
|
282
|
+
* same underlying list, so `onEmit` fires exactly once per event.
|
|
283
|
+
*
|
|
284
|
+
* **Idempotent by `id`:** replaces existing recorder with same `id`.
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```typescript
|
|
288
|
+
* executor.attachEmitRecorder({
|
|
289
|
+
* id: 'token-meter',
|
|
290
|
+
* onEmit: (e) => {
|
|
291
|
+
* if (e.name === 'agentfootprint.llm.tokens') trackTokens(e.payload);
|
|
292
|
+
* },
|
|
293
|
+
* });
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
attachEmitRecorder(recorder: EmitRecorder): void;
|
|
297
|
+
/** Detach an `EmitRecorder` by id. Safe to call if never attached. */
|
|
298
|
+
detachEmitRecorder(id: string): void;
|
|
299
|
+
/**
|
|
300
|
+
* Returns a defensive copy of attached recorders filtered to those that
|
|
301
|
+
* implement `onEmit`. Useful for inspection during testing.
|
|
302
|
+
*/
|
|
303
|
+
getEmitRecorders(): EmitRecorder[];
|
|
219
304
|
/**
|
|
220
305
|
* Returns the execution narrative.
|
|
221
306
|
*
|
|
@@ -18,6 +18,12 @@ import { StageContext } from '../memory/StageContext.js';
|
|
|
18
18
|
import type { CommitEvent, Recorder, RedactionPolicy, RedactionReport } from './types.js';
|
|
19
19
|
export declare class ScopeFacade {
|
|
20
20
|
static readonly BRAND: unique symbol;
|
|
21
|
+
/**
|
|
22
|
+
* Shared sentinel returned by `_getSubflowPath()` for root-level stages
|
|
23
|
+
* (no subflow nesting). Avoids per-call allocation of a fresh
|
|
24
|
+
* `Object.freeze([])` on every `emitEvent` in the common no-subflow case.
|
|
25
|
+
*/
|
|
26
|
+
private static readonly _EMPTY_SUBFLOW_PATH;
|
|
21
27
|
protected _stageContext: StageContext;
|
|
22
28
|
protected _stageName: string;
|
|
23
29
|
protected readonly _readOnlyValues?: unknown;
|
|
@@ -75,6 +81,40 @@ export declare class ScopeFacade {
|
|
|
75
81
|
addErrorInfo(key: string, value: unknown): void;
|
|
76
82
|
addMetric(metricName: string, value: unknown): void;
|
|
77
83
|
addEval(metricName: string, value: unknown): void;
|
|
84
|
+
/**
|
|
85
|
+
* Fire a structured event to every attached recorder implementing
|
|
86
|
+
* `onEmit`. Synchronous, in-order, pass-through — no buffering.
|
|
87
|
+
*
|
|
88
|
+
* - **Fast-path**: zero allocation + zero cost when no recorders are
|
|
89
|
+
* attached (early return on empty list).
|
|
90
|
+
* - **Enrichment**: library auto-adds `stageName`, `runtimeStageId`,
|
|
91
|
+
* `subflowPath`, `pipelineId`, `timestamp` to the event.
|
|
92
|
+
* - **Redaction**: `RedactionPolicy.emitPatterns` regexes are matched
|
|
93
|
+
* against `name` — matched events have their payload replaced with
|
|
94
|
+
* `'[REDACTED]'` before dispatch.
|
|
95
|
+
* - **Error isolation**: a throwing `onEmit` does not propagate — it is
|
|
96
|
+
* caught and routed to `onError` on remaining recorders, matching the
|
|
97
|
+
* pattern used by other scope events.
|
|
98
|
+
*
|
|
99
|
+
* Consumers call this via the `scope.$emit(name, payload)` scope method;
|
|
100
|
+
* the method routes here via `createTypedScope`.
|
|
101
|
+
*/
|
|
102
|
+
emitEvent(name: string, payload: unknown): void;
|
|
103
|
+
/**
|
|
104
|
+
* Build the subflowPath (outer → inner) for event enrichment.
|
|
105
|
+
*
|
|
106
|
+
* Parses from `runtimeStageId` which has the format
|
|
107
|
+
* `[subflowPath/]stageId#executionIndex` (see `lib/engine/runtimeStageId.ts`).
|
|
108
|
+
* Subflow isolation prevents walking the parent-chain across boundaries,
|
|
109
|
+
* so the runtimeStageId — globally unique, includes full path — is the
|
|
110
|
+
* canonical source of truth for the subflow hierarchy at emit time.
|
|
111
|
+
*
|
|
112
|
+
* Examples:
|
|
113
|
+
* 'seed#0' → [] (root)
|
|
114
|
+
* 'sf-inner/inner#5' → ['sf-inner']
|
|
115
|
+
* 'sf-a/sf-b/stage#3' → ['sf-a', 'sf-b'] (nested)
|
|
116
|
+
*/
|
|
117
|
+
private _getSubflowPath;
|
|
78
118
|
/** Returns all state keys without firing onRead. Used by TypedScope ownKeys/has traps. */
|
|
79
119
|
getStateKeys(): string[];
|
|
80
120
|
/** Check key existence without firing onRead. Used by TypedScope has trap.
|
|
@@ -12,9 +12,36 @@
|
|
|
12
12
|
* Only checks plain objects and arrays — class instances, Date, Map, etc. are skipped.
|
|
13
13
|
*/
|
|
14
14
|
export declare function hasCircularReference(value: unknown, ancestors?: WeakSet<object>): boolean;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Enable dev-mode diagnostics across the whole library.
|
|
17
|
+
*
|
|
18
|
+
* When on, the library performs developer-only checks (circular references,
|
|
19
|
+
* empty recorder detection, suspicious predicate shapes, etc.) and emits
|
|
20
|
+
* `console.warn` messages to help you catch mistakes early.
|
|
21
|
+
*
|
|
22
|
+
* Call once at application startup. See the module header for the full
|
|
23
|
+
* list of what dev-mode gates.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { enableDevMode } from 'footprintjs';
|
|
28
|
+
* if (process.env.NODE_ENV !== 'production') enableDevMode();
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
16
31
|
export declare function enableDevMode(): void;
|
|
17
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Disable dev-mode diagnostics across the whole library (default state).
|
|
34
|
+
*
|
|
35
|
+
* All dev-only checks become no-ops. Safe to call in production paths —
|
|
36
|
+
* typically the default never needs to be re-asserted, but this is the
|
|
37
|
+
* documented way to turn the flag off if your code enabled it earlier.
|
|
38
|
+
*/
|
|
18
39
|
export declare function disableDevMode(): void;
|
|
19
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Returns whether dev-mode diagnostics are currently enabled.
|
|
42
|
+
*
|
|
43
|
+
* Library internals call this before running any dev-only check. Consumers
|
|
44
|
+
* rarely need to call it directly — prefer `enableDevMode()` at startup and
|
|
45
|
+
* let the library gate its own diagnostics internally.
|
|
46
|
+
*/
|
|
20
47
|
export declare function isDevMode(): boolean;
|
|
@@ -68,6 +68,18 @@ export interface RedactionPolicy {
|
|
|
68
68
|
/** Field-level redaction within objects — key → array of fields to scrub.
|
|
69
69
|
* Supports dot-notation for nested paths (e.g. 'address.zip'). */
|
|
70
70
|
fields?: Record<string, string[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Regex patterns matched against `EmitEvent.name` for `scope.$emit(...)`
|
|
73
|
+
* calls. Any emit event whose name matches has its payload replaced with
|
|
74
|
+
* the string `'[REDACTED]'` before dispatch to recorders.
|
|
75
|
+
*
|
|
76
|
+
* Example:
|
|
77
|
+
* ```ts
|
|
78
|
+
* { emitPatterns: [/\.auth\./, /\.billing\./] }
|
|
79
|
+
* // Hides payloads of events like 'myapp.auth.check' and 'myapp.billing.spend'
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
emitPatterns?: RegExp[];
|
|
71
83
|
}
|
|
72
84
|
/**
|
|
73
85
|
* Compliance-friendly report of what was redacted. Never includes values.
|
|
@@ -98,6 +110,15 @@ export interface Recorder {
|
|
|
98
110
|
onStageEnd?(event: StageEvent): void;
|
|
99
111
|
onPause?(event: PauseEvent): void;
|
|
100
112
|
onResume?(event: ResumeEvent): void;
|
|
113
|
+
/**
|
|
114
|
+
* Fires for every `scope.$emit(name, payload)` call during a stage.
|
|
115
|
+
* Optional — implement only if you want to observe consumer-emitted
|
|
116
|
+
* structured events. See `EmitRecorder` for the focused interface
|
|
117
|
+
* (structurally compatible; this field is the same shape).
|
|
118
|
+
*
|
|
119
|
+
* @see EmitRecorder in `src/lib/recorder/EmitRecorder.ts`
|
|
120
|
+
*/
|
|
121
|
+
onEmit?(event: import('../recorder/EmitRecorder.js').EmitEvent): void;
|
|
101
122
|
/** Reset state before each executor.run() — prevents cross-run accumulation. */
|
|
102
123
|
clear?(): void;
|
|
103
124
|
/** Expose collected data for inclusion in executor.getSnapshot().recorders. */
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import type { CombinedNarrativeRecorderOptions } from './lib/engine/narrative/CombinedNarrativeRecorder.js';
|
|
27
27
|
import { CombinedNarrativeRecorder } from './lib/engine/narrative/CombinedNarrativeRecorder.js';
|
|
28
|
-
import type { BreakRenderContext, CombinedNarrativeEntry, DecisionRenderContext, ErrorRenderContext, ForkRenderContext, LoopRenderContext, NarrativeRenderer, OpRenderContext, SelectedRenderContext, StageRenderContext, SubflowRenderContext } from './lib/engine/narrative/narrativeTypes.js';
|
|
28
|
+
import type { BreakRenderContext, CombinedNarrativeEntry, DecisionRenderContext, EmitRenderContext, ErrorRenderContext, ForkRenderContext, LoopRenderContext, NarrativeFormatter, NarrativeRenderer, OpRenderContext, SelectedRenderContext, StageRenderContext, SubflowRenderContext } from './lib/engine/narrative/narrativeTypes.js';
|
|
29
29
|
import { AdaptiveNarrativeFlowRecorder } from './lib/engine/narrative/recorders/AdaptiveNarrativeFlowRecorder.js';
|
|
30
30
|
import type { ManifestEntry } from './lib/engine/narrative/recorders/ManifestFlowRecorder.js';
|
|
31
31
|
import { ManifestFlowRecorder } from './lib/engine/narrative/recorders/ManifestFlowRecorder.js';
|
|
32
32
|
import { MilestoneNarrativeFlowRecorder } from './lib/engine/narrative/recorders/MilestoneNarrativeFlowRecorder.js';
|
|
33
33
|
import { WindowedNarrativeFlowRecorder } from './lib/engine/narrative/recorders/WindowedNarrativeFlowRecorder.js';
|
|
34
|
+
import type { EmitEvent, EmitRecorder } from './lib/recorder/EmitRecorder.js';
|
|
34
35
|
import type { DebugEntry, DebugRecorderOptions } from './lib/scope/recorders/DebugRecorder.js';
|
|
35
36
|
import { DebugRecorder } from './lib/scope/recorders/DebugRecorder.js';
|
|
36
37
|
import type { AggregatedMetrics, MetricRecorderOptions, StageMetrics } from './lib/scope/recorders/MetricRecorder.js';
|
|
@@ -59,7 +60,7 @@ export declare function manifest(): ManifestInstance;
|
|
|
59
60
|
export declare function adaptive(): AdaptiveNarrativeFlowRecorder;
|
|
60
61
|
export declare function milestone(): MilestoneNarrativeFlowRecorder;
|
|
61
62
|
export declare function windowed(maxEntries?: number): WindowedNarrativeFlowRecorder;
|
|
62
|
-
export type { BreakRenderContext, CombinedNarrativeRecorderOptions, DecisionRenderContext, ErrorRenderContext, ForkRenderContext, LoopRenderContext, NarrativeRenderer, OpRenderContext, SelectedRenderContext, StageRenderContext, SubflowRenderContext, };
|
|
63
|
+
export type { BreakRenderContext, CombinedNarrativeRecorderOptions, DecisionRenderContext, EmitEvent, EmitRecorder, EmitRenderContext, ErrorRenderContext, ForkRenderContext, LoopRenderContext, NarrativeFormatter, NarrativeRenderer, OpRenderContext, SelectedRenderContext, StageRenderContext, SubflowRenderContext, };
|
|
63
64
|
export type { AggregatedMetrics, MetricRecorderOptions, StageMetrics };
|
|
64
65
|
export type { CompositeSnapshot } from './lib/recorder/index.js';
|
|
65
66
|
export { CompositeRecorder } from './lib/recorder/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "footprintjs",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.0",
|
|
4
4
|
"description": "Explainable backend flows — automatic causal traces, decision evidence, and MCP tool generation for AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sanjay Krishna Anbalagan",
|
|
@@ -97,7 +97,6 @@
|
|
|
97
97
|
]
|
|
98
98
|
},
|
|
99
99
|
"sideEffects": false,
|
|
100
|
-
"dependencies": {},
|
|
101
100
|
"peerDependencies": {
|
|
102
101
|
"zod": "^3.0.0 || ^4.0.0"
|
|
103
102
|
},
|