u-foo 1.9.8 → 2.2.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 (69) 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/defaultBootstrap.js +128 -5
  13. package/src/agent/internalRunner.js +333 -2
  14. package/src/agent/loopObservability.js +190 -0
  15. package/src/agent/loopRuntime.js +457 -0
  16. package/src/agent/ufooAgent.js +178 -120
  17. package/src/agent/upstreamTransport.js +464 -0
  18. package/src/bus/utils.js +3 -2
  19. package/src/chat/dashboardView.js +51 -1
  20. package/src/chat/index.js +3 -1
  21. package/src/config.js +53 -17
  22. package/src/controller/flags.js +160 -0
  23. package/src/controller/gateRouter.js +201 -0
  24. package/src/controller/routerFastPath.js +22 -0
  25. package/src/controller/shadowGuard.js +280 -0
  26. package/src/daemon/index.js +2 -3
  27. package/src/daemon/promptLoop.js +33 -224
  28. package/src/daemon/promptRequest.js +360 -5
  29. package/src/daemon/status.js +2 -0
  30. package/src/history/inputTimeline.js +9 -4
  31. package/src/memory/index.js +24 -0
  32. package/src/providerapi/redactor.js +87 -0
  33. package/src/providerapi/shadowDiff.js +174 -0
  34. package/src/report/store.js +4 -3
  35. package/src/tools/handlers/ackBus.js +26 -0
  36. package/src/tools/handlers/common.js +64 -0
  37. package/src/tools/handlers/dispatchMessage.js +81 -0
  38. package/src/tools/handlers/listAgents.js +14 -0
  39. package/src/tools/handlers/readBusSummary.js +34 -0
  40. package/src/tools/handlers/readOpenDecisions.js +26 -0
  41. package/src/tools/handlers/readProjectRegistry.js +20 -0
  42. package/src/tools/handlers/readPromptHistory.js +123 -0
  43. package/src/tools/handlers/tier2.js +134 -0
  44. package/src/tools/index.js +55 -0
  45. package/src/tools/registry.js +69 -0
  46. package/src/tools/schemaFixtures.js +415 -0
  47. package/src/tools/tier0/listAgents.js +14 -0
  48. package/src/tools/tier0/readBusSummary.js +14 -0
  49. package/src/tools/tier0/readOpenDecisions.js +14 -0
  50. package/src/tools/tier0/readProjectRegistry.js +14 -0
  51. package/src/tools/tier0/readPromptHistory.js +14 -0
  52. package/src/tools/tier1/ackBus.js +14 -0
  53. package/src/tools/tier1/dispatchMessage.js +14 -0
  54. package/src/tools/tier1/routeAgent.js +14 -0
  55. package/src/tools/tier2/closeAgent.js +14 -0
  56. package/src/tools/tier2/launchAgent.js +14 -0
  57. package/src/tools/tier2/manageCron.js +14 -0
  58. package/src/tools/tier2/renameAgent.js +14 -0
  59. package/src/tools/types.js +75 -0
  60. package/src/tools/unimplemented.js +13 -0
  61. package/src/ufoo/paths.js +4 -0
  62. package/bin/ufoo-assistant-agent.js +0 -5
  63. package/bin/ufoo-engine.js +0 -25
  64. package/src/assistant/agent.js +0 -261
  65. package/src/assistant/bridge.js +0 -178
  66. package/src/assistant/constants.js +0 -15
  67. package/src/assistant/engine.js +0 -252
  68. package/src/assistant/stdio.js +0 -58
  69. package/src/assistant/ufooEngineCli.js +0 -312
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const { getUfooPaths } = require("../ufoo/paths");
8
+
9
+ const DEFAULT_SHADOW_SAMPLING_RATE = 0.1;
10
+ const DEFAULT_SHADOW_DAILY_INPUT_TOKEN_LIMIT = 50000;
11
+ const DEFAULT_SHADOW_DAILY_OUTPUT_TOKEN_LIMIT = 10000;
12
+
13
+ const TIER2_MOCK_TOOL_NAMES = Object.freeze([
14
+ "dispatch_message",
15
+ "launch_agent",
16
+ "rename_agent",
17
+ "close_agent",
18
+ "manage_cron",
19
+ ]);
20
+
21
+ function hashMessageId(messageId) {
22
+ const raw = String(messageId || "").trim();
23
+ if (!raw) return 0;
24
+ const digest = crypto.createHash("sha1").update(raw).digest();
25
+ return digest.readUInt32BE(0);
26
+ }
27
+
28
+ function shouldSampleShadow({ messageId = "", samplingRate = DEFAULT_SHADOW_SAMPLING_RATE } = {}) {
29
+ const rate = Number(samplingRate);
30
+ if (!Number.isFinite(rate) || rate <= 0) return { sampled: false, rate: 0 };
31
+ if (rate >= 1) return { sampled: true, rate: 1 };
32
+ const bucket = hashMessageId(messageId) / 0xffffffff;
33
+ return { sampled: bucket < rate, rate };
34
+ }
35
+
36
+ function getShadowBudgetPath(projectRoot, now = new Date()) {
37
+ const { ufooDir } = getUfooPaths(projectRoot);
38
+ const stamp = now.toISOString().slice(0, 10);
39
+ return path.join(ufooDir, "shadow", `budget-${stamp}.json`);
40
+ }
41
+
42
+ function readShadowBudgetState(file) {
43
+ try {
44
+ const raw = fs.readFileSync(file, "utf8");
45
+ const parsed = JSON.parse(raw);
46
+ return {
47
+ input_tokens_used: Number(parsed.input_tokens_used) || 0,
48
+ output_tokens_used: Number(parsed.output_tokens_used) || 0,
49
+ tripped: Boolean(parsed.tripped),
50
+ tripped_reason: String(parsed.tripped_reason || ""),
51
+ };
52
+ } catch {
53
+ return { input_tokens_used: 0, output_tokens_used: 0, tripped: false, tripped_reason: "" };
54
+ }
55
+ }
56
+
57
+ function writeShadowBudgetState(file, state) {
58
+ fs.mkdirSync(path.dirname(file), { recursive: true });
59
+ fs.writeFileSync(file, JSON.stringify(state, null, 2));
60
+ }
61
+
62
+ function createShadowBudgetBreaker({
63
+ projectRoot,
64
+ inputLimit = DEFAULT_SHADOW_DAILY_INPUT_TOKEN_LIMIT,
65
+ outputLimit = DEFAULT_SHADOW_DAILY_OUTPUT_TOKEN_LIMIT,
66
+ now = () => new Date(),
67
+ } = {}) {
68
+ const resolveFile = () => getShadowBudgetPath(projectRoot, now());
69
+
70
+ return {
71
+ check() {
72
+ const file = resolveFile();
73
+ const state = readShadowBudgetState(file);
74
+ if (state.tripped) {
75
+ return { allowed: false, reason: state.tripped_reason || "shadow_budget_tripped", state };
76
+ }
77
+ if (state.input_tokens_used >= inputLimit) {
78
+ return { allowed: false, reason: "shadow_input_budget_exceeded", state };
79
+ }
80
+ if (state.output_tokens_used >= outputLimit) {
81
+ return { allowed: false, reason: "shadow_output_budget_exceeded", state };
82
+ }
83
+ return { allowed: true, state };
84
+ },
85
+ record({ inputTokens = 0, outputTokens = 0, tripped = false, trippedReason = "" } = {}) {
86
+ const file = resolveFile();
87
+ const state = readShadowBudgetState(file);
88
+ state.input_tokens_used += Math.max(0, Number(inputTokens) || 0);
89
+ state.output_tokens_used += Math.max(0, Number(outputTokens) || 0);
90
+ if (tripped) {
91
+ state.tripped = true;
92
+ state.tripped_reason = String(trippedReason || "shadow_budget_tripped");
93
+ } else if (state.input_tokens_used >= inputLimit) {
94
+ state.tripped = true;
95
+ state.tripped_reason = "shadow_input_budget_exceeded";
96
+ } else if (state.output_tokens_used >= outputLimit) {
97
+ state.tripped = true;
98
+ state.tripped_reason = "shadow_output_budget_exceeded";
99
+ }
100
+ writeShadowBudgetState(file, state);
101
+ return state;
102
+ },
103
+ tripForProviderRateLimit(reason = "shadow_provider_rate_limit") {
104
+ const file = resolveFile();
105
+ const state = readShadowBudgetState(file);
106
+ state.tripped = true;
107
+ state.tripped_reason = reason;
108
+ writeShadowBudgetState(file, state);
109
+ return state;
110
+ },
111
+ };
112
+ }
113
+
114
+ function snapshotDirectory(rootDir) {
115
+ const entries = [];
116
+ if (!rootDir || !fs.existsSync(rootDir)) {
117
+ return { root: rootDir, entries };
118
+ }
119
+
120
+ const walk = (dir) => {
121
+ let list;
122
+ try {
123
+ list = fs.readdirSync(dir, { withFileTypes: true });
124
+ } catch {
125
+ return;
126
+ }
127
+ for (const entry of list) {
128
+ const abs = path.join(dir, entry.name);
129
+ if (entry.isDirectory()) {
130
+ walk(abs);
131
+ continue;
132
+ }
133
+ try {
134
+ const stat = fs.statSync(abs);
135
+ entries.push({
136
+ path: abs,
137
+ size: stat.size,
138
+ ino: stat.ino,
139
+ mtime_ms: Math.floor(stat.mtimeMs),
140
+ });
141
+ } catch {
142
+ // ignore transient errors
143
+ }
144
+ }
145
+ };
146
+
147
+ walk(rootDir);
148
+ entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
149
+ return { root: rootDir, entries };
150
+ }
151
+
152
+ function diffSnapshots(before = { entries: [] }, after = { entries: [] }) {
153
+ const diffs = [];
154
+ const beforeMap = new Map();
155
+ for (const item of before.entries || []) beforeMap.set(item.path, item);
156
+ const seen = new Set();
157
+
158
+ for (const item of after.entries || []) {
159
+ seen.add(item.path);
160
+ const prior = beforeMap.get(item.path);
161
+ if (!prior) {
162
+ diffs.push({ path: item.path, kind: "added" });
163
+ continue;
164
+ }
165
+ if (prior.size !== item.size || prior.ino !== item.ino) {
166
+ diffs.push({ path: item.path, kind: "modified" });
167
+ }
168
+ }
169
+
170
+ for (const item of before.entries || []) {
171
+ if (!seen.has(item.path)) {
172
+ diffs.push({ path: item.path, kind: "removed" });
173
+ }
174
+ }
175
+
176
+ return diffs;
177
+ }
178
+
179
+ function resolveBusQueueRoot(projectRoot) {
180
+ const { ufooDir } = getUfooPaths(projectRoot);
181
+ return path.join(ufooDir, "bus", "queues");
182
+ }
183
+
184
+ function resolveMemoryRoot(projectRoot) {
185
+ const { ufooDir } = getUfooPaths(projectRoot);
186
+ return path.join(ufooDir, "memory");
187
+ }
188
+
189
+ function buildTier2ToolExecutor(ctx = {}) {
190
+ const invocations = [];
191
+ return {
192
+ invocations,
193
+ async execute(toolCall = {}) {
194
+ const name = String(toolCall.name || "").trim();
195
+ invocations.push({ name, arguments: toolCall.arguments || {} });
196
+ if (TIER2_MOCK_TOOL_NAMES.includes(name)) {
197
+ return {
198
+ ok: true,
199
+ mocked: true,
200
+ shadow_only: true,
201
+ name,
202
+ result: { mocked: true, shadow_only: true },
203
+ };
204
+ }
205
+ if (typeof ctx.fallback === "function") {
206
+ return ctx.fallback(toolCall);
207
+ }
208
+ return {
209
+ ok: false,
210
+ error: { code: "shadow_only_tool_unavailable", message: `tool ${name} disabled in shadow mode` },
211
+ };
212
+ },
213
+ };
214
+ }
215
+
216
+ function createShadowGuard({ projectRoot, now = () => new Date() } = {}) {
217
+ const busQueueRoot = resolveBusQueueRoot(projectRoot);
218
+ const memoryRoot = resolveMemoryRoot(projectRoot);
219
+ const tier2 = buildTier2ToolExecutor();
220
+
221
+ function takeSnapshot() {
222
+ return {
223
+ ts: now().toISOString(),
224
+ bus_queue: snapshotDirectory(busQueueRoot),
225
+ memory: snapshotDirectory(memoryRoot),
226
+ };
227
+ }
228
+
229
+ function assertNoSideEffects(beforeSnapshot, afterSnapshot = takeSnapshot()) {
230
+ const busDiff = diffSnapshots(beforeSnapshot.bus_queue, afterSnapshot.bus_queue);
231
+ const memoryDiff = diffSnapshots(beforeSnapshot.memory, afterSnapshot.memory);
232
+ const violations = [
233
+ ...busDiff.map((d) => ({ scope: "bus_queue", ...d })),
234
+ ...memoryDiff.map((d) => ({ scope: "memory", ...d })),
235
+ ];
236
+ return {
237
+ ok: violations.length === 0,
238
+ violations,
239
+ };
240
+ }
241
+
242
+ function buildNoOpExecutors() {
243
+ return {
244
+ dispatchMessages: async () => undefined,
245
+ handleOps: async () => [],
246
+ ackBus: async () => 0,
247
+ markPending: () => {},
248
+ tier2ToolExecutor: tier2,
249
+ };
250
+ }
251
+
252
+ return {
253
+ projectRoot,
254
+ busQueueRoot,
255
+ memoryRoot,
256
+ takeSnapshot,
257
+ assertNoSideEffects,
258
+ buildNoOpExecutors,
259
+ tier2ToolInvocations: () => tier2.invocations.slice(),
260
+ };
261
+ }
262
+
263
+ module.exports = {
264
+ DEFAULT_SHADOW_DAILY_INPUT_TOKEN_LIMIT,
265
+ DEFAULT_SHADOW_DAILY_OUTPUT_TOKEN_LIMIT,
266
+ DEFAULT_SHADOW_SAMPLING_RATE,
267
+ TIER2_MOCK_TOOL_NAMES,
268
+ buildTier2ToolExecutor,
269
+ createShadowBudgetBreaker,
270
+ createShadowGuard,
271
+ diffSnapshots,
272
+ getShadowBudgetPath,
273
+ hashMessageId,
274
+ readShadowBudgetState,
275
+ resolveBusQueueRoot,
276
+ resolveMemoryRoot,
277
+ shouldSampleShadow,
278
+ snapshotDirectory,
279
+ writeShadowBudgetState,
280
+ };
@@ -2,7 +2,7 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const net = require("net");
4
4
  const { spawn, spawnSync } = require("child_process");
