u-foo 2.5.15 → 3.0.1
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/package.json +1 -1
- package/src/code/agent.js +350 -246
- package/src/code/commands.js +16 -0
- package/src/code/context/assembler.js +18 -13
- package/src/code/context/executionSegment.js +102 -119
- package/src/code/context/index.js +11 -1
- package/src/code/context/planGraph.js +1410 -0
- package/src/code/context/planGraphService.js +857 -0
- package/src/code/context/planMode.js +405 -0
- package/src/code/context/planProjection.js +432 -0
- package/src/code/context/promptLayers.js +21 -5
- package/src/code/context/stateCommit.js +2 -0
- package/src/code/context/toolRuntime.js +172 -0
- package/src/code/context/userInteraction.js +457 -0
- package/src/code/context/userNudge.js +116 -0
- package/src/code/dispatch.js +17 -1
- package/src/code/index.js +4 -0
- package/src/code/nativeRunner.js +589 -172
- package/src/code/protocol/controlPlane.js +93 -0
- package/src/code/protocol/faultHarness.js +90 -0
- package/src/code/protocol/index.js +20 -0
- package/src/code/protocol/loopEvents.js +102 -0
- package/src/code/protocol/materialize.js +107 -0
- package/src/code/protocol/messageFixtures.js +116 -0
- package/src/code/protocol/ownership.js +147 -0
- package/src/code/protocol/protocolValidator.js +165 -0
- package/src/code/protocol/suspension.js +173 -0
- package/src/code/protocol/toolCallLedger.js +222 -0
- package/src/code/protocol/transitions.js +97 -0
- package/src/code/providers/anthropicMessagesTransport.js +93 -0
- package/src/code/providers/index.js +7 -0
- package/src/code/providers/openaiChatTransport.js +98 -0
- package/src/code/providers/transportContract.js +46 -0
- package/src/code/repl.js +147 -18
- package/src/code/runtime/agentWakeup.js +58 -0
- package/src/code/runtime/graphOwner.js +41 -0
- package/src/code/runtime/graphYieldRouter.js +42 -0
- package/src/code/runtime/index.js +15 -0
- package/src/code/runtime/loopMailbox.js +124 -0
- package/src/code/runtime/runtimeEvents.js +39 -0
- package/src/code/runtime/taskControl.js +565 -0
- package/src/code/runtime/taskFocus.js +165 -0
- package/src/code/runtime/taskLoop.js +394 -0
- package/src/code/runtime/taskRun.js +348 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +249 -0
- package/src/code/sessionStore.js +1 -10
- package/src/code/skills/injection.js +1 -0
- package/src/code/taskDecomposer.js +32 -8
- package/src/code/taskRoute.js +73 -0
- package/src/code/tools/askUser.js +11 -0
- package/src/code/tools/planGraph.js +29 -0
- package/src/ui/format/index.js +25 -1
- package/src/ui/format/markdownRenderer.js +224 -2
- package/src/ui/ink/UcodeApp.js +268 -22
- package/src/code/context/featureFlag.js +0 -13
|
@@ -0,0 +1,1410 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unified Execution Plan IR + graph engine.
|
|
5
|
+
*
|
|
6
|
+
* High-level nodes (task / tool / group / checkpoint) compile down to a flat
|
|
7
|
+
* executable graph. Group strategy is sugar over dependsOn. Runtime executes
|
|
8
|
+
* tool nodes; task/checkpoint yield control back to the LLM.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { randomUUID } = require("crypto");
|
|
12
|
+
const {
|
|
13
|
+
recoverExpiredLeases,
|
|
14
|
+
claimSafeReadyToolBatch,
|
|
15
|
+
} = require("./toolRuntime");
|
|
16
|
+
|
|
17
|
+
const NODE_TYPES = Object.freeze(["task", "tool", "group", "checkpoint"]);
|
|
18
|
+
const NODE_STATUSES = Object.freeze([
|
|
19
|
+
"pending",
|
|
20
|
+
"ready",
|
|
21
|
+
"running",
|
|
22
|
+
"waiting_llm",
|
|
23
|
+
"waiting_approval",
|
|
24
|
+
"succeeded",
|
|
25
|
+
"failed",
|
|
26
|
+
"blocked",
|
|
27
|
+
"skipped",
|
|
28
|
+
"cancelled",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const DEFAULT_KNOWN_TOOLS = new Set([
|
|
32
|
+
"read",
|
|
33
|
+
"write",
|
|
34
|
+
"edit",
|
|
35
|
+
"bash",
|
|
36
|
+
"artifact_read",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const TOOL_ALIASES = Object.freeze({
|
|
40
|
+
"code.read": "read",
|
|
41
|
+
"code.write": "write",
|
|
42
|
+
"code.edit": "edit",
|
|
43
|
+
"code.patch": "edit",
|
|
44
|
+
"shell.run": "bash",
|
|
45
|
+
"test.run": "bash",
|
|
46
|
+
"git.diff": "bash",
|
|
47
|
+
"code.search": "bash",
|
|
48
|
+
"code.read_matches": "read",
|
|
49
|
+
"artifact.read": "artifact_read",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const SIDE_EFFECT_TOOLS = new Set(["write", "edit"]);
|
|
53
|
+
const REF_PATTERN = /\$\{([a-zA-Z0-9_-]+)((?:\.[a-zA-Z0-9_]+|\[\d+\])*)\}/g;
|
|
54
|
+
|
|
55
|
+
function createPlanId(prefix = "plan") {
|
|
56
|
+
return `${prefix}_${Date.now().toString(36)}_${randomUUID().slice(0, 6)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cloneJson(value) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(JSON.stringify(value));
|
|
62
|
+
} catch {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeDependsOn(value) {
|
|
68
|
+
if (!Array.isArray(value)) return [];
|
|
69
|
+
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeBaseNode(source = {}, fallbackId = "") {
|
|
73
|
+
const id = String(source.id || fallbackId || "").trim();
|
|
74
|
+
const type = String(source.type || "").trim().toLowerCase();
|
|
75
|
+
return {
|
|
76
|
+
id,
|
|
77
|
+
type,
|
|
78
|
+
dependsOn: normalizeDependsOn(source.dependsOn || source.depends_on),
|
|
79
|
+
status: String(source.status || "pending").trim().toLowerCase() || "pending",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizePlanNode(source = {}, fallbackId = "node") {
|
|
84
|
+
const base = normalizeBaseNode(source, fallbackId);
|
|
85
|
+
if (base.type === "tool") {
|
|
86
|
+
return {
|
|
87
|
+
...base,
|
|
88
|
+
type: "tool",
|
|
89
|
+
tool: String(source.tool || "").trim(),
|
|
90
|
+
args: source.args && typeof source.args === "object" ? cloneJson(source.args) : {},
|
|
91
|
+
parentTaskId: String(source.parentTaskId || "").trim(),
|
|
92
|
+
createdSeq: Number.isFinite(source.createdSeq) ? Math.floor(source.createdSeq) : 0,
|
|
93
|
+
generated: Boolean(source.generated),
|
|
94
|
+
displayOrder: Number.isFinite(source.displayOrder) ? Math.floor(source.displayOrder) : 0,
|
|
95
|
+
attempt: Number.isFinite(source.attempt) ? Math.max(0, Math.floor(source.attempt)) : 0,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (base.type === "task") {
|
|
99
|
+
const title = String(source.title || "").trim();
|
|
100
|
+
const objective = String(source.objective || title || "").trim();
|
|
101
|
+
let execution;
|
|
102
|
+
if (source.execution && typeof source.execution === "object" && !Array.isArray(source.execution)) {
|
|
103
|
+
const kindRaw = String(source.execution.kind || "inline_llm").trim().toLowerCase();
|
|
104
|
+
let kind = "inline_llm";
|
|
105
|
+
if (kindRaw === "llm") kind = "inline_llm";
|
|
106
|
+
else if (kindRaw === "inline_llm") kind = "inline_llm";
|
|
107
|
+
else if (kindRaw === "expand") kind = "expand";
|
|
108
|
+
else if (kindRaw === "aggregate") kind = "aggregate";
|
|
109
|
+
else if (kindRaw === "task_loop") kind = "task_loop";
|
|
110
|
+
else kind = kindRaw || "inline_llm";
|
|
111
|
+
execution = { ...cloneJson(source.execution), kind };
|
|
112
|
+
// Illegal combinations: expand/inline_llm must not carry workspace/merge
|
|
113
|
+
if (kind !== "task_loop") {
|
|
114
|
+
delete execution.workspace;
|
|
115
|
+
delete execution.completion;
|
|
116
|
+
delete execution.acceptance;
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
const rawExec = String(source.execution || "llm").trim().toLowerCase();
|
|
120
|
+
let kind = "inline_llm";
|
|
121
|
+
if (rawExec === "expand") kind = "expand";
|
|
122
|
+
else if (rawExec === "aggregate") kind = "aggregate";
|
|
123
|
+
else if (rawExec === "task_loop") kind = "task_loop";
|
|
124
|
+
else if (rawExec === "inline_llm" || rawExec === "llm" || !rawExec) kind = "inline_llm";
|
|
125
|
+
execution = { kind };
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
...base,
|
|
129
|
+
type: "task",
|
|
130
|
+
title: title || objective,
|
|
131
|
+
objective,
|
|
132
|
+
successCriteria: Array.isArray(source.successCriteria)
|
|
133
|
+
? source.successCriteria.map(String)
|
|
134
|
+
: (Array.isArray(source.success_criteria) ? source.success_criteria.map(String) : []),
|
|
135
|
+
execution,
|
|
136
|
+
inputs: source.inputs && typeof source.inputs === "object" ? cloneJson(source.inputs) : {},
|
|
137
|
+
parentTaskId: String(source.parentTaskId || "").trim(),
|
|
138
|
+
createdSeq: Number.isFinite(source.createdSeq) ? Math.floor(source.createdSeq) : 0,
|
|
139
|
+
generated: Boolean(source.generated),
|
|
140
|
+
displayOrder: Number.isFinite(source.displayOrder) ? Math.floor(source.displayOrder) : 0,
|
|
141
|
+
attempt: Number.isFinite(source.attempt) ? Math.max(0, Math.floor(source.attempt)) : 0,
|
|
142
|
+
runtime: source.runtime && typeof source.runtime === "object" ? cloneJson(source.runtime) : undefined,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (base.type === "checkpoint") {
|
|
146
|
+
return {
|
|
147
|
+
...base,
|
|
148
|
+
type: "checkpoint",
|
|
149
|
+
mode: String(source.mode || "llm").trim().toLowerCase() === "approval"
|
|
150
|
+
? "approval"
|
|
151
|
+
: "llm",
|
|
152
|
+
reason: String(source.reason || "").trim(),
|
|
153
|
+
stopKind: String(source.stopKind || "checkpoint").trim() || "checkpoint",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (base.type === "group") {
|
|
157
|
+
const strategy = String(source.strategy || "sequence").trim().toLowerCase() === "parallel"
|
|
158
|
+
? "parallel"
|
|
159
|
+
: "sequence";
|
|
160
|
+
const childrenSource = Array.isArray(source.children) ? source.children : [];
|
|
161
|
+
return {
|
|
162
|
+
...base,
|
|
163
|
+
type: "group",
|
|
164
|
+
strategy,
|
|
165
|
+
children: childrenSource.map((child, index) => (
|
|
166
|
+
normalizePlanNode(child, `${base.id || fallbackId}_c${index + 1}`)
|
|
167
|
+
)),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (source.tool) {
|
|
171
|
+
return {
|
|
172
|
+
...base,
|
|
173
|
+
type: "tool",
|
|
174
|
+
id: base.id || fallbackId,
|
|
175
|
+
tool: String(source.tool || "").trim(),
|
|
176
|
+
args: source.args && typeof source.args === "object" ? cloneJson(source.args) : {},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
...base,
|
|
181
|
+
type: base.type || "task",
|
|
182
|
+
title: String(source.title || source.objective || source.goal || "").trim(),
|
|
183
|
+
objective: String(source.objective || source.title || source.goal || "").trim(),
|
|
184
|
+
execution: "llm",
|
|
185
|
+
successCriteria: [],
|
|
186
|
+
inputs: {},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizePlanGraph(input = {}) {
|
|
191
|
+
const source = input && typeof input === "object" ? input : {};
|
|
192
|
+
const failurePolicy = String(source.failurePolicy || "continue_independent").trim().toLowerCase() === "fail_fast"
|
|
193
|
+
? "fail_fast"
|
|
194
|
+
: "continue_independent";
|
|
195
|
+
if (Array.isArray(source.nodes)) {
|
|
196
|
+
return {
|
|
197
|
+
id: String(source.id || createPlanId("plan")).trim(),
|
|
198
|
+
objective: String(source.objective || "").trim(),
|
|
199
|
+
failurePolicy,
|
|
200
|
+
nodes: source.nodes.map((node, index) => normalizePlanNode(node, `n${index + 1}`)),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (source.type === "group" || Array.isArray(source.children)) {
|
|
204
|
+
const root = normalizePlanNode({ ...source, type: "group" }, "root");
|
|
205
|
+
return {
|
|
206
|
+
id: String(source.id || createPlanId("plan")).trim(),
|
|
207
|
+
objective: String(source.objective || "").trim(),
|
|
208
|
+
failurePolicy,
|
|
209
|
+
nodes: [root],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (Array.isArray(source.steps)) {
|
|
213
|
+
const segmentGraph = planGraphFromExecutionSegment(source);
|
|
214
|
+
return { ...segmentGraph, failurePolicy };
|
|
215
|
+
}
|
|
216
|
+
if (source.type || source.tool || source.objective) {
|
|
217
|
+
return {
|
|
218
|
+
id: String(source.id || createPlanId("plan")).trim(),
|
|
219
|
+
objective: String(source.objective || "").trim(),
|
|
220
|
+
failurePolicy,
|
|
221
|
+
nodes: [normalizePlanNode(source, "n1")],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return { id: createPlanId("plan"), objective: "", failurePolicy, nodes: [] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function planGraphFromExecutionSegment(segment = {}) {
|
|
228
|
+
const source = segment && typeof segment === "object" ? segment : {};
|
|
229
|
+
const steps = Array.isArray(source.steps) ? source.steps : [];
|
|
230
|
+
const checkpointAfter = new Set(
|
|
231
|
+
Array.isArray(source.checkpoint && source.checkpoint.after)
|
|
232
|
+
? source.checkpoint.after.map(String)
|
|
233
|
+
: [],
|
|
234
|
+
);
|
|
235
|
+
const nodes = steps.map((step, index) => normalizePlanNode({
|
|
236
|
+
...step,
|
|
237
|
+
type: "tool",
|
|
238
|
+
id: step.id || `s${index + 1}`,
|
|
239
|
+
}, `s${index + 1}`));
|
|
240
|
+
|
|
241
|
+
// Legacy segments run in array order; chain adjacent steps unless already dependent.
|
|
242
|
+
const expanded = [];
|
|
243
|
+
let previousId = null;
|
|
244
|
+
for (const node of nodes) {
|
|
245
|
+
const deps = normalizeDependsOn(node.dependsOn);
|
|
246
|
+
if (previousId && !deps.includes(previousId)) deps.push(previousId);
|
|
247
|
+
const toolNode = { ...node, dependsOn: deps };
|
|
248
|
+
expanded.push(toolNode);
|
|
249
|
+
previousId = toolNode.id;
|
|
250
|
+
|
|
251
|
+
if (checkpointAfter.has(toolNode.id)) {
|
|
252
|
+
const checkpoint = {
|
|
253
|
+
id: `${toolNode.id}__checkpoint`,
|
|
254
|
+
type: "checkpoint",
|
|
255
|
+
mode: "llm",
|
|
256
|
+
reason: `checkpoint after ${toolNode.id}`,
|
|
257
|
+
dependsOn: [toolNode.id],
|
|
258
|
+
status: "pending",
|
|
259
|
+
stopKind: "checkpoint",
|
|
260
|
+
};
|
|
261
|
+
expanded.push(checkpoint);
|
|
262
|
+
previousId = checkpoint.id;
|
|
263
|
+
} else if (SIDE_EFFECT_TOOLS.has(String(toolNode.tool || "").toLowerCase())) {
|
|
264
|
+
const checkpoint = {
|
|
265
|
+
id: `${toolNode.id}__side_effect_gate`,
|
|
266
|
+
type: "checkpoint",
|
|
267
|
+
mode: "llm",
|
|
268
|
+
reason: `side-effect tool ${toolNode.tool} requires review`,
|
|
269
|
+
dependsOn: [toolNode.id],
|
|
270
|
+
status: "pending",
|
|
271
|
+
stopKind: "side_effect",
|
|
272
|
+
};
|
|
273
|
+
expanded.push(checkpoint);
|
|
274
|
+
previousId = checkpoint.id;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
id: String(source.id || createPlanId("seg")).trim(),
|
|
280
|
+
objective: String(source.objective || "").trim(),
|
|
281
|
+
failurePolicy: String(source.failurePolicy || "continue_independent").trim().toLowerCase() === "fail_fast"
|
|
282
|
+
? "fail_fast"
|
|
283
|
+
: "continue_independent",
|
|
284
|
+
nodes: expanded,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function collectRefsFromValue(value, out = new Set()) {
|
|
289
|
+
if (typeof value === "string") {
|
|
290
|
+
REF_PATTERN.lastIndex = 0;
|
|
291
|
+
let match = REF_PATTERN.exec(value);
|
|
292
|
+
while (match) {
|
|
293
|
+
out.add(match[1]);
|
|
294
|
+
match = REF_PATTERN.exec(value);
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
if (Array.isArray(value)) {
|
|
299
|
+
for (const item of value) collectRefsFromValue(item, out);
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
if (value && typeof value === "object") {
|
|
303
|
+
if (value.$ref && typeof value.$ref === "object") {
|
|
304
|
+
const nodeId = String(value.$ref.node || value.$ref.nodeId || "").trim();
|
|
305
|
+
if (nodeId) out.add(nodeId);
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
if (value.$template != null) {
|
|
309
|
+
collectRefsFromValue(value.$template, out);
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
for (const item of Object.values(value)) collectRefsFromValue(item, out);
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function resolveToolName(tool = "") {
|
|
318
|
+
const raw = String(tool || "").trim();
|
|
319
|
+
if (!raw) return "";
|
|
320
|
+
const lower = raw.toLowerCase();
|
|
321
|
+
if (TOOL_ALIASES[lower]) return TOOL_ALIASES[lower];
|
|
322
|
+
return raw;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function flattenPlanNodes(nodes = [], parentDependsOn = [], {
|
|
326
|
+
sequential = false,
|
|
327
|
+
groupSinks = null,
|
|
328
|
+
} = {}) {
|
|
329
|
+
const out = [];
|
|
330
|
+
const sinks = groupSinks || new Map();
|
|
331
|
+
const list = Array.isArray(nodes) ? nodes : [];
|
|
332
|
+
let previousInSequence = null;
|
|
333
|
+
|
|
334
|
+
for (const node of list) {
|
|
335
|
+
if (!node || typeof node !== "object") continue;
|
|
336
|
+
if (node.type === "group") {
|
|
337
|
+
const childDepends = parentDependsOn.slice();
|
|
338
|
+
if (sequential && previousInSequence) childDepends.push(previousInSequence);
|
|
339
|
+
const startIndex = out.length;
|
|
340
|
+
if (node.strategy === "sequence") {
|
|
341
|
+
let seqPrev = null;
|
|
342
|
+
for (const child of node.children || []) {
|
|
343
|
+
const deps = normalizeDependsOn(child.dependsOn).concat(childDepends);
|
|
344
|
+
if (seqPrev) deps.push(seqPrev);
|
|
345
|
+
const flattened = flattenPlanNodes(
|
|
346
|
+
[{ ...child, dependsOn: deps }],
|
|
347
|
+
[],
|
|
348
|
+
{ sequential: false, groupSinks: sinks },
|
|
349
|
+
);
|
|
350
|
+
const leaves = Array.isArray(flattened) ? flattened : flattened.nodes;
|
|
351
|
+
out.push(...leaves);
|
|
352
|
+
if (leaves.length > 0) seqPrev = leaves[leaves.length - 1].id;
|
|
353
|
+
}
|
|
354
|
+
const groupLeaves = out.slice(startIndex);
|
|
355
|
+
if (node.id) {
|
|
356
|
+
sinks.set(
|
|
357
|
+
node.id,
|
|
358
|
+
groupLeaves.length > 0 ? [groupLeaves[groupLeaves.length - 1].id] : [],
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
previousInSequence = sequential
|
|
362
|
+
? (groupLeaves.length > 0 ? groupLeaves[groupLeaves.length - 1].id : previousInSequence)
|
|
363
|
+
: null;
|
|
364
|
+
} else {
|
|
365
|
+
const parallelLeaves = [];
|
|
366
|
+
for (const child of node.children || []) {
|
|
367
|
+
const deps = normalizeDependsOn(child.dependsOn).concat(childDepends);
|
|
368
|
+
const flattened = flattenPlanNodes(
|
|
369
|
+
[{ ...child, dependsOn: deps }],
|
|
370
|
+
[],
|
|
371
|
+
{ sequential: false, groupSinks: sinks },
|
|
372
|
+
);
|
|
373
|
+
const leaves = Array.isArray(flattened) ? flattened : flattened.nodes;
|
|
374
|
+
out.push(...leaves);
|
|
375
|
+
for (const leaf of leaves) parallelLeaves.push(leaf.id);
|
|
376
|
+
}
|
|
377
|
+
if (node.id) sinks.set(node.id, parallelLeaves.slice());
|
|
378
|
+
previousInSequence = null;
|
|
379
|
+
}
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const deps = normalizeDependsOn(node.dependsOn).concat(parentDependsOn);
|
|
384
|
+
if (sequential && previousInSequence) deps.push(previousInSequence);
|
|
385
|
+
out.push({
|
|
386
|
+
...node,
|
|
387
|
+
dependsOn: Array.from(new Set(deps)),
|
|
388
|
+
});
|
|
389
|
+
previousInSequence = node.id;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (groupSinks) return out;
|
|
393
|
+
return { nodes: out, groupSinks: sinks };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function rewriteGroupDependencies(nodes = [], groupSinks = new Map()) {
|
|
397
|
+
if (!groupSinks || groupSinks.size === 0) return nodes;
|
|
398
|
+
return nodes.map((node) => {
|
|
399
|
+
const nextDeps = [];
|
|
400
|
+
for (const dep of normalizeDependsOn(node.dependsOn)) {
|
|
401
|
+
if (groupSinks.has(dep)) {
|
|
402
|
+
nextDeps.push(...groupSinks.get(dep));
|
|
403
|
+
} else {
|
|
404
|
+
nextDeps.push(dep);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
...node,
|
|
409
|
+
dependsOn: Array.from(new Set(nextDeps)),
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function detectCycles(nodes = []) {
|
|
415
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
416
|
+
const visiting = new Set();
|
|
417
|
+
const visited = new Set();
|
|
418
|
+
const stack = [];
|
|
419
|
+
|
|
420
|
+
function visit(id) {
|
|
421
|
+
if (visited.has(id)) return null;
|
|
422
|
+
if (visiting.has(id)) {
|
|
423
|
+
const start = stack.indexOf(id);
|
|
424
|
+
return stack.slice(start >= 0 ? start : 0).concat([id]);
|
|
425
|
+
}
|
|
426
|
+
visiting.add(id);
|
|
427
|
+
stack.push(id);
|
|
428
|
+
const node = byId.get(id);
|
|
429
|
+
for (const dep of (node && node.dependsOn) || []) {
|
|
430
|
+
const cycle = visit(dep);
|
|
431
|
+
if (cycle) return cycle;
|
|
432
|
+
}
|
|
433
|
+
stack.pop();
|
|
434
|
+
visiting.delete(id);
|
|
435
|
+
visited.add(id);
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
for (const node of nodes) {
|
|
440
|
+
const cycle = visit(node.id);
|
|
441
|
+
if (cycle) return cycle;
|
|
442
|
+
}
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function compilePlanGraph(input = {}, options = {}) {
|
|
447
|
+
const knownTools = options.knownTools instanceof Set
|
|
448
|
+
? options.knownTools
|
|
449
|
+
: new Set([
|
|
450
|
+
...DEFAULT_KNOWN_TOOLS,
|
|
451
|
+
...(Array.isArray(options.knownTools) ? options.knownTools.map((t) => String(t).toLowerCase()) : []),
|
|
452
|
+
]);
|
|
453
|
+
const plan = normalizePlanGraph(input);
|
|
454
|
+
const errors = [];
|
|
455
|
+
const warnings = [];
|
|
456
|
+
const flatResult = flattenPlanNodes(plan.nodes);
|
|
457
|
+
const flattened = rewriteGroupDependencies(flatResult.nodes, flatResult.groupSinks);
|
|
458
|
+
const byId = new Map();
|
|
459
|
+
|
|
460
|
+
for (const node of flattened) {
|
|
461
|
+
if (!node.id) {
|
|
462
|
+
errors.push("node missing id");
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (byId.has(node.id)) {
|
|
466
|
+
errors.push(`duplicate node id: ${node.id}`);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
byId.set(node.id, {
|
|
470
|
+
...node,
|
|
471
|
+
dependsOn: normalizeDependsOn(node.dependsOn),
|
|
472
|
+
status: "pending",
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
for (const node of byId.values()) {
|
|
477
|
+
const refs = new Set();
|
|
478
|
+
collectRefsFromValue(node.args, refs);
|
|
479
|
+
collectRefsFromValue(node.inputs, refs);
|
|
480
|
+
for (const ref of refs) {
|
|
481
|
+
if (ref === node.id) {
|
|
482
|
+
errors.push(`node ${node.id} references itself`);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (!byId.has(ref)) {
|
|
486
|
+
errors.push(`node ${node.id} references unknown node ${ref}`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (!node.dependsOn.includes(ref)) {
|
|
490
|
+
node.dependsOn.push(ref);
|
|
491
|
+
warnings.push(`inferred dependsOn ${ref} for ${node.id}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
for (const node of byId.values()) {
|
|
497
|
+
if (node.type !== "tool") continue;
|
|
498
|
+
const resolved = resolveToolName(node.tool);
|
|
499
|
+
if (!resolved) {
|
|
500
|
+
errors.push(`tool node ${node.id} missing tool`);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
node.tool = resolved;
|
|
504
|
+
if (knownTools.size > 0 && !knownTools.has(resolved.toLowerCase())) {
|
|
505
|
+
warnings.push(`unknown tool ${resolved} on node ${node.id}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
for (const node of byId.values()) {
|
|
510
|
+
node.dependsOn = node.dependsOn.filter((dep) => {
|
|
511
|
+
if (byId.has(dep)) return true;
|
|
512
|
+
errors.push(`node ${node.id} depends on missing node ${dep}`);
|
|
513
|
+
return false;
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const executableNodes = Array.from(byId.values());
|
|
518
|
+
const cycle = detectCycles(executableNodes);
|
|
519
|
+
if (cycle) errors.push(`cycle detected: ${cycle.join(" -> ")}`);
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
ok: errors.length === 0,
|
|
523
|
+
errors,
|
|
524
|
+
warnings,
|
|
525
|
+
planId: plan.id,
|
|
526
|
+
objective: plan.objective,
|
|
527
|
+
failurePolicy: plan.failurePolicy || "continue_independent",
|
|
528
|
+
nodes: executableNodes,
|
|
529
|
+
nodeMap: byId,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function getPathValue(root, pathExpr = "") {
|
|
534
|
+
if (!pathExpr) return root;
|
|
535
|
+
const tokens = [];
|
|
536
|
+
const re = /\.([a-zA-Z0-9_]+)|\[(\d+)\]/g;
|
|
537
|
+
let match = re.exec(pathExpr);
|
|
538
|
+
while (match) {
|
|
539
|
+
tokens.push(match[1] != null ? match[1] : Number(match[2]));
|
|
540
|
+
match = re.exec(pathExpr);
|
|
541
|
+
}
|
|
542
|
+
let cur = root;
|
|
543
|
+
for (const token of tokens) {
|
|
544
|
+
if (cur == null) return undefined;
|
|
545
|
+
cur = cur[token];
|
|
546
|
+
}
|
|
547
|
+
return cur;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function resolveRefToken(token, outputs = new Map()) {
|
|
551
|
+
const match = String(token || "").match(/^([a-zA-Z0-9_-]+)((?:\.[a-zA-Z0-9_]+|\[\d+\])*)$/);
|
|
552
|
+
if (!match) return undefined;
|
|
553
|
+
const nodeId = match[1];
|
|
554
|
+
const pathExpr = match[2] || "";
|
|
555
|
+
const record = outputs.get(nodeId);
|
|
556
|
+
if (!record) return undefined;
|
|
557
|
+
if (!pathExpr) return record;
|
|
558
|
+
return getPathValue(record, pathExpr);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function resolveTemplateString(text = "", outputs = new Map()) {
|
|
562
|
+
const source = String(text || "");
|
|
563
|
+
const full = source.match(/^\$\{([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_]+|\[\d+\])*)\}$/);
|
|
564
|
+
if (full) return resolveRefToken(full[1], outputs);
|
|
565
|
+
return source.replace(REF_PATTERN, (_, nodeId, pathExpr = "") => {
|
|
566
|
+
const value = resolveRefToken(`${nodeId}${pathExpr || ""}`, outputs);
|
|
567
|
+
if (value == null) return "";
|
|
568
|
+
if (typeof value === "string") return value;
|
|
569
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
570
|
+
try { return JSON.stringify(value); } catch { return String(value); }
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function resolveStructuredRef(ref = {}, outputs = new Map()) {
|
|
575
|
+
const nodeId = String(ref.node || ref.nodeId || "").trim();
|
|
576
|
+
if (!nodeId) return undefined;
|
|
577
|
+
const record = outputs.get(nodeId);
|
|
578
|
+
if (!record) return undefined;
|
|
579
|
+
const pointer = String(ref.pointer || "").trim();
|
|
580
|
+
if (!pointer || pointer === "/") return record;
|
|
581
|
+
// JSON pointer-ish: /output/field or /summary
|
|
582
|
+
const pathExpr = pointer
|
|
583
|
+
.replace(/^\//, "")
|
|
584
|
+
.split("/")
|
|
585
|
+
.filter(Boolean)
|
|
586
|
+
.map((part) => ( /^\d+$/.test(part) ? `[${part}]` : `.${part}` ))
|
|
587
|
+
.join("");
|
|
588
|
+
return getPathValue(record, pathExpr);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function resolveValue(value, outputs = new Map()) {
|
|
592
|
+
if (typeof value === "string") return resolveTemplateString(value, outputs);
|
|
593
|
+
if (Array.isArray(value)) return value.map((item) => resolveValue(item, outputs));
|
|
594
|
+
if (value && typeof value === "object") {
|
|
595
|
+
if (value.$ref && typeof value.$ref === "object") {
|
|
596
|
+
return resolveStructuredRef(value.$ref, outputs);
|
|
597
|
+
}
|
|
598
|
+
if (Object.prototype.hasOwnProperty.call(value, "$template")) {
|
|
599
|
+
const template = value.$template;
|
|
600
|
+
if (typeof template === "string") return resolveTemplateString(template, outputs);
|
|
601
|
+
return resolveValue(template, outputs);
|
|
602
|
+
}
|
|
603
|
+
const out = {};
|
|
604
|
+
for (const [key, item] of Object.entries(value)) out[key] = resolveValue(item, outputs);
|
|
605
|
+
return out;
|
|
606
|
+
}
|
|
607
|
+
return value;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function normalizeNodeResult(result = {}) {
|
|
611
|
+
const source = result && typeof result === "object" ? result : {};
|
|
612
|
+
const ok = source.ok !== false && source.status !== "failed";
|
|
613
|
+
return {
|
|
614
|
+
status: ok ? "succeeded" : "failed",
|
|
615
|
+
output: source.output != null
|
|
616
|
+
? source.output
|
|
617
|
+
: (source.result != null ? source.result : (ok ? source : {})),
|
|
618
|
+
artifacts: Array.isArray(source.artifacts)
|
|
619
|
+
? source.artifacts
|
|
620
|
+
: (source.artifactId ? [{ id: source.artifactId }] : []),
|
|
621
|
+
summary: String(source.summary || "").trim(),
|
|
622
|
+
error: String(source.error || "").trim(),
|
|
623
|
+
artifactId: String(
|
|
624
|
+
source.artifactId
|
|
625
|
+
|| (source.artifacts && source.artifacts[0] && source.artifacts[0].id)
|
|
626
|
+
|| "",
|
|
627
|
+
).trim(),
|
|
628
|
+
raw: source,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function compareNodeOrder(a, b) {
|
|
633
|
+
const seqA = Number.isFinite(a.createdSeq) ? a.createdSeq : 0;
|
|
634
|
+
const seqB = Number.isFinite(b.createdSeq) ? b.createdSeq : 0;
|
|
635
|
+
if (seqA !== seqB) return seqA - seqB;
|
|
636
|
+
return String(a.id || "").localeCompare(String(b.id || ""));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function getReadyNodes(nodeMap = new Map()) {
|
|
640
|
+
const ready = [];
|
|
641
|
+
for (const node of nodeMap.values()) {
|
|
642
|
+
if (node.status !== "pending") continue;
|
|
643
|
+
const deps = normalizeDependsOn(node.dependsOn);
|
|
644
|
+
const allSucceeded = deps.every((depId) => {
|
|
645
|
+
const dep = nodeMap.get(depId);
|
|
646
|
+
return dep && dep.status === "succeeded";
|
|
647
|
+
});
|
|
648
|
+
if (allSucceeded) ready.push(node);
|
|
649
|
+
}
|
|
650
|
+
ready.sort(compareNodeOrder);
|
|
651
|
+
return ready;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function getTaskExecutionKind(node = null) {
|
|
655
|
+
if (!node || node.type !== "task") return "";
|
|
656
|
+
const exec = node.execution;
|
|
657
|
+
if (exec && typeof exec === "object") {
|
|
658
|
+
return String(exec.kind || "").trim().toLowerCase() || "inline_llm";
|
|
659
|
+
}
|
|
660
|
+
const raw = String(exec || "llm").trim().toLowerCase();
|
|
661
|
+
if (raw === "aggregate") return "aggregate";
|
|
662
|
+
if (raw === "expand") return "expand";
|
|
663
|
+
if (raw === "task_loop") return "task_loop";
|
|
664
|
+
if (raw === "inline_llm") return "inline_llm";
|
|
665
|
+
return raw === "llm" || !raw ? "inline_llm" : raw;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function isAggregateTask(node = null) {
|
|
669
|
+
return Boolean(node && node.type === "task" && getTaskExecutionKind(node) === "aggregate");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function isLlmControlNode(node = null) {
|
|
673
|
+
if (!node) return false;
|
|
674
|
+
if (node.type === "checkpoint") return true;
|
|
675
|
+
if (node.type === "task" && !isAggregateTask(node)) {
|
|
676
|
+
// task_loop nodes stay ready for Agent start_task; they do not yield waiting_llm.
|
|
677
|
+
if (getTaskExecutionKind(node) === "task_loop") return false;
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function isTaskLoopNode(node = null) {
|
|
684
|
+
return Boolean(node && node.type === "task" && getTaskExecutionKind(node) === "task_loop");
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function blockDependents(nodeMap, failedId) {
|
|
688
|
+
let changed = true;
|
|
689
|
+
while (changed) {
|
|
690
|
+
changed = false;
|
|
691
|
+
for (const node of nodeMap.values()) {
|
|
692
|
+
if (node.status !== "pending") continue;
|
|
693
|
+
const blocked = normalizeDependsOn(node.dependsOn).some((depId) => {
|
|
694
|
+
const dep = nodeMap.get(depId);
|
|
695
|
+
return dep && (dep.status === "failed" || dep.status === "blocked" || dep.status === "skipped" || dep.status === "cancelled");
|
|
696
|
+
});
|
|
697
|
+
if (blocked) {
|
|
698
|
+
node.status = "blocked";
|
|
699
|
+
node.error = `blocked by ${failedId}`;
|
|
700
|
+
changed = true;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function isTerminal(nodeMap = new Map()) {
|
|
707
|
+
for (const node of nodeMap.values()) {
|
|
708
|
+
if (
|
|
709
|
+
node.status === "pending"
|
|
710
|
+
|| node.status === "ready"
|
|
711
|
+
|| node.status === "running"
|
|
712
|
+
|| node.status === "waiting_llm"
|
|
713
|
+
|| node.status === "waiting_approval"
|
|
714
|
+
) return false;
|
|
715
|
+
}
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function summarizeGraph(nodeMap = new Map(), meta = {}) {
|
|
720
|
+
const nodes = {};
|
|
721
|
+
const nodeSummaries = [];
|
|
722
|
+
let succeeded = 0;
|
|
723
|
+
let failed = 0;
|
|
724
|
+
for (const node of nodeMap.values()) {
|
|
725
|
+
nodes[node.id] = {
|
|
726
|
+
status: node.status,
|
|
727
|
+
type: node.type,
|
|
728
|
+
tool: node.tool || "",
|
|
729
|
+
error: node.error || "",
|
|
730
|
+
};
|
|
731
|
+
if (node.status === "succeeded") succeeded += 1;
|
|
732
|
+
if (node.status === "failed") failed += 1;
|
|
733
|
+
if (node.result) {
|
|
734
|
+
nodeSummaries.push({
|
|
735
|
+
id: node.id,
|
|
736
|
+
type: node.type,
|
|
737
|
+
status: node.status,
|
|
738
|
+
summary: node.result.summary || "",
|
|
739
|
+
artifact: node.result.artifactId ? `artifact://${node.result.artifactId}` : "",
|
|
740
|
+
});
|
|
741
|
+
} else if (node.status === "failed" || node.status === "blocked") {
|
|
742
|
+
nodeSummaries.push({
|
|
743
|
+
id: node.id,
|
|
744
|
+
type: node.type,
|
|
745
|
+
status: node.status,
|
|
746
|
+
summary: node.error || node.status,
|
|
747
|
+
artifact: "",
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
let status = "success";
|
|
752
|
+
if (failed > 0 && succeeded > 0) status = "partial_failure";
|
|
753
|
+
else if (failed > 0 && succeeded === 0) status = "failed";
|
|
754
|
+
else if (
|
|
755
|
+
meta.stoppedAt === "checkpoint"
|
|
756
|
+
|| meta.stoppedAt === "side_effect"
|
|
757
|
+
|| meta.stoppedAt === "waiting_llm"
|
|
758
|
+
) status = "checkpoint";
|
|
759
|
+
|
|
760
|
+
return {
|
|
761
|
+
planId: meta.planId || "",
|
|
762
|
+
segment_id: meta.planId || "",
|
|
763
|
+
status,
|
|
764
|
+
objective: meta.objective || "",
|
|
765
|
+
stoppedAt: meta.stoppedAt || "",
|
|
766
|
+
waitingFor: meta.waitingFor || null,
|
|
767
|
+
nodes,
|
|
768
|
+
node_summaries: nodeSummaries,
|
|
769
|
+
outputs: meta.outputs || {},
|
|
770
|
+
error: meta.error || "",
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function executePlanGraph(input = {}, options = {}) {
|
|
775
|
+
const compiled = options.compiled && options.compiled.ok
|
|
776
|
+
? options.compiled
|
|
777
|
+
: compilePlanGraph(input, options);
|
|
778
|
+
if (!compiled.ok) {
|
|
779
|
+
return {
|
|
780
|
+
ok: false,
|
|
781
|
+
error: compiled.errors.join("; ") || "plan compile failed",
|
|
782
|
+
compile: compiled,
|
|
783
|
+
summary: summarizeGraph(new Map(), {
|
|
784
|
+
planId: compiled.planId,
|
|
785
|
+
objective: compiled.objective,
|
|
786
|
+
error: compiled.errors.join("; "),
|
|
787
|
+
}),
|
|
788
|
+
segmentId: compiled.planId,
|
|
789
|
+
objective: compiled.objective,
|
|
790
|
+
results: [],
|
|
791
|
+
stoppedAt: "compile",
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const nodeMap = new Map();
|
|
796
|
+
const seedMap = options.seedNodeMap instanceof Map
|
|
797
|
+
? options.seedNodeMap
|
|
798
|
+
: (options.seedNodeMap && typeof options.seedNodeMap === "object"
|
|
799
|
+
? new Map(Object.entries(options.seedNodeMap))
|
|
800
|
+
: null);
|
|
801
|
+
|
|
802
|
+
for (const node of compiled.nodes) {
|
|
803
|
+
const seed = seedMap ? seedMap.get(node.id) : null;
|
|
804
|
+
let status = seed && seed.status ? String(seed.status) : "pending";
|
|
805
|
+
// Re-enter the scheduler after a prior LLM/approval yield.
|
|
806
|
+
// Keep running nodes so lease recovery can decide retry vs fail.
|
|
807
|
+
if (
|
|
808
|
+
status === "waiting_llm"
|
|
809
|
+
|| status === "waiting_approval"
|
|
810
|
+
|| status === "ready"
|
|
811
|
+
) {
|
|
812
|
+
status = "pending";
|
|
813
|
+
}
|
|
814
|
+
nodeMap.set(node.id, {
|
|
815
|
+
...node,
|
|
816
|
+
status,
|
|
817
|
+
result: seed && seed.result ? seed.result : null,
|
|
818
|
+
error: seed && seed.error ? seed.error : "",
|
|
819
|
+
lease: seed && seed.lease ? seed.lease : null,
|
|
820
|
+
attempt: seed && Number.isFinite(seed.attempt) ? seed.attempt : (node.attempt || 0),
|
|
821
|
+
executionId: seed && seed.executionId ? seed.executionId : "",
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const outputs = new Map();
|
|
826
|
+
if (options.seedOutputs instanceof Map) {
|
|
827
|
+
for (const [id, value] of options.seedOutputs.entries()) outputs.set(id, value);
|
|
828
|
+
} else if (options.seedOutputs && typeof options.seedOutputs === "object") {
|
|
829
|
+
for (const [id, value] of Object.entries(options.seedOutputs)) outputs.set(id, value);
|
|
830
|
+
}
|
|
831
|
+
for (const node of nodeMap.values()) {
|
|
832
|
+
if (node.status === "succeeded" && node.result && !outputs.has(node.id)) {
|
|
833
|
+
outputs.set(node.id, node.result);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const runTool = typeof options.runTool === "function"
|
|
838
|
+
? options.runTool
|
|
839
|
+
: (typeof options.runStep === "function"
|
|
840
|
+
? ({ node, args }) => options.runStep({ stepId: node.id, tool: node.tool, args })
|
|
841
|
+
: () => ({ ok: false, error: "no tool runner" }));
|
|
842
|
+
const parallel = options.parallel === true;
|
|
843
|
+
const failurePolicy = options.failurePolicy
|
|
844
|
+
|| compiled.failurePolicy
|
|
845
|
+
|| "continue_independent";
|
|
846
|
+
const maxNodeRuns = Number.isFinite(options.maxNodeRuns)
|
|
847
|
+
? Math.max(1, Math.floor(options.maxNodeRuns))
|
|
848
|
+
: 32;
|
|
849
|
+
const workerId = String(options.workerId || "local");
|
|
850
|
+
const leaseMs = Number.isFinite(options.leaseMs) ? options.leaseMs : undefined;
|
|
851
|
+
|
|
852
|
+
let runs = 0;
|
|
853
|
+
let stoppedAt = "";
|
|
854
|
+
let waitingFor = null;
|
|
855
|
+
|
|
856
|
+
while (!isTerminal(nodeMap) && runs < maxNodeRuns) {
|
|
857
|
+
recoverExpiredLeases(nodeMap);
|
|
858
|
+
|
|
859
|
+
const ready = getReadyNodes(nodeMap);
|
|
860
|
+
if (ready.length === 0) {
|
|
861
|
+
const running = Array.from(nodeMap.values()).filter((node) => node.status === "running");
|
|
862
|
+
if (running.length > 0) {
|
|
863
|
+
// Sync executor: running without in-loop wait means lease recovery needed next pass.
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const toolReady = ready.filter((node) => node.type === "tool");
|
|
870
|
+
const aggregateReady = ready.filter((node) => isAggregateTask(node));
|
|
871
|
+
const controlReady = ready.filter((node) => isLlmControlNode(node));
|
|
872
|
+
|
|
873
|
+
if (aggregateReady.length > 0) {
|
|
874
|
+
for (const node of aggregateReady) {
|
|
875
|
+
const childIds = normalizeDependsOn(node.dependsOn);
|
|
876
|
+
const childSummaries = childIds.map((id) => {
|
|
877
|
+
const child = nodeMap.get(id);
|
|
878
|
+
const summary = child && child.result && child.result.summary
|
|
879
|
+
? child.result.summary
|
|
880
|
+
: (child ? child.status : "missing");
|
|
881
|
+
return `${id}: ${summary}`;
|
|
882
|
+
});
|
|
883
|
+
const normalized = normalizeNodeResult({
|
|
884
|
+
ok: true,
|
|
885
|
+
output: {
|
|
886
|
+
children: childIds,
|
|
887
|
+
childOutputs: childIds.map((id) => {
|
|
888
|
+
const child = nodeMap.get(id);
|
|
889
|
+
return child && child.result ? child.result.output : null;
|
|
890
|
+
}),
|
|
891
|
+
},
|
|
892
|
+
summary: childSummaries.join("; ") || `aggregate ${node.id} complete`,
|
|
893
|
+
});
|
|
894
|
+
node.status = "succeeded";
|
|
895
|
+
node.result = normalized;
|
|
896
|
+
node.lease = null;
|
|
897
|
+
outputs.set(node.id, normalized);
|
|
898
|
+
runs += 1;
|
|
899
|
+
}
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (toolReady.length > 0) {
|
|
904
|
+
const batch = claimSafeReadyToolBatch(toolReady, {
|
|
905
|
+
parallel,
|
|
906
|
+
workerId,
|
|
907
|
+
leaseMs,
|
|
908
|
+
resolveArgs: (node) => resolveValue(node.args || {}, outputs),
|
|
909
|
+
});
|
|
910
|
+
if (batch.length === 0) {
|
|
911
|
+
// All ready tools conflicted; fall through to controls or deadlock.
|
|
912
|
+
} else {
|
|
913
|
+
let failFastTriggered = false;
|
|
914
|
+
for (const node of batch) {
|
|
915
|
+
const args = resolveValue(node.args || {}, outputs);
|
|
916
|
+
let raw;
|
|
917
|
+
try {
|
|
918
|
+
raw = runTool({
|
|
919
|
+
node,
|
|
920
|
+
args,
|
|
921
|
+
tool: node.tool,
|
|
922
|
+
stepId: node.id,
|
|
923
|
+
lease: node.lease,
|
|
924
|
+
executionId: node.executionId,
|
|
925
|
+
attempt: node.attempt,
|
|
926
|
+
});
|
|
927
|
+
if (raw && typeof raw.then === "function") {
|
|
928
|
+
throw new Error("async tool runners are not supported by executePlanGraph");
|
|
929
|
+
}
|
|
930
|
+
} catch (err) {
|
|
931
|
+
raw = { ok: false, error: err && err.message ? err.message : "tool failed" };
|
|
932
|
+
}
|
|
933
|
+
const normalized = normalizeNodeResult(raw);
|
|
934
|
+
if (!normalized.summary) {
|
|
935
|
+
normalized.summary = `${node.tool} ${normalized.status === "succeeded" ? "ok" : "failed"}`;
|
|
936
|
+
}
|
|
937
|
+
runs += 1;
|
|
938
|
+
node.lease = null;
|
|
939
|
+
if (normalized.status === "succeeded") {
|
|
940
|
+
node.status = "succeeded";
|
|
941
|
+
node.result = normalized;
|
|
942
|
+
outputs.set(node.id, normalized);
|
|
943
|
+
} else {
|
|
944
|
+
node.status = "failed";
|
|
945
|
+
node.error = normalized.error || "tool failed";
|
|
946
|
+
node.result = normalized;
|
|
947
|
+
stoppedAt = "error";
|
|
948
|
+
blockDependents(nodeMap, node.id);
|
|
949
|
+
if (failurePolicy === "fail_fast") {
|
|
950
|
+
for (const other of nodeMap.values()) {
|
|
951
|
+
if (other.status === "pending" || other.status === "ready") {
|
|
952
|
+
other.status = "cancelled";
|
|
953
|
+
other.error = `fail_fast after ${node.id}`;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
failFastTriggered = true;
|
|
957
|
+
waitingFor = {
|
|
958
|
+
type: "failures",
|
|
959
|
+
nodes: [{ id: node.id, error: node.error || "failed" }],
|
|
960
|
+
};
|
|
961
|
+
stoppedAt = "tool_failure_requires_decision";
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (failFastTriggered) break;
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const control = controlReady[0];
|
|
972
|
+
if (!control) {
|
|
973
|
+
const failed = Array.from(nodeMap.values()).filter((node) => node.status === "failed");
|
|
974
|
+
if (failed.length > 0) {
|
|
975
|
+
stoppedAt = "tool_failure_requires_decision";
|
|
976
|
+
waitingFor = {
|
|
977
|
+
type: "failures",
|
|
978
|
+
nodes: failed.map((node) => ({ id: node.id, error: node.error || "failed" })),
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
if (control.type === "task") {
|
|
984
|
+
control.status = "waiting_llm";
|
|
985
|
+
stoppedAt = "waiting_llm";
|
|
986
|
+
waitingFor = {
|
|
987
|
+
id: control.id,
|
|
988
|
+
type: "task",
|
|
989
|
+
objective: control.objective || control.title || "",
|
|
990
|
+
title: control.title || control.objective || "",
|
|
991
|
+
inputs: resolveValue(control.inputs || {}, outputs),
|
|
992
|
+
};
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
if (control.type === "checkpoint") {
|
|
996
|
+
control.status = control.mode === "approval" ? "waiting_approval" : "waiting_llm";
|
|
997
|
+
stoppedAt = control.mode === "approval"
|
|
998
|
+
? "approval_required"
|
|
999
|
+
: (control.stopKind === "side_effect" ? "side_effect" : "checkpoint");
|
|
1000
|
+
waitingFor = {
|
|
1001
|
+
id: control.id,
|
|
1002
|
+
type: "checkpoint",
|
|
1003
|
+
mode: control.mode,
|
|
1004
|
+
reason: control.reason || "",
|
|
1005
|
+
stopKind: control.stopKind || "checkpoint",
|
|
1006
|
+
};
|
|
1007
|
+
break;
|
|
1008
|
+
}
|
|
1009
|
+
control.status = "failed";
|
|
1010
|
+
control.error = `unsupported node type ${control.type}`;
|
|
1011
|
+
blockDependents(nodeMap, control.id);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const outputsObj = {};
|
|
1015
|
+
for (const [id, value] of outputs.entries()) outputsObj[id] = value.output;
|
|
1016
|
+
const summary = summarizeGraph(nodeMap, {
|
|
1017
|
+
planId: compiled.planId,
|
|
1018
|
+
objective: compiled.objective,
|
|
1019
|
+
stoppedAt,
|
|
1020
|
+
waitingFor,
|
|
1021
|
+
outputs: outputsObj,
|
|
1022
|
+
});
|
|
1023
|
+
let yieldReason = "";
|
|
1024
|
+
if (stoppedAt === "waiting_llm") yieldReason = "task_ready";
|
|
1025
|
+
else if (stoppedAt === "checkpoint") yieldReason = "llm_checkpoint_ready";
|
|
1026
|
+
else if (stoppedAt === "approval_required") yieldReason = "approval_required";
|
|
1027
|
+
else if (stoppedAt === "side_effect") yieldReason = "llm_checkpoint_ready";
|
|
1028
|
+
else if (stoppedAt === "tool_failure_requires_decision") yieldReason = "tool_failure_requires_decision";
|
|
1029
|
+
else if (stoppedAt === "error") yieldReason = "tool_failure_requires_decision";
|
|
1030
|
+
else if (isTerminal(nodeMap)) yieldReason = "graph_terminal";
|
|
1031
|
+
else yieldReason = "scheduler_deadlock";
|
|
1032
|
+
|
|
1033
|
+
const ok = summary.status === "success"
|
|
1034
|
+
|| summary.status === "checkpoint"
|
|
1035
|
+
|| stoppedAt === "side_effect"
|
|
1036
|
+
|| stoppedAt === "waiting_llm"
|
|
1037
|
+
|| stoppedAt === "checkpoint"
|
|
1038
|
+
|| stoppedAt === "approval_required";
|
|
1039
|
+
return {
|
|
1040
|
+
ok,
|
|
1041
|
+
yieldReason,
|
|
1042
|
+
error: ok ? "" : (stoppedAt === "error"
|
|
1043
|
+
? (Array.from(nodeMap.values()).find((n) => n.status === "failed") || {}).error || "tool failed"
|
|
1044
|
+
: summary.status),
|
|
1045
|
+
compile: compiled,
|
|
1046
|
+
nodes: Array.from(nodeMap.values()),
|
|
1047
|
+
nodeMap,
|
|
1048
|
+
outputs,
|
|
1049
|
+
waitingFor,
|
|
1050
|
+
stoppedAt,
|
|
1051
|
+
summary,
|
|
1052
|
+
segmentId: compiled.planId,
|
|
1053
|
+
objective: compiled.objective,
|
|
1054
|
+
results: Array.from(nodeMap.values())
|
|
1055
|
+
.filter((node) => node.type === "tool" && node.result)
|
|
1056
|
+
.map((node) => ({
|
|
1057
|
+
stepId: node.id,
|
|
1058
|
+
tool: node.tool,
|
|
1059
|
+
ok: node.status === "succeeded",
|
|
1060
|
+
artifactId: node.result.artifactId || "",
|
|
1061
|
+
error: node.error || "",
|
|
1062
|
+
})),
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function applyPlanOperations(planInput = {}, operations = []) {
|
|
1067
|
+
const plan = normalizePlanGraph(planInput);
|
|
1068
|
+
const nodes = plan.nodes.slice();
|
|
1069
|
+
const ops = Array.isArray(operations) ? operations : [];
|
|
1070
|
+
const errors = [];
|
|
1071
|
+
let nextSeq = nodes.reduce((max, node) => Math.max(max, Number(node.createdSeq) || 0), 0) + 1;
|
|
1072
|
+
|
|
1073
|
+
function findIndex(id) {
|
|
1074
|
+
return nodes.findIndex((node) => node.id === id);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function expandTask(op = {}) {
|
|
1078
|
+
const nodeId = String(op.nodeId || op.replaceNode || "").trim();
|
|
1079
|
+
const idx = findIndex(nodeId);
|
|
1080
|
+
if (idx < 0) {
|
|
1081
|
+
errors.push(`expand_node target missing: ${nodeId}`);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const target = nodes[idx];
|
|
1085
|
+
if (target.type !== "task") {
|
|
1086
|
+
errors.push(`expand_node requires task node: ${nodeId}`);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (getTaskExecutionKind(target) === "aggregate") {
|
|
1090
|
+
errors.push(`task already expanded: ${nodeId}`);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (target.status && !["pending", "waiting_llm", "ready"].includes(String(target.status))) {
|
|
1094
|
+
errors.push(`task not expandable in status ${target.status}: ${nodeId}`);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
let children = [];
|
|
1099
|
+
let strategy = "parallel";
|
|
1100
|
+
if (Array.isArray(op.children)) {
|
|
1101
|
+
children = op.children;
|
|
1102
|
+
strategy = String(op.strategy || "parallel").trim().toLowerCase() === "sequence"
|
|
1103
|
+
? "sequence"
|
|
1104
|
+
: "parallel";
|
|
1105
|
+
} else {
|
|
1106
|
+
const replacement = op.with || op.subgraph || op.node;
|
|
1107
|
+
if (replacement && replacement.type === "group") {
|
|
1108
|
+
children = Array.isArray(replacement.children) ? replacement.children : [];
|
|
1109
|
+
strategy = String(replacement.strategy || "parallel").trim().toLowerCase() === "sequence"
|
|
1110
|
+
? "sequence"
|
|
1111
|
+
: "parallel";
|
|
1112
|
+
} else if (replacement) {
|
|
1113
|
+
children = [replacement];
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (children.length === 0) {
|
|
1117
|
+
errors.push(`expand_node requires children: ${nodeId}`);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const parentDeps = normalizeDependsOn(target.dependsOn);
|
|
1122
|
+
const childIds = [];
|
|
1123
|
+
let seqPrev = null;
|
|
1124
|
+
children.forEach((child, index) => {
|
|
1125
|
+
const deps = normalizeDependsOn(child && child.dependsOn).concat(parentDeps);
|
|
1126
|
+
if (strategy === "sequence" && seqPrev) deps.push(seqPrev);
|
|
1127
|
+
const normalized = normalizePlanNode({
|
|
1128
|
+
...child,
|
|
1129
|
+
dependsOn: Array.from(new Set(deps)),
|
|
1130
|
+
parentTaskId: nodeId,
|
|
1131
|
+
generated: true,
|
|
1132
|
+
displayOrder: index,
|
|
1133
|
+
createdSeq: nextSeq,
|
|
1134
|
+
status: "pending",
|
|
1135
|
+
attempt: 0,
|
|
1136
|
+
}, `${nodeId}_c${index + 1}`);
|
|
1137
|
+
nextSeq += 1;
|
|
1138
|
+
if (findIndex(normalized.id) >= 0) {
|
|
1139
|
+
errors.push(`duplicate node id while expanding: ${normalized.id}`);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
nodes.push(normalized);
|
|
1143
|
+
childIds.push(normalized.id);
|
|
1144
|
+
seqPrev = normalized.id;
|
|
1145
|
+
});
|
|
1146
|
+
if (errors.length > 0) return;
|
|
1147
|
+
|
|
1148
|
+
const sinks = strategy === "sequence"
|
|
1149
|
+
? (childIds.length ? [childIds[childIds.length - 1]] : [])
|
|
1150
|
+
: childIds.slice();
|
|
1151
|
+
|
|
1152
|
+
nodes[idx] = {
|
|
1153
|
+
...target,
|
|
1154
|
+
type: "task",
|
|
1155
|
+
execution: { kind: "aggregate" },
|
|
1156
|
+
dependsOn: sinks,
|
|
1157
|
+
status: "pending",
|
|
1158
|
+
result: null,
|
|
1159
|
+
error: "",
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
for (const op of ops) {
|
|
1164
|
+
if (!op || typeof op !== "object") continue;
|
|
1165
|
+
const type = String(op.op || op.type || "").trim().toLowerCase();
|
|
1166
|
+
if (type === "add_node" && op.node) {
|
|
1167
|
+
const node = normalizePlanNode({
|
|
1168
|
+
...op.node,
|
|
1169
|
+
status: "pending",
|
|
1170
|
+
createdSeq: nextSeq,
|
|
1171
|
+
attempt: 0,
|
|
1172
|
+
}, `n${nodes.length + 1}`);
|
|
1173
|
+
nextSeq += 1;
|
|
1174
|
+
if (findIndex(node.id) >= 0) {
|
|
1175
|
+
errors.push(`duplicate node id: ${node.id}`);
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
// Models cannot create aggregate tasks directly.
|
|
1179
|
+
if (node.type === "task" && getTaskExecutionKind(node) === "aggregate") {
|
|
1180
|
+
node.execution = { kind: "inline_llm" };
|
|
1181
|
+
}
|
|
1182
|
+
nodes.push(node);
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
if (type === "expand" || type === "expand_node") {
|
|
1186
|
+
expandTask(op);
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
if (type === "replace_node") {
|
|
1190
|
+
errors.push("replace_node is disabled in v1; use expand_node or add_node");
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
if (type === "add_dependency") {
|
|
1194
|
+
const nodeId = String(op.nodeId || "").trim();
|
|
1195
|
+
const dep = String(op.dependsOn || op.dep || "").trim();
|
|
1196
|
+
const idx = findIndex(nodeId);
|
|
1197
|
+
if (idx < 0 || !dep) continue;
|
|
1198
|
+
const node = nodes[idx];
|
|
1199
|
+
if (node.status && !["pending", "waiting_llm", "ready"].includes(String(node.status))) {
|
|
1200
|
+
errors.push(`cannot modify dependencies of ${nodeId} in status ${node.status}`);
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
nodes[idx] = {
|
|
1204
|
+
...node,
|
|
1205
|
+
dependsOn: Array.from(new Set(normalizeDependsOn(node.dependsOn).concat([dep]))),
|
|
1206
|
+
};
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
if (type === "remove_dependency") {
|
|
1210
|
+
const nodeId = String(op.nodeId || "").trim();
|
|
1211
|
+
const dep = String(op.dependsOn || op.dep || "").trim();
|
|
1212
|
+
const idx = findIndex(nodeId);
|
|
1213
|
+
if (idx < 0 || !dep) continue;
|
|
1214
|
+
const node = nodes[idx];
|
|
1215
|
+
if (node.status && !["pending", "waiting_llm", "ready"].includes(String(node.status))) {
|
|
1216
|
+
errors.push(`cannot modify dependencies of ${nodeId} in status ${node.status}`);
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
nodes[idx] = {
|
|
1220
|
+
...node,
|
|
1221
|
+
dependsOn: normalizeDependsOn(node.dependsOn).filter((item) => item !== dep),
|
|
1222
|
+
};
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
if (type === "complete_task" || type === "skip_node" || type === "cancel_subtree") {
|
|
1226
|
+
errors.push(
|
|
1227
|
+
`${type} is a control action; use plan_graph operation=control (not patch)`,
|
|
1228
|
+
);
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
if (type === "set_status") {
|
|
1232
|
+
errors.push("set_status is removed; use control complete_task / skip_node / cancel_subtree");
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
return {
|
|
1238
|
+
id: plan.id,
|
|
1239
|
+
objective: plan.objective,
|
|
1240
|
+
nodes,
|
|
1241
|
+
errors,
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Runtime status mutations for control-plane actions (not patch/spec edits).
|
|
1247
|
+
* Mutates planGraph.nodes in place. Used by plan_graph operation=control.
|
|
1248
|
+
*/
|
|
1249
|
+
function applyControlNodeAction(planGraph = null, action = {}) {
|
|
1250
|
+
if (!planGraph || !Array.isArray(planGraph.nodes)) {
|
|
1251
|
+
return {
|
|
1252
|
+
ok: false,
|
|
1253
|
+
errors: [{ code: "GRAPH_MISSING", message: "plan graph missing" }],
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
const op = String(action.op || action.type || "").trim().toLowerCase();
|
|
1257
|
+
const nodes = planGraph.nodes;
|
|
1258
|
+
const findIndex = (id) => nodes.findIndex((node) => node && node.id === id);
|
|
1259
|
+
const errors = [];
|
|
1260
|
+
|
|
1261
|
+
if (op === "complete_task") {
|
|
1262
|
+
const nodeId = String(action.nodeId || "").trim();
|
|
1263
|
+
const idx = findIndex(nodeId);
|
|
1264
|
+
if (idx < 0) {
|
|
1265
|
+
return {
|
|
1266
|
+
ok: false,
|
|
1267
|
+
errors: [{ code: "NODE_NOT_FOUND", message: `complete_task target missing: ${nodeId}` }],
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
const node = nodes[idx];
|
|
1271
|
+
if (node.type !== "task" || isAggregateTask(node)) {
|
|
1272
|
+
return {
|
|
1273
|
+
ok: false,
|
|
1274
|
+
errors: [{ code: "NOT_INLINE_TASK", message: `complete_task requires llm task: ${nodeId}` }],
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
if (getTaskExecutionKind(node) === "task_loop") {
|
|
1278
|
+
return {
|
|
1279
|
+
ok: false,
|
|
1280
|
+
errors: [{
|
|
1281
|
+
code: "USE_TASK_RUN_ID",
|
|
1282
|
+
message: `task_loop ${nodeId} must complete via control.complete_task with taskRunId`,
|
|
1283
|
+
}],
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
if (node.status !== "waiting_llm") {
|
|
1287
|
+
return {
|
|
1288
|
+
ok: false,
|
|
1289
|
+
errors: [{
|
|
1290
|
+
code: "BAD_STATUS",
|
|
1291
|
+
message: `complete_task requires waiting_llm: ${nodeId} (got ${node.status})`,
|
|
1292
|
+
}],
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
const result = normalizeNodeResult(action.result || {
|
|
1296
|
+
ok: true,
|
|
1297
|
+
output: action.output || {},
|
|
1298
|
+
summary: action.summary || `completed ${nodeId}`,
|
|
1299
|
+
});
|
|
1300
|
+
nodes[idx] = {
|
|
1301
|
+
...node,
|
|
1302
|
+
status: "succeeded",
|
|
1303
|
+
result,
|
|
1304
|
+
error: "",
|
|
1305
|
+
};
|
|
1306
|
+
} else if (op === "skip_node") {
|
|
1307
|
+
const nodeId = String(action.nodeId || "").trim();
|
|
1308
|
+
const idx = findIndex(nodeId);
|
|
1309
|
+
if (idx < 0) {
|
|
1310
|
+
return {
|
|
1311
|
+
ok: false,
|
|
1312
|
+
errors: [{ code: "NODE_NOT_FOUND", message: `skip_node target missing: ${nodeId}` }],
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
const node = nodes[idx];
|
|
1316
|
+
if (node.status && !["pending", "waiting_llm", "ready"].includes(String(node.status))) {
|
|
1317
|
+
return {
|
|
1318
|
+
ok: false,
|
|
1319
|
+
errors: [{
|
|
1320
|
+
code: "BAD_STATUS",
|
|
1321
|
+
message: `cannot skip ${nodeId} in status ${node.status}`,
|
|
1322
|
+
}],
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
nodes[idx] = { ...node, status: "skipped", error: String(action.reason || "skipped") };
|
|
1326
|
+
} else if (op === "cancel_subtree") {
|
|
1327
|
+
const nodeId = String(action.nodeId || "").trim();
|
|
1328
|
+
const idx = findIndex(nodeId);
|
|
1329
|
+
if (idx < 0) {
|
|
1330
|
+
return {
|
|
1331
|
+
ok: false,
|
|
1332
|
+
errors: [{ code: "NODE_NOT_FOUND", message: `cancel_subtree target missing: ${nodeId}` }],
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
const cancelled = new Set([nodeId]);
|
|
1336
|
+
let changed = true;
|
|
1337
|
+
while (changed) {
|
|
1338
|
+
changed = false;
|
|
1339
|
+
for (const node of nodes) {
|
|
1340
|
+
if (!node || cancelled.has(node.id)) continue;
|
|
1341
|
+
const deps = normalizeDependsOn(node.dependsOn);
|
|
1342
|
+
if (deps.some((dep) => cancelled.has(dep)) || node.parentTaskId === nodeId) {
|
|
1343
|
+
cancelled.add(node.id);
|
|
1344
|
+
changed = true;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
1349
|
+
if (!cancelled.has(nodes[i].id)) continue;
|
|
1350
|
+
if (["succeeded", "failed"].includes(String(nodes[i].status || ""))) continue;
|
|
1351
|
+
nodes[i] = {
|
|
1352
|
+
...nodes[i],
|
|
1353
|
+
status: "cancelled",
|
|
1354
|
+
error: String(action.reason || `cancelled via ${nodeId}`),
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
} else {
|
|
1358
|
+
return {
|
|
1359
|
+
ok: false,
|
|
1360
|
+
errors: [{ code: "UNKNOWN_CONTROL_OP", message: `unknown control op: ${op}` }],
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
planGraph.stateRevision = (Number(planGraph.stateRevision) || 0) + 1;
|
|
1365
|
+
const waiting = planGraph.waitingFor;
|
|
1366
|
+
const touchId = String(action.nodeId || "").trim();
|
|
1367
|
+
if (waiting && touchId && String(waiting.id || "") === touchId) {
|
|
1368
|
+
planGraph.waitingFor = null;
|
|
1369
|
+
planGraph.lastStoppedAt = "";
|
|
1370
|
+
planGraph.lastYieldReason = "";
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
return {
|
|
1374
|
+
ok: errors.length === 0,
|
|
1375
|
+
errors: errors.length ? errors : undefined,
|
|
1376
|
+
nodeId: touchId,
|
|
1377
|
+
op,
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
module.exports = {
|
|
1382
|
+
NODE_TYPES,
|
|
1383
|
+
NODE_STATUSES,
|
|
1384
|
+
TOOL_ALIASES,
|
|
1385
|
+
SIDE_EFFECT_TOOLS,
|
|
1386
|
+
createPlanId,
|
|
1387
|
+
normalizePlanNode,
|
|
1388
|
+
normalizePlanGraph,
|
|
1389
|
+
planGraphFromExecutionSegment,
|
|
1390
|
+
compilePlanGraph,
|
|
1391
|
+
resolveValue,
|
|
1392
|
+
resolveRefToken,
|
|
1393
|
+
resolveStructuredRef,
|
|
1394
|
+
executePlanGraph,
|
|
1395
|
+
applyPlanOperations,
|
|
1396
|
+
applyControlNodeAction,
|
|
1397
|
+
flattenPlanNodes,
|
|
1398
|
+
rewriteGroupDependencies,
|
|
1399
|
+
detectCycles,
|
|
1400
|
+
getReadyNodes,
|
|
1401
|
+
compareNodeOrder,
|
|
1402
|
+
isAggregateTask,
|
|
1403
|
+
isLlmControlNode,
|
|
1404
|
+
isTaskLoopNode,
|
|
1405
|
+
getTaskExecutionKind,
|
|
1406
|
+
isTerminal,
|
|
1407
|
+
summarizeGraph,
|
|
1408
|
+
resolveToolName,
|
|
1409
|
+
collectRefsFromValue,
|
|
1410
|
+
};
|