@rigour-labs/core 5.5.4 → 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.
- 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/firewall/adversarial.d.ts +13 -0
- package/dist/firewall/adversarial.js +125 -0
- package/dist/firewall/arbitration-token.d.ts +8 -0
- package/dist/firewall/arbitration-token.js +58 -0
- package/dist/firewall/attestation.d.ts +38 -0
- package/dist/firewall/attestation.js +197 -0
- package/dist/firewall/capability-broker.d.ts +47 -0
- package/dist/firewall/capability-broker.js +209 -0
- package/dist/firewall/firewall.test.d.ts +1 -0
- package/dist/firewall/firewall.test.js +102 -0
- package/dist/firewall/index.d.ts +10 -0
- package/dist/firewall/index.js +10 -0
- package/dist/firewall/mcp-gateway.d.ts +40 -0
- package/dist/firewall/mcp-gateway.js +64 -0
- package/dist/firewall/policy-hash.d.ts +1 -0
- package/dist/firewall/policy-hash.js +5 -0
- package/dist/firewall/scope-enforcement.d.ts +26 -0
- package/dist/firewall/scope-enforcement.js +155 -0
- package/dist/firewall/transaction.d.ts +43 -0
- package/dist/firewall/transaction.js +216 -0
- package/dist/firewall/typed-command.d.ts +16 -0
- package/dist/firewall/typed-command.js +129 -0
- package/dist/firewall/types.d.ts +96 -0
- package/dist/firewall/types.js +5 -0
- package/dist/hooks/checker.d.ts +2 -0
- package/dist/hooks/checker.js +25 -1
- package/dist/hooks/standalone-checker.js +5 -1
- package/dist/hooks/templates.js +3 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -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
package/dist/hooks/checker.d.ts
CHANGED
package/dist/hooks/checker.js
CHANGED
|
@@ -12,6 +12,7 @@ import path from 'path';
|
|
|
12
12
|
import yaml from 'yaml';
|
|
13
13
|
import { ConfigSchema } from '../types/index.js';
|
|
14
14
|
import { scanInputForCredentials } from './input-validator.js';
|
|
15
|
+
import { evaluateWriteScope, loadAgentScopesFromDisk } from '../firewall/scope-enforcement.js';
|
|
15
16
|
const JS_TS_PATTERN = /\.(ts|tsx|js|jsx|mts|mjs)$/;
|
|
16
17
|
/**
|
|
17
18
|
* Check if a file matches any ignore pattern from rigour.yml.
|
|
@@ -92,14 +93,17 @@ function checkFile(content, relPath, cwd, config) {
|
|
|
92
93
|
*/
|
|
93
94
|
export async function runHookChecker(options) {
|
|
94
95
|
const start = Date.now();
|
|
95
|
-
const { cwd, files, timeout_ms = 5000 } = options;
|
|
96
|
+
const { cwd, files, timeout_ms = 5000, agentId } = options;
|
|
96
97
|
const failures = [];
|
|
98
|
+
let timedOut = false;
|
|
97
99
|
try {
|
|
98
100
|
const config = await loadConfig(cwd);
|
|
99
101
|
const deadline = start + timeout_ms;
|
|
100
102
|
const ignorePatterns = config.ignore ?? [];
|
|
103
|
+
const agentScopes = await loadAgentScopesFromDisk(cwd);
|
|
101
104
|
for (const filePath of files) {
|
|
102
105
|
if (Date.now() > deadline) {
|
|
106
|
+
timedOut = true;
|
|
103
107
|
break;
|
|
104
108
|
}
|
|
105
109
|
const resolved = await resolveFile(filePath, cwd);
|
|
@@ -110,9 +114,29 @@ export async function runHookChecker(options) {
|
|
|
110
114
|
if (isIgnored(resolved.relPath, ignorePatterns)) {
|
|
111
115
|
continue;
|
|
112
116
|
}
|
|
117
|
+
if (agentScopes.length > 0) {
|
|
118
|
+
const scopeEv = evaluateWriteScope(cwd, resolved.relPath, agentScopes, agentId);
|
|
119
|
+
if (scopeEv.decision !== 'allow') {
|
|
120
|
+
failures.push({
|
|
121
|
+
gate: 'agent-scope',
|
|
122
|
+
file: resolved.relPath,
|
|
123
|
+
message: scopeEv.reason,
|
|
124
|
+
severity: 'critical',
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
113
129
|
const fileFailures = checkFile(resolved.content, resolved.relPath, cwd, config);
|
|
114
130
|
failures.push(...fileFailures);
|
|
115
131
|
}
|
|
132
|
+
if (timedOut) {
|
|
133
|
+
failures.push({
|
|
134
|
+
gate: 'hook-timeout',
|
|
135
|
+
file: '',
|
|
136
|
+
message: `Hook checker exceeded ${timeout_ms}ms before all files were scanned (fail-closed)`,
|
|
137
|
+
severity: 'critical',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
116
140
|
return {
|
|
117
141
|
status: failures.length > 0 ? 'fail' : 'pass',
|
|
118
142
|
failures,
|
|
@@ -89,7 +89,11 @@ async function main() {
|
|
|
89
89
|
process.stdout.write(EMPTY_RESULT);
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
|
-
const result = await runHookChecker({
|
|
92
|
+
const result = await runHookChecker({
|
|
93
|
+
cwd,
|
|
94
|
+
files,
|
|
95
|
+
agentId: process.env.RIGOUR_AGENT_ID || undefined,
|
|
96
|
+
});
|
|
93
97
|
process.stdout.write(JSON.stringify(result));
|
|
94
98
|
if (result.status === 'fail') {
|
|
95
99
|
logFailures(result.failures);
|
package/dist/hooks/templates.js
CHANGED
|
@@ -82,6 +82,7 @@ process.stdin.on('end', async () => {
|
|
|
82
82
|
const result = await runHookChecker({
|
|
83
83
|
cwd: process.cwd(),
|
|
84
84
|
files: [payload.file_path],
|
|
85
|
+
agentId: process.env.RIGOUR_AGENT_ID || undefined,
|
|
85
86
|
});
|
|
86
87
|
|
|
87
88
|
// Write result to stdout for Cursor to consume
|
|
@@ -148,6 +149,7 @@ process.stdin.on('end', async () => {
|
|
|
148
149
|
const result = await runHookChecker({
|
|
149
150
|
cwd: process.cwd(),
|
|
150
151
|
files: [filePath],
|
|
152
|
+
agentId: process.env.RIGOUR_AGENT_ID || undefined,
|
|
151
153
|
});
|
|
152
154
|
|
|
153
155
|
if (result.status === 'fail') {
|
|
@@ -206,6 +208,7 @@ process.stdin.on('end', async () => {
|
|
|
206
208
|
const result = await runHookChecker({
|
|
207
209
|
cwd: process.cwd(),
|
|
208
210
|
files: [payload.file_path],
|
|
211
|
+
agentId: process.env.RIGOUR_AGENT_ID || undefined,
|
|
209
212
|
});
|
|
210
213
|
|
|
211
214
|
if (result.status === 'fail') {
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, rese
|
|
|
24
24
|
export type { RigourDB, CompactResult } from './storage/index.js';
|
|
25
25
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
|
|
26
26
|
export type { ProjectStats } from './storage/index.js';
|
|
27
|
+
export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId } from './storage/index.js';
|
|
28
|
+
export type { ApplicableLessons, InteractionEvidence, LessonRecord, LessonState, LessonVisibility } from './storage/index.js';
|
|
29
|
+
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './storage/index.js';
|
|
30
|
+
export type { TeamConfiguration, TeamSemanticConfiguration, TeamModeStatus, TeamDoctorResult, SemanticKnowledgeCandidate, SemanticKnowledgeResult, } from './storage/index.js';
|
|
27
31
|
export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
|
|
28
32
|
export type { TemporalDriftReport, ProvenanceStream, MonthlyBucket, WeeklyBucket, DriftDirection } from './services/temporal-drift.js';
|
|
29
33
|
export { getProvenanceTrends, getQualityTrend } from './services/adaptive-thresholds.js';
|
|
@@ -35,5 +39,11 @@ export type { RenderOptions, GateResult } from './services/terminal-renderer.js'
|
|
|
35
39
|
export * from './context/cache-engine.js';
|
|
36
40
|
export * from './context/context-session.js';
|
|
37
41
|
export * from './context/index-bridge.js';
|
|
42
|
+
export * from './context/automatic-index.js';
|
|
43
|
+
export * from './context/dependency-graph.js';
|
|
38
44
|
export * from './services/context-telemetry-service.js';
|
|
45
|
+
export * from './services/system-health.js';
|
|
46
|
+
export * from './services/agent-history.js';
|
|
47
|
+
export * from './services/engineering-knowledge-graph.js';
|
|
48
|
+
export * from './firewall/index.js';
|
|
39
49
|
export { recordContextEvent, recordModelUsage, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './storage/index.js';
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,8 @@ export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
|
|
|
24
24
|
export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
|
|
25
25
|
// Local Project Memory (hybrid intelligence — SQLite-backed per-project learning)
|
|
26
26
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
|
|
27
|
+
export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId } from './storage/index.js';
|
|
28
|
+
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './storage/index.js';
|
|
27
29
|
// Temporal Drift Engine (v5 — cross-session trend analysis + per-provenance EWMA)
|
|
28
30
|
export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
|
|
29
31
|
// Adaptive Thresholds (v5 — Z-score + per-provenance trends)
|
|
@@ -36,7 +38,13 @@ export { renderScoreGauge, renderSeveritySection, renderGateGrid, renderBrainSta
|
|
|
36
38
|
export * from './context/cache-engine.js';
|
|
37
39
|
export * from './context/context-session.js';
|
|
38
40
|
export * from './context/index-bridge.js';
|
|
41
|
+
export * from './context/automatic-index.js';
|
|
42
|
+
export * from './context/dependency-graph.js';
|
|
39
43
|
export * from './services/context-telemetry-service.js';
|
|
44
|
+
export * from './services/system-health.js';
|
|
45
|
+
export * from './services/agent-history.js';
|
|
46
|
+
export * from './services/engineering-knowledge-graph.js';
|
|
47
|
+
export * from './firewall/index.js';
|
|
40
48
|
export { recordContextEvent, recordModelUsage, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './storage/index.js';
|
|
41
49
|
// Pattern Index is intentionally NOT exported here to prevent
|
|
42
50
|
// native dependency issues (sharp/transformers) from leaking into
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Singleton for the embedding pipeline to avoid re-loading the model.
|
|
8
8
|
*/
|
|
9
9
|
let embeddingPipeline = null;
|
|
10
|
+
let embeddingUnavailable = false;
|
|
10
11
|
/**
|
|
11
12
|
* Get or initialize the embedding pipeline.
|
|
12
13
|
*/
|
|
@@ -21,6 +22,8 @@ async function getPipeline() {
|
|
|
21
22
|
return { data: new Float32Array(vector) };
|
|
22
23
|
};
|
|
23
24
|
}
|
|
25
|
+
if (embeddingUnavailable)
|
|
26
|
+
return null;
|
|
24
27
|
if (!embeddingPipeline) {
|
|
25
28
|
try {
|
|
26
29
|
// Dynamic import to isolate native dependency issues (like sharp)
|
|
@@ -29,8 +32,9 @@ async function getPipeline() {
|
|
|
29
32
|
embeddingPipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
|
|
30
33
|
}
|
|
31
34
|
catch (error) {
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
embeddingUnavailable = true;
|
|
36
|
+
console.warn('Semantic enrichment is degraded; structural and text retrieval remain available.');
|
|
37
|
+
return null;
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
return embeddingPipeline;
|
|
@@ -41,11 +45,14 @@ async function getPipeline() {
|
|
|
41
45
|
export async function generateEmbedding(text) {
|
|
42
46
|
try {
|
|
43
47
|
const extractor = await getPipeline();
|
|
48
|
+
if (!extractor)
|
|
49
|
+
return [];
|
|
44
50
|
const output = await extractor(text, { pooling: 'mean', normalize: true });
|
|
45
51
|
return Array.from(output.data);
|
|
46
52
|
}
|
|
47
53
|
catch (error) {
|
|
48
|
-
|
|
54
|
+
embeddingUnavailable = true;
|
|
55
|
+
console.warn('Semantic enrichment is degraded; structural and text retrieval remain available.');
|
|
49
56
|
return [];
|
|
50
57
|
}
|
|
51
58
|
}
|
|
@@ -15,6 +15,8 @@ export declare class PatternIndexer {
|
|
|
15
15
|
constructor(rootDir: string, config?: Partial<PatternIndexConfig>);
|
|
16
16
|
buildIndex(): Promise<PatternIndex>;
|
|
17
17
|
updateIndex(existingIndex: PatternIndex): Promise<PatternIndex>;
|
|
18
|
+
updateFiles(existingIndex: PatternIndex, changedFiles: string[]): Promise<PatternIndex>;
|
|
19
|
+
enrichIndex(existingIndex: PatternIndex): Promise<PatternIndex>;
|
|
18
20
|
private findFiles;
|
|
19
21
|
private extractPatterns;
|
|
20
22
|
private getScriptKind;
|
|
@@ -12,6 +12,7 @@ import * as fs from 'fs/promises';
|
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { globby } from 'globby';
|
|
14
14
|
import ts from 'typescript';
|
|
15
|
+
import micromatch from 'micromatch';
|
|
15
16
|
import { generateEmbedding } from './embeddings.js';
|
|
16
17
|
import { hashContent } from './indexer-helpers.js';
|
|
17
18
|
import { extractGoPatterns, extractRustPatterns, extractJVMStylePatterns, extractPythonPatterns, extractGenericCPatterns, } from './indexer-lang.js';
|
|
@@ -156,6 +157,77 @@ export class PatternIndexer {
|
|
|
156
157
|
files: updatedFiles,
|
|
157
158
|
};
|
|
158
159
|
}
|
|
160
|
+
async updateFiles(existingIndex, changedFiles) {
|
|
161
|
+
const startTime = Date.now();
|
|
162
|
+
const root = path.resolve(this.rootDir);
|
|
163
|
+
const changed = new Set(changedFiles.flatMap(file => {
|
|
164
|
+
const absolute = path.resolve(root, file);
|
|
165
|
+
const relative = path.relative(root, absolute);
|
|
166
|
+
if (!relative || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
167
|
+
return [];
|
|
168
|
+
return [relative.split(path.sep).join('/')];
|
|
169
|
+
}));
|
|
170
|
+
const patterns = existingIndex.patterns.filter(pattern => !changed.has(pattern.file));
|
|
171
|
+
const files = existingIndex.files.filter(file => !changed.has(file.path));
|
|
172
|
+
for (const relativePath of changed) {
|
|
173
|
+
const absolutePath = path.join(root, relativePath);
|
|
174
|
+
const supported = this.config.extensions.includes(path.extname(relativePath).toLowerCase());
|
|
175
|
+
const included = micromatch.isMatch(relativePath.replace(/\\/g, '/'), this.config.include, { dot: false });
|
|
176
|
+
const excluded = micromatch.isMatch(relativePath.replace(/\\/g, '/'), this.config.exclude, { dot: true });
|
|
177
|
+
if (!supported || !included || excluded)
|
|
178
|
+
continue;
|
|
179
|
+
try {
|
|
180
|
+
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
181
|
+
const filePatterns = await this.extractPatterns(absolutePath, content);
|
|
182
|
+
if (this.config.useEmbeddings) {
|
|
183
|
+
await Promise.all(filePatterns.map(async (pattern) => {
|
|
184
|
+
pattern.embedding = await generateEmbedding(`${pattern.name} ${pattern.type} ${pattern.description}`);
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
patterns.push(...filePatterns);
|
|
188
|
+
files.push({
|
|
189
|
+
path: relativePath,
|
|
190
|
+
hash: hashContent(content),
|
|
191
|
+
patternCount: filePatterns.length,
|
|
192
|
+
indexedAt: new Date().toISOString(),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (error?.code !== 'ENOENT')
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
version: INDEX_VERSION,
|
|
202
|
+
lastUpdated: new Date().toISOString(),
|
|
203
|
+
rootDir: this.rootDir,
|
|
204
|
+
patterns,
|
|
205
|
+
files,
|
|
206
|
+
stats: this.calculateStats(patterns, files, Date.now() - startTime),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async enrichIndex(existingIndex) {
|
|
210
|
+
const startTime = Date.now();
|
|
211
|
+
const patterns = existingIndex.patterns.map(pattern => ({ ...pattern }));
|
|
212
|
+
const pending = patterns.filter(pattern => !pattern.embedding?.length);
|
|
213
|
+
if (pending.length > 0) {
|
|
214
|
+
pending[0].embedding = await generateEmbedding(`${pending[0].name} ${pending[0].type} ${pending[0].description}`);
|
|
215
|
+
if (!pending[0].embedding.length)
|
|
216
|
+
return existingIndex;
|
|
217
|
+
}
|
|
218
|
+
const batchSize = 10;
|
|
219
|
+
for (let i = 1; i < pending.length; i += batchSize) {
|
|
220
|
+
await Promise.all(pending.slice(i, i + batchSize).map(async (pattern) => {
|
|
221
|
+
pattern.embedding = await generateEmbedding(`${pattern.name} ${pattern.type} ${pattern.description}`);
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
...existingIndex,
|
|
226
|
+
lastUpdated: new Date().toISOString(),
|
|
227
|
+
patterns,
|
|
228
|
+
stats: this.calculateStats(patterns, existingIndex.files, Date.now() - startTime),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
159
231
|
// -------------------------------------------------------------------------
|
|
160
232
|
// Private helpers
|
|
161
233
|
// -------------------------------------------------------------------------
|
|
@@ -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
|
+
});
|