u-foo 2.5.15 → 3.0.0

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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/code/agent.js +333 -243
  3. package/src/code/commands.js +16 -0
  4. package/src/code/context/assembler.js +18 -13
  5. package/src/code/context/executionSegment.js +97 -119
  6. package/src/code/context/index.js +11 -1
  7. package/src/code/context/planGraph.js +1410 -0
  8. package/src/code/context/planGraphService.js +857 -0
  9. package/src/code/context/planMode.js +398 -0
  10. package/src/code/context/planProjection.js +432 -0
  11. package/src/code/context/promptLayers.js +21 -5
  12. package/src/code/context/stateCommit.js +2 -0
  13. package/src/code/context/toolRuntime.js +172 -0
  14. package/src/code/context/userInteraction.js +457 -0
  15. package/src/code/context/userNudge.js +116 -0
  16. package/src/code/dispatch.js +17 -1
  17. package/src/code/index.js +2 -0
  18. package/src/code/nativeRunner.js +518 -37
  19. package/src/code/repl.js +160 -18
  20. package/src/code/runtime/agentWakeup.js +58 -0
  21. package/src/code/runtime/graphOwner.js +41 -0
  22. package/src/code/runtime/graphYieldRouter.js +42 -0
  23. package/src/code/runtime/index.js +15 -0
  24. package/src/code/runtime/loopMailbox.js +124 -0
  25. package/src/code/runtime/runtimeEvents.js +39 -0
  26. package/src/code/runtime/taskControl.js +565 -0
  27. package/src/code/runtime/taskFocus.js +165 -0
  28. package/src/code/runtime/taskLoop.js +383 -0
  29. package/src/code/runtime/taskRun.js +187 -0
  30. package/src/code/runtime/toolProvenance.js +70 -0
  31. package/src/code/runtime/workspaceLease.js +208 -0
  32. package/src/code/sessionStore.js +0 -10
  33. package/src/code/skills/injection.js +1 -0
  34. package/src/code/taskDecomposer.js +32 -8
  35. package/src/code/tools/askUser.js +11 -0
  36. package/src/code/tools/planGraph.js +29 -0
  37. package/src/ui/format/index.js +25 -1
  38. package/src/ui/format/markdownRenderer.js +224 -2
  39. package/src/ui/ink/UcodeApp.js +285 -22
  40. package/src/code/context/featureFlag.js +0 -13
