@wundr.io/langgraph-orchestrator 1.0.3
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/README.md +842 -0
- package/dist/checkpointing.d.ts +265 -0
- package/dist/checkpointing.d.ts.map +1 -0
- package/dist/checkpointing.js +577 -0
- package/dist/checkpointing.js.map +1 -0
- package/dist/edges/conditional-edge.d.ts +230 -0
- package/dist/edges/conditional-edge.d.ts.map +1 -0
- package/dist/edges/conditional-edge.js +439 -0
- package/dist/edges/conditional-edge.js.map +1 -0
- package/dist/edges/loop-edge.d.ts +290 -0
- package/dist/edges/loop-edge.d.ts.map +1 -0
- package/dist/edges/loop-edge.js +503 -0
- package/dist/edges/loop-edge.js.map +1 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +269 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/decision-node.d.ts +276 -0
- package/dist/nodes/decision-node.d.ts.map +1 -0
- package/dist/nodes/decision-node.js +403 -0
- package/dist/nodes/decision-node.js.map +1 -0
- package/dist/nodes/human-node.d.ts +272 -0
- package/dist/nodes/human-node.d.ts.map +1 -0
- package/dist/nodes/human-node.js +394 -0
- package/dist/nodes/human-node.js.map +1 -0
- package/dist/nodes/llm-node.d.ts +173 -0
- package/dist/nodes/llm-node.d.ts.map +1 -0
- package/dist/nodes/llm-node.js +325 -0
- package/dist/nodes/llm-node.js.map +1 -0
- package/dist/nodes/tool-node.d.ts +151 -0
- package/dist/nodes/tool-node.d.ts.map +1 -0
- package/dist/nodes/tool-node.js +373 -0
- package/dist/nodes/tool-node.js.map +1 -0
- package/dist/prebuilt-graphs/plan-execute-refine.d.ts +149 -0
- package/dist/prebuilt-graphs/plan-execute-refine.d.ts.map +1 -0
- package/dist/prebuilt-graphs/plan-execute-refine.js +600 -0
- package/dist/prebuilt-graphs/plan-execute-refine.js.map +1 -0
- package/dist/state-graph.d.ts +158 -0
- package/dist/state-graph.d.ts.map +1 -0
- package/dist/state-graph.js +756 -0
- package/dist/state-graph.js.map +1 -0
- package/dist/types.d.ts +762 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +73 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
- package/src/checkpointing.ts +702 -0
- package/src/edges/conditional-edge.ts +518 -0
- package/src/edges/loop-edge.ts +623 -0
- package/src/index.ts +416 -0
- package/src/nodes/decision-node.ts +538 -0
- package/src/nodes/human-node.ts +572 -0
- package/src/nodes/llm-node.ts +448 -0
- package/src/nodes/tool-node.ts +525 -0
- package/src/prebuilt-graphs/plan-execute-refine.ts +769 -0
- package/src/state-graph.ts +990 -0
- package/src/types.ts +729 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* StateGraph - Core class for defining workflow graphs with nodes and edges
|
|
4
|
+
* @module @wundr.io/langgraph-orchestrator
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.StateGraph = void 0;
|
|
8
|
+
const eventemitter3_1 = require("eventemitter3");
|
|
9
|
+
const uuid_1 = require("uuid");
|
|
10
|
+
/**
|
|
11
|
+
* Default graph configuration
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_CONFIG = {
|
|
14
|
+
maxIterations: 100,
|
|
15
|
+
timeout: 300000, // 5 minutes
|
|
16
|
+
checkpointEnabled: true,
|
|
17
|
+
checkpointInterval: 1,
|
|
18
|
+
parallelExecution: false,
|
|
19
|
+
retry: {
|
|
20
|
+
maxRetries: 3,
|
|
21
|
+
initialDelay: 1000,
|
|
22
|
+
backoffMultiplier: 2,
|
|
23
|
+
maxDelay: 30000,
|
|
24
|
+
retryableErrors: ['TIMEOUT', 'RATE_LIMIT', 'NETWORK_ERROR'],
|
|
25
|
+
},
|
|
26
|
+
logLevel: 'info',
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Console-based logger implementation
|
|
30
|
+
*/
|
|
31
|
+
class ConsoleLogger {
|
|
32
|
+
level;
|
|
33
|
+
constructor(level) {
|
|
34
|
+
this.level = level;
|
|
35
|
+
}
|
|
36
|
+
shouldLog(msgLevel) {
|
|
37
|
+
const levels = ['debug', 'info', 'warn', 'error', 'silent'];
|
|
38
|
+
return levels.indexOf(msgLevel) >= levels.indexOf(this.level);
|
|
39
|
+
}
|
|
40
|
+
debug(message, data) {
|
|
41
|
+
if (this.shouldLog('debug')) {
|
|
42
|
+
// Using process.stderr for logging to avoid polluting stdout
|
|
43
|
+
process.stderr.write(`[DEBUG] ${message} ${data ? JSON.stringify(data) : ''}\n`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
info(message, data) {
|
|
47
|
+
if (this.shouldLog('info')) {
|
|
48
|
+
// Using process.stderr for logging to avoid polluting stdout
|
|
49
|
+
process.stderr.write(`[INFO] ${message} ${data ? JSON.stringify(data) : ''}\n`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
warn(message, data) {
|
|
53
|
+
if (this.shouldLog('warn')) {
|
|
54
|
+
console.warn(`[WARN] ${message}`, data ?? '');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
error(message, data) {
|
|
58
|
+
if (this.shouldLog('error')) {
|
|
59
|
+
console.error(`[ERROR] ${message}`, data ?? '');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* StateGraph class for defining and executing workflow graphs
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const graph = new StateGraph('my-workflow')
|
|
69
|
+
* .addNode('start', {
|
|
70
|
+
* id: 'start',
|
|
71
|
+
* name: 'Start Node',
|
|
72
|
+
* type: 'start',
|
|
73
|
+
* config: {},
|
|
74
|
+
* execute: async (state) => ({ state, next: 'process' })
|
|
75
|
+
* })
|
|
76
|
+
* .addNode('process', {
|
|
77
|
+
* id: 'process',
|
|
78
|
+
* name: 'Process Node',
|
|
79
|
+
* type: 'transform',
|
|
80
|
+
* config: {},
|
|
81
|
+
* execute: async (state) => ({ state, next: 'end' })
|
|
82
|
+
* })
|
|
83
|
+
* .addEdge('start', 'process')
|
|
84
|
+
* .setEntryPoint('start');
|
|
85
|
+
*
|
|
86
|
+
* const result = await graph.execute();
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
class StateGraph extends eventemitter3_1.EventEmitter {
|
|
90
|
+
id;
|
|
91
|
+
name;
|
|
92
|
+
description;
|
|
93
|
+
nodes = new Map();
|
|
94
|
+
edges = new Map();
|
|
95
|
+
entryPoint;
|
|
96
|
+
config;
|
|
97
|
+
_checkpointer;
|
|
98
|
+
services = {};
|
|
99
|
+
logger;
|
|
100
|
+
/** Get the checkpointer */
|
|
101
|
+
get checkpointer() {
|
|
102
|
+
return this._checkpointer;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Create a new StateGraph
|
|
106
|
+
* @param name - Human-readable name for the graph
|
|
107
|
+
* @param config - Optional configuration overrides
|
|
108
|
+
*/
|
|
109
|
+
constructor(name, config) {
|
|
110
|
+
super();
|
|
111
|
+
this.id = (0, uuid_1.v4)();
|
|
112
|
+
this.name = name;
|
|
113
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
114
|
+
this.logger = new ConsoleLogger(this.config.logLevel);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Add a node to the graph
|
|
118
|
+
* @param name - Unique name for the node
|
|
119
|
+
* @param definition - Node definition
|
|
120
|
+
* @returns this for chaining
|
|
121
|
+
*/
|
|
122
|
+
addNode(name, definition) {
|
|
123
|
+
if (this.nodes.has(name)) {
|
|
124
|
+
throw new Error(`Node "${name}" already exists in graph "${this.name}"`);
|
|
125
|
+
}
|
|
126
|
+
this.nodes.set(name, definition);
|
|
127
|
+
this.edges.set(name, []);
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Add a direct edge between nodes
|
|
132
|
+
* @param from - Source node name
|
|
133
|
+
* @param to - Target node name
|
|
134
|
+
* @returns this for chaining
|
|
135
|
+
*/
|
|
136
|
+
addEdge(from, to) {
|
|
137
|
+
this.validateNodeExists(from);
|
|
138
|
+
this.validateNodeExists(to);
|
|
139
|
+
const edge = {
|
|
140
|
+
from,
|
|
141
|
+
to,
|
|
142
|
+
type: 'direct',
|
|
143
|
+
};
|
|
144
|
+
const edges = this.edges.get(from) ?? [];
|
|
145
|
+
edges.push(edge);
|
|
146
|
+
this.edges.set(from, edges);
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Add a conditional edge
|
|
151
|
+
* @param from - Source node name
|
|
152
|
+
* @param to - Target node name
|
|
153
|
+
* @param condition - Condition for traversal
|
|
154
|
+
* @returns this for chaining
|
|
155
|
+
*/
|
|
156
|
+
addConditionalEdge(from, to, condition) {
|
|
157
|
+
this.validateNodeExists(from);
|
|
158
|
+
this.validateNodeExists(to);
|
|
159
|
+
const edge = {
|
|
160
|
+
from,
|
|
161
|
+
to,
|
|
162
|
+
type: 'conditional',
|
|
163
|
+
condition,
|
|
164
|
+
};
|
|
165
|
+
const edges = this.edges.get(from) ?? [];
|
|
166
|
+
edges.push(edge);
|
|
167
|
+
this.edges.set(from, edges);
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Add a loop edge that can cycle back
|
|
172
|
+
* @param from - Source node name
|
|
173
|
+
* @param to - Target node name (can be same as from)
|
|
174
|
+
* @param condition - Condition to continue looping
|
|
175
|
+
* @returns this for chaining
|
|
176
|
+
*/
|
|
177
|
+
addLoopEdge(from, to, condition) {
|
|
178
|
+
this.validateNodeExists(from);
|
|
179
|
+
this.validateNodeExists(to);
|
|
180
|
+
const edge = {
|
|
181
|
+
from,
|
|
182
|
+
to,
|
|
183
|
+
type: 'loop',
|
|
184
|
+
condition,
|
|
185
|
+
};
|
|
186
|
+
const edges = this.edges.get(from) ?? [];
|
|
187
|
+
edges.push(edge);
|
|
188
|
+
this.edges.set(from, edges);
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Add parallel edges to multiple targets
|
|
193
|
+
* @param from - Source node name
|
|
194
|
+
* @param targets - Target node names
|
|
195
|
+
* @returns this for chaining
|
|
196
|
+
*/
|
|
197
|
+
addParallelEdges(from, targets) {
|
|
198
|
+
this.validateNodeExists(from);
|
|
199
|
+
targets.forEach(t => this.validateNodeExists(t));
|
|
200
|
+
for (const to of targets) {
|
|
201
|
+
const edge = {
|
|
202
|
+
from,
|
|
203
|
+
to,
|
|
204
|
+
type: 'parallel',
|
|
205
|
+
};
|
|
206
|
+
const edges = this.edges.get(from) ?? [];
|
|
207
|
+
edges.push(edge);
|
|
208
|
+
this.edges.set(from, edges);
|
|
209
|
+
}
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Set the entry point node
|
|
214
|
+
* @param nodeName - Name of the entry point node
|
|
215
|
+
* @returns this for chaining
|
|
216
|
+
*/
|
|
217
|
+
setEntryPoint(nodeName) {
|
|
218
|
+
this.validateNodeExists(nodeName);
|
|
219
|
+
this.entryPoint = nodeName;
|
|
220
|
+
return this;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Set the checkpointer for state persistence
|
|
224
|
+
* @param checkpointer - Checkpointer implementation
|
|
225
|
+
* @returns this for chaining
|
|
226
|
+
*/
|
|
227
|
+
setCheckpointer(checkpointer) {
|
|
228
|
+
this._checkpointer = checkpointer;
|
|
229
|
+
this.services = { ...this.services, checkpointer };
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Set services available to nodes
|
|
234
|
+
* @param services - Services to provide
|
|
235
|
+
* @returns this for chaining
|
|
236
|
+
*/
|
|
237
|
+
setServices(services) {
|
|
238
|
+
this.services = { ...this.services, ...services };
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get the graph configuration
|
|
243
|
+
* @returns GraphConfig object
|
|
244
|
+
*/
|
|
245
|
+
getConfig() {
|
|
246
|
+
if (!this.entryPoint) {
|
|
247
|
+
throw new Error('Entry point not set. Call setEntryPoint() before getConfig()');
|
|
248
|
+
}
|
|
249
|
+
// The nodes Map uses TState-specific NodeDefinition, but GraphConfig uses the base type.
|
|
250
|
+
// This cast is safe because GraphConfig is used for inspection/serialization,
|
|
251
|
+
// while actual execution uses the strongly-typed internal this.nodes Map.
|
|
252
|
+
const typedNodes = new Map();
|
|
253
|
+
for (const [key, value] of this.nodes) {
|
|
254
|
+
// Cast through unknown is necessary here because NodeDefinition<TState>
|
|
255
|
+
// has contravariant parameter in execute function signature
|
|
256
|
+
typedNodes.set(key, value);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
id: this.id,
|
|
260
|
+
name: this.name,
|
|
261
|
+
description: this.description,
|
|
262
|
+
entryPoint: this.entryPoint,
|
|
263
|
+
nodes: typedNodes,
|
|
264
|
+
edges: new Map(this.edges),
|
|
265
|
+
config: this.config,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Execute the graph
|
|
270
|
+
* @param options - Execution options
|
|
271
|
+
* @returns Execution result
|
|
272
|
+
*/
|
|
273
|
+
async execute(options) {
|
|
274
|
+
if (!this.entryPoint) {
|
|
275
|
+
throw new Error('Entry point not set. Call setEntryPoint() before execute()');
|
|
276
|
+
}
|
|
277
|
+
const executionId = (0, uuid_1.v4)();
|
|
278
|
+
const startTime = Date.now();
|
|
279
|
+
const path = [];
|
|
280
|
+
let nodesExecuted = 0;
|
|
281
|
+
let iterations = 0;
|
|
282
|
+
let tokensUsed = 0;
|
|
283
|
+
let checkpointsCreated = 0;
|
|
284
|
+
let retries = 0;
|
|
285
|
+
// Initialize or restore state
|
|
286
|
+
let state = await this.initializeState(executionId, options);
|
|
287
|
+
this.emit('execution:start', state);
|
|
288
|
+
options?.handlers?.onStart?.(state);
|
|
289
|
+
let currentNode = options?.resumeFrom
|
|
290
|
+
? await this.getNodeFromCheckpoint(options.resumeFrom)
|
|
291
|
+
: this.entryPoint;
|
|
292
|
+
try {
|
|
293
|
+
while (currentNode && iterations < this.config.maxIterations) {
|
|
294
|
+
iterations++;
|
|
295
|
+
const node = this.nodes.get(currentNode);
|
|
296
|
+
if (!node) {
|
|
297
|
+
throw this.createError('NODE_NOT_FOUND', `Node "${currentNode}" not found`);
|
|
298
|
+
}
|
|
299
|
+
path.push(currentNode);
|
|
300
|
+
this.logger.debug(`Entering node: ${currentNode}`, {
|
|
301
|
+
iteration: iterations,
|
|
302
|
+
});
|
|
303
|
+
this.emit('node:enter', currentNode, state);
|
|
304
|
+
options?.handlers?.onNodeEnter?.(currentNode, state);
|
|
305
|
+
// Execute node with retry logic
|
|
306
|
+
const result = await this.executeNodeWithRetry(node, state, executionId, iterations);
|
|
307
|
+
nodesExecuted++;
|
|
308
|
+
if (result.metadata?.tokensUsed) {
|
|
309
|
+
tokensUsed += result.metadata.tokensUsed;
|
|
310
|
+
}
|
|
311
|
+
if (result.metadata?.retryCount) {
|
|
312
|
+
retries += result.metadata.retryCount;
|
|
313
|
+
}
|
|
314
|
+
// Update state with history
|
|
315
|
+
state = this.updateStateWithHistory(result.state, currentNode, state);
|
|
316
|
+
this.emit('node:exit', currentNode, result);
|
|
317
|
+
this.emit('state:updated', state);
|
|
318
|
+
options?.handlers?.onNodeExit?.(currentNode, result);
|
|
319
|
+
// Create checkpoint if enabled
|
|
320
|
+
if (this.config.checkpointEnabled &&
|
|
321
|
+
iterations % this.config.checkpointInterval === 0 &&
|
|
322
|
+
this.checkpointer) {
|
|
323
|
+
const checkpoint = await this.createCheckpoint(state, currentNode, executionId, iterations);
|
|
324
|
+
checkpointsCreated++;
|
|
325
|
+
this.emit('checkpoint:created', checkpoint);
|
|
326
|
+
options?.handlers?.onCheckpoint?.(checkpoint);
|
|
327
|
+
}
|
|
328
|
+
// Check for termination
|
|
329
|
+
if (result.terminate) {
|
|
330
|
+
this.logger.info('Workflow terminated by node', {
|
|
331
|
+
node: currentNode,
|
|
332
|
+
});
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
// Determine next node
|
|
336
|
+
const nextNode = await this.determineNextNode(currentNode, result, state);
|
|
337
|
+
currentNode = nextNode ?? '';
|
|
338
|
+
// Check abort signal
|
|
339
|
+
if (options?.signal?.aborted) {
|
|
340
|
+
throw this.createError('ABORTED', 'Execution was aborted');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (iterations >= this.config.maxIterations) {
|
|
344
|
+
throw this.createError('MAX_ITERATIONS', `Exceeded maximum iterations (${this.config.maxIterations})`);
|
|
345
|
+
}
|
|
346
|
+
const duration = Date.now() - startTime;
|
|
347
|
+
const stats = {
|
|
348
|
+
duration,
|
|
349
|
+
nodesExecuted,
|
|
350
|
+
iterations,
|
|
351
|
+
tokensUsed,
|
|
352
|
+
checkpointsCreated,
|
|
353
|
+
retries,
|
|
354
|
+
};
|
|
355
|
+
const result = {
|
|
356
|
+
state,
|
|
357
|
+
success: true,
|
|
358
|
+
stats,
|
|
359
|
+
path,
|
|
360
|
+
};
|
|
361
|
+
this.emit('execution:complete', result);
|
|
362
|
+
options?.handlers?.onComplete?.(state);
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
const workflowError = this.normalizeError(error, currentNode ?? 'unknown');
|
|
367
|
+
state = {
|
|
368
|
+
...state,
|
|
369
|
+
error: workflowError,
|
|
370
|
+
updatedAt: new Date(),
|
|
371
|
+
};
|
|
372
|
+
this.emit('execution:error', workflowError);
|
|
373
|
+
options?.handlers?.onError?.(workflowError);
|
|
374
|
+
const duration = Date.now() - startTime;
|
|
375
|
+
return {
|
|
376
|
+
state,
|
|
377
|
+
success: false,
|
|
378
|
+
error: workflowError,
|
|
379
|
+
stats: {
|
|
380
|
+
duration,
|
|
381
|
+
nodesExecuted,
|
|
382
|
+
iterations,
|
|
383
|
+
tokensUsed,
|
|
384
|
+
checkpointsCreated,
|
|
385
|
+
retries,
|
|
386
|
+
},
|
|
387
|
+
path,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Compile the graph into an executable form
|
|
393
|
+
* @returns Compiled graph configuration
|
|
394
|
+
*/
|
|
395
|
+
compile() {
|
|
396
|
+
// Validate graph structure
|
|
397
|
+
this.validateGraph();
|
|
398
|
+
return this.getConfig();
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Create a subgraph from a subset of nodes
|
|
402
|
+
* @param nodeNames - Names of nodes to include
|
|
403
|
+
* @returns New StateGraph containing the subgraph
|
|
404
|
+
*/
|
|
405
|
+
subgraph(nodeNames) {
|
|
406
|
+
const sub = new StateGraph(`${this.name}-subgraph`, this.config);
|
|
407
|
+
for (const name of nodeNames) {
|
|
408
|
+
const node = this.nodes.get(name);
|
|
409
|
+
if (node) {
|
|
410
|
+
sub.addNode(name, node);
|
|
411
|
+
// Add edges that connect nodes within the subgraph
|
|
412
|
+
const edges = this.edges.get(name) ?? [];
|
|
413
|
+
for (const edge of edges) {
|
|
414
|
+
if (nodeNames.includes(edge.to)) {
|
|
415
|
+
const existing = sub.edges.get(name) ?? [];
|
|
416
|
+
existing.push(edge);
|
|
417
|
+
sub.edges.set(name, existing);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return sub;
|
|
423
|
+
}
|
|
424
|
+
// Private helper methods
|
|
425
|
+
validateNodeExists(nodeName) {
|
|
426
|
+
if (!this.nodes.has(nodeName)) {
|
|
427
|
+
throw new Error(`Node "${nodeName}" does not exist in graph "${this.name}"`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
validateGraph() {
|
|
431
|
+
if (!this.entryPoint) {
|
|
432
|
+
throw new Error('Graph validation failed: No entry point defined');
|
|
433
|
+
}
|
|
434
|
+
if (this.nodes.size === 0) {
|
|
435
|
+
throw new Error('Graph validation failed: No nodes defined');
|
|
436
|
+
}
|
|
437
|
+
// Check all edge targets exist
|
|
438
|
+
for (const [source, edges] of this.edges) {
|
|
439
|
+
for (const edge of edges) {
|
|
440
|
+
if (!this.nodes.has(edge.to)) {
|
|
441
|
+
throw new Error(`Graph validation failed: Edge from "${source}" references non-existent node "${edge.to}"`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Check entry point is reachable
|
|
446
|
+
if (!this.nodes.has(this.entryPoint)) {
|
|
447
|
+
throw new Error(`Graph validation failed: Entry point "${this.entryPoint}" does not exist`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async initializeState(executionId, options) {
|
|
451
|
+
const now = new Date();
|
|
452
|
+
const metadata = {
|
|
453
|
+
sessionId: (0, uuid_1.v4)(),
|
|
454
|
+
executionId,
|
|
455
|
+
stepCount: 0,
|
|
456
|
+
tags: [],
|
|
457
|
+
};
|
|
458
|
+
const baseState = {
|
|
459
|
+
id: (0, uuid_1.v4)(),
|
|
460
|
+
messages: [],
|
|
461
|
+
data: {},
|
|
462
|
+
currentStep: this.entryPoint ?? '',
|
|
463
|
+
history: [],
|
|
464
|
+
createdAt: now,
|
|
465
|
+
updatedAt: now,
|
|
466
|
+
metadata,
|
|
467
|
+
...options?.initialState,
|
|
468
|
+
};
|
|
469
|
+
return baseState;
|
|
470
|
+
}
|
|
471
|
+
async getNodeFromCheckpoint(checkpointId) {
|
|
472
|
+
if (!this.checkpointer) {
|
|
473
|
+
throw new Error('Checkpointer not configured');
|
|
474
|
+
}
|
|
475
|
+
const checkpoint = await this.checkpointer.load(checkpointId);
|
|
476
|
+
if (!checkpoint) {
|
|
477
|
+
throw new Error(`Checkpoint "${checkpointId}" not found`);
|
|
478
|
+
}
|
|
479
|
+
return checkpoint.nodeName;
|
|
480
|
+
}
|
|
481
|
+
async executeNodeWithRetry(node, state, executionId, iterationCount) {
|
|
482
|
+
const retryConfig = { ...this.config.retry, ...node.config.retry };
|
|
483
|
+
let lastError = null;
|
|
484
|
+
let retryCount = 0;
|
|
485
|
+
const context = {
|
|
486
|
+
node: node,
|
|
487
|
+
graph: this.getConfig(),
|
|
488
|
+
executionId,
|
|
489
|
+
iterationCount,
|
|
490
|
+
services: {
|
|
491
|
+
logger: this.logger,
|
|
492
|
+
checkpointer: this._checkpointer,
|
|
493
|
+
toolRegistry: this.services.toolRegistry,
|
|
494
|
+
llmProvider: this.services.llmProvider,
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
while (retryCount <= retryConfig.maxRetries) {
|
|
498
|
+
try {
|
|
499
|
+
// Execute pre-hooks
|
|
500
|
+
let currentState = state;
|
|
501
|
+
if (node.preHooks) {
|
|
502
|
+
for (const hook of node.preHooks) {
|
|
503
|
+
currentState = await hook(currentState, context);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Execute node
|
|
507
|
+
const startTime = Date.now();
|
|
508
|
+
let result = await node.execute(currentState, context);
|
|
509
|
+
const duration = Date.now() - startTime;
|
|
510
|
+
// Execute post-hooks
|
|
511
|
+
if (node.postHooks) {
|
|
512
|
+
for (const hook of node.postHooks) {
|
|
513
|
+
result = {
|
|
514
|
+
...result,
|
|
515
|
+
state: await hook(result.state, context),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Validate output if schema provided
|
|
520
|
+
if (node.outputSchema) {
|
|
521
|
+
node.outputSchema.parse(result.state);
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
...result,
|
|
525
|
+
metadata: {
|
|
526
|
+
...result.metadata,
|
|
527
|
+
duration,
|
|
528
|
+
retryCount,
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
534
|
+
const errorCode = error.code ?? 'UNKNOWN';
|
|
535
|
+
if (!retryConfig.retryableErrors.includes(errorCode) ||
|
|
536
|
+
retryCount >= retryConfig.maxRetries) {
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
const delay = Math.min(retryConfig.initialDelay *
|
|
540
|
+
Math.pow(retryConfig.backoffMultiplier, retryCount), retryConfig.maxDelay);
|
|
541
|
+
this.logger.warn(`Retrying node ${node.name} after ${delay}ms`, {
|
|
542
|
+
attempt: retryCount + 1,
|
|
543
|
+
error: lastError.message,
|
|
544
|
+
});
|
|
545
|
+
await this.sleep(delay);
|
|
546
|
+
retryCount++;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
throw lastError ?? new Error('Unknown error during node execution');
|
|
550
|
+
}
|
|
551
|
+
updateStateWithHistory(newState, nodeName, previousState) {
|
|
552
|
+
const changes = this.computeStateChanges(previousState, newState);
|
|
553
|
+
const historyEntry = {
|
|
554
|
+
step: nodeName,
|
|
555
|
+
timestamp: new Date(),
|
|
556
|
+
snapshot: {
|
|
557
|
+
currentStep: previousState.currentStep,
|
|
558
|
+
data: { ...previousState.data },
|
|
559
|
+
},
|
|
560
|
+
changes,
|
|
561
|
+
};
|
|
562
|
+
return {
|
|
563
|
+
...newState,
|
|
564
|
+
currentStep: nodeName,
|
|
565
|
+
history: [...previousState.history, historyEntry],
|
|
566
|
+
updatedAt: new Date(),
|
|
567
|
+
metadata: {
|
|
568
|
+
...newState.metadata,
|
|
569
|
+
stepCount: previousState.metadata.stepCount + 1,
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
computeStateChanges(previous, current) {
|
|
574
|
+
const changes = [];
|
|
575
|
+
// Compare data fields
|
|
576
|
+
const allKeys = new Set([
|
|
577
|
+
...Object.keys(previous.data),
|
|
578
|
+
...Object.keys(current.data),
|
|
579
|
+
]);
|
|
580
|
+
for (const key of allKeys) {
|
|
581
|
+
const prevValue = previous.data[key];
|
|
582
|
+
const currValue = current.data[key];
|
|
583
|
+
if (JSON.stringify(prevValue) !== JSON.stringify(currValue)) {
|
|
584
|
+
changes.push({
|
|
585
|
+
path: `data.${key}`,
|
|
586
|
+
previousValue: prevValue,
|
|
587
|
+
newValue: currValue,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Compare messages
|
|
592
|
+
if (current.messages.length !== previous.messages.length) {
|
|
593
|
+
changes.push({
|
|
594
|
+
path: 'messages',
|
|
595
|
+
previousValue: previous.messages.length,
|
|
596
|
+
newValue: current.messages.length,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
return changes;
|
|
600
|
+
}
|
|
601
|
+
async determineNextNode(currentNode, result, state) {
|
|
602
|
+
// If node explicitly specifies next
|
|
603
|
+
if (result.next) {
|
|
604
|
+
if (Array.isArray(result.next)) {
|
|
605
|
+
// For parallel execution, return the first one
|
|
606
|
+
// (proper parallel execution would need more complex handling)
|
|
607
|
+
return result.next[0];
|
|
608
|
+
}
|
|
609
|
+
return result.next;
|
|
610
|
+
}
|
|
611
|
+
// Evaluate edges
|
|
612
|
+
const edges = this.edges.get(currentNode) ?? [];
|
|
613
|
+
for (const edge of edges) {
|
|
614
|
+
const shouldFollow = await this.evaluateEdgeCondition(edge, result, state);
|
|
615
|
+
if (shouldFollow) {
|
|
616
|
+
return edge.to;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// No matching edge - end of workflow
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
async evaluateEdgeCondition(edge, result, state) {
|
|
623
|
+
// Direct edges always follow
|
|
624
|
+
if (edge.type === 'direct' || edge.type === 'parallel') {
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
// Conditional and loop edges need condition evaluation
|
|
628
|
+
if (!edge.condition) {
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
const { condition } = edge;
|
|
632
|
+
const context = {
|
|
633
|
+
edge,
|
|
634
|
+
sourceResult: result,
|
|
635
|
+
graph: this.getConfig(),
|
|
636
|
+
};
|
|
637
|
+
switch (condition.type) {
|
|
638
|
+
case 'equals':
|
|
639
|
+
return this.getFieldValue(state, condition.field) === condition.value;
|
|
640
|
+
case 'not_equals':
|
|
641
|
+
return this.getFieldValue(state, condition.field) !== condition.value;
|
|
642
|
+
case 'contains': {
|
|
643
|
+
const fieldValue = this.getFieldValue(state, condition.field);
|
|
644
|
+
return (Array.isArray(fieldValue) && fieldValue.includes(condition.value));
|
|
645
|
+
}
|
|
646
|
+
case 'greater_than':
|
|
647
|
+
return (this.getFieldValue(state, condition.field) >
|
|
648
|
+
condition.value);
|
|
649
|
+
case 'less_than':
|
|
650
|
+
return (this.getFieldValue(state, condition.field) <
|
|
651
|
+
condition.value);
|
|
652
|
+
case 'exists':
|
|
653
|
+
return this.getFieldValue(state, condition.field) !== undefined;
|
|
654
|
+
case 'not_exists':
|
|
655
|
+
return this.getFieldValue(state, condition.field) === undefined;
|
|
656
|
+
case 'custom':
|
|
657
|
+
if (condition.evaluate) {
|
|
658
|
+
return await condition.evaluate(state, context);
|
|
659
|
+
}
|
|
660
|
+
return false;
|
|
661
|
+
default:
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
getFieldValue(state, field) {
|
|
666
|
+
if (!field) {
|
|
667
|
+
return undefined;
|
|
668
|
+
}
|
|
669
|
+
const parts = field.split('.');
|
|
670
|
+
let current = state;
|
|
671
|
+
for (const part of parts) {
|
|
672
|
+
if (current === null || current === undefined) {
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
current = current[part];
|
|
676
|
+
}
|
|
677
|
+
return current;
|
|
678
|
+
}
|
|
679
|
+
async createCheckpoint(state, nodeName, executionId, stepNumber) {
|
|
680
|
+
const checkpoint = {
|
|
681
|
+
id: (0, uuid_1.v4)(),
|
|
682
|
+
executionId,
|
|
683
|
+
stepNumber,
|
|
684
|
+
nodeName,
|
|
685
|
+
state,
|
|
686
|
+
timestamp: new Date(),
|
|
687
|
+
};
|
|
688
|
+
if (this.checkpointer) {
|
|
689
|
+
await this.checkpointer.save(checkpoint);
|
|
690
|
+
}
|
|
691
|
+
return checkpoint;
|
|
692
|
+
}
|
|
693
|
+
createError(code, message, node) {
|
|
694
|
+
return {
|
|
695
|
+
code,
|
|
696
|
+
message,
|
|
697
|
+
node,
|
|
698
|
+
recoverable: ['TIMEOUT', 'RATE_LIMIT', 'NETWORK_ERROR'].includes(code),
|
|
699
|
+
recoveryHints: this.getRecoveryHints(code),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
normalizeError(error, node) {
|
|
703
|
+
if (this.isWorkflowError(error)) {
|
|
704
|
+
return error;
|
|
705
|
+
}
|
|
706
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
707
|
+
return {
|
|
708
|
+
code: error.code ?? 'EXECUTION_ERROR',
|
|
709
|
+
message: err.message,
|
|
710
|
+
stack: err.stack,
|
|
711
|
+
node,
|
|
712
|
+
recoverable: false,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
isWorkflowError(error) {
|
|
716
|
+
return (typeof error === 'object' &&
|
|
717
|
+
error !== null &&
|
|
718
|
+
'code' in error &&
|
|
719
|
+
'message' in error &&
|
|
720
|
+
'recoverable' in error);
|
|
721
|
+
}
|
|
722
|
+
getRecoveryHints(code) {
|
|
723
|
+
const hints = {
|
|
724
|
+
TIMEOUT: [
|
|
725
|
+
'Increase timeout configuration',
|
|
726
|
+
'Check network connectivity',
|
|
727
|
+
'Reduce payload size',
|
|
728
|
+
],
|
|
729
|
+
RATE_LIMIT: [
|
|
730
|
+
'Add delay between requests',
|
|
731
|
+
'Reduce concurrency',
|
|
732
|
+
'Contact API provider',
|
|
733
|
+
],
|
|
734
|
+
NETWORK_ERROR: [
|
|
735
|
+
'Check network connectivity',
|
|
736
|
+
'Verify endpoint availability',
|
|
737
|
+
'Check firewall settings',
|
|
738
|
+
],
|
|
739
|
+
MAX_ITERATIONS: [
|
|
740
|
+
'Review loop conditions',
|
|
741
|
+
'Increase maxIterations config',
|
|
742
|
+
'Check for infinite loops',
|
|
743
|
+
],
|
|
744
|
+
ABORTED: [
|
|
745
|
+
'Execution was cancelled by user',
|
|
746
|
+
'Resume from checkpoint if needed',
|
|
747
|
+
],
|
|
748
|
+
};
|
|
749
|
+
return hints[code] ?? [];
|
|
750
|
+
}
|
|
751
|
+
sleep(ms) {
|
|
752
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
exports.StateGraph = StateGraph;
|
|
756
|
+
//# sourceMappingURL=state-graph.js.map
|