@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.
- package/dist/alert/evaluate.js +151 -0
- package/dist/env.d.ts +4 -0
- package/dist/env.js +40 -12
- package/dist/index.js +364 -38
- package/dist/issue/state.d.ts +39 -0
- package/dist/issue/state.js +151 -0
- 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/claude/adapter.js +151 -2
- package/dist/runtime/codex/adapter.js +39 -3
- package/dist/runtime/codex/proxy.js +4 -1
- package/dist/runtime/openclaw/adapter.js +140 -2
- package/dist/runtime/shared/file-settle.d.ts +19 -0
- package/dist/runtime/shared/file-settle.js +44 -0
- package/dist/runtime/shared/normalize.js +464 -18
- 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) {
|
|
@@ -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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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.
|
|
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": {
|