@@ -0,0 +1,857 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Control-plane Graph Service for plan_graph tool + legacy side-effect bridge.
5
+ *
6
+ * Ownership:
7
+ * - Models mutate graph *spec* via create/patch (no arbitrary status writes).
8
+ * - Scheduler/Executor owns ready/running/succeeded/failed/blocked for tools.
9
+ * - control.complete_task is the model path to finish waiting_llm / TaskRuns.
10
+ * - control.skip_node / cancel_subtree are runtime status actions (not patch).
11
+ */
12
+
13
+ const {
14
+ createPlanId,
15
+ normalizePlanGraph,
16
+ normalizePlanNode,
17
+ compilePlanGraph,
18
+ applyPlanOperations,
19
+ executePlanGraph,
20
+ planGraphFromExecutionSegment,
21
+ getReadyNodes,
22
+ isAggregateTask,
23
+ } = require("./planGraph");
24
+
25
+ function emptyPlanGraphState() {
26
+ return {
27
+ graphId: "",
28
+ specRevision: 0,
29
+ stateRevision: 0,
30
+ // Compatibility alias used by older callers/tests.
31
+ revision: 0,
32
+ objective: "",
33
+ failurePolicy: "continue_independent",
34
+ nodes: [],
35
+ outputs: {},
36
+ waitingFor: null,
37
+ lastStoppedAt: "",
38
+ lastYieldReason: "",
39
+ commandLog: {},
40
+ };
41
+ }
42
+
43
+ function cloneJson(value) {
44
+ try {
45
+ return JSON.parse(JSON.stringify(value));
46
+ } catch {
47
+ return value;
48
+ }
49
+ }
50
+
51
+ function ensurePlanGraphState(executionState = null) {
52
+ const state = executionState && typeof executionState === "object"
53
+ ? executionState
54
+ : {};
55
+ if (!state.planGraph || typeof state.planGraph !== "object") {
56
+ state.planGraph = emptyPlanGraphState();
57
+ } else {
58
+ const pg = state.planGraph;
59
+ if (!Number.isFinite(pg.specRevision)) pg.specRevision = Number(pg.revision) || 0;
60
+ if (!Number.isFinite(pg.stateRevision)) pg.stateRevision = Number(pg.revision) || 0;
61
+ if (!Number.isFinite(pg.revision)) pg.revision = pg.specRevision;
62
+ if (!pg.commandLog || typeof pg.commandLog !== "object") pg.commandLog = {};
63
+ }
64
+ return state;
65
+ }
66
+
67
+ function listNodeIds(nodes = []) {
68
+ return (Array.isArray(nodes) ? nodes : []).map((node) => node.id).filter(Boolean);
69
+ }
70
+
71
+ function summarizeReadyWaiting(nodes = []) {
72
+ const byId = new Map((Array.isArray(nodes) ? nodes : []).map((node) => [node.id, node]));
73
+ const ready = getReadyNodes(byId).map((node) => node.id);
74
+ const waiting = [];
75
+ for (const node of byId.values()) {
76
+ if (node.status === "pending" && !ready.includes(node.id)) waiting.push(node.id);
77
+ if (node.status === "waiting_llm" || node.status === "waiting_approval") waiting.push(node.id);
78
+ }
79
+ return { readyNodes: ready, waitingNodes: Array.from(new Set(waiting)) };
80
+ }
81
+
82
+ function validationErrorsFromCompile(compiled = {}) {
83
+ return (Array.isArray(compiled.errors) ? compiled.errors : []).map((message) => ({
84
+ code: /cycle/i.test(message)
85
+ ? "CYCLE_DETECTED"
86
+ : (/unknown node|references unknown/i.test(message)
87
+ ? "UNKNOWN_OUTPUT_REFERENCE"
88
+ : (/duplicate/i.test(message) ? "DUPLICATE_NODE_ID" : "VALIDATION_ERROR")),
89
+ message: String(message),
90
+ }));
91
+ }
92
+
93
+ function rejected(errors = [], extras = {}) {
94
+ return {
95
+ ok: false,
96
+ status: "rejected",
97
+ errors: Array.isArray(errors) ? errors : [{ code: "VALIDATION_ERROR", message: String(errors) }],
98
+ validationWarnings: [],
99
+ ...extras,
100
+ };
101
+ }
102
+
103
+ function accepted(payload = {}) {
104
+ return {
105
+ ok: true,
106
+ status: "accepted",
107
+ ...payload,
108
+ };
109
+ }
110
+
111
+ function stripModelStatuses(nodes = []) {
112
+ return (Array.isArray(nodes) ? nodes : []).map((node, index) => normalizePlanNode({
113
+ ...node,
114
+ status: "pending",
115
+ attempt: 0,
116
+ result: null,
117
+ error: "",
118
+ createdSeq: Number.isFinite(node.createdSeq) ? node.createdSeq : index + 1,
119
+ // Models cannot author aggregate nodes.
120
+ execution: node.type === "task" && node.execution === "aggregate" ? "llm" : node.execution,
121
+ }, `n${index + 1}`));
122
+ }
123
+
124
+ function normalizeExpandOp(op = {}) {
125
+ const next = { ...op };
126
+ const type = String(op.op || op.type || "").trim().toLowerCase();
127
+ if ((type === "expand" || type === "expand_node") && Array.isArray(op.children) && !op.with && !op.subgraph && !op.node) {
128
+ // Keep children form; applyPlanOperations understands it as aggregate expand.
129
+ next.op = "expand_node";
130
+ }
131
+ return next;
132
+ }
133
+
134
+ function executionSegmentToCreateGraph(segment = {}) {
135
+ return planGraphFromExecutionSegment(segment);
136
+ }
137
+
138
+ /**
139
+ * Normalize tool args OR legacy structured side effects into a plan_graph command.
140
+ * Legacy body bridge is intentionally narrow: only explicit execution_segment.
141
+ */
142
+ function normalizePlanGraphCommand(input = {}) {
143
+ const source = input && typeof input === "object" ? input : null;
144
+ if (!source) return null;
145
+
146
+ const operation = String(source.operation || "").trim().toLowerCase();
147
+ if (
148
+ operation === "create"
149
+ || operation === "patch"
150
+ || operation === "inspect"
151
+ || operation === "clear"
152
+ || operation === "cancel_graph"
153
+ || operation === "archive_graph"
154
+ || operation === "control"
155
+ ) {
156
+ return {
157
+ operation: operation === "clear" || operation === "archive_graph" ? "cancel_graph" : operation,
158
+ graph: source.graph && typeof source.graph === "object" ? source.graph : null,
159
+ operations: Array.isArray(source.operations) ? source.operations.map(normalizeExpandOp) : [],
160
+ actions: Array.isArray(source.actions) ? source.actions : [],
161
+ commandId: String(source.commandId || "").trim(),
162
+ expectedSpecRevision: Number.isFinite(source.expectedSpecRevision)
163
+ ? Math.floor(source.expectedSpecRevision)
164
+ : null,
165
+ graphId: String(source.graphId || "").trim(),
166
+ reason: String(source.reason || "").trim(),
167
+ source: "tool",
168
+ };
169
+ }
170
+
171
+ // Strict legacy: only top-level execution_segment / nextSegment.
172
+ if (source.nextSegment && typeof source.nextSegment === "object") {
173
+ return {
174
+ operation: "create",
175
+ graph: executionSegmentToCreateGraph(source.nextSegment),
176
+ operations: [],
177
+ source: "legacy_segment",
178
+ };
179
+ }
180
+ if (source.type === "execution_segment") {
181
+ return {
182
+ operation: "create",
183
+ graph: executionSegmentToCreateGraph(source),
184
+ operations: [],
185
+ source: "legacy_segment",
186
+ };
187
+ }
188
+
189
+ return null;
190
+ }
191
+
192
+ function snapshotNodes(planGraph = {}) {
193
+ return Array.isArray(planGraph.nodes) ? cloneJson(planGraph.nodes) : [];
194
+ }
195
+
196
+ function applyStatusesFromStore(compiledNodes = [], storeNodes = [], options = {}) {
197
+ const prior = new Map((Array.isArray(storeNodes) ? storeNodes : []).map((node) => [node.id, node]));
198
+ const preferSourceStatus = options.preferSourceStatus === true;
199
+ return compiledNodes.map((node) => {
200
+ const old = prior.get(node.id);
201
+ const sourceStatus = String(node.status || "").trim();
202
+ let status = "pending";
203
+ if (preferSourceStatus && sourceStatus && sourceStatus !== "pending") {
204
+ status = sourceStatus;
205
+ } else if (old && old.status) {
206
+ status = old.status;
207
+ } else if (sourceStatus) {
208
+ status = sourceStatus;
209
+ }
210
+ if (!old) {
211
+ return {
212
+ ...node,
213
+ status,
214
+ result: node.result || null,
215
+ error: node.error || "",
216
+ attempt: Number.isFinite(node.attempt) ? node.attempt : 0,
217
+ };
218
+ }
219
+ return {
220
+ ...node,
221
+ status,
222
+ result: (preferSourceStatus && node.result) ? node.result : (old.result || node.result || null),
223
+ error: (preferSourceStatus && node.error) ? node.error : (old.error || node.error || ""),
224
+ stopKind: node.stopKind || old.stopKind || "",
225
+ attempt: Number.isFinite(node.attempt) ? node.attempt : (Number.isFinite(old.attempt) ? old.attempt : 0),
226
+ parentTaskId: node.parentTaskId || old.parentTaskId || "",
227
+ execution: node.execution || old.execution,
228
+ createdSeq: Number.isFinite(node.createdSeq) && node.createdSeq > 0
229
+ ? node.createdSeq
230
+ : (old.createdSeq || 0),
231
+ generated: Boolean(node.generated || old.generated),
232
+ displayOrder: Number.isFinite(node.displayOrder) ? node.displayOrder : (old.displayOrder || 0),
233
+ };
234
+ });
235
+ }
236
+
237
+ function restoreOutputsMap(planGraph = {}) {
238
+ const outputs = new Map();
239
+ const source = planGraph.outputs && typeof planGraph.outputs === "object" ? planGraph.outputs : {};
240
+ for (const [id, value] of Object.entries(source)) {
241
+ outputs.set(id, value);
242
+ }
243
+ for (const node of Array.isArray(planGraph.nodes) ? planGraph.nodes : []) {
244
+ if (node && node.result && !outputs.has(node.id)) {
245
+ outputs.set(node.id, node.result);
246
+ }
247
+ }
248
+ return outputs;
249
+ }
250
+
251
+ function persistOutputs(outputs = new Map()) {
252
+ const out = {};
253
+ for (const [id, value] of outputs.entries()) out[id] = value;
254
+ return out;
255
+ }
256
+
257
+ function compileStoredGraph(planGraph = {}, options = {}) {
258
+ const compiled = compilePlanGraph({
259
+ id: planGraph.graphId || createPlanId("plan"),
260
+ objective: planGraph.objective || "",
261
+ nodes: Array.isArray(planGraph.nodes) ? planGraph.nodes : [],
262
+ }, options);
263
+ if (!compiled.ok) return compiled;
264
+ const withStatus = applyStatusesFromStore(compiled.nodes, planGraph.nodes);
265
+ return {
266
+ ...compiled,
267
+ nodes: withStatus,
268
+ nodeMap: new Map(withStatus.map((node) => [node.id, node])),
269
+ };
270
+ }
271
+
272
+ function buildAdvanceSummary(result = {}, beforeStatuses = new Map()) {
273
+ const nodes = Array.isArray(result.nodes) ? result.nodes : [];
274
+ const executedNodes = [];
275
+ const failedNodes = [];
276
+ for (const node of nodes) {
277
+ const before = beforeStatuses.get(node.id);
278
+ const becameTerminal = node.status === "succeeded" || node.status === "failed";
279
+ const wasPending = !before || before === "pending" || before === "waiting_llm" || before === "ready" || before === "running";
280
+ if (becameTerminal && wasPending && before !== node.status) {
281
+ const entry = {
282
+ id: node.id,
283
+ type: node.type,
284
+ status: node.status,
285
+ summary: (node.result && node.result.summary) || node.error || node.status,
286
+ };
287
+ if (node.status === "succeeded") executedNodes.push(entry);
288
+ if (node.status === "failed") failedNodes.push(entry);
289
+ }
290
+ }
291
+ const yieldReason = result.yieldReason
292
+ || (result.stoppedAt === "waiting_llm" ? "task_ready" : (result.stoppedAt || ""));
293
+ let advanceStatus = "completed";
294
+ if (yieldReason === "graph_terminal") advanceStatus = "completed";
295
+ else if (yieldReason) advanceStatus = "waiting";
296
+ return {
297
+ status: advanceStatus,
298
+ yieldReason,
299
+ executedNodes,
300
+ failedNodes,
301
+ waitingFor: result.waitingFor || null,
302
+ stoppedAt: result.stoppedAt || "",
303
+ };
304
+ }
305
+
306
+ function advanceStoredGraph(planGraph = {}, options = {}) {
307
+ const beforeStatuses = new Map(
308
+ (Array.isArray(planGraph.nodes) ? planGraph.nodes : []).map((node) => [node.id, node.status || "pending"]),
309
+ );
310
+ const compiled = compileStoredGraph(planGraph, options);
311
+ if (!compiled.ok) {
312
+ return {
313
+ ok: false,
314
+ compile: compiled,
315
+ planGraph,
316
+ errors: validationErrorsFromCompile(compiled),
317
+ };
318
+ }
319
+
320
+ const seededOutputs = restoreOutputsMap(planGraph);
321
+ const result = executePlanGraph(
322
+ {
323
+ id: compiled.planId,
324
+ objective: compiled.objective,
325
+ nodes: compiled.nodes,
326
+ },
327
+ {
328
+ ...options,
329
+ compiled,
330
+ seedNodeMap: compiled.nodeMap,
331
+ seedOutputs: seededOutputs,
332
+ parallel: options.parallel !== false,
333
+ failurePolicy: planGraph.failurePolicy || compiled.failurePolicy || "continue_independent",
334
+ },
335
+ );
336
+
337
+ const next = {
338
+ ...planGraph,
339
+ graphId: compiled.planId || planGraph.graphId || createPlanId("plan"),
340
+ objective: compiled.objective || planGraph.objective || "",
341
+ failurePolicy: compiled.failurePolicy || planGraph.failurePolicy || "continue_independent",
342
+ nodes: Array.isArray(result.nodes) ? result.nodes : compiled.nodes,
343
+ outputs: persistOutputs(result.outputs || seededOutputs),
344
+ waitingFor: result.waitingFor || null,
345
+ lastStoppedAt: result.stoppedAt || "",
346
+ lastYieldReason: result.yieldReason || "",
347
+ stateRevision: (Number(planGraph.stateRevision) || 0) + 1,
348
+ };
349
+ next.revision = next.specRevision;
350
+
351
+ return {
352
+ ok: result.ok !== false,
353
+ compile: compiled,
354
+ planGraph: next,
355
+ result,
356
+ advance: buildAdvanceSummary(result, beforeStatuses),
357
+ errors: result.ok === false && result.stoppedAt === "compile"
358
+ ? validationErrorsFromCompile(compiled)
359
+ : [],
360
+ };
361
+ }
362
+
363
+ function projectPlanView(planGraph = {}) {
364
+ const nodes = Array.isArray(planGraph.nodes) ? planGraph.nodes : [];
365
+ const byParent = new Map();
366
+ for (const node of nodes) {
367
+ const parent = String(node.parentTaskId || "").trim();
368
+ if (!parent) continue;
369
+ if (!byParent.has(parent)) byParent.set(parent, []);
370
+ byParent.get(parent).push(node.id);
371
+ }
372
+ return nodes
373
+ .filter((node) => !node.generated || node.type === "task")
374
+ .concat(nodes.filter((node) => node.generated))
375
+ .map((node) => ({
376
+ id: node.id,
377
+ title: node.title || node.objective || node.tool || node.id,
378
+ parentId: node.parentTaskId || undefined,
379
+ displayOrder: Number(node.displayOrder) || 0,
380
+ status: node.status || "pending",
381
+ type: node.type,
382
+ execution: node.execution || "",
383
+ generated: Boolean(node.generated),
384
+ summary: (node.result && node.result.summary) || "",
385
+ children: byParent.get(node.id) || [],
386
+ }));
387
+ }
388
+
389
+ function inspectPlanGraph(planGraph = {}) {
390
+ const nodes = Array.isArray(planGraph.nodes) ? planGraph.nodes : [];
391
+ const { readyNodes, waitingNodes } = summarizeReadyWaiting(
392
+ nodes.map((node) => ({ ...node, status: node.status || "pending" })),
393
+ );
394
+ return accepted({
395
+ graphId: planGraph.graphId || "",
396
+ commandRevision: Number(planGraph.specRevision) || 0,
397
+ stateRevision: Number(planGraph.stateRevision) || 0,
398
+ revision: Number(planGraph.specRevision) || 0,
399
+ objective: planGraph.objective || "",
400
+ nodesAdded: [],
401
+ nodesUpdated: [],
402
+ readyNodes,
403
+ waitingNodes,
404
+ waitingFor: planGraph.waitingFor || null,
405
+ stoppedAt: planGraph.lastStoppedAt || "",
406
+ yieldReason: planGraph.lastYieldReason || "",
407
+ nodes: nodes.map((node) => ({
408
+ id: node.id,
409
+ type: node.type,
410
+ status: node.status || "pending",
411
+ tool: node.tool || "",
412
+ title: node.title || node.objective || "",
413
+ dependsOn: Array.isArray(node.dependsOn) ? node.dependsOn : [],
414
+ parentTaskId: node.parentTaskId || "",
415
+ execution: node.execution || "",
416
+ generated: Boolean(node.generated),
417
+ })),
418
+ planView: projectPlanView(planGraph),
419
+ validationWarnings: [],
420
+ });
421
+ }
422
+
423
+ function cacheCommand(planGraph, commandId, payload) {
424
+ if (!commandId) return;
425
+ const log = planGraph.commandLog && typeof planGraph.commandLog === "object"
426
+ ? { ...planGraph.commandLog }
427
+ : {};
428
+ log[commandId] = cloneJson(payload);
429
+ planGraph.commandLog = log;
430
+ }
431
+
432
+ /**
433
+ * Apply a normalized plan_graph command against executionState.planGraph.
434
+ */
435
+ function runPlanGraphCommand(commandInput = {}, options = {}) {
436
+ const command = normalizePlanGraphCommand(commandInput) || commandInput;
437
+ const operation = String(command && command.operation || "").trim().toLowerCase();
438
+ const executionState = ensurePlanGraphState(options.executionState);
439
+ const planGraph = executionState.planGraph;
440
+ const commandId = String(command.commandId || "").trim();
441
+
442
+ if (commandId && planGraph.commandLog && planGraph.commandLog[commandId]) {
443
+ const cached = cloneJson(planGraph.commandLog[commandId]);
444
+ return {
445
+ ...cached,
446
+ ok: cached.status === "accepted",
447
+ idempotentReplay: true,
448
+ executionState,
449
+ modelPayload: cached,
450
+ };
451
+ }
452
+
453
+ if (!operation) {
454
+ const payload = rejected([{ code: "MISSING_OPERATION", message: "operation is required" }]);
455
+ return { ...payload, executionState, modelPayload: payload };
456
+ }
457
+
458
+ if (operation === "inspect") {
459
+ const payload = inspectPlanGraph(planGraph);
460
+ return { ...payload, executionState, modelPayload: payload };
461
+ }
462
+
463
+ if (operation === "control") {
464
+ const { runControlActions } = require("../runtime/taskControl");
465
+ const controlResult = runControlActions(executionState, {
466
+ actions: Array.isArray(command.actions) ? command.actions : [],
467
+ commandId,
468
+ runTool: options.runTool,
469
+ knownTools: options.knownTools,
470
+ });
471
+ let advance = {
472
+ status: "completed",
473
+ yieldReason: "control",
474
+ executedNodes: [],
475
+ failedNodes: [],
476
+ waitingFor: null,
477
+ stoppedAt: "",
478
+ };
479
+ // Status-changing control actions may unblock ready tool nodes.
480
+ const statusOps = new Set([
481
+ "complete_task",
482
+ "skip_node",
483
+ "cancel_subtree",
484
+ "cancel_task",
485
+ "fail_task",
486
+ "mark_task_failed",
487
+ ]);
488
+ const touchedStatus = (Array.isArray(command.actions) ? command.actions : [])
489
+ .some((a) => statusOps.has(String(a && a.op || "").trim().toLowerCase()));
490
+ if (
491
+ controlResult.ok
492
+ && touchedStatus
493
+ && options.autoAdvance !== false
494
+ && typeof options.runTool === "function"
495
+ ) {
496
+ const advanced = advanceStoredGraph(executionState.planGraph, {
497
+ knownTools: options.knownTools,
498
+ runTool: options.runTool,
499
+ parallel: options.parallel !== false,
500
+ maxNodeRuns: options.maxNodeRuns,
501
+ });
502
+ if (advanced.planGraph) {
503
+ executionState.planGraph = {
504
+ ...advanced.planGraph,
505
+ commandLog: executionState.planGraph.commandLog || {},
506
+ };
507
+ }
508
+ if (advanced.advance) advance = advanced.advance;
509
+ }
510
+ const live = executionState.planGraph;
511
+ const { readyNodes, waitingNodes } = summarizeReadyWaiting(live.nodes);
512
+ const payload = controlResult.ok
513
+ ? accepted({
514
+ graphId: live.graphId || "",
515
+ commandRevision: Number(live.specRevision) || 0,
516
+ stateRevision: Number(live.stateRevision) || 0,
517
+ revision: Number(live.specRevision) || 0,
518
+ control: controlResult,
519
+ summary: advance.waitingFor
520
+ ? `waiting on ${advance.waitingFor.type}:${advance.waitingFor.id || ""}`
521
+ : (advance.yieldReason || "control actions applied"),
522
+ changes: { nodesAdded: [], nodesUpdated: [] },
523
+ nodesAdded: [],
524
+ nodesUpdated: [],
525
+ readyNodes,
526
+ waitingNodes,
527
+ waitingFor: live.waitingFor || null,
528
+ advance,
529
+ planView: projectPlanView(live),
530
+ validationWarnings: [],
531
+ })
532
+ : rejected(controlResult.errors || [{
533
+ code: "CONTROL_REJECTED",
534
+ message: "one or more control actions rejected",
535
+ }], { control: controlResult });
536
+ if (commandId && payload.status === "accepted") {
537
+ cacheCommand(executionState.planGraph, commandId, payload);
538
+ }
539
+ return { ...payload, executionState, modelPayload: payload, ok: payload.status === "accepted" };
540
+ }
541
+
542
+ if (operation === "cancel_graph" || operation === "clear") {
543
+ const previousId = planGraph.graphId || "";
544
+ executionState.planGraph = emptyPlanGraphState();
545
+ executionState.mode = "single_action";
546
+ const payload = accepted({
547
+ graphId: "",
548
+ commandRevision: 0,
549
+ stateRevision: 0,
550
+ revision: 0,
551
+ changes: { nodesAdded: [], nodesUpdated: [], archivedGraphId: previousId },
552
+ nodesAdded: [],
553
+ nodesUpdated: [],
554
+ readyNodes: [],
555
+ waitingNodes: [],
556
+ advance: { status: "completed", yieldReason: "cancelled", executedNodes: [], failedNodes: [] },
557
+ validationWarnings: [],
558
+ });
559
+ return { ...payload, executionState, modelPayload: payload };
560
+ }
561
+
562
+ if (
563
+ Number.isFinite(command.expectedSpecRevision)
564
+ && command.expectedSpecRevision !== null
565
+ && Number(planGraph.specRevision) !== Number(command.expectedSpecRevision)
566
+ ) {
567
+ const payload = rejected([{
568
+ code: "SPEC_REVISION_MISMATCH",
569
+ message: `expectedSpecRevision ${command.expectedSpecRevision}, actual ${planGraph.specRevision}`,
570
+ }], {
571
+ graphId: planGraph.graphId || "",
572
+ commandRevision: Number(planGraph.specRevision) || 0,
573
+ stateRevision: Number(planGraph.stateRevision) || 0,
574
+ });
575
+ return { ...payload, executionState, modelPayload: payload };
576
+ }
577
+
578
+ if (command.graphId && planGraph.graphId && command.graphId !== planGraph.graphId) {
579
+ const payload = rejected([{
580
+ code: "GRAPH_ID_MISMATCH",
581
+ message: `expected graphId ${command.graphId}, actual ${planGraph.graphId}`,
582
+ }]);
583
+ return { ...payload, executionState, modelPayload: payload };
584
+ }
585
+
586
+ const beforeIds = new Set(listNodeIds(planGraph.nodes));
587
+ let nextPlan = {
588
+ id: planGraph.graphId || createPlanId("plan"),
589
+ objective: planGraph.objective || "",
590
+ nodes: snapshotNodes(planGraph),
591
+ };
592
+ let nodesUpdated = [];
593
+
594
+ if (operation === "create") {
595
+ const graphSource = command.graph || {};
596
+ if (Array.isArray(graphSource.nodes)) {
597
+ nextPlan = normalizePlanGraph({
598
+ ...graphSource,
599
+ nodes: stripModelStatuses(graphSource.nodes),
600
+ });
601
+ } else {
602
+ nextPlan = normalizePlanGraph(graphSource);
603
+ nextPlan.nodes = stripModelStatuses(nextPlan.nodes);
604
+ }
605
+ if (!nextPlan.id) nextPlan.id = createPlanId("plan");
606
+ nodesUpdated = listNodeIds(nextPlan.nodes);
607
+ // Parent graphs are owned by the agent loop.
608
+ const { agentLoopOwner } = require("../runtime/graphOwner");
609
+ nextPlan.owner = agentLoopOwner(
610
+ (executionState.agentLoopId) || "agent",
611
+ );
612
+ } else if (operation === "patch") {
613
+ const ops = Array.isArray(command.operations) ? command.operations.map(normalizeExpandOp) : [];
614
+ // Freeze: reject patch that mutates running task_loop contract fields.
615
+ const { getTaskExecutionKind } = require("./planGraph");
616
+ for (const op of ops) {
617
+ const type = String(op && (op.op || op.type) || "").trim().toLowerCase();
618
+ if (type === "add_node" && op.node && getTaskExecutionKind(op.node) === "task_loop") {
619
+ // Disallow nesting task_loop when current graph is owned by a task_loop
620
+ if (planGraph.owner && planGraph.owner.kind === "task_loop") {
621
+ const payload = rejected([{
622
+ code: "NESTED_TASK_LOOP_NOT_SUPPORTED",
623
+ message: "V1 child graphs cannot create task_loop nodes",
624
+ }]);
625
+ return { ...payload, executionState, modelPayload: payload };
626
+ }
627
+ }
628
+ const targetId = String(op.nodeId || (op.node && op.node.id) || "").trim();
629
+ if (targetId) {
630
+ const existing = (planGraph.nodes || []).find((n) => n && n.id === targetId);
631
+ if (existing && existing.status === "running" && getTaskExecutionKind(existing) === "task_loop") {
632
+ const touchesSpec = type === "expand_node"
633
+ || type === "add_dependency"
634
+ || type === "remove_dependency"
635
+ || (type === "add_node" && op.node)
636
+ || Boolean(op.objective || op.title || op.execution || op.dependsOn);
637
+ if (touchesSpec) {
638
+ const payload = rejected([{
639
+ code: "RUNNING_TASK_SPEC_FROZEN",
640
+ message: `cannot mutate running task_loop ${targetId}`,
641
+ }]);
642
+ return { ...payload, executionState, modelPayload: payload };
643
+ }
644
+ }
645
+ }
646
+ }
647
+ const applied = applyPlanOperations(nextPlan, ops);
648
+ if (Array.isArray(applied.errors) && applied.errors.length > 0) {
649
+ const payload = rejected(applied.errors.map((message) => ({
650
+ code: "PATCH_ERROR",
651
+ message: String(message),
652
+ })), {
653
+ graphId: planGraph.graphId || "",
654
+ commandRevision: Number(planGraph.specRevision) || 0,
655
+ stateRevision: Number(planGraph.stateRevision) || 0,
656
+ });
657
+ return { ...payload, executionState, modelPayload: payload };
658
+ }
659
+ nextPlan = applied;
660
+ for (const op of ops) {
661
+ const nodeId = String(op.nodeId || (op.node && op.node.id) || "").trim();
662
+ if (nodeId) nodesUpdated.push(nodeId);
663
+ if (Array.isArray(op.children)) {
664
+ for (const child of op.children) {
665
+ if (child && child.id) nodesUpdated.push(String(child.id));
666
+ }
667
+ }
668
+ }
669
+ } else {
670
+ const payload = rejected([{ code: "UNKNOWN_OPERATION", message: `unknown operation: ${operation}` }]);
671
+ return { ...payload, executionState, modelPayload: payload };
672
+ }
673
+
674
+ // Validate. Do not rewrite aggregate sinks via group rewrite when storing flat nodes.
675
+ const compiled = compilePlanGraph(nextPlan, options);
676
+ if (!compiled.ok) {
677
+ const payload = rejected(validationErrorsFromCompile(compiled), {
678
+ graphId: planGraph.graphId || "",
679
+ commandRevision: Number(planGraph.specRevision) || 0,
680
+ stateRevision: Number(planGraph.stateRevision) || 0,
681
+ validationWarnings: compiled.warnings || [],
682
+ });
683
+ return { ...payload, executionState, modelPayload: payload, compile: compiled };
684
+ }
685
+
686
+ // Prefer the patched node list (includes aggregate expand status).
687
+ const preferredNodes = applyStatusesFromStore(
688
+ (Array.isArray(nextPlan.nodes) ? nextPlan.nodes : []).map((node) => normalizePlanNode(node, node.id)),
689
+ planGraph.nodes,
690
+ { preferSourceStatus: operation === "patch" },
691
+ );
692
+
693
+ // Re-validate preferred nodes for cycles/refs.
694
+ const preferredCompile = compilePlanGraph({
695
+ id: nextPlan.id,
696
+ objective: nextPlan.objective,
697
+ nodes: preferredNodes,
698
+ }, options);
699
+ if (!preferredCompile.ok) {
700
+ const payload = rejected(validationErrorsFromCompile(preferredCompile), {
701
+ graphId: planGraph.graphId || "",
702
+ commandRevision: Number(planGraph.specRevision) || 0,
703
+ stateRevision: Number(planGraph.stateRevision) || 0,
704
+ });
705
+ return { ...payload, executionState, modelPayload: payload, compile: preferredCompile };
706
+ }
707
+
708
+ const mergedNodes = applyStatusesFromStore(preferredCompile.nodes, preferredNodes, {
709
+ preferSourceStatus: true,
710
+ });
711
+ if (operation === "create") {
712
+ for (const node of mergedNodes) {
713
+ node.status = "pending";
714
+ node.result = null;
715
+ node.error = "";
716
+ node.attempt = 0;
717
+ }
718
+ }
719
+
720
+ const specRevision = (Number(planGraph.specRevision) || 0) + 1;
721
+ executionState.planGraph = {
722
+ ...planGraph,
723
+ graphId: preferredCompile.planId || nextPlan.id,
724
+ specRevision,
725
+ stateRevision: Number(planGraph.stateRevision) || 0,
726
+ revision: specRevision,
727
+ objective: preferredCompile.objective || nextPlan.objective || "",
728
+ failurePolicy: preferredCompile.failurePolicy
729
+ || nextPlan.failurePolicy
730
+ || planGraph.failurePolicy
731
+ || "continue_independent",
732
+ nodes: mergedNodes,
733
+ outputs: operation === "create" ? {} : { ...(planGraph.outputs || {}) },
734
+ waitingFor: null,
735
+ lastStoppedAt: "",
736
+ lastYieldReason: "",
737
+ commandLog: planGraph.commandLog || {},
738
+ owner: nextPlan.owner || planGraph.owner || null,
739
+ };
740
+ if (!executionState.graphs || typeof executionState.graphs !== "object") {
741
+ executionState.graphs = {};
742
+ }
743
+ executionState.graphs[executionState.planGraph.graphId] = executionState.planGraph;
744
+ executionState.mode = "plan_graph";
745
+
746
+ const afterIds = listNodeIds(mergedNodes);
747
+ const nodesAdded = afterIds.filter((id) => !beforeIds.has(id));
748
+
749
+ let advance = {
750
+ status: "completed",
751
+ yieldReason: "",
752
+ executedNodes: [],
753
+ failedNodes: [],
754
+ waitingFor: null,
755
+ stoppedAt: "",
756
+ };
757
+ if (options.autoAdvance !== false && typeof options.runTool === "function") {
758
+ const advanced = advanceStoredGraph(executionState.planGraph, {
759
+ knownTools: options.knownTools,
760
+ runTool: options.runTool,
761
+ parallel: options.parallel !== false,
762
+ maxNodeRuns: options.maxNodeRuns,
763
+ });
764
+ if (advanced.ok === false && advanced.errors && advanced.errors.length > 0 && advanced.result && advanced.result.stoppedAt === "compile") {
765
+ const payload = rejected(advanced.errors, {
766
+ graphId: executionState.planGraph.graphId,
767
+ commandRevision: specRevision,
768
+ stateRevision: Number(executionState.planGraph.stateRevision) || 0,
769
+ });
770
+ return { ...payload, executionState, modelPayload: payload };
771
+ }
772
+ if (advanced.planGraph) {
773
+ executionState.planGraph = {
774
+ ...advanced.planGraph,
775
+ specRevision,
776
+ revision: specRevision,
777
+ commandLog: planGraph.commandLog || {},
778
+ };
779
+ }
780
+ if (advanced.advance) advance = advanced.advance;
781
+ }
782
+
783
+ const live = executionState.planGraph;
784
+ const { readyNodes, waitingNodes } = summarizeReadyWaiting(live.nodes);
785
+ const payload = accepted({
786
+ graphId: live.graphId,
787
+ commandRevision: live.specRevision,
788
+ stateRevision: live.stateRevision,
789
+ revision: live.specRevision,
790
+ changes: {
791
+ nodesAdded,
792
+ nodesUpdated: Array.from(new Set(nodesUpdated)),
793
+ },
794
+ nodesAdded,
795
+ nodesUpdated: Array.from(new Set(nodesUpdated)),
796
+ readyNodes,
797
+ waitingNodes,
798
+ waitingFor: live.waitingFor || null,
799
+ stoppedAt: live.lastStoppedAt || "",
800
+ advance,
801
+ planView: projectPlanView(live),
802
+ validationWarnings: preferredCompile.warnings || [],
803
+ summary: advance.waitingFor
804
+ ? `waiting on ${advance.waitingFor.type}:${advance.waitingFor.id || ""}`
805
+ : (advance.yieldReason || "graph updated"),
806
+ });
807
+
808
+ cacheCommand(executionState.planGraph, commandId, payload);
809
+
810
+ // Agent Loop create enables Plan Mode; TaskLoop child graphs must not.
811
+ let planModeEntered = null;
812
+ if (operation === "create" && payload.status === "accepted") {
813
+ const ownerKind = String(
814
+ (executionState.planGraph && executionState.planGraph.owner && executionState.planGraph.owner.kind)
815
+ || "agent_loop",
816
+ ).trim();
817
+ if (ownerKind === "agent_loop") {
818
+ const { enterPlanModeAfterGraphCreate } = require("./planMode");
819
+ planModeEntered = enterPlanModeAfterGraphCreate(executionState, {
820
+ reason: "plan_graph create",
821
+ });
822
+ }
823
+ }
824
+
825
+ return {
826
+ ...payload,
827
+ executionState,
828
+ modelPayload: payload,
829
+ compile: preferredCompile,
830
+ planModeEntered,
831
+ };
832
+ }
833
+
834
+ function activePlanRequiresExpansion(planGraph = {}) {
835
+ if (!planGraph || !planGraph.graphId) return false;
836
+ const waiting = planGraph.waitingFor;
837
+ if (!waiting || waiting.type !== "task") return false;
838
+ const node = (Array.isArray(planGraph.nodes) ? planGraph.nodes : [])
839
+ .find((entry) => entry.id === waiting.id);
840
+ if (!node || node.type !== "task") return false;
841
+ if (isAggregateTask(node)) return false;
842
+ return node.status === "waiting_llm";
843
+ }
844
+
845
+ module.exports = {
846
+ emptyPlanGraphState,
847
+ ensurePlanGraphState,
848
+ normalizePlanGraphCommand,
849
+ normalizeExpandOp,
850
+ runPlanGraphCommand,
851
+ inspectPlanGraph,
852
+ advanceStoredGraph,
853
+ executionSegmentToCreateGraph,
854
+ projectPlanView,
855
+ activePlanRequiresExpansion,
856
+ stripModelStatuses,
857
+ };