u-foo 3.0.5 → 3.0.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "3.0.5",
3
+ "version": "3.0.6",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -430,17 +430,128 @@ function cacheCommand(planGraph, commandId, payload) {
430
430
  }
431
431
 
432
432
  /**
433
- * Apply a normalized plan_graph command against executionState.planGraph.
433
+ * Resolve which graph a plan_graph command targets.
434
+ * Parent lives in executionState.planGraph; TaskLoop children live in graphs[].
435
+ */
436
+ function selectGraphForCommand(executionState = null, command = {}) {
437
+ const state = ensurePlanGraphState(executionState);
438
+ if (!state.graphs || typeof state.graphs !== "object") state.graphs = {};
439
+ const primary = state.planGraph && typeof state.planGraph === "object"
440
+ ? state.planGraph
441
+ : emptyPlanGraphState();
442
+ if (primary.graphId) state.graphs[primary.graphId] = primary;
443
+
444
+ const requested = String(command && command.graphId || "").trim();
445
+ if (!requested) {
446
+ return {
447
+ ok: true,
448
+ graph: primary,
449
+ isPrimary: true,
450
+ primaryGraphId: String(primary.graphId || ""),
451
+ };
452
+ }
453
+ if (primary.graphId && primary.graphId === requested) {
454
+ return {
455
+ ok: true,
456
+ graph: primary,
457
+ isPrimary: true,
458
+ primaryGraphId: primary.graphId,
459
+ };
460
+ }
461
+ const mapped = state.graphs[requested];
462
+ if (mapped && typeof mapped === "object") {
463
+ return {
464
+ ok: true,
465
+ graph: mapped,
466
+ isPrimary: false,
467
+ primaryGraphId: String(primary.graphId || ""),
468
+ };
469
+ }
470
+ return {
471
+ ok: false,
472
+ code: "GRAPH_NOT_FOUND",
473
+ message: `graphId ${requested} not found`,
474
+ };
475
+ }
476
+
477
+ /**
478
+ * Apply a normalized plan_graph command against executionState.planGraph
479
+ * (or a TaskLoop child graph when command.graphId selects it).
434
480
  */
435
481
  function runPlanGraphCommand(commandInput = {}, options = {}) {
436
482
  const command = normalizePlanGraphCommand(commandInput) || commandInput;
437
483
  const operation = String(command && command.operation || "").trim().toLowerCase();
438
484
  const executionState = ensurePlanGraphState(options.executionState);
439
- const planGraph = executionState.planGraph;
485
+ if (!executionState.graphs || typeof executionState.graphs !== "object") {
486
+ executionState.graphs = {};
487
+ }
440
488
  const commandId = String(command.commandId || "").trim();
441
489
 
490
+ if (!operation) {
491
+ const payload = rejected([{ code: "MISSING_OPERATION", message: "operation is required" }]);
492
+ return { ...payload, executionState, modelPayload: payload };
493
+ }
494
+
495
+ // create/control always target the primary agent graph; patch/inspect may
496
+ // select a TaskLoop child via command.graphId.
497
+ const selected = (operation === "patch" || operation === "inspect")
498
+ ? selectGraphForCommand(executionState, command)
499
+ : {
500
+ ok: true,
501
+ graph: executionState.planGraph,
502
+ isPrimary: true,
503
+ primaryGraphId: String(executionState.planGraph && executionState.planGraph.graphId || ""),
504
+ };
505
+
506
+ if (!selected.ok) {
507
+ const payload = rejected([{
508
+ code: selected.code || "GRAPH_NOT_FOUND",
509
+ message: selected.message || "graph not found",
510
+ }]);
511
+ return { ...payload, executionState, modelPayload: payload };
512
+ }
513
+
514
+ const primaryGraphId = selected.primaryGraphId
515
+ || String(executionState.planGraph && executionState.planGraph.graphId || "");
516
+ // Work on the selected graph for this command; restore primary afterward if child.
517
+ if (!selected.isPrimary) {
518
+ executionState.planGraph = selected.graph;
519
+ }
520
+ const planGraph = executionState.planGraph;
521
+
522
+ function restorePrimaryGraph() {
523
+ if (executionState.planGraph && executionState.planGraph.graphId) {
524
+ executionState.graphs[executionState.planGraph.graphId] = executionState.planGraph;
525
+ }
526
+ if (!selected.isPrimary && primaryGraphId && executionState.graphs[primaryGraphId]) {
527
+ executionState.planGraph = executionState.graphs[primaryGraphId];
528
+ } else if (executionState.planGraph && executionState.planGraph.graphId) {
529
+ executionState.graphs[executionState.planGraph.graphId] = executionState.planGraph;
530
+ }
531
+ }
532
+
533
+ function resumeChildTaskLoopIfNeeded(payload = null) {
534
+ if (selected.isPrimary) return null;
535
+ if (!payload || payload.status !== "accepted") return null;
536
+ if (typeof options.runTool !== "function") return null;
537
+ const childId = String((selected.graph && selected.graph.graphId) || "").trim();
538
+ const childLive = childId ? executionState.graphs[childId] : null;
539
+ const owner = childLive && childLive.owner;
540
+ if (!owner || owner.kind !== "task_loop" || !owner.taskRunId) return null;
541
+ try {
542
+ const { processTaskRun } = require("../runtime/taskLoop");
543
+ return processTaskRun(executionState, owner.taskRunId, {
544
+ runTool: options.runTool,
545
+ knownTools: options.knownTools,
546
+ });
547
+ } catch {
548
+ return null;
549
+ }
550
+ }
551
+
442
552
  if (commandId && planGraph.commandLog && planGraph.commandLog[commandId]) {
443
553
  const cached = cloneJson(planGraph.commandLog[commandId]);
554
+ restorePrimaryGraph();
444
555
  return {
445
556
  ...cached,
446
557
  ok: cached.status === "accepted",
@@ -450,13 +561,9 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
450
561
  };
451
562
  }
452
563
 
453
- if (!operation) {
454
- const payload = rejected([{ code: "MISSING_OPERATION", message: "operation is required" }]);
455
- return { ...payload, executionState, modelPayload: payload };
456
- }
457
-
458
564
  if (operation === "inspect") {
459
565
  const payload = inspectPlanGraph(planGraph);
566
+ restorePrimaryGraph();
460
567
  return { ...payload, executionState, modelPayload: payload };
461
568
  }
462
569
 
@@ -556,9 +663,15 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
556
663
  advance: { status: "completed", yieldReason: "cancelled", executedNodes: [], failedNodes: [] },
557
664
  validationWarnings: [],
558
665
  });
