@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,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Node - Conditional branching node for workflow control flow
|
|
3
|
+
* @module @wundr.io/langgraph-orchestrator
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AgentState,
|
|
10
|
+
NodeDefinition,
|
|
11
|
+
NodeContext,
|
|
12
|
+
NodeResult,
|
|
13
|
+
EdgeCondition,
|
|
14
|
+
ConditionType,
|
|
15
|
+
} from '../types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration for decision node
|
|
19
|
+
*/
|
|
20
|
+
export interface DecisionNodeConfig {
|
|
21
|
+
/** Decision branches */
|
|
22
|
+
readonly branches: DecisionBranch[];
|
|
23
|
+
/** Default branch if no conditions match */
|
|
24
|
+
readonly defaultBranch?: string;
|
|
25
|
+
/** Whether to throw error if no branch matches and no default */
|
|
26
|
+
readonly throwOnNoMatch?: boolean;
|
|
27
|
+
/** Custom decision function */
|
|
28
|
+
readonly decide?: (state: AgentState) => string | Promise<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Decision branch definition
|
|
33
|
+
*/
|
|
34
|
+
export interface DecisionBranch {
|
|
35
|
+
/** Name/ID of this branch */
|
|
36
|
+
readonly name: string;
|
|
37
|
+
/** Target node for this branch */
|
|
38
|
+
readonly target: string;
|
|
39
|
+
/** Condition for taking this branch */
|
|
40
|
+
readonly condition: EdgeCondition;
|
|
41
|
+
/** Priority (higher = checked first) */
|
|
42
|
+
readonly priority?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Schema for decision node configuration validation
|
|
47
|
+
*/
|
|
48
|
+
export const DecisionNodeConfigSchema = z.object({
|
|
49
|
+
branches: z.array(
|
|
50
|
+
z.object({
|
|
51
|
+
name: z.string(),
|
|
52
|
+
target: z.string(),
|
|
53
|
+
condition: z.object({
|
|
54
|
+
type: z.enum([
|
|
55
|
+
'equals',
|
|
56
|
+
'not_equals',
|
|
57
|
+
'contains',
|
|
58
|
+
'greater_than',
|
|
59
|
+
'less_than',
|
|
60
|
+
'exists',
|
|
61
|
+
'not_exists',
|
|
62
|
+
'custom',
|
|
63
|
+
]),
|
|
64
|
+
field: z.string().optional(),
|
|
65
|
+
value: z.unknown().optional(),
|
|
66
|
+
}),
|
|
67
|
+
priority: z.number().optional(),
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
70
|
+
defaultBranch: z.string().optional(),
|
|
71
|
+
throwOnNoMatch: z.boolean().optional(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a decision node for conditional branching
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const decisionNode = createDecisionNode({
|
|
80
|
+
* id: 'router',
|
|
81
|
+
* name: 'Task Router',
|
|
82
|
+
* config: {
|
|
83
|
+
* branches: [
|
|
84
|
+
* {
|
|
85
|
+
* name: 'search',
|
|
86
|
+
* target: 'search-node',
|
|
87
|
+
* condition: { type: 'equals', field: 'data.action', value: 'search' }
|
|
88
|
+
* },
|
|
89
|
+
* {
|
|
90
|
+
* name: 'answer',
|
|
91
|
+
* target: 'answer-node',
|
|
92
|
+
* condition: { type: 'equals', field: 'data.action', value: 'answer' }
|
|
93
|
+
* }
|
|
94
|
+
* ],
|
|
95
|
+
* defaultBranch: 'fallback-node'
|
|
96
|
+
* }
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* graph.addNode('router', decisionNode);
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @param options - Node creation options
|
|
103
|
+
* @returns NodeDefinition for use in StateGraph
|
|
104
|
+
*/
|
|
105
|
+
export function createDecisionNode<
|
|
106
|
+
TState extends AgentState = AgentState,
|
|
107
|
+
>(options: {
|
|
108
|
+
id: string;
|
|
109
|
+
name: string;
|
|
110
|
+
config: DecisionNodeConfig;
|
|
111
|
+
nodeConfig?: NodeDefinition<TState>['config'];
|
|
112
|
+
}): NodeDefinition<TState> {
|
|
113
|
+
const { id, name, config, nodeConfig = {} } = options;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
id,
|
|
117
|
+
name,
|
|
118
|
+
type: 'decision',
|
|
119
|
+
config: nodeConfig,
|
|
120
|
+
execute: async (
|
|
121
|
+
state: TState,
|
|
122
|
+
context: NodeContext,
|
|
123
|
+
): Promise<NodeResult<TState>> => {
|
|
124
|
+
context.services.logger.debug('Evaluating decision branches', {
|
|
125
|
+
branchCount: config.branches.length,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// If custom decision function provided, use it
|
|
129
|
+
if (config.decide) {
|
|
130
|
+
const target = await config.decide(state);
|
|
131
|
+
context.services.logger.debug('Custom decision made', { target });
|
|
132
|
+
return {
|
|
133
|
+
state,
|
|
134
|
+
next: target,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Sort branches by priority
|
|
139
|
+
const sortedBranches = [...config.branches].sort(
|
|
140
|
+
(a, b) => (b.priority ?? 0) - (a.priority ?? 0),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Evaluate branches in order
|
|
144
|
+
for (const branch of sortedBranches) {
|
|
145
|
+
const matches = await evaluateCondition(branch.condition, state);
|
|
146
|
+
|
|
147
|
+
if (matches) {
|
|
148
|
+
context.services.logger.debug('Branch matched', {
|
|
149
|
+
branch: branch.name,
|
|
150
|
+
target: branch.target,
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
state,
|
|
154
|
+
next: branch.target,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// No branch matched
|
|
160
|
+
if (config.defaultBranch) {
|
|
161
|
+
context.services.logger.debug('Using default branch', {
|
|
162
|
+
target: config.defaultBranch,
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
state,
|
|
166
|
+
next: config.defaultBranch,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (config.throwOnNoMatch) {
|
|
171
|
+
throw new Error('No decision branch matched and no default configured');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
context.services.logger.warn(
|
|
175
|
+
'No decision branch matched, workflow may terminate',
|
|
176
|
+
);
|
|
177
|
+
return { state };
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Evaluate a condition against state
|
|
184
|
+
*/
|
|
185
|
+
async function evaluateCondition(
|
|
186
|
+
condition: EdgeCondition,
|
|
187
|
+
state: AgentState,
|
|
188
|
+
): Promise<boolean> {
|
|
189
|
+
const fieldValue = condition.field
|
|
190
|
+
? getFieldValue(state, condition.field)
|
|
191
|
+
: undefined;
|
|
192
|
+
|
|
193
|
+
switch (condition.type) {
|
|
194
|
+
case 'equals':
|
|
195
|
+
return fieldValue === condition.value;
|
|
196
|
+
|
|
197
|
+
case 'not_equals':
|
|
198
|
+
return fieldValue !== condition.value;
|
|
199
|
+
|
|
200
|
+
case 'contains':
|
|
201
|
+
if (Array.isArray(fieldValue)) {
|
|
202
|
+
return fieldValue.includes(condition.value);
|
|
203
|
+
}
|
|
204
|
+
if (typeof fieldValue === 'string') {
|
|
205
|
+
return fieldValue.includes(String(condition.value));
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
|
|
209
|
+
case 'greater_than':
|
|
210
|
+
return (
|
|
211
|
+
typeof fieldValue === 'number' &&
|
|
212
|
+
typeof condition.value === 'number' &&
|
|
213
|
+
fieldValue > condition.value
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
case 'less_than':
|
|
217
|
+
return (
|
|
218
|
+
typeof fieldValue === 'number' &&
|
|
219
|
+
typeof condition.value === 'number' &&
|
|
220
|
+
fieldValue < condition.value
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
case 'exists':
|
|
224
|
+
return fieldValue !== undefined && fieldValue !== null;
|
|
225
|
+
|
|
226
|
+
case 'not_exists':
|
|
227
|
+
return fieldValue === undefined || fieldValue === null;
|
|
228
|
+
|
|
229
|
+
case 'custom':
|
|
230
|
+
if (condition.evaluate) {
|
|
231
|
+
return await condition.evaluate(state, {
|
|
232
|
+
edge: { from: '', to: '', type: 'conditional', condition },
|
|
233
|
+
sourceResult: { state },
|
|
234
|
+
graph: {
|
|
235
|
+
id: '',
|
|
236
|
+
name: '',
|
|
237
|
+
entryPoint: '',
|
|
238
|
+
nodes: new Map(),
|
|
239
|
+
edges: new Map(),
|
|
240
|
+
config: {
|
|
241
|
+
maxIterations: 100,
|
|
242
|
+
timeout: 300000,
|
|
243
|
+
checkpointEnabled: false,
|
|
244
|
+
checkpointInterval: 1,
|
|
245
|
+
parallelExecution: false,
|
|
246
|
+
retry: {
|
|
247
|
+
maxRetries: 0,
|
|
248
|
+
initialDelay: 0,
|
|
249
|
+
backoffMultiplier: 1,
|
|
250
|
+
maxDelay: 0,
|
|
251
|
+
retryableErrors: [],
|
|
252
|
+
},
|
|
253
|
+
logLevel: 'silent',
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
|
|
260
|
+
default:
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get a nested field value from state
|
|
267
|
+
*/
|
|
268
|
+
function getFieldValue(state: AgentState, field: string): unknown {
|
|
269
|
+
const parts = field.split('.');
|
|
270
|
+
let current: unknown = state;
|
|
271
|
+
|
|
272
|
+
for (const part of parts) {
|
|
273
|
+
if (current === null || current === undefined) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
current = (current as Record<string, unknown>)[part];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return current;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create a switch-case style decision node
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```typescript
|
|
287
|
+
* const switchNode = createSwitchNode({
|
|
288
|
+
* id: 'type-switch',
|
|
289
|
+
* name: 'Type Switch',
|
|
290
|
+
* field: 'data.messageType',
|
|
291
|
+
* cases: {
|
|
292
|
+
* 'question': 'question-handler',
|
|
293
|
+
* 'command': 'command-handler',
|
|
294
|
+
* 'feedback': 'feedback-handler'
|
|
295
|
+
* },
|
|
296
|
+
* default: 'unknown-handler'
|
|
297
|
+
* });
|
|
298
|
+
* ```
|
|
299
|
+
*
|
|
300
|
+
* @param options - Switch node options
|
|
301
|
+
* @returns NodeDefinition for use in StateGraph
|
|
302
|
+
*/
|
|
303
|
+
export function createSwitchNode<
|
|
304
|
+
TState extends AgentState = AgentState,
|
|
305
|
+
>(options: {
|
|
306
|
+
id: string;
|
|
307
|
+
name: string;
|
|
308
|
+
field: string;
|
|
309
|
+
cases: Record<string, string>;
|
|
310
|
+
default?: string;
|
|
311
|
+
nodeConfig?: NodeDefinition<TState>['config'];
|
|
312
|
+
}): NodeDefinition<TState> {
|
|
313
|
+
const branches: DecisionBranch[] = Object.entries(options.cases).map(
|
|
314
|
+
([value, target]) => ({
|
|
315
|
+
name: `case-${value}`,
|
|
316
|
+
target,
|
|
317
|
+
condition: {
|
|
318
|
+
type: 'equals' as ConditionType,
|
|
319
|
+
field: options.field,
|
|
320
|
+
value,
|
|
321
|
+
},
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
return createDecisionNode<TState>({
|
|
326
|
+
id: options.id,
|
|
327
|
+
name: options.name,
|
|
328
|
+
config: {
|
|
329
|
+
branches,
|
|
330
|
+
defaultBranch: options.default,
|
|
331
|
+
throwOnNoMatch: !options.default,
|
|
332
|
+
},
|
|
333
|
+
nodeConfig: options.nodeConfig,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Create a threshold-based decision node
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* ```typescript
|
|
342
|
+
* const confidenceRouter = createThresholdNode({
|
|
343
|
+
* id: 'confidence-router',
|
|
344
|
+
* name: 'Confidence Router',
|
|
345
|
+
* field: 'data.confidence',
|
|
346
|
+
* thresholds: [
|
|
347
|
+
* { value: 0.9, target: 'high-confidence' },
|
|
348
|
+
* { value: 0.7, target: 'medium-confidence' },
|
|
349
|
+
* { value: 0.5, target: 'low-confidence' }
|
|
350
|
+
* ],
|
|
351
|
+
* default: 'very-low-confidence'
|
|
352
|
+
* });
|
|
353
|
+
* ```
|
|
354
|
+
*
|
|
355
|
+
* @param options - Threshold node options
|
|
356
|
+
* @returns NodeDefinition for use in StateGraph
|
|
357
|
+
*/
|
|
358
|
+
export function createThresholdNode<
|
|
359
|
+
TState extends AgentState = AgentState,
|
|
360
|
+
>(options: {
|
|
361
|
+
id: string;
|
|
362
|
+
name: string;
|
|
363
|
+
field: string;
|
|
364
|
+
thresholds: Array<{ value: number; target: string }>;
|
|
365
|
+
default?: string;
|
|
366
|
+
nodeConfig?: NodeDefinition<TState>['config'];
|
|
367
|
+
}): NodeDefinition<TState> {
|
|
368
|
+
// Sort thresholds in descending order
|
|
369
|
+
const sortedThresholds = [...options.thresholds].sort(
|
|
370
|
+
(a, b) => b.value - a.value,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const branches: DecisionBranch[] = sortedThresholds.map(
|
|
374
|
+
(threshold, index) => ({
|
|
375
|
+
name: `threshold-${threshold.value}`,
|
|
376
|
+
target: threshold.target,
|
|
377
|
+
condition: {
|
|
378
|
+
type: 'custom' as ConditionType,
|
|
379
|
+
evaluate: async (state: AgentState) => {
|
|
380
|
+
const value = getFieldValue(state, options.field);
|
|
381
|
+
if (typeof value !== 'number') {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Check if value is >= this threshold but < the next higher threshold
|
|
386
|
+
const isAboveThreshold = value >= threshold.value;
|
|
387
|
+
const nextHigherThreshold = sortedThresholds[index - 1];
|
|
388
|
+
const isBelowNextThreshold =
|
|
389
|
+
!nextHigherThreshold || value < nextHigherThreshold.value;
|
|
390
|
+
|
|
391
|
+
return isAboveThreshold && isBelowNextThreshold;
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
priority: sortedThresholds.length - index,
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
return createDecisionNode<TState>({
|
|
399
|
+
id: options.id,
|
|
400
|
+
name: options.name,
|
|
401
|
+
config: {
|
|
402
|
+
branches,
|
|
403
|
+
defaultBranch: options.default,
|
|
404
|
+
throwOnNoMatch: !options.default,
|
|
405
|
+
},
|
|
406
|
+
nodeConfig: options.nodeConfig,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Create a boolean decision node (if-else)
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* ```typescript
|
|
415
|
+
* const ifElseNode = createIfElseNode({
|
|
416
|
+
* id: 'has-error',
|
|
417
|
+
* name: 'Error Check',
|
|
418
|
+
* condition: {
|
|
419
|
+
* type: 'exists',
|
|
420
|
+
* field: 'error'
|
|
421
|
+
* },
|
|
422
|
+
* ifTrue: 'error-handler',
|
|
423
|
+
* ifFalse: 'success-handler'
|
|
424
|
+
* });
|
|
425
|
+
* ```
|
|
426
|
+
*
|
|
427
|
+
* @param options - If-else node options
|
|
428
|
+
* @returns NodeDefinition for use in StateGraph
|
|
429
|
+
*/
|
|
430
|
+
export function createIfElseNode<
|
|
431
|
+
TState extends AgentState = AgentState,
|
|
432
|
+
>(options: {
|
|
433
|
+
id: string;
|
|
434
|
+
name: string;
|
|
435
|
+
condition: EdgeCondition;
|
|
436
|
+
ifTrue: string;
|
|
437
|
+
ifFalse: string;
|
|
438
|
+
nodeConfig?: NodeDefinition<TState>['config'];
|
|
439
|
+
}): NodeDefinition<TState> {
|
|
440
|
+
return createDecisionNode<TState>({
|
|
441
|
+
id: options.id,
|
|
442
|
+
name: options.name,
|
|
443
|
+
config: {
|
|
444
|
+
branches: [
|
|
445
|
+
{
|
|
446
|
+
name: 'if-true',
|
|
447
|
+
target: options.ifTrue,
|
|
448
|
+
condition: options.condition,
|
|
449
|
+
priority: 1,
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
defaultBranch: options.ifFalse,
|
|
453
|
+
},
|
|
454
|
+
nodeConfig: options.nodeConfig,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Create a multi-condition decision node (AND/OR logic)
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```typescript
|
|
463
|
+
* const multiConditionNode = createMultiConditionNode({
|
|
464
|
+
* id: 'complex-router',
|
|
465
|
+
* name: 'Complex Router',
|
|
466
|
+
* branches: [
|
|
467
|
+
* {
|
|
468
|
+
* name: 'premium-user',
|
|
469
|
+
* target: 'premium-flow',
|
|
470
|
+
* conditions: [
|
|
471
|
+
* { type: 'equals', field: 'data.userType', value: 'premium' },
|
|
472
|
+
* { type: 'greater_than', field: 'data.credits', value: 0 }
|
|
473
|
+
* ],
|
|
474
|
+
* logic: 'AND'
|
|
475
|
+
* },
|
|
476
|
+
* {
|
|
477
|
+
* name: 'needs-upgrade',
|
|
478
|
+
* target: 'upgrade-flow',
|
|
479
|
+
* conditions: [
|
|
480
|
+
* { type: 'equals', field: 'data.userType', value: 'free' },
|
|
481
|
+
* { type: 'less_than', field: 'data.credits', value: 1 }
|
|
482
|
+
* ],
|
|
483
|
+
* logic: 'OR'
|
|
484
|
+
* }
|
|
485
|
+
* ],
|
|
486
|
+
* default: 'standard-flow'
|
|
487
|
+
* });
|
|
488
|
+
* ```
|
|
489
|
+
*
|
|
490
|
+
* @param options - Multi-condition node options
|
|
491
|
+
* @returns NodeDefinition for use in StateGraph
|
|
492
|
+
*/
|
|
493
|
+
export function createMultiConditionNode<
|
|
494
|
+
TState extends AgentState = AgentState,
|
|
495
|
+
>(options: {
|
|
496
|
+
id: string;
|
|
497
|
+
name: string;
|
|
498
|
+
branches: Array<{
|
|
499
|
+
name: string;
|
|
500
|
+
target: string;
|
|
501
|
+
conditions: EdgeCondition[];
|
|
502
|
+
logic: 'AND' | 'OR';
|
|
503
|
+
priority?: number;
|
|
504
|
+
}>;
|
|
505
|
+
default?: string;
|
|
506
|
+
nodeConfig?: NodeDefinition<TState>['config'];
|
|
507
|
+
}): NodeDefinition<TState> {
|
|
508
|
+
const branches: DecisionBranch[] = options.branches.map(branch => ({
|
|
509
|
+
name: branch.name,
|
|
510
|
+
target: branch.target,
|
|
511
|
+
priority: branch.priority,
|
|
512
|
+
condition: {
|
|
513
|
+
type: 'custom' as ConditionType,
|
|
514
|
+
evaluate: async (state: AgentState) => {
|
|
515
|
+
const results = await Promise.all(
|
|
516
|
+
branch.conditions.map(cond => evaluateCondition(cond, state)),
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
if (branch.logic === 'AND') {
|
|
520
|
+
return results.every(r => r);
|
|
521
|
+
} else {
|
|
522
|
+
return results.some(r => r);
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
return createDecisionNode<TState>({
|
|
529
|
+
id: options.id,
|
|
530
|
+
name: options.name,
|
|
531
|
+
config: {
|
|
532
|
+
branches,
|
|
533
|
+
defaultBranch: options.default,
|
|
534
|
+
throwOnNoMatch: !options.default,
|
|
535
|
+
},
|
|
536
|
+
nodeConfig: options.nodeConfig,
|
|
537
|
+
});
|
|
538
|
+
}
|