@rigour-labs/core 6.0.0 → 6.1.0-beta.1

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.
Files changed (48) hide show
  1. package/dist/context/automatic-index.d.ts +18 -0
  2. package/dist/context/automatic-index.js +142 -0
  3. package/dist/context/automatic-index.test.d.ts +1 -0
  4. package/dist/context/automatic-index.test.js +45 -0
  5. package/dist/context/cache-engine.d.ts +25 -0
  6. package/dist/context/dependency-graph.d.ts +13 -0
  7. package/dist/context/dependency-graph.js +97 -0
  8. package/dist/context/dependency-graph.test.d.ts +1 -0
  9. package/dist/context/dependency-graph.test.js +13 -0
  10. package/dist/context/index-bridge.js +21 -2
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.js +7 -0
  13. package/dist/pattern-index/embeddings.js +10 -3
  14. package/dist/pattern-index/indexer.d.ts +2 -0
  15. package/dist/pattern-index/indexer.js +72 -0
  16. package/dist/services/agent-history.d.ts +76 -0
  17. package/dist/services/agent-history.js +201 -0
  18. package/dist/services/agent-history.test.d.ts +1 -0
  19. package/dist/services/agent-history.test.js +69 -0
  20. package/dist/services/context-telemetry-service.d.ts +21 -0
  21. package/dist/services/context-telemetry-service.js +36 -7
  22. package/dist/services/context-telemetry-service.test.js +13 -0
  23. package/dist/services/engineering-knowledge-graph.d.ts +55 -0
  24. package/dist/services/engineering-knowledge-graph.js +177 -0
  25. package/dist/services/engineering-knowledge-graph.test.d.ts +1 -0
  26. package/dist/services/engineering-knowledge-graph.test.js +59 -0
  27. package/dist/services/model-pricing.d.ts +2 -0
  28. package/dist/services/model-pricing.js +2 -0
  29. package/dist/services/system-health.d.ts +34 -0
  30. package/dist/services/system-health.js +53 -0
  31. package/dist/storage/db.js +130 -1
  32. package/dist/storage/index.d.ts +4 -0
  33. package/dist/storage/index.js +2 -0
  34. package/dist/storage/lessons.d.ts +60 -0
  35. package/dist/storage/lessons.js +322 -0
  36. package/dist/storage/lessons.test.d.ts +1 -0
  37. package/dist/storage/lessons.test.js +50 -0
  38. package/dist/storage/local-encryption.d.ts +2 -0
  39. package/dist/storage/local-encryption.js +41 -0
  40. package/dist/storage/team-store.d.ts +75 -0
  41. package/dist/storage/team-store.js +321 -0
  42. package/dist/storage/team-store.test.d.ts +1 -0
  43. package/dist/storage/team-store.test.js +11 -0
  44. package/dist/storage/team-vector-store.d.ts +25 -0
  45. package/dist/storage/team-vector-store.js +183 -0
  46. package/dist/storage/team-vector-store.test.d.ts +1 -0
  47. package/dist/storage/team-vector-store.test.js +42 -0
  48. package/package.json +8 -6
