footprintjs 4.16.0 → 4.17.2

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.
Files changed (188) hide show
  1. package/AGENTS.md +567 -82
  2. package/CLAUDE.md +57 -0
  3. package/README.md +2 -0
  4. package/dist/advanced.js +4 -2
  5. package/dist/detach.js +78 -0
  6. package/dist/esm/advanced.js +2 -1
  7. package/dist/esm/detach.js +57 -0
  8. package/dist/esm/lib/builder/FlowChartBuilder.js +171 -61
  9. package/dist/esm/lib/contract/openapi.js +4 -5
  10. package/dist/esm/lib/contract/schema.js +115 -4
  11. package/dist/esm/lib/decide/decide.js +4 -5
  12. package/dist/esm/lib/decide/evidence.js +3 -2
  13. package/dist/esm/lib/detach/drivers/immediate.js +66 -0
  14. package/dist/esm/lib/detach/drivers/microtaskBatch.js +113 -0
  15. package/dist/esm/lib/detach/drivers/sendBeacon.js +78 -0
  16. package/dist/esm/lib/detach/drivers/setImmediate.js +81 -0
  17. package/dist/esm/lib/detach/drivers/setTimeout.js +69 -0
  18. package/dist/esm/lib/detach/drivers/workerThread.js +117 -0
  19. package/dist/esm/lib/detach/flush.js +91 -0
  20. package/dist/esm/lib/detach/handle.js +134 -0
  21. package/dist/esm/lib/detach/registry.js +97 -0
  22. package/dist/esm/lib/detach/runChild.js +40 -0
  23. package/dist/esm/lib/detach/spawn.js +86 -0
  24. package/dist/esm/lib/detach/types.js +37 -0
  25. package/dist/esm/lib/engine/errors/errorInfo.js +4 -4
  26. package/dist/esm/lib/engine/graph/StageNode.js +2 -2
  27. package/dist/esm/lib/engine/handlers/ChildrenExecutor.js +9 -8
  28. package/dist/esm/lib/engine/handlers/ContinuationResolver.js +12 -9
  29. package/dist/esm/lib/engine/handlers/DeciderHandler.js +7 -8
  30. package/dist/esm/lib/engine/handlers/ExtractorRunner.js +14 -8
  31. package/dist/esm/lib/engine/handlers/NodeResolver.js +5 -3
  32. package/dist/esm/lib/engine/handlers/RuntimeStructureManager.js +11 -14
  33. package/dist/esm/lib/engine/handlers/SelectorHandler.js +9 -9
  34. package/dist/esm/lib/engine/handlers/StageRunner.js +9 -11
  35. package/dist/esm/lib/engine/handlers/SubflowExecutor.js +13 -12
  36. package/dist/esm/lib/engine/handlers/SubflowInputMapper.js +4 -4
  37. package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +85 -96
  38. package/dist/esm/lib/engine/narrative/FlowRecorderDispatcher.js +18 -36
  39. package/dist/esm/lib/engine/narrative/NarrativeFlowRecorder.js +6 -5
  40. package/dist/esm/lib/engine/narrative/recorders/AdaptiveNarrativeFlowRecorder.js +7 -6
  41. package/dist/esm/lib/engine/narrative/recorders/ManifestFlowRecorder.js +10 -10
  42. package/dist/esm/lib/engine/narrative/recorders/MilestoneNarrativeFlowRecorder.js +5 -3
  43. package/dist/esm/lib/engine/narrative/recorders/ProgressiveNarrativeFlowRecorder.js +4 -3
  44. package/dist/esm/lib/engine/narrative/recorders/RLENarrativeFlowRecorder.js +4 -4
  45. package/dist/esm/lib/engine/narrative/recorders/SeparateNarrativeFlowRecorder.js +5 -6
  46. package/dist/esm/lib/engine/narrative/recorders/SilentNarrativeFlowRecorder.js +5 -6
  47. package/dist/esm/lib/engine/narrative/recorders/WindowedNarrativeFlowRecorder.js +5 -3
  48. package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +97 -71
  49. package/dist/esm/lib/memory/DiagnosticCollector.js +6 -8
  50. package/dist/esm/lib/memory/EventLog.js +5 -3
  51. package/dist/esm/lib/memory/SharedMemory.js +3 -2
  52. package/dist/esm/lib/memory/StageContext.js +44 -14
  53. package/dist/esm/lib/memory/TransactionBuffer.js +9 -8
  54. package/dist/esm/lib/memory/backtrack.js +3 -4
  55. package/dist/esm/lib/memory/commitLogUtils.js +2 -2
  56. package/dist/esm/lib/memory/utils.js +2 -3
  57. package/dist/esm/lib/pause/types.js +33 -14
  58. package/dist/esm/lib/reactive/createTypedScope.js +10 -8
  59. package/dist/esm/lib/reactive/types.js +3 -1
  60. package/dist/esm/lib/recorder/BoundaryStateTracker.js +263 -0
  61. package/dist/esm/lib/recorder/CompositeRecorder.js +3 -1
  62. package/dist/esm/lib/recorder/InOutRecorder.js +5 -6
  63. package/dist/esm/lib/recorder/KeyedRecorder.js +2 -4
  64. package/dist/esm/lib/recorder/QualityRecorder.js +15 -14
  65. package/dist/esm/lib/recorder/SequenceRecorder.js +11 -12
  66. package/dist/esm/lib/recorder/TopologyRecorder.js +36 -40
  67. package/dist/esm/lib/recorder/index.js +2 -1
  68. package/dist/esm/lib/recorder/qualityTrace.js +4 -5
  69. package/dist/esm/lib/runner/ExecutionRuntime.js +20 -4
  70. package/dist/esm/lib/runner/FlowChartExecutor.js +99 -55
  71. package/dist/esm/lib/runner/RunContext.js +5 -3
  72. package/dist/esm/lib/runner/RunnableChart.js +7 -9
  73. package/dist/esm/lib/runner/getSubtreeSnapshot.js +4 -5
  74. package/dist/esm/lib/schema/errors.js +4 -3
  75. package/dist/esm/lib/schema/validate.js +4 -5
  76. package/dist/esm/lib/scope/ScopeFacade.js +52 -35
  77. package/dist/esm/lib/scope/providers/baseStateCompatible.js +9 -9
  78. package/dist/esm/lib/scope/providers/guards.js +2 -2
  79. package/dist/esm/lib/scope/recorders/DebugRecorder.js +9 -7
  80. package/dist/esm/lib/scope/recorders/MetricRecorder.js +10 -8
  81. package/dist/esm/lib/scope/state/zod/defineScopeFromZod.js +2 -3
  82. package/dist/esm/lib/scope/state/zod/resolver.js +2 -3
  83. package/dist/esm/lib/scope/state/zod/scopeFactory.js +16 -20
  84. package/dist/esm/lib/scope/state/zod/utils/validateHelper.js +57 -14
  85. package/dist/esm/trace.js +4 -1
  86. package/dist/lib/builder/FlowChartBuilder.js +171 -61
  87. package/dist/lib/contract/openapi.js +4 -5
  88. package/dist/lib/contract/schema.js +115 -4
  89. package/dist/lib/decide/decide.js +4 -5
  90. package/dist/lib/decide/evidence.js +3 -2
  91. package/dist/lib/detach/drivers/immediate.js +70 -0
  92. package/dist/lib/detach/drivers/microtaskBatch.js +117 -0
  93. package/dist/lib/detach/drivers/sendBeacon.js +82 -0
  94. package/dist/lib/detach/drivers/setImmediate.js +85 -0
  95. package/dist/lib/detach/drivers/setTimeout.js +73 -0
  96. package/dist/lib/detach/drivers/workerThread.js +121 -0
  97. package/dist/lib/detach/flush.js +95 -0
  98. package/dist/lib/detach/handle.js +140 -0
  99. package/dist/lib/detach/registry.js +106 -0
  100. package/dist/lib/detach/runChild.js +67 -0
  101. package/dist/lib/detach/spawn.js +92 -0
  102. package/dist/lib/detach/types.js +38 -0
  103. package/dist/lib/engine/errors/errorInfo.js +4 -4
  104. package/dist/lib/engine/graph/StageNode.js +2 -2
  105. package/dist/lib/engine/handlers/ChildrenExecutor.js +9 -8
  106. package/dist/lib/engine/handlers/ContinuationResolver.js +12 -9
  107. package/dist/lib/engine/handlers/DeciderHandler.js +7 -8
  108. package/dist/lib/engine/handlers/ExtractorRunner.js +14 -8
  109. package/dist/lib/engine/handlers/NodeResolver.js +5 -3
  110. package/dist/lib/engine/handlers/RuntimeStructureManager.js +11 -14
  111. package/dist/lib/engine/handlers/SelectorHandler.js +9 -9
  112. package/dist/lib/engine/handlers/StageRunner.js +9 -11
  113. package/dist/lib/engine/handlers/SubflowExecutor.js +13 -12
  114. package/dist/lib/engine/handlers/SubflowInputMapper.js +4 -4
  115. package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +85 -96
  116. package/dist/lib/engine/narrative/FlowRecorderDispatcher.js +18 -36
  117. package/dist/lib/engine/narrative/NarrativeFlowRecorder.js +6 -5
  118. package/dist/lib/engine/narrative/recorders/AdaptiveNarrativeFlowRecorder.js +7 -6
  119. package/dist/lib/engine/narrative/recorders/ManifestFlowRecorder.js +10 -10
  120. package/dist/lib/engine/narrative/recorders/MilestoneNarrativeFlowRecorder.js +5 -3
  121. package/dist/lib/engine/narrative/recorders/ProgressiveNarrativeFlowRecorder.js +4 -3
  122. package/dist/lib/engine/narrative/recorders/RLENarrativeFlowRecorder.js +4 -4
  123. package/dist/lib/engine/narrative/recorders/SeparateNarrativeFlowRecorder.js +5 -6
  124. package/dist/lib/engine/narrative/recorders/SilentNarrativeFlowRecorder.js +5 -6
  125. package/dist/lib/engine/narrative/recorders/WindowedNarrativeFlowRecorder.js +5 -3
  126. package/dist/lib/engine/traversal/FlowchartTraverser.js +97 -71
  127. package/dist/lib/memory/DiagnosticCollector.js +6 -8
  128. package/dist/lib/memory/EventLog.js +5 -3
  129. package/dist/lib/memory/SharedMemory.js +3 -2
  130. package/dist/lib/memory/StageContext.js +44 -14
  131. package/dist/lib/memory/TransactionBuffer.js +9 -8
  132. package/dist/lib/memory/backtrack.js +3 -4
  133. package/dist/lib/memory/commitLogUtils.js +2 -2
  134. package/dist/lib/memory/utils.js +2 -3
  135. package/dist/lib/pause/types.js +33 -14
  136. package/dist/lib/reactive/createTypedScope.js +10 -8
  137. package/dist/lib/reactive/types.js +3 -1
  138. package/dist/lib/recorder/BoundaryStateTracker.js +267 -0
  139. package/dist/lib/recorder/CompositeRecorder.js +3 -1
  140. package/dist/lib/recorder/InOutRecorder.js +5 -6
  141. package/dist/lib/recorder/KeyedRecorder.js +2 -4
  142. package/dist/lib/recorder/QualityRecorder.js +15 -14
  143. package/dist/lib/recorder/SequenceRecorder.js +11 -12
  144. package/dist/lib/recorder/TopologyRecorder.js +36 -40
  145. package/dist/lib/recorder/index.js +4 -2
  146. package/dist/lib/recorder/qualityTrace.js +4 -5
  147. package/dist/lib/runner/ExecutionRuntime.js +20 -4
  148. package/dist/lib/runner/FlowChartExecutor.js +99 -55
  149. package/dist/lib/runner/RunContext.js +5 -3
  150. package/dist/lib/runner/RunnableChart.js +7 -9
  151. package/dist/lib/runner/getSubtreeSnapshot.js +4 -5
  152. package/dist/lib/schema/errors.js +4 -3
  153. package/dist/lib/schema/validate.js +4 -5
  154. package/dist/lib/scope/ScopeFacade.js +52 -35
  155. package/dist/lib/scope/providers/baseStateCompatible.js +9 -9
  156. package/dist/lib/scope/providers/guards.js +2 -2
  157. package/dist/lib/scope/recorders/DebugRecorder.js +9 -7
  158. package/dist/lib/scope/recorders/MetricRecorder.js +10 -8
  159. package/dist/lib/scope/state/zod/defineScopeFromZod.js +2 -3
  160. package/dist/lib/scope/state/zod/resolver.js +2 -3
  161. package/dist/lib/scope/state/zod/scopeFactory.js +16 -20
  162. package/dist/lib/scope/state/zod/utils/validateHelper.js +57 -14
  163. package/dist/trace.js +6 -2
  164. package/dist/types/advanced.d.ts +1 -0
  165. package/dist/types/detach.d.ts +59 -0
  166. package/dist/types/lib/builder/FlowChartBuilder.d.ts +81 -0
  167. package/dist/types/lib/detach/drivers/immediate.d.ts +39 -0
  168. package/dist/types/lib/detach/drivers/microtaskBatch.d.ts +57 -0
  169. package/dist/types/lib/detach/drivers/sendBeacon.d.ts +38 -0
  170. package/dist/types/lib/detach/drivers/setImmediate.d.ts +32 -0
  171. package/dist/types/lib/detach/drivers/setTimeout.d.ts +34 -0
  172. package/dist/types/lib/detach/drivers/workerThread.d.ts +50 -0
  173. package/dist/types/lib/detach/flush.d.ts +62 -0
  174. package/dist/types/lib/detach/handle.d.ts +83 -0
  175. package/dist/types/lib/detach/registry.d.ts +82 -0
  176. package/dist/types/lib/detach/runChild.d.ts +41 -0
  177. package/dist/types/lib/detach/spawn.d.ts +64 -0
  178. package/dist/types/lib/detach/types.d.ts +200 -0
  179. package/dist/types/lib/engine/traversal/FlowchartTraverser.d.ts +0 -1
  180. package/dist/types/lib/engine/types.d.ts +0 -1
  181. package/dist/types/lib/reactive/types.d.ts +4 -0
  182. package/dist/types/lib/recorder/BoundaryStateTracker.d.ts +215 -0
  183. package/dist/types/lib/recorder/index.d.ts +1 -0
  184. package/dist/types/lib/runner/FlowChartExecutor.d.ts +28 -0
  185. package/dist/types/lib/scope/ScopeFacade.d.ts +4 -0
  186. package/dist/types/lib/scope/state/zod/utils/validateHelper.d.ts +13 -1
  187. package/dist/types/trace.d.ts +1 -0
  188. package/package.json +6 -1
