@wundr.io/langgraph-orchestrator 1.0.2-dev.20260530174250.ef0ec927
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/package.json +60 -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,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditional Edge - Routing based on state conditions
|
|
3
|
+
* @module @wundr.io/langgraph-orchestrator
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AgentState,
|
|
10
|
+
EdgeDefinition,
|
|
11
|
+
EdgeCondition,
|
|
12
|
+
EdgeConditionEvaluator,
|
|
13
|
+
EdgeContext,
|
|
14
|
+
} from '../types';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builder for conditional edges with fluent API
|
|
18
|
+
*/
|
|
19
|
+
export class ConditionalEdgeBuilder<TState extends AgentState = AgentState> {
|
|
20
|
+
private readonly from: string;
|
|
21
|
+
private readonly conditions: Array<{
|
|
22
|
+
condition: EdgeCondition;
|
|
23
|
+
target: string;
|
|
24
|
+
}> = [];
|
|
25
|
+
private defaultTarget?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a new conditional edge builder
|
|
29
|
+
* @param from - Source node name
|
|
30
|
+
*/
|
|
31
|
+
constructor(from: string) {
|
|
32
|
+
this.from = from;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Add a condition branch
|
|
37
|
+
* @param condition - Condition to evaluate
|
|
38
|
+
* @param target - Target node if condition matches
|
|
39
|
+
* @returns this for chaining
|
|
40
|
+
*/
|
|
41
|
+
when(condition: EdgeCondition, target: string): this {
|
|
42
|
+
this.conditions.push({ condition, target });
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Add an equals condition
|
|
48
|
+
* @param field - Field path to check
|
|
49
|
+
* @param value - Value to compare
|
|
50
|
+
* @param target - Target node if matches
|
|
51
|
+
* @returns this for chaining
|
|
52
|
+
*/
|
|
53
|
+
whenEquals(field: string, value: unknown, target: string): this {
|
|
54
|
+
return this.when({ type: 'equals', field, value }, target);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add a not-equals condition
|
|
59
|
+
* @param field - Field path to check
|
|
60
|
+
* @param value - Value to compare
|
|
61
|
+
* @param target - Target node if doesn't match
|
|
62
|
+
* @returns this for chaining
|
|
63
|
+
*/
|
|
64
|
+
whenNotEquals(field: string, value: unknown, target: string): this {
|
|
65
|
+
return this.when({ type: 'not_equals', field, value }, target);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Add a contains condition
|
|
70
|
+
* @param field - Field path to check (array or string)
|
|
71
|
+
* @param value - Value to look for
|
|
72
|
+
* @param target - Target node if contains
|
|
73
|
+
* @returns this for chaining
|
|
74
|
+
*/
|
|
75
|
+
whenContains(field: string, value: unknown, target: string): this {
|
|
76
|
+
return this.when({ type: 'contains', field, value }, target);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Add a greater-than condition
|
|
81
|
+
* @param field - Field path to check
|
|
82
|
+
* @param value - Value to compare
|
|
83
|
+
* @param target - Target node if greater
|
|
84
|
+
* @returns this for chaining
|
|
85
|
+
*/
|
|
86
|
+
whenGreaterThan(field: string, value: number, target: string): this {
|
|
87
|
+
return this.when({ type: 'greater_than', field, value }, target);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Add a less-than condition
|
|
92
|
+
* @param field - Field path to check
|
|
93
|
+
* @param value - Value to compare
|
|
94
|
+
* @param target - Target node if less
|
|
95
|
+
* @returns this for chaining
|
|
96
|
+
*/
|
|
97
|
+
whenLessThan(field: string, value: number, target: string): this {
|
|
98
|
+
return this.when({ type: 'less_than', field, value }, target);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Add an exists condition
|
|
103
|
+
* @param field - Field path to check
|
|
104
|
+
* @param target - Target node if field exists
|
|
105
|
+
* @returns this for chaining
|
|
106
|
+
*/
|
|
107
|
+
whenExists(field: string, target: string): this {
|
|
108
|
+
return this.when({ type: 'exists', field }, target);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Add a not-exists condition
|
|
113
|
+
* @param field - Field path to check
|
|
114
|
+
* @param target - Target node if field doesn't exist
|
|
115
|
+
* @returns this for chaining
|
|
116
|
+
*/
|
|
117
|
+
whenNotExists(field: string, target: string): this {
|
|
118
|
+
return this.when({ type: 'not_exists', field }, target);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Add a custom condition
|
|
123
|
+
* @param evaluate - Custom evaluator function
|
|
124
|
+
* @param target - Target node if evaluator returns true
|
|
125
|
+
* @returns this for chaining
|
|
126
|
+
*/
|
|
127
|
+
whenCustom(evaluate: EdgeConditionEvaluator<TState>, target: string): this {
|
|
128
|
+
return this.when(
|
|
129
|
+
{ type: 'custom', evaluate: evaluate as EdgeConditionEvaluator },
|
|
130
|
+
target,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Set the default target if no conditions match
|
|
136
|
+
* @param target - Default target node
|
|
137
|
+
* @returns this for chaining
|
|
138
|
+
*/
|
|
139
|
+
otherwise(target: string): this {
|
|
140
|
+
this.defaultTarget = target;
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build the edge definitions
|
|
146
|
+
* @returns Array of EdgeDefinition
|
|
147
|
+
*/
|
|
148
|
+
build(): EdgeDefinition[] {
|
|
149
|
+
const edges: EdgeDefinition[] = this.conditions.map(
|
|
150
|
+
({ condition, target }) => ({
|
|
151
|
+
from: this.from,
|
|
152
|
+
to: target,
|
|
153
|
+
type: 'conditional' as const,
|
|
154
|
+
condition,
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (this.defaultTarget) {
|
|
159
|
+
edges.push({
|
|
160
|
+
from: this.from,
|
|
161
|
+
to: this.defaultTarget,
|
|
162
|
+
type: 'direct',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return edges;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a conditional edge builder
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* const edges = conditionalEdge('router')
|
|
176
|
+
* .whenEquals('data.action', 'search', 'search-node')
|
|
177
|
+
* .whenEquals('data.action', 'answer', 'answer-node')
|
|
178
|
+
* .whenGreaterThan('data.confidence', 0.9, 'direct-answer')
|
|
179
|
+
* .otherwise('fallback')
|
|
180
|
+
* .build();
|
|
181
|
+
*
|
|
182
|
+
* for (const edge of edges) {
|
|
183
|
+
* graph.addConditionalEdge(edge.from, edge.to, edge.condition!);
|
|
184
|
+
* }
|
|
185
|
+
* ```
|
|
186
|
+
*
|
|
187
|
+
* @param from - Source node name
|
|
188
|
+
* @returns ConditionalEdgeBuilder
|
|
189
|
+
*/
|
|
190
|
+
export function conditionalEdge<TState extends AgentState = AgentState>(
|
|
191
|
+
from: string,
|
|
192
|
+
): ConditionalEdgeBuilder<TState> {
|
|
193
|
+
return new ConditionalEdgeBuilder<TState>(from);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Create a router function for LLM-based routing
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```typescript
|
|
201
|
+
* const router = createRouter({
|
|
202
|
+
* routes: {
|
|
203
|
+
* 'search': 'search-node',
|
|
204
|
+
* 'calculate': 'calculator-node',
|
|
205
|
+
* 'answer': 'answer-node'
|
|
206
|
+
* },
|
|
207
|
+
* routeExtractor: (state) => state.data.nextAction as string,
|
|
208
|
+
* defaultRoute: 'answer'
|
|
209
|
+
* });
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* @param options - Router configuration
|
|
213
|
+
* @returns Router function
|
|
214
|
+
*/
|
|
215
|
+
export function createRouter<TState extends AgentState = AgentState>(options: {
|
|
216
|
+
routes: Record<string, string>;
|
|
217
|
+
routeExtractor: (state: TState) => string;
|
|
218
|
+
defaultRoute?: string;
|
|
219
|
+
validator?: (route: string) => boolean;
|
|
220
|
+
}): (state: TState) => string {
|
|
221
|
+
return (state: TState): string => {
|
|
222
|
+
const extractedRoute = options.routeExtractor(state);
|
|
223
|
+
|
|
224
|
+
if (options.validator && !options.validator(extractedRoute)) {
|
|
225
|
+
if (options.defaultRoute) {
|
|
226
|
+
return options.defaultRoute;
|
|
227
|
+
}
|
|
228
|
+
throw new Error(`Invalid route: ${extractedRoute}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const target = options.routes[extractedRoute];
|
|
232
|
+
if (target) {
|
|
233
|
+
return target;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (options.defaultRoute) {
|
|
237
|
+
return options.defaultRoute;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
throw new Error(`No route found for: ${extractedRoute}`);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Condition factory functions
|
|
246
|
+
*/
|
|
247
|
+
export const conditions = {
|
|
248
|
+
/**
|
|
249
|
+
* Create an equals condition
|
|
250
|
+
*/
|
|
251
|
+
equals(field: string, value: unknown): EdgeCondition {
|
|
252
|
+
return { type: 'equals', field, value };
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Create a not-equals condition
|
|
257
|
+
*/
|
|
258
|
+
notEquals(field: string, value: unknown): EdgeCondition {
|
|
259
|
+
return { type: 'not_equals', field, value };
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Create a contains condition
|
|
264
|
+
*/
|
|
265
|
+
contains(field: string, value: unknown): EdgeCondition {
|
|
266
|
+
return { type: 'contains', field, value };
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Create a greater-than condition
|
|
271
|
+
*/
|
|
272
|
+
greaterThan(field: string, value: number): EdgeCondition {
|
|
273
|
+
return { type: 'greater_than', field, value };
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Create a less-than condition
|
|
278
|
+
*/
|
|
279
|
+
lessThan(field: string, value: number): EdgeCondition {
|
|
280
|
+
return { type: 'less_than', field, value };
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Create an exists condition
|
|
285
|
+
*/
|
|
286
|
+
exists(field: string): EdgeCondition {
|
|
287
|
+
return { type: 'exists', field };
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create a not-exists condition
|
|
292
|
+
*/
|
|
293
|
+
notExists(field: string): EdgeCondition {
|
|
294
|
+
return { type: 'not_exists', field };
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create a custom condition
|
|
299
|
+
*/
|
|
300
|
+
custom<TState extends AgentState = AgentState>(
|
|
301
|
+
evaluate: EdgeConditionEvaluator<TState>,
|
|
302
|
+
): EdgeCondition {
|
|
303
|
+
return { type: 'custom', evaluate: evaluate as EdgeConditionEvaluator };
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Create an AND condition (all must be true)
|
|
308
|
+
*/
|
|
309
|
+
and(...conditionList: EdgeCondition[]): EdgeCondition {
|
|
310
|
+
return {
|
|
311
|
+
type: 'custom',
|
|
312
|
+
evaluate: async (state: AgentState, context: EdgeContext) => {
|
|
313
|
+
for (const condition of conditionList) {
|
|
314
|
+
const result = await evaluateCondition(condition, state, context);
|
|
315
|
+
if (!result) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Create an OR condition (any must be true)
|
|
326
|
+
*/
|
|
327
|
+
or(...conditionList: EdgeCondition[]): EdgeCondition {
|
|
328
|
+
return {
|
|
329
|
+
type: 'custom',
|
|
330
|
+
evaluate: async (state: AgentState, context: EdgeContext) => {
|
|
331
|
+
for (const condition of conditionList) {
|
|
332
|
+
const result = await evaluateCondition(condition, state, context);
|
|
333
|
+
if (result) {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Create a NOT condition (negate result)
|
|
344
|
+
*/
|
|
345
|
+
not(condition: EdgeCondition): EdgeCondition {
|
|
346
|
+
return {
|
|
347
|
+
type: 'custom',
|
|
348
|
+
evaluate: async (state: AgentState, context: EdgeContext) => {
|
|
349
|
+
const result = await evaluateCondition(condition, state, context);
|
|
350
|
+
return !result;
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Create a range condition (value between min and max)
|
|
357
|
+
*/
|
|
358
|
+
inRange(field: string, min: number, max: number): EdgeCondition {
|
|
359
|
+
return {
|
|
360
|
+
type: 'custom',
|
|
361
|
+
evaluate: async (state: AgentState) => {
|
|
362
|
+
const value = getFieldValue(state, field);
|
|
363
|
+
return typeof value === 'number' && value >= min && value <= max;
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Create a regex match condition
|
|
370
|
+
*/
|
|
371
|
+
matches(field: string, pattern: RegExp): EdgeCondition {
|
|
372
|
+
return {
|
|
373
|
+
type: 'custom',
|
|
374
|
+
evaluate: async (state: AgentState) => {
|
|
375
|
+
const value = getFieldValue(state, field);
|
|
376
|
+
return typeof value === 'string' && pattern.test(value);
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Create an "in array" condition
|
|
383
|
+
*/
|
|
384
|
+
isIn(field: string, values: unknown[]): EdgeCondition {
|
|
385
|
+
return {
|
|
386
|
+
type: 'custom',
|
|
387
|
+
evaluate: async (state: AgentState) => {
|
|
388
|
+
const value = getFieldValue(state, field);
|
|
389
|
+
return values.includes(value);
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Create a type check condition
|
|
396
|
+
*/
|
|
397
|
+
isType(
|
|
398
|
+
field: string,
|
|
399
|
+
type: 'string' | 'number' | 'boolean' | 'object' | 'array',
|
|
400
|
+
): EdgeCondition {
|
|
401
|
+
return {
|
|
402
|
+
type: 'custom',
|
|
403
|
+
evaluate: async (state: AgentState) => {
|
|
404
|
+
const value = getFieldValue(state, field);
|
|
405
|
+
if (type === 'array') {
|
|
406
|
+
return Array.isArray(value);
|
|
407
|
+
}
|
|
408
|
+
return typeof value === type;
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Evaluate a condition against state
|
|
416
|
+
*/
|
|
417
|
+
async function evaluateCondition(
|
|
418
|
+
condition: EdgeCondition,
|
|
419
|
+
state: AgentState,
|
|
420
|
+
context: EdgeContext,
|
|
421
|
+
): Promise<boolean> {
|
|
422
|
+
const fieldValue = condition.field
|
|
423
|
+
? getFieldValue(state, condition.field)
|
|
424
|
+
: undefined;
|
|
425
|
+
|
|
426
|
+
switch (condition.type) {
|
|
427
|
+
case 'equals':
|
|
428
|
+
return fieldValue === condition.value;
|
|
429
|
+
|
|
430
|
+
case 'not_equals':
|
|
431
|
+
return fieldValue !== condition.value;
|
|
432
|
+
|
|
433
|
+
case 'contains':
|
|
434
|
+
if (Array.isArray(fieldValue)) {
|
|
435
|
+
return fieldValue.includes(condition.value);
|
|
436
|
+
}
|
|
437
|
+
if (typeof fieldValue === 'string') {
|
|
438
|
+
return fieldValue.includes(String(condition.value));
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
|
|
442
|
+
case 'greater_than':
|
|
443
|
+
return (
|
|
444
|
+
typeof fieldValue === 'number' &&
|
|
445
|
+
typeof condition.value === 'number' &&
|
|
446
|
+
fieldValue > condition.value
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
case 'less_than':
|
|
450
|
+
return (
|
|
451
|
+
typeof fieldValue === 'number' &&
|
|
452
|
+
typeof condition.value === 'number' &&
|
|
453
|
+
fieldValue < condition.value
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
case 'exists':
|
|
457
|
+
return fieldValue !== undefined && fieldValue !== null;
|
|
458
|
+
|
|
459
|
+
case 'not_exists':
|
|
460
|
+
return fieldValue === undefined || fieldValue === null;
|
|
461
|
+
|
|
462
|
+
case 'custom':
|
|
463
|
+
if (condition.evaluate) {
|
|
464
|
+
return await condition.evaluate(state, context);
|
|
465
|
+
}
|
|
466
|
+
return false;
|
|
467
|
+
|
|
468
|
+
default:
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Get a nested field value from state
|
|
475
|
+
*/
|
|
476
|
+
function getFieldValue(state: AgentState, field: string): unknown {
|
|
477
|
+
const parts = field.split('.');
|
|
478
|
+
let current: unknown = state;
|
|
479
|
+
|
|
480
|
+
for (const part of parts) {
|
|
481
|
+
if (current === null || current === undefined) {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
current = (current as Record<string, unknown>)[part];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return current;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Schema for edge condition validation
|
|
492
|
+
*/
|
|
493
|
+
export const EdgeConditionSchema = z.object({
|
|
494
|
+
type: z.enum([
|
|
495
|
+
'equals',
|
|
496
|
+
'not_equals',
|
|
497
|
+
'contains',
|
|
498
|
+
'greater_than',
|
|
499
|
+
'less_than',
|
|
500
|
+
'exists',
|
|
501
|
+
'not_exists',
|
|
502
|
+
'custom',
|
|
503
|
+
]),
|
|
504
|
+
field: z.string().optional(),
|
|
505
|
+
value: z.unknown().optional(),
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Validate an edge condition
|
|
510
|
+
*/
|
|
511
|
+
export function validateCondition(condition: EdgeCondition): boolean {
|
|
512
|
+
try {
|
|
513
|
+
EdgeConditionSchema.parse(condition);
|
|
514
|
+
return true;
|
|
515
|
+
} catch {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|