@bububuger/spanory 0.1.16 → 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.
- package/dist/alert/evaluate.js +151 -0
- package/dist/env.d.ts +4 -0
- package/dist/env.js +40 -12
- package/dist/index.js +275 -37
- package/dist/otlp.d.ts +2 -2
- package/dist/report/aggregate.d.ts +1 -0
- package/dist/report/aggregate.js +128 -2
- package/dist/runtime/codex/adapter.js +1 -1
- package/dist/runtime/codex/proxy.js +4 -1
- package/dist/runtime/shared/normalize.js +387 -15
- package/package.json +8 -6
|
@@ -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.
|
|
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.
|
|
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) {
|
|
@@ -217,7 +209,6 @@ function parseBashCommandAttributes(commandLine) {
|
|
|
217
209
|
'agentic.command.name': '',
|
|
218
210
|
'agentic.command.args': '',
|
|
219
211
|
'agentic.command.pipe_count': 0,
|
|
220
|
-
'agentic.command.raw': '',
|
|
221
212
|
};
|
|
222
213
|
}
|
|
223
214
|
const segments = raw.split(/\|(?!\|)/);
|
|
@@ -229,7 +220,6 @@ function parseBashCommandAttributes(commandLine) {
|
|
|
229
220
|
'agentic.command.name': name,
|
|
230
221
|
'agentic.command.args': args,
|
|
231
222
|
'agentic.command.pipe_count': Math.max(segments.length - 1, 0),
|
|
232
|
-
'agentic.command.raw': raw,
|
|
233
223
|
};
|
|
234
224
|
}
|
|
235
225
|
function isMcpToolName(name) {
|
|
@@ -268,6 +258,368 @@ function similarityScore(a, b) {
|
|
|
268
258
|
return 1;
|
|
269
259
|
return Number((intersection / union).toFixed(6));
|
|
270
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
|
+
}
|
|
271
623
|
function actorHeuristic(messages) {
|
|
272
624
|
const hasSidechainSignal = messages.some((m) => m?.isSidechain === true || (typeof m?.agentId === 'string' && m.agentId.trim().length > 0));
|
|
273
625
|
if (hasSidechainSignal)
|
|
@@ -304,8 +656,9 @@ function inferParentLinkAttributes(messages) {
|
|
|
304
656
|
?? m?.sessionMeta?.parentToolCallId));
|
|
305
657
|
const explicitConfidence = firstNonEmptyString(messages.map((m) => m?.parentLinkConfidence ?? m?.parent_link_confidence));
|
|
306
658
|
const attrs = {};
|
|
307
|
-
if (agentId)
|
|
308
|
-
attrs['
|
|
659
|
+
if (agentId) {
|
|
660
|
+
attrs['gen_ai.agent.id'] = agentId;
|
|
661
|
+
}
|
|
309
662
|
if (parentSessionId)
|
|
310
663
|
attrs['agentic.parent.session_id'] = parentSessionId;
|
|
311
664
|
if (parentTurnId)
|
|
@@ -602,6 +955,9 @@ export function normalizeTranscriptMessages({ runtime, projectId, sessionId, mes
|
|
|
602
955
|
let previousInput = '';
|
|
603
956
|
let previousHash = '';
|
|
604
957
|
let hasPreviousTurn = false;
|
|
958
|
+
let previousEstimatedTotal = 0;
|
|
959
|
+
let recentSourceKindsWindow = [];
|
|
960
|
+
let calibrationState = { ema: 1, sampleCount: 0 };
|
|
605
961
|
for (let i = 0; i < turns.length; i += 1) {
|
|
606
962
|
const turnEvents = createTurn(turns[i], `turn-${i + 1}`, projectId, sessionId, runtime);
|
|
607
963
|
if (!turnEvents.length)
|
|
@@ -629,6 +985,22 @@ export function normalizeTranscriptMessages({ runtime, projectId, sessionId, mes
|
|
|
629
985
|
previousInput = input;
|
|
630
986
|
previousHash = inputHash;
|
|
631
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
|
+
}
|
|
632
1004
|
}
|
|
633
1005
|
return events;
|
|
634
1006
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bububuger/spanory",
|
|
3
|
-
"version": "0.1.
|
|
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": {
|