agent-trace 0.2.4 → 0.2.6

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 (2) hide show
  1. package/agent-trace.cjs +246 -89
  2. package/package.json +1 -1
package/agent-trace.cjs CHANGED
@@ -24988,7 +24988,7 @@ async function startDashboardServer(options = {}) {
24988
24988
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
24989
24989
  var SCHEMA_SQL = `
24990
24990
  CREATE TABLE IF NOT EXISTS agent_events (
24991
- event_id TEXT NOT NULL,
24991
+ event_id TEXT NOT NULL UNIQUE,
24992
24992
  event_type TEXT NOT NULL,
24993
24993
  event_timestamp TEXT NOT NULL,
24994
24994
  session_id TEXT NOT NULL,
@@ -25100,6 +25100,7 @@ var SqliteClient = class {
25100
25100
  this.db = new import_better_sqlite3.default(dbPath);
25101
25101
  this.db.pragma("journal_mode = WAL");
25102
25102
  this.db.pragma("synchronous = NORMAL");
25103
+ this.migrateDeduplicateEvents();
25103
25104
  this.db.exec(SCHEMA_SQL);
25104
25105
  }
25105
25106
  async insertJsonEachRow(request) {
@@ -25319,9 +25320,129 @@ var SqliteClient = class {
25319
25320
  close() {
25320
25321
  this.db.close();
25321
25322
  }
25323
+ /**
25324
+ * Migration: deduplicate agent_events rows from older schemas that lacked a UNIQUE constraint.
25325
+ * Runs once — if the old table exists without a unique index, it rebuilds it.
25326
+ */
25327
+ migrateDeduplicateEvents() {
25328
+ const tableExists = this.db.prepare(
25329
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='agent_events'"
25330
+ ).get();
25331
+ if (tableExists === void 0) {
25332
+ return;
25333
+ }
25334
+ const hasUniqueIndex = this.db.prepare(
25335
+ "SELECT 1 FROM sqlite_master WHERE type='index' AND tbl_name='agent_events' AND sql LIKE '%UNIQUE%'"
25336
+ ).get();
25337
+ const indexInfo = this.db.prepare("PRAGMA index_list('agent_events')").all();
25338
+ const hasAutoUnique = indexInfo.some((idx) => idx["unique"] === 1);
25339
+ if (hasUniqueIndex !== void 0 || hasAutoUnique) {
25340
+ return;
25341
+ }
25342
+ const countResult = this.db.prepare(
25343
+ "SELECT COUNT(*) as total FROM agent_events"
25344
+ ).get();
25345
+ const distinctResult = this.db.prepare(
25346
+ "SELECT COUNT(DISTINCT event_id) as distinct_count FROM agent_events"
25347
+ ).get();
25348
+ const total = countResult?.total ?? 0;
25349
+ const distinct = distinctResult?.distinct_count ?? 0;
25350
+ if (total === 0) {
25351
+ this.db.exec("DROP TABLE agent_events");
25352
+ const tracesExist = this.db.prepare(
25353
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_traces'"
25354
+ ).get();
25355
+ if (tracesExist !== void 0) {
25356
+ this.db.exec("DELETE FROM session_traces");
25357
+ }
25358
+ return;
25359
+ }
25360
+ console.log(`[agent-trace] migrating: deduplicating agent_events (${total} rows \u2192 ${distinct} distinct)`);
25361
+ this.db.exec(`
25362
+ CREATE TABLE agent_events_dedup (
25363
+ event_id TEXT NOT NULL UNIQUE,
25364
+ event_type TEXT NOT NULL,
25365
+ event_timestamp TEXT NOT NULL,
25366
+ session_id TEXT NOT NULL,
25367
+ prompt_id TEXT,
25368
+ user_id TEXT NOT NULL DEFAULT 'unknown_user',
25369
+ source TEXT NOT NULL DEFAULT 'hook',
25370
+ agent_type TEXT NOT NULL DEFAULT 'claude_code',
25371
+ tool_name TEXT,
25372
+ tool_success INTEGER,
25373
+ tool_duration_ms REAL,
25374
+ model TEXT,
25375
+ cost_usd REAL,
25376
+ input_tokens INTEGER,
25377
+ output_tokens INTEGER,
25378
+ api_duration_ms REAL,
25379
+ lines_added INTEGER,
25380
+ lines_removed INTEGER,
25381
+ files_changed TEXT NOT NULL DEFAULT '[]',
25382
+ commit_sha TEXT,
25383
+ attributes TEXT NOT NULL DEFAULT '{}'
25384
+ );
25385
+
25386
+ INSERT OR IGNORE INTO agent_events_dedup SELECT * FROM agent_events;
25387
+
25388
+ DROP TABLE agent_events;
25389
+ ALTER TABLE agent_events_dedup RENAME TO agent_events;
25390
+
25391
+ CREATE INDEX IF NOT EXISTS idx_events_session ON agent_events(session_id);
25392
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON agent_events(event_timestamp);
25393
+ `);
25394
+ const afterCount = this.db.prepare("SELECT COUNT(*) as c FROM agent_events").get();
25395
+ console.log(`[agent-trace] migration complete: ${afterCount.c} events after dedup (removed ${total - afterCount.c} duplicates)`);
25396
+ this.rebuildSessionTracesFromEvents();
25397
+ }
25398
+ /**
25399
+ * Rebuild session_traces by aggregating deduplicated agent_events.
25400
+ * Called after dedup migration so the dashboard has correct metrics immediately.
25401
+ */
25402
+ rebuildSessionTracesFromEvents() {
25403
+ const tracesExist = this.db.prepare(
25404
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_traces'"
25405
+ ).get();
25406
+ if (tracesExist === void 0) {
25407
+ return;
25408
+ }
25409
+ this.db.exec("DELETE FROM session_traces");
25410
+ this.db.exec(`
25411
+ INSERT OR REPLACE INTO session_traces
25412
+ (session_id, version, started_at, ended_at, user_id, git_repo, git_branch,
25413
+ prompt_count, tool_call_count, api_call_count, total_cost_usd,
25414
+ total_input_tokens, total_output_tokens, lines_added, lines_removed,
25415
+ models_used, tools_used, files_touched, commit_count, updated_at)
25416
+ SELECT
25417
+ session_id,
25418
+ 1,
25419
+ MIN(event_timestamp),
25420
+ MAX(event_timestamp),
25421
+ COALESCE(MAX(CASE WHEN user_id != 'unknown_user' THEN user_id END), 'unknown_user'),
25422
+ NULL,
25423
+ NULL,
25424
+ SUM(CASE WHEN event_type LIKE '%prompt%' THEN 1 ELSE 0 END),
25425
+ SUM(CASE WHEN event_type LIKE '%tool%' THEN 1 ELSE 0 END),
25426
+ SUM(CASE WHEN event_type LIKE '%api%' THEN 1 ELSE 0 END),
25427
+ COALESCE(SUM(cost_usd), 0),
25428
+ COALESCE(SUM(input_tokens), 0),
25429
+ COALESCE(SUM(output_tokens), 0),
25430
+ COALESCE(SUM(lines_added), 0),
25431
+ COALESCE(SUM(lines_removed), 0),
25432
+ '[]',
25433
+ '[]',
25434
+ '[]',
25435
+ COUNT(DISTINCT commit_sha),
25436
+ MAX(event_timestamp)
25437
+ FROM agent_events
25438
+ GROUP BY session_id
25439
+ `);
25440
+ const rebuilt = this.db.prepare("SELECT COUNT(*) as c FROM session_traces").get();
25441
+ console.log(`[agent-trace] rebuilt ${rebuilt.c} session traces from deduplicated events`);
25442
+ }
25322
25443
  insertEvents(rows) {
25323
25444
  const insert = this.db.prepare(`
25324
- INSERT INTO agent_events
25445
+ INSERT OR IGNORE INTO agent_events
25325
25446
  (event_id, event_type, event_timestamp, session_id, prompt_id, user_id, source, agent_type,
25326
25447
  tool_name, tool_success, tool_duration_ms, model, cost_usd, input_tokens, output_tokens,
25327
25448
  api_duration_ms, lines_added, lines_removed, files_changed, commit_sha, attributes)
@@ -26019,6 +26140,109 @@ function toTimelineEventFromClickHouseRow(row) {
26019
26140
  };
26020
26141
  }