@@ -0,0 +1,41 @@
1
+ /**
2
+ * detach/runChild.ts — The "how do I actually run a child flowchart?" hook.
3
+ *
4
+ * Pattern: Strategy (GoF). Drivers are decoupled from the FlowChartExecutor
5
+ * — each driver is created with a `ChildRunner` it calls to
6
+ * materialize the work. Default implementation imports the
7
+ * executor lazily so drivers can be picked up by tree-shakers
8
+ * that don't pull the runner module into the bundle.
9
+ * Role: Glue between drivers and the engine. Without this seam, every
10
+ * driver would have to import `FlowChartExecutor` directly,
11
+ * creating circular import risk and bundle bloat.
12
+ *
13
+ * Why a separate module (not inlined in each driver):
14
+ * - DRY — every driver shares the same "instantiate executor, run, return
15
+ * result" sequence
16
+ * - Allows test code to swap the runner via factory injection without
17
+ * having to rebuild the whole driver
18
+ * - Future-proofs for the case where we want to inject env/recorders
19
+ * into the child (the runner is the natural place to do that)
20
+ */
21
+ import type { FlowChart } from '../builder/types.js';
22
+ /**
23
+ * Function the driver calls to actually execute the child flowchart.
24
+ * Returns a Promise resolving to the chart's terminal value (whatever
25
+ * `FlowChartExecutor.run()` resolves with), or rejects on failure.
26
+ *
27
+ * Drivers wrap this in their own try/catch so the rejection routes to
28
+ * `handle._markFailed()` instead of escaping into the parent context
29
+ * (passive-recorder rule).
30
+ */
31
+ export type ChildRunner = (child: FlowChart, input: unknown) => Promise<unknown>;
32
+ /**
33
+ * Default runner — instantiates a fresh `FlowChartExecutor` for each
34
+ * child and awaits its run. Returns the executor's traversal result.
35
+ *
36
+ * Lazy-imports `FlowChartExecutor` so drivers that consumers create with
37
+ * their own runner don't pull the engine into their bundle.
38
+ *
39
+ * Uses dynamic import — see https://v8.dev/features/dynamic-import.
40
+ */
41
+ export declare const defaultRunChild: ChildRunner;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * detach/spawn.ts — One-call detach primitive used by both scope and
3
+ * executor surfaces.
4
+ *
5
+ * Pattern: Facade (GoF). Hides driver invocation + refId minting +
6
+ * registry registration behind two named functions
7
+ * (`detachAndJoinLater`, `detachAndForget`). Same helper is
8
+ * called from `scope.$detachAndJoinLater(...)` and from
9
+ * `executor.detachAndJoinLater(...)` — single source of truth.
10
+ *
11
+ * Why a separate module:
12
+ * - Avoids duplicating the "validate driver, mint refId, call schedule"
13
+ * sequence in both scope and executor entry points
14
+ * - Keeps the scope/executor files free of driver knowledge — they
15
+ * just call this and forward the result
16
+ *
17
+ * refId scheme:
18
+ * - When the caller is a stage (scope path): refId = `${runtimeStageId}:detach:${counter}`
19
+ * — the runtimeStageId prefix lets diagnostics correlate the handle
20
+ * back to the source stage
21
+ * - When the caller is bare executor (executor path):
22
+ * refId = `__executor__:detach:${counter}` — uniform "no source stage"
23
+ * marker
24
+ * - Counter is module-private + monotonic for the process lifetime —
25
+ * safe across re-entrant detach calls
26
+ */
27
+ import type { FlowChart } from '../builder/types.js';
28
+ import type { DetachDriver, DetachHandle } from './types.js';
29
+ /** Reset the counter for tests — never call from production code. */
30
+ export declare function _resetSpawnCounterForTests(): void;
31
+ /**
32
+ * Schedule `child` on the given driver, with the consumer's `input`,
33
+ * and return the resulting `DetachHandle`. Callers can `wait()` on it,
34
+ * read its `.status` property, or just hold the reference for later.
35
+ *
36
+ * **Joinable variant** — the caller wants to be able to await the result
37
+ * (or check its status). The `forget` variant simply discards the handle.
38
+ *
39
+ * @param driver - The driver implementation to use. Required (no
40
+ * library-default — passing it explicitly avoids global state and
41
+ * keeps the engine free of driver imports).
42
+ * @param child - The child flowchart to run.
43
+ * @param input - The input to hand to the child's run() call.
44
+ * @param sourcePrefix - Refix prefix for the minted refId; pass the
45
+ * parent's `runtimeStageId` from a stage caller, or `'__executor__'`
46
+ * from a bare-executor caller.
47
+ */
48
+ export declare function detachAndJoinLater(driver: DetachDriver, child: FlowChart, input: unknown, sourcePrefix: string): DetachHandle;
49
+ /**
50
+ * Same as `detachAndJoinLater` but discards the handle. Use when the
51
+ * caller doesn't care about the result and doesn't need to await — e.g.,
52
+ * fire-and-forget telemetry exports.
53
+ *
54
+ * The handle still exists internally (driver creates it, registry holds
55
+ * it briefly) — but the caller cannot reference it. This is intentional:
56
+ * having no handle reference is what gives "forget" its semantic — there
57
+ * is no chance of the caller accidentally awaiting it.
58
+ *
59
+ * Errors raised by the child are STILL routed to the handle's failed
60
+ * state (the driver does that). They just go unobserved unless something
61
+ * else (a recorder, logging) is wired to surface them. See the docs in
62
+ * T7 for recommended observability patterns for "forget" detach.
63
+ */
64
+ export declare function detachAndForget(driver: DetachDriver, child: FlowChart, input: unknown, sourcePrefix: string): void;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * detach/types.ts — Type definitions for the fire-and-forget primitive.
3
+ *
4
+ * Pattern: Strategy + Bridge (GoF). Same shape as cache strategies in
5
+ * agentfootprint v2.6 — ONE interface, N concrete drivers,
6
+ * consumer picks via explicit import.
7
+ * Role: Foundation. Every other detach file imports from here.
8
+ * This is the lockable contract; downstream files implement it.
9
+ *
10
+ * Two sibling concepts:
11
+ *
12
+ * 1. `DetachDriver` — the algorithm that decides WHEN/HOW the work runs.
13
+ * Six built-in algorithms ship as algorithm-named exports
14
+ * (`microtaskBatchDriver`, `setImmediateDriver`, `sendBeaconDriver`,
15
+ * `setTimeoutDriver`, `immediateDriver`, `workerThreadDriver`).
16
+ * Consumers can implement their own (BYOS — bring your own driver).
17
+ *
18
+ * 2. `DetachHandle` — the consumer-facing handle returned by
19
+ * `detachAndJoinLater`. Exposes status as PROPERTIES (sync read)
20
+ * and `wait()` (Promise — opt-in async join). Style 2 from the
21
+ * panel review: properties for sync, single method for async.
22
+ *
23
+ * Naming policy (locked from naming review):
24
+ * - Public API uses simple verbs / properties (product-engineer friendly)
25
+ * - Internal class names CAN use CS terms (Scheduler, Continuation, etc.)
26
+ * - Drivers are algorithm-named (semantic at the algorithm level)
27
+ *
28
+ * Locked design decisions (panel review captured in
29
+ * `docs/inspiration/detach-primitive.md`):
30
+ * - Sync hot path; async only at flush boundaries
31
+ * - Errors → commitLog + typed event, NEVER thrown to parent
32
+ * - Scope isolation between parent and detached child
33
+ * - Lifecycle tied to executor disposal
34
+ * - Type-level rejection of `outputMapper` on detach options
35
+ */
36
+ import type { FlowChart } from '../builder/types.js';
37
+ /**
38
+ * Snapshot of a detached handle's state. Returned by `handle.poll()`
39
+ * (when added in a follow-up; v1 exposes properties directly on
40
+ * `DetachHandle`).
41
+ */
42
+ export interface DetachPollResult {
43
+ readonly status: DetachHandle['status'];
44
+ /** Present iff status === 'done'. */
45
+ readonly result?: unknown;
46
+ /** Present iff status === 'failed'. */
47
+ readonly error?: Error;
48
+ }
49
+ /**
50
+ * Result delivered when the handle's `wait()` Promise resolves
51
+ * successfully (status reached 'done'). Rejection delivers the
52
+ * native `Error` directly — no wrapping shape.
53
+ */
54
+ export interface DetachWaitResult {
55
+ readonly result: unknown;
56
+ }
57
+ /**
58
+ * Handle returned by `chart.detachAndJoinLater(...)` and
59
+ * `executor.detachAndJoinLater(...)`. Exposes status as PROPERTIES so
60
+ * sync access is property-read (cheap, no allocation). The single
61
+ * method `wait()` returns a Promise for opt-in async join.
62
+ *
63
+ * The handle is **not** Promise-shaped (no `.then()`) — that would
64
+ * make it accidentally awaitable, defeating the fire-and-forget
65
+ * semantics. To await, the consumer must call `.wait()` explicitly.
66
+ *
67
+ * Lifecycle:
68
+ *
69
+ * queued → driver received the work, hasn't started it
70
+ * running → driver started the work
71
+ * done → terminal: result available
72
+ * failed → terminal: error available
73
+ *
74
+ * `done` and `failed` are TERMINAL — once reached, status never
75
+ * changes again.
76
+ */
77
+ export interface DetachHandle {
78
+ /** Stable id assigned at detach time. Used as the lookup key in
79
+ * `detachRegistry` and as the scope-storage key prefix. */
80
+ readonly id: string;
81
+ /** Current state. Read directly for sync access. */
82
+ readonly status: 'queued' | 'running' | 'done' | 'failed';
83
+ /** The work's result. Present iff `status === 'done'`. Reading
84
+ * before terminal returns `undefined`. */
85
+ readonly result?: unknown;
86
+ /** The work's error. Present iff `status === 'failed'`. Reading
87
+ * before terminal returns `undefined`. */
88
+ readonly error?: Error;
89
+ /**
90
+ * Opt-in async join. Returns a Promise that:
91
+ * - resolves with `{ result }` when status becomes 'done'
92
+ * - rejects with the captured `Error` when status becomes 'failed'
93
+ * - resolves/rejects IMMEDIATELY if status is already terminal
94
+ * - returns the SAME cached Promise on repeated calls (no
95
+ * re-running, no duplicated I/O)
96
+ *
97
+ * Calling `wait()` does NOT change the handle's lifecycle — it's
98
+ * passive observation. If never called, the work still completes;
99
+ * the handle just goes uncollected (parent execution unaffected).
100
+ *
101
+ * Use when:
102
+ * - You actually need the result and want to await it
103
+ * - Coordinating multiple handles via Promise.all / Promise.race
104
+ * - Backpressure ("don't fire more than N in flight")
105
+ *
106
+ * Don't use when:
107
+ * - Pure fire-and-forget (use `detachAndForget` — no handle returned)
108
+ * - You just want to check status (read `handle.status` directly)
109
+ */
110
+ wait(): Promise<DetachWaitResult>;
111
+ }
112
+ /**
113
+ * Capabilities a driver declares. Drives the runtime decision of
114
+ * "is this driver appropriate for the current environment?" Consumers
115
+ * inspect at construction; the framework doesn't enforce.
116
+ *
117
+ * All capabilities are optional flags — false / undefined means "not
118
+ * supported / no claim." A driver that supports everything sets all
119
+ * to true.
120
+ */
121
+ export interface DriverCapabilities {
122
+ /** Driver works in browser environments (window, document, etc.). */
123
+ readonly browserSafe?: boolean;
124
+ /** Driver works in Node.js environments. */
125
+ readonly nodeSafe?: boolean;
126
+ /** Driver works in edge runtimes (Cloudflare Workers, Deno Deploy,
127
+ * Bun edge, etc. — restricted environments). */
128
+ readonly edgeSafe?: boolean;
129
+ /** Work survives page-unload / process-exit. e.g.,
130
+ * `sendBeaconDriver` schedules via `navigator.sendBeacon` which
131
+ * ships even on tab close. */
132
+ readonly survivesUnload?: boolean;
133
+ /** Work runs on a separate OS thread (no event-loop block).
134
+ * e.g., `workerThreadDriver`. */
135
+ readonly cpuIsolated?: boolean;
136
+ }
137
+ /**
138
+ * A driver is the WHEN/HOW of the detach. Maps `(child, input, refId)`
139
+ * to a `DetachHandle` whose lifecycle the driver owns.
140
+ *
141
+ * Drivers are themselves footprintjs primitives — internally they may
142
+ * be a single function or a multi-stage flowChart. Either way, the
143
+ * interface they expose to consumers is `schedule(...)`.
144
+ *
145
+ * Drivers MUST:
146
+ * - Return synchronously (the agent loop never blocks on schedule)
147
+ * - Not throw — errors during scheduling route through the handle
148
+ * (`handle.status = 'failed'`, `handle.error = ...`)
149
+ * - Honor the passive-recorder rule: the parent's `detach*` call
150
+ * never waits for the driver's deferred work to complete
151
+ *
152
+ * Drivers MAY:
153
+ * - Implement `validate()` for one-time configuration checks at
154
+ * registration / use time (e.g., assert `navigator.sendBeacon` exists)
155
+ * - Build their internal pipeline as a footprintjs flowChart for
156
+ * observability — driver implementation detail, not consumer-facing
157
+ *
158
+ * @example Built-in
159
+ * import { microtaskBatchDriver } from 'footprintjs/detach';
160
+ * const handle = driver.schedule(child, input, refId);
161
+ *
162
+ * @example Custom (BYOS)
163
+ * const lambdaExtensionDriver: DetachDriver = {
164
+ * name: 'lambda-extension',
165
+ * capabilities: { nodeSafe: true, survivesUnload: true },
166
+ * schedule(child, input, refId) {
167
+ * sharedBuffer.push({ refId, child, input });
168
+ * return createHandle(refId, 'queued');
169
+ * },
170
+ * };
171
+ */
172
+ export interface DetachDriver {
173
+ /** Stable name for diagnostics + registry lookup. Conventionally
174
+ * algorithm-named (e.g. `'microtask-batch'`, `'send-beacon'`). */
175
+ readonly name: string;
176
+ /** What this driver supports. Used by consumers to pick the right
177
+ * driver for their environment. */
178
+ readonly capabilities: DriverCapabilities;
179
+ /**
180
+ * Hand the work to the driver's scheduling mechanism. MUST return
181
+ * synchronously with a fresh `DetachHandle`. The actual work may
182
+ * run later (next microtask / setImmediate / browser-beacon-flush /
183
+ * worker-thread / etc.) on the driver's chosen mechanism.
184
+ */
185
+ schedule(child: FlowChart, input: unknown, refId: string): DetachHandle;
186
+ /**
187
+ * Optional one-time validation hook. Called at first use (or
188
+ * registration time, depending on driver) — drivers throw if their
189
+ * configuration is invalid (missing peer dep, unreachable endpoint,
190
+ * wrong API key shape, etc.).
191
+ *
192
+ * Example: `sendBeaconDriver.validate()` checks
193
+ * `typeof navigator?.sendBeacon === 'function'` and throws with a
194
+ * helpful message if absent (e.g., in Node).
195
+ *
196
+ * Per the New Relic panel review: early-fail-with-useful-message
197
+ * beats silent zero-emission.
198
+ */
199
+ validate?(): void;
200
+ }
@@ -18,7 +18,6 @@
18
18
  * Break semantics: If a stage calls breakFn(), commit and STOP.
