@seanhogg/builderforce-memory 2026.7.14 → 2026.9.28

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 (225) hide show
  1. package/README.md +10 -0
  2. package/dist/bridges/AnthropicBridge.d.ts +9 -1
  3. package/dist/bridges/AnthropicBridge.d.ts.map +1 -1
  4. package/dist/bridges/AnthropicBridge.js +58 -2
  5. package/dist/bridges/AnthropicBridge.js.map +1 -1
  6. package/dist/bridges/CachingBridge.d.ts +9 -1
  7. package/dist/bridges/CachingBridge.d.ts.map +1 -1
  8. package/dist/bridges/CachingBridge.js +24 -1
  9. package/dist/bridges/CachingBridge.js.map +1 -1
  10. package/dist/bridges/OpenAIBridge.d.ts +4 -1
  11. package/dist/bridges/OpenAIBridge.d.ts.map +1 -1
  12. package/dist/bridges/OpenAIBridge.js +40 -2
  13. package/dist/bridges/OpenAIBridge.js.map +1 -1
  14. package/dist/bridges/SemanticCachingBridge.d.ts +8 -1
  15. package/dist/bridges/SemanticCachingBridge.d.ts.map +1 -1
  16. package/dist/bridges/SemanticCachingBridge.js +26 -1
  17. package/dist/bridges/SemanticCachingBridge.js.map +1 -1
  18. package/dist/bridges/TransformerBridge.d.ts +13 -0
  19. package/dist/bridges/TransformerBridge.d.ts.map +1 -1
  20. package/dist/bridges/VertexAIBridge.d.ts +102 -0
  21. package/dist/bridges/VertexAIBridge.d.ts.map +1 -0
  22. package/dist/bridges/VertexAIBridge.js +224 -0
  23. package/dist/bridges/VertexAIBridge.js.map +1 -0
  24. package/dist/bridges/index.d.ts +3 -1
  25. package/dist/bridges/index.d.ts.map +1 -1
  26. package/dist/bridges/index.js +1 -0
  27. package/dist/bridges/index.js.map +1 -1
  28. package/dist/eval/EvalHarness.d.ts +37 -0
  29. package/dist/eval/EvalHarness.d.ts.map +1 -0
  30. package/dist/eval/EvalHarness.js +178 -0
  31. package/dist/eval/EvalHarness.js.map +1 -0
  32. package/dist/eval/graders.d.ts +76 -0
  33. package/dist/eval/graders.d.ts.map +1 -0
  34. package/dist/eval/graders.js +168 -0
  35. package/dist/eval/graders.js.map +1 -0
  36. package/dist/eval/index.d.ts +26 -0
  37. package/dist/eval/index.d.ts.map +1 -0
  38. package/dist/eval/index.js +26 -0
  39. package/dist/eval/index.js.map +1 -0
  40. package/dist/eval/types.d.ts +97 -0
  41. package/dist/eval/types.d.ts.map +1 -0
  42. package/dist/eval/types.js +5 -0
  43. package/dist/eval/types.js.map +1 -0
  44. package/dist/evermind/contract.d.ts +40 -0
  45. package/dist/evermind/contract.d.ts.map +1 -0
  46. package/dist/evermind/contract.js +13 -0
  47. package/dist/evermind/contract.js.map +1 -0
  48. package/dist/evermind/index.d.ts +15 -0
  49. package/dist/evermind/index.d.ts.map +1 -0
  50. package/dist/evermind/index.js +4 -0
  51. package/dist/evermind/index.js.map +1 -0
  52. package/dist/evermind/memoryBlock.d.ts +7 -0
  53. package/dist/evermind/memoryBlock.d.ts.map +1 -0
  54. package/dist/evermind/memoryBlock.js +37 -0
  55. package/dist/evermind/memoryBlock.js.map +1 -0
  56. package/dist/evermind/recall.d.ts +48 -0
  57. package/dist/evermind/recall.d.ts.map +1 -0
  58. package/dist/evermind/recall.js +105 -0
  59. package/dist/evermind/recall.js.map +1 -0
  60. package/dist/index.d.ts +18 -3
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +14 -1
  63. package/dist/index.js.map +1 -1
  64. package/dist/ingest/IngestionPipeline.d.ts +95 -0
  65. package/dist/ingest/IngestionPipeline.d.ts.map +1 -0
  66. package/dist/ingest/IngestionPipeline.js +295 -0
  67. package/dist/ingest/IngestionPipeline.js.map +1 -0
  68. package/dist/ingest/index.d.ts +10 -0
  69. package/dist/ingest/index.d.ts.map +1 -0
  70. package/dist/ingest/index.js +8 -0
  71. package/dist/ingest/index.js.map +1 -0
  72. package/dist/ingest/parsers.d.ts +53 -0
  73. package/dist/ingest/parsers.d.ts.map +1 -0
  74. package/dist/ingest/parsers.js +306 -0
  75. package/dist/ingest/parsers.js.map +1 -0
  76. package/dist/ingest/types.d.ts +114 -0
  77. package/dist/ingest/types.d.ts.map +1 -0
  78. package/dist/ingest/types.js +19 -0
  79. package/dist/ingest/types.js.map +1 -0
  80. package/dist/memory/MemoryStore.d.ts +33 -0
  81. package/dist/memory/MemoryStore.d.ts.map +1 -1
  82. package/dist/memory/MemoryStore.js +29 -3
  83. package/dist/memory/MemoryStore.js.map +1 -1
  84. package/dist/memory/index.d.ts +1 -1
  85. package/dist/memory/index.d.ts.map +1 -1
  86. package/dist/orchestration/AgentGraph.d.ts +104 -0
  87. package/dist/orchestration/AgentGraph.d.ts.map +1 -0
  88. package/dist/orchestration/AgentGraph.js +364 -0
  89. package/dist/orchestration/AgentGraph.js.map +1 -0
  90. package/dist/orchestration/checkpoint.d.ts +58 -0
  91. package/dist/orchestration/checkpoint.d.ts.map +1 -0
  92. package/dist/orchestration/checkpoint.js +92 -0
  93. package/dist/orchestration/checkpoint.js.map +1 -0
  94. package/dist/orchestration/index.d.ts +14 -0
  95. package/dist/orchestration/index.d.ts.map +1 -0
  96. package/dist/orchestration/index.js +10 -0
  97. package/dist/orchestration/index.js.map +1 -0
  98. package/dist/orchestration/patterns.d.ts +159 -0
  99. package/dist/orchestration/patterns.d.ts.map +1 -0
  100. package/dist/orchestration/patterns.js +371 -0
  101. package/dist/orchestration/patterns.js.map +1 -0
  102. package/dist/orchestration/types.d.ts +146 -0
  103. package/dist/orchestration/types.d.ts.map +1 -0
  104. package/dist/orchestration/types.js +20 -0
  105. package/dist/orchestration/types.js.map +1 -0
  106. package/dist/rag/EnterpriseRetriever.d.ts +114 -0
  107. package/dist/rag/EnterpriseRetriever.d.ts.map +1 -0
  108. package/dist/rag/EnterpriseRetriever.js +240 -0
  109. package/dist/rag/EnterpriseRetriever.js.map +1 -0
  110. package/dist/rag/index.d.ts +6 -0
  111. package/dist/rag/index.d.ts.map +1 -0
  112. package/dist/rag/index.js +5 -0
  113. package/dist/rag/index.js.map +1 -0
  114. package/dist/retrieval/bm25.d.ts +48 -1
  115. package/dist/retrieval/bm25.d.ts.map +1 -1
  116. package/dist/retrieval/bm25.js +59 -13
  117. package/dist/retrieval/bm25.js.map +1 -1
  118. package/dist/retrieval/index.d.ts +2 -2
  119. package/dist/retrieval/index.d.ts.map +1 -1
  120. package/dist/retrieval/index.js +1 -1
  121. package/dist/retrieval/index.js.map +1 -1
  122. package/dist/runtime/SSMRuntime.d.ts +1 -1
  123. package/dist/runtime/SSMRuntime.d.ts.map +1 -1
  124. package/dist/telemetry/InstrumentedBridge.d.ts +62 -0
  125. package/dist/telemetry/InstrumentedBridge.d.ts.map +1 -0
  126. package/dist/telemetry/InstrumentedBridge.js +120 -0
  127. package/dist/telemetry/InstrumentedBridge.js.map +1 -0
  128. package/dist/telemetry/MetricsRegistry.d.ts +88 -0
  129. package/dist/telemetry/MetricsRegistry.d.ts.map +1 -0
  130. package/dist/telemetry/MetricsRegistry.js +167 -0
  131. package/dist/telemetry/MetricsRegistry.js.map +1 -0
  132. package/dist/telemetry/Tracer.d.ts +137 -0
  133. package/dist/telemetry/Tracer.d.ts.map +1 -0
  134. package/dist/telemetry/Tracer.js +244 -0
  135. package/dist/telemetry/Tracer.js.map +1 -0
  136. package/dist/telemetry/exporters.d.ts +68 -0
  137. package/dist/telemetry/exporters.d.ts.map +1 -0
  138. package/dist/telemetry/exporters.js +145 -0
  139. package/dist/telemetry/exporters.js.map +1 -0
  140. package/dist/telemetry/index.d.ts +19 -0
  141. package/dist/telemetry/index.d.ts.map +1 -0
  142. package/dist/telemetry/index.js +12 -0
  143. package/dist/telemetry/index.js.map +1 -0
  144. package/dist/telemetry/pricing.d.ts +74 -0
  145. package/dist/telemetry/pricing.d.ts.map +1 -0
  146. package/dist/telemetry/pricing.js +98 -0
  147. package/dist/telemetry/pricing.js.map +1 -0
  148. package/dist/telemetry/traceparent.d.ts +50 -0
  149. package/dist/telemetry/traceparent.d.ts.map +1 -0
  150. package/dist/telemetry/traceparent.js +77 -0
  151. package/dist/telemetry/traceparent.js.map +1 -0
  152. package/dist/telemetry/types.d.ts +90 -0
  153. package/dist/telemetry/types.d.ts.map +1 -0
  154. package/dist/telemetry/types.js +17 -0
  155. package/dist/telemetry/types.js.map +1 -0
  156. package/dist/vectorstore/MemoryVectorStore.d.ts +72 -0
  157. package/dist/vectorstore/MemoryVectorStore.d.ts.map +1 -0
  158. package/dist/vectorstore/MemoryVectorStore.js +205 -0
  159. package/dist/vectorstore/MemoryVectorStore.js.map +1 -0
  160. package/dist/vectorstore/RestVectorStore.d.ts +79 -0
  161. package/dist/vectorstore/RestVectorStore.d.ts.map +1 -0
  162. package/dist/vectorstore/RestVectorStore.js +170 -0
  163. package/dist/vectorstore/RestVectorStore.js.map +1 -0
  164. package/dist/vectorstore/dialects.d.ts +101 -0
  165. package/dist/vectorstore/dialects.d.ts.map +1 -0
  166. package/dist/vectorstore/dialects.js +618 -0
  167. package/dist/vectorstore/dialects.js.map +1 -0
  168. package/dist/vectorstore/filter.d.ts +42 -0
  169. package/dist/vectorstore/filter.d.ts.map +1 -0
  170. package/dist/vectorstore/filter.js +132 -0
  171. package/dist/vectorstore/filter.js.map +1 -0
  172. package/dist/vectorstore/index.d.ts +15 -0
  173. package/dist/vectorstore/index.d.ts.map +1 -0
  174. package/dist/vectorstore/index.js +11 -0
  175. package/dist/vectorstore/index.js.map +1 -0
  176. package/dist/vectorstore/types.d.ts +151 -0
  177. package/dist/vectorstore/types.d.ts.map +1 -0
  178. package/dist/vectorstore/types.js +30 -0
  179. package/dist/vectorstore/types.js.map +1 -0
  180. package/package.json +39 -5
  181. package/src/bridges/AnthropicBridge.ts +68 -3
  182. package/src/bridges/CachingBridge.ts +28 -2
  183. package/src/bridges/OpenAIBridge.ts +48 -3
  184. package/src/bridges/SemanticCachingBridge.ts +29 -2
  185. package/src/bridges/TransformerBridge.ts +16 -0
  186. package/src/bridges/VertexAIBridge.ts +297 -0
  187. package/src/bridges/index.ts +3 -1
  188. package/src/eval/EvalHarness.ts +214 -0
  189. package/src/eval/graders.ts +218 -0
  190. package/src/eval/index.ts +26 -0
  191. package/src/eval/types.ts +107 -0
  192. package/src/evermind/contract.ts +39 -0
  193. package/src/evermind/index.ts +18 -0
  194. package/src/evermind/memoryBlock.ts +38 -0
  195. package/src/evermind/recall.ts +122 -0
  196. package/src/index.ts +232 -0
  197. package/src/ingest/IngestionPipeline.ts +378 -0
  198. package/src/ingest/index.ts +46 -0
  199. package/src/ingest/parsers.ts +318 -0
  200. package/src/ingest/types.ts +116 -0
  201. package/src/memory/MemoryStore.ts +55 -3
  202. package/src/memory/index.ts +1 -1
  203. package/src/orchestration/AgentGraph.ts +462 -0
  204. package/src/orchestration/checkpoint.ts +129 -0
  205. package/src/orchestration/index.ts +63 -0
  206. package/src/orchestration/patterns.ts +532 -0
  207. package/src/orchestration/types.ts +128 -0
  208. package/src/rag/EnterpriseRetriever.ts +348 -0
  209. package/src/rag/index.ts +18 -0
  210. package/src/retrieval/bm25.ts +71 -13
  211. package/src/retrieval/index.ts +2 -2
  212. package/src/telemetry/InstrumentedBridge.ts +152 -0
  213. package/src/telemetry/MetricsRegistry.ts +263 -0
  214. package/src/telemetry/Tracer.ts +310 -0
  215. package/src/telemetry/exporters.ts +187 -0
  216. package/src/telemetry/index.ts +56 -0
  217. package/src/telemetry/pricing.ts +129 -0
  218. package/src/telemetry/traceparent.ts +89 -0
  219. package/src/telemetry/types.ts +105 -0
  220. package/src/vectorstore/MemoryVectorStore.ts +250 -0
  221. package/src/vectorstore/RestVectorStore.ts +236 -0
  222. package/src/vectorstore/dialects.ts +702 -0
  223. package/src/vectorstore/filter.ts +148 -0
  224. package/src/vectorstore/index.ts +53 -0
  225. package/src/vectorstore/types.ts +149 -0