26021
26142
 
26143
+ // packages/schema/src/pricing.ts
26144
+ var MODEL_PRICING = [
26145
+ // ── Opus family ──────────────────────────────────────────
26146
+ // Opus 4.5 / 4.6 — $5 in, $25 out
26147
+ ["claude-opus-4-6", {
26148
+ inputPerToken: 5 / 1e6,
26149
+ outputPerToken: 25 / 1e6,
26150
+ cacheReadPerToken: 0.5 / 1e6,
26151
+ cacheWritePerToken: 6.25 / 1e6
26152
+ }],
26153
+ ["claude-opus-4-5", {
26154
+ inputPerToken: 5 / 1e6,
26155
+ outputPerToken: 25 / 1e6,
26156
+ cacheReadPerToken: 0.5 / 1e6,
26157
+ cacheWritePerToken: 6.25 / 1e6
26158
+ }],
26159
+ // Opus 4.1 / 4.0 — $15 in, $75 out
26160
+ ["claude-opus-4-1", {
26161
+ inputPerToken: 15 / 1e6,
26162
+ outputPerToken: 75 / 1e6,
26163
+ cacheReadPerToken: 1.5 / 1e6,
26164
+ cacheWritePerToken: 18.75 / 1e6
26165
+ }],
26166
+ ["claude-opus-4", {
26167
+ inputPerToken: 15 / 1e6,
26168
+ outputPerToken: 75 / 1e6,
26169
+ cacheReadPerToken: 1.5 / 1e6,
26170
+ cacheWritePerToken: 18.75 / 1e6
26171
+ }],
26172
+ // ── Sonnet family ────────────────────────────────────────
26173
+ // Sonnet 4.x — $3 in, $15 out
26174
+ ["claude-sonnet-4", {
26175
+ inputPerToken: 3 / 1e6,
26176
+ outputPerToken: 15 / 1e6,
26177
+ cacheReadPerToken: 0.3 / 1e6,
26178
+ cacheWritePerToken: 3.75 / 1e6
26179
+ }],
26180
+ // ── Haiku family ─────────────────────────────────────────
26181
+ // Haiku 4.5 — $1 in, $5 out
26182
+ ["claude-haiku-4", {
26183
+ inputPerToken: 1 / 1e6,
26184
+ outputPerToken: 5 / 1e6,
26185
+ cacheReadPerToken: 0.1 / 1e6,
26186
+ cacheWritePerToken: 1.25 / 1e6
26187
+ }],
26188
+ // ── Legacy 3.x models ───────────────────────────────────
26189
+ ["claude-3-5-sonnet", {
26190
+ inputPerToken: 3 / 1e6,
26191
+ outputPerToken: 15 / 1e6,
26192
+ cacheReadPerToken: 0.3 / 1e6,
26193
+ cacheWritePerToken: 3.75 / 1e6
26194
+ }],
26195
+ ["claude-3-5-haiku", {
26196
+ inputPerToken: 0.8 / 1e6,
26197
+ outputPerToken: 4 / 1e6,
26198
+ cacheReadPerToken: 0.08 / 1e6,
26199
+ cacheWritePerToken: 1 / 1e6
26200
+ }],
26201
+ ["claude-3-opus", {
26202
+ inputPerToken: 15 / 1e6,
26203
+ outputPerToken: 75 / 1e6,
26204
+ cacheReadPerToken: 1.5 / 1e6,
26205
+ cacheWritePerToken: 18.75 / 1e6
26206
+ }],
26207
+ ["claude-3-sonnet", {
26208
+ inputPerToken: 3 / 1e6,
26209
+ outputPerToken: 15 / 1e6,
26210
+ cacheReadPerToken: 0.3 / 1e6,
26211
+ cacheWritePerToken: 3.75 / 1e6
26212
+ }],
26213
+ ["claude-3-haiku", {
26214
+ inputPerToken: 0.25 / 1e6,
26215
+ outputPerToken: 1.25 / 1e6,
26216
+ cacheReadPerToken: 0.03 / 1e6,
26217
+ cacheWritePerToken: 0.3 / 1e6
26218
+ }]
26219
+ ];
26220
+ var SORTED_PRICING = [...MODEL_PRICING].sort((a, b) => b[0].length - a[0].length);
26221
+ function lookupModelPricing(model) {
26222
+ const normalized = model.toLowerCase();
26223
+ for (const [pattern, pricing] of SORTED_PRICING) {
26224
+ if (normalized.startsWith(pattern)) {
26225
+ return pricing;
26226
+ }
26227
+ }
26228
+ return void 0;
26229
+ }
26230
+ function calculateCostUsd(input) {
26231
+ if (input.model === void 0) {
26232
+ return 0;
26233
+ }
26234
+ const pricing = lookupModelPricing(input.model);
26235
+ if (pricing === void 0) {
26236
+ return 0;
26237
+ }
26238
+ const baseInput = Math.max(0, input.inputTokens);
26239
+ const output = Math.max(0, input.outputTokens);
26240
+ const cacheRead = Math.max(0, input.cacheReadTokens ?? 0);
26241
+ const cacheWrite = Math.max(0, input.cacheWriteTokens ?? 0);
26242
+ const cost = baseInput * pricing.inputPerToken + cacheRead * pricing.cacheReadPerToken + cacheWrite * pricing.cacheWritePerToken + output * pricing.outputPerToken;
26243
+ return Number(cost.toFixed(6));
26244
+ }
26245
+
26022
26246
  // packages/runtime/src/runtime.ts
