u-foo 1.9.7 → 2.1.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 (70) hide show
  1. package/bin/ufoo.js +5 -3
  2. package/package.json +2 -4
  3. package/src/agent/claudeEventTranslator.js +267 -0
  4. package/src/agent/claudeOauthTokenReader.js +52 -0
  5. package/src/agent/claudeThreadProvider.js +343 -0
  6. package/src/agent/cliRunner.js +10 -16
  7. package/src/agent/codexEventTranslator.js +78 -0
  8. package/src/agent/codexThreadProvider.js +181 -0
  9. package/src/agent/controllerToolExecutor.js +233 -0
  10. package/src/agent/credentials/claude.js +324 -0
  11. package/src/agent/credentials/codex.js +203 -0
  12. package/src/agent/credentials/index.js +106 -0
  13. package/src/agent/internalRunner.js +348 -3
  14. package/src/agent/loopObservability.js +190 -0
  15. package/src/agent/loopRuntime.js +457 -0
  16. package/src/agent/ptyRunner.js +8 -7
  17. package/src/agent/ufooAgent.js +178 -120
  18. package/src/agent/upstreamTransport.js +464 -0
  19. package/src/bus/utils.js +3 -2
  20. package/src/chat/dashboardView.js +51 -1
  21. package/src/chat/index.js +3 -1
  22. package/src/config.js +53 -17
  23. package/src/controller/flags.js +160 -0
  24. package/src/controller/gateRouter.js +201 -0
  25. package/src/controller/routerFastPath.js +22 -0
  26. package/src/controller/shadowGuard.js +280 -0
  27. package/src/daemon/index.js +2 -3
  28. package/src/daemon/promptLoop.js +33 -224
  29. package/src/daemon/promptRequest.js +360 -5
  30. package/src/daemon/status.js +2 -0
  31. package/src/history/inputTimeline.js +9 -4
  32. package/src/memory/index.js +24 -0
  33. package/src/providerapi/redactor.js +87 -0
  34. package/src/providerapi/shadowDiff.js +174 -0
  35. package/src/report/store.js +4 -3
  36. package/src/tools/handlers/ackBus.js +26 -0
  37. package/src/tools/handlers/common.js +64 -0
  38. package/src/tools/handlers/dispatchMessage.js +81 -0
  39. package/src/tools/handlers/listAgents.js +14 -0
  40. package/src/tools/handlers/readBusSummary.js +34 -0
  41. package/src/tools/handlers/readOpenDecisions.js +26 -0
  42. package/src/tools/handlers/readProjectRegistry.js +20 -0
  43. package/src/tools/handlers/readPromptHistory.js +123 -0
  44. package/src/tools/handlers/tier2.js +134 -0
  45. package/src/tools/index.js +55 -0
  46. package/src/tools/registry.js +69 -0
  47. package/src/tools/schemaFixtures.js +415 -0
  48. package/src/tools/tier0/listAgents.js +14 -0
  49. package/src/tools/tier0/readBusSummary.js +14 -0
  50. package/src/tools/tier0/readOpenDecisions.js +14 -0
  51. package/src/tools/tier0/readProjectRegistry.js +14 -0
  52. package/src/tools/tier0/readPromptHistory.js +14 -0
  53. package/src/tools/tier1/ackBus.js +14 -0
  54. package/src/tools/tier1/dispatchMessage.js +14 -0
  55. package/src/tools/tier1/routeAgent.js +14 -0
  56. package/src/tools/tier2/closeAgent.js +14 -0
  57. package/src/tools/tier2/launchAgent.js +14 -0
  58. package/src/tools/tier2/manageCron.js +14 -0
  59. package/src/tools/tier2/renameAgent.js +14 -0
  60. package/src/tools/types.js +75 -0
  61. package/src/tools/unimplemented.js +13 -0
  62. package/src/ufoo/paths.js +4 -0
  63. package/bin/ufoo-assistant-agent.js +0 -5
  64. package/bin/ufoo-engine.js +0 -25
  65. package/src/assistant/agent.js +0 -261
  66. package/src/assistant/bridge.js +0 -178
  67. package/src/assistant/constants.js +0 -15
  68. package/src/assistant/engine.js +0 -252
  69. package/src/assistant/stdio.js +0 -58
  70. package/src/assistant/ufooEngineCli.js +0 -312
