autotel-agents 0.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/dist/index.cjs ADDED
@@ -0,0 +1,702 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/attrs.ts
4
+ function str(attrs, ...keys) {
5
+ for (const key of keys) {
6
+ const value = attrs[key];
7
+ if (typeof value === "string" && value.length > 0) return value;
8
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
9
+ }
10
+ }
11
+ function num(attrs, ...keys) {
12
+ for (const key of keys) {
13
+ const value = attrs[key];
14
+ if (typeof value === "number" && Number.isFinite(value)) return value;
15
+ if (typeof value === "string") {
16
+ const parsed = Number(value);
17
+ if (Number.isFinite(parsed) && value.trim() !== "") return parsed;
18
+ }
19
+ }
20
+ }
21
+ function bool(attrs, ...keys) {
22
+ for (const key of keys) {
23
+ const value = attrs[key];
24
+ if (typeof value === "boolean") return value;
25
+ if (typeof value === "string") {
26
+ if (value === "true") return true;
27
+ if (value === "false") return false;
28
+ }
29
+ }
30
+ }
31
+
32
+ //#endregion
33
+ //#region src/cost.ts
34
+ /**
35
+ * Fallback cost estimation. Reported cost (`cost_usd` on `api_request`) always
36
+ * wins — Claude Code computes it cache-accurately. This table is ONLY used when
37
+ * an agent reports tokens but not cost (e.g. a future agent, or a misconfigured
38
+ * run). Estimated values are badged `estimated` in the UI.
39
+ *
40
+ * Prices are USD per 1,000,000 tokens. Matched by substring so model ids like
41
+ * `claude-sonnet-4-6` or `claude-3-5-sonnet-20241022` resolve to a family rate.
42
+ * Keep deliberately small — this is a safety net, not a billing source.
43
+ */
44
+ const PRICES = [
45
+ [
46
+ "claude-opus-4",
47
+ 15,
48
+ 75
49
+ ],
50
+ [
51
+ "claude-sonnet-4",
52
+ 3,
53
+ 15
54
+ ],
55
+ [
56
+ "claude-haiku-4",
57
+ .8,
58
+ 4
59
+ ],
60
+ [
61
+ "claude-3-5-sonnet",
62
+ 3,
63
+ 15
64
+ ],
65
+ [
66
+ "claude-3-5-haiku",
67
+ .8,
68
+ 4
69
+ ],
70
+ [
71
+ "claude-3-opus",
72
+ 15,
73
+ 75
74
+ ],
75
+ [
76
+ "claude-3-haiku",
77
+ .25,
78
+ 1.25
79
+ ],
80
+ [
81
+ "opus",
82
+ 15,
83
+ 75
84
+ ],
85
+ [
86
+ "sonnet",
87
+ 3,
88
+ 15
89
+ ],
90
+ [
91
+ "haiku",
92
+ .8,
93
+ 4
94
+ ]
95
+ ];
96
+ function estimateCostUsd(model, inputTokens, outputTokens) {
97
+ if (!model) return void 0;
98
+ const lower = model.toLowerCase();
99
+ const row = PRICES.find(([match]) => lower.includes(match));
100
+ if (!row) return void 0;
101
+ const [, inputRate, outputRate] = row;
102
+ return (inputTokens ?? 0) / 1e6 * inputRate + (outputTokens ?? 0) / 1e6 * outputRate;
103
+ }
104
+
105
+ //#endregion
106
+ //#region src/identity.ts
107
+ function mergeAttrs(...sources) {
108
+ return Object.assign({}, ...sources);
109
+ }
110
+ /** Pull the common identity attributes shared by every signal in a session. */
111
+ function readIdentity(attrs) {
112
+ return {
113
+ user: str(attrs, "user.id", "user.account_uuid", "user.email"),
114
+ organization: str(attrs, "organization.id"),
115
+ terminal: str(attrs, "terminal.type"),
116
+ appVersion: str(attrs, "app.version"),
117
+ model: str(attrs, "model")
118
+ };
119
+ }
120
+
121
+ //#endregion
122
+ //#region src/mcp.ts
123
+ const MCP_PREFIX = "mcp__";
124
+ function parseToolName(name) {
125
+ if (!name.startsWith(MCP_PREFIX)) return {
126
+ name,
127
+ isMcp: false
128
+ };
129
+ const rest = name.slice(5);
130
+ const sep = rest.indexOf("__");
131
+ if (sep === -1) return {
132
+ name,
133
+ isMcp: true,
134
+ mcpServer: rest || void 0
135
+ };
136
+ const server = rest.slice(0, sep);
137
+ const tool = rest.slice(sep + 2);
138
+ return {
139
+ name,
140
+ isMcp: true,
141
+ mcpServer: server || void 0,
142
+ mcpTool: tool || void 0
143
+ };
144
+ }
145
+ function isMcpTool(name) {
146
+ return name.startsWith(MCP_PREFIX);
147
+ }
148
+
149
+ //#endregion
150
+ //#region src/tool-taxonomy.ts
151
+ /**
152
+ * Tool taxonomy. Claude Code reports every action the model takes as a tool
153
+ * call (`tool_name` on `tool_result` / `tool_decision`), so the *kind* of work
154
+ * an agent is doing is derivable from the name:
155
+ *
156
+ * - sub-agents are the `Task` tool
157
+ * - skills are the `Skill` tool
158
+ * - MCP tools are `mcp__<server>__<tool>`
159
+ * - the rest are built-in file / shell / search / web / todo tools
160
+ *
161
+ * CC's native telemetry does NOT include tool *arguments*, so the specific
162
+ * sub-agent type or skill name usually isn't present — we read them defensively
163
+ * in case a future agent version (or another agent) adds them, and otherwise
164
+ * fall back to the category count.
165
+ */
166
+ const TOOL_CATEGORIES = [
167
+ "file",
168
+ "shell",
169
+ "search",
170
+ "web",
171
+ "todo",
172
+ "subagent",
173
+ "skill",
174
+ "mcp",
175
+ "other"
176
+ ];
177
+ const BUILTIN = {
178
+ read: "file",
179
+ edit: "file",
180
+ write: "file",
181
+ multiedit: "file",
182
+ notebookedit: "file",
183
+ bash: "shell",
184
+ bashoutput: "shell",
185
+ killshell: "shell",
186
+ killbash: "shell",
187
+ grep: "search",
188
+ glob: "search",
189
+ ls: "search",
190
+ webfetch: "web",
191
+ websearch: "web",
192
+ todowrite: "todo",
193
+ task: "subagent",
194
+ agent: "subagent",
195
+ skill: "skill"
196
+ };
197
+ function classifyTool(name) {
198
+ if (isMcpTool(name)) return "mcp";
199
+ return BUILTIN[name.toLowerCase()] ?? "other";
200
+ }
201
+ /** Sub-agent type, when the agent happens to emit it (defensive — often absent). */
202
+ function readSubAgentType(attributes) {
203
+ return str(attributes, "subagent_type", "agent_type", "subagent.type");
204
+ }
205
+ /** Skill name, when present (defensive — often absent). */
206
+ function readSkillName(attributes) {
207
+ return str(attributes, "skill", "skill_name", "skill.name");
208
+ }
209
+
210
+ //#endregion
211
+ //#region src/adapters/prefix-adapter.ts
212
+ /**
213
+ * Factory for "Claude-Code-shaped" agents. Claude Code, opencode and (soon)
214
+ * Codex emit the *same* instrument and event names under different prefixes and
215
+ * instrumentation scopes — opencode literally mirrors Claude Code's contract
216
+ * with an `opencode.` prefix. So one parameterized adapter covers them all, and
217
+ * a new agent is `createPrefixAdapter({ kind, prefix, scopeHint })`.
218
+ */
219
+ const EVENT_SUFFIXES = {
220
+ user_prompt: "user_prompt",
221
+ api_request: "api_request",
222
+ api_error: "api_error",
223
+ tool_result: "tool_result",
224
+ tool_decision: "tool_decision"
225
+ };
226
+ /** Compose a full ToolRef: MCP split + category + (defensive) sub-agent/skill id. */
227
+ function buildToolRef(name, attrs) {
228
+ const category = classifyTool(name);
229
+ const ref = {
230
+ ...parseToolName(name),
231
+ category
232
+ };
233
+ if (category === "subagent") {
234
+ const subAgentType = readSubAgentType(attrs);
235
+ if (subAgentType) ref.subAgentType = subAgentType;
236
+ } else if (category === "skill") {
237
+ const skillName = readSkillName(attrs);
238
+ if (skillName) ref.skillName = skillName;
239
+ }
240
+ return ref;
241
+ }
242
+ const METRIC_SUFFIXES = {
243
+ "lines_of_code.count": "lines_of_code",
244
+ "commit.count": "commit",
245
+ "pull_request.count": "pull_request",
246
+ "active_time.total": "active_time"
247
+ };
248
+ function createPrefixAdapter(config) {
249
+ const { kind, prefix, scopeHint, serviceHint } = config;
250
+ const scopeMatches = (scopeName) => typeof scopeName === "string" && scopeName.includes(scopeHint);
251
+ const serviceMatches = (resource) => {
252
+ const service = str(resource, "service.name");
253
+ return service !== void 0 && service.includes(serviceHint);
254
+ };
255
+ /** Suffix after the agent prefix, e.g. `"claude_code.api_request"` → `"api_request"`. */
256
+ const suffixOf = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
257
+ return {
258
+ kind,
259
+ matchesMetric(record) {
260
+ return record.name.startsWith(prefix) || scopeMatches(record.scope?.name) || serviceMatches(record.resource);
261
+ },
262
+ matchesEvent(record) {
263
+ return record.eventName.startsWith(prefix) || scopeMatches(record.scope?.name) || serviceMatches(record.resource);
264
+ },
265
+ normalizeEvent(record) {
266
+ const attrs = mergeAttrs(record.resource, record.attributes);
267
+ const rawName = suffixOf(record.eventName) || str(attrs, "event.name") || record.eventName;
268
+ const type = EVENT_SUFFIXES[rawName] ?? "other";
269
+ const sessionId = str(attrs, "session.id");
270
+ if (!sessionId) return null;
271
+ const event = {
272
+ id: `${sessionId}:0`,
273
+ sessionId,
274
+ agent: kind,
275
+ type,
276
+ rawEventName: rawName,
277
+ timestamp: record.timestamp,
278
+ model: str(attrs, "model"),
279
+ attributes: record.attributes
280
+ };
281
+ switch (type) {
282
+ case "api_request": {
283
+ event.inputTokens = num(attrs, "input_tokens");
284
+ event.outputTokens = num(attrs, "output_tokens");
285
+ event.cacheReadTokens = num(attrs, "cache_read_tokens");
286
+ event.cacheCreationTokens = num(attrs, "cache_creation_tokens");
287
+ event.durationMs = num(attrs, "duration_ms");
288
+ const reported = num(attrs, "cost_usd", "cost");
289
+ if (reported === void 0) {
290
+ const estimated = estimateCostUsd(event.model, event.inputTokens, event.outputTokens);
291
+ if (estimated !== void 0) {
292
+ event.costUsd = estimated;
293
+ event.costSource = "estimated";
294
+ }
295
+ } else {
296
+ event.costUsd = reported;
297
+ event.costSource = "reported";
298
+ }
299
+ break;
300
+ }
301
+ case "api_error":
302
+ event.errorMessage = str(attrs, "error", "error.message", "message");
303
+ event.statusCode = num(attrs, "status_code", "http.status_code");
304
+ event.durationMs = num(attrs, "duration_ms");
305
+ break;
306
+ case "tool_result": {
307
+ const toolName = str(attrs, "tool_name", "name", "tool");
308
+ if (toolName) event.tool = buildToolRef(toolName, attrs);
309
+ event.success = bool(attrs, "success");
310
+ event.durationMs = num(attrs, "duration_ms");
311
+ const decision = str(attrs, "decision");
312
+ if (decision === "accept" || decision === "reject") event.decision = decision;
313
+ break;
314
+ }
315
+ case "tool_decision": {
316
+ const toolName = str(attrs, "tool_name", "name", "tool");
317
+ if (toolName) event.tool = buildToolRef(toolName, attrs);
318
+ const decision = str(attrs, "decision");
319
+ if (decision === "accept" || decision === "reject") event.decision = decision;
320
+ break;
321
+ }
322
+ case "user_prompt": {
323
+ event.promptLength = num(attrs, "prompt_length", "prompt.length");
324
+ const text = str(attrs, "prompt");
325
+ if (text) event.promptText = text;
326
+ break;
327
+ }
328
+ default: break;
329
+ }
330
+ return event;
331
+ },
332
+ normalizeMetric(record) {
333
+ const kindOfMetric = METRIC_SUFFIXES[suffixOf(record.name)] ?? "other";
334
+ return record.dataPoints.map((point) => {
335
+ const attrs = mergeAttrs(record.resource, point.attributes);
336
+ return {
337
+ agent: kind,
338
+ sessionId: str(attrs, "session.id"),
339
+ identity: readIdentity(attrs),
340
+ kind: kindOfMetric,
341
+ value: point.value,
342
+ temporality: record.temporality,
343
+ timestamp: point.timestamp,
344
+ attributes: point.attributes
345
+ };
346
+ });
347
+ }
348
+ };
349
+ }
350
+
351
+ //#endregion
352
+ //#region src/adapters/claude-code.ts
353
+ /**
354
+ * Claude Code: metrics/events prefixed `claude_code.*`, emitted under the
355
+ * `com.anthropic.claude_code` instrumentation scope.
356
+ * Contract: https://code.claude.com/docs/en/monitoring-usage
357
+ */
358
+ const claudeCodeAdapter = createPrefixAdapter({
359
+ kind: "claude-code",
360
+ prefix: "claude_code.",
361
+ scopeHint: "claude_code",
362
+ serviceHint: "claude-code"
363
+ });
364
+
365
+ //#endregion
366
+ //#region src/adapters/opencode.ts
367
+ /**
368
+ * opencode: the opencode-plugin-otel project mirrors Claude Code's exact
369
+ * instrument and event names under an `opencode.*` prefix and the `com.opencode`
370
+ * scope. Same shape → same factory.
371
+ */
372
+ const opencodeAdapter = createPrefixAdapter({
373
+ kind: "opencode",
374
+ prefix: "opencode.",
375
+ scopeHint: "opencode",
376
+ serviceHint: "opencode"
377
+ });
378
+
379
+ //#endregion
380
+ //#region src/adapters/registry.ts
381
+ /**
382
+ * Ordered adapter registry. First match claims the record. Claude Code is most
383
+ * specific (dedicated scope), so it leads. Add Codex here when its contract
384
+ * lands — one line, no other changes.
385
+ */
386
+ const adapters = [claudeCodeAdapter, opencodeAdapter];
387
+ function detectAdapterForMetric(record) {
388
+ return adapters.find((adapter) => adapter.matchesMetric(record));
389
+ }
390
+ function detectAdapterForEvent(record) {
391
+ return adapters.find((adapter) => adapter.matchesEvent(record));
392
+ }
393
+ /** True if any adapter recognizes this metric — used for zero-config "agent detected" toasts. */
394
+ function isAgentMetric(record) {
395
+ return detectAdapterForMetric(record) !== void 0;
396
+ }
397
+ /** True if any adapter recognizes this log event. */
398
+ function isAgentEvent(record) {
399
+ return detectAdapterForEvent(record) !== void 0;
400
+ }
401
+
402
+ //#endregion
403
+ //#region src/reduce.ts
404
+ /**
405
+ * Session reducers. These are PURE (no I/O, no node:*) but stateful over a
406
+ * caller-owned `Map`, so the devtools server can keep one canonical store and
407
+ * broadcast finished `AgentSession` objects to the widget.
408
+ *
409
+ * Invariants:
410
+ * - Rollups are kept forever; the raw `timeline` is ring-buffered (`timelineLimit`).
411
+ * - Cost/token totals come from `api_request` EVENTS only. `token.usage` and
412
+ * `cost.usage` METRICS are recognized but deliberately not summed, so the two
413
+ * representations of the same fact never double-count.
414
+ */
415
+ const DEFAULT_TIMELINE_LIMIT = 500;
416
+ function emptyToolCategories() {
417
+ const out = {};
418
+ for (const category of TOOL_CATEGORIES) out[category] = 0;
419
+ return out;
420
+ }
421
+ function emptyRollup() {
422
+ return {
423
+ costUsd: 0,
424
+ costReportedUsd: 0,
425
+ costEstimatedUsd: 0,
426
+ inputTokens: 0,
427
+ outputTokens: 0,
428
+ cacheReadTokens: 0,
429
+ cacheCreationTokens: 0,
430
+ apiRequests: 0,
431
+ apiErrors: 0,
432
+ prompts: 0,
433
+ toolCalls: 0,
434
+ accepted: 0,
435
+ rejected: 0,
436
+ linesAdded: 0,
437
+ linesRemoved: 0,
438
+ commits: 0,
439
+ pullRequests: 0,
440
+ activeTimeSeconds: 0,
441
+ models: {},
442
+ tools: {},
443
+ toolCategories: emptyToolCategories(),
444
+ subAgents: {},
445
+ skills: {}
446
+ };
447
+ }
448
+ function createSession(id, agent, timestamp) {
449
+ return {
450
+ id,
451
+ agent,
452
+ firstSeen: timestamp,
453
+ lastSeen: timestamp,
454
+ eventCount: 0,
455
+ metricState: {},
456
+ rollup: emptyRollup(),
457
+ timeline: []
458
+ };
459
+ }
460
+ function getOrCreate(store, id, agent, timestamp) {
461
+ let session = store.get(id);
462
+ if (!session) {
463
+ session = createSession(id, agent, timestamp);
464
+ store.set(id, session);
465
+ }
466
+ return session;
467
+ }
468
+ function applyIdentity(session, identity) {
469
+ if (!session.user && identity.user) session.user = identity.user;
470
+ if (!session.organization && identity.organization) session.organization = identity.organization;
471
+ if (!session.terminal && identity.terminal) session.terminal = identity.terminal;
472
+ if (!session.appVersion && identity.appVersion) session.appVersion = identity.appVersion;
473
+ }
474
+ function touch(session, timestamp) {
475
+ if (timestamp < session.firstSeen) session.firstSeen = timestamp;
476
+ if (timestamp > session.lastSeen) session.lastSeen = timestamp;
477
+ }
478
+ function bumpTool(rollup, event) {
479
+ const ref = event.tool;
480
+ const existing = rollup.tools[ref.name] ?? {
481
+ name: ref.name,
482
+ category: ref.category,
483
+ isMcp: ref.isMcp,
484
+ mcpServer: ref.mcpServer,
485
+ count: 0,
486
+ accepted: 0,
487
+ rejected: 0,
488
+ failures: 0,
489
+ totalDurationMs: 0
490
+ };
491
+ rollup.tools[ref.name] = existing;
492
+ return existing;
493
+ }
494
+ /** Count a tool against its category and (for sub-agents/skills) its named bucket. */
495
+ function tallyToolKind(rollup, event) {
496
+ const ref = event.tool;
497
+ if (!ref) return;
498
+ rollup.toolCategories[ref.category] += 1;
499
+ if (ref.category === "subagent") {
500
+ const key = ref.subAgentType ?? "subagent";
501
+ rollup.subAgents[key] = (rollup.subAgents[key] ?? 0) + 1;
502
+ } else if (ref.category === "skill") {
503
+ const key = ref.skillName ?? "skill";
504
+ rollup.skills[key] = (rollup.skills[key] ?? 0) + 1;
505
+ }
506
+ }
507
+ /** Fold a normalized event into a session rollup + timeline. Returns the session. */
508
+ function foldEvent(session, event, timelineLimit = 500) {
509
+ session.eventCount += 1;
510
+ event.id = `${session.id}:${session.eventCount}`;
511
+ touch(session, event.timestamp);
512
+ const { rollup } = session;
513
+ switch (event.type) {
514
+ case "api_request":
515
+ rollup.apiRequests += 1;
516
+ rollup.inputTokens += event.inputTokens ?? 0;
517
+ rollup.outputTokens += event.outputTokens ?? 0;
518
+ rollup.cacheReadTokens += event.cacheReadTokens ?? 0;
519
+ rollup.cacheCreationTokens += event.cacheCreationTokens ?? 0;
520
+ if (event.costUsd !== void 0) {
521
+ rollup.costUsd += event.costUsd;
522
+ if (event.costSource === "estimated") rollup.costEstimatedUsd += event.costUsd;
523
+ else rollup.costReportedUsd += event.costUsd;
524
+ }
525
+ if (event.model) rollup.models[event.model] = (rollup.models[event.model] ?? 0) + 1;
526
+ break;
527
+ case "api_error":
528
+ rollup.apiErrors += 1;
529
+ break;
530
+ case "user_prompt":
531
+ rollup.prompts += 1;
532
+ break;
533
+ case "tool_result":
534
+ rollup.toolCalls += 1;
535
+ if (event.tool) {
536
+ const usage = bumpTool(rollup, event);
537
+ usage.count += 1;
538
+ usage.totalDurationMs += event.durationMs ?? 0;
539
+ if (event.success === false) usage.failures += 1;
540
+ tallyToolKind(rollup, event);
541
+ }
542
+ break;
543
+ case "tool_decision":
544
+ if (event.decision === "accept") rollup.accepted += 1;
545
+ if (event.decision === "reject") rollup.rejected += 1;
546
+ if (event.tool) {
547
+ const usage = bumpTool(rollup, event);
548
+ if (event.decision === "accept") usage.accepted += 1;
549
+ if (event.decision === "reject") usage.rejected += 1;
550
+ }
551
+ break;
552
+ default: break;
553
+ }
554
+ session.timeline.push(event);
555
+ if (session.timeline.length > timelineLimit) session.timeline.splice(0, session.timeline.length - timelineLimit);
556
+ return session;
557
+ }
558
+ /**
559
+ * The amount to add to a running total for one metric data point.
560
+ *
561
+ * `delta` points already carry the per-interval change, so they're added as-is.
562
+ * `cumulative` points carry an absolute running total per series, so we add the
563
+ * difference from the last value we saw for that series — otherwise re-exporting
564
+ * `lines_of_code.count = 42` every interval would inflate the total without end.
565
+ * A drop (counter reset / new process) is treated as a fresh delta.
566
+ */
567
+ function counterDelta(session, seriesKey, signal) {
568
+ if (signal.temporality !== "cumulative") return signal.value;
569
+ const last = session.metricState[seriesKey] ?? 0;
570
+ session.metricState[seriesKey] = signal.value;
571
+ return signal.value >= last ? signal.value - last : signal.value;
572
+ }
573
+ /** Stable per-series key: a cumulative counter is one series per attribute set. */
574
+ function seriesKey(signal) {
575
+ const attrs = Object.entries(signal.attributes).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${String(v)}`).join(",");
576
+ return `${signal.kind}|${attrs}`;
577
+ }
578
+ /** Fold a metric-only signal (lines, commits, PRs, active time) into the rollup. */
579
+ function foldMetricSignal(session, signal) {
580
+ touch(session, signal.timestamp);
581
+ applyIdentity(session, signal.identity);
582
+ const { rollup } = session;
583
+ const value = counterDelta(session, seriesKey(signal), signal);
584
+ switch (signal.kind) {
585
+ case "lines_of_code": {
586
+ const type = String(signal.attributes["type"] ?? "").toLowerCase();
587
+ if (type.includes("remov") || type.includes("delet")) rollup.linesRemoved += value;
588
+ else rollup.linesAdded += value;
589
+ break;
590
+ }
591
+ case "commit":
592
+ rollup.commits += value;
593
+ break;
594
+ case "pull_request":
595
+ rollup.pullRequests += value;
596
+ break;
597
+ case "active_time":
598
+ rollup.activeTimeSeconds += value;
599
+ break;
600
+ default: break;
601
+ }
602
+ }
603
+ /**
604
+ * Ingest a decoded OTLP log record. No-op (returns null) if no adapter claims it
605
+ * or the record lacks a session id.
606
+ */
607
+ function ingestEventRecord(store, record, options = {}) {
608
+ const adapter = detectAdapterForEvent(record);
609
+ if (!adapter) return null;
610
+ const event = adapter.normalizeEvent(record);
611
+ if (!event) return null;
612
+ const session = getOrCreate(store, event.sessionId, adapter.kind, event.timestamp);
613
+ applyIdentity(session, readIdentity(mergeAttrs(record.resource, record.attributes)));
614
+ return foldEvent(session, event, options.timelineLimit ?? 500);
615
+ }
616
+ /** Ingest a decoded OTLP metric. Returns sessions touched (may be several). */
617
+ function ingestMetricRecord(store, record) {
618
+ const adapter = detectAdapterForMetric(record);
619
+ if (!adapter) return [];
620
+ const touched = /* @__PURE__ */ new Map();
621
+ for (const signal of adapter.normalizeMetric(record)) {
622
+ if (!signal.sessionId) continue;
623
+ const session = getOrCreate(store, signal.sessionId, adapter.kind, signal.timestamp);
624
+ foldMetricSignal(session, signal);
625
+ touched.set(session.id, session);
626
+ }
627
+ return [...touched.values()];
628
+ }
629
+ /** Batch-ingest decoded OTLP log records. */
630
+ function ingestAgentEvents(store, records, options = {}) {
631
+ for (const record of records) ingestEventRecord(store, record, options);
632
+ }
633
+ /** Batch-ingest decoded OTLP metric records. */
634
+ function ingestAgentMetrics(store, records) {
635
+ for (const record of records) ingestMetricRecord(store, record);
636
+ }
637
+ function summarizeSessions(sessions) {
638
+ const agg = {
639
+ sessions: 0,
640
+ costUsd: 0,
641
+ inputTokens: 0,
642
+ outputTokens: 0,
643
+ apiRequests: 0,
644
+ apiErrors: 0,
645
+ accepted: 0,
646
+ rejected: 0,
647
+ models: {},
648
+ tools: {},
649
+ toolCategories: emptyToolCategories(),
650
+ mcpServers: {},
651
+ subAgents: {},
652
+ skills: {}
653
+ };
654
+ for (const session of sessions) {
655
+ agg.sessions += 1;
656
+ const { rollup } = session;
657
+ agg.costUsd += rollup.costUsd;
658
+ agg.inputTokens += rollup.inputTokens;
659
+ agg.outputTokens += rollup.outputTokens;
660
+ agg.apiRequests += rollup.apiRequests;
661
+ agg.apiErrors += rollup.apiErrors;
662
+ agg.accepted += rollup.accepted;
663
+ agg.rejected += rollup.rejected;
664
+ for (const [model, count] of Object.entries(rollup.models)) agg.models[model] = (agg.models[model] ?? 0) + count;
665
+ for (const usage of Object.values(rollup.tools)) {
666
+ agg.tools[usage.name] = (agg.tools[usage.name] ?? 0) + usage.count;
667
+ if (usage.isMcp && usage.mcpServer) agg.mcpServers[usage.mcpServer] = (agg.mcpServers[usage.mcpServer] ?? 0) + usage.count;
668
+ }
669
+ for (const category of TOOL_CATEGORIES) agg.toolCategories[category] += rollup.toolCategories[category];
670
+ for (const [type, count] of Object.entries(rollup.subAgents)) agg.subAgents[type] = (agg.subAgents[type] ?? 0) + count;
671
+ for (const [name, count] of Object.entries(rollup.skills)) agg.skills[name] = (agg.skills[name] ?? 0) + count;
672
+ }
673
+ return agg;
674
+ }
675
+
676
+ //#endregion
677
+ exports.DEFAULT_TIMELINE_LIMIT = DEFAULT_TIMELINE_LIMIT;
678
+ exports.TOOL_CATEGORIES = TOOL_CATEGORIES;
679
+ exports.adapters = adapters;
680
+ exports.classifyTool = classifyTool;
681
+ exports.claudeCodeAdapter = claudeCodeAdapter;
682
+ exports.createPrefixAdapter = createPrefixAdapter;
683
+ exports.detectAdapterForEvent = detectAdapterForEvent;
684
+ exports.detectAdapterForMetric = detectAdapterForMetric;
685
+ exports.estimateCostUsd = estimateCostUsd;
686
+ exports.foldEvent = foldEvent;
687
+ exports.foldMetricSignal = foldMetricSignal;
688
+ exports.ingestAgentEvents = ingestAgentEvents;
689
+ exports.ingestAgentMetrics = ingestAgentMetrics;
690
+ exports.ingestEventRecord = ingestEventRecord;
691
+ exports.ingestMetricRecord = ingestMetricRecord;
692
+ exports.isAgentEvent = isAgentEvent;
693
+ exports.isAgentMetric = isAgentMetric;
694
+ exports.isMcpTool = isMcpTool;
695
+ exports.mergeAttrs = mergeAttrs;
696
+ exports.opencodeAdapter = opencodeAdapter;
697
+ exports.parseToolName = parseToolName;
698
+ exports.readIdentity = readIdentity;
699
+ exports.readSkillName = readSkillName;
700
+ exports.readSubAgentType = readSubAgentType;
701
+ exports.summarizeSessions = summarizeSessions;
702
+ //# sourceMappingURL=index.cjs.map