footprintjs 9.19.1 → 9.21.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 +3 -1
- package/dist/esm/lib/builder/FlowChartBuilder.d.ts +67 -6
- package/dist/esm/lib/builder/FlowChartBuilder.js +143 -9
- package/dist/esm/lib/builder/types.d.ts +21 -0
- package/dist/esm/lib/builder/types.js +1 -1
- package/dist/esm/lib/engine/graph/StageNode.d.ts +14 -0
- package/dist/esm/lib/engine/graph/StageNode.js +1 -1
- package/dist/esm/lib/engine/handlers/RuntimeStructureManager.js +6 -1
- package/dist/esm/lib/engine/handlers/SubflowExecutor.d.ts +12 -0
- package/dist/esm/lib/engine/handlers/SubflowExecutor.js +51 -11
- package/dist/esm/lib/engine/handlers/servedSubflowResults.d.ts +29 -0
- package/dist/esm/lib/engine/handlers/servedSubflowResults.js +49 -0
- package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +7 -4
- package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +6 -1
- package/dist/esm/lib/engine/types.d.ts +27 -0
- package/dist/esm/lib/engine/types.js +1 -1
- package/dist/esm/lib/memory/StageContext.d.ts +16 -0
- package/dist/esm/lib/memory/StageContext.js +30 -3
- package/dist/esm/lib/memory/types.d.ts +13 -0
- package/dist/esm/lib/memory/types.js +1 -1
- package/dist/esm/lib/runner/ExecutionRuntime.d.ts +6 -0
- package/dist/esm/lib/runner/ExecutionRuntime.js +1 -1
- package/dist/esm/lib/runner/FlowChartExecutor.d.ts +3 -0
- package/dist/esm/lib/runner/FlowChartExecutor.js +16 -2
- package/dist/esm/lib/time-travel/index.d.ts +1 -0
- package/dist/esm/lib/time-travel/index.js +2 -1
- package/dist/esm/lib/time-travel/tagStops.d.ts +72 -0
- package/dist/esm/lib/time-travel/tagStops.js +101 -0
- package/dist/esm/trace.d.ts +1 -1
- package/dist/esm/trace.js +2 -2
- package/dist/lib/builder/FlowChartBuilder.js +142 -8
- package/dist/lib/builder/types.js +1 -1
- package/dist/lib/engine/graph/StageNode.js +1 -1
- package/dist/lib/engine/handlers/RuntimeStructureManager.js +6 -1
- package/dist/lib/engine/handlers/SubflowExecutor.js +51 -11
- package/dist/lib/engine/handlers/servedSubflowResults.js +54 -0
- package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +7 -4
- package/dist/lib/engine/traversal/FlowchartTraverser.js +6 -1
- package/dist/lib/engine/types.js +1 -1
- package/dist/lib/memory/StageContext.js +30 -3
- package/dist/lib/memory/types.js +1 -1
- package/dist/lib/runner/ExecutionRuntime.js +1 -1
- package/dist/lib/runner/FlowChartExecutor.js +16 -2
- package/dist/lib/time-travel/index.js +4 -2
- package/dist/lib/time-travel/tagStops.js +105 -0
- package/dist/trace.js +4 -3
- package/dist/types/lib/builder/FlowChartBuilder.d.ts +67 -6
- package/dist/types/lib/builder/types.d.ts +21 -0
- package/dist/types/lib/engine/graph/StageNode.d.ts +14 -0
- package/dist/types/lib/engine/handlers/SubflowExecutor.d.ts +12 -0
- package/dist/types/lib/engine/handlers/servedSubflowResults.d.ts +29 -0
- package/dist/types/lib/engine/types.d.ts +27 -0
- package/dist/types/lib/memory/StageContext.d.ts +16 -0
- package/dist/types/lib/memory/types.d.ts +13 -0
- package/dist/types/lib/runner/ExecutionRuntime.d.ts +6 -0
- package/dist/types/lib/runner/FlowChartExecutor.d.ts +3 -0
- package/dist/types/lib/time-travel/index.d.ts +1 -0
- package/dist/types/lib/time-travel/tagStops.d.ts +72 -0
- package/dist/types/trace.d.ts +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -34,6 +34,8 @@ Entry points: `footprintjs` (public API) · `/recorders` (factories) · `/trace`
|
|
|
34
34
|
|
|
35
35
|
**Retry (9.15.0):** `StageNode.retry` is a POLICY on a stage, not a kind — no new node type, no `computeNodeType` edit. The loop lives in `FlowchartTraverser.executeStage` (:720), so linear/streaming/pausable/decider/selector/fork-child/subflow-inner all get it from one implementation. A failed non-final attempt calls `StageContext.discardStaged()` (buffer + stateView + _stageWrites/_stageReads + _provenanceReads + _untrackedSources); the final attempt is today's law untouched. PauseSignal and abort are NEVER retried. One runtimeStageId + one CommitBundle per stage regardless of attempts; retries do not consume `maxIterations`.
|
|
36
36
|
|
|
37
|
+
**Declared tags (9.21.0):** `StageNode.tags?: readonly string[]` is a NAME on a stage, declared at build time — POLICY-like (the retry precedent): no node type, no `computeNodeType` edit, no type-union site, and both prefixer twins spread `{ ...node }` so it rides subflow prefixing untouched. ONE landing site `FlowChartBuilder.ts · applyTags` (twin of `applyRetryPolicy`) serves the `.tag(...names)` cursor modifier and every `options.tags` site; refuses empty/non-string/`~`/duplicate/double-declare at build. Stamp: `FlowchartTraverser.executeNodeStep` sets `context.tags = node.tags` beside `runtimeStageId` (unconditionally — a re-used context must not keep a prior node's names); `StageContext.commit` spreads `tagsFragment()` beside `untrackedSourcesFragment()` on BOTH commit paths and RELEASES it, so a double-commit path (fork child fan-out repeat, mount exit) records it once — absent when empty (`CommitBundle.tags?`; pinned byte-identical to 9.20.0 by test/lib/engine/scenario/declared-tags-byte-identity.test.ts). NOT inherited by createNext/createChild; the one synthetic node that would lose it is `FlowChartExecutor.resume`'s `innerResumeChain` (copies `pausedNode.tags` on both re-entries). The spec carries `tags` as-is (Map advertises the vocabulary; `RuntimeStructureManager.stageNodeToStructure` copies it for run-time-resolved nodes). Reader: `time-travel/tagStops.ts` = `filterStops(commitStops(...), any-of over log[stop.commitIdx].tags, meta = the array)`, exported from `/trace`. Never a runtime `$tag()` — a value would ride past every redaction point. Design: docs/design/2026-09-declared-tags.md.
|
|
38
|
+
|
|
37
39
|
**Event order (code-verified; older docs had 3/4 swapped):** onStageStart → onRead/onWrite (live, PRE-commit) → **onStageEnd (StageRunner.ts:81) → onCommit (StageContext.ts:587)** → FlowRecorder onDecision/onFork/onSelected/onSubflowEntry → onStageExecuted (uniform, with stageType). Error path: commit STILL happens (FlowchartTraverser.ts:1088) before onError+rethrow — a failing stage's writes land. `interrupt()` rides the ERROR path's shape, not the pausable one: the stage function threw, so onStageEnd does NOT fire (the `addPausableFunction` pause DOES fire it — there the function returned normally).
|
|
38
40
|
|
|
39
41
|
## Extension points
|
|
@@ -63,7 +65,7 @@ run: executor.run (FlowChartExecutor.ts:1455): re-entrancy guard → fresh runId
|
|
|
63
65
|
per stage: executeNodeStep stamps runtimeStageId (:818) → StageRunner.run: scopeFactory → onStageStart → stage fn (writes stage into TransactionBuffer via StageContext.setObject; onWrite fires live) → onStageEnd → traverser calls context.commit() (:1094) → onCommit → onStageExecuted (narrative flushes buffered ops here).
|
|
64
66
|
decider: DeciderHandler.prepareDispatch (:89) — runs stage, **commits BEFORE branch resolution** (:124), matches branchId, fires onDecision(+evidence) → onStageExecuted('decider') → flat hop with InvokerStamp.
|
|
65
67
|
loop: Phase 6 sees isLoopRef → ContinuationResolver.resolveTarget (:107): id-map lookup, iteration guard (max 1000), onLoop → hop; zero stack, state carries forward (never rewound).
|
|
66
|
-
end: onRunEnd (throw → onRunFailed; pause → neither) → deferred terminalFlush → getSnapshot() = {sharedState (LIVE view; dev-mode frozen clone), initialState (the fold base), commitLog (detached+frozen), executionTree, subflowResults (dual-keyed), recorders}.
|
|
68
|
+
end: onRunEnd (throw → onRunFailed; pause → neither) → deferred terminalFlush → getSnapshot() = {sharedState (LIVE view; dev-mode frozen clone), initialState (the fold base), commitLog (detached+frozen), executionTree, subflowResults (dual-keyed; `globalContext` = the subflow's LIVE heap, or under `redact: true` its own nested mirror — 9.20.0, `servedSubflowResults`), recorders}.
|
|
67
69
|
|
|
68
70
|
## Backtracking
|
|
69
71
|
Seven mechanisms, no state rollback anywhere: **M1** TransactionBuffer staging + net-change commit — commit-on-error by design, explicitly NOT rollback (TransactionBuffer.ts:13-18; error path commits then rethrows). **M2** Pause/Resume checkpointing — the only resume-from-prior-point; TWO raise shapes, ONE checkpoint: `addPausableFunction` (resume runs `resumeFn`) and `interrupt(scope, payload)` (resume RE-RUNS THE STAGE FUNCTION FROM ITS TOP — stages are atomic; the answer returns out of the `interrupt()` call via a WeakMap keyed by the scope), discriminated by `checkpoint.pausedBy`; checkpoint is one detached structuredClone; resume rebuilds the cursor from pausedStageId+subflowPath and only overrides the LEAF subflow root (2+-deep pause re-executes outer pre-mount stages). **M3** commit-log replay (EventLog.materialise, commitValueAt — required under delta mode). **M4** loopTo re-entry — forward execution over accumulated state, bounded by maxIterations + dynamicNextHops. **M5** causalChain backward slicing (read-only analysis; honesty flags for untracked reads; `edgeAttribution: 'per-write'` refines edges via `TraceEntry.readKeys` when the writeProvenance dial recorded them — worklist, subset-of-ceiling safe). **M6 (query layer)** slice/ — variable-first triage: sliceForKey + append-fold element provenance + the ONLY safe serializations (sliceToJSON/formatSlice — never JSON.stringify a slice root). **M7 (read-time cursor)** time-travel/ — `timeTravel(snapshot)` walks stops over the finished log with `stateAt()` folding M3's replay at each one (base = `RuntimeSnapshot.initialState`, which now travels with the log); it is a READER, never a second live cursor, and `drill(mount)` opens a separate cursor over a subflow's own isolated log. 9.18.0: `timeTravel([paused, resumed])` chains a cross-executor M2 resume into ONE axis — the resumed leg's `initialState` (= the state at the pause) restarts the fold, commit indices stay run-local (`Stop.sourceIdx` names the leg), and the chain is refused unless the never-reset execution counter is monotonic across legs. Deep dive + step tables: [.claude/rules/backtracking.md](.claude/rules/backtracking.md).
|
|
@@ -38,10 +38,12 @@ export declare class DeciderList<TOut = any, TScope = any> {
|
|
|
38
38
|
addFunctionBranch(id: string, name: string, fn?: StageFunction<TOut, TScope>, description?: string,
|
|
39
39
|
/** `{ loopTo }` declares this branch loops back to an already-declared
|
|
40
40
|
* stage — the loop is SOURCED FROM THIS BRANCH (not the decider).
|
|
41
|
-
* `{ retry }` gives THIS BRANCH's stage a declarative retry policy
|
|
41
|
+
* `{ retry }` gives THIS BRANCH's stage a declarative retry policy;
|
|
42
|
+
* `{ tags }` puts declared tags on it (see `FlowChartBuilder.tag`). */
|
|
42
43
|
options?: {
|
|
43
44
|
readonly loopTo?: string;
|
|
44
45
|
readonly retry?: RetryPolicy;
|
|
46
|
+
readonly tags?: readonly string[];
|
|
45
47
|
}): DeciderList<TOut, TScope>;
|
|
46
48
|
/**
|
|
47
49
|
* Add a pausable stage as a decider branch.
|
|
@@ -56,10 +58,11 @@ export declare class DeciderList<TOut = any, TScope = any> {
|
|
|
56
58
|
* stage — the loop is SOURCED FROM THIS BRANCH (not the decider).
|
|
57
59
|
* `{ retry }` gives THIS BRANCH's `execute` half a retry policy (the
|
|
58
60
|
* `resume` half runs without it — a different function, a different
|
|
59
|
-
* contract). */
|
|
61
|
+
* contract); `{ tags }` puts declared tags on it. */
|
|
60
62
|
options?: {
|
|
61
63
|
readonly loopTo?: string;
|
|
62
64
|
readonly retry?: RetryPolicy;
|
|
65
|
+
readonly tags?: readonly string[];
|
|
63
66
|
}): DeciderList<TOut, TScope>;
|
|
64
67
|
addSubFlowChartBranch(id: string, subflow: FlowChart<any, any>, mountName?: string, options?: SubflowMountOptions): DeciderList<TOut, TScope>;
|
|
65
68
|
addLazySubFlowChartBranch(id: string, resolver: () => FlowChart<any, any>, mountName?: string, options?: SubflowMountOptions): DeciderList<TOut, TScope>;
|
|
@@ -116,9 +119,11 @@ export declare class SelectorFnList<TOut = any, TScope = any> {
|
|
|
116
119
|
private readonly branchDescInfo;
|
|
117
120
|
constructor(builder: FlowChartBuilder<TOut, TScope>, curNode: StageNode<TOut, TScope>, curSpec: SerializedPipelineStructure, parentDescriptionParts?: string[], parentStageDescriptions?: Map<string, string>, reservedStepNumber?: number, selectorDescription?: string);
|
|
118
121
|
addFunctionBranch(id: string, name: string, fn?: StageFunction<TOut, TScope>, description?: string,
|
|
119
|
-
/** `{ retry }` gives THIS BRANCH's stage a declarative retry policy
|
|
122
|
+
/** `{ retry }` gives THIS BRANCH's stage a declarative retry policy;
|
|
123
|
+
* `{ tags }` puts declared tags on it (see `FlowChartBuilder.tag`). */
|
|
120
124
|
options?: {
|
|
121
125
|
readonly retry?: RetryPolicy;
|
|
126
|
+
readonly tags?: readonly string[];
|
|
122
127
|
}): SelectorFnList<TOut, TScope>;
|
|
123
128
|
/**
|
|
124
129
|
* Add a pausable stage as a selector branch.
|
|
@@ -130,9 +135,10 @@ export declare class SelectorFnList<TOut = any, TScope = any> {
|
|
|
130
135
|
addPausableFunctionBranch(id: string, name: string, handler: PausableHandler<TScope>, description?: string,
|
|
131
136
|
/** `{ retry }` gives THIS BRANCH's `execute` half a retry policy (the
|
|
132
137
|
* `resume` half runs without it — a different function, a different
|
|
133
|
-
* contract). */
|
|
138
|
+
* contract); `{ tags }` puts declared tags on it. */
|
|
134
139
|
options?: {
|
|
135
140
|
readonly retry?: RetryPolicy;
|
|
141
|
+
readonly tags?: readonly string[];
|
|
136
142
|
}): SelectorFnList<TOut, TScope>;
|
|
137
143
|
addSubFlowChartBranch(id: string, subflow: FlowChart<any, any>, mountName?: string, options?: SubflowMountOptions): SelectorFnList<TOut, TScope>;
|
|
138
144
|
addLazySubFlowChartBranch(id: string, resolver: () => FlowChart<any, any>, mountName?: string, options?: SubflowMountOptions): SelectorFnList<TOut, TScope>;
|
|
@@ -295,6 +301,7 @@ export declare class FlowChartBuilder<TOut = any, TScope = any> {
|
|
|
295
301
|
}): this;
|
|
296
302
|
start(name: string, fn: StageFunction<TOut, TScope> | PausableHandler<TScope>, id: string, description?: string, options?: {
|
|
297
303
|
retry?: RetryPolicy;
|
|
304
|
+
tags?: readonly string[];
|
|
298
305
|
}): this;
|
|
299
306
|
/**
|
|
300
307
|
* Start a chart whose ROOT stage IS a selector — it runs first (reading
|
|
@@ -311,6 +318,7 @@ export declare class FlowChartBuilder<TOut = any, TScope = any> {
|
|
|
311
318
|
startSelector(name: string, fn: StageFunction<any, TScope>, id: string, description?: string, options?: {
|
|
312
319
|
failFast?: boolean;
|
|
313
320
|
retry?: RetryPolicy;
|
|
321
|
+
tags?: readonly string[];
|
|
314
322
|
}): SelectorFnList<TOut, TScope>;
|
|
315
323
|
addFunction(name: string, fn: StageFunction<TOut, TScope>, id: string, description?: string): this;
|
|
316
324
|
addStreamingFunction(name: string, fn: StageFunction<TOut, TScope>, id: string, streamId?: string, description?: string): this;
|
|
@@ -420,17 +428,20 @@ export declare class FlowChartBuilder<TOut = any, TScope = any> {
|
|
|
420
428
|
addDeciderFunction(name: string, fn: StageFunction<any, TScope>, id: string, description?: string,
|
|
421
429
|
/** `{ retry }` gives the DECIDER STAGE a retry policy. Declared here rather
|
|
422
430
|
* than via `.retry()` because this method returns a `DeciderList`, where a
|
|
423
|
-
* chained modifier could mean the decider OR the branch just added
|
|
431
|
+
* chained modifier could mean the decider OR the branch just added;
|
|
432
|
+
* `{ tags }` likewise. */
|
|
424
433
|
options?: {
|
|
425
434
|
retry?: RetryPolicy;
|
|
435
|
+
tags?: readonly string[];
|
|
426
436
|
}): DeciderList<TOut, TScope>;
|
|
427
437
|
addSelectorFunction(name: string, fn: StageFunction<any, TScope>, id: string, description?: string,
|
|
428
438
|
/** `{ retry }` gives the SELECTOR STAGE a retry policy — same reasoning as
|
|
429
439
|
* `addDeciderFunction`: this method returns a sub-builder, so a chained
|
|
430
|
-
* `.retry()` would be ambiguous. */
|
|
440
|
+
* `.retry()` would be ambiguous; `{ tags }` likewise. */
|
|
431
441
|
options?: {
|
|
432
442
|
failFast?: boolean;
|
|
433
443
|
retry?: RetryPolicy;
|
|
444
|
+
tags?: readonly string[];
|
|
434
445
|
}): SelectorFnList<TOut, TScope>;
|
|
435
446
|
addListOfFunction(children: SimplifiedParallelSpec<TOut, TScope>[], options?: {
|
|
436
447
|
failFast?: boolean;
|
|
@@ -522,6 +533,56 @@ export declare class FlowChartBuilder<TOut = any, TScope = any> {
|
|
|
522
533
|
* `retry` option instead.
|
|
523
534
|
*/
|
|
524
535
|
retry(policy: RetryPolicy): this;
|
|
536
|
+
/**
|
|
537
|
+
* Put NAMES on the stage you JUST added. The first commit bundle of each
|
|
538
|
+
* execution of that stage carries them (`CommitBundle.tags` — the bundle
|
|
539
|
+
* `commitStops` keys on), and the built spec advertises them
|
|
540
|
+
* (`SerializedPipelineStructure.tags`).
|
|
541
|
+
*
|
|
542
|
+
* ## Why declare it instead of deriving it from ids
|
|
543
|
+
*
|
|
544
|
+
* A reader that wants "the LLM turns" of a stored recording had to classify
|
|
545
|
+
* stages from their ids — a switch over `runtimeStageId` that parses `#` and
|
|
546
|
+
* `/`, lives in the consumer, and silently goes stale when a stage is
|
|
547
|
+
* renamed. Declared here, the name travels WITH the commit: a recording from
|
|
548
|
+
* any chart carries its own milestones, and `tagStops` (footprintjs/trace)
|
|
549
|
+
* scrubs them with no id conventions at all.
|
|
550
|
+
*
|
|
551
|
+
* ## What a tag is, and is not
|
|
552
|
+
*
|
|
553
|
+
* A tag is a NAME declared at build time — never a value. There is no
|
|
554
|
+
* run-time `$tag()`: a runtime string could carry data (`'user:' + email`)
|
|
555
|
+
* past every redaction point. Data-dependent marks are a keep rule over
|
|
556
|
+
* the fold at read time, or telemetry via `$emit`. Free strings: footprintjs
|
|
557
|
+
* owns no vocabulary; a consumer declares its own (`'milestone:<kind>'`).
|
|
558
|
+
*
|
|
559
|
+
* Stamped ONCE per execution of the stage: retry attempts share one stamp,
|
|
560
|
+
* a failed stage keeps its tag (the error path commits before it rethrows),
|
|
561
|
+
* a stage that `interrupt()`s and is resumed is two tagged stops on a chain
|
|
562
|
+
* because it ran twice, and an EMPTY commit is a tagged stop too. A subflow
|
|
563
|
+
* mount or a parallel-for-each stage can be tagged; the tag lands on the
|
|
564
|
+
* bundle that records its result.
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* ```ts
|
|
568
|
+
* flowChart<State>('Seed', seedFn, 'seed')
|
|
569
|
+
* .addFunction('Call model', callFn, 'call-llm')
|
|
570
|
+
* .tag('milestone:llm-turn')
|
|
571
|
+
* .addFunction('Route', routeFn, 'route')
|
|
572
|
+
* .tag('milestone:decision', 'audit')
|
|
573
|
+
* ```
|
|
574
|
+
*
|
|
575
|
+
* Applies to the CURRENT cursor — the stage added by the immediately
|
|
576
|
+
* preceding `start()` / `addFunction()` / `addStreamingFunction()` /
|
|
577
|
+
* `addPausableFunction()` / `addSubFlowChartNext()` / `addParallelForEach()`.
|
|
578
|
+
* Where the cursor would be ambiguous (a decider, selector, branch, or fork
|
|
579
|
+
* child), declare the tags in that method's own `tags` option instead.
|
|
580
|
+
*
|
|
581
|
+
* Refused at build time: an empty name, a non-string, the reserved
|
|
582
|
+
* branch-segment marker `~` inside a name, a name declared twice, and a
|
|
583
|
+
* second declaration on the same stage.
|
|
584
|
+
*/
|
|
585
|
+
tag(...names: readonly string[]): this;
|
|
525
586
|
loopTo(stageId: string): this;
|
|
526
587
|
onStream(handler: StreamTokenHandler): this;
|
|
527
588
|
onStreamStart(handler: StreamLifecycleHandler): this;
|