@bububuger/spanory 0.1.15 → 0.1.18

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,5 +1,6 @@
1
1
  // @ts-nocheck
2
2
  import { createHash } from 'node:crypto';
3
+ import { calibratedEstimate, calibrate, CONTEXT_SOURCE_KINDS, estimateTokens, pollutionScoreV1, } from '@bububuger/core';
3
4
  function toNumber(value) {
4
5
  const n = Number(value);
5
6
  return Number.isFinite(n) ? n : undefined;
@@ -48,24 +49,15 @@ function usageAttributes(usage) {
48
49
  attrs['gen_ai.usage.total_tokens'] = usage.total_tokens;
49
50
  }
50
51
  if (usage.cache_read_input_tokens !== undefined) {
51
- attrs['gen_ai.usage.details.cache_read_input_tokens'] = usage.cache_read_input_tokens;
52
+ attrs['gen_ai.usage.cache_read.input_tokens'] = usage.cache_read_input_tokens;
52
53
  }
53
54
  if (usage.cache_creation_input_tokens !== undefined) {
54
- attrs['gen_ai.usage.details.cache_creation_input_tokens'] = usage.cache_creation_input_tokens;
55
+ attrs['gen_ai.usage.cache_creation.input_tokens'] = usage.cache_creation_input_tokens;
55
56
  }
56
57
  const cacheRead = usage.cache_read_input_tokens ?? 0;
57
58
  const denominator = (usage.input_tokens ?? 0) + cacheRead;
58
59
  const cacheHitRate = denominator > 0 ? cacheRead / denominator : 0;
59
60
  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
61
  return attrs;
70
62
  }
71
63
  function modelAttributes(model) {
@@ -210,6 +202,26 @@ function parseSlashCommand(text) {
210
202
  args: plain[2] ? plain[2].trim() : '',
211
203
  };
212
204
  }
