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