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.
- 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/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,464 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require("crypto");
|
|
4
|
+
const { loadConfig } = require("../config");
|
|
5
|
+
const {
|
|
6
|
+
resolveRuntimeConfig,
|
|
7
|
+
resolveCompletionUrl,
|
|
8
|
+
resolveAnthropicMessagesUrl,
|
|
9
|
+
} = require("../code/nativeRunner");
|
|
10
|
+
const { resolveClaudeUpstreamCredentials } = require("./credentials/claude");
|
|
11
|
+
const { resolveCodexUpstreamCredentials } = require("./credentials/codex");
|
|
12
|
+
const { buildUpstreamAuthFromCredential } = require("./credentials");
|
|
13
|
+
|
|
14
|
+
function normalizeProvider(value = "") {
|
|
15
|
+
const text = String(value || "").trim().toLowerCase();
|
|
16
|
+
if (!text) return "ucode";
|
|
17
|
+
if (text === "codex-cli" || text === "codex-code" || text === "codex" || text === "openai") return "codex";
|
|
18
|
+
if (text === "claude-cli" || text === "claude-code" || text === "claude" || text === "anthropic") return "claude";
|
|
19
|
+
if (text === "ucode") return "ucode";
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function clipText(value = "", maxChars = 500) {
|
|
24
|
+
const text = String(value || "");
|
|
25
|
+
if (text.length <= maxChars) return text;
|
|
26
|
+
return `${text.slice(0, maxChars)}...[truncated]`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CODEX_DEFAULT_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
30
|
+
const CODEX_DEFAULT_USER_AGENT = "codex-tui/0.118.0 (Mac OS 26.3.1; arm64) iTerm.app/3.6.9 (codex-tui; 0.118.0)";
|
|
31
|
+
const CODEX_DEFAULT_ORIGINATOR = "codex-tui";
|
|
32
|
+
|
|
33
|
+
function buildOpenAiChatRequest({
|
|
34
|
+
model = "",
|
|
35
|
+
systemPrompt = "",
|
|
36
|
+
prompt = "",
|
|
37
|
+
messages = [],
|
|
38
|
+
tools = [],
|
|
39
|
+
temperature = 0,
|
|
40
|
+
} = {}) {
|
|
41
|
+
const requestMessages = Array.isArray(messages) ? messages.map((message) => ({ ...message })) : [];
|
|
42
|
+
if (!requestMessages.length) {
|
|
43
|
+
if (systemPrompt) requestMessages.push({ role: "system", content: String(systemPrompt) });
|
|
44
|
+
requestMessages.push({ role: "user", content: String(prompt || "") });
|
|
45
|
+
}
|
|
46
|
+
const request = {
|
|
47
|
+
model: String(model || "").trim(),
|
|
48
|
+
messages: requestMessages,
|
|
49
|
+
temperature,
|
|
50
|
+
};
|
|
51
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
52
|
+
request.tools = tools.slice();
|
|
53
|
+
}
|
|
54
|
+
return request;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildAnthropicMessagesRequest({
|
|
58
|
+
model = "",
|
|
59
|
+
systemPrompt = "",
|
|
60
|
+
prompt = "",
|
|
61
|
+
messages = [],
|
|
62
|
+
tools = [],
|
|
63
|
+
maxTokens = 4096,
|
|
64
|
+
temperature = 0,
|
|
65
|
+
} = {}) {
|
|
66
|
+
const requestMessages = Array.isArray(messages) ? messages.map((message) => ({ ...message })) : [];
|
|
67
|
+
if (!requestMessages.length) {
|
|
68
|
+
requestMessages.push({ role: "user", content: String(prompt || "") });
|
|
69
|
+
}
|
|
70
|
+
const request = {
|
|
71
|
+
model: String(model || "").trim(),
|
|
72
|
+
max_tokens: maxTokens,
|
|
73
|
+
messages: requestMessages,
|
|
74
|
+
temperature,
|
|
75
|
+
};
|
|
76
|
+
if (systemPrompt) request.system = systemPrompt;
|
|
77
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
78
|
+
request.tools = tools.slice();
|
|
79
|
+
}
|
|
80
|
+
return request;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeCodexContentPart(role = "user", text = "") {
|
|
84
|
+
return {
|
|
85
|
+
type: role === "assistant" ? "output_text" : "input_text",
|
|
86
|
+
text: String(text || ""),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeCodexMessage(role = "user", content = "") {
|
|
91
|
+
return {
|
|
92
|
+
type: "message",
|
|
93
|
+
role: role === "system" ? "developer" : role,
|
|
94
|
+
content: [normalizeCodexContentPart(role, content)],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildCodexResponsesRequest({
|
|
99
|
+
model = "",
|
|
100
|
+
systemPrompt = "",
|
|
101
|
+
prompt = "",
|
|
102
|
+
messages = [],
|
|
103
|
+
} = {}) {
|
|
104
|
+
const input = [];
|
|
105
|
+
const history = Array.isArray(messages) ? messages : [];
|
|
106
|
+
for (const message of history) {
|
|
107
|
+
if (!message || typeof message !== "object") continue;
|
|
108
|
+
const role = String(message.role || "user").trim() || "user";
|
|
109
|
+
let content = message.content;
|
|
110
|
+
if (Array.isArray(content)) {
|
|
111
|
+
content = content
|
|
112
|
+
.map((item) => {
|
|
113
|
+
if (!item || typeof item !== "object") return "";
|
|
114
|
+
return String(item.text || item.content || "");
|
|
115
|
+
})
|
|
116
|
+
.join("");
|
|
117
|
+
}
|
|
118
|
+
input.push(normalizeCodexMessage(role, content));
|
|
119
|
+
}
|
|
120
|
+
input.push(normalizeCodexMessage("user", prompt));
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
model: String(model || "").trim(),
|
|
124
|
+
instructions: String(systemPrompt || ""),
|
|
125
|
+
stream: true,
|
|
126
|
+
store: false,
|
|
127
|
+
parallel_tool_calls: true,
|
|
128
|
+
include: ["reasoning.encrypted_content"],
|
|
129
|
+
reasoning: {
|
|
130
|
+
effort: "medium",
|
|
131
|
+
summary: "auto",
|
|
132
|
+
},
|
|
133
|
+
input,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveCodexResponseOutput(response = {}) {
|
|
138
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
139
|
+
return output
|
|
140
|
+
.filter((item) => item && item.type === "message")
|
|
141
|
+
.flatMap((item) => (Array.isArray(item.content) ? item.content : []))
|
|
142
|
+
.filter((part) => part && part.type === "output_text")
|
|
143
|
+
.map((part) => String(part.text || ""))
|
|
144
|
+
.join("");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseCodexSsePayload(payload = "") {
|
|
148
|
+
const lines = String(payload || "").split(/\r?\n/);
|
|
149
|
+
const chunks = [];
|
|
150
|
+
let text = "";
|
|
151
|
+
let responseObject = null;
|
|
152
|
+
let usage = null;
|
|
153
|
+
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
const trimmed = line.trim();
|
|
156
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
157
|
+
const dataText = trimmed.slice(5).trim();
|
|
158
|
+
if (!dataText || dataText === "[DONE]") continue;
|
|
159
|
+
let event;
|
|
160
|
+
try {
|
|
161
|
+
event = JSON.parse(dataText);
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
chunks.push(event);
|
|
166
|
+
const type = String(event.type || "");
|
|
167
|
+
if (type === "response.output_text.delta" && typeof event.delta === "string") {
|
|
168
|
+
text += event.delta;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (type === "response.output_item.done" && event.item && event.item.type === "message" && !text) {
|
|
172
|
+
const content = Array.isArray(event.item.content) ? event.item.content : [];
|
|
173
|
+
text += content
|
|
174
|
+
.filter((part) => part && part.type === "output_text")
|
|
175
|
+
.map((part) => String(part.text || ""))
|
|
176
|
+
.join("");
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (type === "response.completed" && event.response && typeof event.response === "object") {
|
|
180
|
+
responseObject = event.response;
|
|
181
|
+
usage = event.response.usage && typeof event.response.usage === "object"
|
|
182
|
+
? event.response.usage
|
|
183
|
+
: null;
|
|
184
|
+
if (!text) {
|
|
185
|
+
text = resolveCodexResponseOutput(event.response);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
text: String(text || "").trim(),
|
|
192
|
+
response: responseObject,
|
|
193
|
+
usage,
|
|
194
|
+
events: chunks,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function resolveUpstreamRuntime({
|
|
199
|
+
projectRoot,
|
|
200
|
+
provider = "",
|
|
201
|
+
model = "",
|
|
202
|
+
env = process.env,
|
|
203
|
+
loadConfigImpl = loadConfig,
|
|
204
|
+
} = {}) {
|
|
205
|
+
const normalizedProvider = normalizeProvider(provider);
|
|
206
|
+
const config = loadConfigImpl(projectRoot);
|
|
207
|
+
|
|
208
|
+
if (normalizedProvider === "codex") {
|
|
209
|
+
const credential = await resolveCodexUpstreamCredentials({
|
|
210
|
+
authPath: config.codexAuthPath,
|
|
211
|
+
env,
|
|
212
|
+
});
|
|
213
|
+
const useCodexResponses = credential.credentialKind === "oauth" && Boolean(credential.accessToken);
|
|
214
|
+
const baseUrl = useCodexResponses
|
|
215
|
+
? String(env.UFOO_CODEX_BASE_URL || "").trim() || CODEX_DEFAULT_BASE_URL
|
|
216
|
+
: String(env.OPENAI_BASE_URL || "").trim() || "https://api.openai.com/v1";
|
|
217
|
+
const resolvedModel = String(model || config.routerModel || config.agentModel || "").trim();
|
|
218
|
+
return {
|
|
219
|
+
provider: "codex",
|
|
220
|
+
transport: useCodexResponses ? "codex-responses" : "openai-chat",
|
|
221
|
+
model: resolvedModel,
|
|
222
|
+
baseUrl,
|
|
223
|
+
credential,
|
|
224
|
+
auth: buildUpstreamAuthFromCredential(credential),
|
|
225
|
+
credentialSource: String(credential.source || ""),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (normalizedProvider === "claude") {
|
|
230
|
+
const credential = await resolveClaudeUpstreamCredentials({
|
|
231
|
+
profile: config.claudeOauthProfile,
|
|
232
|
+
tokenPath: config.claudeOauthTokenPath,
|
|
233
|
+
refreshWindowMs: Number(config.claudeOauthRefreshWindowSec || 300) * 1000,
|
|
234
|
+
env,
|
|
235
|
+
});
|
|
236
|
+
const baseUrl = String(env.ANTHROPIC_BASE_URL || "").trim() || "https://api.anthropic.com/v1";
|
|
237
|
+
const resolvedModel = String(model || config.routerModel || config.agentModel || "").trim();
|
|
238
|
+
return {
|
|
239
|
+
provider: "claude",
|
|
240
|
+
transport: "anthropic-messages",
|
|
241
|
+
model: resolvedModel,
|
|
242
|
+
baseUrl,
|
|
243
|
+
credential,
|
|
244
|
+
auth: buildUpstreamAuthFromCredential(credential),
|
|
245
|
+
credentialSource: String(credential.source || ""),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const runtime = resolveRuntimeConfig({
|
|
250
|
+
workspaceRoot: projectRoot,
|
|
251
|
+
provider: normalizedProvider === "ucode" ? "" : normalizedProvider,
|
|
252
|
+
model,
|
|
253
|
+
});
|
|
254
|
+
const auth = runtime.apiKey ? { apiKey: String(runtime.apiKey || "").trim() } : { headers: {} };
|
|
255
|
+
return {
|
|
256
|
+
provider: String(runtime.provider || normalizedProvider || "ucode"),
|
|
257
|
+
transport: String(runtime.transport || "openai-chat"),
|
|
258
|
+
model: String(runtime.model || "").trim(),
|
|
259
|
+
baseUrl: String(runtime.baseUrl || "").trim(),
|
|
260
|
+
credential: null,
|
|
261
|
+
auth,
|
|
262
|
+
credentialSource: runtime.apiKey ? "runtime-api-key" : "",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function sendUpstreamRequest({
|
|
267
|
+
runtime,
|
|
268
|
+
request,
|
|
269
|
+
timeoutMs = 120000,
|
|
270
|
+
fetchImpl = global.fetch,
|
|
271
|
+
} = {}) {
|
|
272
|
+
if (typeof fetchImpl !== "function") {
|
|
273
|
+
return { ok: false, error: "fetch is unavailable" };
|
|
274
|
+
}
|
|
275
|
+
const resolvedRuntime = runtime && typeof runtime === "object" ? runtime : {};
|
|
276
|
+
const requestModel = String((request && request.model) || resolvedRuntime.model || "").trim();
|
|
277
|
+
if (!requestModel) {
|
|
278
|
+
return { ok: false, error: `${resolvedRuntime.provider || "provider"} model is not configured` };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const isAnthropic = resolvedRuntime.transport === "anthropic-messages";
|
|
282
|
+
const isCodexResponses = resolvedRuntime.transport === "codex-responses";
|
|
283
|
+
const url = isAnthropic
|
|
284
|
+
? resolveAnthropicMessagesUrl(resolvedRuntime.baseUrl)
|
|
285
|
+
: isCodexResponses
|
|
286
|
+
? `${String(resolvedRuntime.baseUrl || "").replace(/\/+$/, "")}/responses`
|
|
287
|
+
: resolveCompletionUrl(resolvedRuntime.baseUrl);
|
|
288
|
+
|
|
289
|
+
if (!url) {
|
|
290
|
+
return { ok: false, error: `${resolvedRuntime.provider || "provider"} baseUrl is not configured` };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const headers = { "content-type": "application/json" };
|
|
294
|
+
if (resolvedRuntime.auth && resolvedRuntime.auth.headers && typeof resolvedRuntime.auth.headers === "object") {
|
|
295
|
+
Object.assign(headers, resolvedRuntime.auth.headers);
|
|
296
|
+
}
|
|
297
|
+
if (isAnthropic) {
|
|
298
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
299
|
+
if (resolvedRuntime.auth && resolvedRuntime.auth.apiKey) headers["x-api-key"] = resolvedRuntime.auth.apiKey;
|
|
300
|
+
} else if (isCodexResponses) {
|
|
301
|
+
headers.Accept = "text/event-stream";
|
|
302
|
+
headers.Connection = "Keep-Alive";
|
|
303
|
+
headers["User-Agent"] = CODEX_DEFAULT_USER_AGENT;
|
|
304
|
+
headers.Originator = CODEX_DEFAULT_ORIGINATOR;
|
|
305
|
+
if (resolvedRuntime.credential && resolvedRuntime.credential.accountId) {
|
|
306
|
+
headers["Chatgpt-Account-Id"] = String(resolvedRuntime.credential.accountId);
|
|
307
|
+
}
|
|
308
|
+
headers.Session_id = randomUUID();
|
|
309
|
+
} else {
|
|
310
|
+
if (resolvedRuntime.auth && resolvedRuntime.auth.apiKey) headers.authorization = `Bearer ${resolvedRuntime.auth.apiKey}`;
|
|
311
|
+
}
|
|
312
|
+
const body = JSON.stringify(request || {});
|
|
313
|
+
|
|
314
|
+
const controller = new AbortController();
|
|
315
|
+
const timer = setTimeout(() => {
|
|
316
|
+
try { controller.abort(); } catch {}
|
|
317
|
+
}, timeoutMs);
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const response = await fetchImpl(url, {
|
|
321
|
+
method: "POST",
|
|
322
|
+
headers,
|
|
323
|
+
body,
|
|
324
|
+
signal: controller.signal,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (!response.ok) {
|
|
328
|
+
const errBody = await response.text().catch(() => "");
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
error: `provider request failed (${response.status}): ${clipText(errBody)}`,
|
|
332
|
+
provider: resolvedRuntime.provider,
|
|
333
|
+
model: requestModel,
|
|
334
|
+
transport: resolvedRuntime.transport,
|
|
335
|
+
credentialSource: resolvedRuntime.credentialSource,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (isCodexResponses) {
|
|
340
|
+
const raw = await response.text();
|
|
341
|
+
const parsed = parseCodexSsePayload(raw);
|
|
342
|
+
return {
|
|
343
|
+
ok: true,
|
|
344
|
+
output: parsed.text,
|
|
345
|
+
provider: String(resolvedRuntime.provider || ""),
|
|
346
|
+
model: requestModel,
|
|
347
|
+
transport: resolvedRuntime.transport,
|
|
348
|
+
credentialSource: resolvedRuntime.credentialSource,
|
|
349
|
+
data: parsed.response,
|
|
350
|
+
usage: parsed.usage,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const data = await response.json();
|
|
355
|
+
let text = "";
|
|
356
|
+
if (isAnthropic) {
|
|
357
|
+
const content = Array.isArray(data.content) ? data.content : [];
|
|
358
|
+
text = content
|
|
359
|
+
.filter((item) => item && item.type === "text")
|
|
360
|
+
.map((item) => String(item.text || ""))
|
|
361
|
+
.join("");
|
|
362
|
+
} else {
|
|
363
|
+
const choice = data.choices && data.choices[0];
|
|
364
|
+
text = choice && choice.message && typeof choice.message.content === "string"
|
|
365
|
+
? choice.message.content
|
|
366
|
+
: "";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
ok: true,
|
|
371
|
+
output: text.trim(),
|
|
372
|
+
provider: String(resolvedRuntime.provider || ""),
|
|
373
|
+
model: requestModel,
|
|
374
|
+
transport: resolvedRuntime.transport,
|
|
375
|
+
credentialSource: resolvedRuntime.credentialSource,
|
|
376
|
+
data,
|
|
377
|
+
usage: data && typeof data === "object" && data.usage && typeof data.usage === "object"
|
|
378
|
+
? data.usage
|
|
379
|
+
: null,
|
|
380
|
+
};
|
|
381
|
+
} catch (err) {
|
|
382
|
+
const message = err && err.message ? err.message : "upstream request failed";
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
error: message,
|
|
386
|
+
provider: resolvedRuntime.provider,
|
|
387
|
+
model: requestModel,
|
|
388
|
+
transport: resolvedRuntime.transport,
|
|
389
|
+
credentialSource: resolvedRuntime.credentialSource,
|
|
390
|
+
};
|
|
391
|
+
} finally {
|
|
392
|
+
clearTimeout(timer);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function sendUpstreamPrompt({
|
|
397
|
+
projectRoot,
|
|
398
|
+
prompt,
|
|
399
|
+
systemPrompt,
|
|
400
|
+
provider = "",
|
|
401
|
+
model = "",
|
|
402
|
+
messages = [],
|
|
403
|
+
tools = [],
|
|
404
|
+
maxTokens = 4096,
|
|
405
|
+
temperature = 0,
|
|
406
|
+
timeoutMs = 120000,
|
|
407
|
+
fetchImpl = global.fetch,
|
|
408
|
+
env = process.env,
|
|
409
|
+
loadConfigImpl = loadConfig,
|
|
410
|
+
} = {}) {
|
|
411
|
+
const runtime = await resolveUpstreamRuntime({
|
|
412
|
+
projectRoot,
|
|
413
|
+
provider,
|
|
414
|
+
model,
|
|
415
|
+
env,
|
|
416
|
+
loadConfigImpl,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const requestModel = String(runtime.model || "").trim();
|
|
420
|
+
const request = runtime.transport === "anthropic-messages"
|
|
421
|
+
? buildAnthropicMessagesRequest({
|
|
422
|
+
model: requestModel,
|
|
423
|
+
systemPrompt,
|
|
424
|
+
prompt,
|
|
425
|
+
messages,
|
|
426
|
+
tools,
|
|
427
|
+
maxTokens,
|
|
428
|
+
temperature,
|
|
429
|
+
})
|
|
430
|
+
: runtime.transport === "codex-responses"
|
|
431
|
+
? buildCodexResponsesRequest({
|
|
432
|
+
model: requestModel,
|
|
433
|
+
systemPrompt,
|
|
434
|
+
prompt,
|
|
435
|
+
messages,
|
|
436
|
+
})
|
|
437
|
+
: buildOpenAiChatRequest({
|
|
438
|
+
model: requestModel,
|
|
439
|
+
systemPrompt,
|
|
440
|
+
prompt,
|
|
441
|
+
messages,
|
|
442
|
+
tools,
|
|
443
|
+
temperature,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
return sendUpstreamRequest({
|
|
447
|
+
runtime,
|
|
448
|
+
request,
|
|
449
|
+
timeoutMs,
|
|
450
|
+
fetchImpl,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = {
|
|
455
|
+
buildAnthropicMessagesRequest,
|
|
456
|
+
buildCodexResponsesRequest,
|
|
457
|
+
buildOpenAiChatRequest,
|
|
458
|
+
normalizeProvider,
|
|
459
|
+
parseCodexSsePayload,
|
|
460
|
+
resolveCodexResponseOutput,
|
|
461
|
+
resolveUpstreamRuntime,
|
|
462
|
+
sendUpstreamRequest,
|
|
463
|
+
sendUpstreamPrompt,
|
|
464
|
+
};
|
package/src/bus/utils.js
CHANGED
|
@@ -2,6 +2,7 @@ const crypto = require("crypto");
|
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { spawnSync } = require("child_process");
|
|
5
|
+
const { redactSecrets } = require("../providerapi/redactor");
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 获取当前 UTC 时间戳(ISO 8601 格式)
|
|
@@ -207,7 +208,7 @@ function readJSON(filePath, defaultValue = null) {
|
|
|
207
208
|
* 写入 JSON 文件(格式化)
|
|
208
209
|
*/
|
|
209
210
|
function writeJSON(filePath, data) {
|
|
210
|
-
const content = JSON.stringify(data, null, 2);
|
|
211
|
+
const content = JSON.stringify(redactSecrets(data), null, 2);
|
|
211
212
|
writeFileAtomic(filePath, content);
|
|
212
213
|
}
|
|
213
214
|
|
|
@@ -231,7 +232,7 @@ function readJSONL(filePath) {
|
|
|
231
232
|
* 追加一行到 JSONL 文件
|
|
232
233
|
*/
|
|
233
234
|
function appendJSONL(filePath, data) {
|
|
234
|
-
const line = JSON.stringify(data);
|
|
235
|
+
const line = JSON.stringify(redactSecrets(data));
|
|
235
236
|
appendFileAtomic(filePath, `${line}\n`);
|
|
236
237
|
}
|
|
237
238
|
|
|
@@ -36,6 +36,7 @@ function buildSummaryLine(options = {}) {
|
|
|
36
36
|
launchMode = "terminal",
|
|
37
37
|
agentProvider = "codex-cli",
|
|
38
38
|
cronTasks = [],
|
|
39
|
+
loopSummary = null,
|
|
39
40
|
} = options;
|
|
40
41
|
const agents = activeAgents.length > 0
|
|
41
42
|
? activeAgents.slice(0, 3)
|
|
@@ -43,10 +44,56 @@ function buildSummaryLine(options = {}) {
|
|
|
43
44
|
.join(", ")
|
|
44
45
|
+ (activeAgents.length > 3 ? ` +${activeAgents.length - 3}` : "")
|
|
45
46
|
: "none";
|
|
46
|
-
|
|
47
|
+
let line = `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`
|
|
47
48
|
+ ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`
|
|
48
49
|
+ ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`
|
|
49
50
|
+ ` {gray-fg}Cron:{/gray-fg} {cyan-fg}${Array.isArray(cronTasks) ? cronTasks.length : 0}{/cyan-fg}`;
|
|
51
|
+
const loopPart = formatLoopSummary(loopSummary);
|
|
52
|
+
if (loopPart) {
|
|
53
|
+
line += ` {gray-fg}Loop:{/gray-fg} {cyan-fg}${loopPart}{/cyan-fg}`;
|
|
54
|
+
}
|
|
55
|
+
return line;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatToolDistribution(items = []) {
|
|
59
|
+
const tools = Array.isArray(items) ? items : [];
|
|
60
|
+
if (tools.length === 0) return "";
|
|
61
|
+
const visible = tools.slice(0, 2).map((item) => `${item.name}x${item.count}`);
|
|
62
|
+
if (tools.length > 2) {
|
|
63
|
+
visible.push(`+${tools.length - 2}`);
|
|
64
|
+
}
|
|
65
|
+
return visible.join(",");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatLoopSummary(loopSummary) {
|
|
69
|
+
if (!loopSummary || typeof loopSummary !== "object") return "";
|
|
70
|
+
const rounds = Number(loopSummary.rounds) || 0;
|
|
71
|
+
const toolCalls = Number(loopSummary.tool_calls) || 0;
|
|
72
|
+
const totalTokens = Number(loopSummary.total_tokens) || 0;
|
|
73
|
+
const cacheReadTokens = Number(loopSummary.cache_read_tokens) || 0;
|
|
74
|
+
const cacheCreationTokens = Number(loopSummary.cache_creation_tokens) || 0;
|
|
75
|
+
const terminalReason = String(loopSummary.terminal_reason || "").trim();
|
|
76
|
+
const toolDistribution = formatToolDistribution(loopSummary.tools);
|
|
77
|
+
|
|
78
|
+
if (rounds <= 0 && toolCalls <= 0 && totalTokens <= 0 && !terminalReason && !toolDistribution) {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parts = [
|
|
83
|
+
`r${rounds}`,
|
|
84
|
+
`tc${toolCalls}`,
|
|
85
|
+
`tok${totalTokens}`,
|
|
86
|
+
];
|
|
87
|
+
if (cacheReadTokens > 0 || cacheCreationTokens > 0) {
|
|
88
|
+
parts.push(`cache${cacheReadTokens}/${cacheCreationTokens}`);
|
|
89
|
+
}
|
|
90
|
+
if (toolDistribution) {
|
|
91
|
+
parts.push(toolDistribution);
|
|
92
|
+
}
|
|
93
|
+
if (terminalReason) {
|
|
94
|
+
parts.push(terminalReason);
|
|
95
|
+
}
|
|
96
|
+
return parts.join(" ");
|
|
50
97
|
}
|
|
51
98
|
|
|
52
99
|
function buildProjectRailLine(options = {}) {
|
|
@@ -251,6 +298,7 @@ function computeDashboardContent(options = {}) {
|
|
|
251
298
|
selectedResumeIndex = 0,
|
|
252
299
|
selectedCronIndex = -1,
|
|
253
300
|
cronTasks = [],
|
|
301
|
+
loopSummary = null,
|
|
254
302
|
pendingReports = 0,
|
|
255
303
|
providerOptions = [],
|
|
256
304
|
resumeOptions = [],
|
|
@@ -290,6 +338,7 @@ function computeDashboardContent(options = {}) {
|
|
|
290
338
|
launchMode,
|
|
291
339
|
agentProvider,
|
|
292
340
|
cronTasks,
|
|
341
|
+
loopSummary,
|
|
293
342
|
});
|
|
294
343
|
return {
|
|
295
344
|
content: `${rail.line}\n ${line2}`,
|
|
@@ -352,6 +401,7 @@ function computeDashboardContent(options = {}) {
|
|
|
352
401
|
launchMode,
|
|
353
402
|
agentProvider,
|
|
354
403
|
cronTasks,
|
|
404
|
+
loopSummary,
|
|
355
405
|
});
|
|
356
406
|
|
|
357
407
|
return { content, windowStart: agentListWindowStart };
|
package/src/chat/index.js
CHANGED
|
@@ -133,6 +133,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
133
133
|
let agentProvider = config.agentProvider;
|
|
134
134
|
let autoResume = config.autoResume !== false;
|
|
135
135
|
let cronTasks = [];
|
|
136
|
+
let loopSummary = null;
|
|
136
137
|
|
|
137
138
|
// Dynamic input height settings.
|
|
138
139
|
// Layout: dashboard(N) + inputBottom(1) + content + inputTop(1) + status(1)
|
|
@@ -726,7 +727,6 @@ async function runChat(projectRoot, options = {}) {
|
|
|
726
727
|
const providerOptions = [
|
|
727
728
|
{ label: "codex", value: "codex-cli" },
|
|
728
729
|
{ label: "claude", value: "claude-cli" },
|
|
729
|
-
{ label: "ucode", value: "ucode" },
|
|
730
730
|
];
|
|
731
731
|
let selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
|
|
732
732
|
const resumeOptions = [
|
|
@@ -1219,6 +1219,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1219
1219
|
selectedResumeIndex,
|
|
1220
1220
|
selectedCronIndex,
|
|
1221
1221
|
cronTasks,
|
|
1222
|
+
loopSummary,
|
|
1222
1223
|
providerOptions,
|
|
1223
1224
|
resumeOptions,
|
|
1224
1225
|
dashHints: DASH_HINTS,
|
|
@@ -1265,6 +1266,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1265
1266
|
refreshProjectRuntimes();
|
|
1266
1267
|
}
|
|
1267
1268
|
cronTasks = Array.isArray(status?.cron?.tasks) ? status.cron.tasks : [];
|
|
1269
|
+
loopSummary = status && status.loop && typeof status.loop === "object" ? status.loop : null;
|
|
1268
1270
|
const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
1269
1271
|
let fallbackMap = null;
|
|
1270
1272
|
if (metaList.length === 0 && activeAgents.length > 0) {
|