@@ -0,0 +1,457 @@
1
+ "use strict";
2
+
3
+ const { executeControllerTool } = require("./controllerToolExecutor");
4
+ const { createLoopObserver } = require("./loopObservability");
5
+
6
+ const DEFAULT_LOOP_OPTIONS = {
7
+ enabled: false,
8
+ maxRounds: 3,
9
+ maxToolCalls: 3,
10
+ maxToolErrors: 2,
11
+ maxPromptChars: 12000,
12
+ };
13
+
14
+ const TERMINAL_REASONS = Object.freeze({
15
+ FINAL_ANSWER: "final_answer",
16
+ BUDGET_EXCEEDED: "budget_exceeded",
17
+ TOOL_FAILURE: "tool_failure",
18
+ USER_CANCEL: "user_cancel",
19
+ PROVIDER_ERROR: "provider_error",
20
+ });
21
+
22
+ const FALLBACK_USED_VALUES = Object.freeze({
23
+ NONE: "none",
24
+ ASSISTANT_CALL: "assistant_call",
25
+ LEGACY_ROUTER: "legacy_router",
26
+ HELPER_AGENT: "helper_agent",
27
+ });
28
+
29
+ function normalizeTerminalReason(value) {
30
+ const raw = String(value || "").trim();
31
+ if (!raw) return TERMINAL_REASONS.FINAL_ANSWER;
32
+ if (Object.values(TERMINAL_REASONS).includes(raw)) return raw;
33
+ return TERMINAL_REASONS.FINAL_ANSWER;
34
+ }
35
+
36
+ function normalizeFallbackUsed(value) {
37
+ const raw = String(value || "").trim();
38
+ if (!raw) return FALLBACK_USED_VALUES.NONE;
39
+ if (Object.values(FALLBACK_USED_VALUES).includes(raw)) return raw;
40
+ return FALLBACK_USED_VALUES.NONE;
41
+ }
42
+
43
+ function toNonNegativeInt(value) {
44
+ const num = Number(value);
45
+ if (!Number.isFinite(num) || num < 0) return 0;
46
+ return Math.floor(num);
47
+ }
48
+
49
+ function extractModelMetrics(result) {
50
+ const meta = result && result.meta && typeof result.meta === "object" ? result.meta : null;
51
+ const payloadMeta = result && result.payload && typeof result.payload === "object"
52
+ && result.payload.meta && typeof result.payload.meta === "object"
53
+ ? result.payload.meta
54
+ : null;
55
+ const source = { ...(payloadMeta || {}), ...(meta || {}) };
56
+ return {
57
+ input_tokens: toNonNegativeInt(source.input_tokens),
58
+ output_tokens: toNonNegativeInt(source.output_tokens),
59
+ cache_read_tokens: toNonNegativeInt(source.cache_read_tokens),
60
+ cache_creation_tokens: toNonNegativeInt(source.cache_creation_tokens),
61
+ latency_ms: toNonNegativeInt(source.latency_ms),
62
+ first_token_ms: toNonNegativeInt(source.first_token_ms),
63
+ stop_reason: String(source.stop_reason || "").trim(),
64
+ };
65
+ }
66
+
67
+ function normalizePositiveInt(value, fallback) {
68
+ const num = Number.parseInt(value, 10);
69
+ if (Number.isFinite(num) && num > 0) return num;
70
+ return fallback;
71
+ }
72
+
73
+ function resolveLoopRuntimeOptions(env = process.env) {
74
+ const mode = String(env.UFOO_AGENT_RUNTIME_MODE || env.UFOO_AGENT_LOOP_MODE || "").trim().toLowerCase();
75
+ const enabled = mode === "loop" || String(env.UFOO_AGENT_ENABLE_LOOP || "").trim() === "1";
76
+ return {
77
+ enabled,
78
+ maxRounds: normalizePositiveInt(env.UFOO_AGENT_LOOP_MAX_ROUNDS, DEFAULT_LOOP_OPTIONS.maxRounds),
79
+ maxToolCalls: normalizePositiveInt(env.UFOO_AGENT_LOOP_MAX_TOOL_CALLS, DEFAULT_LOOP_OPTIONS.maxToolCalls),
80
+ maxToolErrors: normalizePositiveInt(env.UFOO_AGENT_LOOP_MAX_TOOL_ERRORS, DEFAULT_LOOP_OPTIONS.maxToolErrors),
81
+ maxPromptChars: normalizePositiveInt(env.UFOO_AGENT_LOOP_MAX_PROMPT_CHARS, DEFAULT_LOOP_OPTIONS.maxPromptChars),
82
+ };
83
+ }
84
+
85
+ function normalizePayload(payload) {
86
+ if (!payload || typeof payload !== "object") {
87
+ return { reply: "", dispatch: [], ops: [], done: true };
88
+ }
89
+ return {
90
+ ...payload,
91
+ reply: typeof payload.reply === "string" ? payload.reply : "",
92
+ dispatch: Array.isArray(payload.dispatch) ? payload.dispatch : [],
93
+ ops: Array.isArray(payload.ops) ? payload.ops : [],
94
+ done: payload.done !== false,
95
+ };
96
+ }
97
+
98
+ function buildLoopContinuationPrompt({
99
+ originalPrompt,
100
+ toolResults,
101
+ lastReply,
102
+ loopState,
103
+ }) {
104
+ const lines = [];
105
+ lines.push(String(originalPrompt || ""));
106
+ lines.push("");
107
+ if (lastReply) {
108
+ lines.push("Previous draft reply:");
109
+ lines.push(String(lastReply || ""));
110
+ lines.push("");
111
+ }
112
+ lines.push("Controller loop state (JSON):");
113
+ lines.push(JSON.stringify(loopState, null, 2));
114
+ lines.push("");
115
+ lines.push("Controller tool results so far (JSON):");
116
+ lines.push(JSON.stringify(toolResults, null, 2));
117
+ lines.push("");
118
+ lines.push("Use these results to decide the next tool_call or final JSON response.");
119
+ return lines.join("\n");
120
+ }
121
+
122
+ async function finalizeLoopRun({
123
+ projectRoot,
124
+ payload,
125
+ processManager,
126
+ dispatchMessages,
127
+ handleOps,
128
+ markPending,
129
+ finalizeLocally = true,
130
+ }) {
131
+ if (finalizeLocally === false) {
132
+ return { ok: true, payload, opsResults: [] };
133
+ }
134
+
135
+ const dispatch = Array.isArray(payload.dispatch) ? payload.dispatch : [];
136
+ const ops = Array.isArray(payload.ops) ? payload.ops : [];
137
+
138
+ for (const item of dispatch) {
139
+ if (item && item.target && item.target !== "broadcast" && typeof markPending === "function") {
140
+ markPending(item.target);
141
+ }
142
+ }
143
+
144
+ if (typeof dispatchMessages === "function") {
145
+ await dispatchMessages(projectRoot, dispatch);
146
+ }
147
+
148
+ const opsResults = typeof handleOps === "function"
149
+ ? await handleOps(projectRoot, ops, processManager || null)
150
+ : [];
151
+
152
+ return { ok: true, payload, opsResults };
153
+ }
154
+
155
+ function buildTerminalPayload(reason, lastPayload, rounds, toolCalls, toolErrors, totals = {}) {
156
+ const payload = normalizePayload(lastPayload);
157
+ const canonicalReason = normalizeTerminalReason(reason);
158
+ if (!payload.reply) {
159
+ payload.reply = `Controller loop stopped: ${canonicalReason}.`;
160
+ }
161
+ payload.dispatch = [];
162
+ payload.ops = [];
163
+ payload.loop = {
164
+ terminal_reason: canonicalReason,
165
+ rounds,
166
+ tool_calls: toolCalls,
167
+ tool_errors: toolErrors,
168
+ fallback_used: normalizeFallbackUsed(totals.fallback_used),
169
+ total_tokens: toNonNegativeInt(totals.total_tokens),
170
+ total_latency_ms: toNonNegativeInt(totals.total_latency_ms),
171
+ };
172
+ return payload;
173
+ }
174
+
175
+ async function runPromptWithControllerLoop({
176
+ projectRoot,
177
+ prompt,
178
+ provider,
179
+ model,
180
+ processManager = null,
181
+ runUfooAgent,
182
+ dispatchMessages,
183
+ handleOps,
184
+ ackBus,
185
+ markPending = () => {},
186
+ log = () => {},
187
+ ufooAgentOptions = {},
188
+ finalizeLocally = true,
189
+ loopRuntime = DEFAULT_LOOP_OPTIONS,
190
+ observer: providedObserver = null,
191
+ observabilityDefaults = {},
192
+ now = () => Date.now(),
193
+ isCancelled = null,
194
+ }) {
195
+ const options = { ...DEFAULT_LOOP_OPTIONS, ...(loopRuntime || {}) };
196
+ const observer = providedObserver || createLoopObserver({
197
+ projectRoot,
198
+ enabled: options.enabled !== false,
199
+ defaults: observabilityDefaults,
200
+ });
201
+
202
+ let currentPrompt = String(prompt || "");
203
+ let lastPayload = null;
204
+ let toolCalls = 0;
205
+ let toolErrors = 0;
206
+ let totalTokens = 0;
207
+ let totalLatencyMs = 0;
208
+ const toolResults = [];
209
+
210
+ const checkCancellation = () => {
211
+ if (typeof isCancelled !== "function") return false;
212
+ try {
213
+ return isCancelled() === true;
214
+ } catch {
215
+ return false;
216
+ }
217
+ };
218
+
219
+ const totals = () => ({
220
+ fallback_used: FALLBACK_USED_VALUES.NONE,
221
+ total_tokens: totalTokens,
222
+ total_latency_ms: totalLatencyMs,
223
+ });
224
+
225
+ const terminate = (reason, payloadBase, roundsCount) => {
226
+ const finalPayload = buildTerminalPayload(
227
+ reason,
228
+ payloadBase,
229
+ roundsCount,
230
+ toolCalls,
231
+ toolErrors,
232
+ totals()
233
+ );
234
+ observer.emit("loop_terminal", finalPayload.loop);
235
+ return finalPayload;
236
+ };
237
+
238
+ for (let round = 1; round <= options.maxRounds; round += 1) {
239
+ if (checkCancellation()) {
240
+ const payload = terminate(TERMINAL_REASONS.USER_CANCEL, lastPayload, round - 1);
241
+ return finalizeLoopRun({
242
+ projectRoot,
243
+ payload,
244
+ processManager,
245
+ dispatchMessages,
246
+ handleOps,
247
+ markPending,
248
+ finalizeLocally,
249
+ });
250
+ }
251
+
252
+ if (currentPrompt.length > options.maxPromptChars) {
253
+ const payload = terminate(TERMINAL_REASONS.BUDGET_EXCEEDED, lastPayload, round - 1);
254
+ return finalizeLoopRun({
255
+ projectRoot,
256
+ payload,
257
+ processManager,
258
+ dispatchMessages,
259
+ handleOps,
260
+ markPending,
261
+ finalizeLocally,
262
+ });
263
+ }
264
+
265
+ const roundStartedAt = now();
266
+ observer.emit("model_call_started", {
267
+ round,
268
+ provider: String(provider || ""),
269
+ model: String(model || ""),
270
+ prompt_chars: currentPrompt.length,
271
+ });
272
+
273
+ const result = await runUfooAgent({
274
+ projectRoot,
275
+ prompt: currentPrompt,
276
+ provider,
277
+ model,
278
+ ...ufooAgentOptions,
279
+ loopRuntime: {
280
+ enabled: true,
281
+ round,
282
+ maxRounds: options.maxRounds,
283
+ maxToolCalls: options.maxToolCalls,
284
+ remainingToolCalls: Math.max(options.maxToolCalls - toolCalls, 0),
285
+ },
286
+ });
287
+
288
+ const metrics = extractModelMetrics(result);
289
+ const modelLatency = metrics.latency_ms > 0 ? metrics.latency_ms : Math.max(0, now() - roundStartedAt);
290
+ totalTokens += metrics.input_tokens + metrics.output_tokens;
291
+ totalLatencyMs += modelLatency;
292
+
293
+ const toolCall = result && result.payload && typeof result.payload === "object"
294
+ && result.payload.tool_call && typeof result.payload.tool_call === "object"
295
+ ? result.payload.tool_call
296
+ : null;
297
+
298
+ observer.emit("model_call", {
299
+ round,
300
+ provider: String(provider || ""),
301
+ model: String(model || ""),
302
+ ok: result && result.ok === true,
303
+ input_tokens: metrics.input_tokens,
304
+ output_tokens: metrics.output_tokens,
305
+ cache_read_tokens: metrics.cache_read_tokens,
306
+ cache_creation_tokens: metrics.cache_creation_tokens,
307
+ latency_ms: modelLatency,
308
+ first_token_ms: metrics.first_token_ms,
309
+ tool_call_count: toolCall ? 1 : 0,
310
+ stop_reason: metrics.stop_reason,
311
+ error: result && result.ok === false ? String(result.error || "") : "",
312
+ });
313
+ observer.emit("model_call_finished", {
314
+ round,
315
+ ok: result && result.ok === true,
316
+ error: result && result.ok === false ? String(result.error || "") : "",
317
+ });
318
+
319
+ if (!result || result.ok !== true) {
320
+ const payload = terminate(TERMINAL_REASONS.PROVIDER_ERROR, lastPayload, round);
321
+ return {
322
+ ok: false,
323
+ error: result && result.error ? result.error : "ufoo-agent loop failed",
324
+ payload,
325
+ };
326
+ }
327
+
328
+ const payload = normalizePayload(result.payload);
329
+ lastPayload = payload;
330
+
331
+ if (!toolCall) {
332
+ const finalPayload = {
333
+ ...payload,
334
+ loop: {
335
+ terminal_reason: TERMINAL_REASONS.FINAL_ANSWER,
336
+ rounds: round,
337
+ tool_calls: toolCalls,
338
+ tool_errors: toolErrors,
339
+ fallback_used: FALLBACK_USED_VALUES.NONE,
340
+ total_tokens: totalTokens,
341
+ total_latency_ms: totalLatencyMs,
342
+ },
343
+ };
344
+ observer.emit("loop_terminal", finalPayload.loop);
345
+ return finalizeLoopRun({
346
+ projectRoot,
347
+ payload: finalPayload,
348
+ processManager,
349
+ dispatchMessages,
350
+ handleOps,
351
+ markPending,
352
+ finalizeLocally,
353
+ });
354
+ }
355
+
356
+ if (toolCalls >= options.maxToolCalls) {
357
+ const finalPayload = terminate(TERMINAL_REASONS.BUDGET_EXCEEDED, payload, round);
358
+ return finalizeLoopRun({
359
+ projectRoot,
360
+ payload: finalPayload,
361
+ processManager,
362
+ dispatchMessages,
363
+ handleOps,
364
+ markPending,
365
+ finalizeLocally,
366
+ });
367
+ }
368
+
369
+ toolCalls += 1;
370
+ const toolStartedAt = now();
371
+ const toolResult = await executeControllerTool({
372
+ projectRoot,
373
+ subscriber: "ufoo-agent",
374
+ processManager,
375
+ dispatchMessages,
376
+ handleOps,
377
+ ackBus,
378
+ markPending,
379
+ observer,
380
+ turnId: `loop-round-${round}`,
381
+ }, toolCall);
382
+ const toolDuration = Math.max(0, now() - toolStartedAt);
383
+
384
+ let toolResultSize = 0;
385
+ try {
386
+ toolResultSize = toolResult && toolResult.result !== undefined
387
+ ? JSON.stringify(toolResult.result).length
388
+ : 0;
389
+ } catch {
390
+ toolResultSize = 0;
391
+ }
392
+
393
+ observer.emit("tool_call", {
394
+ round,
395
+ tool_name: String(toolResult && toolResult.name ? toolResult.name : toolCall.name || ""),
396
+ tool_call_id: String(toolResult && toolResult.tool_call_id ? toolResult.tool_call_id : ""),
397
+ turn_id: toolResult && toolResult.turn_id ? String(toolResult.turn_id) : `loop-round-${round}`,
398
+ duration_ms: toolDuration,
399
+ result_size: toolResultSize,
400
+ retry_count: 0,
401
+ final_status: toolResult && toolResult.ok === true ? "ok" : "error",
402
+ });
403
+
404
+ if (!toolResult.ok) {
405
+ toolErrors += 1;
406
+ }
407
+ toolResults.push(toolResult);
408
+
409
+ if (toolErrors >= options.maxToolErrors) {
410
+ const finalPayload = terminate(TERMINAL_REASONS.TOOL_FAILURE, payload, round);
411
+ return finalizeLoopRun({
412
+ projectRoot,
413
+ payload: finalPayload,
414
+ processManager,
415
+ dispatchMessages,
416
+ handleOps,
417
+ markPending,
418
+ finalizeLocally,
419
+ });
420
+ }
421
+
422
+ currentPrompt = buildLoopContinuationPrompt({
423
+ originalPrompt: prompt,
424
+ toolResults,
425
+ lastReply: payload.reply,
426
+ loopState: {
427
+ round,
428
+ max_rounds: options.maxRounds,
429
+ tool_calls_used: toolCalls,
430
+ tool_calls_remaining: Math.max(options.maxToolCalls - toolCalls, 0),
431
+ tool_errors: toolErrors,
432
+ },
433
+ });
434
+ }
435
+
436
+ const payload = terminate(TERMINAL_REASONS.BUDGET_EXCEEDED, lastPayload, options.maxRounds);
437
+ return finalizeLoopRun({
438
+ projectRoot,
439
+ payload,
440
+ processManager,
441
+ dispatchMessages,
442
+ handleOps,
443
+ markPending,
444
+ finalizeLocally,
445
+ });
446
+ }
447
+
448
+ module.exports = {
449
+ DEFAULT_LOOP_OPTIONS,
450
+ FALLBACK_USED_VALUES,
451
+ TERMINAL_REASONS,
452
+ buildLoopContinuationPrompt,
453
+ normalizeFallbackUsed,
454
+ normalizeTerminalReason,
455
+ resolveLoopRuntimeOptions,
456
+ runPromptWithControllerLoop,
457
+ };
@@ -113,24 +113,25 @@ function buildPrompt(text, marker) {
113
113
  return `${text}\n\n请在完成后输出以下标记(单独一行):\n${marker}\n`;
114
114
  }
