@iris-eval/mcp-server 0.8.2 → 0.10.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 +9 -2
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +65 -0
- package/dist/config/defaults.js +17 -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-CeJbaq6m.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 +11 -30
- package/dist/dashboard/seed-demo-data.js +1 -1
- 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/citation-verify/verifier.d.ts +16 -1
- package/dist/eval/citation-verify/verifier.js +14 -4
- package/dist/eval/compose.d.ts +57 -0
- package/dist/eval/compose.js +179 -0
- package/dist/eval/criticality.d.ts +15 -1
- package/dist/eval/criticality.js +6 -0
- package/dist/eval/decision-moment.js +33 -4
- package/dist/eval/dormant.d.ts +4 -0
- package/dist/eval/dormant.js +22 -0
- package/dist/eval/engine.d.ts +6 -2
- package/dist/eval/engine.js +126 -12
- 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 +30 -0
- package/dist/eval/llm-judge/evaluator.js +26 -2
- 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/risk.d.ts +60 -0
- package/dist/eval/risk.js +187 -0
- package/dist/eval/rules/completeness.js +36 -1
- package/dist/eval/rules/cost.d.ts +2 -2
- package/dist/eval/rules/cost.js +50 -6
- package/dist/eval/rules/custom.d.ts +0 -12
- package/dist/eval/rules/custom.js +22 -0
- package/dist/eval/rules/relevance.js +23 -2
- package/dist/eval/rules/safety.d.ts +6 -2
- package/dist/eval/rules/safety.js +224 -51
- package/dist/eval/seeded-random.d.ts +4 -0
- package/dist/eval/seeded-random.js +36 -0
- package/dist/eval/stamp.d.ts +14 -0
- package/dist/eval/stamp.js +89 -0
- package/dist/eval/stats.d.ts +33 -0
- package/dist/eval/stats.js +109 -0
- package/dist/eval/text/checksums.d.ts +23 -0
- package/dist/eval/text/checksums.js +97 -0
- package/dist/eval/text/normalise.d.ts +30 -0
- package/dist/eval/text/normalise.js +265 -0
- package/dist/eval/text/sentences.d.ts +15 -0
- package/dist/eval/text/sentences.js +149 -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 +17 -3
- 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 +149 -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 +39 -60
- package/dist/tools/evaluate-with-llm-judge.d.ts +34 -0
- package/dist/tools/evaluate-with-llm-judge.js +124 -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 +18 -2
- package/dist/tools/verify-citations.js +122 -96
- package/dist/types/config.d.ts +44 -0
- package/dist/types/eval.d.ts +309 -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,265 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* One normalisation pass, shared by every rule that matches text.
|
|
3
|
+
*
|
|
4
|
+
* The problem it solves is measured, not hypothetical. `npm run proof`
|
|
5
|
+
* publishes a transforms table: for every positive a critical rule catches,
|
|
6
|
+
* the text inside the evidence span is transformed the way an evader would
|
|
7
|
+
* transform it, and the rule is re-run. Before this module, `no_pii` kept
|
|
8
|
+
* 38% of its catches under a zero-width space, 22% under Cyrillic
|
|
9
|
+
* homoglyphs and **none at all** under full-width digits, and
|
|
10
|
+
* `no_blocklist_words` survived nothing but a change of case.
|
|
11
|
+
*
|
|
12
|
+
* What it does, in order, per grapheme cluster:
|
|
13
|
+
* 1. drops format characters that carry no meaning — zero-width spaces
|
|
14
|
+
* and joiners, the soft hyphen, the byte-order mark;
|
|
15
|
+
* 2. NFKC-folds the cluster, which turns full-width and mathematical
|
|
16
|
+
* alphanumerics into ASCII (4111 → 4111, 𝐩𝐚𝐬𝐬 → pass);
|
|
17
|
+
* 3. maps the confusables NFKC does NOT fold — Cyrillic and Greek letters
|
|
18
|
+
* that are drawn like Latin ones (раssword with a Cyrillic а and р);
|
|
19
|
+
* 4. collapses every run of whitespace to ONE character — a newline when
|
|
20
|
+
* the run contains one, a space otherwise. Line structure is meaning:
|
|
21
|
+
* a forged "System:" line and a fenced block are line-shaped, and
|
|
22
|
+
* flattening newlines to spaces measurably cost the injection rule
|
|
23
|
+
* recall on three transforms. Horizontal runs carry no such meaning.
|
|
24
|
+
*
|
|
25
|
+
* What it deliberately does NOT do is leetspeak (0 → o, 1 → i). That
|
|
26
|
+
* substitution is correct for injection phrasing and catastrophic for
|
|
27
|
+
* everything else: it would turn a credit card number into letters and
|
|
28
|
+
* blind every digit-based detector. The injection rule applies it on top of
|
|
29
|
+
* this pass, to this pass's output, and owns it alone.
|
|
30
|
+
*
|
|
31
|
+
* Every rule that matches on `text` reports evidence through `map`, so a
|
|
32
|
+
* span still indexes the RAW output the caller sent — the arc-1 contract
|
|
33
|
+
* ("spans are offsets into the raw text") is what makes redaction and the
|
|
34
|
+
* transforms measurement correct, and normalising without a map would
|
|
35
|
+
* quietly break it.
|
|
36
|
+
*/
|
|
37
|
+
/** Format characters that carry no textual meaning and are pure evasion when they sit inside a token. */
|
|
38
|
+
const DROPPED = new Set([
|
|
39
|
+
'', // zero-width space
|
|
40
|
+
'', // zero-width non-joiner
|
|
41
|
+
'', // zero-width joiner
|
|
42
|
+
'', // left-to-right mark
|
|
43
|
+
'', // right-to-left mark
|
|
44
|
+
'', // word joiner
|
|
45
|
+
'', // byte-order mark / zero-width no-break space
|
|
46
|
+
'', // soft hyphen
|
|
47
|
+
]);
|
|
48
|
+
/**
|
|
49
|
+
* Letters that NFKC leaves alone but a reader cannot tell apart from Latin.
|
|
50
|
+
* Cyrillic first, then Greek; lowercase and uppercase where both are
|
|
51
|
+
* confusable. Deliberately conservative: only characters whose common
|
|
52
|
+
* rendering is indistinguishable in the fonts an agent's output is read in.
|
|
53
|
+
*/
|
|
54
|
+
const CONFUSABLES = new Map([
|
|
55
|
+
// Cyrillic → Latin
|
|
56
|
+
['а', 'a'], ['А', 'A'],
|
|
57
|
+
['е', 'e'], ['Е', 'E'],
|
|
58
|
+
['о', 'o'], ['О', 'O'],
|
|
59
|
+
['р', 'p'], ['Р', 'P'],
|
|
60
|
+
['с', 'c'], ['С', 'C'],
|
|
61
|
+
['х', 'x'], ['Х', 'X'],
|
|
62
|
+
['у', 'y'], ['У', 'Y'],
|
|
63
|
+
['к', 'k'], ['К', 'K'],
|
|
64
|
+
['м', 'm'], ['М', 'M'],
|
|
65
|
+
['н', 'h'], ['Н', 'H'],
|
|
66
|
+
['т', 't'], ['Т', 'T'],
|
|
67
|
+
['в', 'v'], ['В', 'B'],
|
|
68
|
+
['і', 'i'], ['І', 'I'],
|
|
69
|
+
['ј', 'j'], ['Ј', 'J'],
|
|
70
|
+
['ѕ', 's'], ['Ѕ', 'S'],
|
|
71
|
+
['б', '6'],
|
|
72
|
+
['г', 'r'],
|
|
73
|
+
['з', '3'],
|
|
74
|
+
['һ', 'h'],
|
|
75
|
+
['ҙ', 'z'],
|
|
76
|
+
// Greek → Latin
|
|
77
|
+
['ο', 'o'], ['Ο', 'O'],
|
|
78
|
+
['α', 'a'], ['Α', 'A'],
|
|
79
|
+
['ε', 'e'], ['Ε', 'E'],
|
|
80
|
+
['ρ', 'p'], ['Ρ', 'P'],
|
|
81
|
+
['τ', 't'], ['Τ', 'T'],
|
|
82
|
+
['ν', 'v'], ['Ν', 'N'],
|
|
83
|
+
['υ', 'u'], ['Υ', 'Y'],
|
|
84
|
+
['ι', 'i'], ['Ι', 'I'],
|
|
85
|
+
['κ', 'k'], ['Κ', 'K'],
|
|
86
|
+
['β', 'B'], ['Β', 'B'],
|
|
87
|
+
['η', 'n'], ['Η', 'H'],
|
|
88
|
+
['χ', 'x'], ['Χ', 'X'],
|
|
89
|
+
['μ', 'u'], ['Μ', 'M'],
|
|
90
|
+
['γ', 'y'], ['Ζ', 'Z'],
|
|
91
|
+
['Φ', 'O'],
|
|
92
|
+
// Other scripts whose letters are drawn as Latin
|
|
93
|
+
['ԁ', 'd'],
|
|
94
|
+
['ԛ', 'q'],
|
|
95
|
+
['ɡ', 'g'],
|
|
96
|
+
['ẞ', 'S'],
|
|
97
|
+
['ո', 'n'],
|
|
98
|
+
['ս', 'u'],
|
|
99
|
+
['օ', 'o'],
|
|
100
|
+
]);
|
|
101
|
+
/**
|
|
102
|
+
* Printable ASCII plus the newline. Nothing in that set folds, so the only
|
|
103
|
+
* thing that could change such a string is a whitespace RUN — which makes
|
|
104
|
+
* two linear scans a complete test for "this text is already normalised".
|
|
105
|
+
*
|
|
106
|
+
* This is the hot path and it is why the pass is affordable. Ordinary agent
|
|
107
|
+
* output is plain text; a one-megabyte payload of it used to cost a grapheme
|
|
108
|
+
* segmentation and a character-by-character rebuild, and the hostile-payload
|
|
109
|
+
* budget in the test battery caught exactly that.
|
|
110
|
+
*/
|
|
111
|
+
const PLAIN_TEXT = /^[\x20-\x7E\n]*$/;
|
|
112
|
+
const WHITESPACE_RUN = /\s\s/;
|
|
113
|
+
/** The result for text that is already in normal form: no copy, no map until asked. */
|
|
114
|
+
function identity(raw) {
|
|
115
|
+
let cached;
|
|
116
|
+
return {
|
|
117
|
+
text: raw,
|
|
118
|
+
unchanged: true,
|
|
119
|
+
get map() {
|
|
120
|
+
if (cached === undefined) {
|
|
121
|
+
cached = new Int32Array(raw.length + 1);
|
|
122
|
+
for (let i = 0; i <= raw.length; i++)
|
|
123
|
+
cached[i] = i;
|
|
124
|
+
}
|
|
125
|
+
return cached;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
let segmenter;
|
|
130
|
+
function graphemes(raw) {
|
|
131
|
+
if (typeof Intl?.Segmenter === 'function') {
|
|
132
|
+
segmenter ??= new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
133
|
+
return segmenter.segment(raw);
|
|
134
|
+
}
|
|
135
|
+
// Environments without Intl.Segmenter fall back to code points, which is
|
|
136
|
+
// correct for everything this pass folds and only differs on combining
|
|
137
|
+
// sequences it would leave alone anyway.
|
|
138
|
+
return [...raw];
|
|
139
|
+
}
|
|
140
|
+
const WHITESPACE = /\s/u;
|
|
141
|
+
const LINE_BREAK = /[\n\r\u2028\u2029]/u;
|
|
142
|
+
/** Folds `raw` for matching and returns the offset map that puts evidence back on the raw text. */
|
|
143
|
+
export function normalise(raw) {
|
|
144
|
+
// Already in normal form: two linear scans and no allocation at all.
|
|
145
|
+
if (PLAIN_TEXT.test(raw) && !WHITESPACE_RUN.test(raw))
|
|
146
|
+
return identity(raw);
|
|
147
|
+
const out = [];
|
|
148
|
+
const offsets = [];
|
|
149
|
+
/** The whitespace run being accumulated: where it started, and whether it broke a line. */
|
|
150
|
+
let run = null;
|
|
151
|
+
let changed = false;
|
|
152
|
+
/** False as soon as one output character does not sit at its own raw offset. */
|
|
153
|
+
let identityMap = true;
|
|
154
|
+
const push = (chars, at) => {
|
|
155
|
+
for (const ch of chars) {
|
|
156
|
+
if (at !== out.length)
|
|
157
|
+
identityMap = false;
|
|
158
|
+
out.push(ch);
|
|
159
|
+
offsets.push(at);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
/** Emits the pending whitespace run as one character: a newline if it broke a line, else a space. */
|
|
163
|
+
const flushRun = () => {
|
|
164
|
+
if (run === null)
|
|
165
|
+
return;
|
|
166
|
+
const ch = run.hadBreak ? '\n' : ' ';
|
|
167
|
+
if (raw.slice(run.at, run.at + 1) !== ch)
|
|
168
|
+
changed = true;
|
|
169
|
+
push(ch, run.at);
|
|
170
|
+
run = null;
|
|
171
|
+
};
|
|
172
|
+
const segments = graphemes(raw);
|
|
173
|
+
const iterate = (rawCluster, index) => {
|
|
174
|
+
/*
|
|
175
|
+
* Strip the format characters from INSIDE the cluster, not just from
|
|
176
|
+
* clusters that are one. A zero-width non-joiner between two digits
|
|
177
|
+
* binds into the neighbouring grapheme, so a whole-cluster test misses
|
|
178
|
+
* exactly the evasion this exists to fold.
|
|
179
|
+
*/
|
|
180
|
+
let cluster = rawCluster;
|
|
181
|
+
if (cluster.length > 1 || DROPPED.has(cluster)) {
|
|
182
|
+
let stripped = '';
|
|
183
|
+
for (const ch of cluster)
|
|
184
|
+
if (!DROPPED.has(ch))
|
|
185
|
+
stripped += ch;
|
|
186
|
+
if (stripped !== cluster) {
|
|
187
|
+
changed = true;
|
|
188
|
+
cluster = stripped;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (cluster === '')
|
|
192
|
+
return;
|
|
193
|
+
if (WHITESPACE.test(cluster)) {
|
|
194
|
+
const hadBreak = LINE_BREAK.test(cluster);
|
|
195
|
+
if (run === null)
|
|
196
|
+
run = { at: index, hadBreak };
|
|
197
|
+
else {
|
|
198
|
+
run.hadBreak ||= hadBreak;
|
|
199
|
+
changed = true;
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
flushRun();
|
|
204
|
+
let folded = cluster.normalize('NFKC');
|
|
205
|
+
if (folded !== cluster)
|
|
206
|
+
changed = true;
|
|
207
|
+
if (CONFUSABLES.size > 0) {
|
|
208
|
+
let mapped = '';
|
|
209
|
+
for (const ch of folded) {
|
|
210
|
+
const sub = CONFUSABLES.get(ch);
|
|
211
|
+
if (sub === undefined)
|
|
212
|
+
mapped += ch;
|
|
213
|
+
else {
|
|
214
|
+
mapped += sub;
|
|
215
|
+
changed = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
folded = mapped;
|
|
219
|
+
}
|
|
220
|
+
// A cluster that folds away entirely (a lone combining mark NFKC drops)
|
|
221
|
+
// contributes nothing; its offset is covered by the next kept character.
|
|
222
|
+
push(folded, index);
|
|
223
|
+
};
|
|
224
|
+
if (Array.isArray(segments)) {
|
|
225
|
+
let at = 0;
|
|
226
|
+
for (const cluster of segments) {
|
|
227
|
+
iterate(cluster, at);
|
|
228
|
+
at += cluster.length;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
for (const { segment, index } of segments)
|
|
233
|
+
iterate(segment, index);
|
|
234
|
+
}
|
|
235
|
+
flushRun();
|
|
236
|
+
const text = out.join('');
|
|
237
|
+
let cached;
|
|
238
|
+
return {
|
|
239
|
+
text,
|
|
240
|
+
unchanged: !changed && identityMap && text.length === raw.length,
|
|
241
|
+
get map() {
|
|
242
|
+
if (cached === undefined) {
|
|
243
|
+
cached = new Int32Array(offsets.length + 1);
|
|
244
|
+
cached.set(offsets);
|
|
245
|
+
cached[offsets.length] = raw.length;
|
|
246
|
+
}
|
|
247
|
+
return cached;
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* A span in normalised coordinates as a span in raw coordinates. Always
|
|
253
|
+
* widens rather than narrows: when characters were dropped between the last
|
|
254
|
+
* matched character and the next kept one, the raw span covers them, which
|
|
255
|
+
* is what a reader wants — the evasion is part of the evidence.
|
|
256
|
+
*/
|
|
257
|
+
export function toRawSpan(n, start, end) {
|
|
258
|
+
// The identity case never touches the map, so no array is built for the
|
|
259
|
+
// ordinary text that makes up almost every evaluation.
|
|
260
|
+
if (n.unchanged)
|
|
261
|
+
return [Math.max(0, start), Math.max(start, end)];
|
|
262
|
+
const s = Math.max(0, Math.min(start, n.map.length - 1));
|
|
263
|
+
const e = Math.max(s, Math.min(end, n.map.length - 1));
|
|
264
|
+
return [n.map[s], n.map[e]];
|
|
265
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named sentencesOf, not sentencesOf: the hallucination rule in
|
|
3
|
+
* src/eval/rules/safety.ts has its own sentencesOf that also breaks on
|
|
4
|
+
* every newline, because it wants per-line units for grounding checks. The
|
|
5
|
+
* two are not the same job and unifying them would move that rule's
|
|
6
|
+
* numbers, so it is a separate measured change, not a rename.
|
|
7
|
+
*
|
|
8
|
+
* Splits `text` into sentences. Never returns an empty sentence; a text with
|
|
9
|
+
* no terminator is one sentence. Line breaks do not split on their own — a
|
|
10
|
+
* wrapped paragraph is one sentence — but a blank line does, because a new
|
|
11
|
+
* block is a new thought and a bullet list is not one long sentence.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sentencesOf(text: string): string[];
|
|
14
|
+
/** How many sentences the text contains. The one number `sentence_count` reports. */
|
|
15
|
+
export declare function countSentences(text: string): number;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* One sentence splitter, for the two rules that count or walk sentences.
|
|
3
|
+
*
|
|
4
|
+
* Both had their own, and both were wrong in the same way. `sentence_count`
|
|
5
|
+
* split on `/[.!?]+/`, so "The latency is 3.5 seconds." counted as two
|
|
6
|
+
* sentences and "Dr. Chen approved it." as two more; the arc-zero review
|
|
7
|
+
* measured the damage at 43% of that rule's family. `topic_consistency`
|
|
8
|
+
* split on a full stop followed by whitespace, which fixes the decimal only
|
|
9
|
+
* when the decimal has no space after it and never fixes the abbreviation.
|
|
10
|
+
*
|
|
11
|
+
* A sentence ends at `.`, `!` or `?` when what follows looks like the start
|
|
12
|
+
* of a new sentence and what precedes is not one of the things that ends in
|
|
13
|
+
* a full stop without ending a sentence:
|
|
14
|
+
*
|
|
15
|
+
* - never between digits, so 3.5 and 1.2.3 stay whole;
|
|
16
|
+
* - never after a closed list of abbreviations (Dr, Mr, Mrs, Ms, e.g,
|
|
17
|
+
* i.e, vs, etc, No, Fig, St, Inc, Ltd, Jr, Sr, approx, cf, al);
|
|
18
|
+
* - never after a single capital letter, which is an initial (J. Smith);
|
|
19
|
+
* - only when the next non-space character starts a sentence: an
|
|
20
|
+
* uppercase letter, an opening quote or bracket, or a digit.
|
|
21
|
+
*
|
|
22
|
+
* The list is closed on purpose. An open-ended abbreviation heuristic
|
|
23
|
+
* (a short token ending in a full stop) swallows real sentence ends —
|
|
24
|
+
* "It was fun. Then we left." — and this splitter is used to COUNT, where
|
|
25
|
+
* missing a break is as wrong as inventing one.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Abbreviations that are essentially never the last word of a sentence, so
|
|
29
|
+
* the full stop after them is punctuation and not an end. Lowercase, no
|
|
30
|
+
* trailing stop.
|
|
31
|
+
*/
|
|
32
|
+
const ALWAYS_ABBREVIATION = new Set([
|
|
33
|
+
'dr', 'mr', 'mrs', 'ms', 'prof', 'sr', 'jr', 'st', 'mt',
|
|
34
|
+
'e.g', 'i.e', 'vs', 'al', 'cf', 'approx', 'est',
|
|
35
|
+
'dept', 'univ', 'a.m', 'p.m', 'u.s', 'u.k',
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* Abbreviations that are ALSO ordinary sentence endings — "shipped in Oct.
|
|
39
|
+
* The rollout held" ends a sentence; "shipped on Oct. 5" does not. What
|
|
40
|
+
* separates them is what follows: a number means the abbreviation is being
|
|
41
|
+
* used as a label, anything else means the sentence ended. Guessing either
|
|
42
|
+
* way unconditionally is wrong about half the time, and this splitter is
|
|
43
|
+
* used to COUNT, where a missed break costs exactly as much as an invented
|
|
44
|
+
* one.
|
|
45
|
+
*/
|
|
46
|
+
const ABBREVIATION_BEFORE_NUMBER = new Set([
|
|
47
|
+
'no', 'fig', 'eq', 'ch', 'vol', 'pp', 'etc',
|
|
48
|
+
'inc', 'ltd', 'co', 'corp',
|
|
49
|
+
'jan', 'feb', 'mar', 'apr', 'jun', 'jul', 'aug', 'sep', 'sept', 'oct', 'nov', 'dec',
|
|
50
|
+
]);
|
|
51
|
+
const TERMINATORS = new Set(['.', '!', '?']);
|
|
52
|
+
/** True when the character can open a new sentence. */
|
|
53
|
+
function opensSentence(ch) {
|
|
54
|
+
if (ch === undefined)
|
|
55
|
+
return false;
|
|
56
|
+
if (ch >= 'A' && ch <= 'Z')
|
|
57
|
+
return true;
|
|
58
|
+
if (ch >= '0' && ch <= '9')
|
|
59
|
+
return true;
|
|
60
|
+
return '"‘’“”\'([{*_#-—'.includes(ch);
|
|
61
|
+
}
|
|
62
|
+
const isDigit = (ch) => ch !== undefined && ch >= '0' && ch <= '9';
|
|
63
|
+
/** The token immediately before `at`, lowercased, without its trailing stop. */
|
|
64
|
+
function precedingToken(text, at) {
|
|
65
|
+
let i = at - 1;
|
|
66
|
+
while (i >= 0 && !/[\s(["']/.test(text[i]))
|
|
67
|
+
i--;
|
|
68
|
+
return text.slice(i + 1, at).toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Named sentencesOf, not sentencesOf: the hallucination rule in
|
|
72
|
+
* src/eval/rules/safety.ts has its own sentencesOf that also breaks on
|
|
73
|
+
* every newline, because it wants per-line units for grounding checks. The
|
|
74
|
+
* two are not the same job and unifying them would move that rule's
|
|
75
|
+
* numbers, so it is a separate measured change, not a rename.
|
|
76
|
+
*
|
|
77
|
+
* Splits `text` into sentences. Never returns an empty sentence; a text with
|
|
78
|
+
* no terminator is one sentence. Line breaks do not split on their own — a
|
|
79
|
+
* wrapped paragraph is one sentence — but a blank line does, because a new
|
|
80
|
+
* block is a new thought and a bullet list is not one long sentence.
|
|
81
|
+
*/
|
|
82
|
+
export function sentencesOf(text) {
|
|
83
|
+
const out = [];
|
|
84
|
+
let start = 0;
|
|
85
|
+
for (let i = 0; i < text.length; i++) {
|
|
86
|
+
const ch = text[i];
|
|
87
|
+
// A blank line ends a sentence whatever came before it.
|
|
88
|
+
if (ch === '\n' && /^\s*\n/.test(text.slice(i + 1))) {
|
|
89
|
+
const piece = text.slice(start, i).trim();
|
|
90
|
+
if (piece.length > 0)
|
|
91
|
+
out.push(piece);
|
|
92
|
+
start = i + 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!TERMINATORS.has(ch))
|
|
96
|
+
continue;
|
|
97
|
+
// 3.5 — a full stop between digits is a decimal point.
|
|
98
|
+
if (ch === '.' && isDigit(text[i - 1]) && isDigit(text[i + 1]))
|
|
99
|
+
continue;
|
|
100
|
+
// Run past a cluster of terminators ("What?!").
|
|
101
|
+
let end = i;
|
|
102
|
+
while (end + 1 < text.length && TERMINATORS.has(text[end + 1]))
|
|
103
|
+
end++;
|
|
104
|
+
// Closing quotes and brackets belong to the sentence that ends here.
|
|
105
|
+
let after = end + 1;
|
|
106
|
+
while (after < text.length && '"’”\')]}'.includes(text[after]))
|
|
107
|
+
after++;
|
|
108
|
+
// What comes next has to look like a new sentence.
|
|
109
|
+
let next = after;
|
|
110
|
+
while (next < text.length && /[ \t\r\n]/.test(text[next]))
|
|
111
|
+
next++;
|
|
112
|
+
/*
|
|
113
|
+
* No whitespace after the stop is usually a mid-token full stop — a
|
|
114
|
+
* version (v0.10.0), a filename (package.json), a hostname
|
|
115
|
+
* (iris-eval.com/proof) — and must not break. The exception is a
|
|
116
|
+
* following CAPITAL, which is a missing space between two sentences
|
|
117
|
+
* ("...ready.Ship it") and not a token: no filename or version has one.
|
|
118
|
+
*/
|
|
119
|
+
if (next === after && next < text.length && !(text[next] >= 'A' && text[next] <= 'Z'))
|
|
120
|
+
continue;
|
|
121
|
+
if (next < text.length && !opensSentence(text[next]))
|
|
122
|
+
continue;
|
|
123
|
+
// Dr. Chen — an abbreviation, not an end.
|
|
124
|
+
if (ch === '.') {
|
|
125
|
+
const token = precedingToken(text, i);
|
|
126
|
+
if (ALWAYS_ABBREVIATION.has(token))
|
|
127
|
+
continue;
|
|
128
|
+
// Oct. 5 is a date; "in Oct. The rollout held" is two sentences.
|
|
129
|
+
if (ABBREVIATION_BEFORE_NUMBER.has(token) && isDigit(text[next]))
|
|
130
|
+
continue;
|
|
131
|
+
// A single capital letter is an initial: J. Smith.
|
|
132
|
+
if (token.length === 1 && /[a-z]/i.test(token))
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const piece = text.slice(start, after).trim();
|
|
136
|
+
if (piece.length > 0)
|
|
137
|
+
out.push(piece);
|
|
138
|
+
start = after;
|
|
139
|
+
i = after - 1;
|
|
140
|
+
}
|
|
141
|
+
const tail = text.slice(start).trim();
|
|
142
|
+
if (tail.length > 0)
|
|
143
|
+
out.push(tail);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
/** How many sentences the text contains. The one number `sentence_count` reports. */
|
|
147
|
+
export function countSentences(text) {
|
|
148
|
+
return sentencesOf(text).length;
|
|
149
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Coverage, EvalResult, EvalRule, EvalRuleResult, Need, Provenance, Verdict } from '../types/eval.js';
|
|
2
|
+
import type { EffectiveCriticality } from './criticality.js';
|
|
3
|
+
/**
|
|
4
|
+
* The basis of the verdict under the shipped composer (a weighted mean plus
|
|
5
|
+
* the critical veto). `passed` is `state === 'pass'` and equals the engine's
|
|
6
|
+
* own `passed` for every result; the basis says which layer decided.
|
|
7
|
+
*/
|
|
8
|
+
export declare function deriveVerdict(result: Pick<EvalResult, 'passed' | 'score' | 'rule_results' | 'insufficient_data' | 'critical_failures' | 'rules_evaluated'>, threshold: number): Verdict;
|
|
9
|
+
/**
|
|
10
|
+
* Which evaluation questions were judged, which were not and why. At write
|
|
11
|
+
* time the engine passes the inputs the call carried; at read time they are
|
|
12
|
+
* reconstructed as the union of what the rules saw (a rule that saw an input
|
|
13
|
+
* proves the call carried it; one that did not cannot prove the reverse).
|
|
14
|
+
*/
|
|
15
|
+
export declare function deriveCoverage(ruleResults: readonly EvalRuleResult[], present?: ReadonlySet<Need>): Coverage;
|
|
16
|
+
/** The critical rules that skipped — derived from the stamped flags on every read, never a column. */
|
|
17
|
+
export declare function deriveCriticalSkipped(ruleResults: readonly EvalRuleResult[]): string[] | undefined;
|
|
18
|
+
/** sha256 over the rules that ran — name, definition version, kind, effective criticality, weight — so two evaluations under the same ruleset hash the same. */
|
|
19
|
+
export declare function rulesetHash(rules: readonly EvalRule[], resolve: (rule: EvalRule) => EffectiveCriticality): string;
|
|
20
|
+
/** sha256 over the evaluation configuration that shapes a verdict. */
|
|
21
|
+
export declare function configHash(config: {
|
|
22
|
+
threshold: number;
|
|
23
|
+
ruleThresholds?: Record<string, unknown>;
|
|
24
|
+
criticalRules?: readonly string[];
|
|
25
|
+
nonCriticalRules?: readonly string[];
|
|
26
|
+
}): string;
|
|
27
|
+
export declare function buildProvenance(input: {
|
|
28
|
+
irisVersion: string;
|
|
29
|
+
rulesetHash: string;
|
|
30
|
+
configHash: string;
|
|
31
|
+
threshold: number;
|
|
32
|
+
ruleThresholds?: Record<string, unknown>;
|
|
33
|
+
judgedAt: string;
|
|
34
|
+
}): Provenance;
|
|
@@ -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;
|