666
+ restorePrimaryGraph();
559
667
  return { ...payload, executionState, modelPayload: payload };
560
668
  }
561
669
 
670
+ function rejectWorking(payload, extra = {}) {
671
+ restorePrimaryGraph();
672
+ return { ...payload, executionState, modelPayload: payload, ...extra };
673
+ }
674
+
562
675
  if (
563
676
  Number.isFinite(command.expectedSpecRevision)
564
677
  && command.expectedSpecRevision !== null
@@ -572,15 +685,16 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
572
685
  commandRevision: Number(planGraph.specRevision) || 0,
573
686
  stateRevision: Number(planGraph.stateRevision) || 0,
574
687
  });
575
- return { ...payload, executionState, modelPayload: payload };
688
+ return rejectWorking(payload);
576
689
  }
577
690
 
578
691
  if (command.graphId && planGraph.graphId && command.graphId !== planGraph.graphId) {
692
+ // Should not happen after selectGraphForCommand switched the working graph.
579
693
  const payload = rejected([{
580
694
  code: "GRAPH_ID_MISMATCH",
581
695
  message: `expected graphId ${command.graphId}, actual ${planGraph.graphId}`,
582
696
  }]);
583
- return { ...payload, executionState, modelPayload: payload };
697
+ return rejectWorking(payload);
584
698
  }
585
699
 
586
700
  const beforeIds = new Set(listNodeIds(planGraph.nodes));
@@ -622,7 +736,7 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
622
736
  code: "NESTED_TASK_LOOP_NOT_SUPPORTED",
623
737
  message: "V1 child graphs cannot create task_loop nodes",
624
738
  }]);
625
- return { ...payload, executionState, modelPayload: payload };
739
+ return rejectWorking(payload);
626
740
  }
627
741
  }
628
742
  const targetId = String(op.nodeId || (op.node && op.node.id) || "").trim();
@@ -639,7 +753,7 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
639
753
  code: "RUNNING_TASK_SPEC_FROZEN",
640
754
  message: `cannot mutate running task_loop ${targetId}`,
641
755
  }]);
642
- return { ...payload, executionState, modelPayload: payload };
756
+ return rejectWorking(payload);
643
757
  }
644
758
  }
645
759
  }
@@ -654,7 +768,7 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
654
768
  commandRevision: Number(planGraph.specRevision) || 0,
655
769
  stateRevision: Number(planGraph.stateRevision) || 0,
656
770
  });
657
- return { ...payload, executionState, modelPayload: payload };
771
+ return rejectWorking(payload);
658
772
  }
659
773
  nextPlan = applied;
660
774
  for (const op of ops) {
@@ -668,7 +782,7 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
668
782
  }
669
783
  } else {
670
784
  const payload = rejected([{ code: "UNKNOWN_OPERATION", message: `unknown operation: ${operation}` }]);
671
- return { ...payload, executionState, modelPayload: payload };
785
+ return rejectWorking(payload);
672
786
  }
