@rigour-labs/core 6.0.0 → 6.1.0-beta.2
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/context/automatic-index.d.ts +18 -0
- package/dist/context/automatic-index.js +142 -0
- package/dist/context/automatic-index.test.d.ts +1 -0
- package/dist/context/automatic-index.test.js +45 -0
- package/dist/context/cache-engine.d.ts +25 -0
- package/dist/context/dependency-graph.d.ts +13 -0
- package/dist/context/dependency-graph.js +97 -0
- package/dist/context/dependency-graph.test.d.ts +1 -0
- package/dist/context/dependency-graph.test.js +13 -0
- package/dist/context/index-bridge.js +21 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/pattern-index/embeddings.js +10 -3
- package/dist/pattern-index/indexer.d.ts +2 -0
- package/dist/pattern-index/indexer.js +72 -0
- package/dist/services/agent-history.d.ts +76 -0
- package/dist/services/agent-history.js +201 -0
- package/dist/services/agent-history.test.d.ts +1 -0
- package/dist/services/agent-history.test.js +69 -0
- package/dist/services/context-telemetry-service.d.ts +21 -0
- package/dist/services/context-telemetry-service.js +36 -7
- package/dist/services/context-telemetry-service.test.js +13 -0
- package/dist/services/engineering-knowledge-graph.d.ts +55 -0
- package/dist/services/engineering-knowledge-graph.js +177 -0
- package/dist/services/engineering-knowledge-graph.test.d.ts +1 -0
- package/dist/services/engineering-knowledge-graph.test.js +59 -0
- package/dist/services/model-pricing.d.ts +2 -0
- package/dist/services/model-pricing.js +2 -0
- package/dist/services/system-health.d.ts +34 -0
- package/dist/services/system-health.js +53 -0
- package/dist/storage/db.js +130 -1
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/lessons.d.ts +60 -0
- package/dist/storage/lessons.js +322 -0
- package/dist/storage/lessons.test.d.ts +1 -0
- package/dist/storage/lessons.test.js +50 -0
- package/dist/storage/local-encryption.d.ts +2 -0
- package/dist/storage/local-encryption.js +41 -0
- package/dist/storage/team-store.d.ts +75 -0
- package/dist/storage/team-store.js +321 -0
- package/dist/storage/team-store.test.d.ts +1 -0
- package/dist/storage/team-store.test.js +11 -0
- package/dist/storage/team-vector-store.d.ts +25 -0
- package/dist/storage/team-vector-store.js +183 -0
- package/dist/storage/team-vector-store.test.d.ts +1 -0
- package/dist/storage/team-vector-store.test.js +42 -0
- 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
|
|
107
|
-
const
|
|
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:
|
|
119
|
-
estimatedCostAvoidedUsd:
|
|
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:
|
|
122
|
-
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 {};
|