@@ -0,0 +1,462 @@
1
+ /**
2
+ * orchestration/AgentGraph.ts — a stateful multi-agent graph.
3
+ *
4
+ * Execution is a superstep loop: run the current frontier of nodes concurrently,
5
+ * merge their partial updates through the channel reducers, then compute the next
6
+ * frontier from the edges. This is the model LangGraph popularised, and it is the
7
+ * right one for agents because the three hard parts of multi-agent orchestration —
8
+ * concurrent fan-out, deterministic merge, and resumability — all fall out of it:
9
+ *
10
+ * • Fan-out is just a frontier with more than one node.
11
+ * • A merge conflict has a declared answer (the channel's reducer), not a race.
12
+ * • The state between two supersteps is a complete description of the run, so a
13
+ * checkpoint written there can resume it exactly — which is what makes
14
+ * human-in-the-loop approval and crash recovery the same mechanism.
15
+ *
16
+ * Everything is traced: one span per run, one per node, so a model call inside a
17
+ * node nests under the node that made it and cost rolls up per agent.
18
+ */
19
+
20
+ import type { Span, Tracer } from '../telemetry/Tracer.js';
21
+ import {
22
+ END,
23
+ START,
24
+ type ChannelSpecs,
25
+ type Checkpointer,
26
+ type EdgeCondition,
27
+ type GraphEndReason,
28
+ type GraphEvent,
29
+ type GraphRunResult,
30
+ type GraphState,
31
+ type NodeContext,
32
+ type NodeFn,
33
+ } from './types.js';
34
+
35
+ interface NodeEntry<S extends GraphState> {
36
+ name: string;
37
+ fn: NodeFn<S>;
38
+ /** Retries on throw before the node is treated as failed. Default 0. */
39
+ retries: number;
40
+ }
41
+
42
+ interface EdgeEntry<S extends GraphState> {
43
+ from: string;
44
+ /** Static target, or a condition that computes targets from state. */
45
+ to?: string;
46
+ condition?: EdgeCondition<S>;
47
+ /** Maps a condition's return value onto node names. */
48
+ mapping?: Record<string, string>;
49
+ }
50
+
51
+ export interface AgentGraphOptions<S extends GraphState> {
52
+ /** Merge semantics per state key. Keys without a spec are last-write-wins. */
53
+ channels?: ChannelSpecs<S>;
54
+ /**
55
+ * Maximum supersteps before the run is stopped. Default 25. A cyclic graph
56
+ * (which is the point of ReAct and reflection) needs a hard stop, or a model
57
+ * that will not converge becomes an unbounded bill.
58
+ */
59
+ recursionLimit?: number;
60
+ /** Node names to pause BEFORE — the human-in-the-loop approval gate. */
61
+ interruptBefore?: string[];
62
+ /**
63
+ * When true (default), a node that throws after its retries fails the run.
64
+ * Set false for best-effort fan-out where one dead branch is acceptable; the
65
+ * error is recorded on the span and emitted either way.
66
+ */
67
+ failFast?: boolean;
68
+ checkpointer?: Checkpointer<S>;
69
+ tracer?: Tracer;
70
+ /** Name used on the run span. Default `'agent-graph'`. */
71
+ name?: string;
72
+ }
73
+
74
+ export interface RunOptions {
75
+ /** Correlates checkpoints for one conversation/job. Required to checkpoint. */
76
+ threadId?: string;
77
+ /** Resume from this checkpoint instead of starting fresh. */
78
+ checkpointId?: string;
79
+ signal?: AbortSignal;
80
+ /** Parent span, so a graph nested inside another agent nests in the trace. */
81
+ parent?: Span;
82
+ /** Overrides the graph-level limit for this run. */
83
+ recursionLimit?: number;
84
+ }
85
+
86
+ export class AgentGraph<S extends GraphState = GraphState> {
87
+ private readonly _nodes = new Map<string, NodeEntry<S>>();
88
+ private readonly _edges: EdgeEntry<S>[] = [];
89
+ private readonly _channels: ChannelSpecs<S>;
90
+ private readonly _recursionLimit: number;
91
+ private readonly _interruptBefore: Set<string>;
92
+ private readonly _failFast: boolean;
93
+ private readonly _checkpointer: Checkpointer<S> | undefined;
94
+ private readonly _tracer: Tracer | undefined;
95
+ private readonly _name: string;
96
+
97
+ private _entry: string | undefined;
98
+ private _compiled = false;
99
+
100
+ constructor(opts: AgentGraphOptions<S> = {}) {
101
+ this._channels = opts.channels ?? {};
102
+ this._recursionLimit = Math.max(1, opts.recursionLimit ?? 25);
103
+ this._interruptBefore = new Set(opts.interruptBefore ?? []);
104
+ this._failFast = opts.failFast ?? true;
105
+ this._checkpointer = opts.checkpointer;
106
+ this._tracer = opts.tracer;
107
+ this._name = opts.name ?? 'agent-graph';
108
+ }
109
+
110
+ addNode(name: string, fn: NodeFn<S>, opts: { retries?: number } = {}): this {
111
+ if (name === START || name === END) {
112
+ throw new Error(`"${name}" is a reserved node name.`);
113
+ }
114
+ if (this._nodes.has(name)) throw new Error(`Node "${name}" is already defined.`);
115
+ this._nodes.set(name, { name, fn, retries: Math.max(0, opts.retries ?? 0) });
116
+ this._compiled = false;
117
+ return this;
118
+ }
119
+
120
+ addEdge(from: string, to: string): this {
121
+ this._edges.push({ from, to });
122
+ if (from === START) this._entry = to;
123
+ this._compiled = false;
124
+ return this;
125
+ }
126
+
127
+ /**
128
+ * A conditional edge is how delegation, retry-on-failure and "am I done yet"
129
+ * are expressed. `mapping` translates a condition's domain language
130
+ * (`'approve' | 'revise'`) into node names, keeping routing readable.
131
+ */
132
+ addConditionalEdges(
133
+ from: string,
134
+ condition: EdgeCondition<S>,
135
+ mapping?: Record<string, string>,
136
+ ): this {
137
+ const edge: EdgeEntry<S> = { from, condition };
138
+ if (mapping) edge.mapping = mapping;
139
+ this._edges.push(edge);
140
+ this._compiled = false;
141
+ return this;
142
+ }
143
+
144
+ setEntryPoint(name: string): this {
145
+ this._entry = name;
146
+ this._compiled = false;
147
+ return this;
148
+ }
149
+
150
+ /**
151
+ * Validates the graph. Catching a dangling edge here rather than at superstep
152
+ * seven — after six model calls have been billed — is the entire point.
153
+ */
154
+ compile(): this {
155
+ if (!this._entry) throw new Error('Graph has no entry point: call setEntryPoint() or addEdge(START, …).');
156
+ if (!this._nodes.has(this._entry)) throw new Error(`Entry point "${this._entry}" is not a node.`);
157
+
158
+ for (const edge of this._edges) {
159
+ if (edge.from !== START && !this._nodes.has(edge.from)) {
160
+ throw new Error(`Edge source "${edge.from}" is not a node.`);
161
+ }
162
+ if (edge.to && edge.to !== END && !this._nodes.has(edge.to)) {
163
+ throw new Error(`Edge target "${edge.to}" is not a node.`);
164
+ }
165
+ for (const target of Object.values(edge.mapping ?? {})) {
166
+ if (target !== END && !this._nodes.has(target)) {
167
+ throw new Error(`Conditional edge from "${edge.from}" maps to unknown node "${target}".`);
168
+ }
169
+ }
170
+ }
171
+
172
+ for (const name of this._nodes.keys()) {
173
+ const hasOutgoing = this._edges.some((e) => e.from === name);
174
+ if (!hasOutgoing) {
175
+ throw new Error(`Node "${name}" has no outgoing edge; add one to END if it is terminal.`);
176
+ }
177
+ }
178
+
179
+ for (const name of this._interruptBefore) {
180
+ if (!this._nodes.has(name)) throw new Error(`interruptBefore names unknown node "${name}".`);
181
+ }
182
+
183
+ this._compiled = true;
184
+ return this;
185
+ }
186
+
187
+ async invoke(input: Partial<S>, opts: RunOptions = {}): Promise<GraphRunResult<S>> {
188
+ let last: GraphEvent<S> | undefined;
189
+ let result: GraphRunResult<S> | undefined;
190
+ for await (const event of this.stream(input, opts)) {
191
+ last = event;
192
+ if (event.type === 'end') result = this._lastResult;
193
+ }
194
+ if (!result) {
195
+ throw new Error(`Graph run produced no terminal event (last: ${last?.type ?? 'none'}).`);
196
+ }
197
+ return result;
198
+ }
199
+
200
+ private _lastResult: GraphRunResult<S> | undefined;
201
+
202
+ /** Streams execution events — what a live orchestration view renders. */
203
+ async *stream(input: Partial<S>, opts: RunOptions = {}): AsyncIterable<GraphEvent<S>> {
204
+ if (!this._compiled) this.compile();
205
+
206
+ const limit = Math.max(1, opts.recursionLimit ?? this._recursionLimit);
207
+ const runSpan = this._tracer?.startSpan(this._name, {
208
+ kind: 'graph',
209
+ ...(opts.parent ? { parent: opts.parent } : {}),
210
+ attributes: { 'graph.nodes': this._nodes.size, 'graph.recursion_limit': limit },
211
+ });
212
+
213
+ const resumed = opts.threadId && this._checkpointer
214
+ ? await this._checkpointer.load(opts.threadId, opts.checkpointId)
215
+ : undefined;
216
+
217
+ let state: S = resumed ? { ...resumed.state } : this._initialState(input);
218
+ let frontier: string[] = resumed ? [...resumed.frontier] : [this._entry as string];
219
+ const path: string[] = resumed ? [...resumed.path] : [];
220
+ let step = resumed ? resumed.step : 0;
221
+ let reason: GraphEndReason = 'complete';
222
+ let error: string | undefined;
223
+ let checkpointId: string | undefined;
224
+
225
+ try {
226
+ while (frontier.length > 0) {
227
+ if (step >= limit) {
228
+ reason = 'recursion-limit';
229
+ break;
230
+ }
231
+
232
+ const pausing = frontier.filter((n) => this._interruptBefore.has(n));
233
+ if (pausing.length > 0) {
234
+ reason = 'interrupted';
235
+ checkpointId = await this._checkpoint(opts.threadId, step, state, frontier, path);
236
+ yield { type: 'interrupt', step, before: pausing, state };
237
+ break;
238
+ }
239
+
240
+ yield { type: 'step-start', step, frontier: [...frontier] };
241
+
242
+ const pending: Array<{ node: string; update: Partial<S> }> = [];
243
+ const customEvents: Array<{ node: string; event: { type: string; data?: unknown } }> = [];
244
+ const failures: Array<{ node: string; error: string }> = [];
245
+
246
+ // Deterministic order so a fan-out merge reproduces from a checkpoint.
247
+ const running = [...frontier].sort();
248
+ for (const name of running) yield { type: 'node-start', step, node: name };
249
+
250
+ await Promise.all(running.map(async (name) => {
251
+ const entry = this._nodes.get(name) as NodeEntry<S>;
252
+ const nodeSpan = this._tracer?.startSpan(`node.${name}`, {
253
+ kind: 'agent',
254
+ ...(runSpan ? { parent: runSpan } : {}),
255
+ attributes: { 'graph.node': name, 'graph.step': step },
256
+ });
257
+
258
+ const ctx: NodeContext<S> = {
259
+ step,
260
+ ...(nodeSpan ? { span: nodeSpan } : {}),
261
+ ...(opts.signal ? { signal: opts.signal } : {}),
262
+ state,
263
+ emit: (event) => customEvents.push({ node: name, event }),
264
+ };
265
+
266
+ for (let attempt = 0; attempt <= entry.retries; attempt++) {
267
+ try {
268
+ const update = await entry.fn(state, ctx);
269
+ pending.push({ node: name, update: (update ?? {}) as Partial<S> });
270
+ nodeSpan?.setAttribute('graph.attempts', attempt + 1);
271
+ nodeSpan?.end('ok');
272
+ return;
273
+ } catch (err) {
274
+ if (attempt === entry.retries) {
275
+ const message = err instanceof Error ? err.message : String(err);
276
+ failures.push({ node: name, error: message });
277
+ nodeSpan?.fail(err);
278
+ }
279
+ }
280
+ }
281
+ }));
282
+
283
+ for (const { node, event } of customEvents) {
284
+ yield { type: 'custom', step, node, event };
285
+ }
286
+ for (const failure of failures) {
287
+ yield { type: 'node-error', step, node: failure.node, error: failure.error };
288
+ }
289
+
290
+ // Merge in the same sorted order the nodes ran in.
291
+ pending.sort((a, b) => a.node.localeCompare(b.node));
292
+ for (const { node, update } of pending) {
293
+ state = this._merge(state, update);
294
+ path.push(node);
295
+ yield { type: 'node-end', step, node, update };
296
+ }
297
+
298
+ if (failures.length > 0 && this._failFast) {
299
+ reason = 'error';
300
+ error = failures.map((f) => `${f.node}: ${f.error}`).join('; ');
301
+ break;
302
+ }
303
+
304
+ yield { type: 'step-end', step, state };
305
+ checkpointId = await this._checkpoint(opts.threadId, step + 1, state, [], path);
306
+
307
+ const succeeded = new Set(pending.map((p) => p.node));
308
+ frontier = await this._nextFrontier([...succeeded], state, step);
309
+ step += 1;
310
+
311
+ if (opts.signal?.aborted) {
312
+ reason = 'error';
313
+ error = 'Run aborted.';
314
+ break;
315
+ }
316
+ }
317
+
318
+ runSpan?.setAttributes({
319
+ 'graph.steps': step,
320
+ 'graph.reason': reason,
321
+ 'graph.path_length': path.length,
322
+ });
323
+ runSpan?.end(reason === 'error' ? 'error' : 'ok');
324
+
325
+ const result: GraphRunResult<S> = { state, reason, steps: step, path };
326
+ if (runSpan) result.traceId = runSpan.traceId;
327
+ if (checkpointId) result.checkpointId = checkpointId;
328
+ if (error) result.error = error;
329
+ this._lastResult = result;
330
+
331
+ yield { type: 'end', step, state, reason };
332
+ } catch (err) {
333
+ runSpan?.fail(err);
334
+ throw err;
335
+ } finally {
336
+ await this._tracer?.flush();
337
+ }
338
+ }
339
+
340
+ /** Resumes an interrupted run, optionally applying a human's edits to state. */
341
+ async resume(threadId: string, update: Partial<S> = {}, opts: RunOptions = {}): Promise<GraphRunResult<S>> {
342
+ if (!this._checkpointer) throw new Error('resume() requires a checkpointer.');
343
+ const checkpoint = await this._checkpointer.load(threadId, opts.checkpointId);
344
+ if (!checkpoint) throw new Error(`No checkpoint for thread "${threadId}".`);
345
+
346
+ // The human's edit is merged through the SAME reducers a node's update goes
347
+ // through, so approving-with-changes cannot corrupt a channel's invariants.
348
+ const merged = this._merge(checkpoint.state, update);
349
+ await this._checkpointer.save({ ...checkpoint, state: merged });
350
+
351
+ // Clearing interruptBefore for the resumed run is what prevents the graph
352
+ // from pausing again on the very node the human just approved.
353
+ const resumedGraph = this._withoutInterrupts();
354
+ return resumedGraph.invoke({}, { ...opts, threadId, checkpointId: checkpoint.id });
355
+ }
356
+
357
+ private _withoutInterrupts(): AgentGraph<S> {
358
+ const clone = new AgentGraph<S>({
359
+ channels: this._channels,
360
+ recursionLimit: this._recursionLimit,
361
+ failFast: this._failFast,
362
+ ...(this._checkpointer ? { checkpointer: this._checkpointer } : {}),
363
+ ...(this._tracer ? { tracer: this._tracer } : {}),
364
+ name: this._name,
365
+ });
366
+ for (const [name, entry] of this._nodes) clone.addNode(name, entry.fn, { retries: entry.retries });
367
+ for (const edge of this._edges) clone._edges.push(edge);
368
+ clone._entry = this._entry as string;
369
+ return clone.compile();
370
+ }
371
+
372
+ private async _nextFrontier(from: string[], state: S, step: number): Promise<string[]> {
373
+ const next = new Set<string>();
374
+ for (const node of from) {
375
+ for (const edge of this._edges.filter((e) => e.from === node)) {
376
+ if (edge.to) {
377
+ if (edge.to !== END) next.add(edge.to);
378
+ continue;
379
+ }
380
+ const decision = await (edge.condition as EdgeCondition<S>)(state, { step });
381
+ for (const raw of Array.isArray(decision) ? decision : [decision]) {
382
+ const target = edge.mapping?.[raw] ?? raw;
383
+ if (target === END) continue;
384
+ if (!this._nodes.has(target)) {
385
+ throw new Error(`Conditional edge from "${node}" returned unknown target "${target}".`);
386
+ }
387
+ next.add(target);
388
+ }
389
+ }
390
+ }
391
+ return [...next];
392
+ }
393
+
394
+ private _initialState(input: Partial<S>): S {
395
+ const state = {} as S;
396
+ for (const [key, spec] of Object.entries(this._channels) as Array<[keyof S, { default?: () => unknown }]>) {
397
+ if (spec?.default) state[key] = spec.default() as S[keyof S];
398
+ }
399
+ return this._merge(state, input);
400
+ }
401
+
402
+ private _merge(state: S, update: Partial<S>): S {
403
+ const next = { ...state };
404
+ for (const [key, value] of Object.entries(update) as Array<[keyof S, S[keyof S]]>) {
405
+ if (value === undefined) continue;
406
+ const reducer = (this._channels[key] as { reducer?: (c: unknown, u: unknown) => unknown } | undefined)?.reducer;
407
+ next[key] = reducer ? reducer(state[key], value) as S[keyof S] : value;
408
+ }
409
+ return next;
410
+ }
411
+
412
+ private async _checkpoint(
413
+ threadId: string | undefined,
414
+ step: number,
415
+ state: S,
416
+ frontier: string[],
417
+ path: string[],
418
+ ): Promise<string | undefined> {
419
+ if (!threadId || !this._checkpointer) return undefined;
420
+ const id = `${threadId}:${step}`;
421
+ await this._checkpointer.save({
422
+ id, threadId, step, state,
423
+ frontier: [...frontier],
424
+ path: [...path],
425
+ createdAt: Date.now(),
426
+ });
427
+ return id;
428
+ }
429
+ }
430
+
431
+ // ── channel reducers ──────────────────────────────────────────────────────────
432
+
433
+ /** Appends to an array channel — the merge every message log wants. */
434
+ export function appendReducer<T>(current: T[] | undefined, update: T[] | T): T[] {
435
+ const additions = Array.isArray(update) ? update : [update];
436
+ return [...(current ?? []), ...additions];
437
+ }
438
+
439
+ /** Shallow-merges object channels instead of replacing them. */
440
+ export function mergeReducer<T extends Record<string, unknown>>(current: T | undefined, update: T): T {
441
+ return { ...(current ?? {} as T), ...update };
442
+ }
443
+
444
+ /** Sums numeric channels — how a run counts tokens or tool calls across agents. */
445
+ export function sumReducer(current: number | undefined, update: number): number {
446
+ return (current ?? 0) + update;
447
+ }
448
+
449
+ /** Set-union for array channels, preserving first-seen order. */
450
+ export function unionReducer<T>(current: T[] | undefined, update: T[] | T): T[] {
451
+ const additions = Array.isArray(update) ? update : [update];
452
+ const seen = new Set(current ?? []);
453
+ const out = [...(current ?? [])];
454
+ for (const item of additions) {
455
+ if (seen.has(item)) continue;
456
+ seen.add(item);
457
+ out.push(item);
458
+ }
459
+ return out;
460
+ }
461
+
462
+ export { START, END };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * orchestration/checkpoint.ts — durable state between supersteps.
3
+ *
4
+ * Checkpointing is what turns three separate features into one mechanism:
5
+ * crash recovery, human-in-the-loop approval, and time-travel debugging. All three
6
+ * are "load the state as of superstep N and continue", so they share an
7
+ * implementation rather than each growing their own.
8
+ */
9
+
10
+ import type { Checkpoint, Checkpointer, GraphState } from './types.js';
11
+
12
+ export interface InMemoryCheckpointerOptions {
13
+ /**
14
+ * Checkpoints retained per thread, newest kept. Default 50 — bounded, because
15
+ * an unbounded history in a long-lived process is a memory leak with a nice name.
16
+ */
17
+ maxPerThread?: number;
18
+ }
19
+
20
+ export class InMemoryCheckpointer<S extends GraphState = GraphState> implements Checkpointer<S> {
21
+ private readonly _threads = new Map<string, Checkpoint<S>[]>();
22
+ private readonly _max: number;
23
+
24
+ constructor(opts: InMemoryCheckpointerOptions = {}) {
25
+ this._max = Math.max(1, opts.maxPerThread ?? 50);
26
+ }
27
+
28
+ async save(checkpoint: Checkpoint<S>): Promise<void> {
29
+ const history = this._threads.get(checkpoint.threadId) ?? [];
30
+ // Same id means the same superstep was re-entered (a resumed run): replace
31
+ // rather than append, so history stays a line and not a tangle.
32
+ const existing = history.findIndex((c) => c.id === checkpoint.id);
33
+ if (existing >= 0) history[existing] = checkpoint;
34
+ else history.push(checkpoint);
35
+
36
+ history.sort((a, b) => a.step - b.step);
37
+ if (history.length > this._max) history.splice(0, history.length - this._max);
38
+ this._threads.set(checkpoint.threadId, history);
39
+ }
40
+
41
+ async load(threadId: string, checkpointId?: string): Promise<Checkpoint<S> | undefined> {
42
+ const history = this._threads.get(threadId);
43
+ if (!history || history.length === 0) return undefined;
44
+ if (checkpointId) return history.find((c) => c.id === checkpointId);
45
+ return history[history.length - 1];
46
+ }
47
+
48
+ async list(threadId: string): Promise<Checkpoint<S>[]> {
49
+ return [...(this._threads.get(threadId) ?? [])].reverse();
50
+ }
51
+
52
+ async clear(threadId: string): Promise<void> {
53
+ this._threads.delete(threadId);
54
+ }
55
+
56
+ get threadCount(): number { return this._threads.size; }
57
+ }
58
+
59
+ /**
60
+ * Persists checkpoints through an injected key-value store.
61
+ *
62
+ * The `KeyValueLike` shape is deliberately the smallest common denominator of
63
+ * Cloudflare KV, Redis, Firestore-as-KV and a plain `Map`, so a deployment binds
64
+ * its own storage without this package taking a dependency on any of them.
65
+ */
66
+ export interface KeyValueLike {
67
+ get(key: string): Promise<string | null | undefined>;
68
+ put(key: string, value: string): Promise<void>;
69
+ delete(key: string): Promise<void>;
70
+ }
71
+
72
+ export interface KeyValueCheckpointerOptions {
73
+ kv: KeyValueLike;
74
+ /** Key prefix. Default `'evermind:ckpt:'`. */
75
+ prefix?: string;
76
+ /** Checkpoints retained per thread. Default 50. */
77
+ maxPerThread?: number;
78
+ }
79
+
80
+ export class KeyValueCheckpointer<S extends GraphState = GraphState> implements Checkpointer<S> {
81
+ private readonly _kv: KeyValueLike;
82
+ private readonly _prefix: string;
83
+ private readonly _max: number;
84
+
85
+ constructor(opts: KeyValueCheckpointerOptions) {
86
+ this._kv = opts.kv;
87
+ this._prefix = opts.prefix ?? 'evermind:ckpt:';
88
+ this._max = Math.max(1, opts.maxPerThread ?? 50);
89
+ }
90
+
91
+ async save(checkpoint: Checkpoint<S>): Promise<void> {
92
+ const history = await this.list(checkpoint.threadId);
93
+ const without = history.filter((c) => c.id !== checkpoint.id);
94
+ const next = [...without, checkpoint]
95
+ .sort((a, b) => a.step - b.step)
96
+ .slice(-this._max);
97
+ // One key per thread rather than one per checkpoint: a thread's history is
98
+ // always read whole, and a single round trip beats a list-then-fan-out.
99
+ await this._kv.put(this._key(checkpoint.threadId), JSON.stringify(next));
100
+ }
101
+
102
+ async load(threadId: string, checkpointId?: string): Promise<Checkpoint<S> | undefined> {
103
+ const history = await this.list(threadId);
104
+ if (history.length === 0) return undefined;
105
+ return checkpointId ? history.find((c) => c.id === checkpointId) : history[0];
106
+ }
107
+
108
+ /** Newest first. */
109
+ async list(threadId: string): Promise<Checkpoint<S>[]> {
110
+ const raw = await this._kv.get(this._key(threadId));
111
+ if (!raw) return [];
112
+ try {
113
+ const parsed = JSON.parse(raw) as Checkpoint<S>[];
114
+ return Array.isArray(parsed) ? [...parsed].sort((a, b) => b.step - a.step) : [];
115
+ } catch {
116
+ // A corrupt entry must not wedge the thread forever; treat it as empty
117
+ // so the next run rebuilds rather than throwing on every resume.
118
+ return [];
119
+ }
120
+ }
121
+
122
+ async clear(threadId: string): Promise<void> {
123
+ await this._kv.delete(this._key(threadId));
124
+ }
125
+
126
+ private _key(threadId: string): string {
127
+ return `${this._prefix}${threadId}`;
128
+ }
129
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * orchestration — stateful multi-agent graphs (supersteps, declared channel
3
+ * merges, checkpointing) plus the three patterns enterprise work reduces to:
4
+ * ReAct, self-reflection, and hierarchical delegation.
5
+ */
6
+
7
+ export {
8
+ AgentGraph,
9
+ appendReducer,
10
+ mergeReducer,
11
+ sumReducer,
12
+ unionReducer,
13
+ } from './AgentGraph.js';
14
+ export type { AgentGraphOptions, RunOptions } from './AgentGraph.js';
15
+
16
+ export { InMemoryCheckpointer, KeyValueCheckpointer } from './checkpoint.js';
17
+ export type {
18
+ InMemoryCheckpointerOptions,
19
+ KeyValueCheckpointerOptions,
20
+ KeyValueLike,
21
+ } from './checkpoint.js';
22
+
23
+ export {
24
+ createReactAgent,
25
+ createReflectionAgent,
26
+ createSupervisor,
27
+ asWorker,
28
+ parseReactTurn,
29
+ renderToolCatalog,
30
+ renderTranscript,
31
+ REACT_PROTOCOL,
32
+ DEFAULT_CRITIC_PROMPT,
33
+ FINISH,
34
+ } from './patterns.js';
35
+ export type {
36
+ AgentTool,
37
+ AgentTurn,
38
+ AgentTurnRole,
39
+ ParsedAction,
40
+ ReactAgentOptions,
41
+ ReactState,
42
+ ReflectionAgentOptions,
43
+ ReflectionState,
44
+ SupervisorOptions,
45
+ SupervisorState,
46
+ Worker,
47
+ } from './patterns.js';
48
+
49
+ export { START, END } from './types.js';
50
+ export type {
51
+ ChannelReducer,
52
+ ChannelSpec,
53
+ ChannelSpecs,
54
+ Checkpoint,
55
+ Checkpointer,
56
+ EdgeCondition,
57
+ GraphEndReason,
58
+ GraphEvent,
59
+ GraphRunResult,
60
+ GraphState,
61
+ NodeContext,
62
+ NodeFn,
63
+ } from './types.js';