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.
Files changed (70) hide show
  1. package/bin/ufoo.js +5 -3
  2. package/package.json +2 -4
  3. package/src/agent/claudeEventTranslator.js +267 -0
  4. package/src/agent/claudeOauthTokenReader.js +52 -0
  5. package/src/agent/claudeThreadProvider.js +343 -0
  6. package/src/agent/cliRunner.js +10 -16
  7. package/src/agent/codexEventTranslator.js +78 -0
  8. package/src/agent/codexThreadProvider.js +181 -0
  9. package/src/agent/controllerToolExecutor.js +233 -0
  10. package/src/agent/credentials/claude.js +324 -0
  11. package/src/agent/credentials/codex.js +203 -0
  12. package/src/agent/credentials/index.js +106 -0
  13. package/src/agent/internalRunner.js +348 -3
  14. package/src/agent/loopObservability.js +190 -0
  15. package/src/agent/loopRuntime.js +457 -0
  16. package/src/agent/ptyRunner.js +8 -7
  17. package/src/agent/ufooAgent.js +178 -120
  18. package/src/agent/upstreamTransport.js +464 -0
  19. package/src/bus/utils.js +3 -2
  20. package/src/chat/dashboardView.js +51 -1
  21. package/src/chat/index.js +3 -1
  22. package/src/config.js +53 -17
  23. package/src/controller/flags.js +160 -0
  24. package/src/controller/gateRouter.js +201 -0
  25. package/src/controller/routerFastPath.js +22 -0
  26. package/src/controller/shadowGuard.js +280 -0
  27. package/src/daemon/index.js +2 -3
  28. package/src/daemon/promptLoop.js +33 -224
  29. package/src/daemon/promptRequest.js +360 -5
  30. package/src/daemon/status.js +2 -0
  31. package/src/history/inputTimeline.js +9 -4
  32. package/src/memory/index.js +24 -0
  33. package/src/providerapi/redactor.js +87 -0
  34. package/src/providerapi/shadowDiff.js +174 -0
  35. package/src/report/store.js +4 -3
  36. package/src/tools/handlers/ackBus.js +26 -0
  37. package/src/tools/handlers/common.js +64 -0
  38. package/src/tools/handlers/dispatchMessage.js +81 -0
  39. package/src/tools/handlers/listAgents.js +14 -0
  40. package/src/tools/handlers/readBusSummary.js +34 -0
  41. package/src/tools/handlers/readOpenDecisions.js +26 -0
  42. package/src/tools/handlers/readProjectRegistry.js +20 -0
  43. package/src/tools/handlers/readPromptHistory.js +123 -0
  44. package/src/tools/handlers/tier2.js +134 -0
  45. package/src/tools/index.js +55 -0
  46. package/src/tools/registry.js +69 -0
  47. package/src/tools/schemaFixtures.js +415 -0
  48. package/src/tools/tier0/listAgents.js +14 -0
  49. package/src/tools/tier0/readBusSummary.js +14 -0
  50. package/src/tools/tier0/readOpenDecisions.js +14 -0
  51. package/src/tools/tier0/readProjectRegistry.js +14 -0
  52. package/src/tools/tier0/readPromptHistory.js +14 -0
  53. package/src/tools/tier1/ackBus.js +14 -0
  54. package/src/tools/tier1/dispatchMessage.js +14 -0
  55. package/src/tools/tier1/routeAgent.js +14 -0
  56. package/src/tools/tier2/closeAgent.js +14 -0
  57. package/src/tools/tier2/launchAgent.js +14 -0
  58. package/src/tools/tier2/manageCron.js +14 -0
  59. package/src/tools/tier2/renameAgent.js +14 -0
  60. package/src/tools/types.js +75 -0
  61. package/src/tools/unimplemented.js +13 -0
  62. package/src/ufoo/paths.js +4 -0
  63. package/bin/ufoo-assistant-agent.js +0 -5
  64. package/bin/ufoo-engine.js +0 -25
  65. package/src/assistant/agent.js +0 -261
  66. package/src/assistant/bridge.js +0 -178
  67. package/src/assistant/constants.js +0 -15
  68. package/src/assistant/engine.js +0 -252
  69. package/src/assistant/stdio.js +0 -58
  70. 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(projectRoot, agentType, provider, model, subscriber, nickname, evt, cliSessionState, busSender) {
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 runInternalRunner({ projectRoot, agentType = "codex" }) {
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
+ };