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