5
- const { runUfooAgent } = require("../agent/ufooAgent");
5
+ const { runUfooAgent, runUfooRouteAgent } = require("../agent/ufooAgent");
6
6
  const { launchAgent, closeAgent, getRecoverableAgents, resumeAgents } = require("./ops");
7
7
  const { buildStatus } = require("./status");
8
8
  const EventBus = require("../bus");
@@ -18,7 +18,6 @@ const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
18
18
  const { createDaemonCronController } = require("./cronOps");
19
19
  const { createGroupOrchestrator } = require("./groupOrchestrator");
20
20
  const { normalizeFormat, renderGroupDiagramFromTemplate, renderGroupDiagramFromRuntime } = require("../group/diagram");
21
- const { runAssistantTask } = require("../assistant/bridge");
22
21
  const { runPromptWithAssistant } = require("./promptLoop");
23
22
  const { handlePromptRequest } = require("./promptRequest");
24
23
  const { recordAgentReport } = require("./reporting");
@@ -1013,7 +1012,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1013
1012
  processManager,
1014
1013
  runPromptWithAssistant,
1015
1014
  runUfooAgent,
1016
- runAssistantTask,
1015
+ runUfooRouteAgent,
1017
1016
  dispatchMessages,
1018
1017
  handleOps,
1019
1018
  markPending: (target) => busBridge.markPending(target),
