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