@@ -0,0 +1,76 @@
1
+ export type AgentEventOutcome = 'success' | 'error' | 'rejected' | 'unknown';
2
+ export interface AgentGuidance {
3
+ kind: 'context-scope' | 'pattern' | 'memory';
4
+ recommendation: string;
5
+ patternRefs: Array<{
6
+ id: string;
7
+ label: string;
8
+ file?: string;
9
+ }>;
10
+ lessonRefs: Array<{
11
+ id: string;
12
+ label: string;
13
+ visibility?: string;
14
+ }>;
15
+ memoryRefs: Array<{
16
+ id: string;
17
+ label: string;
18
+ }>;
19
+ selectedFiles: string[];
20
+ excludedFileCount: number;
21
+ conflicts: number;
22
+ }
23
+ export interface AgentImpact {
24
+ guidanceCount: number;
25
+ knowledgeItems: number;
26
+ candidateTokens: number;
27
+ returnedTokens: number;
28
+ avoidedTokens: number;
29
+ candidateFiles: number;
30
+ returnedFiles: number;
31
+ excludedFiles: number;
32
+ cacheHits: number;
33
+ recommendations: string[];
34
+ }
35
+ export interface AgentHistoryEvent {
36
+ id: string;
37
+ timestamp: string;
38
+ type: string;
39
+ agentId: string;
40
+ taskId?: string;
41
+ sessionId?: string;
42
+ requestId?: string;
43
+ tool?: string;
44
+ outcome: AgentEventOutcome;
45
+ files: string[];
46
+ summary: string;
47
+ guidance?: AgentGuidance;
48
+ telemetry?: {
49
+ candidateTokens: number;
50
+ returnedTokens: number;
51
+ candidateFiles: number;
52
+ returnedFiles: number;
53
+ cacheStatus: string;
54
+ classification: 'observed' | 'measured estimate' | 'modelled estimate';
55
+ };
56
+ }
57
+ export interface AgentRun {
58
+ id: string;
59
+ agentId: string;
60
+ taskId?: string;
61
+ sessionId?: string;
62
+ startedAt: string;
63
+ endedAt: string;
64
+ status: AgentEventOutcome | 'active';
65
+ eventCount: number;
66
+ files: string[];
67
+ tools: string[];
68
+ impact?: AgentImpact;
69
+ }
70
+ export declare function normalizeAgentEvent(input: unknown, index?: number): AgentHistoryEvent;
71
+ /**
72
+ * Normalizes a page as a unit so a response inherits identity and scope from its
73
+ * matching request. Older event logs often stored those fields only on tool_call.
74
+ */
75
+ export declare function normalizeAgentEvents(inputs: unknown[]): AgentHistoryEvent[];
76
+ export declare function buildAgentRuns(events: AgentHistoryEvent[]): AgentRun[];
@@ -0,0 +1,201 @@
1
+ function text(value) {
2
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
3
+ }
4
+ function validDate(value) {
5
+ const candidate = text(value);
6
+ return candidate && !Number.isNaN(Date.parse(candidate)) ? candidate : new Date(0).toISOString();
7
+ }
8
+ function strings(value) {
9
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.length > 0) : [];
10
+ }
11
+ function number(value) {
12
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
13
+ }
14
+ function refs(value) {
15
+ if (!Array.isArray(value))
16
+ return [];
17
+ return value.flatMap(item => {
18
+ if (!item || typeof item !== 'object')
19
+ return [];
20
+ const record = item;
21
+ const id = text(record.id);
22
+ const label = text(record.label);
23
+ return id && label ? [{ id, label, file: text(record.file), visibility: text(record.visibility) }] : [];
24
+ });
25
+ }
26
+ function eventGuidance(event) {
27
+ const value = event.guidance ?? event._guidance;
28
+ if (!value || typeof value !== 'object')
29
+ return undefined;
30
+ const record = value;
31
+ const recommendation = text(record.recommendation);
32
+ const kind = text(record.kind);
33
+ if (!recommendation || !['context-scope', 'pattern', 'memory'].includes(kind ?? ''))
34
+ return undefined;
35
+ return {
36
+ kind: kind,
37
+ recommendation,
38
+ patternRefs: refs(record.patternRefs),
39
+ lessonRefs: refs(record.lessonRefs),
40
+ memoryRefs: refs(record.memoryRefs),
41
+ selectedFiles: strings(record.selectedFiles),
42
+ excludedFileCount: number(record.excludedFileCount),
43
+ conflicts: number(record.conflicts),
44
+ };
45
+ }
46
+ function eventTelemetry(event) {
47
+ const value = event.telemetry ?? event._telemetry;
48
+ if (!value || typeof value !== 'object')
49
+ return undefined;
50
+ const record = value;
51
+ const classification = text(record.classification);
52
+ return {
53
+ candidateTokens: number(record.candidateTokens),
54
+ returnedTokens: number(record.returnedTokens),
55
+ candidateFiles: number(record.candidateFiles),
56
+ returnedFiles: number(record.returnedFiles),
57
+ cacheStatus: text(record.cacheStatus) ?? 'none',
58
+ classification: classification === 'observed' || classification === 'modelled estimate'
59
+ ? classification
60
+ : 'measured estimate',
61
+ };
62
+ }
63
+ function emptyImpact() {
64
+ return {
65
+ guidanceCount: 0,
66
+ knowledgeItems: 0,
67
+ candidateTokens: 0,
68
+ returnedTokens: 0,
69
+ avoidedTokens: 0,
70
+ candidateFiles: 0,
71
+ returnedFiles: 0,
72
+ excludedFiles: 0,
73
+ cacheHits: 0,
74
+ recommendations: [],
75
+ };
76
+ }
77
+ function eventFiles(event) {
78
+ const args = event.arguments && typeof event.arguments === 'object' ? event.arguments : {};
79
+ const report = event._rigour_report && typeof event._rigour_report === 'object'
80
+ ? event._rigour_report
81
+ : {};
82
+ const failures = Array.isArray(report.failures) ? report.failures : [];
83
+ const reported = failures.flatMap((failure) => failure && typeof failure === 'object'
84
+ ? strings(failure.files)
85
+ : []);
86
+ return [...new Set([
87
+ ...strings(event.files),
88
+ ...strings(args.files),
89
+ ...reported,
90
+ text(event.file),
91
+ text(event.path),
92
+ ].filter((file) => Boolean(file)))];
93
+ }
94
+ function eventOutcome(event) {
95
+ const value = String(event.outcome ?? event.status ?? '').toLowerCase();
96
+ const type = String(event.type ?? '').toLowerCase();
97
+ if (value.includes('reject') || type.includes('reject') || type.includes('deny'))
98
+ return 'rejected';
99
+ if (value.includes('error') || value.includes('fail') || type.includes('error') || type.includes('fail'))
100
+ return 'error';
101
+ if (value.includes('success') || value === 'pass' || type.includes('success') || type.includes('completed'))
102
+ return 'success';
103
+ return 'unknown';
104
+ }
105
+ export function normalizeAgentEvent(input, index = 0) {
106
+ const event = input && typeof input === 'object' ? input : {};
107
+ const type = text(event.type) ?? 'unknown';
108
+ const tool = text(event.tool ?? event.toolName);
109
+ const outcome = eventOutcome(event);
110
+ const summary = text(event.summary ?? event.message ?? event.description)
111
+ ?? `${tool ?? type}${outcome === 'unknown' ? '' : ` · ${outcome}`}`;
112
+ return {
113
+ id: text(event.id) ?? text(event.requestId) ?? `legacy-event-${index + 1}`,
114
+ timestamp: validDate(event.timestamp ?? event.createdAt),
115
+ type,
116
+ agentId: text(event.agentId ?? event.agent_id) ?? 'unknown-agent',
117
+ taskId: text(event.taskId ?? event.task_id),
118
+ sessionId: text(event.sessionId ?? event.session_id),
119
+ requestId: text(event.requestId ?? event.request_id),
120
+ tool,
121
+ outcome,
122
+ files: eventFiles(event),
123
+ summary,
124
+ guidance: eventGuidance(event),
125
+ telemetry: eventTelemetry(event),
126
+ };
127
+ }
128
+ /**
129
+ * Normalizes a page as a unit so a response inherits identity and scope from its
130
+ * matching request. Older event logs often stored those fields only on tool_call.
131
+ */
132
+ export function normalizeAgentEvents(inputs) {
133
+ const requestContext = new Map();
134
+ for (const input of inputs) {
135
+ const event = input && typeof input === 'object' ? input : {};
136
+ const requestId = text(event.requestId ?? event.request_id);
137
+ if (!requestId || String(event.type) !== 'tool_call')
138
+ continue;
139
+ const args = event.arguments && typeof event.arguments === 'object'
140
+ ? event.arguments
141
+ : {};
142
+ requestContext.set(requestId, {
143
+ agentId: event.agentId ?? event.agent_id ?? args.agentId ?? args.agent_id ?? args.agent
144
+ ?? args.fromAgentId ?? args.toAgentId,
145
+ taskId: event.taskId ?? event.task_id ?? args.taskId ?? args.task_id,
146
+ sessionId: event.sessionId ?? event.session_id ?? args.sessionId ?? args.session_id,
147
+ files: event.files ?? args.files ?? args.filesChanged ?? args.filesInScope,
148
+ });
149
+ }
150
+ return inputs.map((input, index) => {
151
+ const event = input && typeof input === 'object' ? input : {};
152
+ const requestId = text(event.requestId ?? event.request_id);
153
+ return normalizeAgentEvent(requestId ? { ...requestContext.get(requestId), ...event } : event, index);
154
+ });
155
+ }
156
+ export function buildAgentRuns(events) {
157
+ const runs = new Map();
158
+ for (const event of [...events].sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))) {
159
+ const day = event.timestamp.slice(0, 10);
160
+ const id = event.sessionId ?? event.taskId ?? event.requestId ?? `${event.agentId}:${day}`;
161
+ const current = runs.get(id) ?? {
162
+ id,
163
+ agentId: event.agentId,
164
+ taskId: event.taskId,
165
+ sessionId: event.sessionId,
166
+ startedAt: event.timestamp,
167
+ endedAt: event.timestamp,
168
+ status: 'active',
169
+ eventCount: 0,
170
+ files: [],
171
+ tools: [],
172
+ impact: emptyImpact(),
173
+ };
174
+ current.endedAt = event.timestamp;
175
+ current.eventCount++;
176
+ current.files = [...new Set([...current.files, ...event.files])];
177
+ if (event.tool)
178
+ current.tools = [...new Set([...current.tools, event.tool])];
179
+ if (event.guidance) {
180
+ current.impact ??= emptyImpact();
181
+ const telemetry = event.telemetry;
182
+ current.impact.guidanceCount++;
183
+ current.impact.knowledgeItems += event.guidance.patternRefs.length
184
+ + event.guidance.lessonRefs.length
185
+ + event.guidance.memoryRefs.length;
186
+ current.impact.candidateTokens += telemetry?.candidateTokens ?? 0;
187
+ current.impact.returnedTokens += telemetry?.returnedTokens ?? 0;
188
+ current.impact.avoidedTokens += Math.max(0, (telemetry?.candidateTokens ?? 0) - (telemetry?.returnedTokens ?? 0));
189
+ current.impact.candidateFiles += telemetry?.candidateFiles ?? 0;
190
+ current.impact.returnedFiles += telemetry?.returnedFiles ?? 0;
191
+ current.impact.excludedFiles += event.guidance.excludedFileCount;
192
+ if (telemetry?.cacheStatus && !['none', 'miss'].includes(telemetry.cacheStatus))
193
+ current.impact.cacheHits++;
194
+ current.impact.recommendations = [...new Set([...current.impact.recommendations, event.guidance.recommendation])];
195
+ }
196
+ if (event.outcome !== 'unknown')
197
+ current.status = event.outcome;
198
+ runs.set(id, current);
199
+ }
200
+ return [...runs.values()].sort((a, b) => Date.parse(b.endedAt) - Date.parse(a.endedAt));
201
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildAgentRuns, normalizeAgentEvent, normalizeAgentEvents } from './agent-history.js';
3
+ describe('agent history', () => {
4
+ it('normalizes incomplete historical records without dropping them', () => {
5
+ const event = normalizeAgentEvent({ type: 'tool_error', arguments: { files: ['src/a.ts'] } });
6
+ expect(event.agentId).toBe('unknown-agent');
7
+ expect(event.outcome).toBe('error');
8
+ expect(event.files).toEqual(['src/a.ts']);
9
+ });
10
+ it('groups events into stable runs and retains outcomes', () => {
11
+ const events = [
12
+ normalizeAgentEvent({ id: '1', sessionId: 's1', agentId: 'a1', timestamp: '2026-01-01T00:00:00Z', tool: 'read' }),
13
+ normalizeAgentEvent({ id: '2', sessionId: 's1', agentId: 'a1', timestamp: '2026-01-01T00:01:00Z', type: 'completed', files: ['src/a.ts'] }),
14
+ ];
15
+ expect(buildAgentRuns(events)).toMatchObject([{ id: 's1', eventCount: 2, status: 'success', files: ['src/a.ts'] }]);
16
+ });
17
+ it('normalizes guidance safely and builds an honest impact receipt', () => {
18
+ const events = [normalizeAgentEvent({
19
+ id: 'response-1', requestId: 'request-1', agentId: 'agent-1', timestamp: '2026-01-01T00:00:00Z',
20
+ type: 'tool_response', status: 'success',
21
+ guidance: {
22
+ kind: 'context-scope',
23
+ recommendation: 'Read only the two evidence-backed files.',
24
+ patternRefs: [{ id: 'p1', label: 'withTransaction' }, { bad: true }],
25
+ lessonRefs: [{ id: 'l1', label: 'Bound retries' }],
26
+ selectedFiles: ['src/a.ts', 42],
27
+ excludedFileCount: 18,
28
+ },
29
+ telemetry: {
30
+ candidateTokens: 2400,
31
+ returnedTokens: 400,
32
+ candidateFiles: 20,
33
+ returnedFiles: 2,
34
+ cacheStatus: 'exact-hit',
35
+ classification: 'measured estimate',
36
+ },
37
+ })];
38
+ expect(events[0].guidance?.selectedFiles).toEqual(['src/a.ts']);
39
+ expect(buildAgentRuns(events)[0].impact).toMatchObject({
40
+ guidanceCount: 1,
41
+ knowledgeItems: 2,
42
+ avoidedTokens: 2000,
43
+ excludedFiles: 18,
44
+ cacheHits: 1,
45
+ });
46
+ });
47
+ it('ignores malformed guidance rather than breaking legacy history', () => {
48
+ const event = normalizeAgentEvent({ guidance: { kind: 'context-scope', recommendation: 42 }, telemetry: 'bad' });
49
+ expect(event.guidance).toBeUndefined();
50
+ expect(event.telemetry).toBeUndefined();
51
+ });
52
+ });
53
+ describe('normalizeAgentEvents', () => {
54
+ it('carries request identity into the matching response', () => {
55
+ const events = normalizeAgentEvents([
56
+ {
57
+ type: 'tool_call', requestId: 'request-1', tool: 'rigour_checkpoint',
58
+ timestamp: '2026-09-10T00:00:00Z',
59
+ arguments: { agentId: 'agent-a', taskId: 'task-a', filesChanged: ['src/a.ts'] },
60
+ },
61
+ {
62
+ type: 'tool_response', requestId: 'request-1', tool: 'rigour_checkpoint', status: 'success',
63
+ timestamp: '2026-09-10T00:00:01Z',
64
+ },
65
+ ]);
66
+ expect(events[1]).toMatchObject({ agentId: 'agent-a', taskId: 'task-a', files: ['src/a.ts'] });
67
+ expect(buildAgentRuns(events)).toHaveLength(1);
68
+ });
69
+ });
@@ -17,6 +17,7 @@ export interface TaskContextStats {
17
17
  potentialAvoidedTokens: number;
18
18
  cacheHitRate: number;
19
19
  repeatedReadsPrevented: number;
20
+ deduplicatedTokens: number;
20
21
  checkpointReplayAvoided: number;
21
22
  isEstimated: boolean;
22
23
  }
