u-foo 2.3.3 → 2.3.5

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": "2.3.3",
3
+ "version": "2.3.5",
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",
@@ -33,8 +33,8 @@ function computeAgentBar(options = {}) {
33
33
  let windowItems = Math.max(1, Math.min(maxAgentWindow, activeAgents.length));
34
34
  let start = agentListWindowStart;
35
35
  const ufooItem = focusMode === "dashboard" && selectedAgentIndex === 0
36
- ? "\x1b[90;7mucode\x1b[0m"
37
- : "\x1b[36mucode\x1b[0m";
36
+ ? "\x1b[90;7mufoo\x1b[0m"
37
+ : "\x1b[36mufoo\x1b[0m";
38
38
  const ufooLen = stripAnsi(ufooItem).length;
39
39
 
40
40
  const computeStart = (items) => {
@@ -78,7 +78,7 @@ function computeAgentBar(options = {}) {
78
78
  const prefix = indicator
79
79
  ? `${indicatorColor}${indicator}\x1b[0m`
80
80
  : "";
81
- const idx = s + i + 1; // +1 for ucode at index 0
81
+ const idx = s + i + 1; // +1 for ufoo chat at index 0
82
82
  if (focusMode === "dashboard" && idx === selectedAgentIndex) {
83
83
  return `${prefix}\x1b[90;7m${label}\x1b[0m`;
84
84
  }
package/src/code/agent.js CHANGED
@@ -1119,6 +1119,7 @@ async function runUbusCommand(state = {}, options = {}) {
1119
1119
  // eslint-disable-next-line no-await-in-loop
1120
1120
  nlResult = await runNl(message.task, state, {
1121
1121
  onProgress: progressReporter,
1122
+ signal: options.signal,
1122
1123
  });
1123
1124
  } catch (err) {
1124
1125
  sendErrors.push(`task from ${message.publisher} failed: ${err && err.message ? err.message : "task failed"}`);
@@ -1180,7 +1181,9 @@ async function runUbusCommand(state = {}, options = {}) {
1180
1181
  });
1181
1182
  }
1182
1183
 
1183
- const nlResult = await runNl(item.task, state);
1184
+ const nlResult = await runNl(item.task, state, {
1185
+ signal: options.signal,
1186
+ });
1184
1187
  const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
1185
1188
  const sendRes = shell(`ufoo bus send ${shellQuote(item.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
1186
1189
  if (!sendRes.ok) {
@@ -9,6 +9,8 @@ const { getBashToolDescription } = require("./prompts/toolDescriptions/bash");
9
9
  const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
10
10
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
11
11
  const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
12
+ const DEFAULT_MAX_NATIVE_TOOL_CALLS = 12;
13
+ const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 1;
12
14
 
13
15
  function nowMs() {
14
16
  return Date.now();
@@ -20,6 +22,36 @@ function normalizeTimeoutMs(value) {
20
22
  return Math.max(1000, Math.floor(parsed));
21
23
  }
22
24
 
25
+ function normalizePositiveInt(value, fallback) {
26
+ const parsed = Number.parseInt(String(value || ""), 10);
27
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
28
+ return Math.floor(parsed);
29
+ }
30
+
31
+ function resolveNativeToolBudget(env = process.env) {
32
+ return {
33
+ maxToolCalls: normalizePositiveInt(env.UFOO_UCODE_MAX_TOOL_CALLS, DEFAULT_MAX_NATIVE_TOOL_CALLS),
34
+ maxToolErrors: normalizePositiveInt(env.UFOO_UCODE_MAX_TOOL_ERRORS, DEFAULT_MAX_NATIVE_TOOL_ERRORS),
35
+ };
36
+ }
37
+
38
+ function enforceNativeToolBudget({
39
+ toolCallsExecuted = 0,
40
+ toolErrors = 0,
41
+ maxToolCalls = DEFAULT_MAX_NATIVE_TOOL_CALLS,
42
+ maxToolErrors = DEFAULT_MAX_NATIVE_TOOL_ERRORS,
43
+ lastTool = "",
44
+ lastError = "",
45
+ } = {}) {
46
+ if (toolCallsExecuted > maxToolCalls) {
47
+ throw new Error(`tool call budget exceeded (${maxToolCalls})`);
48
+ }
49
+ if (toolErrors >= maxToolErrors) {
50
+ const detail = [lastTool, lastError].filter(Boolean).join(": ");
51
+ throw new Error(`tool error budget exceeded (${maxToolErrors})${detail ? `: ${detail}` : ""}`);
52
+ }
53
+ }
54
+
23
55
  function createGuards({ signal = null, timeoutMs = 300000 } = {}) {
24
56
  const startedAt = nowMs();
25
57
  const budgetMs = normalizeTimeoutMs(timeoutMs);
@@ -907,6 +939,8 @@ async function runNativeLoopOpenAi({
907
939
  let aggregated = "";
908
940
  let streamed = false;
909
941
  let toolCallsExecuted = 0;
942
+ let toolErrors = 0;
943
+ const toolBudget = resolveNativeToolBudget();
910
944
 
911
945
  while (true) {
912
946
  guards.ensureActive();
@@ -991,6 +1025,17 @@ async function runNativeLoopOpenAi({
991
1025
  onToolEvent,
992
1026
  });
993
1027
  toolCallsExecuted += 1;
1028
+ if (!toolResult || toolResult.ok === false) {
1029
+ toolErrors += 1;
1030
+ }
1031
+ enforceNativeToolBudget({
1032
+ toolCallsExecuted,
1033
+ toolErrors,
1034
+ maxToolCalls: toolBudget.maxToolCalls,
1035
+ maxToolErrors: toolBudget.maxToolErrors,
1036
+ lastTool: toolCall.function.name,
1037
+ lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
1038
+ });
994
1039
  messages.push({
995
1040
  role: "tool",
996
1041
  tool_call_id: toolCall.id,
@@ -1034,6 +1079,8 @@ async function runNativeLoopAnthropic({
1034
1079
  let aggregated = "";
1035
1080
  let streamed = false;
1036
1081
  let toolCallsExecuted = 0;
1082
+ let toolErrors = 0;
1083
+ const toolBudget = resolveNativeToolBudget();
1037
1084
 
1038
1085
  while (true) {
1039
1086
  guards.ensureActive();
@@ -1109,6 +1156,17 @@ async function runNativeLoopAnthropic({
1109
1156
  onToolEvent,
1110
1157
  });
1111
1158
  toolCallsExecuted += 1;
1159
+ if (!toolResult || toolResult.ok === false) {
1160
+ toolErrors += 1;
1161
+ }
1162
+ enforceNativeToolBudget({
1163
+ toolCallsExecuted,
1164
+ toolErrors,
1165
+ maxToolCalls: toolBudget.maxToolCalls,
1166
+ maxToolErrors: toolBudget.maxToolErrors,
1167
+ lastTool: call.name,
1168
+ lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
1169
+ });
1112
1170
  toolResults.push({
1113
1171
  type: "tool_result",
1114
1172
  tool_use_id: String(call.id || ""),
@@ -147,8 +147,9 @@ async function runDecomposedTask({
147
147
  }
148
148
  }
149
149
 
150
- // Stop on error for critical steps
151
- if (!stepResult.ok && (step.id === "identify" || step.id === "locate")) {
150
+ // Stop on any step failure. A failed tool/provider call means the
151
+ // current plan is no longer reliable, and continuing can trigger loops.
152
+ if (!stepResult.ok) {
152
153
  return {
153
154
  ok: false,
154
155
  error: `Failed at ${step.name}: ${stepResult.error}`,
@@ -266,4 +267,4 @@ module.exports = {
266
267
  runDecomposedTask,
267
268
  compileSummary,
268
269
  createBusProgressReporter,
269
- };
270
+ };
package/src/code/tui.js CHANGED
@@ -993,6 +993,7 @@ function runUcodeTui({
993
993
  const ubusResult = await runUbusCommand(state, {
994
994
  workspaceRoot,
995
995
  subscriberId: autoBusSubscriberId,
996
+ signal: abortController.signal,
996
997
  onMessageReceived: (msg) => {
997
998
  // Display the incoming message immediately
998
999
  const { extractAgentNickname } = require("./agent");
@@ -1254,59 +1255,63 @@ function runUcodeTui({
1254
1255
  });
1255
1256
  let streamState = null;
1256
1257
  let renderedToolLogCount = 0;
1257
- const nlResult = await runNaturalLanguageTask(result.task, state, {
1258
- signal: abortController.signal,
1259
- onDelta: (delta) => {
1260
- const text = escapeStripper.write(String(delta || ""));
1261
- if (!text) return;
1258
+ let nlResult = null;
1259
+ try {
1260
+ nlResult = await runNaturalLanguageTask(result.task, state, {
1261
+ signal: abortController.signal,
1262
+ onDelta: (delta) => {
1263
+ const text = escapeStripper.write(String(delta || ""));
1264
+ if (!text) return;
1265
+ if (!streamState) {
1266
+ streamState = createNlStreamState();
1267
+ }
1268
+ appendNlStreamDelta(streamState, text);
1269
+ },
1270
+ onToolLog: (entry) => {
1271
+ renderedToolLogCount += 1;
1272
+ logToolHint(entry);
1273
+ },
1274
+ });
1275
+ const tail = escapeStripper.flush();
1276
+ if (tail) {
1262
1277
  if (!streamState) {
1263
1278
  streamState = createNlStreamState();
1264
1279
  }
1265
- appendNlStreamDelta(streamState, text);
1266
- },
1267
- onToolLog: (entry) => {
1268
- renderedToolLogCount += 1;
1269
- logToolHint(entry);
1270
- },
1271
- });
1272
- const tail = escapeStripper.flush();
1273
- if (tail) {
1274
- if (!streamState) {
1275
- streamState = createNlStreamState();
1280
+ appendNlStreamDelta(streamState, tail);
1276
1281
  }
1277
- appendNlStreamDelta(streamState, tail);
1278
- }
1279
- pendingTask = null;
1280
- updateStatus("", "none");
1281
- let finalStreamInfo = { lastChar: "" };
1282
- if (streamState) {
1283
- finalStreamInfo = finalizeNlStream(streamState);
1284
- }
1285
- if (Array.isArray(nlResult && nlResult.logs) && nlResult.logs.length > renderedToolLogCount) {
1286
- for (const entry of nlResult.logs.slice(renderedToolLogCount)) {
1287
- logToolHint(entry);
1282
+ let finalStreamInfo = { lastChar: "" };
1283
+ if (streamState) {
1284
+ finalStreamInfo = finalizeNlStream(streamState);
1288
1285
  }
1289
- }
1290
- const streamed = Boolean(nlResult && nlResult.streamed);
1291
- const hasVisibleStreamText = Boolean(
1292
- streamState
1293
- && typeof streamState.full === "string"
1294
- && /[^\s]/.test(streamState.full)
1295
- );
1296
- const streamLastChar = nlResult && typeof nlResult.streamLastChar === "string"
1297
- ? nlResult.streamLastChar.slice(-1)
1298
- : finalStreamInfo.lastChar;
1299
- if (streamed && hasVisibleStreamText && streamLastChar !== "\n") {
1300
- logBox.log("");
1301
- screen.render();
1302
- }
1303
- const shouldSkipSummary = Boolean(streamed && nlResult && nlResult.ok && hasVisibleStreamText);
1304
- if (!shouldSkipSummary) {
1305
- logText(formatNlResult(nlResult, false));
1306
- }
1307
- const persisted = persistSessionState(state);
1308
- if (!persisted || persisted.ok === false) {
1309
- logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
1286
+ if (Array.isArray(nlResult && nlResult.logs) && nlResult.logs.length > renderedToolLogCount) {
1287
+ for (const entry of nlResult.logs.slice(renderedToolLogCount)) {
1288
+ logToolHint(entry);
1289
+ }
1290
+ }
1291
+ const streamed = Boolean(nlResult && nlResult.streamed);
1292
+ const hasVisibleStreamText = Boolean(
1293
+ streamState
1294
+ && typeof streamState.full === "string"
1295
+ && /[^\s]/.test(streamState.full)
1296
+ );
1297
+ const streamLastChar = nlResult && typeof nlResult.streamLastChar === "string"
1298
+ ? nlResult.streamLastChar.slice(-1)
1299
+ : finalStreamInfo.lastChar;
1300
+ if (streamed && hasVisibleStreamText && streamLastChar !== "\n") {
1301
+ logBox.log("");
1302
+ screen.render();
1303
+ }
1304
+ const shouldSkipSummary = Boolean(streamed && nlResult && nlResult.ok && hasVisibleStreamText);
1305
+ if (!shouldSkipSummary) {
1306
+ logText(formatNlResult(nlResult, false));
1307
+ }
1308
+ const persisted = persistSessionState(state);
1309
+ if (!persisted || persisted.ok === false) {
1310
+ logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
1311
+ }
1312
+ } finally {
1313
+ pendingTask = null;
1314
+ updateStatus("", "none");
1310
1315
  }
1311
1316
  }
1312
1317
  };
package/src/daemon/ops.js CHANGED
@@ -44,6 +44,10 @@ function toTmuxBinary(agent = "") {
44
44
  return "";
45
45
  }
46
46
 
47
+ function resolveUfooRunnerPath() {
48
+ return path.resolve(__dirname, "../../bin/ufoo.js");
49
+ }
50
+
47
51
  function normalizeLaunchScope(value, fallback = "inplace") {
48
52
  const raw = String(value || "").trim().toLowerCase();
49
53
  if (!raw) return fallback;
@@ -546,7 +550,7 @@ async function spawnManagedHostAgent(
546
550
  if (hasPreRegisteredSubscriber) {
547
551
  // Group mode: use ufoo launcher for activity_state monitoring
548
552
  // This enables ReadyDetector and bootstrap to work correctly
549
- const ufooRunner = path.join(projectRoot, "bin", "ufoo.js");
553
+ const ufooRunner = resolveUfooRunnerPath();
550
554
  const launchCmd = `${shellEscape(process.execPath)} ${shellEscape(ufooRunner)} agent-pty-runner ${shellEscape(normalizedAgent)}${argText}`.trim();
551
555
  runCmd = titleCmd
552
556
  ? `cd ${shellEscape(projectRoot)} && ${titleCmd} && ${launchCmd}`
@@ -591,7 +595,7 @@ async function spawnManagedHostAgent(
591
595
  }
592
596
 
593
597
  async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "", processManager = null, extraEnv = {}) {
594
- const runner = path.join(projectRoot, "bin", "ufoo.js");
598
+ const runner = resolveUfooRunnerPath();
595
599
  const logDir = getUfooPaths(projectRoot).runDir;
596
600
  fs.mkdirSync(logDir, { recursive: true });
597
601
 
@@ -624,17 +628,18 @@ async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "",
624
628
  bus.loadBusData();
625
629
  process.env.UFOO_PARENT_PID = String(originalPid);
626
630
 
627
- // For ucode/ufoo agents, default nickname to "ucode" if not specified
628
- const defaultNickname = agentType === "ufoo-code" ? "ucode" : agent;
629
- const finalNickname = count > 1 ? `${nickname || defaultNickname}-${i + 1}` : (nickname || defaultNickname);
631
+ const requestedNickname = nickname
632
+ ? (count > 1 ? `${nickname}-${i + 1}` : nickname)
633
+ : "";
630
634
  const usePty = process.env.UFOO_INTERNAL_PTY !== "0";
631
635
  const launchMode = usePty ? "internal-pty" : "internal";
632
636
 
633
637
  // 传递 launch_mode 和 parent PID 到 join
634
- await bus.subscriberManager.join(sessionId, agentType, finalNickname, {
638
+ const joinResult = await bus.subscriberManager.join(sessionId, agentType, requestedNickname, {
635
639
  launchMode,
636
640
  parentPid: originalPid,
637
641
  });
642
+ const finalNickname = joinResult.nickname || requestedNickname || "";
638
643
  bus.saveBusData();
639
644
 
640
645
  const runnerCmd = usePty ? "agent-pty-runner" : "agent-runner";