205
+ function parseBashCommandAttributes(commandLine) {
206
+ const raw = String(commandLine ?? '').trim();
207
+ if (!raw) {
208
+ return {
209
+ 'agentic.command.name': '',
210
+ 'agentic.command.args': '',
211
+ 'agentic.command.pipe_count': 0,
212
+ };
213
+ }
214
+ const segments = raw.split(/\|(?!\|)/);
215
+ const firstSegment = String(segments[0] ?? '').trim();
216
+ const tokens = firstSegment ? firstSegment.split(/\s+/) : [];
217
+ const name = String(tokens[0] ?? '').trim();
218
+ const args = tokens.length > 1 ? tokens.slice(1).join(' ') : '';
219
+ return {
220
+ 'agentic.command.name': name,
221
+ 'agentic.command.args': args,
222
+ 'agentic.command.pipe_count': Math.max(segments.length - 1, 0),
223
+ };
224
+ }
213
225
  function isMcpToolName(name) {
214
226
  const n = String(name || '').toLowerCase();
215
227
  return n === 'mcp' || n.startsWith('mcp__') || n.startsWith('mcp-');
@@ -246,12 +258,424 @@ function similarityScore(a, b) {
246
258
  return 1;
247
259
  return Number((intersection / union).toFixed(6));
248
260
  }
261
+ const DEFAULT_CONTEXT_WINDOW_TOKENS = 200000;
262
+ const CONTEXT_ENABLED_RUNTIMES = new Set(['claude-code', 'codex', 'openclaw', 'opencode']);
263
+ const CONTEXT_PARSING_ENABLED = process.env.SPANORY_CONTEXT_ENABLED !== '0';
264
+ function contextWindowTokens() {
265
+ const raw = Number(process.env.SPANORY_CONTEXT_WINDOW_TOKENS);
266
+ if (Number.isFinite(raw) && raw > 0)
267
+ return Math.floor(raw);
268
+ return DEFAULT_CONTEXT_WINDOW_TOKENS;
269
+ }
270
+ function clamp(value, min, max) {
271
+ return Math.max(min, Math.min(max, value));
272
+ }
273
+ function round6(value) {
274
+ return Number(value.toFixed(6));
275
+ }
276
+ function detectCompactInferred(currentTokens, previousTokens) {
277
+ if (!(previousTokens > 0))
278
+ return { detected: false, compactionRatio: 0 };
279
+ const dropRatio = (previousTokens - currentTokens) / previousTokens;
280
+ if (dropRatio > 0.4)
281
+ return { detected: true, compactionRatio: round6(dropRatio) };
282
+ return { detected: false, compactionRatio: 0 };
283
+ }
284
+ function createSourceDeltaMap() {
285
+ const out = {};
286
+ for (const kind of CONTEXT_SOURCE_KINDS)
287
+ out[kind] = 0;
288
+ return out;
289
+ }
290
+ function addSourceDelta(map, kind, delta) {
291
+ if (!kind || !Object.prototype.hasOwnProperty.call(map, kind))
292
+ return;
293
+ const n = Number(delta);
294
+ if (!Number.isFinite(n) || n <= 0)
295
+ return;
296
+ map[kind] += n;
297
+ }
298
+ function moveSourceDelta(map, fromKind, toKind, delta) {
299
+ const n = Number(delta);
300
+ if (!Number.isFinite(n) || n <= 0)
301
+ return;
302
+ if (!Object.prototype.hasOwnProperty.call(map, fromKind))
303
+ return;
304
+ if (!Object.prototype.hasOwnProperty.call(map, toKind))
305
+ return;
306
+ const moved = Math.min(map[fromKind], n);
307
+ if (moved <= 0)
308
+ return;
309
+ map[fromKind] -= moved;
310
+ map[toKind] += moved;
311
+ }
312
+ function parseJsonObject(raw) {
313
+ const text = String(raw ?? '').trim();
314
+ if (!text || !text.startsWith('{'))
315
+ return null;
316
+ try {
317
+ const parsed = JSON.parse(text);
318
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
319
+ return parsed;
320
+ }
321
+ catch {
322
+ // ignore parse errors
323
+ }
324
+ return null;
325
+ }
326
+ function extractMentionFileSignals(text) {
327
+ const input = String(text ?? '');
328
+ if (!input)
329
+ return [];
330
+ const matches = input.match(/(?:\/|\b)[^\s"'`]+?\.[a-zA-Z0-9]{1,8}\b/g) ?? [];
331
+ return [...new Set(matches.map((m) => m.trim()).filter(Boolean))];
332
+ }
333
+ function extractSourceName(event) {
334
+ const attrs = event?.attributes ?? {};
335
+ return String(attrs['gen_ai.tool.name'] ?? attrs['agentic.command.name'] ?? event?.name ?? '').trim();
336
+ }
337
+ function classifyContextSignals(turnEvents) {
338
+ const sourceDelta = createSourceDeltaMap();
339
+ const sourceNames = new Map();
340
+ let compactRequested = false;
341
+ let restoreRequested = false;
342
+ function markSourceName(kind, name) {
343
+ const text = String(name ?? '').trim();
344
+ if (!text)
345
+ return;
346
+ if (!sourceNames.has(kind))
347
+ sourceNames.set(kind, new Set());
348
+ sourceNames.get(kind).add(text);
349
+ }
350
+ function absorbFileSignals(text, fromKind) {
351
+ const mentions = extractMentionFileSignals(text);
352
+ if (!mentions.length)
353
+ return;
354
+ const mentionTokens = estimateTokens(mentions.join('\n'));
355
+ addSourceDelta(sourceDelta, 'mention_file', mentionTokens);
356
+ moveSourceDelta(sourceDelta, fromKind, 'mention_file', mentionTokens);
357
+ markSourceName('mention_file', mentions[0]);
358
+ const hasClaudeMd = mentions.some((item) => /(?:^|\/)(?:claude|agents)\.md$/i.test(item));
359
+ if (hasClaudeMd) {
360
+ addSourceDelta(sourceDelta, 'claude_md', mentionTokens);
361
+ moveSourceDelta(sourceDelta, 'mention_file', 'claude_md', mentionTokens);
362
+ markSourceName('claude_md', mentions.find((item) => /(?:^|\/)(?:claude|agents)\.md$/i.test(item)));
363
+ }
364
+ }
365
+ for (const event of turnEvents) {
366
+ const category = String(event?.category ?? '');
367
+ const attrs = event?.attributes ?? {};
368
+ const input = String(event?.input ?? '');
369
+ const output = String(event?.output ?? '');
370
+ const inputTokens = estimateTokens(input);
371
+ const outputTokens = estimateTokens(output);
372
+ const sourceName = extractSourceName(event);
373
+ if (category === 'turn') {
374
+ addSourceDelta(sourceDelta, 'turn', inputTokens + outputTokens);
375
+ absorbFileSignals(input, 'turn');
376
+ const sender = String(attrs['agentic.input.sender'] ?? '').toLowerCase();
377
+ if (sender === 'system') {
378
+ addSourceDelta(sourceDelta, 'system_prompt', inputTokens);
379
+ moveSourceDelta(sourceDelta, 'turn', 'system_prompt', inputTokens);
380
+ markSourceName('system_prompt', 'system_input');
381
+ }
382
+ if (/\b(delegate|handoff|coordinate|sync)\b/i.test(input)) {
383
+ const coordinationTokens = Math.min(estimateTokens(input), Math.max(1, Math.floor(inputTokens / 2)));
384
+ addSourceDelta(sourceDelta, 'team_coordination', coordinationTokens);
385
+ moveSourceDelta(sourceDelta, 'turn', 'team_coordination', coordinationTokens);
386
+ markSourceName('team_coordination', 'turn_coordination');
387
+ }
388
+ continue;
389
+ }
390
+ if (category === 'agent_command') {
391
+ const commandName = String(attrs['agentic.command.name'] ?? '').trim().toLowerCase();
392
+ addSourceDelta(sourceDelta, 'skill', inputTokens + outputTokens);
393
+ markSourceName('skill', sourceName ? `/${sourceName}` : 'slash_command');
394
+ if (commandName === 'compact')
395
+ compactRequested = true;
396
+ if (commandName === 'restore' || commandName === 'resume')
397
+ restoreRequested = true;
398
+ continue;
399
+ }
400
+ if (category === 'agent_task') {
401
+ addSourceDelta(sourceDelta, 'subagent', inputTokens + outputTokens);
402
+ addSourceDelta(sourceDelta, 'team_coordination', inputTokens);
403
+ markSourceName('subagent', sourceName || 'Task');
404
+ markSourceName('team_coordination', sourceName || 'Task');
405
+ continue;
406
+ }
407
+ if (category === 'tool' || category === 'mcp' || category === 'shell_command') {
408
+ addSourceDelta(sourceDelta, 'tool_input', inputTokens);
409
+ addSourceDelta(sourceDelta, 'tool_output', outputTokens);
410
+ if (sourceName) {
411
+ markSourceName('tool_input', sourceName);
412
+ markSourceName('tool_output', sourceName);
413
+ }
414
+ absorbFileSignals(input, 'tool_input');
415
+ const toolName = String(attrs['gen_ai.tool.name'] ?? '').toLowerCase();
416
+ if (toolName.includes('memory') || /\bmemory\b/i.test(input)) {
417
+ const memoryTokens = Math.max(1, Math.floor(inputTokens * 0.6));
418
+ addSourceDelta(sourceDelta, 'memory', memoryTokens);
419
+ moveSourceDelta(sourceDelta, 'tool_input', 'memory', memoryTokens);
420
+ markSourceName('memory', sourceName || 'memory');
421
+ }
422
+ continue;
423
+ }
424
+ }
425
+ const knownTotal = CONTEXT_SOURCE_KINDS
426
+ .filter((kind) => kind !== 'unknown')
427
+ .reduce((acc, kind) => acc + Number(sourceDelta[kind] ?? 0), 0);
428
+ if (knownTotal <= 0) {
429
+ sourceDelta.unknown = 1;
430
+ markSourceName('unknown', 'unclassified');
431
+ }
432
+ return { sourceDelta, sourceNames, compactRequested, restoreRequested };
433
+ }
434
+ function composeContextEvents({ runtime, projectId, sessionId, turnEvent, turnEvents, previousEstimatedTotal, recentSourceKindsWindow, calibrationState, }) {
435
+ const attrs = turnEvent?.attributes ?? {};
436
+ const usageInput = Number(attrs['gen_ai.usage.input_tokens'] ?? 0);
437
+ const usageCacheRead = Number(attrs['gen_ai.usage.cache_read.input_tokens'] ?? 0);
438
+ const usageEstimated = Math.max(0, usageInput + usageCacheRead);
439
+ const fallbackEstimated = estimateTokens(`${turnEvent?.input ?? ''}\n${turnEvent?.output ?? ''}`);
440
+ let estimationMethod = 'heuristic';
441
+ let estimationConfidence = 0.4;
442
+ let estimatedTotalTokens = Math.max(0, fallbackEstimated);
443
+ let nextCalibrationState = calibrationState;
444
+ if (usageEstimated > 0) {
445
+ estimationMethod = 'usage';
446
+ estimationConfidence = 1;
447
+ estimatedTotalTokens = usageEstimated;
448
+ nextCalibrationState = calibrate(calibrationState, usageEstimated, Math.max(fallbackEstimated, 1));
449
+ }
450
+ else if (Number(calibrationState?.sampleCount ?? 0) >= 2) {
451
+ estimationMethod = 'calibrated';
452
+ estimationConfidence = round6(0.7 + 0.03 * Math.min(Number(calibrationState.sampleCount ?? 0), 10));
453
+ estimatedTotalTokens = Math.max(0, calibratedEstimate(fallbackEstimated, calibrationState));
454
+ }
455
+ const { sourceDelta, sourceNames, compactRequested, restoreRequested } = classifyContextSignals(turnEvents);
456
+ const deltaTokens = previousEstimatedTotal > 0 ? (estimatedTotalTokens - previousEstimatedTotal) : 0;
457
+ const windowLimitTokens = contextWindowTokens();
458
+ const fillRatio = round6(clamp(estimatedTotalTokens / windowLimitTokens, 0, 1));
459
+ const compositionEntries = Object.entries(sourceDelta)
460
+ .filter(([, value]) => Number(value) > 0)
461
+ .sort((a, b) => Number(b[1]) - Number(a[1]));
462
+ const activeKinds = compositionEntries.map(([kind]) => kind);
463
+ const nextRecentSourceKindsWindow = [...recentSourceKindsWindow, activeKinds].slice(-5);
464
+ const repeatCountByKind = {};
465
+ for (const kind of CONTEXT_SOURCE_KINDS) {
466
+ repeatCountByKind[kind] = nextRecentSourceKindsWindow.reduce((count, row) => count + (row.includes(kind) ? 1 : 0), 0);
467
+ }
468
+ const totalDelta = compositionEntries.reduce((sum, [, value]) => sum + Number(value), 0);
469
+ const composition = Object.fromEntries(compositionEntries);
470
+ const topSources = compositionEntries.slice(0, 3).map(([kind]) => kind);
471
+ const passthroughAttrs = {};
472
+ const passthroughKeys = [
473
+ 'gen_ai.agent.id',
474
+ 'agentic.parent.session_id',
475
+ 'agentic.parent.turn_id',
476
+ 'agentic.parent.tool_call_id',
477
+ 'agentic.parent.link.confidence',
478
+ 'agentic.runtime.version',
479
+ ];
480
+ for (const key of passthroughKeys) {
481
+ if (attrs[key] !== undefined)
482
+ passthroughAttrs[key] = attrs[key];
483
+ }
484
+ const baseAttrs = {
485
+ 'agentic.event.category': 'context',
486
+ 'agentic.context.event_type': 'context_snapshot',
487
+ 'agentic.context.estimated_total_tokens': estimatedTotalTokens,
488
+ 'agentic.context.fill_ratio': fillRatio,
489
+ 'agentic.context.delta_tokens': deltaTokens,
490
+ 'agentic.context.composition': JSON.stringify(composition),
491
+ 'agentic.context.top_sources': JSON.stringify(topSources),
492
+ 'agentic.context.estimation_method': estimationMethod,
493
+ 'agentic.context.estimation_confidence': estimationConfidence,
494
+ ...passthroughAttrs,
495
+ };
496
+ const out = [
497
+ {
498
+ runtime,
499
+ projectId,
500
+ sessionId,
501
+ turnId: turnEvent.turnId,
502
+ category: 'context',
503
+ name: 'Context Snapshot',
504
+ startedAt: turnEvent.startedAt,
505
+ endedAt: turnEvent.endedAt,
506
+ input: '',
507
+ output: '',
508
+ attributes: baseAttrs,
509
+ },
510
+ ];
511
+ if (compactRequested && previousEstimatedTotal > 0) {
512
+ out.push({
513
+ runtime,
514
+ projectId,
515
+ sessionId,
516
+ turnId: turnEvent.turnId,
517
+ category: 'context',
518
+ name: 'Context Boundary',
519
+ startedAt: turnEvent.startedAt,
520
+ endedAt: turnEvent.startedAt,
521
+ input: '',
522
+ output: '',
523
+ attributes: {
524
+ 'agentic.event.category': 'context',
525
+ 'agentic.context.event_type': 'context_boundary',
526
+ 'agentic.context.boundary_kind': 'compact_before',
527
+ 'agentic.context.compaction_ratio': 0,
528
+ 'agentic.context.detection_method': 'hook',
529
+ ...passthroughAttrs,
530
+ },
531
+ });
532
+ }
533
+ const inferred = detectCompactInferred(estimatedTotalTokens, previousEstimatedTotal);
534
+ if (inferred.detected) {
535
+ out.push({
536
+ runtime,
537
+ projectId,
538
+ sessionId,
539
+ turnId: turnEvent.turnId,
540
+ category: 'context',
541
+ name: 'Context Boundary',
542
+ startedAt: turnEvent.startedAt,
543
+ endedAt: turnEvent.endedAt,
544
+ input: '',
545
+ output: '',
546
+ attributes: {
547
+ 'agentic.event.category': 'context',
548
+ 'agentic.context.event_type': 'context_boundary',
549
+ 'agentic.context.boundary_kind': 'compact_after',
550
+ 'agentic.context.compaction_ratio': inferred.compactionRatio,
551
+ 'agentic.context.detection_method': 'inferred',
552
+ ...passthroughAttrs,
553
+ },
554
+ });
555
+ }
556
+ if (restoreRequested) {
557
+ out.push({
558
+ runtime,
559
+ projectId,
560
+ sessionId,
561
+ turnId: turnEvent.turnId,
562
+ category: 'context',
563
+ name: 'Context Boundary',
564
+ startedAt: turnEvent.startedAt,
565
+ endedAt: turnEvent.startedAt,
566
+ input: '',
567
+ output: '',
568
+ attributes: {
569
+ 'agentic.event.category': 'context',
570
+ 'agentic.context.event_type': 'context_boundary',
571
+ 'agentic.context.boundary_kind': 'restore',
572
+ 'agentic.context.compaction_ratio': 0,
573
+ 'agentic.context.detection_method': 'hook',
574
+ ...passthroughAttrs,
575
+ },
576
+ });
577
+ }
578
+ for (const [kind, value] of compositionEntries) {
579
+ const tokenDelta = Number(value);
580
+ if (tokenDelta <= 0)
581
+ continue;
582
+ const sourceShare = totalDelta > 0 ? round6(tokenDelta / totalDelta) : 0;
583
+ const repeatCountRecent = Number(repeatCountByKind[kind] ?? 0);
584
+ const names = [...(sourceNames.get(kind) ?? [])];
585
+ out.push({
586
+ runtime,
587
+ projectId,
588
+ sessionId,
589
+ turnId: turnEvent.turnId,
590
+ category: 'context',
591
+ name: 'Context Source Attribution',
592
+ startedAt: turnEvent.startedAt,
593
+ endedAt: turnEvent.endedAt,
594
+ input: '',
595
+ output: '',
596
+ attributes: {
597
+ 'agentic.event.category': 'context',
598
+ 'agentic.context.event_type': 'context_source_attribution',
599
+ 'agentic.context.source_kind': kind,
600
+ 'agentic.context.source_name': names[0] ?? kind,
601
+ 'agentic.context.token_delta': tokenDelta,
602
+ 'agentic.context.source_share': sourceShare,
603
+ 'agentic.context.repeat_count_recent': repeatCountRecent,
604
+ 'agentic.context.pollution_score': pollutionScoreV1({
605
+ tokenDelta,
606
+ windowLimitTokens,
607
+ sourceShare,
608
+ repeatCountRecent,
609
+ sourceKind: kind,
610
+ }),
611
+ 'agentic.context.score_version': 'pollution_score_v1',
612
+ ...passthroughAttrs,
613
+ },
614
+ });
615
+ }
616
+ return {
617
+ events: out,
618
+ estimatedTotalTokens,
619
+ recentSourceKindsWindow: nextRecentSourceKindsWindow,
620
+ calibrationState: nextCalibrationState,
621
+ };
622
+ }
249
623
  function actorHeuristic(messages) {
250
624
  const hasSidechainSignal = messages.some((m) => m?.isSidechain === true || (typeof m?.agentId === 'string' && m.agentId.trim().length > 0));
251
625
  if (hasSidechainSignal)
252
626
  return { role: 'unknown', confidence: 0.6 };
253
627
  return { role: 'main', confidence: 0.95 };
254
628
  }
629
+ function firstNonEmptyString(values) {
630
+ for (const value of values) {
631
+ const text = String(value ?? '').trim();
632
+ if (text)
633
+ return text;
634
+ }
635
+ return '';
636
+ }
637
+ function inferParentLinkAttributes(messages) {
638
+ const agentId = firstNonEmptyString(messages.map((m) => m?.agentId ?? m?.agent_id ?? m?.message?.agentId ?? m?.message?.agent_id));
639
+ const parentSessionId = firstNonEmptyString(messages.map((m) => m?.parentSessionId
640
+ ?? m?.parent_session_id
641
+ ?? m?.parent?.sessionId
642
+ ?? m?.parent?.session_id
643
+ ?? m?.session_meta?.parent_session_id
644
+ ?? m?.sessionMeta?.parentSessionId));
645
+ const parentTurnId = firstNonEmptyString(messages.map((m) => m?.parentTurnId
646
+ ?? m?.parent_turn_id
647
+ ?? m?.parent?.turnId
648
+ ?? m?.parent?.turn_id
649
+ ?? m?.session_meta?.parent_turn_id
650
+ ?? m?.sessionMeta?.parentTurnId));
651
+ const parentToolCallId = firstNonEmptyString(messages.map((m) => m?.parentToolCallId
652
+ ?? m?.parent_tool_call_id
653
+ ?? m?.parent?.toolCallId
654
+ ?? m?.parent?.tool_call_id
655
+ ?? m?.session_meta?.parent_tool_call_id
656
+ ?? m?.sessionMeta?.parentToolCallId));
657
+ const explicitConfidence = firstNonEmptyString(messages.map((m) => m?.parentLinkConfidence ?? m?.parent_link_confidence));
658
+ const attrs = {};
659
+ if (agentId) {
660
+ attrs['gen_ai.agent.id'] = agentId;
661
+ }
662
+ if (parentSessionId)
663
+ attrs['agentic.parent.session_id'] = parentSessionId;
664
+ if (parentTurnId)
665
+ attrs['agentic.parent.turn_id'] = parentTurnId;
666
+ if (parentToolCallId)
667
+ attrs['agentic.parent.tool_call_id'] = parentToolCallId;
668
+ if (explicitConfidence) {
669
+ attrs['agentic.parent.link.confidence'] = explicitConfidence;
670
+ }
671
+ else if (parentSessionId || parentTurnId || parentToolCallId) {
672
+ attrs['agentic.parent.link.confidence'] = 'exact';
673
+ }
674
+ else if (agentId) {
675
+ attrs['agentic.parent.link.confidence'] = 'unknown';
676
+ }
677
+ return attrs;
678
+ }
255
679
  function createTurn(messages, turnId, projectId, sessionId, runtime) {
256
680
  const user = messages.find(isPromptUserMessage) ?? messages.find((m) => m.role === 'user' && !m.isMeta) ?? messages[0];
257
681
  const assistantsRaw = messages.filter((m) => m.role === 'assistant');
@@ -283,6 +707,8 @@ function createTurn(messages, turnId, projectId, sessionId, runtime) {
283
707
  }
284
708
  const usage = Object.keys(totalUsage).length ? totalUsage : undefined;
285
709
  const actor = actorHeuristic(messages);
710
+ const parentLinkAttrs = inferParentLinkAttributes(messages);
711
+ const sharedAttrs = { ...runtimeAttrs, ...parentLinkAttrs };
286
712
  const events = [
287
713
  {
288
714
  runtime,
@@ -299,7 +725,7 @@ function createTurn(messages, turnId, projectId, sessionId, runtime) {
299
725
  'agentic.event.category': 'turn',
300
726
  'langfuse.observation.type': 'agent',
301
727
  'gen_ai.operation.name': 'invoke_agent',
302
- ...runtimeAttrs,
728
+ ...sharedAttrs,
303
729
  ...modelAttributes(latestModel),
304
730
  'agentic.actor.role': actor.role,
305
731
  'agentic.actor.role_confidence': actor.confidence,
@@ -348,7 +774,7 @@ function createTurn(messages, turnId, projectId, sessionId, runtime) {
348
774
  attributes: {
349
775
  'agentic.event.category': isMcp ? 'mcp' : 'agent_command',
350
776
  'langfuse.observation.type': isMcp ? 'tool' : 'event',
351
- ...runtimeAttrs,
777
+ ...sharedAttrs,
352
778
  'agentic.command.name': slash.name,
353
779
  'agentic.command.args': slash.args,
354
780
  'gen_ai.operation.name': isMcp ? 'execute_tool' : 'invoke_agent',
@@ -377,7 +803,7 @@ function createTurn(messages, turnId, projectId, sessionId, runtime) {
377
803
  attributes: {
378
804
  'agentic.event.category': 'reasoning',
379
805
  'langfuse.observation.type': 'span',
380
- ...runtimeAttrs,
806
+ ...sharedAttrs,
381
807
  'gen_ai.operation.name': 'invoke_agent',
382
808
  ...modelAttributes(assistant.model),
383
809
  },
@@ -408,8 +834,9 @@ function createTurn(messages, turnId, projectId, sessionId, runtime) {
408
834
  attributes: {
409
835
  'agentic.event.category': 'shell_command',
410
836
  'langfuse.observation.type': 'tool',
411
- ...runtimeAttrs,
837
+ ...sharedAttrs,
412
838
  'process.command_line': commandLine,
839
+ ...parseBashCommandAttributes(commandLine),
413
840
  'gen_ai.tool.name': 'Bash',
414
841
  'gen_ai.tool.call.id': toolId,
415
842
  'gen_ai.operation.name': 'execute_tool',
@@ -435,7 +862,7 @@ function createTurn(messages, turnId, projectId, sessionId, runtime) {
435
862
  attributes: {
436
863
  'agentic.event.category': 'mcp',
437
864
  'langfuse.observation.type': 'tool',
438
- ...runtimeAttrs,
865
+ ...sharedAttrs,
439
866
  'gen_ai.tool.name': toolName,
440
867
  'mcp.request.id': toolId,
441
868
  'gen_ai.operation.name': 'execute_tool',
@@ -461,7 +888,7 @@ function createTurn(messages, turnId, projectId, sessionId, runtime) {
461
888
  attributes: {
462
889
  'agentic.event.category': 'agent_task',
463
890
  'langfuse.observation.type': 'agent',
464
- ...runtimeAttrs,
891
+ ...sharedAttrs,
465
892
  'gen_ai.tool.name': 'Task',
466
893
  'gen_ai.tool.call.id': toolId,
467
894
  'gen_ai.operation.name': 'invoke_agent',
@@ -486,7 +913,7 @@ function createTurn(messages, turnId, projectId, sessionId, runtime) {
486
913
  attributes: {
487
914
  'agentic.event.category': 'tool',
488
915
  'langfuse.observation.type': 'tool',
489
- ...runtimeAttrs,
916
+ ...sharedAttrs,
490
917
  'gen_ai.tool.name': toolName,
491
918
  'gen_ai.tool.call.id': toolId,
492
919
  'gen_ai.operation.name': 'execute_tool',
@@ -528,6 +955,9 @@ export function normalizeTranscriptMessages({ runtime, projectId, sessionId, mes
528
955
  let previousInput = '';
529
956
  let previousHash = '';
530
957
  let hasPreviousTurn = false;
958
+ let previousEstimatedTotal = 0;
959
+ let recentSourceKindsWindow = [];
960
+ let calibrationState = { ema: 1, sampleCount: 0 };
531
961
  for (let i = 0; i < turns.length; i += 1) {
532
962
  const turnEvents = createTurn(turns[i], `turn-${i + 1}`, projectId, sessionId, runtime);
533
963
  if (!turnEvents.length)
@@ -555,6 +985,22 @@ export function normalizeTranscriptMessages({ runtime, projectId, sessionId, mes
555
985
  previousInput = input;
556
986
  previousHash = inputHash;
557
987
  events.push(...turnEvents);
988
+ if (CONTEXT_PARSING_ENABLED && CONTEXT_ENABLED_RUNTIMES.has(runtime)) {
989
+ const contextResult = composeContextEvents({
990
+ runtime,
991
+ projectId,
992
+ sessionId,
993
+ turnEvent,
994
+ turnEvents,
995
+ previousEstimatedTotal,
996
+ recentSourceKindsWindow,
997
+ calibrationState,
998
+ });
999
+ events.push(...contextResult.events);
1000
+ previousEstimatedTotal = contextResult.estimatedTotalTokens;
1001
+ recentSourceKindsWindow = contextResult.recentSourceKindsWindow;
1002
+ calibrationState = contextResult.calibrationState;
1003
+ }
558
1004
  }
559
1005
  return events;
560
1006
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bububuger/spanory",
3
- "version": "0.1.15",
3
+ "version": "0.1.18",
4
4
  "description": "Spanory CLI for cross-runtime agent observability",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,7 +10,8 @@
10
10
  "directory": "packages/cli"
11
11
  },
12
12
  "publishConfig": {
13
- "access": "public"
13
+ "access": "public",
14
+ "registry": "https://registry.npmjs.org"
14
15
  },
15
16
  "files": [
16
17
  "dist"
@@ -19,18 +20,19 @@
19
20
  "spanory": "dist/index.js"
20
21
  },
21
22
  "scripts": {
22
- "check": "tsc -p tsconfig.runtime.json --noEmit",
23
- "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.runtime.json",
24
- "test": "vitest run test/unit",
23
+ "check": "npm run --workspace @bububuger/core build && tsc -p tsconfig.runtime.json --noEmit",
24
+ "build": "npm run --workspace @bububuger/core build && node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.runtime.json && chmod +x dist/index.js",
25
+ "test": "npm run build && vitest run test/unit",
25
26
  "test:bdd": "npm run build && vitest run test/bdd",
26
27
  "test:golden:update": "npm run build && node test/fixtures/golden/otlp/update-golden.mjs && node test/fixtures/golden/codex/update-golden.mjs",
27
- "build:bundle": "npm run --workspace @bububuger/backend-langfuse build && npm run --workspace @bububuger/otlp-core build && npm run build && mkdir -p build && esbuild dist/index.js --bundle --platform=node --format=cjs --target=node18 --outfile=build/spanory.cjs",
28
+ "build:bundle": "npm run --workspace @bububuger/backend-langfuse build && npm run --workspace @bububuger/otlp-core build && npm run build && mkdir -p build && esbuild dist/index.js --bundle --platform=node --format=cjs --target=node18 --define:process.env.SPANORY_VERSION=\\\"$npm_package_version\\\" --outfile=build/spanory.cjs",
28
29
  "build:bin:macos-arm64": "npm run build:bundle && pkg build/spanory.cjs --targets node18-macos-arm64 --output ../../dist/spanory-macos-arm64",
29
30
  "build:bin:macos-x64": "npm run build:bundle && pkg build/spanory.cjs --targets node18-macos-x64 --output ../../dist/spanory-macos-x64",
30
31
  "build:bin:linux-x64": "npm run build:bundle && pkg build/spanory.cjs --targets node18-linux-x64 --output ../../dist/spanory-linux-x64",
31
32
  "build:bin:win-x64": "npm run build:bundle && pkg build/spanory.cjs --targets node18-win-x64 --output ../../dist/spanory-win-x64.exe"
32
33
  },
33
34
  "dependencies": {
35
+ "@bububuger/core": "^0.1.1",
34
36
  "commander": "^14.0.1"
35
37
  },
36
38
  "devDependencies": {