@@ -26,15 +27,35 @@ export interface TaskCostStats {
26
27
  outputTokens: number;
27
28
  costUsd: number;
28
29
  source: string;
30
+ models: string[];
29
31
  isEstimated: boolean;
30
32
  pricingBasis?: string;
33
+ classification: 'observed' | 'modelled estimate' | 'unavailable';
34
+ observedCoverage: number;
31
35
  };
32
36
  estimated: {
37
+ /** Compatibility alias: retrieval-only avoided tokens. */
33
38
  potentialContextAvoided: number;
39
+ /** Compatibility alias: retrieval-only avoided cost. */
34
40
  estimatedCostAvoidedUsd: number;
41
+ retrievalAvoidedTokens: number;
42
+ retrievalAvoidedCostUsd: number;
43
+ checkpointReplayAvoidedTokens: number;
44
+ checkpointReplayAvoidedCostUsd: number;
45
+ retrievalAvoidedCostRangeUsd?: {
46
+ min: number;
47
+ max: number;
48
+ };
49
+ checkpointReplayAvoidedCostRangeUsd?: {
50
+ min: number;
51
+ max: number;
52
+ };
53
+ categoriesAreAdditive: false;
35
54
  isEstimated: boolean;
36
55
  inputPricePerMillionUsd?: number;
37
56
  pricingBasis?: string;
57
+ pricingSource: string;
58
+ pricingEffectiveDate: string;
38
59
  };