115
115
 
116
- function resolveCommand(agentType) {
116
+ function resolveCommand(agentType, extraArgs = []) {
117
117
  const normalizedAgent = String(agentType || "").trim().toLowerCase();
118
+ const extra = Array.isArray(extraArgs) ? extraArgs : [];
118
119
  const rawCmd = String(process.env.UFOO_PTY_CMD || "").trim();
119
120
  if (rawCmd) {
120
121
  const rawArgs = String(process.env.UFOO_PTY_ARGS || "").trim();
121
122
  const args = rawArgs ? rawArgs.split(/\s+/).filter(Boolean) : [];
122
- return { command: rawCmd, args };
123
+ return { command: rawCmd, args: [...args, ...extra] };
123
124
  }
124
125
  if (normalizedAgent === "claude" || normalizedAgent === "claude-code") {
125
- return { command: "claude", args: [] };
126
+ return { command: "claude", args: [...extra] };
126
127
  }
127
128
  if (normalizedAgent === "ufoo" || normalizedAgent === "ucode" || normalizedAgent === "ufoo-code") {
128
- return { command: "ucode", args: [] };
129
+ return { command: "ucode", args: [...extra] };
129
130
  }
130
- return { command: "codex", args: ["--no-alt-screen", "--sandbox", "workspace-write"] };
131
+ return { command: "codex", args: ["--no-alt-screen", "--sandbox", "workspace-write", ...extra] };
131
132
  }
132
133
 
133
- async function runPtyRunner({ projectRoot, agentType = "codex" }) {
134
+ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }) {
134
135
  let pty;
135
136
  try {
136
137
  // eslint-disable-next-line global-require
@@ -156,7 +157,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
156
157
  const logFile = path.join(runDir, "pty-runner.log");
157
158
  const injectSockPath = path.join(queueDir, "inject.sock");
158
159
 
159
- const { command, args } = resolveCommand(agentType);
160
+ const { command, args } = resolveCommand(agentType, extraArgs);
160
161
  const env = {
161
162
  ...process.env,
162
163
  UFOO_LAUNCH_MODE: "internal-pty",