@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,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop Edge - Cyclic edges for iterative workflows
|
|
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
|
+
* Configuration for loop behavior
|
|
18
|
+
*/
|
|
19
|
+
export interface LoopConfig {
|
|
20
|
+
/** Maximum number of iterations */
|
|
21
|
+
readonly maxIterations?: number;
|
|
22
|
+
/** Counter field in state.data */
|
|
23
|
+
readonly counterField?: string;
|
|
24
|
+
/** Condition to continue looping */
|
|
25
|
+
readonly condition: EdgeCondition;
|
|
26
|
+
/** What to do when max iterations reached */
|
|
27
|
+
readonly onMaxIterations?: 'error' | 'exit' | 'force-exit';
|
|
28
|
+
/** Exit node when loop completes */
|
|
29
|
+
readonly exitNode?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builder for loop edges with fluent API
|
|
34
|
+
*/
|
|
35
|
+
export class LoopEdgeBuilder<TState extends AgentState = AgentState> {
|
|
36
|
+
private readonly from: string;
|
|
37
|
+
private readonly to: string;
|
|
38
|
+
private maxIterations: number = 100;
|
|
39
|
+
private counterField: string = '__loopCounter';
|
|
40
|
+
private condition?: EdgeCondition;
|
|
41
|
+
private exitNode?: string;
|
|
42
|
+
private onMaxIterations: 'error' | 'exit' | 'force-exit' = 'exit';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a new loop edge builder
|
|
46
|
+
* @param from - Source node name
|
|
47
|
+
* @param to - Target node name (can be same as from for self-loop)
|
|
48
|
+
*/
|
|
49
|
+
constructor(from: string, to?: string) {
|
|
50
|
+
this.from = from;
|
|
51
|
+
this.to = to ?? from; // Default to self-loop
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Set maximum iterations
|
|
56
|
+
* @param max - Maximum number of loop iterations
|
|
57
|
+
* @returns this for chaining
|
|
58
|
+
*/
|
|
59
|
+
maxIter(max: number): this {
|
|
60
|
+
this.maxIterations = max;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set the counter field name in state
|
|
66
|
+
* @param field - Field name for storing iteration count
|
|
67
|
+
* @returns this for chaining
|
|
68
|
+
*/
|
|
69
|
+
counter(field: string): this {
|
|
70
|
+
this.counterField = field;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set condition to continue looping
|
|
76
|
+
* @param condition - Condition that must be true to continue
|
|
77
|
+
* @returns this for chaining
|
|
78
|
+
*/
|
|
79
|
+
while(condition: EdgeCondition): this {
|
|
80
|
+
this.condition = condition;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Continue while field equals value
|
|
86
|
+
* @param field - Field to check
|
|
87
|
+
* @param value - Expected value
|
|
88
|
+
* @returns this for chaining
|
|
89
|
+
*/
|
|
90
|
+
whileEquals(field: string, value: unknown): this {
|
|
91
|
+
return this.while({ type: 'equals', field, value });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Continue while field is less than value
|
|
96
|
+
* @param field - Field to check
|
|
97
|
+
* @param value - Maximum value
|
|
98
|
+
* @returns this for chaining
|
|
99
|
+
*/
|
|
100
|
+
whileLessThan(field: string, value: number): this {
|
|
101
|
+
return this.while({ type: 'less_than', field, value });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Continue while custom condition is true
|
|
106
|
+
* @param evaluate - Custom evaluator function
|
|
107
|
+
* @returns this for chaining
|
|
108
|
+
*/
|
|
109
|
+
whileCustom(evaluate: EdgeConditionEvaluator<TState>): this {
|
|
110
|
+
return this.while({
|
|
111
|
+
type: 'custom',
|
|
112
|
+
evaluate: evaluate as EdgeConditionEvaluator,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Set the exit node when loop completes
|
|
118
|
+
* @param node - Node to transition to after loop
|
|
119
|
+
* @returns this for chaining
|
|
120
|
+
*/
|
|
121
|
+
exitTo(node: string): this {
|
|
122
|
+
this.exitNode = node;
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Set behavior when max iterations reached
|
|
128
|
+
* @param behavior - What to do on max iterations
|
|
129
|
+
* @returns this for chaining
|
|
130
|
+
*/
|
|
131
|
+
onMax(behavior: 'error' | 'exit' | 'force-exit'): this {
|
|
132
|
+
this.onMaxIterations = behavior;
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Build the loop edge configuration
|
|
138
|
+
* @returns LoopConfig object
|
|
139
|
+
*/
|
|
140
|
+
build(): LoopConfig {
|
|
141
|
+
if (!this.condition) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
'Loop condition is required. Call while() before build()',
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
maxIterations: this.maxIterations,
|
|
149
|
+
counterField: this.counterField,
|
|
150
|
+
condition: this.condition,
|
|
151
|
+
onMaxIterations: this.onMaxIterations,
|
|
152
|
+
exitNode: this.exitNode,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build as EdgeDefinition
|
|
158
|
+
* @returns EdgeDefinition for the loop
|
|
159
|
+
*/
|
|
160
|
+
buildEdge(): EdgeDefinition {
|
|
161
|
+
if (!this.condition) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
'Loop condition is required. Call while() before buildEdge()',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
from: this.from,
|
|
169
|
+
to: this.to,
|
|
170
|
+
type: 'loop',
|
|
171
|
+
condition: this.createLoopCondition(),
|
|
172
|
+
metadata: {
|
|
173
|
+
maxIterations: this.maxIterations,
|
|
174
|
+
counterField: this.counterField,
|
|
175
|
+
onMaxIterations: this.onMaxIterations,
|
|
176
|
+
exitNode: this.exitNode,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create the composite loop condition
|
|
183
|
+
*/
|
|
184
|
+
private createLoopCondition(): EdgeCondition {
|
|
185
|
+
const originalCondition = this.condition!;
|
|
186
|
+
const maxIterations = this.maxIterations;
|
|
187
|
+
const counterField = this.counterField;
|
|
188
|
+
const onMaxIterations = this.onMaxIterations;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
type: 'custom',
|
|
192
|
+
evaluate: async (state: AgentState, context: EdgeContext) => {
|
|
193
|
+
// Get current iteration count
|
|
194
|
+
const currentCount = (state.data[counterField] as number) ?? 0;
|
|
195
|
+
|
|
196
|
+
// Check max iterations
|
|
197
|
+
if (currentCount >= maxIterations) {
|
|
198
|
+
if (onMaxIterations === 'error') {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Loop exceeded maximum iterations (${maxIterations})`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return false; // Exit loop
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Evaluate the actual condition
|
|
207
|
+
return await evaluateCondition(originalCondition, state, context);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a loop edge builder
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* const loopEdge = loopEdge('process')
|
|
219
|
+
* .maxIter(10)
|
|
220
|
+
* .whileLessThan('data.retryCount', 3)
|
|
221
|
+
* .exitTo('success')
|
|
222
|
+
* .onMax('exit')
|
|
223
|
+
* .buildEdge();
|
|
224
|
+
*
|
|
225
|
+
* graph.addLoopEdge(loopEdge.from, loopEdge.to, loopEdge.condition!);
|
|
226
|
+
* ```
|
|
227
|
+
*
|
|
228
|
+
* @param from - Source node name
|
|
229
|
+
* @param to - Optional target node (defaults to from for self-loop)
|
|
230
|
+
* @returns LoopEdgeBuilder
|
|
231
|
+
*/
|
|
232
|
+
export function loopEdge<TState extends AgentState = AgentState>(
|
|
233
|
+
from: string,
|
|
234
|
+
to?: string,
|
|
235
|
+
): LoopEdgeBuilder<TState> {
|
|
236
|
+
return new LoopEdgeBuilder<TState>(from, to);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create a for-loop style edge
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* const forLoop = createForLoop({
|
|
245
|
+
* from: 'process-item',
|
|
246
|
+
* iterations: 5,
|
|
247
|
+
* counterField: 'data.itemIndex',
|
|
248
|
+
* exitNode: 'complete'
|
|
249
|
+
* });
|
|
250
|
+
* ```
|
|
251
|
+
*
|
|
252
|
+
* @param options - For loop configuration
|
|
253
|
+
* @returns EdgeDefinition for the loop
|
|
254
|
+
*/
|
|
255
|
+
export function createForLoop(options: {
|
|
256
|
+
from: string;
|
|
257
|
+
to?: string;
|
|
258
|
+
iterations: number;
|
|
259
|
+
counterField?: string;
|
|
260
|
+
exitNode?: string;
|
|
261
|
+
}): EdgeDefinition {
|
|
262
|
+
const counterField = options.counterField ?? '__forLoopCounter';
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
from: options.from,
|
|
266
|
+
to: options.to ?? options.from,
|
|
267
|
+
type: 'loop',
|
|
268
|
+
condition: {
|
|
269
|
+
type: 'custom',
|
|
270
|
+
evaluate: async (state: AgentState) => {
|
|
271
|
+
const current = (state.data[counterField] as number) ?? 0;
|
|
272
|
+
return current < options.iterations;
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
metadata: {
|
|
276
|
+
loopType: 'for',
|
|
277
|
+
iterations: options.iterations,
|
|
278
|
+
counterField,
|
|
279
|
+
exitNode: options.exitNode,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Create a while-loop style edge
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* const whileLoop = createWhileLoop({
|
|
290
|
+
* from: 'check',
|
|
291
|
+
* to: 'process',
|
|
292
|
+
* condition: conditions.lessThan('data.errorCount', 5),
|
|
293
|
+
* exitNode: 'done',
|
|
294
|
+
* maxIterations: 100
|
|
295
|
+
* });
|
|
296
|
+
* ```
|
|
297
|
+
*
|
|
298
|
+
* @param options - While loop configuration
|
|
299
|
+
* @returns EdgeDefinition for the loop
|
|
300
|
+
*/
|
|
301
|
+
export function createWhileLoop(options: {
|
|
302
|
+
from: string;
|
|
303
|
+
to?: string;
|
|
304
|
+
condition: EdgeCondition;
|
|
305
|
+
exitNode?: string;
|
|
306
|
+
maxIterations?: number;
|
|
307
|
+
}): EdgeDefinition {
|
|
308
|
+
const maxIterations = options.maxIterations ?? 1000;
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
from: options.from,
|
|
312
|
+
to: options.to ?? options.from,
|
|
313
|
+
type: 'loop',
|
|
314
|
+
condition: {
|
|
315
|
+
type: 'custom',
|
|
316
|
+
evaluate: async (state: AgentState, context: EdgeContext) => {
|
|
317
|
+
// Check iteration guard
|
|
318
|
+
const iterations =
|
|
319
|
+
context.graph?.config?.maxIterations ?? maxIterations;
|
|
320
|
+
if (state.metadata.stepCount >= iterations) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return await evaluateCondition(options.condition, state, context);
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
metadata: {
|
|
328
|
+
loopType: 'while',
|
|
329
|
+
exitNode: options.exitNode,
|
|
330
|
+
maxIterations,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Create a do-while loop (executes at least once)
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```typescript
|
|
340
|
+
* const doWhileLoop = createDoWhileLoop({
|
|
341
|
+
* from: 'retry',
|
|
342
|
+
* condition: conditions.equals('data.success', false),
|
|
343
|
+
* exitNode: 'complete',
|
|
344
|
+
* maxIterations: 5
|
|
345
|
+
* });
|
|
346
|
+
* ```
|
|
347
|
+
*
|
|
348
|
+
* @param options - Do-while loop configuration
|
|
349
|
+
* @returns EdgeDefinition for the loop
|
|
350
|
+
*/
|
|
351
|
+
export function createDoWhileLoop(options: {
|
|
352
|
+
from: string;
|
|
353
|
+
to?: string;
|
|
354
|
+
condition: EdgeCondition;
|
|
355
|
+
exitNode?: string;
|
|
356
|
+
maxIterations?: number;
|
|
357
|
+
}): EdgeDefinition {
|
|
358
|
+
const maxIterations = options.maxIterations ?? 1000;
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
from: options.from,
|
|
362
|
+
to: options.to ?? options.from,
|
|
363
|
+
type: 'loop',
|
|
364
|
+
condition: {
|
|
365
|
+
type: 'custom',
|
|
366
|
+
evaluate: async (state: AgentState, context: EdgeContext) => {
|
|
367
|
+
// Check iteration guard
|
|
368
|
+
const iterations =
|
|
369
|
+
context.graph?.config?.maxIterations ?? maxIterations;
|
|
370
|
+
if (state.metadata.stepCount >= iterations) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// First iteration always executes (check if this is first time through)
|
|
375
|
+
const loopCount = (state.data['__doWhileCount'] as number) ?? 0;
|
|
376
|
+
if (loopCount === 0) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return await evaluateCondition(options.condition, state, context);
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
metadata: {
|
|
384
|
+
loopType: 'do-while',
|
|
385
|
+
exitNode: options.exitNode,
|
|
386
|
+
maxIterations,
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Create a retry loop with exponential backoff
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* ```typescript
|
|
396
|
+
* const retryLoop = createRetryLoop({
|
|
397
|
+
* from: 'api-call',
|
|
398
|
+
* maxRetries: 3,
|
|
399
|
+
* exitOnSuccess: 'process-result',
|
|
400
|
+
* exitOnFailure: 'handle-error',
|
|
401
|
+
* successCondition: conditions.equals('data.success', true)
|
|
402
|
+
* });
|
|
403
|
+
* ```
|
|
404
|
+
*
|
|
405
|
+
* @param options - Retry loop configuration
|
|
406
|
+
* @returns EdgeDefinition for the loop
|
|
407
|
+
*/
|
|
408
|
+
export function createRetryLoop(options: {
|
|
409
|
+
from: string;
|
|
410
|
+
to?: string;
|
|
411
|
+
maxRetries: number;
|
|
412
|
+
retryCountField?: string;
|
|
413
|
+
successCondition: EdgeCondition;
|
|
414
|
+
exitOnSuccess?: string;
|
|
415
|
+
exitOnFailure?: string;
|
|
416
|
+
}): EdgeDefinition {
|
|
417
|
+
const retryCountField = options.retryCountField ?? '__retryCount';
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
from: options.from,
|
|
421
|
+
to: options.to ?? options.from,
|
|
422
|
+
type: 'loop',
|
|
423
|
+
condition: {
|
|
424
|
+
type: 'custom',
|
|
425
|
+
evaluate: async (state: AgentState, context: EdgeContext) => {
|
|
426
|
+
const retryCount = (state.data[retryCountField] as number) ?? 0;
|
|
427
|
+
|
|
428
|
+
// Check if we've succeeded
|
|
429
|
+
const succeeded = await evaluateCondition(
|
|
430
|
+
options.successCondition,
|
|
431
|
+
state,
|
|
432
|
+
context,
|
|
433
|
+
);
|
|
434
|
+
if (succeeded) {
|
|
435
|
+
return false; // Exit loop to success node
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Check if we've exhausted retries
|
|
439
|
+
if (retryCount >= options.maxRetries) {
|
|
440
|
+
return false; // Exit loop to failure node
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Continue retrying
|
|
444
|
+
return true;
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
metadata: {
|
|
448
|
+
loopType: 'retry',
|
|
449
|
+
maxRetries: options.maxRetries,
|
|
450
|
+
retryCountField,
|
|
451
|
+
exitOnSuccess: options.exitOnSuccess,
|
|
452
|
+
exitOnFailure: options.exitOnFailure,
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Create a pagination loop for iterating through paged data
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* ```typescript
|
|
462
|
+
* const paginationLoop = createPaginationLoop({
|
|
463
|
+
* from: 'fetch-page',
|
|
464
|
+
* pageField: 'data.currentPage',
|
|
465
|
+
* hasMoreField: 'data.hasMore',
|
|
466
|
+
* exitNode: 'process-all'
|
|
467
|
+
* });
|
|
468
|
+
* ```
|
|
469
|
+
*
|
|
470
|
+
* @param options - Pagination loop configuration
|
|
471
|
+
* @returns EdgeDefinition for the loop
|
|
472
|
+
*/
|
|
473
|
+
export function createPaginationLoop(options: {
|
|
474
|
+
from: string;
|
|
475
|
+
to?: string;
|
|
476
|
+
pageField?: string;
|
|
477
|
+
hasMoreField: string;
|
|
478
|
+
maxPages?: number;
|
|
479
|
+
exitNode?: string;
|
|
480
|
+
}): EdgeDefinition {
|
|
481
|
+
const pageField = options.pageField ?? '__currentPage';
|
|
482
|
+
const maxPages = options.maxPages ?? 1000;
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
from: options.from,
|
|
486
|
+
to: options.to ?? options.from,
|
|
487
|
+
type: 'loop',
|
|
488
|
+
condition: {
|
|
489
|
+
type: 'custom',
|
|
490
|
+
evaluate: async (state: AgentState) => {
|
|
491
|
+
const currentPage = (state.data[pageField] as number) ?? 0;
|
|
492
|
+
|
|
493
|
+
// Check page limit
|
|
494
|
+
if (currentPage >= maxPages) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Check if there are more pages
|
|
499
|
+
const hasMore = getFieldValue(state, options.hasMoreField);
|
|
500
|
+
return hasMore === true;
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
metadata: {
|
|
504
|
+
loopType: 'pagination',
|
|
505
|
+
pageField,
|
|
506
|
+
hasMoreField: options.hasMoreField,
|
|
507
|
+
maxPages,
|
|
508
|
+
exitNode: options.exitNode,
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Evaluate a condition against state
|
|
515
|
+
*/
|
|
516
|
+
async function evaluateCondition(
|
|
517
|
+
condition: EdgeCondition,
|
|
518
|
+
state: AgentState,
|
|
519
|
+
context: EdgeContext,
|
|
520
|
+
): Promise<boolean> {
|
|
521
|
+
const fieldValue = condition.field
|
|
522
|
+
? getFieldValue(state, condition.field)
|
|
523
|
+
: undefined;
|
|
524
|
+
|
|
525
|
+
switch (condition.type) {
|
|
526
|
+
case 'equals':
|
|
527
|
+
return fieldValue === condition.value;
|
|
528
|
+
|
|
529
|
+
case 'not_equals':
|
|
530
|
+
return fieldValue !== condition.value;
|
|
531
|
+
|
|
532
|
+
case 'contains':
|
|
533
|
+
if (Array.isArray(fieldValue)) {
|
|
534
|
+
return fieldValue.includes(condition.value);
|
|
535
|
+
}
|
|
536
|
+
if (typeof fieldValue === 'string') {
|
|
537
|
+
return fieldValue.includes(String(condition.value));
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
|
|
541
|
+
case 'greater_than':
|
|
542
|
+
return (
|
|
543
|
+
typeof fieldValue === 'number' &&
|
|
544
|
+
typeof condition.value === 'number' &&
|
|
545
|
+
fieldValue > condition.value
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
case 'less_than':
|
|
549
|
+
return (
|
|
550
|
+
typeof fieldValue === 'number' &&
|
|
551
|
+
typeof condition.value === 'number' &&
|
|
552
|
+
fieldValue < condition.value
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
case 'exists':
|
|
556
|
+
return fieldValue !== undefined && fieldValue !== null;
|
|
557
|
+
|
|
558
|
+
case 'not_exists':
|
|
559
|
+
return fieldValue === undefined || fieldValue === null;
|
|
560
|
+
|
|
561
|
+
case 'custom':
|
|
562
|
+
if (condition.evaluate) {
|
|
563
|
+
return await condition.evaluate(state, context);
|
|
564
|
+
}
|
|
565
|
+
return false;
|
|
566
|
+
|
|
567
|
+
default:
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Get a nested field value from state
|
|
574
|
+
*/
|
|
575
|
+
function getFieldValue(state: AgentState, field: string): unknown {
|
|
576
|
+
const parts = field.split('.');
|
|
577
|
+
let current: unknown = state;
|
|
578
|
+
|
|
579
|
+
for (const part of parts) {
|
|
580
|
+
if (current === null || current === undefined) {
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
current = (current as Record<string, unknown>)[part];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return current;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Schema for loop configuration validation
|
|
591
|
+
*/
|
|
592
|
+
export const LoopConfigSchema = z.object({
|
|
593
|
+
maxIterations: z.number().min(1).optional(),
|
|
594
|
+
counterField: z.string().optional(),
|
|
595
|
+
condition: z.object({
|
|
596
|
+
type: z.enum([
|
|
597
|
+
'equals',
|
|
598
|
+
'not_equals',
|
|
599
|
+
'contains',
|
|
600
|
+
'greater_than',
|
|
601
|
+
'less_than',
|
|
602
|
+
'exists',
|
|
603
|
+
'not_exists',
|
|
604
|
+
'custom',
|
|
605
|
+
]),
|
|
606
|
+
field: z.string().optional(),
|
|
607
|
+
value: z.unknown().optional(),
|
|
608
|
+
}),
|
|
609
|
+
onMaxIterations: z.enum(['error', 'exit', 'force-exit']).optional(),
|
|
610
|
+
exitNode: z.string().optional(),
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Validate a loop configuration
|
|
615
|
+
*/
|
|
616
|
+
export function validateLoopConfig(config: LoopConfig): boolean {
|
|
617
|
+
try {
|
|
618
|
+
LoopConfigSchema.parse(config);
|
|
619
|
+
return true;
|
|
620
|
+
} catch {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
}
|