@iris-eval/mcp-server 0.8.2 → 0.9.0
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/README.md +8 -1
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +65 -0
- package/dist/config/defaults.js +2 -0
- package/dist/custom-rule-store.d.ts +4 -0
- package/dist/custom-rule-store.js +8 -3
- package/dist/dashboard/assets/{index-CyzO6OC7.js → index-Cz8_oOqG.js} +1 -1
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/routes/capabilities.d.ts +3 -0
- package/dist/dashboard/routes/capabilities.js +11 -0
- package/dist/dashboard/routes/health.d.ts +5 -1
- package/dist/dashboard/routes/health.js +15 -3
- package/dist/dashboard/routes/rules.js +4 -1
- package/dist/dashboard/routes/traces.d.ts +3 -0
- package/dist/dashboard/routes/traces.js +9 -28
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +6 -2
- package/dist/eval/accuracy.d.ts +41 -0
- package/dist/eval/accuracy.js +97 -0
- package/dist/eval/criticality.d.ts +8 -1
- package/dist/eval/criticality.js +6 -0
- package/dist/eval/dormant.d.ts +4 -0
- package/dist/eval/dormant.js +22 -0
- package/dist/eval/engine.d.ts +1 -0
- package/dist/eval/engine.js +49 -3
- package/dist/eval/failure-classes.d.ts +8 -0
- package/dist/eval/failure-classes.js +18 -0
- package/dist/eval/llm-judge/evaluator.d.ts +10 -0
- package/dist/eval/llm-judge/evaluator.js +16 -1
- package/dist/eval/published-accuracy.d.ts +230 -0
- package/dist/eval/published-accuracy.js +86 -0
- package/dist/eval/questions.d.ts +12 -0
- package/dist/eval/questions.js +14 -0
- package/dist/eval/response-schema.d.ts +652 -0
- package/dist/eval/response-schema.js +130 -0
- package/dist/eval/response.d.ts +12 -0
- package/dist/eval/response.js +30 -0
- package/dist/eval/rules/completeness.js +31 -0
- package/dist/eval/rules/cost.d.ts +1 -1
- package/dist/eval/rules/cost.js +44 -0
- package/dist/eval/rules/custom.d.ts +0 -12
- package/dist/eval/rules/custom.js +21 -0
- package/dist/eval/rules/relevance.js +16 -0
- package/dist/eval/rules/safety.js +178 -1
- package/dist/eval/stamp.d.ts +14 -0
- package/dist/eval/stamp.js +88 -0
- package/dist/eval/stats.d.ts +33 -0
- package/dist/eval/stats.js +109 -0
- package/dist/eval/verdict.d.ts +34 -0
- package/dist/eval/verdict.js +131 -0
- package/dist/index.js +5 -28
- package/dist/instructions.d.ts +17 -0
- package/dist/instructions.js +53 -0
- package/dist/judge-enablement.d.ts +34 -0
- package/dist/judge-enablement.js +78 -0
- package/dist/judge-enablement.json +10 -0
- package/dist/preferences.d.ts +1 -1
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +29 -0
- package/dist/resources/index.d.ts +5 -2
- package/dist/resources/index.js +65 -5
- package/dist/resources/uris.d.ts +12 -0
- package/dist/resources/uris.js +24 -0
- package/dist/retention.d.ts +20 -0
- package/dist/retention.js +44 -0
- package/dist/self-test.d.ts +1 -0
- package/dist/self-test.js +14 -0
- package/dist/server.d.ts +10 -1
- package/dist/server.js +34 -7
- package/dist/storage/index.js +1 -1
- package/dist/storage/migrations/007-eval-provenance.d.ts +3 -0
- package/dist/storage/migrations/007-eval-provenance.js +30 -0
- package/dist/storage/migrations/index.js +24 -4
- package/dist/storage/sqlite-adapter.d.ts +26 -1
- package/dist/storage/sqlite-adapter.js +135 -15
- package/dist/tools/delete-rule.d.ts +8 -0
- package/dist/tools/delete-rule.js +30 -38
- package/dist/tools/delete-trace.d.ts +5 -0
- package/dist/tools/delete-trace.js +24 -27
- package/dist/tools/deploy-rule.d.ts +13 -1
- package/dist/tools/deploy-rule.js +37 -34
- package/dist/tools/describe.d.ts +20 -0
- package/dist/tools/describe.js +36 -0
- package/dist/tools/errors.d.ts +36 -0
- package/dist/tools/errors.js +134 -0
- package/dist/tools/evaluate-output.d.ts +8 -1
- package/dist/tools/evaluate-output.js +37 -58
- package/dist/tools/evaluate-with-llm-judge.d.ts +31 -0
- package/dist/tools/evaluate-with-llm-judge.js +96 -69
- package/dist/tools/get-traces.d.ts +9 -0
- package/dist/tools/get-traces.js +29 -28
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +22 -1
- package/dist/tools/list-rules.d.ts +13 -0
- package/dist/tools/list-rules.js +43 -46
- package/dist/tools/log-trace.d.ts +4 -0
- package/dist/tools/log-trace.js +31 -29
- package/dist/tools/respond.d.ts +42 -0
- package/dist/tools/respond.js +90 -0
- package/dist/tools/strict-input.js +1 -1
- package/dist/tools/trace-link.d.ts +2 -0
- package/dist/tools/trace-link.js +13 -2
- package/dist/tools/verify-citations.d.ts +17 -2
- package/dist/tools/verify-citations.js +98 -93
- package/dist/types/config.d.ts +9 -0
- package/dist/types/eval.d.ts +258 -0
- package/dist/types/eval.js +2 -1
- package/dist/types/query.d.ts +2 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/resources/dashboard-summary.d.ts +0 -3
- package/dist/resources/dashboard-summary.js +0 -16
- package/dist/resources/trace-detail.d.ts +0 -3
- package/dist/resources/trace-detail.js +0 -30
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The verdict, the coverage and the provenance — computed once, derived on
|
|
3
|
+
* read, never fabricated.
|
|
4
|
+
*
|
|
5
|
+
* Arc zero (2026-09-05) found the same fact encoded three ways
|
|
6
|
+
* (`insufficient_data`, `critical_skipped`, `rule_results[].budgetExceeded`),
|
|
7
|
+
* the verdict's basis nowhere (a reader could not tell a veto from a low
|
|
8
|
+
* score without knowing the rule library), coverage counted in rules rather
|
|
9
|
+
* than evaluation questions, and no stored evaluation carrying the version,
|
|
10
|
+
* ruleset or thresholds that produced it — "why did this pass on that day"
|
|
11
|
+
* was unanswerable from Iris alone.
|
|
12
|
+
*
|
|
13
|
+
* This module answers those from what the engine already knows. It changes
|
|
14
|
+
* no verdict: `deriveVerdict` names the basis of today's arithmetic
|
|
15
|
+
* (`passed` here equals the engine's `passed`, and a test proves it); the
|
|
16
|
+
* compose-by-kind release replaces the arithmetic and keeps the shape.
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { RULE_QUESTION_IDS } from './questions.js';
|
|
20
|
+
import { NEEDS } from './failure-classes.js';
|
|
21
|
+
import { publishedProvenance } from './accuracy.js';
|
|
22
|
+
/**
|
|
23
|
+
* The basis of the verdict under the shipped composer (a weighted mean plus
|
|
24
|
+
* the critical veto). `passed` is `state === 'pass'` and equals the engine's
|
|
25
|
+
* own `passed` for every result; the basis says which layer decided.
|
|
26
|
+
*/
|
|
27
|
+
export function deriveVerdict(result, threshold) {
|
|
28
|
+
const evaluated = result.rules_evaluated ?? result.rule_results.filter((r) => !r.skipped).length;
|
|
29
|
+
if (result.insufficient_data || evaluated === 0) {
|
|
30
|
+
return { state: 'unknown', passed: false, basis: 'no_rules', by: [], risk: null };
|
|
31
|
+
}
|
|
32
|
+
const vetoes = result.critical_failures ?? [];
|
|
33
|
+
if (vetoes.length > 0) {
|
|
34
|
+
const kinds = new Map(result.rule_results.map((r) => [r.ruleName, r.kind]));
|
|
35
|
+
const allPolicies = vetoes.every((name) => kinds.get(name) === 'policy');
|
|
36
|
+
return { state: 'fail', passed: false, basis: allPolicies ? 'policy_gate' : 'detector_veto', by: [...vetoes], risk: null };
|
|
37
|
+
}
|
|
38
|
+
if (result.score < threshold) {
|
|
39
|
+
const by = result.rule_results.filter((r) => !r.skipped && !r.passed).map((r) => r.ruleName);
|
|
40
|
+
return { state: 'fail', passed: false, basis: 'score_below_threshold', by, risk: null };
|
|
41
|
+
}
|
|
42
|
+
return { state: 'pass', passed: true, basis: 'clean', by: [], risk: null };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Which evaluation questions were judged, which were not and why. At write
|
|
46
|
+
* time the engine passes the inputs the call carried; at read time they are
|
|
47
|
+
* reconstructed as the union of what the rules saw (a rule that saw an input
|
|
48
|
+
* proves the call carried it; one that did not cannot prove the reverse).
|
|
49
|
+
*/
|
|
50
|
+
export function deriveCoverage(ruleResults, present) {
|
|
51
|
+
const inputs = {};
|
|
52
|
+
const seen = new Set(present ?? []);
|
|
53
|
+
if (!present)
|
|
54
|
+
for (const r of ruleResults)
|
|
55
|
+
for (const n of r.saw ?? [])
|
|
56
|
+
seen.add(n);
|
|
57
|
+
for (const n of NEEDS)
|
|
58
|
+
inputs[n] = seen.has(n);
|
|
59
|
+
const questions = [];
|
|
60
|
+
for (const id of RULE_QUESTION_IDS) {
|
|
61
|
+
const rows = ruleResults.filter((r) => r.question === id);
|
|
62
|
+
if (rows.length === 0) {
|
|
63
|
+
questions.push({ id, status: 'not_applicable', why: 'no rule that answers this question ran in the selected bundles' });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (rows.some((r) => !r.skipped)) {
|
|
67
|
+
questions.push({ id, status: 'judged' });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const defeated = rows.filter((r) => r.skipClass === 'defeated').map((r) => r.ruleName);
|
|
71
|
+
const broken = rows.filter((r) => r.skipClass === 'config_invalid').map((r) => r.ruleName);
|
|
72
|
+
if (defeated.length > 0) {
|
|
73
|
+
questions.push({ id, status: 'unjudged', why: `defeated: ${defeated.join(', ')} could not judge this output (sandbox budget)` });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (broken.length > 0) {
|
|
77
|
+
questions.push({ id, status: 'unjudged', why: `config_invalid: ${broken.join(', ')} has a broken definition` });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const missing = new Set();
|
|
81
|
+
for (const r of rows) {
|
|
82
|
+
const saw = new Set(r.saw ?? []);
|
|
83
|
+
// The rule's needs are not on the result; what it lacked is what its
|
|
84
|
+
// skip reason names. Prefer the structured form when the stamp gave
|
|
85
|
+
// us `saw`: anything in NEEDS the call did not carry and this rule's
|
|
86
|
+
// family is known to read. Fall back to the skip reason text.
|
|
87
|
+
if (r.skipReason)
|
|
88
|
+
missing.add(r.skipReason);
|
|
89
|
+
else
|
|
90
|
+
for (const n of NEEDS)
|
|
91
|
+
if (!saw.has(n) && !seen.has(n))
|
|
92
|
+
missing.add(n);
|
|
93
|
+
}
|
|
94
|
+
questions.push({ id, status: 'unjudged', why: `not supplied: ${[...missing].join('; ')}` });
|
|
95
|
+
}
|
|
96
|
+
return { inputs, questions };
|
|
97
|
+
}
|
|
98
|
+
/** The critical rules that skipped — derived from the stamped flags on every read, never a column. */
|
|
99
|
+
export function deriveCriticalSkipped(ruleResults) {
|
|
100
|
+
if (!ruleResults.some((r) => r.critical !== undefined))
|
|
101
|
+
return undefined;
|
|
102
|
+
const names = ruleResults.filter((r) => r.skipped && r.critical).map((r) => r.ruleName);
|
|
103
|
+
return names.length > 0 ? names : undefined;
|
|
104
|
+
}
|
|
105
|
+
/** sha256 over the rules that ran — name, definition version, kind, effective criticality, weight — so two evaluations under the same ruleset hash the same. */
|
|
106
|
+
export function rulesetHash(rules, resolve) {
|
|
107
|
+
const rows = rules
|
|
108
|
+
.map((r) => [r.name, r.version ?? 0, r.kind ?? '', resolve(r).critical ? 1 : 0, r.weight])
|
|
109
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
110
|
+
return createHash('sha256').update(JSON.stringify(rows)).digest('hex').slice(0, 16);
|
|
111
|
+
}
|
|
112
|
+
/** sha256 over the evaluation configuration that shapes a verdict. */
|
|
113
|
+
export function configHash(config) {
|
|
114
|
+
const stable = JSON.stringify({
|
|
115
|
+
threshold: config.threshold,
|
|
116
|
+
ruleThresholds: Object.fromEntries(Object.entries(config.ruleThresholds ?? {}).sort(([a], [b]) => (a < b ? -1 : 1))),
|
|
117
|
+
criticalRules: [...(config.criticalRules ?? [])].sort(),
|
|
118
|
+
nonCriticalRules: [...(config.nonCriticalRules ?? [])].sort(),
|
|
119
|
+
});
|
|
120
|
+
return createHash('sha256').update(stable).digest('hex').slice(0, 16);
|
|
121
|
+
}
|
|
122
|
+
export function buildProvenance(input) {
|
|
123
|
+
return {
|
|
124
|
+
irisVersion: input.irisVersion,
|
|
125
|
+
rulesetHash: input.rulesetHash,
|
|
126
|
+
configHash: input.configHash,
|
|
127
|
+
thresholds: { default: input.threshold, ...(input.ruleThresholds ? { perRule: input.ruleThresholds } : {}) },
|
|
128
|
+
corpusVersion: publishedProvenance().corpusVersion,
|
|
129
|
+
judgedAt: input.judgedAt,
|
|
130
|
+
};
|
|
131
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { PKG_VERSION } from './config/defaults.js';
|
|
|
6
6
|
import { createStorage } from './storage/index.js';
|
|
7
7
|
import { withDemoIngestGuard } from './storage/demo-guard.js';
|
|
8
8
|
import { createIrisServer } from './server.js';
|
|
9
|
+
import { runRetentionSweep, scheduleRetentionSweep } from './retention.js';
|
|
9
10
|
import { createStdioTransport } from './transport/stdio.js';
|
|
10
11
|
import { createHttpTransport } from './transport/http.js';
|
|
11
12
|
import { createDashboardServer } from './dashboard/server.js';
|
|
@@ -290,34 +291,10 @@ async function main() {
|
|
|
290
291
|
logger.info(`Loaded ${enabled.length} deployed custom rule(s) from ${customRuleStore.pathFor(LOCAL_TENANT)}`);
|
|
291
292
|
}
|
|
292
293
|
const httpServers = [];
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// call this per-tenant so retention applies uniformly; the adapter
|
|
298
|
-
// method already scopes DELETEs by tenant, so the behavior scales
|
|
299
|
-
// cleanly.
|
|
300
|
-
if (config.retention.days > 0) {
|
|
301
|
-
try {
|
|
302
|
-
const deletedTraces = await storage.deleteTracesOlderThan(LOCAL_TENANT, config.retention.days);
|
|
303
|
-
/*
|
|
304
|
-
* Evaluations too (#372). Deleting a trace only NULLs trace_id on
|
|
305
|
-
* its evaluations, so every eval row — output_text verbatim,
|
|
306
|
-
* including whatever no_pii flagged — used to outlive the retention
|
|
307
|
-
* window indefinitely while the traces around it were swept.
|
|
308
|
-
*/
|
|
309
|
-
const deletedEvals = await storage.deleteEvalResultsOlderThan(LOCAL_TENANT, config.retention.days);
|
|
310
|
-
if (deletedTraces + deletedEvals > 0) {
|
|
311
|
-
// Fold the WAL into the main file and truncate it, so the swept
|
|
312
|
-
// rows do not survive as readable text in iris.db-wal.
|
|
313
|
-
await storage.checkpoint();
|
|
314
|
-
logger.info(`Retention cleanup: deleted ${deletedTraces} trace(s) and ${deletedEvals} evaluation(s) older than ${config.retention.days} days`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
catch (err) {
|
|
318
|
-
logger.warn(`Retention cleanup skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
294
|
+
// Retention: one sweep at boot and the same sweep on a timer that never
|
|
295
|
+
// holds the process open (src/retention.ts).
|
|
296
|
+
await runRetentionSweep(storage, config, logger);
|
|
297
|
+
scheduleRetentionSweep(storage, config, logger);
|
|
321
298
|
if (config.transport.type === 'http') {
|
|
322
299
|
const { transport, httpServer } = await createHttpTransport(mcpServer, config, logger);
|
|
323
300
|
httpServers.push(httpServer);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { JudgeState } from './judge-enablement.js';
|
|
2
|
+
export declare const INSTRUCTIONS_MAX_CHARS = 2600;
|
|
3
|
+
export interface InstructionsInput {
|
|
4
|
+
ruleCount: number;
|
|
5
|
+
categories: readonly string[];
|
|
6
|
+
threshold: number;
|
|
7
|
+
critical: readonly string[];
|
|
8
|
+
judge: JudgeState;
|
|
9
|
+
}
|
|
10
|
+
export declare function buildInstructions(i: InstructionsInput): string;
|
|
11
|
+
export declare const EVALUATE_MY_AGENT_PROMPT = "evaluate-my-agent";
|
|
12
|
+
/**
|
|
13
|
+
* The prompt a client shows as a slash command: a walk of log → evaluate →
|
|
14
|
+
* read → explain, in plain words, carrying the version so a cached copy
|
|
15
|
+
* cannot outlive a bump. Rendered from the same facts as the instructions.
|
|
16
|
+
*/
|
|
17
|
+
export declare function evaluateMyAgentPrompt(what: 'output' | 'trace-file', version: string): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { JUDGE_KEY_VARS, judgeStateLine } from './judge-enablement.js';
|
|
2
|
+
export const INSTRUCTIONS_MAX_CHARS = 2600;
|
|
3
|
+
function prose(items) {
|
|
4
|
+
if (items.length <= 1)
|
|
5
|
+
return items.join('');
|
|
6
|
+
return `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
|
|
7
|
+
}
|
|
8
|
+
export function buildInstructions(i) {
|
|
9
|
+
const judgeHowTo = i.judge.enabled
|
|
10
|
+
? 'They spend your key, never a shared one; the cap is refused before any spend.'
|
|
11
|
+
: `To enable them, put your own ${JUDGE_KEY_VARS.anthropic} or ${JUDGE_KEY_VARS.openai} in the env block of the iris-eval entry in your MCP client config and restart the session; iris://capabilities carries the full steps.`;
|
|
12
|
+
const critical = i.critical.length > 0 ? i.critical.join(', ') : 'none';
|
|
13
|
+
const text = [
|
|
14
|
+
'Iris evaluates what an AI agent wrote and what it did. It runs locally, stores traces and verdicts in SQLite, and sends nothing anywhere unless you enable the judge.',
|
|
15
|
+
`Three verbs. LOG an execution with log_trace (input, output, tool_calls, cost). EVALUATE with evaluate_output: ${i.ruleCount} deterministic rules in ${prose(i.categories)}. ` +
|
|
16
|
+
'Pass input so the relevance and hallucination rules can judge; pass tool_calls or a trace_id so the trajectory rules can judge; pass cost_usd so the cost rules can judge. ' +
|
|
17
|
+
'A rule with nothing to judge SKIPS and is named; it is never counted as a pass. INSPECT with get_traces and list_rules, or read a resource.',
|
|
18
|
+
`Reading a verdict. passed is the ship verdict: true only when score clears the threshold (${i.threshold} on this server) AND no critical rule failed. ` +
|
|
19
|
+
'verdict.basis says which layer decided (policy_gate, detector_veto, score_below_threshold, clean, or no_rules when nothing could be judged) and verdict.by names the rules. ' +
|
|
20
|
+
`Critical on this server: ${critical} (configurable; list_rules shows the effective value). ` +
|
|
21
|
+
'coverage says which evaluation questions were judged and why the others were not. A critical rule that could not judge is named in critical_skipped: treat that as UNKNOWN, not clean. ' +
|
|
22
|
+
'score is a quality gradient over the rules that ran; never read it alone as a safety signal.',
|
|
23
|
+
`The LLM judge (evaluate_with_llm_judge) and the citation verifier (verify_citations) are ${judgeStateLine(i.judge)}. ${judgeHowTo}`,
|
|
24
|
+
'Resources: iris://capabilities (what this server can judge, what each rule needs, judge state, limits), iris://proof (measured precision and recall per rule with 95% intervals), ' +
|
|
25
|
+
'iris://traces/{trace_id}, iris://evaluations/{id}, iris://dashboard/summary. Responses link what they created.',
|
|
26
|
+
'Do not use Iris to validate arbitrary JSON Schema, to screen inputs before they reach an agent (the injection rule reads output), or for semantic judgment without a key.',
|
|
27
|
+
'Errors from a tool return {"error":{"code","message","recovery":[]}} with isError true; follow recovery before retrying. ' +
|
|
28
|
+
'An argument the schema rejects comes back as plain text naming IRIS_INVALID_ARGUMENT and the valid arguments.',
|
|
29
|
+
].join('\n\n');
|
|
30
|
+
if (text.length > INSTRUCTIONS_MAX_CHARS) {
|
|
31
|
+
throw new Error(`instructions are ${text.length} characters; the ceiling is ${INSTRUCTIONS_MAX_CHARS}`);
|
|
32
|
+
}
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
export const EVALUATE_MY_AGENT_PROMPT = 'evaluate-my-agent';
|
|
36
|
+
/**
|
|
37
|
+
* The prompt a client shows as a slash command: a walk of log → evaluate →
|
|
38
|
+
* read → explain, in plain words, carrying the version so a cached copy
|
|
39
|
+
* cannot outlive a bump. Rendered from the same facts as the instructions.
|
|
40
|
+
*/
|
|
41
|
+
export function evaluateMyAgentPrompt(what, version) {
|
|
42
|
+
const source = what === 'trace-file'
|
|
43
|
+
? 'Read the trace file I point you at (a JSON object or array with input, output and tool_calls per run).'
|
|
44
|
+
: 'Take the agent output I give you, with the input that produced it and the tool calls if I have them.';
|
|
45
|
+
return [
|
|
46
|
+
`Evaluate my agent with Iris ${version}. Do these steps and report in plain words.`,
|
|
47
|
+
`1. ${source}`,
|
|
48
|
+
'2. Log each run with log_trace, then evaluate it with evaluate_output, passing input, tool_calls (or the trace_id) and cost_usd whenever you have them so the relevance, trajectory and cost rules can judge.',
|
|
49
|
+
'3. Read the verdict: passed is the ship verdict; verdict.basis and verdict.by say which rule decided; coverage says what was not judged and why; critical_skipped means UNKNOWN, not clean.',
|
|
50
|
+
'4. For anything that failed, follow the resource link to iris://evaluations/{id} and quote the rule, its message and its evidence (offsets into my output, never a paraphrase of what the rule matched).',
|
|
51
|
+
'5. Tell me: what passed, what failed and why, what was not judged and what input would let Iris judge it. If I ask for a semantic judgment and the judge is not enabled, give me the recovery steps from the error instead of searching for them.',
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type JudgeProvider = 'anthropic' | 'openai';
|
|
2
|
+
export declare const JUDGE_KEY_VARS: Readonly<Record<JudgeProvider, string>>;
|
|
3
|
+
export declare const JUDGE_COST_CAP_VAR = "IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL";
|
|
4
|
+
export declare const JUDGE_DEFAULT_COST_CAP_USD = 0.25;
|
|
5
|
+
export interface JudgeState {
|
|
6
|
+
/** A key for at least one provider is present in this process's environment. */
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
/** The provider Iris would pick when a call names no provider: the first one with a key. */
|
|
9
|
+
provider: JudgeProvider | null;
|
|
10
|
+
/** Every provider with a key present. */
|
|
11
|
+
providers: JudgeProvider[];
|
|
12
|
+
/** The per-call spend cap the judge refuses beyond, before any spend. */
|
|
13
|
+
costCapUsd: number;
|
|
14
|
+
}
|
|
15
|
+
/** The per-call cap from the environment, else the shipped default. */
|
|
16
|
+
export declare function judgeCostCapUsd(): number;
|
|
17
|
+
export declare function judgeState(): JudgeState;
|
|
18
|
+
export declare const JUDGE_ENABLE_TITLE: string;
|
|
19
|
+
/**
|
|
20
|
+
* The enable workflow, one step per entry, from judge-enablement.json.
|
|
21
|
+
* Rendered verbatim into the IRIS_JUDGE_NOT_ENABLED error's `recovery`,
|
|
22
|
+
* the capabilities resource's `howToEnable`, and (through the truthbase)
|
|
23
|
+
* the README, the judge doc and the skill files. Change the JSON and
|
|
24
|
+
* nowhere else.
|
|
25
|
+
*/
|
|
26
|
+
export declare const JUDGE_ENABLE_STEPS: readonly string[];
|
|
27
|
+
/** The block as prose: the title in bold, then the numbered steps — the exact form every surface carries. */
|
|
28
|
+
export declare function judgeEnableBlock(): string;
|
|
29
|
+
/** Shared with scripts/claims/render-llms.mjs, which renders the same shape from .claims.json. */
|
|
30
|
+
export declare function renderJudgeEnableBlock(title: string, steps: readonly string[]): string;
|
|
31
|
+
/** The steps as an error's `recovery[]`, with the variable the call needed named first. */
|
|
32
|
+
export declare function judgeRecovery(provider: JudgeProvider): string[];
|
|
33
|
+
/** One line for the instructions and the self-test: what this process can see. */
|
|
34
|
+
export declare function judgeStateLine(state: JudgeState): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The judge is a user-keyed feature (founder ruling, 2026-09-04): an end
|
|
3
|
+
* user enables it by supplying their own provider key, and every surface
|
|
4
|
+
* that mentions it has to say so in the same words — the error a tool
|
|
5
|
+
* returns without a key, the capabilities resource, the server
|
|
6
|
+
* instructions, the self-test, the README and the docs. The words live
|
|
7
|
+
* once, in judge-enablement.json beside this file: the runtime imports
|
|
8
|
+
* it, the truthbase generator copies it into .claims.json, the skill files
|
|
9
|
+
* render it as a slot, and tests/judge-enablement-surfaces.test.ts asserts
|
|
10
|
+
* the README and the judge doc carry the same block verbatim.
|
|
11
|
+
*
|
|
12
|
+
* What "enabled" means: a key for at least one provider reached THIS
|
|
13
|
+
* process's environment. That is the only fact Iris can check, and it is
|
|
14
|
+
* the fact users get wrong — a key exported in a shell is not passed to
|
|
15
|
+
* the child process an MCP client spawns unless the client's config says
|
|
16
|
+
* so. Step 2 exists because of that.
|
|
17
|
+
*/
|
|
18
|
+
import enable from './judge-enablement.json' with { type: 'json' };
|
|
19
|
+
export const JUDGE_KEY_VARS = {
|
|
20
|
+
anthropic: 'IRIS_ANTHROPIC_API_KEY',
|
|
21
|
+
openai: 'IRIS_OPENAI_API_KEY',
|
|
22
|
+
};
|
|
23
|
+
export const JUDGE_COST_CAP_VAR = 'IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL';
|
|
24
|
+
export const JUDGE_DEFAULT_COST_CAP_USD = 0.25;
|
|
25
|
+
/** The per-call cap from the environment, else the shipped default. */
|
|
26
|
+
export function judgeCostCapUsd() {
|
|
27
|
+
const raw = process.env.IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL;
|
|
28
|
+
if (raw) {
|
|
29
|
+
const parsed = Number(raw);
|
|
30
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
return JUDGE_DEFAULT_COST_CAP_USD;
|
|
34
|
+
}
|
|
35
|
+
export function judgeState() {
|
|
36
|
+
// Literal reads on purpose: the docs contract and the manifest parity
|
|
37
|
+
// test grep `process.env.IRIS_*` to learn what the server reads.
|
|
38
|
+
const present = {
|
|
39
|
+
anthropic: Boolean(process.env.IRIS_ANTHROPIC_API_KEY),
|
|
40
|
+
openai: Boolean(process.env.IRIS_OPENAI_API_KEY),
|
|
41
|
+
};
|
|
42
|
+
const providers = Object.keys(JUDGE_KEY_VARS).filter((p) => present[p]);
|
|
43
|
+
return {
|
|
44
|
+
enabled: providers.length > 0,
|
|
45
|
+
provider: providers[0] ?? null,
|
|
46
|
+
providers,
|
|
47
|
+
costCapUsd: judgeCostCapUsd(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export const JUDGE_ENABLE_TITLE = enable.title;
|
|
51
|
+
/**
|
|
52
|
+
* The enable workflow, one step per entry, from judge-enablement.json.
|
|
53
|
+
* Rendered verbatim into the IRIS_JUDGE_NOT_ENABLED error's `recovery`,
|
|
54
|
+
* the capabilities resource's `howToEnable`, and (through the truthbase)
|
|
55
|
+
* the README, the judge doc and the skill files. Change the JSON and
|
|
56
|
+
* nowhere else.
|
|
57
|
+
*/
|
|
58
|
+
export const JUDGE_ENABLE_STEPS = enable.steps;
|
|
59
|
+
/** The block as prose: the title in bold, then the numbered steps — the exact form every surface carries. */
|
|
60
|
+
export function judgeEnableBlock() {
|
|
61
|
+
return renderJudgeEnableBlock(JUDGE_ENABLE_TITLE, JUDGE_ENABLE_STEPS);
|
|
62
|
+
}
|
|
63
|
+
/** Shared with scripts/claims/render-llms.mjs, which renders the same shape from .claims.json. */
|
|
64
|
+
export function renderJudgeEnableBlock(title, steps) {
|
|
65
|
+
return [`**${title}**`, ...steps.map((s, i) => `${i + 1}. ${s}`)].join('\n');
|
|
66
|
+
}
|
|
67
|
+
/** The steps as an error's `recovery[]`, with the variable the call needed named first. */
|
|
68
|
+
export function judgeRecovery(provider) {
|
|
69
|
+
return [`Set ${JUDGE_KEY_VARS[provider]} for the process that runs Iris.`, ...JUDGE_ENABLE_STEPS];
|
|
70
|
+
}
|
|
71
|
+
/** One line for the instructions and the self-test: what this process can see. */
|
|
72
|
+
export function judgeStateLine(state) {
|
|
73
|
+
if (state.enabled) {
|
|
74
|
+
const list = state.providers.join(' and ');
|
|
75
|
+
return `enabled (${list} key found in this process; each call is capped at ${state.costCapUsd} USD)`;
|
|
76
|
+
}
|
|
77
|
+
return `not enabled (no ${JUDGE_KEY_VARS.anthropic} or ${JUDGE_KEY_VARS.openai} reached this process)`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Enable the LLM judge (optional; the deterministic rules never need it)",
|
|
3
|
+
"steps": [
|
|
4
|
+
"Get an API key from Anthropic or OpenAI.",
|
|
5
|
+
"Put it in the environment of the process that runs Iris, not only your shell. Claude Code, Claude Desktop, Cursor and most MCP clients: the \"env\" block of the iris-eval entry in your MCP config — \"iris-eval\": { \"command\": \"npx\", \"args\": [\"-y\", \"@iris-eval/mcp-server\"], \"env\": { \"IRIS_ANTHROPIC_API_KEY\": \"sk-ant-...\" } } (IRIS_OPENAI_API_KEY for an OpenAI key). Docker: -e IRIS_ANTHROPIC_API_KEY=... on the run command. HTTP or CI: export it before starting iris-mcp.",
|
|
6
|
+
"Restart the MCP session. A running process never sees a variable set after it started.",
|
|
7
|
+
"Confirm from inside your client: read iris://capabilities — judge.enabled must be true there. A key exported in your shell is not passed to the process your client spawns unless its config lists it. On a machine, `npx @iris-eval/mcp-server --self-test` prints the judge line for that shell, and GET /api/v1/health reports judge.enabled on a running dashboard.",
|
|
8
|
+
"Spend guard: each call is capped by IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL (default 0.25 USD) and refused before any spend if the worst case would exceed it. Iris calls the provider directly with your key and never proxies it."
|
|
9
|
+
]
|
|
10
|
+
}
|
package/dist/preferences.d.ts
CHANGED
|
@@ -22,9 +22,9 @@ export declare const PreferencesSchema: z.ZodObject<{
|
|
|
22
22
|
firstSeen: z.ZodOptional<z.ZodString>;
|
|
23
23
|
dismissedBanners: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
24
24
|
theme: z.ZodDefault<z.ZodEnum<{
|
|
25
|
+
system: "system";
|
|
25
26
|
light: "light";
|
|
26
27
|
dark: "dark";
|
|
27
|
-
system: "system";
|
|
28
28
|
}>>;
|
|
29
29
|
momentFilters: z.ZodDefault<z.ZodObject<{
|
|
30
30
|
agentName: z.ZodOptional<z.ZodString>;
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The one prompt: `evaluate-my-agent`. A client that supports prompts
|
|
3
|
+
* shows it as a slash command; a client that does not never sees it, and
|
|
4
|
+
* nothing in the README depends on it. Rendered from the same facts as
|
|
5
|
+
* the server instructions (src/instructions.ts) and carrying the version,
|
|
6
|
+
* so a cached copy cannot outlive a bump.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { EVALUATE_MY_AGENT_PROMPT, evaluateMyAgentPrompt } from './instructions.js';
|
|
10
|
+
export const PROMPT_NAMES = [EVALUATE_MY_AGENT_PROMPT];
|
|
11
|
+
export function registerPrompts(server, version) {
|
|
12
|
+
server.registerPrompt(EVALUATE_MY_AGENT_PROMPT, {
|
|
13
|
+
title: 'Evaluate my agent',
|
|
14
|
+
description: 'A walk through logging an agent run, evaluating it and reading the verdict with Iris, in plain words.',
|
|
15
|
+
argsSchema: {
|
|
16
|
+
what: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('"output" (default): you have an agent output to hand; "trace-file": you have a file of runs to evaluate'),
|
|
20
|
+
},
|
|
21
|
+
}, ({ what }) => ({
|
|
22
|
+
messages: [
|
|
23
|
+
{
|
|
24
|
+
role: 'user',
|
|
25
|
+
content: { type: 'text', text: evaluateMyAgentPrompt(what === 'trace-file' ? 'trace-file' : 'output', version) },
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { IStorageAdapter } from '../types/query.js';
|
|
3
|
-
|
|
3
|
+
import type { Capabilities } from '../capabilities.js';
|
|
4
|
+
/** The MCP resource-not-found code (the spec reserves -32002 for it). */
|
|
5
|
+
export declare const RESOURCE_NOT_FOUND = -32002;
|
|
6
|
+
export declare function registerAllResources(server: McpServer, storage: IStorageAdapter, capabilities: () => Capabilities): void;
|
package/dist/resources/index.js
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/*
|
|
2
|
+
* The resources, registered the way the SDK wants them: fixed URIs with
|
|
3
|
+
* registerResource, parameterised ones with a ResourceTemplate (so a
|
|
4
|
+
* client's resources/templates/list shows the shape, and the SDK parses
|
|
5
|
+
* the variable instead of the handler splitting a path). A resource that
|
|
6
|
+
* does not exist is the protocol's resource-not-found error, never a 200
|
|
7
|
+
* body with an "error" key a client would have to sniff for.
|
|
8
|
+
*/
|
|
9
|
+
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { ruleProof } from '../capabilities.js';
|
|
12
|
+
import { publishedProvenance, publishedRuleNames } from '../eval/accuracy.js';
|
|
13
|
+
import { toEvaluationResponse } from '../eval/response.js';
|
|
14
|
+
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
15
|
+
import { CAPABILITIES_RESOURCE_URI, DASHBOARD_SUMMARY_RESOURCE_URI, EVALUATION_RESOURCE_TEMPLATE, PROOF_RESOURCE_URI, TRACE_RESOURCE_TEMPLATE, } from './uris.js';
|
|
16
|
+
/** The MCP resource-not-found code (the spec reserves -32002 for it). */
|
|
17
|
+
export const RESOURCE_NOT_FOUND = -32002;
|
|
18
|
+
function notFound(uri, what) {
|
|
19
|
+
return new McpError(RESOURCE_NOT_FOUND, `Resource not found: ${uri} — no ${what} with that id is stored for this tenant`, { uri });
|
|
20
|
+
}
|
|
21
|
+
const json = (uri, body) => ({
|
|
22
|
+
contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(body, null, 2) }],
|
|
23
|
+
});
|
|
24
|
+
export function registerAllResources(server, storage, capabilities) {
|
|
25
|
+
server.registerResource('capabilities', CAPABILITIES_RESOURCE_URI, {
|
|
26
|
+
title: 'Capabilities',
|
|
27
|
+
description: 'What this server can judge: the rule roster with what each rule needs and its published accuracy, the judge state with the steps that enable it, the citation verifier posture, the dashboard address, the limits, and the tools, resources and prompts registered.',
|
|
28
|
+
mimeType: 'application/json',
|
|
29
|
+
}, async (uri) => json(uri.href, capabilities()));
|
|
30
|
+
server.registerResource('proof', PROOF_RESOURCE_URI, {
|
|
31
|
+
title: 'Proof',
|
|
32
|
+
description: 'The published accuracy of every measured built-in rule (the same numbers as https://iris-eval.com/proof): precision and recall on the proof corpus with 95% intervals, the confusion counts, positive predictive value at four prevalences, and the corpus version and labelling the numbers come from.',
|
|
33
|
+
mimeType: 'application/json',
|
|
34
|
+
}, async (uri) => {
|
|
35
|
+
const prov = publishedProvenance();
|
|
36
|
+
const rules = {};
|
|
37
|
+
for (const name of publishedRuleNames())
|
|
38
|
+
rules[name] = ruleProof(name);
|
|
39
|
+
return json(uri.href, {
|
|
40
|
+
corpusVersion: prov.corpusVersion,
|
|
41
|
+
release: prov.release,
|
|
42
|
+
labelling: prov.labelling,
|
|
43
|
+
method: 'Each rule is run over its proof-corpus family; precision and recall carry Wilson 95% intervals; ppvAt is the positive predictive value at the stated prevalence, from the same counts. Reproduce with `npm run proof` in the repository.',
|
|
44
|
+
rules,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
server.registerResource('dashboard-summary', DASHBOARD_SUMMARY_RESOURCE_URI, { title: 'Dashboard summary', description: 'Dashboard summary with key metrics and trends for the last hour', mimeType: 'application/json' }, async (uri) => json(uri.href, await storage.getDashboardSummary(LOCAL_TENANT)));
|
|
48
|
+
server.registerResource('trace-detail', new ResourceTemplate(TRACE_RESOURCE_TEMPLATE, { list: undefined }), { title: 'Trace', description: 'One stored trace with its spans and every evaluation linked to it', mimeType: 'application/json' }, async (uri, variables) => {
|
|
49
|
+
const traceId = String(variables.trace_id ?? '');
|
|
50
|
+
const trace = await storage.getTrace(LOCAL_TENANT, traceId);
|
|
51
|
+
if (!trace)
|
|
52
|
+
throw notFound(uri.href, 'trace');
|
|
53
|
+
const [spans, evals] = await Promise.all([
|
|
54
|
+
storage.getSpansByTraceId(LOCAL_TENANT, traceId),
|
|
55
|
+
storage.getEvalsByTraceId(LOCAL_TENANT, traceId),
|
|
56
|
+
]);
|
|
57
|
+
return json(uri.href, { trace, spans, evals: evals.map((e) => toEvaluationResponse(e, { traceId })) });
|
|
58
|
+
});
|
|
59
|
+
server.registerResource('evaluation-detail', new ResourceTemplate(EVALUATION_RESOURCE_TEMPLATE, { list: undefined }), { title: 'Evaluation', description: 'One stored evaluation, in the same shape evaluate_output returned it: verdict, coverage, provenance and every rule result with its evidence', mimeType: 'application/json' }, async (uri, variables) => {
|
|
60
|
+
const id = String(variables.id ?? '');
|
|
61
|
+
const result = await storage.getEvalById(LOCAL_TENANT, id);
|
|
62
|
+
if (!result)
|
|
63
|
+
throw notFound(uri.href, 'evaluation');
|
|
64
|
+
return json(uri.href, toEvaluationResponse(result, { traceId: result.trace_id }));
|
|
65
|
+
});
|
|
6
66
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const CAPABILITIES_RESOURCE_URI = "iris://capabilities";
|
|
2
|
+
export declare const PROOF_RESOURCE_URI = "iris://proof";
|
|
3
|
+
export declare const DASHBOARD_SUMMARY_RESOURCE_URI = "iris://dashboard/summary";
|
|
4
|
+
export declare const TRACE_RESOURCE_TEMPLATE = "iris://traces/{trace_id}";
|
|
5
|
+
export declare const EVALUATION_RESOURCE_TEMPLATE = "iris://evaluations/{id}";
|
|
6
|
+
export declare const RESOURCE_URIS: readonly ["iris://capabilities", "iris://proof", "iris://dashboard/summary", "iris://traces/{trace_id}", "iris://evaluations/{id}"];
|
|
7
|
+
/** The URI of one stored trace / evaluation, from its template. */
|
|
8
|
+
export declare const traceUri: (traceId: string) => string;
|
|
9
|
+
export declare const evaluationUri: (id: string) => string;
|
|
10
|
+
/** Fixed URIs (resources/list) versus templates (resources/templates/list). */
|
|
11
|
+
export declare const FIXED_RESOURCE_URIS: readonly ["iris://capabilities", "iris://proof", "iris://dashboard/summary"];
|
|
12
|
+
export declare const RESOURCE_TEMPLATES: readonly ["iris://traces/{trace_id}", "iris://evaluations/{id}"];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Every resource URI this server registers, as literals: the capabilities
|
|
3
|
+
* object lists them, the docs contract checks prose against them, and a
|
|
4
|
+
* test asserts the list equals what resources/list and
|
|
5
|
+
* resources/templates/list return.
|
|
6
|
+
*/
|
|
7
|
+
export const CAPABILITIES_RESOURCE_URI = 'iris://capabilities';
|
|
8
|
+
export const PROOF_RESOURCE_URI = 'iris://proof';
|
|
9
|
+
export const DASHBOARD_SUMMARY_RESOURCE_URI = 'iris://dashboard/summary';
|
|
10
|
+
export const TRACE_RESOURCE_TEMPLATE = 'iris://traces/{trace_id}';
|
|
11
|
+
export const EVALUATION_RESOURCE_TEMPLATE = 'iris://evaluations/{id}';
|
|
12
|
+
export const RESOURCE_URIS = [
|
|
13
|
+
CAPABILITIES_RESOURCE_URI,
|
|
14
|
+
PROOF_RESOURCE_URI,
|
|
15
|
+
DASHBOARD_SUMMARY_RESOURCE_URI,
|
|
16
|
+
TRACE_RESOURCE_TEMPLATE,
|
|
17
|
+
EVALUATION_RESOURCE_TEMPLATE,
|
|
18
|
+
];
|
|
19
|
+
/** The URI of one stored trace / evaluation, from its template. */
|
|
20
|
+
export const traceUri = (traceId) => TRACE_RESOURCE_TEMPLATE.replace('{trace_id}', traceId);
|
|
21
|
+
export const evaluationUri = (id) => EVALUATION_RESOURCE_TEMPLATE.replace('{id}', id);
|
|
22
|
+
/** Fixed URIs (resources/list) versus templates (resources/templates/list). */
|
|
23
|
+
export const FIXED_RESOURCE_URIS = [CAPABILITIES_RESOURCE_URI, PROOF_RESOURCE_URI, DASHBOARD_SUMMARY_RESOURCE_URI];
|
|
24
|
+
export const RESOURCE_TEMPLATES = [TRACE_RESOURCE_TEMPLATE, EVALUATION_RESOURCE_TEMPLATE];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IrisConfig } from './types/config.js';
|
|
2
|
+
import type { IStorageAdapter } from './types/query.js';
|
|
3
|
+
export interface RetentionLogger {
|
|
4
|
+
info(msg: string): void;
|
|
5
|
+
warn(msg: string): void;
|
|
6
|
+
}
|
|
7
|
+
export interface SweepOutcome {
|
|
8
|
+
deletedTraces: number;
|
|
9
|
+
deletedEvals: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One sweep for the local tenant. Never throws — a failed sweep is logged
|
|
13
|
+
* and the server keeps serving; the next tick tries again.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runRetentionSweep(storage: IStorageAdapter, config: IrisConfig, logger: RetentionLogger): Promise<SweepOutcome | null>;
|
|
16
|
+
/**
|
|
17
|
+
* The timer. Returns the handle (already unref'd) so a caller can clear it
|
|
18
|
+
* on shutdown, or null when retention or the timer is disabled.
|
|
19
|
+
*/
|
|
20
|
+
export declare function scheduleRetentionSweep(storage: IStorageAdapter, config: IrisConfig, logger: RetentionLogger): NodeJS.Timeout | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LOCAL_TENANT } from './types/tenant.js';
|
|
2
|
+
/**
|
|
3
|
+
* One sweep for the local tenant. Never throws — a failed sweep is logged
|
|
4
|
+
* and the server keeps serving; the next tick tries again.
|
|
5
|
+
*/
|
|
6
|
+
export async function runRetentionSweep(storage, config, logger) {
|
|
7
|
+
if (config.retention.days <= 0)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const deletedTraces = await storage.deleteTracesOlderThan(LOCAL_TENANT, config.retention.days);
|
|
11
|
+
/*
|
|
12
|
+
* Evaluations too (#372). Deleting a trace only NULLs trace_id on
|
|
13
|
+
* its evaluations, so every eval row — output_text verbatim,
|
|
14
|
+
* including whatever no_pii flagged — used to outlive the retention
|
|
15
|
+
* window indefinitely while the traces around it were swept.
|
|
16
|
+
*/
|
|
17
|
+
const deletedEvals = await storage.deleteEvalResultsOlderThan(LOCAL_TENANT, config.retention.days);
|
|
18
|
+
if (deletedTraces + deletedEvals > 0) {
|
|
19
|
+
// Fold the WAL into the main file and truncate it, so the swept
|
|
20
|
+
// rows do not survive as readable text in iris.db-wal.
|
|
21
|
+
await storage.checkpoint();
|
|
22
|
+
logger.info(`Retention cleanup: deleted ${deletedTraces} trace(s) and ${deletedEvals} evaluation(s) older than ${config.retention.days} days`);
|
|
23
|
+
}
|
|
24
|
+
return { deletedTraces, deletedEvals };
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
logger.warn(`Retention cleanup skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The timer. Returns the handle (already unref'd) so a caller can clear it
|
|
33
|
+
* on shutdown, or null when retention or the timer is disabled.
|
|
34
|
+
*/
|
|
35
|
+
export function scheduleRetentionSweep(storage, config, logger) {
|
|
36
|
+
const hours = config.retention.sweepIntervalHours;
|
|
37
|
+
if (config.retention.days <= 0 || !(hours > 0))
|
|
38
|
+
return null;
|
|
39
|
+
const timer = setInterval(() => {
|
|
40
|
+
void runRetentionSweep(storage, config, logger);
|
|
41
|
+
}, hours * 60 * 60 * 1000);
|
|
42
|
+
timer.unref();
|
|
43
|
+
return timer;
|
|
44
|
+
}
|
package/dist/self-test.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const SELF_TEST_STEPS: {
|
|
2
2
|
readonly configuredHome: "configured IRIS_HOME is writable";
|
|
3
|
+
readonly judge: "judge key in this shell";
|
|
3
4
|
readonly tempHome: "create isolated temp home";
|
|
4
5
|
readonly storage: "initialize storage";
|
|
5
6
|
readonly trace: "log a trace";
|