@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.
@@ -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
+ }