19
19
  * Patch model: Stage writes into local patch; commitPatch() after return or throw.
20
20
  */
21
- /// <reference types="node" />
22
21
  import type { ScopeProtectionMode } from '../../scope/protection/types.js';
23
22
  import { FlowRecorderDispatcher } from '../narrative/FlowRecorderDispatcher.js';
24
23
  import type { FlowRecorder, IControlFlowNarrative } from '../narrative/types.js';
@@ -4,7 +4,6 @@
4
4
  * Centralizes type definitions to avoid circular dependencies.
5
5
  * Every handler receives HandlerDeps (the DI bag) instead of importing the traverser.
6
6
  */
7
- /// <reference types="node" />
8
7
  import type { StageContext } from '../memory/StageContext.js';
9
8
  import type { FlowControlType, FlowMessage } from '../memory/types.js';
10
9
  import type { ScopeProtectionMode } from '../scope/protection/types.js';
@@ -31,6 +31,8 @@ export interface ReactiveTarget {
31
31
  addMetric(name: string, value: unknown): void;
32
32
  addEval(name: string, value: unknown): void;
33
33
  emitEvent(name: string, payload?: unknown): void;
34
+ detachAndJoinLater(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): import('../detach/types.js').DetachHandle;
35
+ detachAndForget(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): void;
34
36
  }
