u-foo 3.0.3 → 3.0.4

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.3",
3
+ "version": "3.0.4",
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",
@@ -232,6 +232,7 @@ function renderPlanModeContext(executionState = null) {
232
232
  " 2) expand_node on the current ready/waiting task when needed",
233
233
  " 3) Runtime executes ready tools / TaskLoops; continue from results",
234
234
  " 4) control.complete_task (inline) or control.start_task (task_loop)",
235
+ "Do not end the turn with text only while a task is waiting — advance it.",
235
236
  "Do not mix plan_graph with data-plane tools in the same turn.",
236
237
  );
237
238
 
@@ -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
+ "- 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.",
59
60
  "- 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.",
60
61
  "- While Plan Mode is ON, workspace mutations must be represented as plan_graph tool nodes or performed inside a running TaskRun/task_loop.",
61
62
  "- Treat a User reminder as the latest user instruction. Reconcile it before continuing from tool results. If it is compatible with the active plan, resume the waiting plan node; otherwise patch, cancel, or replan first.",
@@ -104,6 +104,52 @@ function buildContinuationUserPrompt(userText = "", executionState = null) {
104
104
  return formatUserReminderMessage([text], { waitingFor: waiting });
105
105
  }
106
106
 
107
+ const PLAN_AUTO_CONTINUE_STOP_REASONS = new Set([
108
+ "approval_required",
109
+ "graph_terminal",
110
+ "scheduler_deadlock",
111
+ ]);
112
+
113
+ /**
114
+ * Whether the Agent Loop should keep going after a text-only model turn
115
+ * because the plan graph is still waiting on an agent-actionable task.
116
+ */
117
+ function shouldAutoContinuePlan(executionState = null) {
118
+ if (!executionState || typeof executionState !== "object") return false;
119
+ if (executionState.pendingUserInteraction) return false;
120
+ const pg = executionState.planGraph && typeof executionState.planGraph === "object"
121
+ ? executionState.planGraph
122
+ : null;
123
+ if (!pg) return false;
124
+ const waiting = pg.waitingFor && typeof pg.waitingFor === "object" ? pg.waitingFor : null;
125
+ if (!waiting || !waiting.id) return false;
126
+ if (String(waiting.type || "").trim().toLowerCase() !== "task") return false;
127
+ const yieldReason = String(pg.lastYieldReason || "").trim().toLowerCase();
128
+ if (yieldReason && PLAN_AUTO_CONTINUE_STOP_REASONS.has(yieldReason)) return false;
129
+ return true;
130
+ }
131
+
132
+ /**
133
+ * Internal reminder injected by runtime when the model ends a turn while the
134
+ * plan is still waiting on a task. Same shape as user nudges.
135
+ */
136
+ function buildPlanAutoContinueReminder(executionState = null) {
137
+ const waiting = executionState
138
+ && executionState.planGraph
139
+ && executionState.planGraph.waitingFor
140
+ ? executionState.planGraph.waitingFor
141
+ : null;
142
+ if (!waiting || !waiting.id) return "";
143
+ return formatUserReminderMessage(
144
+ [
145
+ "Continue the active plan. Serve the waiting task now via plan_graph "
146
+ + "(expand_node, control.start_task, or control.complete_task as appropriate). "
147
+ + "Do not end the turn with text only while this node is waiting.",
148
+ ],
149
+ { waitingFor: waiting },
150
+ );
151
+ }
152
+
107
153
  module.exports = {
108
154
  ensurePendingUserPrompts,
109
155
  enqueueUserPrompt,
@@ -113,4 +159,7 @@ module.exports = {
113
159
  shouldFrameAsUserReminder,
114
160
  formatUserReminderMessage,
115
161
  buildContinuationUserPrompt,
162
+ shouldAutoContinuePlan,
163
+ buildPlanAutoContinueReminder,
164
+ PLAN_AUTO_CONTINUE_STOP_REASONS,
116
165
  };
@@ -27,6 +27,8 @@ const {
27
27
  clearUserPrompts,
28
28
  formatUserReminderMessage,
29
29
  ensurePendingUserPrompts,
30
+ shouldAutoContinuePlan,
31
+ buildPlanAutoContinueReminder,
30
32
  } = require("./context/userNudge");
31
33
  const {
32
34
  runAskUserTool,
@@ -85,6 +87,8 @@ const DEFAULT_KIMI_MODEL = "k3";
85
87
  const DEFAULT_MAX_NATIVE_TOOL_CALLS = 100;
86
88
  const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 20;
87
89
  const DEFAULT_NATIVE_TIMEOUT_MS = 43200000; // 12 hours
90
+ /** Max text-only auto-continues while a plan is waiting on a task (per user submit). */
91
+ const DEFAULT_MAX_PLAN_AUTO_CONTINUES = 24;
88
92
  // Anthropic Messages rejects max_tokens above the model's real cap (64K on
89
93
  // current models), so the transports use different defaults. Override either
90
94
  // via UFOO_UCODE_MAX_TOKENS (positive integer).
@@ -1759,6 +1763,9 @@ async function runNativeLoop({
1759
1763
  // materialize Provider messages yet. STRICT via UFOO_UCODE_PROTOCOL_STRICT=1.
1760
1764
  let activeLedger = null;
1761
1765
  let lastProtocolLedger = null;
1766
+ let planAutoContinues = 0;
1767
+ let lastAutoContinueWaitingId = "";
1768
+ let consecutiveEmptyAutoContinues = 0;
1762
1769
 
1763
1770
  if (resume) {
1764
1771
  await withFaultPoint("before_provider_resume", () => {});
@@ -1775,6 +1782,29 @@ async function runNativeLoop({
1775
1782
  messages.push({ role: "user", content });
1776
1783
  }
1777
1784
 
1785
+ function tryInjectPlanAutoContinue() {
1786
+ if (!shouldAutoContinuePlan(executionState)) return false;
1787
+ if (planAutoContinues >= DEFAULT_MAX_PLAN_AUTO_CONTINUES) return false;
1788
+ const waitingId = String(
1789
+ (executionState.planGraph && executionState.planGraph.waitingFor
1790
+ && executionState.planGraph.waitingFor.id) || ""
1791
+ ).trim();
1792
+ if (
1793
+ consecutiveEmptyAutoContinues >= 2
1794
+ && waitingId
1795
+ && waitingId === lastAutoContinueWaitingId
1796
+ ) {
1797
+ return false;
1798
+ }
1799
+ const reminder = buildPlanAutoContinueReminder(executionState);
1800
+ if (!reminder) return false;
1801
+ messages.push({ role: "user", content: reminder });
1802
+ planAutoContinues += 1;
1803
+ lastAutoContinueWaitingId = waitingId;
1804
+ consecutiveEmptyAutoContinues += 1;
1805
+ return true;
1806
+ }
1807
+
1778
1808
  while (true) {
1779
1809
  guards.ensureActive();
1780
1810
 
@@ -1846,6 +1876,9 @@ async function runNativeLoop({
1846
1876
  if (!aggregated.trim() && text) {
1847
1877
  aggregated = text;
1848
1878
  }
1879
+ if (tryInjectPlanAutoContinue()) {
1880
+ continue;
1881
+ }
1849
1882
  return {
1850
1883
  text: aggregated,
1851
1884
  streamed,
@@ -1857,6 +1890,9 @@ async function runNativeLoop({
1857
1890
  };
1858
1891
  }
1859
1892
 
1893
+ // A tool-using turn resets the empty auto-continue streak (progress possible).
1894
+ consecutiveEmptyAutoContinues = 0;
1895
+
1860
1896
  const pendingCalls = transport.prepareToolCalls({ messages, turnResult, toolCalls });
1861
1897
  if (!pendingCalls) {
1862
1898
  return {