26023
26247
  var import_node_http2 = __toESM(require("node:http"));
26024
26248
 
@@ -26897,6 +27121,10 @@ function buildNormalizedPayload(record, message, eventType) {
26897
27121
  if (cacheReadTokens !== void 0) {
26898
27122
  payload["cache_read_tokens"] = cacheReadTokens;
26899
27123
  }
27124
+ const cacheWriteTokens = readNumber(usage, ["cache_creation_input_tokens", "cacheCreationInputTokens"]);
27125
+ if (cacheWriteTokens !== void 0) {
27126
+ payload["cache_write_tokens"] = cacheWriteTokens;
27127
+ }
26900
27128
  const promptText = readPromptText(message);
26901
27129
  if (promptText !== void 0) {
26902
27130
  payload["prompt_text"] = promptText;
@@ -27967,92 +28195,6 @@ var InMemoryRuntimePersistence = class extends WriterBackedRuntimePersistence {
27967
28195
  }
27968
28196
  };
27969
28197
 
27970
- // packages/schema/src/pricing.ts
27971
- var MODEL_PRICING = [
27972
- // Opus 4 / 4.6
27973
- ["claude-opus-4", {
27974
- inputPerToken: 15 / 1e6,
27975
- outputPerToken: 75 / 1e6,
27976
- cacheReadPerToken: 1.5 / 1e6,
27977
- cacheWritePerToken: 18.75 / 1e6
27978
- }],
27979
- // Sonnet 4 / 4.6
27980
- ["claude-sonnet-4", {
27981
- inputPerToken: 3 / 1e6,
27982
- outputPerToken: 15 / 1e6,
27983
- cacheReadPerToken: 0.3 / 1e6,
27984
- cacheWritePerToken: 3.75 / 1e6
27985
- }],
27986
- // Haiku 4.5
27987
- ["claude-haiku-4", {
27988
- inputPerToken: 0.8 / 1e6,
27989
- outputPerToken: 4 / 1e6,
27990
- cacheReadPerToken: 0.08 / 1e6,
27991
- cacheWritePerToken: 1 / 1e6
27992
- }],
27993
- // Claude 3.5 Sonnet
27994
- ["claude-3-5-sonnet", {
27995
- inputPerToken: 3 / 1e6,
27996
- outputPerToken: 15 / 1e6,
27997
- cacheReadPerToken: 0.3 / 1e6,
27998
- cacheWritePerToken: 3.75 / 1e6
27999
- }],
28000
- // Claude 3.5 Haiku
28001
- ["claude-3-5-haiku", {
28002
- inputPerToken: 0.8 / 1e6,
28003
- outputPerToken: 4 / 1e6,
28004
- cacheReadPerToken: 0.08 / 1e6,
28005
- cacheWritePerToken: 1 / 1e6
28006
- }],
28007
- // Claude 3 Opus
28008
- ["claude-3-opus", {
28009
- inputPerToken: 15 / 1e6,
28010
- outputPerToken: 75 / 1e6,
28011
- cacheReadPerToken: 1.5 / 1e6,
28012
- cacheWritePerToken: 18.75 / 1e6
28013
- }],
28014
- // Claude 3 Sonnet
28015
- ["claude-3-sonnet", {
28016
- inputPerToken: 3 / 1e6,
28017
- outputPerToken: 15 / 1e6,
28018
- cacheReadPerToken: 0.3 / 1e6,
28019
- cacheWritePerToken: 3.75 / 1e6
28020
- }],
28021
- // Claude 3 Haiku
28022
- ["claude-3-haiku", {
28023
- inputPerToken: 0.25 / 1e6,
28024
- outputPerToken: 1.25 / 1e6,
28025
- cacheReadPerToken: 0.03 / 1e6,
28026
- cacheWritePerToken: 0.3 / 1e6
28027
- }]
28028
- ];
28029
- var SORTED_PRICING = [...MODEL_PRICING].sort((a, b) => b[0].length - a[0].length);
28030
- function lookupModelPricing(model) {
28031
- const normalized = model.toLowerCase();
28032
- for (const [pattern, pricing] of SORTED_PRICING) {
28033
- if (normalized.startsWith(pattern)) {
28034
- return pricing;
28035
- }
28036
- }
28037
- return void 0;
28038
- }
28039
- function calculateCostUsd(input) {
28040
- if (input.model === void 0) {
28041
- return 0;
28042
- }
28043
- const pricing = lookupModelPricing(input.model);
28044
- if (pricing === void 0) {
28045
- return 0;
28046
- }
28047
- const totalInput = Math.max(0, input.inputTokens);
28048
- const output = Math.max(0, input.outputTokens);
28049
- const cacheRead = Math.max(0, input.cacheReadTokens ?? 0);
28050
- const cacheWrite = Math.max(0, input.cacheWriteTokens ?? 0);
28051
- const regularInput = Math.max(0, totalInput - cacheRead - cacheWrite);
28052
- const cost = regularInput * pricing.inputPerToken + cacheRead * pricing.cacheReadPerToken + cacheWrite * pricing.cacheWritePerToken + output * pricing.outputPerToken;
28053
- return Number(cost.toFixed(6));
28054
- }
28055
-
28056
28198
  // packages/runtime/src/projector.ts
28057
28199
  function asRecord5(value) {
28058
28200
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -28543,11 +28685,26 @@ function hydrateFromSqlite(runtime, sqlite, limit, eventLimit) {
28543
28685
  const extra = commits.filter((c) => !pgShas.has(c.sha));
28544
28686
  commits = [...mapped, ...extra];
28545
28687
  }
28546
- return {
28688
+ let hydratedTrace = {
28547
28689
  ...trace,
28548
28690
  timeline,
28549
28691
  git: { ...trace.git, commits }
28550
28692
  };
28693
+ if (hydratedTrace.metrics.totalCostUsd === 0 && (hydratedTrace.metrics.totalInputTokens > 0 || hydratedTrace.metrics.totalOutputTokens > 0) && hydratedTrace.metrics.modelsUsed.length > 0) {
28694
+ const model = String(hydratedTrace.metrics.modelsUsed[0]);
28695
+ const recalculated = calculateCostUsd({
28696
+ model,
28697
+ inputTokens: hydratedTrace.metrics.totalInputTokens,
28698
+ outputTokens: hydratedTrace.metrics.totalOutputTokens
28699
+ });
28700
+ if (recalculated > 0) {
28701
+ hydratedTrace = {
28702
+ ...hydratedTrace,
28703
+ metrics: { ...hydratedTrace.metrics, totalCostUsd: recalculated }
28704
+ };
28705
+ }
28706
+ }
28707
+ return hydratedTrace;
28551
28708
  });
28552
28709
  for (const trace of traces) {
28553
28710
  runtime.sessionRepository.upsert(trace);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-trace",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Self-hosted observability for AI coding agents. One command, zero config.",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {