u-foo 1.9.7 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ufoo.js +5 -3
- 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 +10 -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 +348 -3
- package/src/agent/loopObservability.js +190 -0
- package/src/agent/loopRuntime.js +457 -0
- package/src/agent/ptyRunner.js +8 -7
- 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
|
@@ -6,11 +6,32 @@ const EventBus = require("../bus");
|
|
|
6
6
|
const { runCliAgent } = require("./cliRunner");
|
|
7
7
|
const { normalizeCliOutput } = require("./normalizeOutput");
|
|
8
8
|
const { createActivityStatePublisher } = require("./activityStatePublisher");
|
|
9
|
+
const { loadConfig, normalizeCodexInternalThreadMode } = require("../config");
|
|
10
|
+
const {
|
|
11
|
+
createCodexThreadProvider,
|
|
12
|
+
defaultCodexTransportStreamFactory,
|
|
13
|
+
} = require("./codexThreadProvider");
|
|
14
|
+
const {
|
|
15
|
+
createClaudeThreadProvider,
|
|
16
|
+
defaultClaudeTransportStreamFactory,
|
|
17
|
+
} = require("./claudeThreadProvider");
|
|
18
|
+
const { resolveClaudeUpstreamCredentials } = require("./credentials/claude");
|
|
19
|
+
const { buildUpstreamAuthFromCredential } = require("./credentials");
|
|
20
|
+
const { listToolsForCallerTier, CALLER_TIERS } = require("../tools");
|
|
21
|
+
const { redactToolCallPayload, redactSecrets } = require("../providerapi/redactor");
|
|
9
22
|
|
|
10
23
|
function sleep(ms) {
|
|
11
24
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
25
|
}
|
|
13
26
|
|
|
27
|
+
function normalizeWorkerThreadToolMode(value = "") {
|
|
28
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
29
|
+
if (raw === "worker-tier01" || raw === "tier01" || raw === "enabled" || raw === "1" || raw === "true") {
|
|
30
|
+
return "worker-tier01";
|
|
31
|
+
}
|
|
32
|
+
return "disabled";
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
function buildEnv(agentType, sessionId, publisher, nickname) {
|
|
15
36
|
const env = { ...process.env };
|
|
16
37
|
env.AI_BUS_PUBLISHER = publisher || env.AI_BUS_PUBLISHER || "";
|
|
@@ -63,6 +84,18 @@ function createBusSender(projectRoot, subscriber) {
|
|
|
63
84
|
return { enqueue, flush };
|
|
64
85
|
}
|
|
65
86
|
|
|
87
|
+
function shouldFallbackToLegacyThreadProvider(err, provider) {
|
|
88
|
+
if (provider !== "claude-cli" || !err || typeof err !== "object") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const code = String(err.code || "").trim().toUpperCase();
|
|
92
|
+
return (
|
|
93
|
+
code === "CLAUDE_AUTH_UNAVAILABLE"
|
|
94
|
+
|| code === "CLAUDE_OAUTH_SCHEMA_UNSUPPORTED"
|
|
95
|
+
|| code === "ANTHROPIC_SDK_UNAVAILABLE"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
66
99
|
function drainQueue(queueFile) {
|
|
67
100
|
if (!fs.existsSync(queueFile)) return [];
|
|
68
101
|
const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
|
|
@@ -96,7 +129,19 @@ function drainQueue(queueFile) {
|
|
|
96
129
|
return content.split(/\r?\n/).filter(Boolean);
|
|
97
130
|
}
|
|
98
131
|
|
|
99
|
-
async function handleEvent(
|
|
132
|
+
async function handleEvent(
|
|
133
|
+
projectRoot,
|
|
134
|
+
agentType,
|
|
135
|
+
provider,
|
|
136
|
+
model,
|
|
137
|
+
subscriber,
|
|
138
|
+
nickname,
|
|
139
|
+
evt,
|
|
140
|
+
cliSessionState,
|
|
141
|
+
busSender,
|
|
142
|
+
extraArgs = [],
|
|
143
|
+
threadRuntime = null
|
|
144
|
+
) {
|
|
100
145
|
if (!evt || !evt.data || !evt.data.message) return;
|
|
101
146
|
const prompt = evt.data.message;
|
|
102
147
|
const publisher = evt.publisher || "unknown";
|
|
@@ -111,6 +156,21 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
|
|
|
111
156
|
busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: text }));
|
|
112
157
|
};
|
|
113
158
|
|
|
159
|
+
if (threadRuntime && threadRuntime.enabled && threadRuntime.thread) {
|
|
160
|
+
const threadedResult = await handleThreadedEvent({
|
|
161
|
+
agentType,
|
|
162
|
+
provider,
|
|
163
|
+
publisher,
|
|
164
|
+
prompt,
|
|
165
|
+
busSender,
|
|
166
|
+
emitStreamDelta,
|
|
167
|
+
threadRuntime,
|
|
168
|
+
});
|
|
169
|
+
if (!threadedResult || !threadedResult.fallbackToLegacy) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
114
174
|
let res = await runCliAgent({
|
|
115
175
|
provider,
|
|
116
176
|
model,
|
|
@@ -118,6 +178,7 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
|
|
|
118
178
|
sessionId: cliSessionState.cliSessionId,
|
|
119
179
|
sandbox,
|
|
120
180
|
cwd: projectRoot,
|
|
181
|
+
extraArgs,
|
|
121
182
|
onStreamDelta: emitStreamDelta,
|
|
122
183
|
});
|
|
123
184
|
|
|
@@ -136,6 +197,7 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
|
|
|
136
197
|
sessionId: null, // Let runCliAgent generate new session
|
|
137
198
|
sandbox,
|
|
138
199
|
cwd: projectRoot,
|
|
200
|
+
extraArgs,
|
|
139
201
|
onStreamDelta: emitStreamDelta,
|
|
140
202
|
});
|
|
141
203
|
}
|
|
@@ -175,7 +237,271 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
|
|
|
175
237
|
await busSender.flush();
|
|
176
238
|
}
|
|
177
239
|
|
|
178
|
-
async function
|
|
240
|
+
async function handleThreadedEvent({
|
|
241
|
+
agentType,
|
|
242
|
+
provider,
|
|
243
|
+
publisher,
|
|
244
|
+
prompt,
|
|
245
|
+
busSender,
|
|
246
|
+
emitStreamDelta,
|
|
247
|
+
threadRuntime,
|
|
248
|
+
}) {
|
|
249
|
+
try {
|
|
250
|
+
for await (const event of threadRuntime.thread.runStreamed(prompt, {})) {
|
|
251
|
+
if (!event || typeof event !== "object") continue;
|
|
252
|
+
if (event.type === "text_delta" && event.delta) {
|
|
253
|
+
emitStreamDelta(event.delta);
|
|
254
|
+
} else if (event.type === "turn_failed") {
|
|
255
|
+
throw new Error(event.error || `thread turn failed for ${agentType}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
busSender.enqueue(
|
|
260
|
+
publisher,
|
|
261
|
+
JSON.stringify({ stream: true, done: true, reason: "complete" })
|
|
262
|
+
);
|
|
263
|
+
await busSender.flush();
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (shouldFallbackToLegacyThreadProvider(err, provider)) {
|
|
266
|
+
return { fallbackToLegacy: true };
|
|
267
|
+
}
|
|
268
|
+
if (threadRuntime && typeof threadRuntime.rebuildThread === "function") {
|
|
269
|
+
await threadRuntime.rebuildThread();
|
|
270
|
+
}
|
|
271
|
+
busSender.enqueue(
|
|
272
|
+
publisher,
|
|
273
|
+
JSON.stringify({
|
|
274
|
+
stream: true,
|
|
275
|
+
delta: `[internal:${agentType}] error: ${err && err.message ? err.message : "unknown error"}`,
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
busSender.enqueue(
|
|
279
|
+
publisher,
|
|
280
|
+
JSON.stringify({ stream: true, done: true, reason: "error" })
|
|
281
|
+
);
|
|
282
|
+
await busSender.flush();
|
|
283
|
+
return { fallbackToLegacy: false };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getCodexThreadMode(projectRoot) {
|
|
288
|
+
const envValue = process.env.UFOO_CODEX_INTERNAL_THREAD_MODE;
|
|
289
|
+
if (typeof envValue === "string" && envValue.trim()) {
|
|
290
|
+
return normalizeCodexInternalThreadMode(envValue);
|
|
291
|
+
}
|
|
292
|
+
return loadConfig(projectRoot).codexInternalThreadMode;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getWorkerThreadToolMode() {
|
|
296
|
+
return normalizeWorkerThreadToolMode(process.env.UFOO_CODEX_INTERNAL_THREAD_TOOLS);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function buildWorkerThreadToolRuntime({ projectRoot, subscriber, observer }) {
|
|
300
|
+
const mode = getWorkerThreadToolMode();
|
|
301
|
+
if (mode !== "worker-tier01") {
|
|
302
|
+
return {
|
|
303
|
+
enabled: false,
|
|
304
|
+
mode,
|
|
305
|
+
tools: [],
|
|
306
|
+
executeToolCall: null,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const eventBus = new EventBus(projectRoot);
|
|
311
|
+
const toolDefinitions = listToolsForCallerTier(CALLER_TIERS.WORKER);
|
|
312
|
+
const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool]));
|
|
313
|
+
const emitAudit = (phase, payload) => {
|
|
314
|
+
if (observer && typeof observer.onToolCall === "function") {
|
|
315
|
+
try { observer.onToolCall({ phase, payload }); } catch { /* ignore observer errors */ }
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
enabled: toolDefinitions.length > 0,
|
|
321
|
+
mode,
|
|
322
|
+
tools: toolDefinitions.map((tool) => ({
|
|
323
|
+
name: tool.name,
|
|
324
|
+
description: tool.description,
|
|
325
|
+
input_schema: tool.input_schema,
|
|
326
|
+
})),
|
|
327
|
+
// Keep a shared-handler executor ready for a future continuation-capable SDK path.
|
|
328
|
+
// The current Codex seam injects tool descriptors only and does not execute live
|
|
329
|
+
// tool calls inside the SDK stream yet.
|
|
330
|
+
async executeToolCall(toolCall = {}) {
|
|
331
|
+
const name = String(toolCall.name || "").trim();
|
|
332
|
+
const definition = toolsByName.get(name);
|
|
333
|
+
const rawArgs = toolCall.arguments || toolCall.args || {};
|
|
334
|
+
// Slice 1 (§10.7 tool pre-call): build a redacted audit envelope before
|
|
335
|
+
// the handler receives args, so observability consumers never see raw secrets.
|
|
336
|
+
const redactedPayload = redactToolCallPayload({
|
|
337
|
+
name,
|
|
338
|
+
args: rawArgs,
|
|
339
|
+
tool_call_id: toolCall.tool_call_id || toolCall.toolCallId || "",
|
|
340
|
+
caller_tier: CALLER_TIERS.WORKER,
|
|
341
|
+
});
|
|
342
|
+
emitAudit("pre_call", redactedPayload);
|
|
343
|
+
if (!definition) {
|
|
344
|
+
const errorResult = {
|
|
345
|
+
ok: false,
|
|
346
|
+
error: {
|
|
347
|
+
code: "unsupported_tool",
|
|
348
|
+
message: `worker tool is unavailable: ${name}`,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
emitAudit("post_call", { ...redactedPayload, result: errorResult });
|
|
352
|
+
return errorResult;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const result = await definition.handler({
|
|
357
|
+
caller_tier: CALLER_TIERS.WORKER,
|
|
358
|
+
projectRoot,
|
|
359
|
+
subscriber,
|
|
360
|
+
eventBus,
|
|
361
|
+
}, rawArgs);
|
|
362
|
+
const safeResult = redactSecrets(result);
|
|
363
|
+
emitAudit("post_call", { ...redactedPayload, result: safeResult });
|
|
364
|
+
return safeResult;
|
|
365
|
+
} catch (err) {
|
|
366
|
+
const errorResult = {
|
|
367
|
+
ok: false,
|
|
368
|
+
error: {
|
|
369
|
+
code: err && err.code ? err.code : "tool_execution_failed",
|
|
370
|
+
message: err && err.message ? err.message : String(err || "tool execution failed"),
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
const safeErrorResult = redactSecrets(errorResult);
|
|
374
|
+
emitAudit("post_call", { ...redactedPayload, result: safeErrorResult });
|
|
375
|
+
return safeErrorResult;
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function getClaudeThreadMode() {
|
|
382
|
+
const envValue = process.env.UFOO_CLAUDE_INTERNAL_THREAD_MODE;
|
|
383
|
+
const raw = String(envValue || "").trim().toLowerCase();
|
|
384
|
+
if (raw === "api") return "api";
|
|
385
|
+
return "legacy";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function buildClaudeAuthProvider(projectRoot) {
|
|
389
|
+
const config = loadConfig(projectRoot);
|
|
390
|
+
return async () => {
|
|
391
|
+
const credential = await resolveClaudeUpstreamCredentials({
|
|
392
|
+
profile: config.claudeOauthProfile,
|
|
393
|
+
tokenPath: config.claudeOauthTokenPath,
|
|
394
|
+
refreshWindowMs: Number(config.claudeOauthRefreshWindowSec || 300) * 1000,
|
|
395
|
+
});
|
|
396
|
+
return buildUpstreamAuthFromCredential(credential);
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], subscriber = "" }) {
|
|
401
|
+
const disabledRuntime = {
|
|
402
|
+
enabled: false,
|
|
403
|
+
thread: null,
|
|
404
|
+
toolRuntime: { enabled: false, mode: "disabled", tools: [] },
|
|
405
|
+
close: async () => {},
|
|
406
|
+
rebuildThread: async () => {},
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
if (provider === "codex-cli") {
|
|
410
|
+
if (getCodexThreadMode(projectRoot) !== "sdk") {
|
|
411
|
+
return disabledRuntime;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const toolRuntime = buildWorkerThreadToolRuntime({
|
|
416
|
+
projectRoot,
|
|
417
|
+
subscriber,
|
|
418
|
+
});
|
|
419
|
+
let providerInstance = createCodexThreadProvider({
|
|
420
|
+
model,
|
|
421
|
+
cwd: projectRoot,
|
|
422
|
+
extraArgs,
|
|
423
|
+
tools: toolRuntime.tools,
|
|
424
|
+
streamFactory: defaultCodexTransportStreamFactory,
|
|
425
|
+
});
|
|
426
|
+
let thread = providerInstance.startThread();
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
enabled: true,
|
|
430
|
+
toolRuntime,
|
|
431
|
+
get thread() {
|
|
432
|
+
return thread;
|
|
433
|
+
},
|
|
434
|
+
async rebuildThread() {
|
|
435
|
+
if (thread && typeof thread.close === "function") {
|
|
436
|
+
await thread.close();
|
|
437
|
+
}
|
|
438
|
+
providerInstance = createCodexThreadProvider({
|
|
439
|
+
model,
|
|
440
|
+
cwd: projectRoot,
|
|
441
|
+
extraArgs,
|
|
442
|
+
tools: toolRuntime.tools,
|
|
443
|
+
streamFactory: defaultCodexTransportStreamFactory,
|
|
444
|
+
});
|
|
445
|
+
thread = providerInstance.startThread();
|
|
446
|
+
},
|
|
447
|
+
async close() {
|
|
448
|
+
if (thread && typeof thread.close === "function") {
|
|
449
|
+
await thread.close();
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
} catch {
|
|
454
|
+
return disabledRuntime;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (provider === "claude-cli") {
|
|
459
|
+
if (getClaudeThreadMode() !== "api") {
|
|
460
|
+
return disabledRuntime;
|
|
461
|
+
}
|
|
462
|
+
if (typeof createClaudeThreadProvider !== "function" || typeof resolveClaudeUpstreamCredentials !== "function") {
|
|
463
|
+
return disabledRuntime;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
let providerInstance = createClaudeThreadProvider({
|
|
468
|
+
model,
|
|
469
|
+
authProvider: buildClaudeAuthProvider(projectRoot),
|
|
470
|
+
streamFactory: defaultClaudeTransportStreamFactory,
|
|
471
|
+
});
|
|
472
|
+
let thread = providerInstance.startThread();
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
enabled: true,
|
|
476
|
+
get thread() {
|
|
477
|
+
return thread;
|
|
478
|
+
},
|
|
479
|
+
async rebuildThread() {
|
|
480
|
+
if (thread && typeof thread.close === "function") {
|
|
481
|
+
await thread.close();
|
|
482
|
+
}
|
|
483
|
+
providerInstance = createClaudeThreadProvider({
|
|
484
|
+
model,
|
|
485
|
+
authProvider: buildClaudeAuthProvider(projectRoot),
|
|
486
|
+
streamFactory: defaultClaudeTransportStreamFactory,
|
|
487
|
+
});
|
|
488
|
+
thread = providerInstance.startThread();
|
|
489
|
+
},
|
|
490
|
+
async close() {
|
|
491
|
+
if (thread && typeof thread.close === "function") {
|
|
492
|
+
await thread.close();
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
} catch {
|
|
497
|
+
return disabledRuntime;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return disabledRuntime;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs = [] }) {
|
|
179
505
|
// Internal runner 必须由 daemon 启动,UFOO_SUBSCRIBER_ID 应该已经设置
|
|
180
506
|
const { subscriber, agentType: parsedAgentType, sessionId } = parseSubscriberId();
|
|
181
507
|
const nickname = process.env.UFOO_NICKNAME || "";
|
|
@@ -189,6 +515,13 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
|
189
515
|
const provider = normalizedAgentType === "codex" ? "codex-cli" : "claude-cli";
|
|
190
516
|
const model = process.env.UFOO_AGENT_MODEL || "";
|
|
191
517
|
const busSender = createBusSender(projectRoot, subscriber);
|
|
518
|
+
const threadRuntime = createThreadRuntime({
|
|
519
|
+
projectRoot,
|
|
520
|
+
provider,
|
|
521
|
+
model,
|
|
522
|
+
extraArgs,
|
|
523
|
+
subscriber,
|
|
524
|
+
});
|
|
192
525
|
|
|
193
526
|
// Session state management for CLI continuity
|
|
194
527
|
// Use stable path based on nickname (if exists) or agent type, NOT subscriber ID
|
|
@@ -280,7 +613,9 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
|
280
613
|
nickname,
|
|
281
614
|
evt,
|
|
282
615
|
cliSessionState,
|
|
283
|
-
busSender
|
|
616
|
+
busSender,
|
|
617
|
+
extraArgs,
|
|
618
|
+
threadRuntime
|
|
284
619
|
);
|
|
285
620
|
}
|
|
286
621
|
|
|
@@ -310,10 +645,20 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
|
310
645
|
// eslint-disable-next-line no-await-in-loop
|
|
311
646
|
await sleep(1000);
|
|
312
647
|
}
|
|
648
|
+
|
|
649
|
+
await threadRuntime.close();
|
|
313
650
|
}
|
|
314
651
|
|
|
315
652
|
module.exports = {
|
|
316
653
|
runInternalRunner,
|
|
317
654
|
createBusSender,
|
|
318
655
|
handleEvent,
|
|
656
|
+
createThreadRuntime,
|
|
657
|
+
getCodexThreadMode,
|
|
658
|
+
getWorkerThreadToolMode,
|
|
659
|
+
buildWorkerThreadToolRuntime,
|
|
660
|
+
normalizeWorkerThreadToolMode,
|
|
661
|
+
getClaudeThreadMode,
|
|
662
|
+
buildClaudeAuthProvider,
|
|
663
|
+
shouldFallbackToLegacyThreadProvider,
|
|
319
664
|
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
6
|
+
const { redactSecrets } = require("../providerapi/redactor");
|
|
7
|
+
|
|
8
|
+
const LOOP_EVENT_SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
function getLoopObservabilityPaths(projectRoot) {
|
|
11
|
+
const { agentDir } = getUfooPaths(projectRoot);
|
|
12
|
+
return {
|
|
13
|
+
eventsFile: path.join(agentDir, "ufoo-agent.loop-events.jsonl"),
|
|
14
|
+
auditFile: path.join(agentDir, "ufoo-agent.audit.jsonl"),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getShadowObservabilityPaths(projectRoot, now = new Date()) {
|
|
19
|
+
const { ufooDir } = getUfooPaths(projectRoot);
|
|
20
|
+
const stamp = now.toISOString().slice(0, 10);
|
|
21
|
+
return {
|
|
22
|
+
shadowDir: path.join(ufooDir, "shadow"),
|
|
23
|
+
diffFile: path.join(ufooDir, "shadow", `diff-${stamp}.jsonl`),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function appendJsonLine(file, payload) {
|
|
28
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
29
|
+
fs.appendFileSync(file, `${JSON.stringify(redactSecrets(payload))}\n`, "utf8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function appendShadowDiff(projectRoot, payload = {}, now = new Date()) {
|
|
33
|
+
if (!projectRoot) return null;
|
|
34
|
+
const { diffFile } = getShadowObservabilityPaths(projectRoot, now);
|
|
35
|
+
appendJsonLine(diffFile, {
|
|
36
|
+
schema_version: LOOP_EVENT_SCHEMA_VERSION,
|
|
37
|
+
ts: now.toISOString(),
|
|
38
|
+
shadow_only: true,
|
|
39
|
+
...payload,
|
|
40
|
+
});
|
|
41
|
+
return diffFile;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createLoopObserver({ projectRoot, enabled = true, defaults = {} } = {}) {
|
|
45
|
+
const paths = projectRoot ? getLoopObservabilityPaths(projectRoot) : null;
|
|
46
|
+
const baseDefaults = defaults && typeof defaults === "object" ? { ...defaults } : {};
|
|
47
|
+
|
|
48
|
+
function emit(event, payload = {}) {
|
|
49
|
+
if (!enabled || !paths) return;
|
|
50
|
+
appendJsonLine(paths.eventsFile, {
|
|
51
|
+
schema_version: LOOP_EVENT_SCHEMA_VERSION,
|
|
52
|
+
ts: new Date().toISOString(),
|
|
53
|
+
event: String(event || "").trim() || "unknown",
|
|
54
|
+
...baseDefaults,
|
|
55
|
+
...payload,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function audit(payload = {}) {
|
|
60
|
+
if (!enabled || !paths) return;
|
|
61
|
+
appendJsonLine(paths.auditFile, {
|
|
62
|
+
schema_version: LOOP_EVENT_SCHEMA_VERSION,
|
|
63
|
+
ts: new Date().toISOString(),
|
|
64
|
+
...baseDefaults,
|
|
65
|
+
...payload,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
emit,
|
|
71
|
+
audit,
|
|
72
|
+
paths,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readRecentLoopSummary(projectRoot, options = {}) {
|
|
77
|
+
if (!projectRoot) return null;
|
|
78
|
+
const { eventsFile } = getLoopObservabilityPaths(projectRoot);
|
|
79
|
+
if (!fs.existsSync(eventsFile)) return null;
|
|
80
|
+
|
|
81
|
+
const maxLines = Number.isFinite(options.maxLines) && options.maxLines > 0
|
|
82
|
+
? Math.floor(options.maxLines)
|
|
83
|
+
: 400;
|
|
84
|
+
|
|
85
|
+
let rows = [];
|
|
86
|
+
try {
|
|
87
|
+
rows = fs.readFileSync(eventsFile, "utf8")
|
|
88
|
+
.split(/\r?\n/)
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.slice(-maxLines)
|
|
91
|
+
.map((line) => {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(line);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (rows.length === 0) return null;
|
|
104
|
+
|
|
105
|
+
let startIndex = 0;
|
|
106
|
+
let endIndex = rows.length;
|
|
107
|
+
let terminalIndex = -1;
|
|
108
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
109
|
+
if (rows[i] && rows[i].event === "loop_terminal") {
|
|
110
|
+
terminalIndex = i;
|
|
111
|
+
endIndex = i + 1;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (terminalIndex >= 0) {
|
|
116
|
+
for (let i = terminalIndex - 1; i >= 0; i -= 1) {
|
|
117
|
+
if (rows[i] && rows[i].event === "loop_terminal") {
|
|
118
|
+
startIndex = i + 1;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const segment = rows.slice(startIndex, endIndex);
|
|
125
|
+
if (segment.length === 0) return null;
|
|
126
|
+
|
|
127
|
+
const toolCounts = new Map();
|
|
128
|
+
const summary = {
|
|
129
|
+
status: terminalIndex >= 0 ? "completed" : "in_progress",
|
|
130
|
+
event_count: segment.length,
|
|
131
|
+
model_calls: 0,
|
|
132
|
+
rounds: 0,
|
|
133
|
+
tool_calls: 0,
|
|
134
|
+
input_tokens: 0,
|
|
135
|
+
output_tokens: 0,
|
|
136
|
+
cache_read_tokens: 0,
|
|
137
|
+
cache_creation_tokens: 0,
|
|
138
|
+
total_tokens: 0,
|
|
139
|
+
total_latency_ms: 0,
|
|
140
|
+
first_token_ms: 0,
|
|
141
|
+
terminal_reason: "",
|
|
142
|
+
started_at: "",
|
|
143
|
+
ended_at: "",
|
|
144
|
+
tools: [],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
for (const row of segment) {
|
|
148
|
+
if (!summary.started_at && row.ts) summary.started_at = String(row.ts);
|
|
149
|
+
if (row.ts) summary.ended_at = String(row.ts);
|
|
150
|
+
if (row.event === "model_call") {
|
|
151
|
+
summary.model_calls += 1;
|
|
152
|
+
summary.rounds = Math.max(summary.rounds, Number(row.round) || 0);
|
|
153
|
+
summary.input_tokens += Number(row.input_tokens) || 0;
|
|
154
|
+
summary.output_tokens += Number(row.output_tokens) || 0;
|
|
155
|
+
summary.cache_read_tokens += Number(row.cache_read_tokens) || 0;
|
|
156
|
+
summary.cache_creation_tokens += Number(row.cache_creation_tokens) || 0;
|
|
157
|
+
summary.total_latency_ms += Number(row.latency_ms) || 0;
|
|
158
|
+
summary.first_token_ms += Number(row.first_token_ms) || 0;
|
|
159
|
+
} else if (row.event === "tool_call") {
|
|
160
|
+
summary.tool_calls += 1;
|
|
161
|
+
const name = String(row.tool_name || "").trim() || "unknown";
|
|
162
|
+
toolCounts.set(name, (toolCounts.get(name) || 0) + 1);
|
|
163
|
+
} else if (row.event === "loop_terminal") {
|
|
164
|
+
summary.terminal_reason = String(row.terminal_reason || "").trim();
|
|
165
|
+
if ((Number(row.rounds) || 0) > 0) summary.rounds = Number(row.rounds) || summary.rounds;
|
|
166
|
+
if ((Number(row.tool_calls) || 0) >= 0) summary.tool_calls = Number(row.tool_calls) || summary.tool_calls;
|
|
167
|
+
if ((Number(row.total_tokens) || 0) > 0) summary.total_tokens = Number(row.total_tokens) || 0;
|
|
168
|
+
if ((Number(row.total_latency_ms) || 0) > 0) summary.total_latency_ms = Number(row.total_latency_ms) || 0;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (summary.total_tokens <= 0) {
|
|
173
|
+
summary.total_tokens = summary.input_tokens + summary.output_tokens;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
summary.tools = Array.from(toolCounts.entries())
|
|
177
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
178
|
+
.map(([name, count]) => ({ name, count }));
|
|
179
|
+
|
|
180
|
+
return summary;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
LOOP_EVENT_SCHEMA_VERSION,
|
|
185
|
+
appendShadowDiff,
|
|
186
|
+
getLoopObservabilityPaths,
|
|
187
|
+
getShadowObservabilityPaths,
|
|
188
|
+
createLoopObserver,
|
|
189
|
+
readRecentLoopSummary,
|
|
190
|
+
};
|