@@ -1,22 +1,3 @@
1
- function buildAssistantContinuationPrompt({
2
- originalPrompt,
3
- previousReply,
4
- reports,
5
- }) {
6
- const lines = [];
7
- lines.push(`User: ${originalPrompt}`);
8
- if (previousReply) {
9
- lines.push("");
10
- lines.push(`Your previous reply draft: ${previousReply}`);
11
- }
12
- lines.push("");
13
- lines.push("Assistant execution reports (JSON):");
14
- lines.push(JSON.stringify(reports, null, 2));
15
- lines.push("");
16
- lines.push("Using these reports, return the final JSON response.");
17
- return lines.join("\n");
18
- }
19
-
20
1
  function normalizePayload(payload) {
21
2
  if (!payload || typeof payload !== "object") {
22
3
  return { reply: "", dispatch: [], ops: [] };
@@ -29,88 +10,27 @@ function normalizePayload(payload) {
29
10
  };
30
11
  }
31
12
 
32
- function annotateAssistantFailureFallback(payload, assistantResult) {
33
- if (!payload || typeof payload !== "object") return payload;
34
- const dispatchCount = Array.isArray(payload.dispatch) ? payload.dispatch.length : 0;
35
- const opsCount = Array.isArray(payload.ops) ? payload.ops.length : 0;
36
- if (dispatchCount > 0 || opsCount > 0) return payload;
37
-
38
- const error = assistantResult && typeof assistantResult.error === "string" && assistantResult.error
39
- ? assistantResult.error
40
- : "assistant task failed";
41
- const note = `Assistant execution failed: ${error}. No action was applied.`;
42
- const reply = typeof payload.reply === "string" && payload.reply
43
- ? `${payload.reply}\n${note}`
44
- : note;
45
- return {
46
- ...payload,
47
- reply,
48
- };
49
- }
50
-
51
- function extractAssistantCall(payload) {
13
+ function stripAssistantCall(payload) {
52
14
  if (!payload || typeof payload !== "object") {
53
- return { assistantCall: null, ops: [] };
15
+ return { reply: "", dispatch: [], ops: [] };
54
16
  }
55
17
 
56
18
  const ops = Array.isArray(payload.ops) ? payload.ops : [];
57
- let assistantCall = payload.assistant_call || null;
58
19
  const normalOps = [];
59
20
 
60
21
  for (const op of ops) {
61
22
  if (op && op.action === "assistant_call") {
62
- if (!assistantCall) assistantCall = op;
63
23
  continue;
64
24
  }
65
25
  if (op) normalOps.push(op);
66
26
  }
67
27
 
68
- return { assistantCall, ops: normalOps };
69
- }
70
-
71
- function normalizeAssistantCall(call) {
72
- if (!call) return null;
73
- if (typeof call === "string") {
74
- return { task: call, kind: "mixed", context: "", expect: "" };
75
- }
76
- if (typeof call !== "object") return null;
77
- const task = typeof call.task === "string" ? call.task : "";
78
- if (!task) return null;
79
- return {
80
- task,
81
- kind: typeof call.kind === "string" ? call.kind : "mixed",
82
- context: typeof call.context === "string" ? call.context : "",
83
- expect: typeof call.expect === "string" ? call.expect : "",
84
- provider: typeof call.provider === "string" ? call.provider : "",
85
- model: typeof call.model === "string" ? call.model : "",
86
- timeoutMs: Number.isFinite(call.timeout_ms) ? call.timeout_ms : null,
28
+ const nextPayload = {
29
+ ...normalizePayload(payload),
30
+ ops: normalOps,
87
31
  };
88
- }
89
-
90
- function buildAssistantReport(call, result) {
91
- return {
92
- kind: call.kind,
93
- task: call.task,
94
- ok: result && result.ok !== false,
95
- summary: result && typeof result.summary === "string" ? result.summary : "",
96
- error: result && typeof result.error === "string" ? result.error : "",
97
- artifacts: result && Array.isArray(result.artifacts) ? result.artifacts : [],
98
- logs: result && Array.isArray(result.logs) ? result.logs : [],
99
- metrics: result && typeof result.metrics === "object" ? result.metrics : {},
100
- };
101
- }
102
-
103
- function createAssistantTaskId() {
104
- return `assistant-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
105
- }
106
-
107
- async function emitAssistantReport(reportTaskStatus, payload) {
108
- if (typeof reportTaskStatus !== "function") return;
109
- try {
110
- await reportTaskStatus(payload);
111
- } catch {
112
- // best effort: reporting must not break prompt flow
113
- }
32
+ delete nextPayload.assistant_call;
33
+ return nextPayload;
114
34
  }
115
35
 
116
36
  async function finalizePromptRun({
@@ -153,22 +73,24 @@ async function runPromptWithAssistant({
153
73
  model,
154
74
  processManager = null,
155
75
  runUfooAgent,
156
- runAssistantTask,
76
+ runPromptWithControllerLoop = null,
157
77
  dispatchMessages,
158
78
  handleOps,
159
79
  markPending = () => {},
160
- reportTaskStatus = () => {},
161
- maxAssistantLoops = 2,
162
- log = () => {},
163
80
  ufooAgentOptions = {},
164
81
  finalizeLocally = true,
82
+ loopRuntime = null,
165
83
  }) {
84
+ const agentOptions = {
85
+ ...(ufooAgentOptions && typeof ufooAgentOptions === "object" ? ufooAgentOptions : {}),
86
+ };
87
+
166
88
  const firstResult = await runUfooAgent({
167
89
  projectRoot,
168
90
  prompt: prompt || "",
169
91
  provider,
170
92
  model,
171
- ...ufooAgentOptions,
93
+ ...agentOptions,
172
94
  });
173
95
 
174
96
  if (!firstResult || !firstResult.ok) {
@@ -178,144 +100,35 @@ async function runPromptWithAssistant({
178
100
  };
179
101
  }
180
102
 
181
- const firstPayload = normalizePayload(firstResult.payload);
182
- const extractedFirst = extractAssistantCall(firstPayload);
183
- const assistantCall = normalizeAssistantCall(extractedFirst.assistantCall);
184
- const basePayload = {
185
- ...firstPayload,
186
- ops: extractedFirst.ops,
187
- };
188
- delete basePayload.assistant_call;
189
-
190
- if (!assistantCall || maxAssistantLoops < 1) {
191
- return finalizePromptRun({
103
+ const firstPayload = stripAssistantCall(firstResult.payload);
104
+ const shouldUpgradeToLoop = Boolean(
105
+ loopRuntime
106
+ && loopRuntime.enabled
107
+ && firstPayload
108
+ && firstPayload.upgrade_to_loop_router === true
109
+ && typeof runPromptWithControllerLoop === "function"
110
+ );
111
+
112
+ if (shouldUpgradeToLoop) {
113
+ return runPromptWithControllerLoop({
192
114
  projectRoot,
193
- payload: basePayload,
115
+ prompt: prompt || "",
116
+ provider,
117
+ model,
194
118
  processManager,
119
+ runUfooAgent,
195
120
  dispatchMessages,
196
121
  handleOps,
197
122
  markPending,
123
+ ufooAgentOptions,
198
124
  finalizeLocally,
125
+ loopRuntime,
199
126
  });
200
127
  }
201
128
 
202
- const assistantTaskId = createAssistantTaskId();
203
- await emitAssistantReport(reportTaskStatus, {
204
- phase: "start",
205
- source: "assistant",
206
- agent_id: "ufoo-assistant-agent",
207
- scope: "private",
208
- controller_id: "ufoo-agent",
209
- task_id: assistantTaskId,
210
- message: assistantCall.task,
211
- summary: "",
212
- error: "",
213
- ok: true,
214
- meta: {
215
- kind: assistantCall.kind,
216
- provider: assistantCall.provider || provider || "",
217
- model: assistantCall.model || model || "",
218
- },
219
- });
220
-
221
- let assistantResult;
222
- try {
223
- assistantResult = await runAssistantTask({
224
- projectRoot,
225
- provider: assistantCall.provider || "",
226
- fallbackProvider: provider,
227
- model: assistantCall.model || model,
228
- task: assistantCall.task,
229
- kind: assistantCall.kind,
230
- context: assistantCall.context,
231
- expect: assistantCall.expect,
232
- timeoutMs: assistantCall.timeoutMs || undefined,
233
- });
234
- } catch (err) {
235
- assistantResult = {
236
- ok: false,
237
- summary: "",
238
- artifacts: [],
239
- logs: [],
240
- error: err && err.message ? err.message : "assistant task failed",
241
- metrics: {},
242
- };
243
- }
244
-
245
- await emitAssistantReport(reportTaskStatus, {
246
- phase: assistantResult && assistantResult.ok === false ? "error" : "done",
247
- source: "assistant",
248
- agent_id: "ufoo-assistant-agent",
249
- scope: "private",
250
- controller_id: "ufoo-agent",
251
- task_id: assistantTaskId,
252
- message: assistantCall.task,
253
- summary: assistantResult && typeof assistantResult.summary === "string" ? assistantResult.summary : "",
254
- error: assistantResult && typeof assistantResult.error === "string" ? assistantResult.error : "",
255
- ok: assistantResult && assistantResult.ok !== false,
256
- meta: {
257
- kind: assistantCall.kind,
258
- provider: assistantCall.provider || provider || "",
259
- model: assistantCall.model || model || "",
260
- metrics: assistantResult && assistantResult.metrics && typeof assistantResult.metrics === "object"
261
- ? assistantResult.metrics
262
- : {},
263
- },
264
- });
265
-
266
- if (!assistantResult || assistantResult.ok === false) {
267
- log("assistant-loop fallback to round1 payload");
268
- const fallbackPayload = annotateAssistantFailureFallback(basePayload, assistantResult);
269
- return finalizePromptRun({
270
- projectRoot,
271
- payload: fallbackPayload,
272
- processManager,
273
- dispatchMessages,
274
- handleOps,
275
- markPending,
276
- });
277
- }
278
-
279
- const reports = [buildAssistantReport(assistantCall, assistantResult)];
280
- const continuationPrompt = buildAssistantContinuationPrompt({
281
- originalPrompt: prompt || "",
282
- previousReply: basePayload.reply || "",
283
- reports,
284
- });
285
-
286
- const secondResult = await runUfooAgent({
287
- projectRoot,
288
- prompt: continuationPrompt,
289
- provider,
290
- model,
291
- ...ufooAgentOptions,
292
- });
293
-
294
- if (!secondResult || !secondResult.ok) {
295
- log("assistant-loop fallback to round1 payload (round2 failed)");
296
- return finalizePromptRun({
297
- projectRoot,
298
- payload: basePayload,
299
- processManager,
300
- dispatchMessages,
301
- handleOps,
302
- markPending,
303
- finalizeLocally,
304
- });
305
- }
306
-
307
- const secondPayload = normalizePayload(secondResult.payload);
308
- const extractedSecond = extractAssistantCall(secondPayload);
309
- const finalPayload = {
310
- ...secondPayload,
311
- ops: extractedSecond.ops,
312
- assistant: { runs: reports },
313
- };
314
- delete finalPayload.assistant_call;
315
-
316
129
  return finalizePromptRun({
317
130
  projectRoot,
318
- payload: finalPayload,
131
+ payload: firstPayload,
319
132
  processManager,
320
133
  dispatchMessages,
321
134
  handleOps,
@@ -326,10 +139,6 @@ async function runPromptWithAssistant({
326
139
 
327
140
  module.exports = {
328
141
  runPromptWithAssistant,
329
- buildAssistantContinuationPrompt,
330
142
  normalizePayload,
331
- annotateAssistantFailureFallback,
332
- extractAssistantCall,
333
- normalizeAssistantCall,
334
- buildAssistantReport,
143
+ stripAssistantCall,
335
144
  };