35
37
  export interface ScopeMethods {
36
38
  $getValue(key: string): unknown;
@@ -122,6 +124,8 @@ export interface ScopeMethods {
122
124
  * ```
123
125
  */
124
126
  $emit(name: string, payload?: unknown): void;
127
+ $detachAndJoinLater(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): import('../detach/types.js').DetachHandle;
128
+ $detachAndForget(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): void;
125
129
  /**
126
130
  * Stop the current execution context.
127
131
  *
@@ -0,0 +1,215 @@
1
+ /**
2
+ * BoundaryStateTracker<TState> — third storage primitive on the recorder
3
+ * shelf, alongside `SequenceRecorder<T>` and `KeyedRecorder<T>`.
4
+ *
5
+ * **Mental model — observers vs. bookkeepers:**
6
+ *
7
+ * `Recorder` / `FlowRecorder` / `EmitRecorder` / `CombinedRecorder`
8
+ * are OBSERVER interfaces — they describe how a recorder hears
9
+ * events from the executor.
10
+ *
11
+ * `SequenceRecorder<T>` / `KeyedRecorder<T>` / `BoundaryStateTracker<TState>`
12
+ * are STORAGE primitives — three different bookkeeping shelves with
13
+ * different durability and indexing properties. A real recorder
14
+ * class typically picks ONE observer interface AND ONE storage
15
+ * shelf, combining them via `extends + implements`.
16
+ *
17
+ * **What this primitive answers:** "At any moment during the run, what
18
+ * is the LIVE transient state of every currently-active boundary?"
19
+ *
20
+ * A "boundary" is a matched event pair `[start, stop]` bracketing an
21
+ * interval — for example, `(llm_start, llm_end)` for an LLM call,
22
+ * `(tool_start, tool_end)` for tool execution, `(turn_start, turn_end)`
23
+ * for an agent turn. Between the brackets, intermediate events evolve
24
+ * the boundary's state (token chunks accumulating into a partial
25
+ * answer, args streaming in, etc.). On `stop`, the state clears.
26
+ *
27
+ * Algorithmically this is the **DFS bracket-sequence pattern** —
28
+ * stack-frame state during a graph-traversal interval. Same shape used
29
+ * by Tarjan's SCC algorithm, tree decomposition, and push-down
30
+ * automata. The `active` map is the open-brackets stack at any moment.
31
+ *
32
+ * **Comparison with the other storage primitives:**
33
+ *
34
+ * | Primitive | Stores | Time scope | Memory |
35
+ * |---------------------------------|--------------------------------------|---------------------|-------------|
36
+ * | `SequenceRecorder<T>` | append-only ordered + keyed entries | durable across run | O(N events) |
37
+ * | `KeyedRecorder<T>` | 1:1 entry per `runtimeStageId` | durable across run | O(N steps) |
38
+ * | `BoundaryStateTracker<TState>` | transient bracket-scoped state | live; clears on stop | O(K active) |
39
+ *
40
+ * **When to pick which:**
41
+ *
42
+ * - "I need to keep a permanent log of every event for time-travel"
43
+ * → `SequenceRecorder<T>`
44
+ * - "I want one durable record per stage execution (e.g., metrics)"
45
+ * → `KeyedRecorder<T>`
46
+ * - "I need to know what's happening RIGHT NOW inside an in-flight
47
+ * boundary (e.g., partial LLM stream, partial tool args)"
48
+ * → `BoundaryStateTracker<TState>` (this class)
49
+ *
50
+ * **What this is NOT for:**
51
+ *
52
+ * - Time-travel queries ("what was the state at past slider step N?")
53
+ * — transient state clears on stop. For time-travel, snapshot the
54
+ * state at each emit into a separate `SequenceRecorder<TState>`,
55
+ * or wait for a future `BoundarySnapshotRecorder<TState>` primitive.
56
+ *
57
+ * - Aggregations across the whole run (totals, counts) — those are
58
+ * `SequenceRecorder.aggregate()` / `KeyedRecorder.aggregate()`.
59
+ *
60
+ * - Stage-level concerns — those use `Recorder.onStageStart` /
61
+ * `Recorder.onStageEnd`. This primitive operates at finer
62
+ * granularity (events emitted DURING a stage execution).
63
+ *
64
+ * **Lifecycle contract — STRICT:**
65
+ *
66
+ * Every `startBoundary(key, ...)` call MUST be paired with a
67
+ * `stopBoundary(key)` call. Failure to wire the stop side produces a
68
+ * memory leak: the active map grows without bound, and `getAllActive()`
69
+ * returns stale entries that look in-flight but aren't. Common cause:
70
+ * subclass wires `start` to one event handler and forgets to wire
71
+ * `stop`. **Always wire both at the same time.**
72
+ *
73
+ * Dev mode (`enableDevMode()`) detects leaks at `clear()` time —
74
+ * warning includes the leaked keys so you can find the missing wiring.
75
+ *
76
+ * **Concurrency / nesting:**
77
+ *
78
+ * - Concurrent boundaries (parallel branches with two LLM calls
79
+ * active at once) work correctly: each is keyed independently in
80
+ * the active map.
81
+ * - Nested boundaries of DIFFERENT KINDS (Agent boundary contains
82
+ * LLM boundary) require SEPARATE tracker instances — one per kind.
83
+ * The base class tracks one boundary kind per instance.
84
+ *
85
+ * **Key convention:**
86
+ *
87
+ * The `key: string` is whatever your subclass picks. Convention:
88
+ * use `runtimeStageId` when the boundary maps 1:1 to a stage
89
+ * execution — this gives free interop with `SequenceRecorder
90
+ * .getEntriesForStep`, `KeyedRecorder.getByKey`, `findCommit` /
91
+ * `findLastWriter`, and the rest of the trace ecosystem. Use a more
92
+ * granular key (e.g., `toolCallId`) only when there are multiple
93
+ * concurrent boundaries WITHIN one stage execution.
94
+ *
95
+ * @example Build a live LLM tracker (combining storage + observer):
96
+ *
97
+ * ```typescript
98
+ * import {
99
+ * BoundaryStateTracker,
100
+ * type CombinedRecorder,
101
+ * type EmitEvent,
102
+ * } from 'footprintjs';
103
+ *
104
+ * interface LLMLiveState {
105
+ * readonly partial: string;
106
+ * readonly tokens: number;
107
+ * }
108
+ *
109
+ * class LiveLLMTracker
110
+ * extends BoundaryStateTracker<LLMLiveState> // STORAGE shelf
111
+ * implements CombinedRecorder // OBSERVER interface
112
+ * {
113
+ * readonly id = 'live-llm';
114
+ *
115
+ * // Observer half — translate events into bracket mutations.
116
+ * onEmit(event: EmitEvent): void {
117
+ * if (event.name === 'agentfootprint.stream.llm_start') {
118
+ * this.startBoundary(event.runtimeStageId, { partial: '', tokens: 0 });
119
+ * } else if (event.name === 'agentfootprint.stream.llm_end') {
120
+ * this.stopBoundary(event.runtimeStageId);
121
+ * } else if (event.name === 'agentfootprint.stream.token') {
122
+ * this.updateBoundary(event.runtimeStageId, (s) => ({
123
+ * partial: s.partial + (event.payload as { content: string }).content,
124
+ * tokens: s.tokens + 1,
125
+ * }));
126
+ * }
127
+ * }
128
+ *
129
+ * // Public read API — O(1) at any moment.
130
+ * isInFlight(): boolean { return this.hasActive; }
131
+ * getPartial(stageId: string): string {
132
+ * return this.getActive(stageId)?.partial ?? '';
133
+ * }
134
+ * }
135
+ *
136
+ * // Attached the same way as any other CombinedRecorder.
137
+ * const tracker = new LiveLLMTracker();
138
+ * executor.attachCombinedRecorder(tracker);
139
+ * await executor.run({ input });
140
+ *
141
+ * // Read live state at any time during or after the run.
142
+ * tracker.isInFlight();
143
+ * tracker.getPartial(rid);
144
+ * ```
145
+ */
146
+ export declare abstract class BoundaryStateTracker<TState> {
147
+ /** Stable id — same idempotency contract as other recorders. */
148
+ abstract readonly id: string;
149
+ /** Open-brackets stack: key → current transient state. */
150
+ private readonly active;
151
+ /** Per-key count of `updateBoundary` calls that landed without a
152
+ * matching active boundary. Drives rate-limited dev-mode warnings
153
+ * so a stuck loop doesn't spam the console. */
154
+ private readonly missedUpdates;
155
+ /**
156
+ * Open a new boundary with initial transient state.
157
+ *
158
+ * If a boundary with the same `key` is already active, the prior
159
+ * state is overwritten (last-writer-wins). In dev mode, a warning
160
+ * is logged because re-starting an active key usually indicates a
161
+ * missed `stopBoundary` call upstream.
162
+ *
163
+ * @param key Boundary identifier — by convention, `runtimeStageId`
164
+ * for 1:1-with-stage boundaries, or a more granular id
165
+ * (e.g., `toolCallId`) when multiple concurrent boundaries
166
+ * run within one stage execution.
167
+ * @param initial Initial state — typed by `TState`.
168
+ */
169
+ protected startBoundary(key: string, initial: TState): void;
170
+ /**
171
+ * Evolve the transient state of an active boundary using an updater
172
+ * function. Silent no-op if no boundary is active for `key`
173
+ * (defensive against out-of-order events). In dev mode, a rate-limited
174
+ * warning is logged on the 1st, 10th, and 100th missed update per key.
175
+ *
176
+ * @param key Boundary identifier (must match a prior `startBoundary`).
177
+ * @param updater Pure function: previous state → next state.
178
+ */
179
+ protected updateBoundary(key: string, updater: (prev: TState) => TState): void;
180
+ /**
181
+ * Close the boundary identified by `key` and return its FINAL
182
+ * transient state (for any cleanup the subclass wants — e.g., emit a
183
+ * snapshot to a SequenceRecorder for durable storage).
184
+ *
185
+ * @param key Boundary identifier.
186
+ * @returns The final state, or `undefined` if no boundary was active.
187
+ */
188
+ protected stopBoundary(key: string): TState | undefined;
189
+ /** Current transient state of ONE active boundary. `undefined` if no
190
+ * boundary is active for `key`. */
191
+ getActive(key: string): TState | undefined;
192
+ /**
193
+ * All currently-active boundaries.
194
+ *
195
+ * **Type-only readonly:** the returned reference IS the internal Map.
196
+ * TypeScript prevents mutation through the `ReadonlyMap` type, but a
197
+ * runtime cast or non-TS consumer can mutate it and corrupt internal
198
+ * state. **Do not mutate.**
199
+ */
200
+ getAllActive(): ReadonlyMap<string, TState>;
201
+ /** True if any boundary is currently active. O(1). */
202
+ get hasActive(): boolean;
203
+ /** Number of currently-active boundaries. O(1). */
204
+ get activeCount(): number;
205
+ /**
206
+ * Reset all transient state. Called by executors before each `run()`
207
+ * so consumers get a clean slate per run — same lifecycle contract
208
+ * as `SequenceRecorder.clear()`.
209
+ *
210
+ * In dev mode, warns if any boundaries are still active when called
211
+ * — likely indicates a missed `stopBoundary` upstream. The leaked
212
+ * keys are listed (truncated to 10) so the wiring bug is findable.
213
+ */
214
+ clear(): void;
215
+ }
@@ -1,3 +1,4 @@
1
+ export { BoundaryStateTracker } from './BoundaryStateTracker.js';
1
2
  export type { CombinedRecorder } from './CombinedRecorder.js';
2
3
  export { hasEmitRecorderMethods, hasFlowRecorderMethods, hasRecorderMethods, isFlowEvent } from './CombinedRecorder.js';
3
4
  export type { CompositeSnapshot } from './CompositeRecorder.js';
@@ -228,6 +228,34 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
228
228
  * ```
229
229
  */
230
230
  attachRecorder(recorder: Recorder): void;
231
+ /**
232
+ * Detach a child flowchart on the given driver and return a `DetachHandle`
233
+ * the caller can `wait()` on (Promise) or read `.status` from (sync).
234
+ *
235
+ * The driver is a REQUIRED first argument — there is no library-default,
236
+ * to keep the engine free of driver imports and to make the choice of
237
+ * scheduling algorithm explicit at the call site.
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * import { microtaskBatchDriver } from 'footprintjs/detach';
242
+ *
243
+ * const exec = new FlowChartExecutor(parentChart);
244
+ * const handle = exec.detachAndJoinLater(microtaskBatchDriver, telemetryChart, { event: 'x' });
245
+ * await handle.wait(); // optional
246
+ * ```
247
+ */
248
+ detachAndJoinLater(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): import('../detach/types.js').DetachHandle;
249
+ /**
250
+ * Detach a child flowchart on the given driver and DISCARD the handle.
251
+ * Use for telemetry exports / fire-and-forget side effects where the
252
+ * caller doesn't care about the result.
253
+ *
254
+ * Errors raised by the child still land on the (discarded) handle — they
255
+ * go silent unless surfaced through a recorder. For observable detach,
256
+ * prefer `detachAndJoinLater` and surface failures via `.wait().catch()`.
257
+ */
258
+ detachAndForget(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): void;
231
259
  /** Detach all scope Recorders with the given ID. */
232
260
  detachRecorder(id: string): void;
233
261
  /** Returns a defensive copy of attached scope Recorders. */
@@ -100,6 +100,10 @@ export declare class ScopeFacade {
100
100
  * the method routes here via `createTypedScope`.
101
101
  */
102
102
  emitEvent(name: string, payload: unknown): void;
103
+ /** See `ScopeMethods.$detachAndJoinLater`. */
104
+ detachAndJoinLater(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): import('../detach/types.js').DetachHandle;
105
+ /** See `ScopeMethods.$detachAndForget`. */
106
+ detachAndForget(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): void;
103
107
  /**
104
108
  * Build the subflowPath (outer → inner) for event enrichment.
105
109
  *
@@ -6,7 +6,19 @@
6
6
  import { type ZodRecord, type ZodTypeAny } from 'zod';
7
7
  /** Check if the value is a Zod schema node. */
8
8
  export declare function isZodNode(x: unknown): x is ZodTypeAny;
9
- /** Peel wrappers; returns the underlying base Zod node (or null). */
9
+ /** Peel wrappers; returns the underlying base Zod node (or null).
10
+ *
11
+ * Wrapper-aware: only descends through fields that are KNOWN to hold
12
+ * the inner schema for wrapper Zod types (Optional, Default, Nullable,
13
+ * Effects/Pipeline). Notably, `_def.type` is treated as the inner
14
+ * schema ONLY for v3 Effects/Pipeline — it is NOT the inner schema
15
+ * for ZodArray (where `_def.type` holds the ELEMENT schema, which is
16
+ * a separate concern from wrapper unwrapping).
17
+ *
18
+ * Without this gate, `unwrap(z.array(z.string()))` would incorrectly
19
+ * follow `_def.type` and return `ZodString`, breaking array detection
20
+ * in `scopeFactory.analyze()`.
21
+ */
10
22
  export declare function unwrap(schema: ZodTypeAny | null | undefined): ZodTypeAny | null;
11
23
  /** Version-tolerant access to ZodRecord value schema. */
12
24
  export declare function getRecordValueType(rec: ZodRecord<any, any>): ZodTypeAny | null;
@@ -27,6 +27,7 @@ export type { CausalChainOptions, CausalNode, KeysReadLookup } from './lib/memor
27
27
  export { causalChain, flattenCausalDAG, formatCausalChain } from './lib/memory/backtrack.js';
28
28
  export { KeyedRecorder } from './lib/recorder/KeyedRecorder.js';
29
29
  export { SequenceRecorder } from './lib/recorder/SequenceRecorder.js';
30
+ export { BoundaryStateTracker } from './lib/recorder/BoundaryStateTracker.js';
30
31
  export type { Topology, TopologyEdge, TopologyIncomingKind, TopologyNode, TopologyRecorderOptions, } from './lib/recorder/TopologyRecorder.js';
31
32
  export { TopologyRecorder, topologyRecorder } from './lib/recorder/TopologyRecorder.js';
32
33
  export type { InOutEntry, InOutPhase, InOutRecorderOptions } from './lib/recorder/InOutRecorder.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "footprintjs",
3
- "version": "4.16.0",
3
+ "version": "4.17.2",
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",
@@ -88,6 +88,11 @@
88
88
  "types": "./dist/types/trace.d.ts",
89
89
  "import": "./dist/esm/trace.js",
90
90
  "require": "./dist/trace.js"
91
+ },
92
+ "./detach": {
93
+ "types": "./dist/types/detach.d.ts",
94
+ "import": "./dist/esm/detach.js",
95
+ "require": "./dist/detach.js"
91
96
  }
92
97
  },
93
98
  "lint-staged": {