39
60
  }
40
61
  export interface CachePerformanceStats {
@@ -8,7 +8,7 @@ import fs from 'fs-extra';
8
8
  import path from 'path';
9
9
  import { getContextEvents, getModelUsages, getCheckpointMetrics, recordModelUsage, } from '../storage/index.js';
10
10
  import { getCursorApiKey } from '../settings.js';
11
- import { estimateAvoidedContextCostUsd, estimateTokenCostUsd, computeWeightedInputPricePerMillion, } from './model-pricing.js';
11
+ import { estimateAvoidedContextCostUsd, estimateTokenCostUsd, computeWeightedInputPricePerMillion, MODEL_PRICING_EFFECTIVE_DATE, MODEL_PRICING_SOURCE, } from './model-pricing.js';
12
12
  import { fetchCursorUsagePages } from './cursor-usage-client.js';
13
13
  import { CURSOR_ADMIN_API_SOURCE, centsToUsd, isCursorAdminApiEvent, normalizeCursorUsageEvent } from './cursor-usage-normalizer.js';
14
14
  /** Heuristic token counter (~4 chars per token for code/text) */
@@ -46,6 +46,7 @@ export async function getTaskContextStats(taskId, cwd) {
46
46
  let returnedTokens = 0;
47
47
  let cacheHits = 0;
48
48
  let repeatedReads = 0;
49
+ let deduplicatedTokens = 0;
49
50
  for (const e of events) {
50
51
  candidateTokens += e.candidateTokens || 0;
51
52
  returnedTokens += e.returnedTokens || 0;
@@ -53,6 +54,7 @@ export async function getTaskContextStats(taskId, cwd) {
53
54
  cacheHits++;
54
55
  }
55
56
  const deduped = e.deduplicatedTokens ?? 0;
57
+ deduplicatedTokens += deduped;
56
58
  if (deduped > 0) {
57
59
  repeatedReads += Math.ceil(deduped / 500);
58
60
  }
@@ -68,6 +70,7 @@ export async function getTaskContextStats(taskId, cwd) {
68
70
  potentialAvoidedTokens,
69
71
  cacheHitRate,
70
72
  repeatedReadsPrevented: repeatedReads,
73
+ deduplicatedTokens,
71
74
  checkpointReplayAvoided,
72
75
  isEstimated: retrievals === 0,
73
76
  };
@@ -83,10 +86,13 @@ export async function getTaskCostStats(taskId, cwd) {
83
86
  let observedCostUsd = 0;
84
87
  let primarySource = 'estimated';
85
88
  const hasObservedUsage = usages.length > 0;
89
+ let observedUsageCount = 0;
86
90
  for (const u of usages) {
87
91
  actualInputTokens += u.inputTokens || 0;
88
92
  actualOutputTokens += u.outputTokens || 0;
89
93
  observedCostUsd += u.observedCostUsd || 0;
94
+ if ((u.observedCostUsd || 0) > 0)
95
+ observedUsageCount++;
90
96
  if (u.source && u.source !== 'estimated') {
91
97
  primarySource = u.source;
92
98
  }
@@ -103,23 +109,46 @@ export async function getTaskCostStats(taskId, cwd) {
103
109
  else if (hasObservedCost) {
104
110
  actualPricingBasis = 'observed-cursor-charged-cents';
105
111
  }
106
- const totalPotentialAvoided = stats.potentialAvoidedTokens + stats.checkpointReplayAvoided;
107
- const avoidedPricing = estimateAvoidedContextCostUsd(totalPotentialAvoided, usages);
112
+ const retrievalPricing = estimateAvoidedContextCostUsd(stats.potentialAvoidedTokens, usages);
113
+ const checkpointPricing = estimateAvoidedContextCostUsd(stats.checkpointReplayAvoided, usages);
114
+ const unknownPricing = retrievalPricing.pricing.pricingBasis.includes('fallback');
115
+ const range = (tokens) => ({
116
+ min: parseFloat(((tokens / 1_000_000) * 0.15).toFixed(4)),
117
+ max: parseFloat(((tokens / 1_000_000) * 15).toFixed(4)),
118
+ });
108
119
  return {
109
120
  actual: {
110
121
  inputTokens: actualInputTokens,
111
122
  outputTokens: actualOutputTokens,
112
123
  costUsd: parseFloat(actualCostUsd.toFixed(4)),
113
124
  source: primarySource,
125
+ models: Array.from(new Set(usages.map(usage => usage.model).filter((model) => Boolean(model)))),
114
126
  isEstimated: actualIsEstimated,
115
127
  pricingBasis: actualPricingBasis,
128
+ classification: hasObservedCost
129
+ ? 'observed'
130
+ : hasObservedUsage
131
+ ? 'modelled estimate'
132
+ : 'unavailable',
133
+ observedCoverage: usages.length > 0
134
+ ? parseFloat((observedUsageCount / usages.length).toFixed(2))
135
+ : 0,
116
136
  },
117
137
  estimated: {
118
- potentialContextAvoided: totalPotentialAvoided,
119
- estimatedCostAvoidedUsd: avoidedPricing.costUsd,
138
+ potentialContextAvoided: stats.potentialAvoidedTokens,
139
+ estimatedCostAvoidedUsd: retrievalPricing.costUsd,
140
+ retrievalAvoidedTokens: stats.potentialAvoidedTokens,
141
+ retrievalAvoidedCostUsd: retrievalPricing.costUsd,
142
+ checkpointReplayAvoidedTokens: stats.checkpointReplayAvoided,
143
+ checkpointReplayAvoidedCostUsd: checkpointPricing.costUsd,
144
+ retrievalAvoidedCostRangeUsd: unknownPricing ? range(stats.potentialAvoidedTokens) : undefined,
145
+ checkpointReplayAvoidedCostRangeUsd: unknownPricing ? range(stats.checkpointReplayAvoided) : undefined,
146
+ categoriesAreAdditive: false,
120
147
  isEstimated: true,
121
- inputPricePerMillionUsd: avoidedPricing.pricing.inputPricePerMillionUsd,
122
- pricingBasis: avoidedPricing.pricing.pricingBasis,
148
+ inputPricePerMillionUsd: retrievalPricing.pricing.inputPricePerMillionUsd,
149
+ pricingBasis: retrievalPricing.pricing.pricingBasis,
150
+ pricingSource: MODEL_PRICING_SOURCE,
151
+ pricingEffectiveDate: MODEL_PRICING_EFFECTIVE_DATE,
123
152
  },
124
153
  };
125
154
  }
@@ -39,6 +39,7 @@ describe('context-telemetry-service', () => {
39
39
  expect(stats.retrievals).toBe(1);
40
40
  expect(stats.potentialAvoidedTokens).toBe(8000);
41
41
  expect(stats.repeatedReadsPrevented).toBeGreaterThan(0);
42
+ expect(stats.deduplicatedTokens).toBe(3000);
42
43
  expect(stats.isEstimated).toBe(false);
43
44
  });
44
45
  it('aggregates cache performance from events', async () => {
@@ -75,6 +76,7 @@ describe('context-telemetry-service', () => {
75
76
  expect(cost.actual.inputTokens).toBe(510000);
76
77
  expect(cost.actual.costUsd).toBe(4.83);
77
78
  expect(cost.actual.isEstimated).toBe(false);
79
+ expect(cost.actual.classification).toBe('observed');
78
80
  });
79
81
  it('deduplicates CSV rows on re-import', async () => {
80
82
  const csv = `Date,Model,Input Tokens,Output Tokens,Cost
@@ -166,4 +168,15 @@ describe('context-telemetry-service', () => {
166
168
  expect(cost.estimated.inputPricePerMillionUsd).toBe(0.5);
167
169
  expect(cost.estimated.estimatedCostAvoidedUsd).toBeGreaterThan(0);
168
170
  });
171
+ it('does not combine retrieval and checkpoint counterfactuals', async () => {
172
+ const id = `${taskId}-separate-${Math.random().toString(36).slice(2)}`;
173
+ await recordContextEvent({ taskId: id, toolName: 'rigour_context_scope', cacheStatus: 'miss', candidateTokens: 5000, returnedTokens: 1000 }, testCwd);
174
+ await recordCheckpointMetric({ checkpointId: `${id}-cp`, taskId: id, agentId: 'a1', rawStateTokens: 9000, checkpointTokens: 1000, replayTokensAvoided: 8000 }, testCwd);
175
+ const cost = await getTaskCostStats(id, testCwd);
176
+ expect(cost.estimated.retrievalAvoidedTokens).toBe(4000);
177
+ expect(cost.estimated.checkpointReplayAvoidedTokens).toBe(8000);
178
+ expect(cost.estimated.potentialContextAvoided).toBe(4000);
179
+ expect(cost.estimated.categoriesAreAdditive).toBe(false);
180
+ expect(cost.estimated.retrievalAvoidedCostRangeUsd?.max).toBeGreaterThan(cost.estimated.retrievalAvoidedCostRangeUsd?.min ?? 0);
181
+ });
169
182
  });
@@ -0,0 +1,55 @@
1
+ import type { DependencyGraph } from '../context/dependency-graph.js';
2
+ import type { LessonRecord } from '../storage/lessons.js';
3
+ import type { AgentHistoryEvent, AgentRun } from './agent-history.js';
4
+ export type KnowledgeNodeType = 'repository' | 'file' | 'agent' | 'task' | 'run' | 'advice' | 'pattern' | 'memory' | 'lesson' | 'policy' | 'outcome';
5
+ export interface KnowledgeNode {
6
+ id: string;
7
+ type: KnowledgeNodeType;
8
+ label: string;
9
+ detail?: string;
10
+ state?: string;
11
+ repositoryId?: string;
12
+ weight: number;
13
+ evidence?: Record<string, unknown>;
14
+ }
15
+ export interface KnowledgeEdge {
16
+ id: string;
17
+ from: string;
18
+ to: string;
19
+ type: 'contains' | 'imports' | 'performed' | 'belongs_to' | 'touched' | 'produced' | 'resulted_in' | 'defined_in' | 'protects' | 'remembered' | 'guided' | 'informed';
20
+ }
21
+ export interface EngineeringKnowledgeGraph {
22
+ schemaVersion: 1;
23
+ generatedAt: string;
24
+ nodes: KnowledgeNode[];
25
+ edges: KnowledgeEdge[];
26
+ truncated: boolean;
27
+ counts: Record<string, number>;
28
+ }
29
+ interface GraphInput {
30
+ repository: {
31
+ id: string;
32
+ name: string;
33
+ };
34
+ dependencyGraph?: DependencyGraph | null;
35
+ events: AgentHistoryEvent[];
36
+ runs: AgentRun[];
37
+ lessons: LessonRecord[];
38
+ patterns?: Array<{
39
+ id: string;
40
+ name: string;
41
+ type: string;
42
+ file: string;
43
+ description?: string;
44
+ usageCount?: number;
45
+ }>;
46
+ memories?: Array<{
47
+ id: string;
48
+ label: string;
49
+ detail?: string;
50
+ source?: string;
51
+ }>;
52
+ maxFiles?: number;
53
+ }
54
+ export declare function buildEngineeringKnowledgeGraph(input: GraphInput): EngineeringKnowledgeGraph;
55
+ export {};