u-foo 1.9.8 → 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 (68) hide show
  1. package/package.json +2 -4
  2. package/src/agent/claudeEventTranslator.js +267 -0
  3. package/src/agent/claudeOauthTokenReader.js +52 -0
  4. package/src/agent/claudeThreadProvider.js +343 -0
  5. package/src/agent/cliRunner.js +4 -16
  6. package/src/agent/codexEventTranslator.js +78 -0
  7. package/src/agent/codexThreadProvider.js +181 -0
  8. package/src/agent/controllerToolExecutor.js +233 -0
  9. package/src/agent/credentials/claude.js +324 -0
  10. package/src/agent/credentials/codex.js +203 -0
  11. package/src/agent/credentials/index.js +106 -0
  12. package/src/agent/internalRunner.js +333 -2
  13. package/src/agent/loopObservability.js +190 -0
  14. package/src/agent/loopRuntime.js +457 -0
  15. package/src/agent/ufooAgent.js +178 -120
  16. package/src/agent/upstreamTransport.js +464 -0
  17. package/src/bus/utils.js +3 -2
  18. package/src/chat/dashboardView.js +51 -1
  19. package/src/chat/index.js +3 -1
  20. package/src/config.js +53 -17
  21. package/src/controller/flags.js +160 -0
  22. package/src/controller/gateRouter.js +201 -0
  23. package/src/controller/routerFastPath.js +22 -0
  24. package/src/controller/shadowGuard.js +280 -0
  25. package/src/daemon/index.js +2 -3
  26. package/src/daemon/promptLoop.js +33 -224
  27. package/src/daemon/promptRequest.js +360 -5
  28. package/src/daemon/status.js +2 -0
  29. package/src/history/inputTimeline.js +9 -4
  30. package/src/memory/index.js +24 -0
  31. package/src/providerapi/redactor.js +87 -0
  32. package/src/providerapi/shadowDiff.js +174 -0
  33. package/src/report/store.js +4 -3
  34. package/src/tools/handlers/ackBus.js +26 -0
  35. package/src/tools/handlers/common.js +64 -0
  36. package/src/tools/handlers/dispatchMessage.js +81 -0
  37. package/src/tools/handlers/listAgents.js +14 -0
  38. package/src/tools/handlers/readBusSummary.js +34 -0
  39. package/src/tools/handlers/readOpenDecisions.js +26 -0
  40. package/src/tools/handlers/readProjectRegistry.js +20 -0
  41. package/src/tools/handlers/readPromptHistory.js +123 -0
  42. package/src/tools/handlers/tier2.js +134 -0
  43. package/src/tools/index.js +55 -0
  44. package/src/tools/registry.js +69 -0
  45. package/src/tools/schemaFixtures.js +415 -0
  46. package/src/tools/tier0/listAgents.js +14 -0
  47. package/src/tools/tier0/readBusSummary.js +14 -0
  48. package/src/tools/tier0/readOpenDecisions.js +14 -0
  49. package/src/tools/tier0/readProjectRegistry.js +14 -0
  50. package/src/tools/tier0/readPromptHistory.js +14 -0
  51. package/src/tools/tier1/ackBus.js +14 -0
  52. package/src/tools/tier1/dispatchMessage.js +14 -0
  53. package/src/tools/tier1/routeAgent.js +14 -0
  54. package/src/tools/tier2/closeAgent.js +14 -0
  55. package/src/tools/tier2/launchAgent.js +14 -0
  56. package/src/tools/tier2/manageCron.js +14 -0
  57. package/src/tools/tier2/renameAgent.js +14 -0
  58. package/src/tools/types.js +75 -0
  59. package/src/tools/unimplemented.js +13 -0
  60. package/src/ufoo/paths.js +4 -0
  61. package/bin/ufoo-assistant-agent.js +0 -5
  62. package/bin/ufoo-engine.js +0 -25
  63. package/src/assistant/agent.js +0 -261
  64. package/src/assistant/bridge.js +0 -178
  65. package/src/assistant/constants.js +0 -15
  66. package/src/assistant/engine.js +0 -252
  67. package/src/assistant/stdio.js +0 -58
  68. 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
+ };