agent-skill-evals 0.1.0 → 0.1.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/README.md +14 -145
- package/dist/agent/index.d.mts +2 -3
- package/dist/agent/index.mjs +1 -1
- package/dist/agent-B15R7Xlr.mjs +2105 -0
- package/dist/agent-B15R7Xlr.mjs.map +1 -0
- package/dist/assertions/index.d.mts +6 -0
- package/dist/assertions/index.d.mts.map +1 -1
- package/dist/assertions/index.mjs +164 -449
- package/dist/assertions/index.mjs.map +1 -1
- package/dist/catalog-9Wh-AfRv.mjs +409 -0
- package/dist/catalog-9Wh-AfRv.mjs.map +1 -0
- package/dist/cli/init.d.mts +22 -0
- package/dist/cli/init.d.mts.map +1 -0
- package/dist/cli/init.mjs +369 -0
- package/dist/cli/init.mjs.map +1 -0
- package/dist/index-D9lGL0Qg.d.mts +381 -0
- package/dist/index-D9lGL0Qg.d.mts.map +1 -0
- package/dist/presets-DwfDWckA.mjs +80 -0
- package/dist/presets-DwfDWckA.mjs.map +1 -0
- package/dist/test-generator/index.d.mts +945 -0
- package/dist/test-generator/index.d.mts.map +1 -0
- package/dist/test-generator/index.mjs +2 -0
- package/dist/test-pack-Cu1WOssl.mjs +215 -0
- package/dist/test-pack-Cu1WOssl.mjs.map +1 -0
- package/package.json +16 -9
- package/schema/test-pack.schema.json +871 -0
- package/dist/agent-CM7fIL_C.mjs +0 -1525
- package/dist/agent-CM7fIL_C.mjs.map +0 -1
- package/dist/assertion-entries-CfmNt-fp.d.mts +0 -9
- package/dist/assertion-entries-CfmNt-fp.d.mts.map +0 -1
- package/dist/index-4l7TCFny.d.mts +0 -90
- package/dist/index-4l7TCFny.d.mts.map +0 -1
- package/dist/internal-services-5-mRgNls.mjs +0 -226
- package/dist/internal-services-5-mRgNls.mjs.map +0 -1
- package/dist/internal-services-DbsekQ_K.d.mts +0 -76
- package/dist/internal-services-DbsekQ_K.d.mts.map +0 -1
- package/dist/skill-checks/index.d.mts +0 -113
- package/dist/skill-checks/index.d.mts.map +0 -1
- package/dist/skill-checks/index.mjs +0 -408
- package/dist/skill-checks/index.mjs.map +0 -1
|
@@ -1,66 +1,74 @@
|
|
|
1
|
-
import { c as parseRuntimeTestFields, n as
|
|
2
|
-
import {
|
|
1
|
+
import { a as EvidenceCollector, c as decodeEvidenceSnapshot, i as makeWorldHandle, l as parseRuntimeTestFields, n as assertMockServicesHealthy, o as evidenceFromSnapshot, r as stopMockServices, s as writeEvidenceTo } from "../agent-B15R7Xlr.mjs";
|
|
2
|
+
import { n as RUNTIME_CHECKS_BY_TYPE } from "../catalog-9Wh-AfRv.mjs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import
|
|
5
|
-
import * as Effect from "effect/Effect";
|
|
6
|
-
import * as Schema from "effect/Schema";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
7
5
|
//#region src/assertions/_shared.ts
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return JSON.parse(buf);
|
|
17
|
-
} catch {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
}), Effect.catchAll(() => Effect.succeed(null)));
|
|
21
|
-
if (parsed) return parsed;
|
|
22
|
-
}
|
|
6
|
+
async function loadMetadata(context) {
|
|
7
|
+
const direct = context.providerResponse?.metadata;
|
|
8
|
+
if (direct && typeof direct === "object" && "worldPath" in direct) return direct;
|
|
9
|
+
const runDir = process.env.AGENT_SKILL_EVALS_RUN_DIR;
|
|
10
|
+
if (runDir) try {
|
|
11
|
+
const buf = await readFile(join(runDir, "agent-skill-evals-meta.json"), "utf8");
|
|
12
|
+
return JSON.parse(buf);
|
|
13
|
+
} catch {
|
|
23
14
|
return null;
|
|
24
|
-
}
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
25
17
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
18
|
+
async function loadEvidence(meta) {
|
|
19
|
+
let buf;
|
|
20
|
+
try {
|
|
21
|
+
buf = await readFile(meta.evidencePath, "utf8");
|
|
22
|
+
} catch (err) {
|
|
23
|
+
throw new Error(`evidence: failed to read ${meta.evidencePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
24
|
+
}
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(buf);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
throw new Error(`evidence: invalid JSON in ${meta.evidencePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
30
|
+
}
|
|
31
|
+
const decoded = decodeEvidenceSnapshot(parsed);
|
|
32
|
+
if (!decoded.ok) throw new Error(`evidence: ${decoded.error.message}`);
|
|
33
|
+
return EvidenceCollector.fromSnapshot(decoded.value);
|
|
37
34
|
}
|
|
38
35
|
function loadWorld(meta, evidenceCollector) {
|
|
39
|
-
|
|
36
|
+
const world = makeWorldHandle(meta.worldPath, (event) => evidenceCollector.addCommand(event));
|
|
37
|
+
if (!meta.mockEnv) return world;
|
|
38
|
+
return {
|
|
39
|
+
...world,
|
|
40
|
+
exec(command, args, opts = {}) {
|
|
41
|
+
return world.exec(command, args, {
|
|
42
|
+
...opts,
|
|
43
|
+
env: {
|
|
44
|
+
...meta.mockEnv,
|
|
45
|
+
...opts.env ?? {}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
40
50
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
reason: `unknown effect type: ${entry.type}`
|
|
51
|
-
});
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
const r = yield* plugin.verify({
|
|
55
|
-
assertion: entry.args,
|
|
56
|
-
world,
|
|
57
|
-
evidence: evidenceFromSnapshot(evidenceCollector.toSnapshot()),
|
|
58
|
-
mode
|
|
51
|
+
async function runEntries(entries, world, evidenceCollector, mode) {
|
|
52
|
+
const results = [];
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const plugin = RUNTIME_CHECKS_BY_TYPE.get(entry.type);
|
|
55
|
+
if (!plugin) {
|
|
56
|
+
results.push({
|
|
57
|
+
pass: false,
|
|
58
|
+
score: 0,
|
|
59
|
+
reason: `unknown effect type: ${entry.type}`
|
|
59
60
|
});
|
|
60
|
-
|
|
61
|
+
continue;
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
const r = await plugin.verify({
|
|
64
|
+
assertion: entry.args,
|
|
65
|
+
world,
|
|
66
|
+
evidence: evidenceFromSnapshot(evidenceCollector.toSnapshot()),
|
|
67
|
+
mode
|
|
68
|
+
});
|
|
69
|
+
results.push(r);
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
64
72
|
}
|
|
65
73
|
function aggregate(results, emptyReason, options = {}) {
|
|
66
74
|
if (results.length === 0) {
|
|
@@ -86,47 +94,82 @@ function aggregate(results, emptyReason, options = {}) {
|
|
|
86
94
|
}
|
|
87
95
|
//#endregion
|
|
88
96
|
//#region src/assertions/skill-test.ts
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
function withEvidencePointer(result, meta) {
|
|
98
|
+
if (result.pass) return result;
|
|
99
|
+
return {
|
|
100
|
+
...result,
|
|
101
|
+
reason: `${result.reason}\nEvidence: ${meta.evidencePath}\nWorld: ${meta.worldPath}`
|
|
102
|
+
};
|
|
91
103
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
async function skillTest(_output, context) {
|
|
105
|
+
const meta = await loadMetadata(context);
|
|
106
|
+
if (!meta) return {
|
|
107
|
+
pass: false,
|
|
108
|
+
score: 0,
|
|
109
|
+
reason: "skill.test: provider metadata missing"
|
|
110
|
+
};
|
|
111
|
+
const preconditionResults = meta.preconditionResults ?? [];
|
|
112
|
+
if (!meta.preconditionsPassed) {
|
|
113
|
+
const result = withEvidencePointer(aggregate(preconditionResults, "skill.test: preconditions failed"), meta);
|
|
114
|
+
await stopMockServices(meta.runDir);
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
const vars = context.vars ?? context.test?.vars ?? {};
|
|
118
|
+
let evidenceCollector;
|
|
119
|
+
try {
|
|
120
|
+
evidenceCollector = await loadEvidence(meta);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
await stopMockServices(meta.runDir);
|
|
123
|
+
return {
|
|
96
124
|
pass: false,
|
|
97
125
|
score: 0,
|
|
98
|
-
reason:
|
|
126
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
99
127
|
};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
pass: false,
|
|
108
|
-
score: 0,
|
|
109
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
const evidenceCollector = loadedEvidence.right;
|
|
113
|
-
const world = loadWorld(meta, evidenceCollector);
|
|
114
|
-
const parsed = parseRuntimeTestFields(vars);
|
|
115
|
-
const parseResults = parsed.errors.filter((error) => error.field !== "preconditions").map((error) => ({
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
assertMockServicesHealthy(meta.runDir);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
await stopMockServices(meta.runDir);
|
|
133
|
+
return withEvidencePointer({
|
|
116
134
|
pass: false,
|
|
117
135
|
score: 0,
|
|
118
|
-
reason: `
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
reason: `Mock Service failure: ${err instanceof Error ? err.message : String(err)}`
|
|
137
|
+
}, meta);
|
|
138
|
+
}
|
|
139
|
+
const world = loadWorld(meta, evidenceCollector);
|
|
140
|
+
const warningResults = evidenceCollector.toSnapshot().warnings?.map((warning) => ({
|
|
141
|
+
pass: false,
|
|
142
|
+
score: 0,
|
|
143
|
+
reason: `evidence warning: ${warning}`
|
|
144
|
+
})) ?? [];
|
|
145
|
+
const parsed = parseRuntimeTestFields(vars);
|
|
146
|
+
const parseResults = parsed.errors.filter((error) => error.field !== "preconditions").map((error) => ({
|
|
147
|
+
pass: false,
|
|
148
|
+
score: 0,
|
|
149
|
+
reason: `runtime test field ${error.index === void 0 ? error.field : `${error.field}[${error.index}]`}: ${error.reason}`
|
|
150
|
+
}));
|
|
151
|
+
const expectationResults = await runEntries(parsed.expect, world, evidenceCollector, "should");
|
|
152
|
+
let writeError;
|
|
153
|
+
try {
|
|
154
|
+
await writeEvidenceTo(evidenceCollector, meta.runDir);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
writeError = err instanceof Error ? err.message : String(err);
|
|
157
|
+
}
|
|
158
|
+
const graded = withEvidencePointer(aggregate([
|
|
159
|
+
...preconditionResults,
|
|
160
|
+
...warningResults,
|
|
161
|
+
...parseResults,
|
|
162
|
+
...expectationResults
|
|
163
|
+
], "skill.test: no Runtime Test Fields checks declared", { emptyPass: false }), meta);
|
|
164
|
+
if (writeError !== void 0) {
|
|
165
|
+
await stopMockServices(meta.runDir);
|
|
166
|
+
return {
|
|
167
|
+
...graded,
|
|
168
|
+
reason: `${graded.reason}\n(note: failed to update evidence.json: ${writeError})`
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
await stopMockServices(meta.runDir);
|
|
172
|
+
return graded;
|
|
130
173
|
}
|
|
131
174
|
//#endregion
|
|
132
175
|
//#region src/assertions/skill-budget.ts
|
|
@@ -136,20 +179,20 @@ const budgetFields = [
|
|
|
136
179
|
["completion", "maxCompletionTokens"],
|
|
137
180
|
["cached", "maxCachedTokens"]
|
|
138
181
|
];
|
|
139
|
-
function configObject
|
|
182
|
+
function configObject(value) {
|
|
140
183
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
141
184
|
}
|
|
142
|
-
function numberSetting
|
|
185
|
+
function numberSetting(config, key) {
|
|
143
186
|
const value = config[key];
|
|
144
187
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
145
188
|
}
|
|
146
189
|
function budgetSettings(context) {
|
|
147
|
-
const config = configObject
|
|
190
|
+
const config = configObject(context.config?.agentSkillEvals);
|
|
148
191
|
return {
|
|
149
|
-
maxTotalTokens: numberSetting
|
|
150
|
-
maxPromptTokens: numberSetting
|
|
151
|
-
maxCompletionTokens: numberSetting
|
|
152
|
-
maxCachedTokens: numberSetting
|
|
192
|
+
maxTotalTokens: numberSetting(config, "maxTotalTokens"),
|
|
193
|
+
maxPromptTokens: numberSetting(config, "maxPromptTokens"),
|
|
194
|
+
maxCompletionTokens: numberSetting(config, "maxCompletionTokens"),
|
|
195
|
+
maxCachedTokens: numberSetting(config, "maxCachedTokens")
|
|
153
196
|
};
|
|
154
197
|
}
|
|
155
198
|
function tokenUsage(context) {
|
|
@@ -193,379 +236,51 @@ async function skillBudget(_output, context) {
|
|
|
193
236
|
};
|
|
194
237
|
}
|
|
195
238
|
//#endregion
|
|
196
|
-
//#region src/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
function pass(reason, components) {
|
|
203
|
-
return {
|
|
204
|
-
pass: true,
|
|
205
|
-
score: 1,
|
|
206
|
-
reason,
|
|
207
|
-
componentResults: components
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
function fail(reason, components) {
|
|
211
|
-
return {
|
|
212
|
-
pass: false,
|
|
213
|
-
score: 0,
|
|
214
|
-
reason,
|
|
215
|
-
componentResults: components
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
//#endregion
|
|
219
|
-
//#region src/skill-checks/assertions-static/settings.ts
|
|
220
|
-
const DEFAULT_SETTINGS = {
|
|
221
|
-
maxSkillLines: 200,
|
|
222
|
-
destructiveEffects: ["file.changes_outside_scope", "tool.called"],
|
|
223
|
-
requireTokenBudget: false,
|
|
224
|
-
riskyEffects: ["file.changes_outside_scope", "tool.called"]
|
|
239
|
+
//#region src/assertions/promptfoo.ts
|
|
240
|
+
const routes = {
|
|
241
|
+
"skill.budget": skillBudget,
|
|
242
|
+
"skill.test": skillTest
|
|
225
243
|
};
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return Schema.is(StringArraySchema)(config[key]) ? config[key] : fallback;
|
|
235
|
-
}
|
|
236
|
-
function booleanSetting(config, key, fallback) {
|
|
237
|
-
return typeof config[key] === "boolean" ? config[key] : fallback;
|
|
238
|
-
}
|
|
239
|
-
function skillCheckSettings(context) {
|
|
240
|
-
const config = configObject(context.config?.agentSkillEvals);
|
|
241
|
-
return {
|
|
242
|
-
maxSkillLines: numberSetting(config, "maxSkillLines", DEFAULT_SETTINGS.maxSkillLines),
|
|
243
|
-
destructiveEffects: stringArraySetting(config, "destructiveEffects", DEFAULT_SETTINGS.destructiveEffects),
|
|
244
|
-
requireTokenBudget: booleanSetting(config, "requireTokenBudget", DEFAULT_SETTINGS.requireTokenBudget),
|
|
245
|
-
riskyEffects: stringArraySetting(config, "riskyEffects", DEFAULT_SETTINGS.riskyEffects)
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
//#endregion
|
|
249
|
-
//#region src/skill-checks/assertions-static/context-economy.ts
|
|
250
|
-
/**
|
|
251
|
-
* SPEC §7.2 — context economy. Hard: referenced files must exist (already
|
|
252
|
-
* surfaced via missingFiles). Warnings: SKILL.md size, reference depth,
|
|
253
|
-
* missing TOC in long reference files.
|
|
254
|
-
*
|
|
255
|
-
* Implemented as warnings-only: this assertion always passes, but emits a
|
|
256
|
-
* `score < 1` reason describing how SKILL.md compares to thresholds.
|
|
257
|
-
*/
|
|
258
|
-
async function contextEconomy(_output, context) {
|
|
259
|
-
const meta = getStaticMeta(context);
|
|
260
|
-
if (!meta) return fail("context-economy: provider metadata missing");
|
|
261
|
-
const skill = meta.skill;
|
|
262
|
-
if (!skill) return fail("context-economy: skill not parsed");
|
|
263
|
-
const { maxSkillLines: maxLines } = skillCheckSettings(context);
|
|
264
|
-
const components = [];
|
|
265
|
-
components.push({
|
|
266
|
-
pass: skill.totalLines <= maxLines,
|
|
267
|
-
score: skill.totalLines <= maxLines ? 1 : .5,
|
|
268
|
-
reason: `SKILL.md ${skill.totalLines} line(s) (limit ${maxLines})`
|
|
269
|
-
});
|
|
270
|
-
components.push({
|
|
271
|
-
pass: skill.missingReferences.length === 0,
|
|
272
|
-
score: skill.missingReferences.length === 0 ? 1 : 0,
|
|
273
|
-
reason: skill.missingReferences.length === 0 ? `${skill.references.length} reference(s) all resolved` : `missing references: ${skill.missingReferences.slice(0, 3).join(", ")}`
|
|
274
|
-
});
|
|
275
|
-
const failed = components.filter((c) => !c.pass && c.reason.startsWith("missing references"));
|
|
276
|
-
if (failed.length === 0) {
|
|
277
|
-
const oversize = components.find((c) => c.score < 1);
|
|
278
|
-
return oversize ? {
|
|
279
|
-
pass: true,
|
|
280
|
-
score: .5,
|
|
281
|
-
reason: oversize.reason,
|
|
282
|
-
componentResults: components
|
|
283
|
-
} : pass(`context-economy: ok`, components);
|
|
244
|
+
const knownMetrics = Object.keys(routes);
|
|
245
|
+
const availableMetrics = [...knownMetrics].sort().join(", ");
|
|
246
|
+
function sameConfig(a, b) {
|
|
247
|
+
if (a === void 0 && b === void 0) return true;
|
|
248
|
+
try {
|
|
249
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
250
|
+
} catch {
|
|
251
|
+
return false;
|
|
284
252
|
}
|
|
285
|
-
return fail(failed.map((f) => `✗ ${f.reason}`).join("; "), components);
|
|
286
|
-
}
|
|
287
|
-
//#endregion
|
|
288
|
-
//#region src/skill-checks/assertions-static/executable-helper.ts
|
|
289
|
-
/**
|
|
290
|
-
* SPEC §7.4 — verifier scripts referenced by tests must exist and be
|
|
291
|
-
* runnable. Generated tests must not reference missing fixtures.
|
|
292
|
-
*/
|
|
293
|
-
async function executableHelper(_output, context) {
|
|
294
|
-
const meta = getStaticMeta(context);
|
|
295
|
-
if (!meta) return fail("executable-helper: provider metadata missing");
|
|
296
|
-
const tests = meta.tests;
|
|
297
|
-
if (!tests) return fail("executable-helper: tests not parsed");
|
|
298
|
-
const components = [];
|
|
299
|
-
components.push({
|
|
300
|
-
pass: tests.missingVerifierScripts.length === 0,
|
|
301
|
-
score: tests.missingVerifierScripts.length === 0 ? 1 : 0,
|
|
302
|
-
reason: tests.missingVerifierScripts.length === 0 ? `${tests.verifierScripts.length} verifier script(s) all present` : `missing verifier scripts: ${tests.missingVerifierScripts.slice(0, 3).join(", ")}`
|
|
303
|
-
});
|
|
304
|
-
components.push({
|
|
305
|
-
pass: tests.nonExecutableVerifierScripts.length === 0,
|
|
306
|
-
score: tests.nonExecutableVerifierScripts.length === 0 ? 1 : 0,
|
|
307
|
-
reason: tests.nonExecutableVerifierScripts.length === 0 ? `${tests.verifierScripts.length} verifier script(s) executable` : `non-executable verifier scripts: ${tests.nonExecutableVerifierScripts.slice(0, 3).join(", ")}`
|
|
308
|
-
});
|
|
309
|
-
components.push({
|
|
310
|
-
pass: tests.missingFixturePaths.length === 0,
|
|
311
|
-
score: tests.missingFixturePaths.length === 0 ? 1 : 0,
|
|
312
|
-
reason: tests.missingFixturePaths.length === 0 ? `${tests.fixturePaths.length} fixture(s) all present` : `missing fixtures: ${tests.missingFixturePaths.slice(0, 3).join(", ")}`
|
|
313
|
-
});
|
|
314
|
-
const failed = components.filter((c) => !c.pass);
|
|
315
|
-
if (failed.length === 0) return pass("executable-helper: ok", components);
|
|
316
|
-
return fail(failed.map((f) => `✗ ${f.reason}`).join("; "), components);
|
|
317
|
-
}
|
|
318
|
-
//#endregion
|
|
319
|
-
//#region src/skill-checks/assertions-static/instruction-calibration.ts
|
|
320
|
-
/**
|
|
321
|
-
* SPEC §7.3 — for skills whose test pack uses destructive effects, the
|
|
322
|
-
* SKILL.md must contain confirmation/clarification language and at least
|
|
323
|
-
* one negative test must declare forbidden effects.
|
|
324
|
-
*
|
|
325
|
-
* `assert.config.agentSkillEvals.destructiveEffects` overrides the default list.
|
|
326
|
-
*/
|
|
327
|
-
const CONFIRMATION_RE = /\b(confirm|ask first|do not.*without|before.*push|require.*approval|do not.*destructive)\b/i;
|
|
328
|
-
const PLAN_BEFORE_ACT_RE = /\b(plan first|plan before|read.*before.*write|validate.*before|dry.run)\b/i;
|
|
329
|
-
async function instructionCalibration(_output, context) {
|
|
330
|
-
const meta = getStaticMeta(context);
|
|
331
|
-
if (!meta) return fail("skill.instructions: provider metadata missing");
|
|
332
|
-
const skill = meta.skill;
|
|
333
|
-
const tests = meta.tests;
|
|
334
|
-
if (!skill) return fail("skill.instructions: skill not parsed");
|
|
335
|
-
const destructive = new Set(skillCheckSettings(context).destructiveEffects);
|
|
336
|
-
if (!(tests ? tests.tests.some((t) => t.effectTypes.some((e) => destructive.has(e))) : false)) return pass("skill.instructions: no destructive effects in test pack");
|
|
337
|
-
const components = [];
|
|
338
|
-
const hasConfirm = CONFIRMATION_RE.test(skill.body) || PLAN_BEFORE_ACT_RE.test(skill.body);
|
|
339
|
-
components.push({
|
|
340
|
-
pass: hasConfirm,
|
|
341
|
-
score: hasConfirm ? 1 : 0,
|
|
342
|
-
reason: hasConfirm ? "SKILL.md describes confirmation / plan-before-act" : "SKILL.md uses destructive effects but lacks confirmation / plan-before-act language"
|
|
343
|
-
});
|
|
344
|
-
const declaresForbidden = tests ? tests.tests.some((t) => Array.isArray(t.vars.should_not) && t.vars.should_not.length > 0) : false;
|
|
345
|
-
components.push({
|
|
346
|
-
pass: declaresForbidden,
|
|
347
|
-
score: declaresForbidden ? 1 : 0,
|
|
348
|
-
reason: declaresForbidden ? "test pack declares forbidden effects (should_not)" : "no should_not declared in any test, despite destructive effects"
|
|
349
|
-
});
|
|
350
|
-
const failed = components.filter((c) => !c.pass);
|
|
351
|
-
if (failed.length === 0) return pass("skill.instructions: ok", components);
|
|
352
|
-
return fail(failed.map((f) => `✗ ${f.reason}`).join("; "), components);
|
|
353
|
-
}
|
|
354
|
-
//#endregion
|
|
355
|
-
//#region src/skill-checks/assertions-static/negative-coverage.ts
|
|
356
|
-
/**
|
|
357
|
-
* SPEC §7.6 — risky skills (those whose test pack uses any risky effect)
|
|
358
|
-
* must include at least one negative test.
|
|
359
|
-
*
|
|
360
|
-
* `assert.config.agentSkillEvals.riskyEffects` overrides the default risky-effect list.
|
|
361
|
-
*/
|
|
362
|
-
async function negativeCoverage(_output, context) {
|
|
363
|
-
const meta = getStaticMeta(context);
|
|
364
|
-
if (!meta) return fail("skill.tests: provider metadata missing");
|
|
365
|
-
const tests = meta.tests;
|
|
366
|
-
if (!tests) return fail("skill.tests: tests not parsed");
|
|
367
|
-
const risky = new Set(skillCheckSettings(context).riskyEffects);
|
|
368
|
-
if (!tests.tests.some((t) => t.effectTypes.some((e) => risky.has(e)))) return pass("skill.tests: no risky effects, negative test not required");
|
|
369
|
-
const negatives = tests.tests.filter((t) => t.isNegative);
|
|
370
|
-
if (negatives.length === 0) return fail(`skill.tests: skill uses risky effects (${[...risky].join(", ")}) but no negative test (kind: negative) is declared`);
|
|
371
|
-
return pass(`skill.tests: ${negatives.length} negative test(s)`);
|
|
372
|
-
}
|
|
373
|
-
//#endregion
|
|
374
|
-
//#region src/skill-checks/assertions-static/routing-metadata.ts
|
|
375
|
-
const GENERIC_PHRASES = [
|
|
376
|
-
/\bhelp(s|ing)?\b/i,
|
|
377
|
-
/\bgithub workflows?\b/i,
|
|
378
|
-
/\bvarious\b/i,
|
|
379
|
-
/\bany kind of\b/i
|
|
380
|
-
];
|
|
381
|
-
const WHEN_TO_USE_RE = /\b(use when|use this|when (?:the )?(?:user|you))\b/i;
|
|
382
|
-
const WHEN_NOT_RE = /\bdo not use|do not invoke|don'?t use|not for|avoid using\b/i;
|
|
383
|
-
/**
|
|
384
|
-
* SPEC §7.1 — routing metadata hard checks.
|
|
385
|
-
*/
|
|
386
|
-
async function routingMetadata(_output, context) {
|
|
387
|
-
const meta = getStaticMeta(context);
|
|
388
|
-
if (!meta) return fail("routing-metadata: provider metadata missing");
|
|
389
|
-
const skill = meta.skill;
|
|
390
|
-
if (!skill) return fail("routing-metadata: skill not parsed (vars.skillPath missing?)");
|
|
391
|
-
const fm = skill.frontmatter;
|
|
392
|
-
const components = [];
|
|
393
|
-
const name = typeof fm.name === "string" ? fm.name.trim() : "";
|
|
394
|
-
components.push({
|
|
395
|
-
pass: name.length > 0,
|
|
396
|
-
score: name.length > 0 ? 1 : 0,
|
|
397
|
-
reason: name ? `name: ${name}` : "missing `name` frontmatter"
|
|
398
|
-
});
|
|
399
|
-
const desc = typeof fm.description === "string" ? fm.description.trim() : "";
|
|
400
|
-
components.push({
|
|
401
|
-
pass: desc.length > 0,
|
|
402
|
-
score: desc.length > 0 ? 1 : 0,
|
|
403
|
-
reason: desc ? `description present (${desc.length} chars)` : "missing `description` frontmatter"
|
|
404
|
-
});
|
|
405
|
-
const sayWhen = WHEN_TO_USE_RE.test(desc);
|
|
406
|
-
components.push({
|
|
407
|
-
pass: sayWhen,
|
|
408
|
-
score: sayWhen ? 1 : 0,
|
|
409
|
-
reason: sayWhen ? "description says when to use" : "description does not say when to use (e.g. 'Use when …')"
|
|
410
|
-
});
|
|
411
|
-
const sayWhenNot = WHEN_NOT_RE.test(desc);
|
|
412
|
-
components.push({
|
|
413
|
-
pass: sayWhenNot,
|
|
414
|
-
score: sayWhenNot ? 1 : 0,
|
|
415
|
-
reason: sayWhenNot ? "description says when not to use" : "description does not say when not to use (e.g. 'Do not use for …')"
|
|
416
|
-
});
|
|
417
|
-
const generic = GENERIC_PHRASES.some((re) => re.test(desc)) && desc.length < 80;
|
|
418
|
-
components.push({
|
|
419
|
-
pass: !generic,
|
|
420
|
-
score: generic ? 0 : 1,
|
|
421
|
-
reason: generic ? "description is too generic" : "description is specific enough"
|
|
422
|
-
});
|
|
423
|
-
const failed = components.filter((c) => !c.pass);
|
|
424
|
-
if (failed.length === 0) return pass(`routing-metadata: ${components.length}/${components.length} checks ok`, components);
|
|
425
|
-
return fail(failed.map((f) => `✗ ${f.reason}`).join("; "), components);
|
|
426
253
|
}
|
|
427
|
-
//#endregion
|
|
428
|
-
//#region src/skill-checks/assertions-static/scenario-validity.ts
|
|
429
254
|
/**
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
255
|
+
* Promptfoo hands a javascript assertion its `config` but not its `metric`,
|
|
256
|
+
* so a lone `metric:` declaration used to require a duplicate `config.metric`.
|
|
257
|
+
* Recover the metric from the test's assert list instead: the entry whose
|
|
258
|
+
* config matches the one Promptfoo passed is the assertion being graded.
|
|
433
259
|
*/
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
if (!
|
|
437
|
-
const
|
|
438
|
-
if (
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
if (tests.parseErrors.length > 0) components.push({
|
|
442
|
-
pass: false,
|
|
443
|
-
score: 0,
|
|
444
|
-
reason: `parse errors: ${tests.parseErrors.map((e) => `${e.filePath}: ${e.error}`).join("; ")}`
|
|
445
|
-
});
|
|
446
|
-
if (tests.matchedFiles.length === 0) components.push({
|
|
447
|
-
pass: false,
|
|
448
|
-
score: 0,
|
|
449
|
-
reason: "testsGlob matched no test files"
|
|
450
|
-
});
|
|
451
|
-
for (const t of tests.tests) {
|
|
452
|
-
const issues = [];
|
|
453
|
-
for (const error of t.entryErrors) {
|
|
454
|
-
const at = error.index === void 0 ? error.field : `${error.field}[${error.index}]`;
|
|
455
|
-
issues.push(`${at}: ${error.reason}`);
|
|
456
|
-
}
|
|
457
|
-
if (typeof t.vars.prompt !== "string" || t.vars.prompt.length === 0) issues.push("missing vars.prompt");
|
|
458
|
-
if (!t.hasFixture) issues.push("missing vars.fixture (or vars.fixtureless: true)");
|
|
459
|
-
if (!(Array.isArray(t.vars.should) && t.vars.should.length > 0 || Array.isArray(t.vars.should_not) && t.vars.should_not.length > 0)) issues.push("no should / should_not");
|
|
460
|
-
if (settings.requireTokenBudget && !t.hasTokenBudget) issues.push("missing skill.budget assertion");
|
|
461
|
-
components.push({
|
|
462
|
-
pass: issues.length === 0,
|
|
463
|
-
score: issues.length === 0 ? 1 : 0,
|
|
464
|
-
reason: `${t.description ?? "(no description)"}: ${issues.length === 0 ? "ok" : issues.join(", ")}`
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
if (meta.unresolvedEffectTypes.length > 0) components.push({
|
|
468
|
-
pass: false,
|
|
469
|
-
score: 0,
|
|
470
|
-
reason: `unsupported effect types: ${meta.unresolvedEffectTypes.join(", ")}`
|
|
471
|
-
});
|
|
472
|
-
if (meta.missingFiles.length > 0) components.push({
|
|
473
|
-
pass: false,
|
|
474
|
-
score: 0,
|
|
475
|
-
reason: `missing referenced files: ${meta.missingFiles.slice(0, 5).join(", ")}`
|
|
476
|
-
});
|
|
477
|
-
const failed = components.filter((c) => !c.pass);
|
|
478
|
-
if (failed.length === 0) return pass(`skill.tests: ${tests.tests.length} test(s) ok`, components);
|
|
479
|
-
return fail(failed.map((f) => `✗ ${f.reason}`).join("; "), components);
|
|
480
|
-
}
|
|
481
|
-
//#endregion
|
|
482
|
-
//#region src/skill-checks/assertions-static/promptfoo.ts
|
|
483
|
-
const metricChecks = {
|
|
484
|
-
"skill.activation": [routingMetadata],
|
|
485
|
-
"skill.budgets": [scenarioValidity],
|
|
486
|
-
"skill.context": [contextEconomy],
|
|
487
|
-
"skill.instructions": [instructionCalibration],
|
|
488
|
-
"skill.tests": [scenarioValidity, negativeCoverage],
|
|
489
|
-
"skill.verifiers": [executableHelper]
|
|
490
|
-
};
|
|
491
|
-
metricChecks["skill.checks"] = [
|
|
492
|
-
routingMetadata,
|
|
493
|
-
contextEconomy,
|
|
494
|
-
instructionCalibration,
|
|
495
|
-
scenarioValidity,
|
|
496
|
-
negativeCoverage,
|
|
497
|
-
executableHelper
|
|
498
|
-
];
|
|
499
|
-
const availableMetrics$1 = Object.keys(metricChecks).sort().join(", ");
|
|
500
|
-
function metricFrom$1(context) {
|
|
501
|
-
return [
|
|
502
|
-
context.assertion?.metric,
|
|
503
|
-
context.assert?.metric,
|
|
504
|
-
context.config?.metric,
|
|
505
|
-
context.metric
|
|
506
|
-
].find((metric) => typeof metric === "string");
|
|
507
|
-
}
|
|
508
|
-
function runChecksEffect(output, context, checks) {
|
|
509
|
-
return Effect.gen(function* () {
|
|
510
|
-
const results = yield* Effect.forEach(checks, (check) => Effect.promise(() => check(output, context)), { concurrency: "unbounded" });
|
|
511
|
-
const failed = results.filter((result) => !result.pass);
|
|
512
|
-
const soft = results.filter((result) => result.pass && result.score < 1);
|
|
513
|
-
return {
|
|
514
|
-
pass: failed.length === 0,
|
|
515
|
-
score: failed.length === 0 ? Math.min(...results.map((result) => result.score)) : 0,
|
|
516
|
-
reason: failed.length === 0 ? soft.length > 0 ? soft.map((result) => result.reason).join("; ") : `skill checks: ${results.length} check(s) passed` : failed.map((result) => result.reason).join("; "),
|
|
517
|
-
componentResults: results
|
|
518
|
-
};
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
async function agentSkillEvalsStaticAssertions(output, context) {
|
|
522
|
-
return Effect.runPromise(agentSkillEvalsStaticAssertionsEffect(output, context));
|
|
523
|
-
}
|
|
524
|
-
function agentSkillEvalsStaticAssertionsEffect(output, context) {
|
|
525
|
-
return Effect.gen(function* () {
|
|
526
|
-
const metric = metricFrom$1(context);
|
|
527
|
-
const checks = metric ? metricChecks[metric] : void 0;
|
|
528
|
-
if (!checks) return {
|
|
529
|
-
pass: false,
|
|
530
|
-
score: 0,
|
|
531
|
-
reason: `agent-skill-evals skill checks: unknown metric "${metric ?? "missing"}". Available metrics: ${availableMetrics$1}`
|
|
532
|
-
};
|
|
533
|
-
return yield* runChecksEffect(output, context, checks);
|
|
534
|
-
});
|
|
260
|
+
function metricFromTestAsserts(context) {
|
|
261
|
+
const asserts = context.test?.assert;
|
|
262
|
+
if (!Array.isArray(asserts)) return void 0;
|
|
263
|
+
const candidates = asserts.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.metric === "string" && knownMetrics.includes(entry.metric));
|
|
264
|
+
if (candidates.length === 1) return candidates[0].metric;
|
|
265
|
+
const matched = candidates.filter((entry) => sameConfig(entry.config, context.config));
|
|
266
|
+
if (matched.length === 1) return matched[0].metric;
|
|
535
267
|
}
|
|
536
|
-
//#endregion
|
|
537
|
-
//#region src/assertions/promptfoo.ts
|
|
538
|
-
const routes = {
|
|
539
|
-
"skill.budget": skillBudget,
|
|
540
|
-
"skill.test": skillTest
|
|
541
|
-
};
|
|
542
|
-
const staticMetrics = [
|
|
543
|
-
"skill.checks",
|
|
544
|
-
"skill.activation",
|
|
545
|
-
"skill.budgets",
|
|
546
|
-
"skill.context",
|
|
547
|
-
"skill.instructions",
|
|
548
|
-
"skill.tests",
|
|
549
|
-
"skill.verifiers"
|
|
550
|
-
];
|
|
551
|
-
const availableMetrics = [...Object.keys(routes), ...staticMetrics].sort().join(", ");
|
|
552
268
|
function metricFrom(context) {
|
|
553
269
|
return [
|
|
554
270
|
context.assertion?.metric,
|
|
555
271
|
context.assert?.metric,
|
|
556
272
|
context.config?.metric,
|
|
557
273
|
context.metric
|
|
558
|
-
].find((metric) => typeof metric === "string");
|
|
274
|
+
].find((metric) => typeof metric === "string") ?? metricFromTestAsserts(context);
|
|
559
275
|
}
|
|
560
276
|
async function agentSkillEvalsAssertions(output, context) {
|
|
561
277
|
const metric = metricFrom(context);
|
|
562
278
|
const assertion = metric ? routes[metric] : void 0;
|
|
563
279
|
if (assertion) return assertion(output, context);
|
|
564
|
-
if (metric && staticMetrics.includes(metric)) return agentSkillEvalsStaticAssertions(output, context);
|
|
565
280
|
return {
|
|
566
281
|
pass: false,
|
|
567
282
|
score: 0,
|
|
568
|
-
reason: `agent-skill-evals assertions: unknown metric "${metric ?? "missing"}". Available metrics: ${availableMetrics}
|
|
283
|
+
reason: `agent-skill-evals assertions: unknown metric "${metric ?? "missing"}". Available metrics: ${availableMetrics}. Declare the metric on the assertion (metric: skill.test). If one test case uses several agent-skill-evals assertions with identical config blocks, disambiguate with config.metric.`
|
|
569
284
|
};
|
|
570
285
|
}
|
|
571
286
|
//#endregion
|