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
|
@@ -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()}`;
|
|
@@ -106,7 +139,8 @@ async function handleEvent(
|
|
|
106
139
|
evt,
|
|
107
140
|
cliSessionState,
|
|
108
141
|
busSender,
|
|
109
|
-
extraArgs = []
|
|
142
|
+
extraArgs = [],
|
|
143
|
+
threadRuntime = null
|
|
110
144
|
) {
|
|
111
145
|
if (!evt || !evt.data || !evt.data.message) return;
|
|
112
146
|
const prompt = evt.data.message;
|
|
@@ -122,6 +156,21 @@ async function handleEvent(
|
|
|
122
156
|
busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: text }));
|
|
123
157
|
};
|
|
124
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
|
+
|
|
125
174
|
let res = await runCliAgent({
|
|
126
175
|
provider,
|
|
127
176
|
model,
|
|
@@ -188,6 +237,270 @@ async function handleEvent(
|
|
|
188
237
|
await busSender.flush();
|
|
189
238
|
}
|
|
190
239
|
|
|
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
|
+
|
|
191
504
|
async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs = [] }) {
|
|
192
505
|
// Internal runner 必须由 daemon 启动,UFOO_SUBSCRIBER_ID 应该已经设置
|
|
193
506
|
const { subscriber, agentType: parsedAgentType, sessionId } = parseSubscriberId();
|
|
@@ -202,6 +515,13 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
202
515
|
const provider = normalizedAgentType === "codex" ? "codex-cli" : "claude-cli";
|
|
203
516
|
const model = process.env.UFOO_AGENT_MODEL || "";
|
|
204
517
|
const busSender = createBusSender(projectRoot, subscriber);
|
|
518
|
+
const threadRuntime = createThreadRuntime({
|
|
519
|
+
projectRoot,
|
|
520
|
+
provider,
|
|
521
|
+
model,
|
|
522
|
+
extraArgs,
|
|
523
|
+
subscriber,
|
|
524
|
+
});
|
|
205
525
|
|
|
206
526
|
// Session state management for CLI continuity
|
|
207
527
|
// Use stable path based on nickname (if exists) or agent type, NOT subscriber ID
|
|
@@ -294,7 +614,8 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
294
614
|
evt,
|
|
295
615
|
cliSessionState,
|
|
296
616
|
busSender,
|
|
297
|
-
extraArgs
|
|
617
|
+
extraArgs,
|
|
618
|
+
threadRuntime
|
|
298
619
|
);
|
|
299
620
|
}
|
|
300
621
|
|
|
@@ -324,10 +645,20 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
324
645
|
// eslint-disable-next-line no-await-in-loop
|
|
325
646
|
await sleep(1000);
|
|
326
647
|
}
|
|
648
|
+
|
|
649
|
+
await threadRuntime.close();
|
|
327
650
|
}
|
|
328
651
|
|
|
329
652
|
module.exports = {
|
|
330
653
|
runInternalRunner,
|
|
331
654
|
createBusSender,
|
|
332
655
|
handleEvent,
|
|
656
|
+
createThreadRuntime,
|
|
657
|
+
getCodexThreadMode,
|
|
658
|
+
getWorkerThreadToolMode,
|
|
659
|
+
buildWorkerThreadToolRuntime,
|
|
660
|
+
normalizeWorkerThreadToolMode,
|
|
661
|
+
getClaudeThreadMode,
|
|
662
|
+
buildClaudeAuthProvider,
|
|
663
|
+
shouldFallbackToLegacyThreadProvider,
|
|
333
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
|
+
};
|