u-foo 3.0.5 → 3.0.7

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.7",
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",
package/src/code/agent.js CHANGED
@@ -610,6 +610,21 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
610
610
  : runNativeAgentTask;
611
611
  const onPhase = typeof options.onPhase === "function" ? options.onPhase : null;
612
612
  const onThinkingDelta = typeof options.onThinkingDelta === "function" ? options.onThinkingDelta : null;
613
+ const onContextUsage = typeof options.onContextUsage === "function" ? options.onContextUsage : null;
614
+ const applyContextMeter = (meter = null) => {
615
+ if (!meter || typeof meter !== "object") return null;
616
+ state.contextMeter = {
617
+ usedTokens: Number(meter.usedTokens) || 0,
618
+ limitTokens: Number(meter.limitTokens) || 0,
619
+ model: String(meter.model || state.model || "").trim(),
620
+ label: String(meter.label || "").trim(),
621
+ updatedAt: String(meter.updatedAt || "").trim() || new Date().toISOString(),
622
+ };
623
+ if (typeof onContextUsage === "function") {
624
+ try { onContextUsage(state.contextMeter); } catch { /* ignore */ }
625
+ }
626
+ return state.contextMeter;
627
+ };
613
628
  let lastTranscriptBaseline = 0;
614
629
  const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => {
615
630
  toolEventsThisAttempt = 0;
@@ -630,6 +645,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
630
645
  onStreamDelta: onStream,
631
646
  onThinkingDelta,
632
647
  onPhase,
648
+ onContextUsage: applyContextMeter,
633
649
  executionState: state.executionState || null,
634
650
  onArtifactPersisted: (persisted) => recordToolCallInSession(state, persisted, workspaceRoot),
635
651
  onToolEvent: (event) => {
@@ -779,6 +795,9 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
779
795
  const artifactIds = Array.isArray(state.workingSet)
780
796
  ? state.workingSet.map((entry) => entry.artifactId).filter(Boolean)
781
797
  : [];
798
+ if (cliRes && cliRes.contextMeter) {
799
+ applyContextMeter(cliRes.contextMeter);
800
+ }
782
801
  if (cliRes && cliRes.waitingUserInteraction) {
783
802
  return {
784
803
  ok: true,
@@ -791,6 +810,8 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
791
810
  streamLastChar,
792
811
  waitingUserInteraction: true,
793
812
  interactionId: cliRes.interactionId || "",
813
+ contextMeter: state.contextMeter || null,
814
+ usage: cliRes.usage || null,
794
815
  };
795
816
  }
796
817
  return {
@@ -802,6 +823,8 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
802
823
  metrics: {},
803
824
  streamed: Boolean(streamed || cliRes.streamed),
804
825
  streamLastChar,
826
+ contextMeter: state.contextMeter || null,
827
+ usage: cliRes.usage || null,
805
828
  };
806
829
  } catch (err) {
807
830
  return {
@@ -925,6 +948,9 @@ function buildSessionSnapshotFromState(state = {}) {
925
948
  ? Math.max(0, Math.floor(source.toolCallsSinceCommit))
926
949
  : 0,
927
950
  activeSkills: Array.isArray(source.activeSkills) ? source.activeSkills : [],
951
+ contextMeter: source.contextMeter && typeof source.contextMeter === "object"
952
+ ? source.contextMeter
953
+ : null,
928
954
  };
929
955
  }
930
956
 
@@ -999,6 +1025,9 @@ function resumeSessionState(state = {}, sessionId = "", workspaceRoot = process.
999
1025
  ? snapshot.toolCallsSinceCommit
1000
1026
  : 0;
1001
1027
  state.activeSkills = Array.isArray(snapshot.activeSkills) ? snapshot.activeSkills : [];
1028
+ state.contextMeter = snapshot.contextMeter && typeof snapshot.contextMeter === "object"
1029
+ ? snapshot.contextMeter
1030
+ : null;
1002
1031
  ensureContextSessionState(state);
1003
1032
  const { ensureTranscript } = require("./context/assembler");
1004
1033
  ensureTranscript(state, state.workspaceRoot);
@@ -1087,6 +1116,13 @@ async function resumeAfterUserInteraction(answerText = "", state = {}, options =
1087
1116
  onStreamDelta: trackingOnDelta,
1088
1117
  onThinkingDelta: typeof options.onThinkingDelta === "function" ? options.onThinkingDelta : null,
1089
1118
  onPhase: typeof options.onPhase === "function" ? options.onPhase : null,
1119
+ onContextUsage: (meter) => {
1120
+ if (!meter || typeof meter !== "object") return;
1121
+ state.contextMeter = meter;
1122
+ if (typeof options.onContextUsage === "function") {
1123
+ try { options.onContextUsage(meter); } catch { /* ignore */ }
1124
+ }
1125
+ },
1090
1126
  executionState: state.executionState,
1091
1127
  signal: options.signal,
1092
1128
  resume: true,
@@ -1098,6 +1134,9 @@ async function resumeAfterUserInteraction(answerText = "", state = {}, options =
1098
1134
  if (cliRes && Array.isArray(cliRes.messages)) {
1099
1135
  state.nlMessages = stripSkillBlocksFromMessages(cliRes.messages);
1100
1136
  }
1137
+ if (cliRes && cliRes.contextMeter) {
1138
+ state.contextMeter = cliRes.contextMeter;
1139
+ }
1101
1140
 
1102
1141
  if (!cliRes || cliRes.ok === false) {
1103
1142
  return {
@@ -1107,6 +1146,7 @@ async function resumeAfterUserInteraction(answerText = "", state = {}, options =
1107
1146
  waitingUserInteraction: false,
1108
1147
  streamed: false,
1109
1148
  streamLastChar: "",
1149
+ contextMeter: state.contextMeter || null,
1110
1150
  };
1111
1151
  }
1112
1152
 
@@ -1119,6 +1159,7 @@ async function resumeAfterUserInteraction(answerText = "", state = {}, options =
1119
1159
  interactionId: cliRes.interactionId || "",
1120
1160
  streamed: Boolean(cliRes.streamed),
1121
1161
  streamLastChar,
1162
+ contextMeter: state.contextMeter || null,
1122
1163
  };
1123
1164
  }
1124
1165
 
@@ -1129,6 +1170,7 @@ async function resumeAfterUserInteraction(answerText = "", state = {}, options =
1129
1170
  waitingUserInteraction: false,
1130
1171
  streamed: Boolean(cliRes.streamed),
1131
1172
  streamLastChar,
1173
+ contextMeter: state.contextMeter || null,
1132
1174
  };
1133
1175
  }
1134
1176
 
@@ -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
  };