@bububuger/spanory 0.1.16 → 0.1.19

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.
@@ -1,644 +0,0 @@
1
- // @ts-nocheck
2
- import { createHash } from 'node:crypto';
3
- function toNumber(value) {
4
- const n = Number(value);
5
- return Number.isFinite(n) ? n : undefined;
6
- }
7
- export function pickUsage(raw) {
8
- if (!raw || typeof raw !== 'object')
9
- return undefined;
10
- const inputTokens = toNumber(raw.input_tokens ?? raw.prompt_tokens);
11
- const outputTokens = toNumber(raw.output_tokens ?? raw.completion_tokens);
12
- const totalTokens = toNumber(raw.total_tokens) ?? ((inputTokens ?? 0) + (outputTokens ?? 0) || undefined);
13
- const cacheReadInputTokens = toNumber(raw.cache_read_input_tokens);
14
- const cacheCreationInputTokens = toNumber(raw.cache_creation_input_tokens);
15
- const usage = {};
16
- if (inputTokens !== undefined)
17
- usage.input_tokens = inputTokens;
18
- if (outputTokens !== undefined)
19
- usage.output_tokens = outputTokens;
20
- if (totalTokens !== undefined)
21
- usage.total_tokens = totalTokens;
22
- if (cacheReadInputTokens !== undefined)
23
- usage.cache_read_input_tokens = cacheReadInputTokens;
24
- if (cacheCreationInputTokens !== undefined)
25
- usage.cache_creation_input_tokens = cacheCreationInputTokens;
26
- return Object.keys(usage).length ? usage : undefined;
27
- }
28
- function addUsage(total, usage) {
29
- if (!usage)
30
- return;
31
- for (const [key, value] of Object.entries(usage)) {
32
- total[key] = (total[key] ?? 0) + Number(value);
33
- }
34
- }
35
- function usageAttributes(usage) {
36
- if (!usage)
37
- return {};
38
- const attrs = {};
39
- if (usage.input_tokens !== undefined) {
40
- attrs['gen_ai.usage.input_tokens'] = usage.input_tokens;
41
- attrs['gen_ai.usage.prompt_tokens'] = usage.input_tokens;
42
- }
43
- if (usage.output_tokens !== undefined) {
44
- attrs['gen_ai.usage.output_tokens'] = usage.output_tokens;
45
- attrs['gen_ai.usage.completion_tokens'] = usage.output_tokens;
46
- }
47
- if (usage.total_tokens !== undefined) {
48
- attrs['gen_ai.usage.total_tokens'] = usage.total_tokens;
49
- }
50
- if (usage.cache_read_input_tokens !== undefined) {
51
- attrs['gen_ai.usage.details.cache_read_input_tokens'] = usage.cache_read_input_tokens;
52
- }
53
- if (usage.cache_creation_input_tokens !== undefined) {
54
- attrs['gen_ai.usage.details.cache_creation_input_tokens'] = usage.cache_creation_input_tokens;
55
- }
56
- const cacheRead = usage.cache_read_input_tokens ?? 0;
57
- const denominator = (usage.input_tokens ?? 0) + cacheRead;
58
- const cacheHitRate = denominator > 0 ? cacheRead / denominator : 0;
59
- attrs['gen_ai.usage.details.cache_hit_rate'] = Number(cacheHitRate.toFixed(6));
60
- attrs['langfuse.observation.usage_details'] = JSON.stringify({
61
- ...(usage.input_tokens !== undefined ? { input: usage.input_tokens } : {}),
62
- ...(usage.output_tokens !== undefined ? { output: usage.output_tokens } : {}),
63
- ...(usage.total_tokens !== undefined ? { total: usage.total_tokens } : {}),
64
- ...(usage.cache_read_input_tokens !== undefined ? { input_cache_read: usage.cache_read_input_tokens } : {}),
65
- ...(usage.cache_creation_input_tokens !== undefined
66
- ? { input_cache_creation: usage.cache_creation_input_tokens }
67
- : {}),
68
- });
69
- return attrs;
70
- }
71
- function modelAttributes(model) {
72
- if (!model)
73
- return {};
74
- return {
75
- 'langfuse.observation.model.name': model,
76
- 'gen_ai.request.model': model,
77
- };
78
- }
79
- function extractText(content) {
80
- if (typeof content === 'string')
81
- return content;
82
- if (!Array.isArray(content))
83
- return '';
84
- return content
85
- .map((block) => {
86
- if (typeof block === 'string')
87
- return block;
88
- if (block && typeof block === 'object' && block.type === 'text')
89
- return String(block.text ?? '');
90
- return '';
91
- })
92
- .filter(Boolean)
93
- .join('\n');
94
- }
95
- function extractToolUses(content) {
96
- if (!Array.isArray(content))
97
- return [];
98
- return content.filter((block) => block && typeof block === 'object' && block.type === 'tool_use');
99
- }
100
- function extractToolResults(content) {
101
- if (!Array.isArray(content))
102
- return [];
103
- return content.filter((block) => block && typeof block === 'object' && block.type === 'tool_result');
104
- }
105
- function extractReasoningBlocks(content) {
106
- if (!Array.isArray(content))
107
- return [];
108
- return content.filter((block) => block && typeof block === 'object' && block.type === 'reasoning');
109
- }
110
- function isoFromUnknownTimestamp(value, fallback) {
111
- const candidate = value instanceof Date ? value : new Date(value ?? '');
112
- if (!Number.isNaN(candidate.getTime()))
113
- return candidate.toISOString();
114
- return fallback.toISOString();
115
- }
116
- function isToolResultOnlyContent(content) {
117
- return Array.isArray(content)
118
- && content.length > 0
119
- && content.every((block) => block && typeof block === 'object' && block.type === 'tool_result');
120
- }
121
- function isPromptUserMessage(message) {
122
- if (!message || message.role !== 'user' || message.isMeta)
123
- return false;
124
- const { content } = message;
125
- if (typeof content === 'string')
126
- return content.trim().length > 0;
127
- if (!Array.isArray(content))
128
- return false;
129
- if (isToolResultOnlyContent(content))
130
- return false;
131
- return content.length > 0;
132
- }
133
- const GATEWAY_INPUT_METADATA_BLOCK_RE = /Conversation info \(untrusted metadata\):\s*```json\s*([\s\S]*?)\s*```\s*/i;
134
- function runtimeVersionAttributes(version) {
135
- if (version === undefined || version === null)
136
- return {};
137
- const normalized = String(version).trim();
138
- if (!normalized)
139
- return {};
140
- return {
141
- 'agentic.runtime.version': normalized,
142
- };
143
- }
144
- function extractGatewayInputMetadata(text) {
145
- if (!text)
146
- return { input: '', attributes: {} };
147
- const match = text.match(GATEWAY_INPUT_METADATA_BLOCK_RE);
148
- if (!match)
149
- return { input: text.trim(), attributes: {} };
150
- const attributes = {};
151
- const metadataRaw = match[1];
152
- try {
153
- const parsed = JSON.parse(metadataRaw);
154
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
155
- attributes['agentic.input.metadata'] = JSON.stringify(parsed);
156
- if (parsed.message_id !== undefined)
157
- attributes['agentic.input.message_id'] = String(parsed.message_id);
158
- if (parsed.sender !== undefined)
159
- attributes['agentic.input.sender'] = String(parsed.sender);
160
- }
161
- }
162
- catch {
163
- // ignore malformed metadata JSON and only strip wrapper text
164
- }
165
- const input = text.slice(match.index + match[0].length).trim() || text.trim();
166
- return { input, attributes };
167
- }
168
- function normalizeUserInput(content) {
169
- const text = extractText(content).trim();
170
- if (text)
171
- return extractGatewayInputMetadata(text);
172
- if (Array.isArray(content))
173
- return { input: JSON.stringify(content), attributes: {} };
174
- if (typeof content === 'string')
175
- return extractGatewayInputMetadata(content);
176
- return { input: '', attributes: {} };
177
- }
178
- function extractToolResultText(block, message) {
179
- const raw = block?.content;
180
- if (typeof raw === 'string' && raw.trim())
181
- return raw;
182
- if (Array.isArray(raw)) {
183
- const text = extractText(raw).trim();
184
- if (text)
185
- return text;
186
- return JSON.stringify(raw);
187
- }
188
- if (raw && typeof raw === 'object')
189
- return JSON.stringify(raw);
190
- const stdout = message?.toolUseResult?.stdout;
191
- if (typeof stdout === 'string' && stdout.length > 0)
192
- return stdout;
193
- const stderr = message?.toolUseResult?.stderr;
194
- if (typeof stderr === 'string' && stderr.length > 0)
195
- return stderr;
196
- return '';
197
- }
198
- function parseSlashCommand(text) {
199
- const m = text.match(/<command-name>\s*\/([^<\s]+)\s*<\/command-name>/i);
200
- if (m) {
201
- const argsMatch = text.match(/<command-args>([\s\S]*?)<\/command-args>/i);
202
- return { name: m[1].trim(), args: argsMatch ? argsMatch[1].trim() : '' };
203
- }
204
- // fallback for plain slash commands like "/compact please summarize"
205
- const plain = String(text ?? '').trim().match(/^\/([a-zA-Z0-9._:-]+)(?:\s+([\s\S]*))?$/);
206
- if (!plain)
207
- return null;
208
- return {
209
- name: plain[1].trim(),
210
- args: plain[2] ? plain[2].trim() : '',
211
- };
212
- }
213
- function parseBashCommandAttributes(commandLine) {
214
- const raw = String(commandLine ?? '').trim();
215
- if (!raw) {
216
- return {
217
- 'agentic.command.name': '',
218
- 'agentic.command.args': '',
219
- 'agentic.command.pipe_count': 0,
220
- 'agentic.command.raw': '',
221
- };
222
- }
223
- const segments = raw.split(/\|(?!\|)/);
224
- const firstSegment = String(segments[0] ?? '').trim();
225
- const tokens = firstSegment ? firstSegment.split(/\s+/) : [];
226
- const name = String(tokens[0] ?? '').trim();
227
- const args = tokens.length > 1 ? tokens.slice(1).join(' ') : '';
228
- return {
229
- 'agentic.command.name': name,
230
- 'agentic.command.args': args,
231
- 'agentic.command.pipe_count': Math.max(segments.length - 1, 0),
232
- 'agentic.command.raw': raw,
233
- };
234
- }
235
- function isMcpToolName(name) {
236
- const n = String(name || '').toLowerCase();
237
- return n === 'mcp' || n.startsWith('mcp__') || n.startsWith('mcp-');
238
- }
239
- function hashText(text) {
240
- return createHash('sha256').update(String(text ?? '')).digest('hex');
241
- }
242
- function lineCount(text) {
243
- const s = String(text ?? '');
244
- if (!s)
245
- return 0;
246
- return s.split(/\r?\n/).length;
247
- }
248
- function tokenSet(text) {
249
- const tokens = String(text ?? '').trim().split(/\s+/).filter(Boolean);
250
- return new Set(tokens);
251
- }
252
- function similarityScore(a, b) {
253
- if (a === b)
254
- return 1;
255
- const setA = tokenSet(a);
256
- const setB = tokenSet(b);
257
- if (setA.size === 0 && setB.size === 0)
258
- return 1;
259
- if (setA.size === 0 || setB.size === 0)
260
- return 0;
261
- let intersection = 0;
262
- for (const token of setA) {
263
- if (setB.has(token))
264
- intersection += 1;
265
- }
266
- const union = setA.size + setB.size - intersection;
267
- if (union === 0)
268
- return 1;
269
- return Number((intersection / union).toFixed(6));
270
- }
271
- function actorHeuristic(messages) {
272
- const hasSidechainSignal = messages.some((m) => m?.isSidechain === true || (typeof m?.agentId === 'string' && m.agentId.trim().length > 0));
273
- if (hasSidechainSignal)
274
- return { role: 'unknown', confidence: 0.6 };
275
- return { role: 'main', confidence: 0.95 };
276
- }
277
- function firstNonEmptyString(values) {
278
- for (const value of values) {
279
- const text = String(value ?? '').trim();
280
- if (text)
281
- return text;
282
- }
283
- return '';
284
- }
285
- function inferParentLinkAttributes(messages) {
286
- const agentId = firstNonEmptyString(messages.map((m) => m?.agentId ?? m?.agent_id ?? m?.message?.agentId ?? m?.message?.agent_id));
287
- const parentSessionId = firstNonEmptyString(messages.map((m) => m?.parentSessionId
288
- ?? m?.parent_session_id
289
- ?? m?.parent?.sessionId
290
- ?? m?.parent?.session_id
291
- ?? m?.session_meta?.parent_session_id
292
- ?? m?.sessionMeta?.parentSessionId));
293
- const parentTurnId = firstNonEmptyString(messages.map((m) => m?.parentTurnId
294
- ?? m?.parent_turn_id
295
- ?? m?.parent?.turnId
296
- ?? m?.parent?.turn_id
297
- ?? m?.session_meta?.parent_turn_id
298
- ?? m?.sessionMeta?.parentTurnId));
299
- const parentToolCallId = firstNonEmptyString(messages.map((m) => m?.parentToolCallId
300
- ?? m?.parent_tool_call_id
301
- ?? m?.parent?.toolCallId
302
- ?? m?.parent?.tool_call_id
303
- ?? m?.session_meta?.parent_tool_call_id
304
- ?? m?.sessionMeta?.parentToolCallId));
305
- const explicitConfidence = firstNonEmptyString(messages.map((m) => m?.parentLinkConfidence ?? m?.parent_link_confidence));
306
- const attrs = {};
307
- if (agentId)
308
- attrs['agentic.agent_id'] = agentId;
309
- if (parentSessionId)
310
- attrs['agentic.parent.session_id'] = parentSessionId;
311
- if (parentTurnId)
312
- attrs['agentic.parent.turn_id'] = parentTurnId;
313
- if (parentToolCallId)
314
- attrs['agentic.parent.tool_call_id'] = parentToolCallId;
315
- if (explicitConfidence) {
316
- attrs['agentic.parent.link.confidence'] = explicitConfidence;
317
- }
318
- else if (parentSessionId || parentTurnId || parentToolCallId) {
319
- attrs['agentic.parent.link.confidence'] = 'exact';
320
- }
321
- else if (agentId) {
322
- attrs['agentic.parent.link.confidence'] = 'unknown';
323
- }
324
- return attrs;
325
- }
326
- function createTurn(messages, turnId, projectId, sessionId, runtime) {
327
- const user = messages.find(isPromptUserMessage) ?? messages.find((m) => m.role === 'user' && !m.isMeta) ?? messages[0];
328
- const assistantsRaw = messages.filter((m) => m.role === 'assistant');
329
- const assistantOrder = [];
330
- const assistantLatest = new Map();
331
- for (let i = 0; i < assistantsRaw.length; i += 1) {
332
- const msg = assistantsRaw[i];
333
- const key = msg.messageId ? `id:${msg.messageId}` : `idx:${i}`;
334
- if (!assistantLatest.has(key))
335
- assistantOrder.push(key);
336
- assistantLatest.set(key, msg);
337
- }
338
- const assistants = assistantOrder.map((key) => assistantLatest.get(key)).filter(Boolean);
339
- const start = user?.timestamp ?? messages[0]?.timestamp ?? new Date();
340
- const end = messages[messages.length - 1]?.timestamp ?? start;
341
- const output = assistants.map((m) => extractText(m.content)).filter(Boolean).join('\n');
342
- const runtimeVersion = [...messages]
343
- .map((m) => String(m.runtimeVersion ?? '').trim())
344
- .filter(Boolean)
345
- .at(-1);
346
- const runtimeAttrs = runtimeVersionAttributes(runtimeVersion);
347
- const normalizedInput = normalizeUserInput(user?.content);
348
- const totalUsage = {};
349
- let latestModel;
350
- for (const msg of assistants) {
351
- if (msg.model)
352
- latestModel = msg.model;
353
- addUsage(totalUsage, msg.usage);
354
- }
355
- const usage = Object.keys(totalUsage).length ? totalUsage : undefined;
356
- const actor = actorHeuristic(messages);
357
- const parentLinkAttrs = inferParentLinkAttributes(messages);
358
- const sharedAttrs = { ...runtimeAttrs, ...parentLinkAttrs };
359
- const events = [
360
- {
361
- runtime,
362
- projectId,
363
- sessionId,
364
- turnId,
365
- category: 'turn',
366
- name: `${runtime} - Turn ${turnId}`,
367
- startedAt: start.toISOString(),
368
- endedAt: end.toISOString(),
369
- input: normalizedInput.input,
370
- output,
371
- attributes: {
372
- 'agentic.event.category': 'turn',
373
- 'langfuse.observation.type': 'agent',
374
- 'gen_ai.operation.name': 'invoke_agent',
375
- ...sharedAttrs,
376
- ...modelAttributes(latestModel),
377
- 'agentic.actor.role': actor.role,
378
- 'agentic.actor.role_confidence': actor.confidence,
379
- ...normalizedInput.attributes,
380
- ...usageAttributes(usage),
381
- },
382
- },
383
- ];
384
- const resultByToolId = new Map();
385
- for (const msg of messages) {
386
- if (msg.role !== 'user')
387
- continue;
388
- const resultAt = isoFromUnknownTimestamp(msg.timestamp, end);
389
- for (const tr of extractToolResults(msg.content)) {
390
- const toolUseId = String(tr.tool_use_id ?? tr.toolUseId ?? '');
391
- if (!toolUseId)
392
- continue;
393
- const content = extractToolResultText(tr, msg);
394
- if (!resultByToolId.has(toolUseId) || !resultByToolId.get(toolUseId)?.content) {
395
- resultByToolId.set(toolUseId, { content, endedAt: resultAt });
396
- }
397
- }
398
- if (msg.sourceToolUseId) {
399
- const fallback = extractToolResultText({}, msg);
400
- if (fallback
401
- && (!resultByToolId.has(msg.sourceToolUseId) || !resultByToolId.get(msg.sourceToolUseId)?.content)) {
402
- resultByToolId.set(msg.sourceToolUseId, { content: fallback, endedAt: resultAt });
403
- }
404
- }
405
- }
406
- if (user?.role === 'user') {
407
- const slash = parseSlashCommand(extractText(user.content));
408
- if (slash) {
409
- const isMcp = slash.name.toLowerCase() === 'mcp' || slash.name.toLowerCase().startsWith('mcp:');
410
- events.push({
411
- runtime,
412
- projectId,
413
- sessionId,
414
- turnId,
415
- category: isMcp ? 'mcp' : 'agent_command',
416
- name: isMcp ? 'MCP Slash Command' : `Agent Command: /${slash.name}`,
417
- startedAt: start.toISOString(),
418
- endedAt: start.toISOString(),
419
- input: extractText(user.content),
420
- output: '',
421
- attributes: {
422
- 'agentic.event.category': isMcp ? 'mcp' : 'agent_command',
423
- 'langfuse.observation.type': isMcp ? 'tool' : 'event',
424
- ...sharedAttrs,
425
- 'agentic.command.name': slash.name,
426
- 'agentic.command.args': slash.args,
427
- 'gen_ai.operation.name': isMcp ? 'execute_tool' : 'invoke_agent',
428
- },
429
- });
430
- }
431
- }
432
- for (const assistant of assistants) {
433
- const reasoningBlocks = extractReasoningBlocks(assistant.content);
434
- for (const reasoning of reasoningBlocks) {
435
- const reasoningText = String(reasoning?.text ?? '').trim();
436
- if (!reasoningText)
437
- continue;
438
- const reasoningAt = isoFromUnknownTimestamp(reasoning?.timestamp, assistant.timestamp);
439
- events.push({
440
- runtime,
441
- projectId,
442
- sessionId,
443
- turnId,
444
- category: 'reasoning',
445
- name: 'Assistant Reasoning',
446
- startedAt: reasoningAt,
447
- endedAt: reasoningAt,
448
- input: '',
449
- output: reasoningText,
450
- attributes: {
451
- 'agentic.event.category': 'reasoning',
452
- 'langfuse.observation.type': 'span',
453
- ...sharedAttrs,
454
- 'gen_ai.operation.name': 'invoke_agent',
455
- ...modelAttributes(assistant.model),
456
- },
457
- });
458
- }
459
- const toolUses = extractToolUses(assistant.content);
460
- for (const tu of toolUses) {
461
- const toolName = String(tu.name ?? '');
462
- const toolId = String(tu.id ?? '');
463
- const toolInput = tu.input ?? {};
464
- const toolResult = resultByToolId.get(toolId);
465
- const toolOutput = toolResult?.content ?? '';
466
- const t = assistant.timestamp.toISOString();
467
- const toolEndedAt = toolResult?.endedAt ?? t;
468
- if (toolName === 'Bash') {
469
- const commandLine = String(toolInput.command ?? '');
470
- events.push({
471
- runtime,
472
- projectId,
473
- sessionId,
474
- turnId,
475
- category: 'shell_command',
476
- name: 'Tool: Bash',
477
- startedAt: t,
478
- endedAt: toolEndedAt,
479
- input: commandLine,
480
- output: toolOutput,
481
- attributes: {
482
- 'agentic.event.category': 'shell_command',
483
- 'langfuse.observation.type': 'tool',
484
- ...sharedAttrs,
485
- 'process.command_line': commandLine,
486
- ...parseBashCommandAttributes(commandLine),
487
- 'gen_ai.tool.name': 'Bash',
488
- 'gen_ai.tool.call.id': toolId,
489
- 'gen_ai.operation.name': 'execute_tool',
490
- ...modelAttributes(assistant.model),
491
- ...usageAttributes(assistant.usage),
492
- },
493
- });
494
- continue;
495
- }
496
- if (isMcpToolName(toolName)) {
497
- const serverName = toolName.startsWith('mcp__') ? toolName.split('__')[1] : undefined;
498
- events.push({
499
- runtime,
500
- projectId,
501
- sessionId,
502
- turnId,
503
- category: 'mcp',
504
- name: `Tool: ${toolName}`,
505
- startedAt: t,
506
- endedAt: toolEndedAt,
507
- input: JSON.stringify(toolInput),
508
- output: toolOutput,
509
- attributes: {
510
- 'agentic.event.category': 'mcp',
511
- 'langfuse.observation.type': 'tool',
512
- ...sharedAttrs,
513
- 'gen_ai.tool.name': toolName,
514
- 'mcp.request.id': toolId,
515
- 'gen_ai.operation.name': 'execute_tool',
516
- ...(serverName ? { 'agentic.mcp.server.name': serverName } : {}),
517
- ...modelAttributes(assistant.model),
518
- ...usageAttributes(assistant.usage),
519
- },
520
- });
521
- continue;
522
- }
523
- if (toolName === 'Task') {
524
- events.push({
525
- runtime,
526
- projectId,
527
- sessionId,
528
- turnId,
529
- category: 'agent_task',
530
- name: 'Tool: Task',
531
- startedAt: t,
532
- endedAt: toolEndedAt,
533
- input: JSON.stringify(toolInput),
534
- output: toolOutput,
535
- attributes: {
536
- 'agentic.event.category': 'agent_task',
537
- 'langfuse.observation.type': 'agent',
538
- ...sharedAttrs,
539
- 'gen_ai.tool.name': 'Task',
540
- 'gen_ai.tool.call.id': toolId,
541
- 'gen_ai.operation.name': 'invoke_agent',
542
- ...modelAttributes(assistant.model),
543
- ...usageAttributes(assistant.usage),
544
- },
545
- });
546
- continue;
547
- }
548
- if (toolName) {
549
- events.push({
550
- runtime,
551
- projectId,
552
- sessionId,
553
- turnId,
554
- category: 'tool',
555
- name: `Tool: ${toolName}`,
556
- startedAt: t,
557
- endedAt: toolEndedAt,
558
- input: JSON.stringify(toolInput),
559
- output: toolOutput,
560
- attributes: {
561
- 'agentic.event.category': 'tool',
562
- 'langfuse.observation.type': 'tool',
563
- ...sharedAttrs,
564
- 'gen_ai.tool.name': toolName,
565
- 'gen_ai.tool.call.id': toolId,
566
- 'gen_ai.operation.name': 'execute_tool',
567
- ...modelAttributes(assistant.model),
568
- ...usageAttributes(assistant.usage),
569
- },
570
- });
571
- }
572
- }
573
- }
574
- const turnInput = String(events[0].input ?? '').trim();
575
- const turnOutput = String(events[0].output ?? '').trim();
576
- if (!turnInput && !turnOutput && events.length === 1) {
577
- return [];
578
- }
579
- events[0].attributes['agentic.subagent.calls'] = events.filter((e) => e.category === 'agent_task').length;
580
- return events;
581
- }
582
- export function groupByTurns(messages) {
583
- const turns = [];
584
- let current = [];
585
- for (const msg of messages) {
586
- if (isPromptUserMessage(msg)) {
587
- if (current.length > 0)
588
- turns.push(current);
589
- current = [msg];
590
- continue;
591
- }
592
- if (current.length > 0)
593
- current.push(msg);
594
- }
595
- if (current.length > 0)
596
- turns.push(current);
597
- return turns;
598
- }
599
- export function normalizeTranscriptMessages({ runtime, projectId, sessionId, messages }) {
600
- const turns = groupByTurns(messages);
601
- const events = [];
602
- let previousInput = '';
603
- let previousHash = '';
604
- let hasPreviousTurn = false;
605
- for (let i = 0; i < turns.length; i += 1) {
606
- const turnEvents = createTurn(turns[i], `turn-${i + 1}`, projectId, sessionId, runtime);
607
- if (!turnEvents.length)
608
- continue;
609
- const turnEvent = turnEvents[0];
610
- const input = String(turnEvent.input ?? '');
611
- const inputHash = hashText(input);
612
- if (!hasPreviousTurn) {
613
- turnEvent.attributes['agentic.turn.input.hash'] = inputHash;
614
- turnEvent.attributes['agentic.turn.input.prev_hash'] = '';
615
- turnEvent.attributes['agentic.turn.diff.char_delta'] = 0;
616
- turnEvent.attributes['agentic.turn.diff.line_delta'] = 0;
617
- turnEvent.attributes['agentic.turn.diff.similarity'] = 1;
618
- turnEvent.attributes['agentic.turn.diff.changed'] = false;
619
- hasPreviousTurn = true;
620
- }
621
- else {
622
- turnEvent.attributes['agentic.turn.input.hash'] = inputHash;
623
- turnEvent.attributes['agentic.turn.input.prev_hash'] = previousHash;
624
- turnEvent.attributes['agentic.turn.diff.char_delta'] = input.length - previousInput.length;
625
- turnEvent.attributes['agentic.turn.diff.line_delta'] = lineCount(input) - lineCount(previousInput);
626
- turnEvent.attributes['agentic.turn.diff.similarity'] = similarityScore(previousInput, input);
627
- turnEvent.attributes['agentic.turn.diff.changed'] = inputHash !== previousHash;
628
- }
629
- previousInput = input;
630
- previousHash = inputHash;
631
- events.push(...turnEvents);
632
- }
633
- return events;
634
- }
635
- export function parseProjectIdFromTranscriptPath(transcriptPath, marker) {
636
- if (!transcriptPath)
637
- return undefined;
638
- const normalized = transcriptPath.replace(/\\/g, '/');
639
- const idx = normalized.indexOf(marker);
640
- if (idx === -1)
641
- return undefined;
642
- const rest = normalized.slice(idx + marker.length);
643
- return rest.split('/')[0] || undefined;
644
- }