673
787
 
674
788
  // Validate. Do not rewrite aggregate sinks via group rewrite when storing flat nodes.
@@ -680,7 +794,7 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
680
794
  stateRevision: Number(planGraph.stateRevision) || 0,
681
795
  validationWarnings: compiled.warnings || [],
682
796
  });
683
- return { ...payload, executionState, modelPayload: payload, compile: compiled };
797
+ return rejectWorking(payload, { compile: compiled });
684
798
  }
685
799
 
686
800
  // Prefer the patched node list (includes aggregate expand status).
@@ -702,7 +816,7 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
702
816
  commandRevision: Number(planGraph.specRevision) || 0,
703
817
  stateRevision: Number(planGraph.stateRevision) || 0,
704
818
  });
705
- return { ...payload, executionState, modelPayload: payload, compile: preferredCompile };
819
+ return rejectWorking(payload, { compile: preferredCompile });
706
820
  }
707
821
 
708
822
  const mergedNodes = applyStatusesFromStore(preferredCompile.nodes, preferredNodes, {
@@ -736,6 +850,8 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
736
850
  lastYieldReason: "",
737
851
  commandLog: planGraph.commandLog || {},
738
852
  owner: nextPlan.owner || planGraph.owner || null,
853
+ parentGraphId: planGraph.parentGraphId || "",
854
+ parentNodeId: planGraph.parentNodeId || "",
739
855
  };
740
856
  if (!executionState.graphs || typeof executionState.graphs !== "object") {
741
857
  executionState.graphs = {};
@@ -767,7 +883,7 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
767
883
  commandRevision: specRevision,
768
884
  stateRevision: Number(executionState.planGraph.stateRevision) || 0,
769
885
  });
770
- return { ...payload, executionState, modelPayload: payload };
886
+ return rejectWorking(payload);
771
887
  }
772
888
  if (advanced.planGraph) {
773
889
  executionState.planGraph = {
@@ -775,6 +891,9 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
775
891
  specRevision,
776
892
  revision: specRevision,
777
893
  commandLog: planGraph.commandLog || {},
894
+ owner: advanced.planGraph.owner || planGraph.owner || null,
895
+ parentGraphId: advanced.planGraph.parentGraphId || planGraph.parentGraphId || "",
896
+ parentNodeId: advanced.planGraph.parentNodeId || planGraph.parentNodeId || "",
778
897
  };
779
898
  }
780
899
  if (advanced.advance) advance = advanced.advance;
@@ -822,12 +941,16 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
822
941
  }
823
942
  }
824
943
 
944
+ restorePrimaryGraph();
945
+ const resumed = resumeChildTaskLoopIfNeeded(payload);
946
+
825
947
  return {
826
948
  ...payload,
827
949
  executionState,
828
950
  modelPayload: payload,
829
951
  compile: preferredCompile,
830
952
  planModeEntered,
953
+ taskLoopResume: resumed || null,
831
954
  };
832
955
  }
833
956
 
@@ -853,5 +976,6 @@ module.exports = {
853
976
  executionSegmentToCreateGraph,
854
977
  projectPlanView,
855
978
  activePlanRequiresExpansion,
979
+ selectGraphForCommand,
856
980
  stripModelStatuses,
857
981
  };
@@ -56,6 +56,7 @@ function buildImmutablePrefix() {
56
56
  "- After an accepted plan_graph create or patch, Runtime automatically advances ready tool nodes. Never invent or request an execute_graph tool.",
57
57
  "- Do not call plan_graph or task_run together with read, read_image, write, edit, bash, or artifact_read in the same assistant turn.",
58
58
  "- When an active graph is waiting on a task, advance that node through plan_graph instead of bypassing it with direct workspace tools: use patch.expand_node for execution.kind=expand, control.complete_task (nodeId) for execution.kind=inline_llm, or control.start_task for execution.kind=task_loop.",
59
+ "- TaskLoop start returns childGraphId. While that TaskRun is waiting_model on child root, patch with graphId=<childGraphId> and expand_node nodeId=root (add tool children). Do not ask the user to /plan off.",
59
60
  "- Do not end a turn with text only while the plan is still waiting on a task; expand, start, or complete that node. Runtime will auto-continue if you stop early, but prefer advancing in the same turn.",
60
61
  "- control.complete_task with nodeId completes a waiting_llm inline_llm task for the current Graph owner. control.complete_task with taskRunId (or task_run complete) is reserved for the owning TaskLoop. Do not directly complete expand or aggregate tasks.",
61
62
  "- While Plan Mode is ON, workspace mutations must be represented as plan_graph tool nodes or performed inside a running TaskRun/task_loop.",
@@ -387,15 +387,40 @@ function resolveUserInteraction(executionState = null, rawText = "") {
387
387
  };
388
388
  }
