@axiom-lattice/core 2.1.78 → 2.1.80
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/dist/chunk-C5HUS7YV.mjs +331 -0
- package/dist/chunk-C5HUS7YV.mjs.map +1 -0
- package/dist/chunk-FN4TRQK4.mjs +1258 -0
- package/dist/chunk-FN4TRQK4.mjs.map +1 -0
- package/dist/compile-SYSKVQHB.mjs +9 -0
- package/dist/compile-SYSKVQHB.mjs.map +1 -0
- package/dist/index.d.mts +122 -3
- package/dist/index.d.ts +122 -3
- package/dist/index.js +2602 -765
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +776 -588
- package/dist/index.mjs.map +1 -1
- package/dist/memory_lattice-E66HTTVV.mjs +13 -0
- package/dist/memory_lattice-E66HTTVV.mjs.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
// src/workflow/compile.ts
|
|
2
|
+
import {
|
|
3
|
+
StateGraph,
|
|
4
|
+
START,
|
|
5
|
+
END
|
|
6
|
+
} from "@langchain/langgraph";
|
|
7
|
+
|
|
8
|
+
// src/workflow/utils.ts
|
|
9
|
+
import {
|
|
10
|
+
Annotation
|
|
11
|
+
} from "@langchain/langgraph";
|
|
12
|
+
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
13
|
+
function buildStateAnnotation(fields) {
|
|
14
|
+
const annotations = {};
|
|
15
|
+
if (fields) {
|
|
16
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
17
|
+
annotations[key] = Annotation({
|
|
18
|
+
default: () => field.default ?? defaultValueForType(field.type),
|
|
19
|
+
reducer: buildReducer(field.reducer ?? "replace")
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!annotations["messages"]) {
|
|
24
|
+
annotations["messages"] = Annotation({
|
|
25
|
+
default: () => [],
|
|
26
|
+
reducer: (prev, next) => [...prev, ...next]
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
annotations["phase"] = Annotation({
|
|
30
|
+
default: () => "init",
|
|
31
|
+
reducer: (_prev, next) => next
|
|
32
|
+
});
|
|
33
|
+
if (!annotations["status"]) {
|
|
34
|
+
annotations["status"] = Annotation({
|
|
35
|
+
default: () => "",
|
|
36
|
+
reducer: (_prev, next) => next
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (!annotations["_runId"]) {
|
|
40
|
+
annotations["_runId"] = Annotation({
|
|
41
|
+
default: () => void 0,
|
|
42
|
+
reducer: (_prev, next) => next ?? _prev
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return Annotation.Root(annotations);
|
|
46
|
+
}
|
|
47
|
+
function defaultValueForType(type) {
|
|
48
|
+
switch (type) {
|
|
49
|
+
case "string":
|
|
50
|
+
return "";
|
|
51
|
+
case "number":
|
|
52
|
+
return null;
|
|
53
|
+
case "boolean":
|
|
54
|
+
return false;
|
|
55
|
+
case "object":
|
|
56
|
+
return null;
|
|
57
|
+
case "array":
|
|
58
|
+
return [];
|
|
59
|
+
default:
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function buildReducer(kind) {
|
|
64
|
+
switch (kind) {
|
|
65
|
+
case "append":
|
|
66
|
+
return (prev, next) => {
|
|
67
|
+
const arr = Array.isArray(prev) ? prev : [];
|
|
68
|
+
return arr.concat(Array.isArray(next) ? next : [next]);
|
|
69
|
+
};
|
|
70
|
+
case "merge":
|
|
71
|
+
return (prev, next) => {
|
|
72
|
+
const obj = prev && typeof prev === "object" && !Array.isArray(prev) ? prev : {};
|
|
73
|
+
const merge = next && typeof next === "object" && !Array.isArray(next) ? next : {};
|
|
74
|
+
return { ...obj, ...merge };
|
|
75
|
+
};
|
|
76
|
+
case "replace":
|
|
77
|
+
default:
|
|
78
|
+
return (_prev, next) => next;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function resolvePath(obj, path) {
|
|
82
|
+
const cleanPath = path.startsWith("state.") ? path.slice(6) : path;
|
|
83
|
+
const parts = cleanPath.split(/\.|\[|\]\.?/).filter(Boolean);
|
|
84
|
+
let current = obj;
|
|
85
|
+
for (const part of parts) {
|
|
86
|
+
if (current === null || current === void 0) return void 0;
|
|
87
|
+
if (typeof current === "object") {
|
|
88
|
+
current = current[part];
|
|
89
|
+
} else {
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
function renderTemplate(template, state, item) {
|
|
96
|
+
return template.replace(/\$\{([^}]+)\}/g, (_match, expr) => {
|
|
97
|
+
const trimmed = expr.trim();
|
|
98
|
+
if (trimmed.startsWith("state.")) {
|
|
99
|
+
const val = resolvePath(state, trimmed);
|
|
100
|
+
return val === void 0 ? "" : formatValue(val);
|
|
101
|
+
}
|
|
102
|
+
if (trimmed.startsWith("item.") || trimmed === "item") {
|
|
103
|
+
if (!item) return "";
|
|
104
|
+
if (trimmed === "item") {
|
|
105
|
+
if (typeof item === "object" && "item" in item) {
|
|
106
|
+
return formatValue(item.item);
|
|
107
|
+
}
|
|
108
|
+
return formatValue(item);
|
|
109
|
+
}
|
|
110
|
+
const itemPath = trimmed;
|
|
111
|
+
const val = resolvePath({ item }, itemPath);
|
|
112
|
+
return val === void 0 ? "" : formatValue(val);
|
|
113
|
+
}
|
|
114
|
+
if (item && typeof item === "object") {
|
|
115
|
+
const firstSeg = trimmed.split(".")[0];
|
|
116
|
+
if (firstSeg in item) {
|
|
117
|
+
const val = resolvePath({ item }, `item.${trimmed}`);
|
|
118
|
+
return val === void 0 ? "" : formatValue(val);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return `\${${trimmed}}`;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function formatValue(val) {
|
|
125
|
+
if (typeof val === "string") return val;
|
|
126
|
+
if (val === null || val === void 0) return "";
|
|
127
|
+
if (Array.isArray(val)) {
|
|
128
|
+
const parts = val.map((item) => {
|
|
129
|
+
const content = item?.content ?? item?.kwargs?.content;
|
|
130
|
+
if (typeof content === "string") return content;
|
|
131
|
+
if (Array.isArray(content)) {
|
|
132
|
+
return content.filter((c) => c?.type === "text").map((c) => String(c.text ?? "")).join("\n");
|
|
133
|
+
}
|
|
134
|
+
if (content && typeof content === "object") return JSON.stringify(content, null, 2);
|
|
135
|
+
if (item && typeof item === "object") return JSON.stringify(item, null, 2);
|
|
136
|
+
return String(item);
|
|
137
|
+
});
|
|
138
|
+
const result = parts.filter(Boolean).join("\n");
|
|
139
|
+
return result || JSON.stringify(val, null, 2);
|
|
140
|
+
}
|
|
141
|
+
return JSON.stringify(val, null, 2);
|
|
142
|
+
}
|
|
143
|
+
function buildInput(input, state, item, outputSchema) {
|
|
144
|
+
let prompt = input?.template ? renderTemplate(input.template, state, item) : "";
|
|
145
|
+
if (outputSchema) {
|
|
146
|
+
const example = schemaToExample(outputSchema);
|
|
147
|
+
const schemaInstruction = `
|
|
148
|
+
|
|
149
|
+
Output must be valid JSON in exactly this shape. Return ONLY the JSON, no other text or markdown:
|
|
150
|
+
${example}`;
|
|
151
|
+
prompt = prompt + schemaInstruction;
|
|
152
|
+
}
|
|
153
|
+
return { messages: [new HumanMessage(prompt)] };
|
|
154
|
+
}
|
|
155
|
+
function schemaToExample(schema) {
|
|
156
|
+
const example = schemaValueToExample(schema);
|
|
157
|
+
return JSON.stringify(example, null, 2);
|
|
158
|
+
}
|
|
159
|
+
function schemaValueToExample(value) {
|
|
160
|
+
if (value === null || value === void 0) return null;
|
|
161
|
+
if (typeof value !== "object") return value;
|
|
162
|
+
const obj = value;
|
|
163
|
+
if (obj.type === "string") return "<string>";
|
|
164
|
+
if (obj.type === "number") return "<number>";
|
|
165
|
+
if (obj.type === "boolean") return "<boolean>";
|
|
166
|
+
if (obj.type === "integer") return "<integer>";
|
|
167
|
+
if (obj.type === "array" && obj.items) {
|
|
168
|
+
return [schemaValueToExample(obj.items)];
|
|
169
|
+
}
|
|
170
|
+
if (obj.type === "object" && obj.properties && typeof obj.properties === "object") {
|
|
171
|
+
const example2 = {};
|
|
172
|
+
for (const [k, v] of Object.entries(obj.properties)) {
|
|
173
|
+
example2[k] = schemaValueToExample(v);
|
|
174
|
+
}
|
|
175
|
+
return example2;
|
|
176
|
+
}
|
|
177
|
+
if (obj.type === "object") return {};
|
|
178
|
+
if (Array.isArray(value)) {
|
|
179
|
+
if (value.length === 1) {
|
|
180
|
+
return [schemaValueToExample(value[0])];
|
|
181
|
+
}
|
|
182
|
+
return value.map(schemaValueToExample);
|
|
183
|
+
}
|
|
184
|
+
const example = {};
|
|
185
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
186
|
+
example[k] = schemaValueToExample(v);
|
|
187
|
+
}
|
|
188
|
+
return example;
|
|
189
|
+
}
|
|
190
|
+
function extractOutput(result) {
|
|
191
|
+
if ("structuredResponse" in result && result.structuredResponse !== void 0) {
|
|
192
|
+
return result.structuredResponse;
|
|
193
|
+
}
|
|
194
|
+
const messages = result.messages;
|
|
195
|
+
if (messages && Array.isArray(messages)) {
|
|
196
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
197
|
+
const msg = messages[i];
|
|
198
|
+
if (msg._getType() === "ai") {
|
|
199
|
+
const content = msg.content;
|
|
200
|
+
if (typeof content === "object" && content !== null && !Array.isArray(content)) {
|
|
201
|
+
return content;
|
|
202
|
+
}
|
|
203
|
+
if (Array.isArray(content)) {
|
|
204
|
+
const textParts = content.filter(
|
|
205
|
+
(c) => typeof c === "object" && c !== null && c.type === "text"
|
|
206
|
+
).map((c) => c.text).join("\n");
|
|
207
|
+
if (textParts) {
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(textParts);
|
|
210
|
+
} catch {
|
|
211
|
+
const fenced = extractJsonFromFence(textParts);
|
|
212
|
+
if (fenced !== void 0) return fenced;
|
|
213
|
+
return textParts;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return content;
|
|
217
|
+
}
|
|
218
|
+
if (typeof content === "string") {
|
|
219
|
+
try {
|
|
220
|
+
return JSON.parse(content);
|
|
221
|
+
} catch {
|
|
222
|
+
const fenced = extractJsonFromFence(content);
|
|
223
|
+
if (fenced !== void 0) return fenced;
|
|
224
|
+
return content;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return content;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const { messages: _msgs, ...rest } = result;
|
|
232
|
+
void _msgs;
|
|
233
|
+
return rest;
|
|
234
|
+
}
|
|
235
|
+
function extractJsonFromFence(text) {
|
|
236
|
+
const jsonFence = text.match(/```json\s*\n([\s\S]*?)```/);
|
|
237
|
+
if (jsonFence) {
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(jsonFence[1].trim());
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const plainFence = text.match(/```\s*\n([\s\S]*?)```/);
|
|
244
|
+
if (plainFence) {
|
|
245
|
+
try {
|
|
246
|
+
return JSON.parse(plainFence[1].trim());
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
async function parallelLimit(items, limit, fn) {
|
|
253
|
+
const results = new Array(items.length);
|
|
254
|
+
let index = 0;
|
|
255
|
+
async function worker() {
|
|
256
|
+
while (index < items.length) {
|
|
257
|
+
const i = index++;
|
|
258
|
+
results[i] = await fn(items[i], i);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
262
|
+
return results;
|
|
263
|
+
}
|
|
264
|
+
async function invokeWithRetry(fn, maxRetries, retryOn, timeoutMs) {
|
|
265
|
+
let lastError;
|
|
266
|
+
for (let i = 0; i <= maxRetries; i++) {
|
|
267
|
+
try {
|
|
268
|
+
const promise = fn();
|
|
269
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
270
|
+
return await withTimeout(promise, timeoutMs);
|
|
271
|
+
}
|
|
272
|
+
return await promise;
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (err?.name === "GraphInterrupt") {
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
lastError = err;
|
|
278
|
+
if (i === maxRetries) throw err;
|
|
279
|
+
if (retryOn?.length) {
|
|
280
|
+
const errName = err?.name ?? "";
|
|
281
|
+
if (!retryOn.includes(errName)) throw err;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
throw lastError;
|
|
286
|
+
}
|
|
287
|
+
function withTimeout(promise, ms) {
|
|
288
|
+
return Promise.race([
|
|
289
|
+
promise,
|
|
290
|
+
new Promise(
|
|
291
|
+
(_, reject) => setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms)
|
|
292
|
+
)
|
|
293
|
+
]);
|
|
294
|
+
}
|
|
295
|
+
function createAgentNode(node, resolveAgent, trackingStore) {
|
|
296
|
+
return async (state, config) => {
|
|
297
|
+
const runId = state._runId;
|
|
298
|
+
const tenantId = config?.configurable?.tenantId ?? "default";
|
|
299
|
+
const startedAt = Date.now();
|
|
300
|
+
let stepId;
|
|
301
|
+
if (trackingStore && runId) {
|
|
302
|
+
trackingStore.updateWorkflowRun(runId, {
|
|
303
|
+
status: "running",
|
|
304
|
+
completedAt: null
|
|
305
|
+
}).catch(() => {
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
console.log(`[WF][${node.id}] START "${node.name}" | hasSchema=${!!node.output?.schema}`);
|
|
310
|
+
const responseFormat = node.output?.schema;
|
|
311
|
+
console.log(`[WF][${node.id}] resolving agent...`);
|
|
312
|
+
const client = await resolveAgent(node.ref, responseFormat, node.type);
|
|
313
|
+
console.log(`[WF][${node.id}] agent resolved, building input...`);
|
|
314
|
+
const input = buildInput(
|
|
315
|
+
node.input,
|
|
316
|
+
state,
|
|
317
|
+
void 0,
|
|
318
|
+
responseFormat ? void 0 : node.output?.schema
|
|
319
|
+
);
|
|
320
|
+
const renderedInput = input.messages[0]?.content ?? "";
|
|
321
|
+
console.log(`[WF][${node.id}] === INPUT (full) ===
|
|
322
|
+
${renderedInput}
|
|
323
|
+
=== END INPUT ===`);
|
|
324
|
+
if (trackingStore && runId) {
|
|
325
|
+
const step = await trackingStore.upsertRunStep({
|
|
326
|
+
runId,
|
|
327
|
+
tenantId,
|
|
328
|
+
stepType: node.type,
|
|
329
|
+
stepName: node.name,
|
|
330
|
+
input: { template: node.input?.template, rendered: renderedInput }
|
|
331
|
+
}).catch((e) => {
|
|
332
|
+
console.warn("Failed to upsert run step:", e.message);
|
|
333
|
+
return null;
|
|
334
|
+
});
|
|
335
|
+
stepId = step?.id;
|
|
336
|
+
if (step && step.status === "interrupted") {
|
|
337
|
+
trackingStore.updateRunStep(runId, step.id, { status: "running" }).catch(() => {
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const subConfig = {
|
|
342
|
+
...config,
|
|
343
|
+
configurable: {
|
|
344
|
+
...config?.configurable ?? {},
|
|
345
|
+
thread_id: `${config?.configurable?.thread_id ?? "default"}:${node.id}`
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
let result;
|
|
349
|
+
try {
|
|
350
|
+
console.log(`[WF][${node.id}] invoking agent.invoke()...`);
|
|
351
|
+
result = await invokeWithRetry(
|
|
352
|
+
() => client.invoke(input, subConfig),
|
|
353
|
+
node.config?.maxRetries ?? 0,
|
|
354
|
+
node.config?.retryOn,
|
|
355
|
+
node.config?.timeout
|
|
356
|
+
);
|
|
357
|
+
console.log(`[WF][${node.id}] invoke SUCCESS, extracting output...`);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
if (err?.name === "GraphInterrupt") {
|
|
360
|
+
if (trackingStore && runId && stepId) {
|
|
361
|
+
trackingStore.updateRunStep(runId, stepId, { status: "interrupted" }).catch(() => {
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (trackingStore && runId) {
|
|
365
|
+
trackingStore.updateWorkflowRun(runId, { status: "interrupted", completedAt: null }).catch(() => {
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
throw err;
|
|
369
|
+
}
|
|
370
|
+
const errMsg = err?.message ?? "";
|
|
371
|
+
console.log(`[WF][${node.id}] invoke FAILED: ${errMsg}`);
|
|
372
|
+
if (responseFormat && (errMsg.includes("tool_choice") || errMsg.includes("thinking") || errMsg.includes("reasoning") || errMsg.includes("InvalidParameter"))) {
|
|
373
|
+
console.log(`[WF][${node.id}] attempting fallback (text-based schema injection)...`);
|
|
374
|
+
const fallbackClient = await resolveAgent(node.ref, void 0, node.type);
|
|
375
|
+
const fallbackInput = buildInput(node.input, state, void 0, responseFormat);
|
|
376
|
+
result = await invokeWithRetry(
|
|
377
|
+
() => fallbackClient.invoke(fallbackInput, subConfig),
|
|
378
|
+
node.config?.maxRetries ?? 0,
|
|
379
|
+
node.config?.retryOn,
|
|
380
|
+
node.config?.timeout
|
|
381
|
+
);
|
|
382
|
+
console.log(`[WF][${node.id}] fallback invoke SUCCESS`);
|
|
383
|
+
} else {
|
|
384
|
+
throw err;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const output = extractOutput(result);
|
|
388
|
+
console.log(`[WF][${node.id}] === OUTPUT (full) | key=${node.output?.key} ===
|
|
389
|
+
${JSON.stringify(output, null, 2)}
|
|
390
|
+
=== END OUTPUT ===`);
|
|
391
|
+
const update = { phase: node.id };
|
|
392
|
+
if (node.output?.key) {
|
|
393
|
+
update[node.output.key] = output;
|
|
394
|
+
}
|
|
395
|
+
const subMessages = result.messages;
|
|
396
|
+
if (subMessages && Array.isArray(subMessages)) {
|
|
397
|
+
const aiMessages = subMessages.filter((m) => {
|
|
398
|
+
const t = typeof m._getType === "function" ? m._getType() : m.role ?? m.type;
|
|
399
|
+
return t === "ai";
|
|
400
|
+
});
|
|
401
|
+
if (aiMessages.length > 0) {
|
|
402
|
+
update.messages = aiMessages;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
console.log(`[WF][${node.id}] DONE (${Date.now() - startedAt}ms)`);
|
|
406
|
+
if (trackingStore && runId && stepId) {
|
|
407
|
+
trackingStore.updateRunStep(runId, stepId, {
|
|
408
|
+
status: "completed",
|
|
409
|
+
output,
|
|
410
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
411
|
+
durationMs: Date.now() - startedAt
|
|
412
|
+
}).catch((e) => {
|
|
413
|
+
console.warn("Failed to update run step:", e.message);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return update;
|
|
417
|
+
} catch (err) {
|
|
418
|
+
if (err?.name === "GraphInterrupt") {
|
|
419
|
+
throw err;
|
|
420
|
+
}
|
|
421
|
+
if (trackingStore && runId) {
|
|
422
|
+
if (stepId) {
|
|
423
|
+
trackingStore.updateRunStep(runId, stepId, {
|
|
424
|
+
status: "failed",
|
|
425
|
+
errorMessage: err.message,
|
|
426
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
427
|
+
durationMs: Date.now() - startedAt
|
|
428
|
+
}).catch((e) => {
|
|
429
|
+
console.warn("Failed to update run step:", e.message);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
trackingStore.updateWorkflowRun(runId, {
|
|
433
|
+
status: "failed",
|
|
434
|
+
errorMessage: err.message,
|
|
435
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
436
|
+
}).catch((e) => {
|
|
437
|
+
console.warn("Failed to finalize WorkflowRun:", e.message);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function createHumanFeedbackNode(node, resolveAgent, trackingStore) {
|
|
445
|
+
return async (state, config) => {
|
|
446
|
+
const runId = state._runId;
|
|
447
|
+
const tenantId = config?.configurable?.tenantId ?? "default";
|
|
448
|
+
const startedAt = Date.now();
|
|
449
|
+
let stepId;
|
|
450
|
+
if (trackingStore && runId) {
|
|
451
|
+
trackingStore.updateWorkflowRun(runId, {
|
|
452
|
+
status: "running",
|
|
453
|
+
completedAt: null
|
|
454
|
+
}).catch(() => {
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
console.log(`[WF][${node.id}] HUMAN_FEEDBACK START "${node.config?.title || node.name}"`);
|
|
459
|
+
const responseFormat = node.output?.schema;
|
|
460
|
+
const client = await resolveAgent(void 0, responseFormat, node.type);
|
|
461
|
+
console.log(`[WF][${node.id}] agent resolved, building input...`);
|
|
462
|
+
const input = buildInput(
|
|
463
|
+
node.input,
|
|
464
|
+
state,
|
|
465
|
+
void 0,
|
|
466
|
+
responseFormat ? void 0 : node.output?.schema
|
|
467
|
+
);
|
|
468
|
+
const humanContent = input.messages[0]?.content ?? "";
|
|
469
|
+
input.messages = [
|
|
470
|
+
new SystemMessage(
|
|
471
|
+
`Follow this exact procedure. Do NOT skip any step.
|
|
472
|
+
|
|
473
|
+
STEP 1: Read the prompt. It contains content to present to the user and a question to ask.
|
|
474
|
+
|
|
475
|
+
STEP 2: Call ask_user_to_clarify. Your question text MUST contain the ACTUAL content from the prompt \u2014 copy-paste the relevant text, data, or facts directly into the question. A question like "Do you approve this recommendation?" is WRONG because the user has no idea what the recommendation says. Instead: "Do you approve this recommendation: [paste the actual recommendation text here]?"
|
|
476
|
+
|
|
477
|
+
STEP 3: The tool will pause. The user will see your questions and respond. You will receive their answers.
|
|
478
|
+
|
|
479
|
+
STEP 4: Based on the user's answers, produce your final output.`
|
|
480
|
+
),
|
|
481
|
+
...input.messages
|
|
482
|
+
];
|
|
483
|
+
const renderedInput = humanContent;
|
|
484
|
+
if (trackingStore && runId) {
|
|
485
|
+
const step = await trackingStore.upsertRunStep({
|
|
486
|
+
runId,
|
|
487
|
+
tenantId,
|
|
488
|
+
stepType: node.type,
|
|
489
|
+
stepName: node.name,
|
|
490
|
+
input: { template: node.input?.template, rendered: renderedInput }
|
|
491
|
+
}).catch((e) => {
|
|
492
|
+
console.warn("Failed to upsert run step:", e.message);
|
|
493
|
+
return null;
|
|
494
|
+
});
|
|
495
|
+
stepId = step?.id;
|
|
496
|
+
if (step && step.status === "interrupted") {
|
|
497
|
+
trackingStore.updateRunStep(runId, step.id, { status: "running" }).catch(() => {
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const subConfig = {
|
|
502
|
+
...config,
|
|
503
|
+
configurable: {
|
|
504
|
+
...config?.configurable ?? {},
|
|
505
|
+
thread_id: `${config?.configurable?.thread_id ?? "default"}:${node.id}`
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
console.log(`[WF][${node.id}] === INPUT (full) ===
|
|
509
|
+
${renderedInput}
|
|
510
|
+
=== END INPUT ===`);
|
|
511
|
+
const result = await invokeWithRetry(
|
|
512
|
+
() => client.invoke(input, subConfig),
|
|
513
|
+
node.config?.maxRetries ?? 0,
|
|
514
|
+
node.config?.retryOn,
|
|
515
|
+
node.config?.timeout
|
|
516
|
+
);
|
|
517
|
+
console.log(`[WF][${node.id}] invoke SUCCESS, extracting output...`);
|
|
518
|
+
const output = extractOutput(result);
|
|
519
|
+
console.log(`[WF][${node.id}] === OUTPUT (full) | key=${node.output?.key} ===
|
|
520
|
+
${JSON.stringify(output, null, 2)}
|
|
521
|
+
=== END OUTPUT ===`);
|
|
522
|
+
const update = { phase: node.id };
|
|
523
|
+
if (node.output?.key) {
|
|
524
|
+
update[node.output.key] = output;
|
|
525
|
+
}
|
|
526
|
+
if (output && typeof output === "object" && !Array.isArray(output)) {
|
|
527
|
+
Object.assign(update, output);
|
|
528
|
+
}
|
|
529
|
+
const subMessages = result.messages;
|
|
530
|
+
if (subMessages && Array.isArray(subMessages)) {
|
|
531
|
+
const aiMessages = subMessages.filter((m) => {
|
|
532
|
+
const t = typeof m._getType === "function" ? m._getType() : m.role ?? m.type;
|
|
533
|
+
return t === "ai";
|
|
534
|
+
});
|
|
535
|
+
if (aiMessages.length > 0) {
|
|
536
|
+
update.messages = aiMessages;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
console.log(`[WF][${node.id}] DONE (${Date.now() - startedAt}ms)`);
|
|
540
|
+
if (trackingStore && runId && stepId) {
|
|
541
|
+
trackingStore.updateRunStep(runId, stepId, {
|
|
542
|
+
status: "completed",
|
|
543
|
+
output,
|
|
544
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
545
|
+
durationMs: Date.now() - startedAt
|
|
546
|
+
}).catch((e) => {
|
|
547
|
+
console.warn("Failed to update run step:", e.message);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return update;
|
|
551
|
+
} catch (err) {
|
|
552
|
+
if (err?.name === "GraphInterrupt") {
|
|
553
|
+
if (trackingStore && runId && stepId) {
|
|
554
|
+
trackingStore.updateRunStep(runId, stepId, {
|
|
555
|
+
status: "interrupted"
|
|
556
|
+
}).catch(() => {
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
if (trackingStore && runId) {
|
|
560
|
+
trackingStore.updateWorkflowRun(runId, {
|
|
561
|
+
status: "interrupted",
|
|
562
|
+
completedAt: null
|
|
563
|
+
}).catch(() => {
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
throw err;
|
|
567
|
+
}
|
|
568
|
+
if (trackingStore && runId) {
|
|
569
|
+
if (stepId) {
|
|
570
|
+
trackingStore.updateRunStep(runId, stepId, {
|
|
571
|
+
status: "failed",
|
|
572
|
+
errorMessage: err.message,
|
|
573
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
574
|
+
durationMs: Date.now() - startedAt
|
|
575
|
+
}).catch((e) => {
|
|
576
|
+
console.warn("Failed to update run step:", e.message);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
trackingStore.updateWorkflowRun(runId, {
|
|
580
|
+
status: "failed",
|
|
581
|
+
errorMessage: err.message,
|
|
582
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
583
|
+
}).catch((e) => {
|
|
584
|
+
console.warn("Failed to finalize WorkflowRun:", e.message);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
throw err;
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function createMapNode(node, resolveAgent, trackingStore) {
|
|
592
|
+
return async (state, config) => {
|
|
593
|
+
const runId = state._runId;
|
|
594
|
+
const tenantId = config?.configurable?.tenantId ?? "default";
|
|
595
|
+
const startedAt = Date.now();
|
|
596
|
+
let stepId;
|
|
597
|
+
if (trackingStore && runId) {
|
|
598
|
+
trackingStore.updateWorkflowRun(runId, {
|
|
599
|
+
status: "running",
|
|
600
|
+
completedAt: null
|
|
601
|
+
}).catch(() => {
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
const items = resolvePath(state, node.source);
|
|
606
|
+
if (trackingStore && runId) {
|
|
607
|
+
const step = await trackingStore.upsertRunStep({
|
|
608
|
+
runId,
|
|
609
|
+
tenantId,
|
|
610
|
+
stepType: node.type,
|
|
611
|
+
stepName: node.name,
|
|
612
|
+
input: { source: node.source, itemCount: Array.isArray(items) ? items.length : 0 }
|
|
613
|
+
}).catch((e) => {
|
|
614
|
+
console.warn("Failed to upsert run step:", e.message);
|
|
615
|
+
return null;
|
|
616
|
+
});
|
|
617
|
+
stepId = step?.id;
|
|
618
|
+
if (step && step.status === "interrupted") {
|
|
619
|
+
trackingStore.updateRunStep(runId, step.id, { status: "running" }).catch(() => {
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (!Array.isArray(items)) {
|
|
624
|
+
throw new Error(
|
|
625
|
+
`Map source "${node.source}" resolved to ${typeof items}, expected array`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
const batchSize = node.config?.batchSize ?? 50;
|
|
629
|
+
const maxConcurrency = node.config?.maxConcurrency ?? 10;
|
|
630
|
+
const innerConcurrency = node.config?.innerConcurrency ?? 5;
|
|
631
|
+
const itemKey = node.itemKey ?? "item";
|
|
632
|
+
const innerClient = await resolveAgent(node.node.ref, node.node.schema, node.type);
|
|
633
|
+
const batches = chunk(items, batchSize);
|
|
634
|
+
const batchResults = await parallelLimit(batches, maxConcurrency, async (batch) => {
|
|
635
|
+
const itemResults = await parallelLimit(batch, innerConcurrency, async (item, itemIdx) => {
|
|
636
|
+
const itemCtx = { [itemKey]: item };
|
|
637
|
+
const input = buildInput(node.node.input, state, itemCtx);
|
|
638
|
+
const subConfig = {
|
|
639
|
+
...config,
|
|
640
|
+
configurable: {
|
|
641
|
+
...config?.configurable ?? {},
|
|
642
|
+
thread_id: `${config?.configurable?.thread_id ?? "default"}:${node.id}:item:${itemIdx}`
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
const result = await invokeWithRetry(
|
|
646
|
+
() => innerClient.invoke(input, subConfig),
|
|
647
|
+
node.config?.maxRetries ?? 0,
|
|
648
|
+
node.config?.retryOn,
|
|
649
|
+
node.config?.timeout
|
|
650
|
+
);
|
|
651
|
+
return extractOutput(result);
|
|
652
|
+
});
|
|
653
|
+
return itemResults;
|
|
654
|
+
});
|
|
655
|
+
const allResults = batchResults.flat();
|
|
656
|
+
let finalOutput = allResults;
|
|
657
|
+
if (node.reduce) {
|
|
658
|
+
const reduceClient = await resolveAgent(node.reduce.ref, node.reduce.schema, node.type);
|
|
659
|
+
const tempResultsKey = node.output?.key ?? `${node.id}_results`;
|
|
660
|
+
const tempState = { ...state, [tempResultsKey]: allResults };
|
|
661
|
+
const reduceInput = buildInput(node.reduce.input, tempState);
|
|
662
|
+
const reduceResult = await invokeWithRetry(
|
|
663
|
+
() => reduceClient.invoke(reduceInput, {
|
|
664
|
+
...config,
|
|
665
|
+
configurable: {
|
|
666
|
+
...config?.configurable ?? {},
|
|
667
|
+
thread_id: `${config?.configurable?.thread_id ?? "default"}:${node.id}:reduce`
|
|
668
|
+
}
|
|
669
|
+
}),
|
|
670
|
+
node.config?.maxRetries ?? 0,
|
|
671
|
+
node.config?.retryOn,
|
|
672
|
+
node.config?.timeout
|
|
673
|
+
);
|
|
674
|
+
finalOutput = extractOutput(reduceResult);
|
|
675
|
+
}
|
|
676
|
+
const update = { phase: node.id };
|
|
677
|
+
if (node.output?.key) {
|
|
678
|
+
update[node.output.key] = finalOutput;
|
|
679
|
+
}
|
|
680
|
+
if (trackingStore && runId && stepId) {
|
|
681
|
+
trackingStore.updateRunStep(runId, stepId, {
|
|
682
|
+
status: "completed",
|
|
683
|
+
output: finalOutput,
|
|
684
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
685
|
+
durationMs: Date.now() - startedAt
|
|
686
|
+
}).catch((e) => {
|
|
687
|
+
console.warn("Failed to update run step:", e.message);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
return update;
|
|
691
|
+
} catch (err) {
|
|
692
|
+
if (err?.name === "GraphInterrupt") {
|
|
693
|
+
if (trackingStore && runId && stepId) {
|
|
694
|
+
trackingStore.updateRunStep(runId, stepId, { status: "interrupted" }).catch(() => {
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
if (trackingStore && runId) {
|
|
698
|
+
trackingStore.updateWorkflowRun(runId, { status: "interrupted", completedAt: null }).catch(() => {
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
throw err;
|
|
702
|
+
}
|
|
703
|
+
console.error(`[WF][${node.id}] FAILED after ${Date.now() - startedAt}ms:`, err.stack || err.message);
|
|
704
|
+
if (trackingStore && runId) {
|
|
705
|
+
if (stepId) {
|
|
706
|
+
trackingStore.updateRunStep(runId, stepId, {
|
|
707
|
+
status: "failed",
|
|
708
|
+
errorMessage: err.message,
|
|
709
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
710
|
+
durationMs: Date.now() - startedAt
|
|
711
|
+
}).catch(() => {
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
trackingStore.updateWorkflowRun(runId, {
|
|
715
|
+
status: "failed",
|
|
716
|
+
errorMessage: err.message,
|
|
717
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
718
|
+
}).catch(() => {
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
throw err;
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function createInputNode(node, trackingStore) {
|
|
726
|
+
return async (state, config) => {
|
|
727
|
+
const tenantId = config?.configurable?.tenantId ?? "default";
|
|
728
|
+
const threadId = config?.configurable?.thread_id ?? "default";
|
|
729
|
+
const assistantId = config?.configurable?.assistantId || config?.configurable?.assistant_id || "unknown";
|
|
730
|
+
const messages = state.messages;
|
|
731
|
+
const humanMsg = messages?.find((m) => m._getType() === "human");
|
|
732
|
+
const inputText = typeof humanMsg?.content === "string" ? humanMsg.content : JSON.stringify(humanMsg?.content ?? "");
|
|
733
|
+
console.log(`[WF][n_input] input captured: "${inputText.slice(0, 200)}"`);
|
|
734
|
+
const update = { phase: node.id };
|
|
735
|
+
if (node.output?.key) update[node.output.key] = inputText;
|
|
736
|
+
if (trackingStore && !state._runId) {
|
|
737
|
+
try {
|
|
738
|
+
const run = await trackingStore.createWorkflowRun({
|
|
739
|
+
tenantId,
|
|
740
|
+
assistantId,
|
|
741
|
+
threadId,
|
|
742
|
+
topologyEdges: [],
|
|
743
|
+
metadata: { workflowName: node.name }
|
|
744
|
+
});
|
|
745
|
+
update._runId = run.id;
|
|
746
|
+
await trackingStore.createRunStep({
|
|
747
|
+
runId: run.id,
|
|
748
|
+
tenantId,
|
|
749
|
+
stepType: node.type,
|
|
750
|
+
stepName: node.name,
|
|
751
|
+
input: { rendered: inputText }
|
|
752
|
+
}).catch((e) => {
|
|
753
|
+
console.warn("Failed to create input run step:", e.message);
|
|
754
|
+
});
|
|
755
|
+
} catch (e) {
|
|
756
|
+
console.warn("Failed to create workflow run:", e.message);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return update;
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function terminalStatusToRunStatus(status) {
|
|
763
|
+
switch (status) {
|
|
764
|
+
case "failed":
|
|
765
|
+
return "failed";
|
|
766
|
+
case "cancelled":
|
|
767
|
+
return "cancelled";
|
|
768
|
+
case "success":
|
|
769
|
+
default:
|
|
770
|
+
return "completed";
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function createTerminalNode(node, trackingStore) {
|
|
774
|
+
const runStatus = terminalStatusToRunStatus(node.status);
|
|
775
|
+
return async (state) => {
|
|
776
|
+
const runId = state._runId;
|
|
777
|
+
if (trackingStore && runId) {
|
|
778
|
+
try {
|
|
779
|
+
await trackingStore.updateWorkflowRun(runId, {
|
|
780
|
+
status: runStatus,
|
|
781
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
782
|
+
});
|
|
783
|
+
} catch (e) {
|
|
784
|
+
console.warn(`[WF][terminal] Failed to finalize WorkflowRun "${runId}":`, e.message);
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
await trackingStore.createRunStep({
|
|
788
|
+
runId,
|
|
789
|
+
tenantId: state._tenantId ?? "default",
|
|
790
|
+
stepType: "terminal",
|
|
791
|
+
stepName: node.name,
|
|
792
|
+
input: { status: node.status }
|
|
793
|
+
});
|
|
794
|
+
} catch (e) {
|
|
795
|
+
console.warn(`[WF][terminal] Failed to create terminal RunStep:`, e.message);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
status: node.status,
|
|
800
|
+
phase: node.id
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function createNodeHandler(node, resolveAgent, trackingStore) {
|
|
805
|
+
switch (node.type) {
|
|
806
|
+
case "agent":
|
|
807
|
+
return createAgentNode(node, resolveAgent, trackingStore);
|
|
808
|
+
case "human_feedback":
|
|
809
|
+
return createHumanFeedbackNode(node, resolveAgent, trackingStore);
|
|
810
|
+
case "map":
|
|
811
|
+
return createMapNode(node, resolveAgent, trackingStore);
|
|
812
|
+
case "terminal":
|
|
813
|
+
return createTerminalNode(node, trackingStore);
|
|
814
|
+
case "input":
|
|
815
|
+
return createInputNode(node, trackingStore);
|
|
816
|
+
default:
|
|
817
|
+
throw new Error(`Unknown node type: ${node.type}`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function chunk(arr, size) {
|
|
821
|
+
const result = [];
|
|
822
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
823
|
+
result.push(arr.slice(i, i + size));
|
|
824
|
+
}
|
|
825
|
+
return result;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/workflow/normalize.ts
|
|
829
|
+
var _counter = 0;
|
|
830
|
+
function uid(prefix) {
|
|
831
|
+
return `${prefix}_${_counter++}`;
|
|
832
|
+
}
|
|
833
|
+
function nodeId(stepId) {
|
|
834
|
+
return `n_${stepId}`;
|
|
835
|
+
}
|
|
836
|
+
function translate(template) {
|
|
837
|
+
return template.replace(/\{\{([^}]+)\}\}/g, (_m, expr) => {
|
|
838
|
+
const t = expr.trim();
|
|
839
|
+
if (t === "item") return "${item}";
|
|
840
|
+
return `\${state.${t}}`;
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
function inferFieldType(id) {
|
|
844
|
+
if (/s$|list|array|items|results/i.test(id)) return { type: "array", default: [] };
|
|
845
|
+
return { type: "string" };
|
|
846
|
+
}
|
|
847
|
+
function expandStep(step) {
|
|
848
|
+
switch (step.type ?? "agent") {
|
|
849
|
+
case "agent":
|
|
850
|
+
return expandAgent(step);
|
|
851
|
+
case "human":
|
|
852
|
+
return expandHuman(step);
|
|
853
|
+
case "condition":
|
|
854
|
+
return expandCondition(step);
|
|
855
|
+
case "map":
|
|
856
|
+
return expandMap(step);
|
|
857
|
+
case "parallel":
|
|
858
|
+
return expandParallel(step);
|
|
859
|
+
case "end":
|
|
860
|
+
return expandEnd(step);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function validateSchema(schema, stepId) {
|
|
864
|
+
if (schema.type !== "object") {
|
|
865
|
+
throw new Error(
|
|
866
|
+
`Step "${stepId}": schema.type must be "object", got "${String(schema.type)}". Use standard JSON Schema: { "type": "object", "properties": { ... } }`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
if (!schema.properties || typeof schema.properties !== "object") {
|
|
870
|
+
throw new Error(
|
|
871
|
+
`Step "${stepId}": schema must have a "properties" object. Legacy shorthand like { "field": "string" } is not supported. Use: { "type": "object", "properties": { "field": { "type": "string" } } }`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function expandAgent(s) {
|
|
876
|
+
const sid = s.id || uid("agent");
|
|
877
|
+
const nid = nodeId(sid);
|
|
878
|
+
if (s.schema !== void 0 && s.schema !== null) {
|
|
879
|
+
if (typeof s.schema !== "object") {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`Step "${sid}": schema must be a JSON Schema object, e.g. { "type": "object", "properties": { ... } }. Got ${typeof s.schema} (${JSON.stringify(s.schema)}).`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
validateSchema(s.schema, sid);
|
|
885
|
+
}
|
|
886
|
+
const schema = s.schema && typeof s.schema === "object" ? s.schema : void 0;
|
|
887
|
+
console.log(`[WF EXPAND] agent step id="${sid}" nodeId="${nid}" | hasSchema=${!!schema}`);
|
|
888
|
+
return {
|
|
889
|
+
nodes: [{ id: nid, type: "agent", name: sid, input: { template: translate(s.prompt) }, output: { key: sid, ...schema ? { schema } : {} } }],
|
|
890
|
+
edges: [],
|
|
891
|
+
entryId: nid,
|
|
892
|
+
exitIds: [nid]
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
function expandHuman(s) {
|
|
896
|
+
const sid = s.id || uid("human");
|
|
897
|
+
const nid = nodeId(sid);
|
|
898
|
+
if (s.schema !== void 0 && s.schema !== null) {
|
|
899
|
+
if (typeof s.schema !== "object") {
|
|
900
|
+
throw new Error(
|
|
901
|
+
`Step "${sid}": schema must be a JSON Schema object, e.g. { "type": "object", "properties": { ... } }. Got ${typeof s.schema} (${JSON.stringify(s.schema)}).`
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
validateSchema(s.schema, sid);
|
|
905
|
+
}
|
|
906
|
+
const schema = s.schema && typeof s.schema === "object" ? s.schema : void 0;
|
|
907
|
+
return {
|
|
908
|
+
nodes: [{ id: nid, type: "human_feedback", name: sid, config: { title: s.title ?? sid }, input: { template: translate(s.prompt) }, output: { key: sid, ...schema ? { schema } : {} } }],
|
|
909
|
+
edges: [],
|
|
910
|
+
entryId: nid,
|
|
911
|
+
exitIds: [nid]
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function expandCondition(s) {
|
|
915
|
+
const sid = s.id || uid("cond");
|
|
916
|
+
const nid = nodeId(sid);
|
|
917
|
+
if (s.branches) {
|
|
918
|
+
const branchExps = {};
|
|
919
|
+
for (const [key, steps] of Object.entries(s.branches)) {
|
|
920
|
+
branchExps[key] = expandSteps(Array.isArray(steps) ? steps : [steps]);
|
|
921
|
+
}
|
|
922
|
+
const allNodes = Object.values(branchExps).flatMap((e) => e.nodes);
|
|
923
|
+
const allEdges = Object.values(branchExps).flatMap((e) => e.edges);
|
|
924
|
+
const allExitIds = [...new Set(Object.values(branchExps).flatMap((e) => e.exitIds))];
|
|
925
|
+
const branchEntryIds = Object.fromEntries(
|
|
926
|
+
Object.entries(branchExps).map(([k, v]) => [k, v.entryId])
|
|
927
|
+
);
|
|
928
|
+
return {
|
|
929
|
+
nodes: allNodes,
|
|
930
|
+
edges: allEdges,
|
|
931
|
+
entryId: nid,
|
|
932
|
+
exitIds: allExitIds,
|
|
933
|
+
branchEntryIds
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
if (!s.then) {
|
|
937
|
+
throw new Error(`Condition step "${sid}": must have either "then" or "branches"`);
|
|
938
|
+
}
|
|
939
|
+
const thenSteps = Array.isArray(s.then) ? s.then : [s.then];
|
|
940
|
+
const elseSteps = s.else ? Array.isArray(s.else) ? s.else : [s.else] : [];
|
|
941
|
+
const thenExp = expandSteps(thenSteps);
|
|
942
|
+
const elseExp = expandSteps(elseSteps);
|
|
943
|
+
return {
|
|
944
|
+
nodes: [...thenExp.nodes, ...elseExp.nodes],
|
|
945
|
+
edges: [...thenExp.edges, ...elseExp.edges],
|
|
946
|
+
entryId: nid,
|
|
947
|
+
exitIds: [...thenExp.exitIds, ...elseExp.exitIds],
|
|
948
|
+
thenEntryId: thenExp.entryId,
|
|
949
|
+
elseEntryId: elseExp.entryId
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function expandMap(s) {
|
|
953
|
+
const sid = s.id;
|
|
954
|
+
const nid = nodeId(sid);
|
|
955
|
+
const inner = s.each;
|
|
956
|
+
return {
|
|
957
|
+
nodes: [{
|
|
958
|
+
id: nid,
|
|
959
|
+
type: "map",
|
|
960
|
+
name: sid,
|
|
961
|
+
source: `state.${s.source}`,
|
|
962
|
+
itemKey: "item",
|
|
963
|
+
config: { batchSize: s.batch ?? 50, maxConcurrency: s.concurrency ?? 5, innerConcurrency: s.concurrency ?? 5 },
|
|
964
|
+
node: { type: "agent", input: inner.prompt ? { template: translate(inner.prompt) } : void 0, ...inner.schema && typeof inner.schema === "object" ? { schema: inner.schema } : {} },
|
|
965
|
+
...s.reduce ? { reduce: { input: s.reduce.prompt ? { template: translate(s.reduce.prompt) } : void 0, ...s.reduce.schema && typeof s.reduce.schema === "object" ? { schema: s.reduce.schema } : {} } } : {},
|
|
966
|
+
output: { key: sid }
|
|
967
|
+
}],
|
|
968
|
+
edges: [],
|
|
969
|
+
entryId: nid,
|
|
970
|
+
exitIds: [nid]
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function expandParallel(s) {
|
|
974
|
+
const nodes = [];
|
|
975
|
+
const edges = [];
|
|
976
|
+
const entryIds = [];
|
|
977
|
+
const exitIds = [];
|
|
978
|
+
for (const step of s.steps) {
|
|
979
|
+
const exp = expandStep(step);
|
|
980
|
+
nodes.push(...exp.nodes);
|
|
981
|
+
edges.push(...exp.edges);
|
|
982
|
+
entryIds.push(exp.entryId);
|
|
983
|
+
if (exp.exitIds.length > 0) exitIds.push(...exp.exitIds);
|
|
984
|
+
}
|
|
985
|
+
return { nodes, edges, entryId: entryIds[0], exitIds };
|
|
986
|
+
}
|
|
987
|
+
function expandEnd(s) {
|
|
988
|
+
const sid = uid("end");
|
|
989
|
+
const nid = nodeId(sid);
|
|
990
|
+
return { nodes: [{ id: nid, type: "terminal", name: sid, status: s.status ?? "success" }], edges: [], entryId: nid, exitIds: [] };
|
|
991
|
+
}
|
|
992
|
+
function expandSteps(steps, fromStart = false) {
|
|
993
|
+
if (steps.length === 0) return { nodes: [], edges: [], entryId: "END", exitIds: ["END"] };
|
|
994
|
+
const allNodes = [];
|
|
995
|
+
const allEdges = [];
|
|
996
|
+
let prevExitIds = [];
|
|
997
|
+
let firstEntryId = null;
|
|
998
|
+
for (let i = 0; i < steps.length; i++) {
|
|
999
|
+
const exp = expandStep(steps[i]);
|
|
1000
|
+
if (exp.nodes.length > 0) allNodes.push(...exp.nodes);
|
|
1001
|
+
if (exp.edges.length > 0) allEdges.push(...exp.edges);
|
|
1002
|
+
if (firstEntryId === null) firstEntryId = exp.entryId;
|
|
1003
|
+
if (prevExitIds.length > 0) addEdge(steps[i], exp, prevExitIds, allEdges);
|
|
1004
|
+
else if (fromStart && i === 0) addEdge(steps[i], exp, ["START"], allEdges);
|
|
1005
|
+
prevExitIds = exp.exitIds.length > 0 ? exp.exitIds : prevExitIds;
|
|
1006
|
+
}
|
|
1007
|
+
return { nodes: allNodes, edges: allEdges, entryId: firstEntryId || "END", exitIds: prevExitIds };
|
|
1008
|
+
}
|
|
1009
|
+
function toSafeStateExpr(expr) {
|
|
1010
|
+
const match = expr.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)/);
|
|
1011
|
+
if (!match) return `state.${expr}`;
|
|
1012
|
+
const field = match[0];
|
|
1013
|
+
const rest = expr.slice(field.length);
|
|
1014
|
+
return `state["${field}"]${rest}`;
|
|
1015
|
+
}
|
|
1016
|
+
function addEdge(step, exp, fromIds, allEdges) {
|
|
1017
|
+
if (step.type === "condition") {
|
|
1018
|
+
const s = step;
|
|
1019
|
+
if (s.if.includes("{{")) {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`Condition step "${s.id || "unnamed"}": the \`if\` field contains "{{". Use a plain state field name or JavaScript expression (e.g. "intent", "score >= 60"). Template markers {{...}} are only for \`prompt\` fields, not \`if\`. Found: "${s.if}"`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
if (s.branches) {
|
|
1025
|
+
const condExp = exp;
|
|
1026
|
+
const exprCode = toSafeStateExpr(s.if);
|
|
1027
|
+
const mapping = {};
|
|
1028
|
+
for (const [key, branchId] of Object.entries(condExp.branchEntryIds)) {
|
|
1029
|
+
if (branchId) mapping[key] = branchId;
|
|
1030
|
+
}
|
|
1031
|
+
for (const fromId of fromIds) allEdges.push({ from: fromId, type: "conditional", rule: { type: "expression", code: exprCode, mapping } });
|
|
1032
|
+
} else {
|
|
1033
|
+
const condExp = exp;
|
|
1034
|
+
const exprCode = `${toSafeStateExpr(s.if)} ? 'then' : 'else'`;
|
|
1035
|
+
const mapping = {};
|
|
1036
|
+
if (condExp.thenEntryId) mapping.then = condExp.thenEntryId;
|
|
1037
|
+
if (condExp.elseEntryId) mapping.else = condExp.elseEntryId;
|
|
1038
|
+
for (const fromId of fromIds) allEdges.push({ from: fromId, type: "conditional", rule: { type: "expression", code: exprCode, mapping } });
|
|
1039
|
+
}
|
|
1040
|
+
} else if (step.type === "parallel") {
|
|
1041
|
+
for (const fromId of fromIds) {
|
|
1042
|
+
for (const sub of step.steps) allEdges.push({ from: fromId, to: expandStep(sub).entryId });
|
|
1043
|
+
}
|
|
1044
|
+
} else {
|
|
1045
|
+
for (const fromId of fromIds) allEdges.push({ from: fromId, to: exp.entryId });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function collectIds(steps) {
|
|
1049
|
+
const ids = [];
|
|
1050
|
+
function walk(s) {
|
|
1051
|
+
const id = s.type === "condition" ? s.id || uid("cond") : s.type === "parallel" ? s.id || uid("par") : s.type === "end" ? uid("end") : s.id || s.id || uid("human");
|
|
1052
|
+
if (s.type !== "end" && s.type !== "condition") ids.push(id);
|
|
1053
|
+
if (s.type === "condition") {
|
|
1054
|
+
const c = s;
|
|
1055
|
+
if (c.branches) {
|
|
1056
|
+
for (const steps2 of Object.values(c.branches)) {
|
|
1057
|
+
(Array.isArray(steps2) ? steps2 : [steps2]).forEach(walk);
|
|
1058
|
+
}
|
|
1059
|
+
} else {
|
|
1060
|
+
(Array.isArray(c.then) ? c.then : [c.then]).forEach(walk);
|
|
1061
|
+
if (c.else) (Array.isArray(c.else) ? c.else : [c.else]).forEach(walk);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (s.type === "parallel") s.steps.forEach(walk);
|
|
1065
|
+
}
|
|
1066
|
+
steps.forEach(walk);
|
|
1067
|
+
return [...new Set(ids)];
|
|
1068
|
+
}
|
|
1069
|
+
function collectSourceIds(steps) {
|
|
1070
|
+
const ids = [];
|
|
1071
|
+
function walk(s) {
|
|
1072
|
+
if (s.type === "map") ids.push(s.source);
|
|
1073
|
+
if (s.type === "condition") {
|
|
1074
|
+
const c = s;
|
|
1075
|
+
if (c.branches) {
|
|
1076
|
+
for (const steps2 of Object.values(c.branches)) {
|
|
1077
|
+
(Array.isArray(steps2) ? steps2 : [steps2]).forEach(walk);
|
|
1078
|
+
}
|
|
1079
|
+
} else {
|
|
1080
|
+
(Array.isArray(c.then) ? c.then : [c.then]).forEach(walk);
|
|
1081
|
+
if (c.else) (Array.isArray(c.else) ? c.else : [c.else]).forEach(walk);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (s.type === "parallel") s.steps.forEach(walk);
|
|
1085
|
+
}
|
|
1086
|
+
steps.forEach(walk);
|
|
1087
|
+
return [...new Set(ids)];
|
|
1088
|
+
}
|
|
1089
|
+
function expand(dsl) {
|
|
1090
|
+
_counter = 0;
|
|
1091
|
+
console.log(`[WF EXPAND] expanding workflow "${dsl.name}" | stepCount=${dsl.steps.length}`);
|
|
1092
|
+
const inputNode = {
|
|
1093
|
+
id: "n_input",
|
|
1094
|
+
type: "input",
|
|
1095
|
+
name: "input",
|
|
1096
|
+
output: { key: "input" }
|
|
1097
|
+
};
|
|
1098
|
+
const expanded = expandSteps(dsl.steps, true);
|
|
1099
|
+
const edges = expanded.entryId === "END" ? [{ from: "START", to: "n_input" }] : [
|
|
1100
|
+
...expanded.edges.map((e) => ({ ...e, from: e.from === "START" ? "n_input" : e.from })),
|
|
1101
|
+
{ from: "START", to: "n_input" }
|
|
1102
|
+
];
|
|
1103
|
+
const nodes = [inputNode, ...expanded.nodes];
|
|
1104
|
+
const ids = collectIds(dsl.steps);
|
|
1105
|
+
const sourceIds = collectSourceIds(dsl.steps);
|
|
1106
|
+
const fields = { input: { type: "string" } };
|
|
1107
|
+
for (const id of ids) fields[id] = inferFieldType(id);
|
|
1108
|
+
for (const id of sourceIds) {
|
|
1109
|
+
if (!fields[id]) fields[id] = inferFieldType(id);
|
|
1110
|
+
}
|
|
1111
|
+
const result = { version: "1.0", name: dsl.name, state: { fields }, nodes, edges };
|
|
1112
|
+
console.log(`[WF EXPAND] done | nodeCount=${result.nodes.length} | nodeIds=[${result.nodes.map((n) => `${n.id}:${n.type}`).join(", ")}] | fieldIds=[${Object.keys(fields).join(", ")}]`);
|
|
1113
|
+
return result;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/workflow/compile.ts
|
|
1117
|
+
function compileWorkflow(dsl, resolveAgent, checkpointer, trackingStore) {
|
|
1118
|
+
console.log(`[WF COMPILE] compiling "${dsl.name}" | stepCount=${dsl.steps.length}`);
|
|
1119
|
+
const ir = expand(dsl);
|
|
1120
|
+
validateAndThrow(ir);
|
|
1121
|
+
console.log(`[WF COMPILE] validation passed`);
|
|
1122
|
+
const StateAnnotation = buildStateAnnotation(ir.state?.fields);
|
|
1123
|
+
const builder = new StateGraph(StateAnnotation);
|
|
1124
|
+
for (const node of ir.nodes) {
|
|
1125
|
+
console.log(`[WF COMPILE] registering node: id=${node.id} type=${node.type} name=${node.name}`);
|
|
1126
|
+
const handler = createNodeHandler(node, resolveAgent, trackingStore);
|
|
1127
|
+
builder.addNode(node.id, handler);
|
|
1128
|
+
}
|
|
1129
|
+
for (const node of ir.nodes) {
|
|
1130
|
+
if (node.type === "terminal") {
|
|
1131
|
+
builder.addEdge(node.id, END);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
for (const edge of ir.edges) {
|
|
1135
|
+
const from = edge.from === "START" ? START : edge.from;
|
|
1136
|
+
if (edge.type === "conditional") {
|
|
1137
|
+
if (!edge.rule) {
|
|
1138
|
+
throw new Error(`Conditional edge from "${edge.from}" has no rule`);
|
|
1139
|
+
}
|
|
1140
|
+
const router = buildRouter(edge.rule);
|
|
1141
|
+
builder.addConditionalEdges(from, router, edge.rule.mapping);
|
|
1142
|
+
} else {
|
|
1143
|
+
const targets = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1144
|
+
for (const to of targets) {
|
|
1145
|
+
builder.addEdge(from, to === "END" ? END : to);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
const graph = builder.compile({ checkpointer, name: ir.name });
|
|
1150
|
+
console.log(`[WF COMPILE] graph compiled successfully, ready to run`);
|
|
1151
|
+
return graph;
|
|
1152
|
+
}
|
|
1153
|
+
function validateDSL(dsl) {
|
|
1154
|
+
const errors = [];
|
|
1155
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
1156
|
+
for (const node of dsl.nodes) {
|
|
1157
|
+
if (nodeIds.has(node.id)) {
|
|
1158
|
+
errors.push({ type: "error", message: `Duplicate node id "${node.id}"` });
|
|
1159
|
+
}
|
|
1160
|
+
nodeIds.add(node.id);
|
|
1161
|
+
}
|
|
1162
|
+
const terminalIds = new Set(
|
|
1163
|
+
dsl.nodes.filter((n) => n.type === "terminal").map((n) => n.id)
|
|
1164
|
+
);
|
|
1165
|
+
const declaredFields = new Set(Object.keys(dsl.state?.fields ?? {}));
|
|
1166
|
+
for (const edge of dsl.edges) {
|
|
1167
|
+
if (edge.from !== "START" && !nodeIds.has(edge.from)) {
|
|
1168
|
+
errors.push({ type: "error", message: `Edge from "${edge.from}" references unknown node` });
|
|
1169
|
+
}
|
|
1170
|
+
if (edge.from !== "START" && terminalIds.has(edge.from)) {
|
|
1171
|
+
errors.push({ type: "error", message: `Terminal node "${edge.from}" cannot have outgoing edges` });
|
|
1172
|
+
}
|
|
1173
|
+
const targets = Array.isArray(edge.to) ? edge.to : edge.to ? [edge.to] : [];
|
|
1174
|
+
for (const to of targets) {
|
|
1175
|
+
if (to !== "END" && !nodeIds.has(to)) {
|
|
1176
|
+
errors.push({ type: "error", message: `Edge to "${to}" references unknown node` });
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (edge.type === "conditional" && edge.rule) {
|
|
1180
|
+
for (const targetId of Object.values(edge.rule.mapping)) {
|
|
1181
|
+
if (targetId !== "END" && !nodeIds.has(targetId)) {
|
|
1182
|
+
errors.push({ type: "error", message: `Conditional edge mapping references unknown node "${targetId}"` });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (edge.rule.type === "state_field" && edge.rule.field) {
|
|
1186
|
+
if (!declaredFields.has(edge.rule.field)) {
|
|
1187
|
+
errors.push({ type: "warning", message: `Conditional edge reads "${edge.rule.field}" but it is not declared in state.fields` });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
for (const node of dsl.nodes) {
|
|
1193
|
+
if (node.type === "terminal") {
|
|
1194
|
+
const hasIncoming = dsl.edges.some((e) => {
|
|
1195
|
+
const targets = Array.isArray(e.to) ? e.to : e.to ? [e.to] : [];
|
|
1196
|
+
return targets.includes(node.id) || e.type === "conditional" && e.rule && Object.values(e.rule.mapping).includes(node.id);
|
|
1197
|
+
});
|
|
1198
|
+
if (!hasIncoming) {
|
|
1199
|
+
errors.push({ type: "warning", message: `Terminal node "${node.id}" has no incoming edge` });
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return errors;
|
|
1204
|
+
}
|
|
1205
|
+
function validateAndThrow(dsl) {
|
|
1206
|
+
const errors = validateDSL(dsl);
|
|
1207
|
+
const critical = errors.filter((e) => e.type === "error");
|
|
1208
|
+
if (critical.length > 0) {
|
|
1209
|
+
throw new Error(critical.map((e) => e.message).join("; "));
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
function buildRouter(rule) {
|
|
1213
|
+
if (rule.type === "state_field") {
|
|
1214
|
+
if (!rule.field) throw new Error("state_field rule requires a field name");
|
|
1215
|
+
return (state) => {
|
|
1216
|
+
const value = resolvePath(state, `state.${rule.field}`);
|
|
1217
|
+
const key = String(value);
|
|
1218
|
+
if (rule.mapping[key] !== void 0) return key;
|
|
1219
|
+
if (rule.mapping["default"] !== void 0) return "default";
|
|
1220
|
+
throw new Error(
|
|
1221
|
+
`Conditional router produced key "${key}" (from field "${rule.field}"), but it is not in mapping: ${JSON.stringify(Object.keys(rule.mapping))}`
|
|
1222
|
+
);
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
if (rule.type === "expression") {
|
|
1226
|
+
if (!rule.code) throw new Error("expression rule requires code");
|
|
1227
|
+
const fn = new Function("state", `return ${rule.code}`);
|
|
1228
|
+
return (state) => {
|
|
1229
|
+
const value = fn(state);
|
|
1230
|
+
const key = String(value);
|
|
1231
|
+
if (rule.mapping[key] !== void 0) return key;
|
|
1232
|
+
if (rule.mapping["default"] !== void 0) return "default";
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`Conditional router produced key "${key}" (from expression "${rule.code}"), but it is not in mapping: ${JSON.stringify(Object.keys(rule.mapping))}`
|
|
1235
|
+
);
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
throw new Error(`Unknown rule type: ${rule.type}`);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
export {
|
|
1242
|
+
buildStateAnnotation,
|
|
1243
|
+
resolvePath,
|
|
1244
|
+
renderTemplate,
|
|
1245
|
+
buildInput,
|
|
1246
|
+
extractOutput,
|
|
1247
|
+
parallelLimit,
|
|
1248
|
+
invokeWithRetry,
|
|
1249
|
+
createAgentNode,
|
|
1250
|
+
createHumanFeedbackNode,
|
|
1251
|
+
createMapNode,
|
|
1252
|
+
createTerminalNode,
|
|
1253
|
+
createNodeHandler,
|
|
1254
|
+
expand,
|
|
1255
|
+
compileWorkflow,
|
|
1256
|
+
validateDSL
|
|
1257
|
+
};
|
|
1258
|
+
//# sourceMappingURL=chunk-FN4TRQK4.mjs.map
|