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.
- package/package.json +2 -4
- package/src/agent/claudeEventTranslator.js +267 -0
- package/src/agent/claudeOauthTokenReader.js +52 -0
- package/src/agent/claudeThreadProvider.js +343 -0
- package/src/agent/cliRunner.js +4 -16
- package/src/agent/codexEventTranslator.js +78 -0
- package/src/agent/codexThreadProvider.js +181 -0
- package/src/agent/controllerToolExecutor.js +233 -0
- package/src/agent/credentials/claude.js +324 -0
- package/src/agent/credentials/codex.js +203 -0
- package/src/agent/credentials/index.js +106 -0
- package/src/agent/defaultBootstrap.js +128 -5
- package/src/agent/internalRunner.js +333 -2
- package/src/agent/loopObservability.js +190 -0
- package/src/agent/loopRuntime.js +457 -0
- package/src/agent/ufooAgent.js +178 -120
- package/src/agent/upstreamTransport.js +464 -0
- package/src/bus/utils.js +3 -2
- package/src/chat/dashboardView.js +51 -1
- package/src/chat/index.js +3 -1
- package/src/config.js +53 -17
- package/src/controller/flags.js +160 -0
- package/src/controller/gateRouter.js +201 -0
- package/src/controller/routerFastPath.js +22 -0
- package/src/controller/shadowGuard.js +280 -0
- package/src/daemon/index.js +2 -3
- package/src/daemon/promptLoop.js +33 -224
- package/src/daemon/promptRequest.js +360 -5
- package/src/daemon/status.js +2 -0
- package/src/history/inputTimeline.js +9 -4
- package/src/memory/index.js +24 -0
- package/src/providerapi/redactor.js +87 -0
- package/src/providerapi/shadowDiff.js +174 -0
- package/src/report/store.js +4 -3
- package/src/tools/handlers/ackBus.js +26 -0
- package/src/tools/handlers/common.js +64 -0
- package/src/tools/handlers/dispatchMessage.js +81 -0
- package/src/tools/handlers/listAgents.js +14 -0
- package/src/tools/handlers/readBusSummary.js +34 -0
- package/src/tools/handlers/readOpenDecisions.js +26 -0
- package/src/tools/handlers/readProjectRegistry.js +20 -0
- package/src/tools/handlers/readPromptHistory.js +123 -0
- package/src/tools/handlers/tier2.js +134 -0
- package/src/tools/index.js +55 -0
- package/src/tools/registry.js +69 -0
- package/src/tools/schemaFixtures.js +415 -0
- package/src/tools/tier0/listAgents.js +14 -0
- package/src/tools/tier0/readBusSummary.js +14 -0
- package/src/tools/tier0/readOpenDecisions.js +14 -0
- package/src/tools/tier0/readProjectRegistry.js +14 -0
- package/src/tools/tier0/readPromptHistory.js +14 -0
- package/src/tools/tier1/ackBus.js +14 -0
- package/src/tools/tier1/dispatchMessage.js +14 -0
- package/src/tools/tier1/routeAgent.js +14 -0
- package/src/tools/tier2/closeAgent.js +14 -0
- package/src/tools/tier2/launchAgent.js +14 -0
- package/src/tools/tier2/manageCron.js +14 -0
- package/src/tools/tier2/renameAgent.js +14 -0
- package/src/tools/types.js +75 -0
- package/src/tools/unimplemented.js +13 -0
- package/src/ufoo/paths.js +4 -0
- package/bin/ufoo-assistant-agent.js +0 -5
- package/bin/ufoo-engine.js +0 -25
- package/src/assistant/agent.js +0 -261
- package/src/assistant/bridge.js +0 -178
- package/src/assistant/constants.js +0 -15
- package/src/assistant/engine.js +0 -252
- package/src/assistant/stdio.js +0 -58
- 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
|
+
};
|
package/src/daemon/index.js
CHANGED
|
@@ -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
|
-
|
|
1015
|
+
runUfooRouteAgent,
|
|
1017
1016
|
dispatchMessages,
|
|
1018
1017
|
handleOps,
|
|
1019
1018
|
markPending: (target) => busBridge.markPending(target),
|
package/src/daemon/promptLoop.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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 =
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
return
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
332
|
-
extractAssistantCall,
|
|
333
|
-
normalizeAssistantCall,
|
|
334
|
-
buildAssistantReport,
|
|
143
|
+
stripAssistantCall,
|
|
335
144
|
};
|