agent-profiler 0.1.0 → 1.0.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/src/core/db.ts DELETED
@@ -1,491 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import Database from "better-sqlite3";
6
- import { getConfiguredDatabasePath } from "./profile.js";
7
- import type { DerivedIngestFields } from "./eventMetadata.js";
8
- import type { NormalizedAgentEvent } from "./normalize.js";
9
- import type { WorkspaceGitMeta } from "./gitWorkspace.js";
10
-
11
- const HOME_DIR = path.join(os.homedir(), ".agent-profiler");
12
- const WORKSPACE_DIR = path.join(process.cwd(), ".agent-profiler");
13
- /** Same directory as this module: `src/core` when using tsx, `dist/core` when using build + copied schema. */
14
- const SCHEMA_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "schema.sql");
15
-
16
- export function getDefaultDbPath(): string {
17
- const fromEnv = process.env.AGENT_PROFILER_DB_PATH;
18
- if (fromEnv && fromEnv.trim().length > 0) return fromEnv;
19
- const configured = getConfiguredDatabasePath(process.cwd());
20
- if (configured && configured.trim().length > 0) return configured;
21
- return path.join(HOME_DIR, "events.sqlite");
22
- }
23
-
24
- type SqliteDatabase = {
25
- pragma(source: string): unknown;
26
- exec(sql: string): unknown;
27
- prepare(sql: string): {
28
- run(...params: unknown[]): unknown;
29
- get(...params: unknown[]): unknown;
30
- all(...params: unknown[]): unknown[];
31
- };
32
- close(): void;
33
- };
34
-
35
- export function resolveWritableDbPath(dbPath: string): string {
36
- try {
37
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
38
- return dbPath;
39
- } catch {
40
- const fallbackPath = path.join(WORKSPACE_DIR, "events.sqlite");
41
- fs.mkdirSync(path.dirname(fallbackPath), { recursive: true });
42
- return fallbackPath;
43
- }
44
- }
45
-
46
- export function openDb(dbPath = getDefaultDbPath()): SqliteDatabase {
47
- const writableDbPath = resolveWritableDbPath(dbPath);
48
- const db = new Database(writableDbPath) as unknown as SqliteDatabase;
49
- db.pragma("journal_mode = WAL");
50
- applySchema(db);
51
- return db;
52
- }
53
-
54
- export function resolveUsableDbPath(preferredDbPath: string): string {
55
- const primary = resolveWritableDbPath(preferredDbPath);
56
- try {
57
- const db = new Database(primary);
58
- db.pragma("journal_mode = WAL");
59
- db.close();
60
- return primary;
61
- } catch {
62
- const fallbackPath = path.join(WORKSPACE_DIR, "events.sqlite");
63
- fs.mkdirSync(path.dirname(fallbackPath), { recursive: true });
64
- const db = new Database(fallbackPath);
65
- db.pragma("journal_mode = WAL");
66
- db.close();
67
- return fallbackPath;
68
- }
69
- }
70
-
71
- export function applySchema(db: SqliteDatabase): void {
72
- const schemaSql = fs.readFileSync(SCHEMA_PATH, "utf8");
73
- db.exec(schemaSql);
74
- migrateEventsSchema(db);
75
- }
76
-
77
- function migrateEventsSchema(db: SqliteDatabase): void {
78
- const columns = db.prepare(`PRAGMA table_info(events)`).all() as { name: string }[];
79
- const names = new Set(columns.map((c) => c.name));
80
-
81
- const add = (sql: string, colName: string) => {
82
- if (!names.has(colName)) {
83
- db.exec(sql);
84
- names.add(colName);
85
- }
86
- };
87
-
88
- add(`ALTER TABLE events ADD COLUMN workspace_path TEXT`, "workspace_path");
89
- add(`ALTER TABLE events ADD COLUMN git_repo_root TEXT`, "git_repo_root");
90
- add(`ALTER TABLE events ADD COLUMN git_repo_name TEXT`, "git_repo_name");
91
- add(`ALTER TABLE events ADD COLUMN git_branch TEXT`, "git_branch");
92
- add(`ALTER TABLE events ADD COLUMN interaction_kind TEXT`, "interaction_kind");
93
- add(`ALTER TABLE events ADD COLUMN correlation_id TEXT`, "correlation_id");
94
- add(`ALTER TABLE events ADD COLUMN tool_canonical_name TEXT`, "tool_canonical_name");
95
- add(`ALTER TABLE events ADD COLUMN mcp_server TEXT`, "mcp_server");
96
- add(`ALTER TABLE events ADD COLUMN mcp_tool TEXT`, "mcp_tool");
97
- add(`ALTER TABLE events ADD COLUMN payload_byte_length INTEGER`, "payload_byte_length");
98
- add(`ALTER TABLE events ADD COLUMN prompt_fingerprint TEXT`, "prompt_fingerprint");
99
-
100
- db.exec(
101
- `CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace_path, created_at)`,
102
- );
103
- db.exec(
104
- `CREATE INDEX IF NOT EXISTS idx_events_interaction_kind ON events(interaction_kind, created_at)`,
105
- );
106
- db.exec(
107
- `CREATE INDEX IF NOT EXISTS idx_events_correlation ON events(correlation_id, session_id)`,
108
- );
109
- db.exec(
110
- `CREATE INDEX IF NOT EXISTS idx_events_prompt_fingerprint ON events(prompt_fingerprint, created_at)`,
111
- );
112
- db.exec(
113
- `CREATE INDEX IF NOT EXISTS idx_spans_mcp ON interaction_spans(mcp_server, mcp_tool, started_at)`,
114
- );
115
- db.exec(
116
- `CREATE INDEX IF NOT EXISTS idx_spans_session ON interaction_spans(session_key, started_at)`,
117
- );
118
- }
119
-
120
- export function insertEvent(
121
- db: SqliteDatabase,
122
- event: NormalizedAgentEvent,
123
- payloadHash: string,
124
- workspaceGit: WorkspaceGitMeta,
125
- derived: DerivedIngestFields,
126
- ): number {
127
- const stmt = db.prepare(`
128
- INSERT INTO events (
129
- created_at,
130
- source,
131
- source_event,
132
- repo_path,
133
- session_id,
134
- turn_id,
135
- model,
136
- role,
137
- estimated_input_tokens,
138
- estimated_output_tokens,
139
- estimated_total_tokens,
140
- payload_hash,
141
- raw_payload,
142
- workspace_path,
143
- git_repo_root,
144
- git_repo_name,
145
- git_branch,
146
- interaction_kind,
147
- correlation_id,
148
- tool_canonical_name,
149
- mcp_server,
150
- mcp_tool,
151
- payload_byte_length,
152
- prompt_fingerprint
153
- )
154
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
155
- `);
156
-
157
- const info = stmt.run(
158
- new Date().toISOString(),
159
- event.source,
160
- event.sourceEvent,
161
- event.repoPath ?? null,
162
- event.sessionId ?? null,
163
- event.turnId ?? null,
164
- event.model ?? null,
165
- event.role,
166
- event.estimatedInputTokens,
167
- event.estimatedOutputTokens,
168
- event.estimatedTotalTokens,
169
- payloadHash,
170
- JSON.stringify(event.rawPayload),
171
- workspaceGit.workspacePath,
172
- workspaceGit.gitRepoRoot,
173
- workspaceGit.gitRepoName,
174
- workspaceGit.gitBranch,
175
- derived.interactionKind,
176
- derived.correlationId,
177
- derived.toolCanonicalName,
178
- derived.mcpServer,
179
- derived.mcpTool,
180
- derived.payloadByteLength,
181
- derived.promptFingerprint,
182
- ) as { lastInsertRowid: number | bigint };
183
-
184
- return Number(info.lastInsertRowid);
185
- }
186
-
187
- type SpanRow = {
188
- id: number;
189
- arg_token_estimate: number;
190
- result_token_estimate: number;
191
- pre_event_id: number | null;
192
- post_event_id: number | null;
193
- failure_event_id: number | null;
194
- };
195
-
196
- /**
197
- * Links pre/post/failure hook rows for the same logical tool or MCP call via correlation_id.
198
- */
199
- export function mergeInteractionSpan(
200
- db: SqliteDatabase,
201
- eventId: number,
202
- normalized: NormalizedAgentEvent,
203
- workspaceGit: WorkspaceGitMeta,
204
- derived: DerivedIngestFields,
205
- ): void {
206
- if (!derived.correlationId || !derived.toolPhase) return;
207
-
208
- const sessionKey = normalized.sessionId ?? "";
209
- const source = normalized.source;
210
- const now = new Date().toISOString();
211
-
212
- const existing = db
213
- .prepare(
214
- `SELECT id, arg_token_estimate, result_token_estimate, pre_event_id, post_event_id, failure_event_id
215
- FROM interaction_spans
216
- WHERE session_key = ? AND source = ? AND correlation_id = ?`,
217
- )
218
- .get(sessionKey, source, derived.correlationId) as SpanRow | undefined;
219
-
220
- const argTok = Math.max(normalized.estimatedInputTokens, 0);
221
- const resTok = Math.max(normalized.estimatedOutputTokens, 0);
222
-
223
- const toolName = derived.toolCanonicalName;
224
- const mcpS = derived.mcpServer;
225
- const mcpT = derived.mcpTool;
226
-
227
- if (!existing) {
228
- const ins = db.prepare(`
229
- INSERT INTO interaction_spans (
230
- session_key, source, correlation_id, turn_id,
231
- tool_canonical_name, mcp_server, mcp_tool,
232
- pre_event_id, post_event_id, failure_event_id,
233
- arg_token_estimate, result_token_estimate,
234
- workspace_path, git_repo_root, git_repo_name, git_branch,
235
- started_at, completed_at
236
- )
237
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
238
- `);
239
- if (derived.toolPhase === "pre") {
240
- ins.run(
241
- sessionKey,
242
- source,
243
- derived.correlationId,
244
- normalized.turnId ?? null,
245
- toolName,
246
- mcpS,
247
- mcpT,
248
- eventId,
249
- null,
250
- null,
251
- argTok,
252
- 0,
253
- workspaceGit.workspacePath,
254
- workspaceGit.gitRepoRoot,
255
- workspaceGit.gitRepoName,
256
- workspaceGit.gitBranch,
257
- now,
258
- null,
259
- );
260
- } else if (derived.toolPhase === "post") {
261
- ins.run(
262
- sessionKey,
263
- source,
264
- derived.correlationId,
265
- normalized.turnId ?? null,
266
- toolName,
267
- mcpS,
268
- mcpT,
269
- null,
270
- eventId,
271
- null,
272
- 0,
273
- resTok,
274
- workspaceGit.workspacePath,
275
- workspaceGit.gitRepoRoot,
276
- workspaceGit.gitRepoName,
277
- workspaceGit.gitBranch,
278
- null,
279
- now,
280
- );
281
- } else {
282
- ins.run(
283
- sessionKey,
284
- source,
285
- derived.correlationId,
286
- normalized.turnId ?? null,
287
- toolName,
288
- mcpS,
289
- mcpT,
290
- null,
291
- null,
292
- eventId,
293
- 0,
294
- resTok,
295
- workspaceGit.workspacePath,
296
- workspaceGit.gitRepoRoot,
297
- workspaceGit.gitRepoName,
298
- workspaceGit.gitBranch,
299
- null,
300
- now,
301
- );
302
- }
303
- return;
304
- }
305
-
306
- if (derived.toolPhase === "pre") {
307
- db.prepare(
308
- `UPDATE interaction_spans SET
309
- pre_event_id = COALESCE(pre_event_id, ?),
310
- started_at = COALESCE(started_at, ?),
311
- arg_token_estimate = ?,
312
- tool_canonical_name = COALESCE(tool_canonical_name, ?),
313
- mcp_server = COALESCE(mcp_server, ?),
314
- mcp_tool = COALESCE(mcp_tool, ?),
315
- turn_id = COALESCE(turn_id, ?)
316
- WHERE id = ?`,
317
- ).run(
318
- eventId,
319
- now,
320
- Math.max(existing.arg_token_estimate, argTok),
321
- toolName,
322
- mcpS,
323
- mcpT,
324
- normalized.turnId ?? null,
325
- existing.id,
326
- );
327
- } else if (derived.toolPhase === "post") {
328
- db.prepare(
329
- `UPDATE interaction_spans SET
330
- post_event_id = COALESCE(post_event_id, ?),
331
- completed_at = COALESCE(completed_at, ?),
332
- result_token_estimate = ?,
333
- tool_canonical_name = COALESCE(tool_canonical_name, ?),
334
- mcp_server = COALESCE(mcp_server, ?),
335
- mcp_tool = COALESCE(mcp_tool, ?),
336
- turn_id = COALESCE(turn_id, ?)
337
- WHERE id = ?`,
338
- ).run(
339
- eventId,
340
- now,
341
- Math.max(existing.result_token_estimate, resTok),
342
- toolName,
343
- mcpS,
344
- mcpT,
345
- normalized.turnId ?? null,
346
- existing.id,
347
- );
348
- } else {
349
- db.prepare(
350
- `UPDATE interaction_spans SET
351
- failure_event_id = COALESCE(failure_event_id, ?),
352
- completed_at = COALESCE(completed_at, ?),
353
- result_token_estimate = ?,
354
- tool_canonical_name = COALESCE(tool_canonical_name, ?),
355
- mcp_server = COALESCE(mcp_server, ?),
356
- mcp_tool = COALESCE(mcp_tool, ?),
357
- turn_id = COALESCE(turn_id, ?)
358
- WHERE id = ?`,
359
- ).run(
360
- eventId,
361
- now,
362
- Math.max(existing.result_token_estimate, resTok),
363
- toolName,
364
- mcpS,
365
- mcpT,
366
- normalized.turnId ?? null,
367
- existing.id,
368
- );
369
- }
370
- }
371
-
372
- export type StoredEventSummary = {
373
- createdAt: string;
374
- source: string;
375
- sourceEvent: string;
376
- estimatedTotalTokens: number;
377
- };
378
-
379
- export function getLastEventSummary(db: SqliteDatabase): StoredEventSummary | null {
380
- const row = db
381
- .prepare(
382
- `
383
- SELECT
384
- created_at AS createdAt,
385
- source,
386
- source_event AS sourceEvent,
387
- estimated_total_tokens AS estimatedTotalTokens
388
- FROM events
389
- ORDER BY created_at DESC
390
- LIMIT 1
391
- `,
392
- )
393
- .get() as StoredEventSummary | undefined;
394
-
395
- return row ?? null;
396
- }
397
-
398
- export type StoredEvent = {
399
- id: number;
400
- createdAt: string;
401
- source: string;
402
- sourceEvent: string;
403
- repoPath: string | null;
404
- sessionId: string | null;
405
- turnId: string | null;
406
- model: string | null;
407
- role: string;
408
- estimatedInputTokens: number;
409
- estimatedOutputTokens: number;
410
- estimatedTotalTokens: number;
411
- rawPayload: string;
412
- workspacePath: string | null;
413
- gitRepoRoot: string | null;
414
- gitRepoName: string | null;
415
- gitBranch: string | null;
416
- };
417
-
418
- export function getEventsForLatestSession(db: SqliteDatabase): StoredEvent[] {
419
- const latest = db
420
- .prepare(
421
- `
422
- SELECT source, session_id AS sessionId, repo_path AS repoPath
423
- FROM events
424
- ORDER BY created_at DESC
425
- LIMIT 1
426
- `,
427
- )
428
- .get() as { source: string; sessionId: string | null; repoPath: string | null } | undefined;
429
-
430
- if (!latest) return [];
431
-
432
- const rows = latest.sessionId
433
- ? db
434
- .prepare(
435
- `
436
- SELECT
437
- id,
438
- created_at AS createdAt,
439
- source,
440
- source_event AS sourceEvent,
441
- repo_path AS repoPath,
442
- session_id AS sessionId,
443
- turn_id AS turnId,
444
- model,
445
- role,
446
- estimated_input_tokens AS estimatedInputTokens,
447
- estimated_output_tokens AS estimatedOutputTokens,
448
- estimated_total_tokens AS estimatedTotalTokens,
449
- raw_payload AS rawPayload,
450
- workspace_path AS workspacePath,
451
- git_repo_root AS gitRepoRoot,
452
- git_repo_name AS gitRepoName,
453
- git_branch AS gitBranch
454
- FROM events
455
- WHERE source = ? AND session_id = ?
456
- ORDER BY created_at ASC, id ASC
457
- `,
458
- )
459
- .all(latest.source, latest.sessionId)
460
- : db
461
- .prepare(
462
- `
463
- SELECT
464
- id,
465
- created_at AS createdAt,
466
- source,
467
- source_event AS sourceEvent,
468
- repo_path AS repoPath,
469
- session_id AS sessionId,
470
- turn_id AS turnId,
471
- model,
472
- role,
473
- estimated_input_tokens AS estimatedInputTokens,
474
- estimated_output_tokens AS estimatedOutputTokens,
475
- estimated_total_tokens AS estimatedTotalTokens,
476
- raw_payload AS rawPayload,
477
- workspace_path AS workspacePath,
478
- git_repo_root AS gitRepoRoot,
479
- git_repo_name AS gitRepoName,
480
- git_branch AS gitBranch
481
- FROM events
482
- WHERE source = ? AND repo_path IS ?
483
- ORDER BY created_at DESC, id DESC
484
- LIMIT 200
485
- `,
486
- )
487
- .all(latest.source, latest.repoPath ?? null)
488
- .reverse();
489
-
490
- return rows as StoredEvent[];
491
- }
@@ -1,184 +0,0 @@
1
- import crypto from "node:crypto";
2
- import type { NormalizedAgentEvent } from "./normalize.js";
3
-
4
- /** Hook adapters that emit structured telemetry (matches CLI init sources). */
5
- export type TelemetryHookSource = "cursor" | "codex";
6
-
7
- export type ToolSpanPhase = "pre" | "post" | "failure";
8
-
9
- export type DerivedIngestFields = {
10
- /** Normalized category for analytics / future intent models. */
11
- interactionKind: string;
12
- correlationId: string | null;
13
- toolCanonicalName: string | null;
14
- mcpServer: string | null;
15
- mcpTool: string | null;
16
- payloadByteLength: number;
17
- /** SHA-256 of trimmed prompt text when this row is a user submission. */
18
- promptFingerprint: string | null;
19
- /** When set, row participates in `interaction_spans` merge. */
20
- toolPhase: ToolSpanPhase | null;
21
- };
22
-
23
- function asRecord(value: unknown): Record<string, unknown> {
24
- if (value && typeof value === "object" && !Array.isArray(value)) {
25
- return value as Record<string, unknown>;
26
- }
27
- return {};
28
- }
29
-
30
- function pickFirstString(values: unknown[]): string | undefined {
31
- for (const value of values) {
32
- if (typeof value === "string" && value.trim().length > 0) {
33
- return value;
34
- }
35
- }
36
- return undefined;
37
- }
38
-
39
- /** Split Codex-style `mcp__server__tool` or loose `MCP: server — tool`. */
40
- export function parseMcpToolName(canonical: string): {
41
- mcpServer: string | null;
42
- mcpTool: string | null;
43
- } {
44
- const t = canonical.trim();
45
- if (!t) return { mcpServer: null, mcpTool: null };
46
-
47
- if (t.startsWith("mcp__")) {
48
- const parts = t.split("__").filter(Boolean);
49
- if (parts.length >= 3) {
50
- return { mcpServer: parts[1] ?? null, mcpTool: parts.slice(2).join("__") || null };
51
- }
52
- }
53
-
54
- if (t.toLowerCase().startsWith("mcp:")) {
55
- const rest = t.slice(4).trim();
56
- const sep = rest.indexOf(":");
57
- if (sep > 0) {
58
- return {
59
- mcpServer: rest.slice(0, sep).trim() || null,
60
- mcpTool: rest.slice(sep + 1).trim() || null,
61
- };
62
- }
63
- }
64
-
65
- return { mcpServer: null, mcpTool: null };
66
- }
67
-
68
- function fingerprintPrompt(text: string): string | null {
69
- const trimmed = text.trim();
70
- if (!trimmed) return null;
71
- return crypto.createHash("sha256").update(trimmed, "utf8").digest("hex");
72
- }
73
-
74
- function hookToInteractionKind(
75
- source: TelemetryHookSource,
76
- hookEventName: string,
77
- ): { kind: string; toolPhase: ToolSpanPhase | null } {
78
- const cursorMap: Record<string, { kind: string; toolPhase: ToolSpanPhase | null }> = {
79
- beforeSubmitPrompt: { kind: "user_prompt_submit", toolPhase: null },
80
- afterAgentResponse: { kind: "model_output", toolPhase: null },
81
- afterAgentThought: { kind: "model_thought", toolPhase: null },
82
- preToolUse: { kind: "tool_request", toolPhase: "pre" },
83
- postToolUse: { kind: "tool_result_event", toolPhase: "post" },
84
- postToolUseFailure: { kind: "tool_failure_event", toolPhase: "failure" },
85
- beforeMCPExecution: { kind: "mcp_request", toolPhase: "pre" },
86
- afterMCPExecution: { kind: "mcp_result_event", toolPhase: "post" },
87
- beforeShellExecution: { kind: "shell_command_request", toolPhase: "pre" },
88
- afterShellExecution: { kind: "shell_output", toolPhase: null },
89
- afterFileEdit: { kind: "file_edit", toolPhase: null },
90
- beforeReadFile: { kind: "file_read_request", toolPhase: "pre" },
91
- start: { kind: "session_start", toolPhase: null },
92
- sessionStart: { kind: "session_start", toolPhase: null },
93
- stop: { kind: "session_stop", toolPhase: null },
94
- sessionEnd: { kind: "session_end", toolPhase: null },
95
- preCompact: { kind: "context_compact", toolPhase: null },
96
- };
97
-
98
- const codexMap: Record<string, { kind: string; toolPhase: ToolSpanPhase | null }> = {
99
- SessionStart: { kind: "session_start", toolPhase: null },
100
- UserPromptSubmit: { kind: "user_prompt_submit", toolPhase: null },
101
- PreToolUse: { kind: "tool_request", toolPhase: "pre" },
102
- PostToolUse: { kind: "tool_result_event", toolPhase: "post" },
103
- Stop: { kind: "session_stop", toolPhase: null },
104
- };
105
-
106
- const mapped =
107
- source === "codex" ? codexMap[hookEventName] : cursorMap[hookEventName];
108
- if (mapped) return mapped;
109
-
110
- return { kind: `other:${hookEventName}`, toolPhase: null };
111
- }
112
-
113
- function extractCorrelationId(payload: Record<string, unknown>): string | null {
114
- return (
115
- pickFirstString([
116
- payload.tool_use_id,
117
- payload.toolUseId,
118
- payload.toolCallId,
119
- payload.callId,
120
- payload.id,
121
- payload.invocationId,
122
- ]) ?? null
123
- );
124
- }
125
-
126
- function extractToolCanonicalName(payload: Record<string, unknown>): string | null {
127
- const name =
128
- pickFirstString([
129
- payload.tool_name,
130
- payload.toolName,
131
- payload.tool,
132
- payload.name,
133
- payload.type,
134
- ]) ?? null;
135
- return name?.trim() || null;
136
- }
137
-
138
- /**
139
- * Derives query-friendly fields + MCP split + span phase for tool correlation.
140
- */
141
- export function deriveIngestFields(
142
- source: TelemetryHookSource,
143
- hookEventName: string,
144
- rawPayload: unknown,
145
- rawJsonText: string,
146
- normalized: NormalizedAgentEvent,
147
- ): DerivedIngestFields {
148
- const { kind, toolPhase } = hookToInteractionKind(source, hookEventName);
149
- const payload = asRecord(rawPayload);
150
- const correlationId = extractCorrelationId(payload);
151
- let toolCanonicalName = extractToolCanonicalName(payload);
152
-
153
- if (!toolCanonicalName && payload.tool_input && typeof payload.tool_input === "object") {
154
- const ti = payload.tool_input as Record<string, unknown>;
155
- toolCanonicalName =
156
- pickFirstString([ti.command, ti.tool, ti.name])?.trim() ||
157
- toolCanonicalName;
158
- }
159
-
160
- const { mcpServer, mcpTool } = toolCanonicalName
161
- ? parseMcpToolName(toolCanonicalName)
162
- : { mcpServer: null, mcpTool: null };
163
-
164
- let promptFingerprint: string | null = null;
165
- if (kind === "user_prompt_submit") {
166
- const prompt =
167
- pickFirstString([payload.prompt, payload.text, payload.message]) ??
168
- normalized.observableText;
169
- promptFingerprint = fingerprintPrompt(prompt);
170
- }
171
-
172
- const payloadByteLength = Buffer.byteLength(rawJsonText, "utf8");
173
-
174
- return {
175
- interactionKind: kind,
176
- correlationId,
177
- toolCanonicalName,
178
- mcpServer,
179
- mcpTool,
180
- payloadByteLength,
181
- promptFingerprint,
182
- toolPhase,
183
- };
184
- }