389
389
 
390
- function formatInteractionPromptLines(pending = null) {
390
+ function wrapLabeledBlock(label = "", prompt = "", cols = 80) {
391
+ const title = String(label || "Question").trim() || "Question";
392
+ const text = String(prompt || "").replace(/\s+/g, " ").trim();
393
+ const width = Math.max(24, Math.min(120, Math.floor(Number(cols) || 80) - 2));
394
+ if (!text) return [`${title}:`];
395
+
396
+ const prefix = `${title}: `;
397
+ const firstWidth = Math.max(8, width - prefix.length);
398
+ const contPrefix = " ";
399
+ const contWidth = Math.max(8, width - contPrefix.length);
400
+ const lines = [];
401
+
402
+ let offset = 0;
403
+ const firstChunk = text.slice(0, firstWidth);
404
+ lines.push(`${prefix}${firstChunk}`);
405
+ offset = firstChunk.length;
406
+ while (offset < text.length) {
407
+ const chunk = text.slice(offset, offset + contWidth);
408
+ lines.push(`${contPrefix}${chunk}`);
409
+ offset += chunk.length;
410
+ }
411
+ return lines;
412
+ }
413
+
414
+ function formatInteractionPromptLines(pending = null, options = {}) {
391
415
  if (!pending) return [];
416
+ const cols = Number(options.cols) > 0 ? Number(options.cols) : 80;
392
417
  const lines = [];
393
418
  const kind = pending.kind || "chat";
394
419
  if (kind === "approval") {
395
- lines.push(`Approval: ${pending.prompt}`);
420
+ lines.push(...wrapLabeledBlock("Approval", pending.prompt, cols));
396
421
  lines.push(" [yes] Yes [no] No or type a free-text reply");
397
422
  } else if (kind === "choice") {
398
- lines.push(`Choice: ${pending.prompt}`);
423
+ lines.push(...wrapLabeledBlock("Choice", pending.prompt, cols));
399
424
  for (const opt of pending.options || []) {
400
425
  lines.push(` [${opt.key}] ${opt.label}`);
401
426
  }
@@ -403,7 +428,7 @@ function formatInteractionPromptLines(pending = null) {
403
428
  lines.push(" or type a free-text reply");
404
429
  }
405
430
  } else {
406
- lines.push(`Question: ${pending.prompt}`);
431
+ lines.push(...wrapLabeledBlock("Question", pending.prompt, cols));
407
432
  lines.push(" (type your reply)");
408
433
  }
409
434
  return lines;
@@ -96,14 +96,16 @@ function buildTaskRunWakeReminder(executionState = null) {
96
96
  if (runs.length === 0 && !hasPendingAgentMailbox(executionState)) return "";
97
97
  const lines = [
98
98
  "Runtime wake (not a user message): active TaskRun(s) still need the Agent Loop.",
99
- "Continue serving them now (read/inspect/edit via tools, or plan_graph/task_run control).",
100
- "Do not end the turn with text only while a TaskRun is waiting_model.",
99
+ "Continue serving them now. For a TaskLoop child graph waiting on root:",
100
+ "call plan_graph operation=patch with graphId=<childGraphId> and expand_node on nodeId=root",
101
+ "(tool children). Do not ask the user to /plan off. Do not end with text only.",
101
102
  ];
102
103
  for (const run of runs.slice(0, 4)) {
103
104
  const label = run.title || run.objective || run.parentNodeId || run.id;
104
105
  lines.push(
105
106
  `- taskRunId=${run.id} phase=${run.phase || ""} status=${run.status || ""}`
106
- + (label ? ` — ${String(label).slice(0, 160)}` : "")
107
+ + (run.childGraphId ? ` childGraphId=${run.childGraphId}` : "")
108
+ + (label ? ` — ${String(label).slice(0, 120)}` : "")
107
109
  );
108
110
  }
109
111
  return lines.join("\n");
@@ -172,7 +172,9 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
172
172
  );
173
173
  setPlanUi((prev) => (prev && prev.hash === next.hash ? prev : next));
174
174
  const pending = getPendingUserInteraction(props.state && props.state.executionState);
175
- setInteractionLines(pending ? formatInteractionPromptLines(pending) : []);
175
+ setInteractionLines(pending ? formatInteractionPromptLines(pending, {
176
+ cols: size.cols || 80,
177
+ }) : []);
176
178
  return next;
177
179
  } catch {
178
180
  return null;
@@ -1284,7 +1286,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
1284
1286
  ...interactionLines.map((line, idx) => h(Text, {
1285
1287
  key: `ask-${idx}`,
1286
1288
  color: "yellow",
1287
- wrap: "truncate",
1289
+ wrap: "wrap",
1288
1290
  }, line || " ")),
1289
1291
  )
1290
1292
  : null,