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
|
@@ -0,0 +1,2105 @@
|
|
|
1
|
+
import { n as RUNTIME_CHECKS_BY_TYPE } from "./catalog-9Wh-AfRv.mjs";
|
|
2
|
+
import { i as resolveInvocation, n as PRESET_IDS, r as presetStalenessHint } from "./presets-DwfDWckA.mjs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
import { chmod, copyFile, cp, mkdir, mkdtemp, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { createServer } from "node:net";
|
|
10
|
+
//#region src/assertion-entries.ts
|
|
11
|
+
function isEntryObject(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Normalizes generated Promptfoo `vars.preconditions | expect` entries.
|
|
16
|
+
*
|
|
17
|
+
* Supports the clean Test Pack shorthand form:
|
|
18
|
+
* `{ "file.exists": { path: "app.js" } }`
|
|
19
|
+
*/
|
|
20
|
+
function parseRuntimeTestFields(vars) {
|
|
21
|
+
const preconditions = parseAssertionEntries(vars.preconditions, "preconditions", { allowMissing: true });
|
|
22
|
+
const expect = parseAssertionEntries(vars.expect, "expect", { allowMissing: true });
|
|
23
|
+
return {
|
|
24
|
+
preconditions: preconditions.entries,
|
|
25
|
+
expect: expect.entries,
|
|
26
|
+
errors: [...preconditions.errors, ...expect.errors]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function parseAssertionEntries(raw, field, options = {}) {
|
|
30
|
+
const entries = [];
|
|
31
|
+
const errors = [];
|
|
32
|
+
if (raw === void 0 || raw === null) {
|
|
33
|
+
if (!options.allowMissing) errors.push({
|
|
34
|
+
field,
|
|
35
|
+
reason: "must be an array of assertion entries"
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
entries,
|
|
39
|
+
errors
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(raw)) return {
|
|
43
|
+
entries,
|
|
44
|
+
errors: [{
|
|
45
|
+
field,
|
|
46
|
+
reason: "must be an array of assertion entries"
|
|
47
|
+
}]
|
|
48
|
+
};
|
|
49
|
+
raw.forEach((entry, index) => {
|
|
50
|
+
const parsed = parseAssertionEntry(entry, field, index);
|
|
51
|
+
if ("error" in parsed) errors.push(parsed.error);
|
|
52
|
+
else entries.push(parsed.entry);
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
entries,
|
|
56
|
+
errors
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function parseAssertionEntry(entry, field, index) {
|
|
60
|
+
if (!isEntryObject(entry)) return { error: {
|
|
61
|
+
field,
|
|
62
|
+
index,
|
|
63
|
+
reason: "entry must be a shorthand assertion object"
|
|
64
|
+
} };
|
|
65
|
+
const keys = Object.keys(entry);
|
|
66
|
+
if (keys.length !== 1) return { error: {
|
|
67
|
+
field,
|
|
68
|
+
index,
|
|
69
|
+
reason: "shorthand assertion object must have exactly one key"
|
|
70
|
+
} };
|
|
71
|
+
const type = keys[0];
|
|
72
|
+
const args = entry[type];
|
|
73
|
+
if (!isEntryObject(args)) return { error: {
|
|
74
|
+
field,
|
|
75
|
+
index,
|
|
76
|
+
reason: `shorthand assertion "${type}" value must be an object`
|
|
77
|
+
} };
|
|
78
|
+
return { entry: {
|
|
79
|
+
type,
|
|
80
|
+
args
|
|
81
|
+
} };
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/evidence-schema.ts
|
|
85
|
+
const EVIDENCE_SCHEMA_VERSION = "agent-skill-evals.evidence.v2";
|
|
86
|
+
const OptionalString = z.string().optional();
|
|
87
|
+
const OptionalNumber = z.number().optional();
|
|
88
|
+
const StringArray = z.array(z.string());
|
|
89
|
+
const CommandEventSchema = z.object({
|
|
90
|
+
command: z.string(),
|
|
91
|
+
args: z.array(z.string()).default([]),
|
|
92
|
+
exitCode: z.number(),
|
|
93
|
+
signal: OptionalString,
|
|
94
|
+
stdout: OptionalString,
|
|
95
|
+
stderr: OptionalString,
|
|
96
|
+
startedAt: z.number(),
|
|
97
|
+
durationMs: z.number(),
|
|
98
|
+
turn: OptionalNumber
|
|
99
|
+
});
|
|
100
|
+
const FileEventSchema = z.object({
|
|
101
|
+
path: z.string(),
|
|
102
|
+
op: z.enum([
|
|
103
|
+
"create",
|
|
104
|
+
"modify",
|
|
105
|
+
"delete"
|
|
106
|
+
])
|
|
107
|
+
});
|
|
108
|
+
const ToolCallEventSchema = z.object({
|
|
109
|
+
tool: z.string(),
|
|
110
|
+
provider: OptionalString,
|
|
111
|
+
server: OptionalString,
|
|
112
|
+
args: z.unknown().optional(),
|
|
113
|
+
result: z.unknown().optional(),
|
|
114
|
+
startedAt: z.number(),
|
|
115
|
+
durationMs: z.number(),
|
|
116
|
+
turn: OptionalNumber
|
|
117
|
+
});
|
|
118
|
+
const SkillLoadEventSchema = z.object({
|
|
119
|
+
skill: z.string(),
|
|
120
|
+
delivery: z.enum([
|
|
121
|
+
"native",
|
|
122
|
+
"mcp",
|
|
123
|
+
"explicit"
|
|
124
|
+
]),
|
|
125
|
+
provider: OptionalString,
|
|
126
|
+
server: OptionalString,
|
|
127
|
+
source: OptionalString,
|
|
128
|
+
startedAt: z.number()
|
|
129
|
+
});
|
|
130
|
+
const SkillAvailableEventSchema = z.object({
|
|
131
|
+
skill: z.string(),
|
|
132
|
+
path: z.string(),
|
|
133
|
+
role: z.enum([
|
|
134
|
+
"under-test",
|
|
135
|
+
"supporting",
|
|
136
|
+
"distractor"
|
|
137
|
+
])
|
|
138
|
+
});
|
|
139
|
+
const UsageSchema = z.object({
|
|
140
|
+
inputTokens: OptionalNumber,
|
|
141
|
+
outputTokens: OptionalNumber,
|
|
142
|
+
totalTokens: OptionalNumber,
|
|
143
|
+
cacheReadTokens: OptionalNumber,
|
|
144
|
+
cacheWriteTokens: OptionalNumber
|
|
145
|
+
});
|
|
146
|
+
const RunSummarySchema = z.object({
|
|
147
|
+
runDir: z.string(),
|
|
148
|
+
worldPath: z.string(),
|
|
149
|
+
fixture: z.string().optional(),
|
|
150
|
+
durationMs: OptionalNumber
|
|
151
|
+
});
|
|
152
|
+
const RuntimeIdentitySchema = z.object({
|
|
153
|
+
adapter: OptionalString,
|
|
154
|
+
preset: OptionalString,
|
|
155
|
+
command: OptionalString,
|
|
156
|
+
cliVersion: OptionalString,
|
|
157
|
+
model: OptionalString,
|
|
158
|
+
continuation: z.enum(["transcript-replay", "native-session"]).optional()
|
|
159
|
+
});
|
|
160
|
+
const TurnRecordSchema = z.object({
|
|
161
|
+
turn: z.number(),
|
|
162
|
+
role: z.enum(["user", "agent"]),
|
|
163
|
+
text: z.string(),
|
|
164
|
+
startedAt: z.number(),
|
|
165
|
+
durationMs: z.number(),
|
|
166
|
+
usage: UsageSchema.optional()
|
|
167
|
+
});
|
|
168
|
+
const EvidenceSnapshotSchema = z.object({
|
|
169
|
+
schemaVersion: z.literal(EVIDENCE_SCHEMA_VERSION),
|
|
170
|
+
output: z.string().default(""),
|
|
171
|
+
run: RunSummarySchema,
|
|
172
|
+
commands: z.array(CommandEventSchema).default([]),
|
|
173
|
+
filesWritten: z.array(FileEventSchema).default([]),
|
|
174
|
+
toolCalls: z.array(ToolCallEventSchema).default([]),
|
|
175
|
+
skillsLoaded: z.array(SkillLoadEventSchema).default([]),
|
|
176
|
+
skillsAvailable: z.array(SkillAvailableEventSchema).default([]),
|
|
177
|
+
usage: UsageSchema.default({}),
|
|
178
|
+
turns: z.array(TurnRecordSchema).optional(),
|
|
179
|
+
runtime: RuntimeIdentitySchema.optional(),
|
|
180
|
+
warnings: StringArray.optional(),
|
|
181
|
+
extensions: z.record(z.string(), z.unknown()).optional()
|
|
182
|
+
});
|
|
183
|
+
function decodeEvidenceSnapshot(input) {
|
|
184
|
+
const decoded = EvidenceSnapshotSchema.safeParse(input);
|
|
185
|
+
if (decoded.success) return {
|
|
186
|
+
ok: true,
|
|
187
|
+
value: decoded.data
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
error: /* @__PURE__ */ new Error(`the evidence file does not match the agent-skill-evals.evidence.v2 shape. It was probably written by a different agent-skill-evals version or edited by hand — re-run the eval to regenerate it. Details: ${z.prettifyError(decoded.error)}`)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function parseEvidenceSnapshot(input) {
|
|
195
|
+
const decoded = decodeEvidenceSnapshot(input);
|
|
196
|
+
if (decoded.ok) return decoded.value;
|
|
197
|
+
throw decoded.error;
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/agent/evidence.ts
|
|
201
|
+
const DEFAULT_SKILL_EVIDENCE_CONFIG = {
|
|
202
|
+
mcpResource: {
|
|
203
|
+
uriArgPaths: ["uri"],
|
|
204
|
+
uriPatterns: ["^skill://(?<skill>[^/]+)/SKILL\\.md$"]
|
|
205
|
+
},
|
|
206
|
+
mcpTool: { toolPatterns: ["^load_(?<skill>[A-Za-z0-9_-]+)_skill$"] }
|
|
207
|
+
};
|
|
208
|
+
var EvidenceCollector = class EvidenceCollector {
|
|
209
|
+
skillEvidenceConfig;
|
|
210
|
+
currentTurn;
|
|
211
|
+
/** Usage from completed turns; the live snapshot.usage covers only the current turn. */
|
|
212
|
+
completedTurnsUsage = {};
|
|
213
|
+
snapshot = {
|
|
214
|
+
schemaVersion: EVIDENCE_SCHEMA_VERSION,
|
|
215
|
+
output: "",
|
|
216
|
+
run: {
|
|
217
|
+
runDir: "",
|
|
218
|
+
worldPath: "",
|
|
219
|
+
fixture: ""
|
|
220
|
+
},
|
|
221
|
+
commands: [],
|
|
222
|
+
filesWritten: [],
|
|
223
|
+
toolCalls: [],
|
|
224
|
+
skillsLoaded: [],
|
|
225
|
+
skillsAvailable: [],
|
|
226
|
+
usage: {}
|
|
227
|
+
};
|
|
228
|
+
constructor(skillEvidenceConfig = {}) {
|
|
229
|
+
this.skillEvidenceConfig = mergeSkillEvidenceConfig(skillEvidenceConfig);
|
|
230
|
+
}
|
|
231
|
+
addCommand(e) {
|
|
232
|
+
this.snapshot.commands.push(this.withTurn(e));
|
|
233
|
+
}
|
|
234
|
+
addFileWrite(e) {
|
|
235
|
+
this.snapshot.filesWritten.push(e);
|
|
236
|
+
}
|
|
237
|
+
addToolCall(e) {
|
|
238
|
+
const event = this.withTurn(e);
|
|
239
|
+
this.snapshot.toolCalls.push(event);
|
|
240
|
+
const skillLoad = skillLoadFromToolCall(event, this.skillEvidenceConfig);
|
|
241
|
+
if (skillLoad) this.addSkillLoad(skillLoad);
|
|
242
|
+
}
|
|
243
|
+
withTurn(event) {
|
|
244
|
+
return this.currentTurn === void 0 ? event : {
|
|
245
|
+
...event,
|
|
246
|
+
turn: this.currentTurn
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Start an agent turn in a multi-turn run. Adapters overwrite usage per
|
|
251
|
+
* CLI invocation (setUsage), so each turn's usage is scoped: the previous
|
|
252
|
+
* turn's counts move into the completed-turns total first.
|
|
253
|
+
*/
|
|
254
|
+
beginAgentTurn(turn) {
|
|
255
|
+
this.completedTurnsUsage = mergeUsage(this.completedTurnsUsage, this.snapshot.usage);
|
|
256
|
+
this.snapshot.usage = {};
|
|
257
|
+
this.currentTurn = turn;
|
|
258
|
+
}
|
|
259
|
+
endAgentTurn(record) {
|
|
260
|
+
if (this.currentTurn === void 0) return;
|
|
261
|
+
this.addTurnRecord({
|
|
262
|
+
turn: this.currentTurn,
|
|
263
|
+
role: "agent",
|
|
264
|
+
text: record.text,
|
|
265
|
+
startedAt: record.startedAt,
|
|
266
|
+
durationMs: record.durationMs,
|
|
267
|
+
usage: { ...this.snapshot.usage }
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
addUserTurn(turn, text, startedAt) {
|
|
271
|
+
this.addTurnRecord({
|
|
272
|
+
turn,
|
|
273
|
+
role: "user",
|
|
274
|
+
text,
|
|
275
|
+
startedAt,
|
|
276
|
+
durationMs: 0
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
addTurnRecord(record) {
|
|
280
|
+
const turns = this.snapshot.turns ?? [];
|
|
281
|
+
turns.push(record);
|
|
282
|
+
this.snapshot.turns = turns;
|
|
283
|
+
}
|
|
284
|
+
addSkillLoad(e) {
|
|
285
|
+
if (this.snapshot.skillsLoaded.some((existing) => existing.skill === e.skill && existing.delivery === e.delivery && existing.provider === e.provider && existing.server === e.server)) return;
|
|
286
|
+
this.snapshot.skillsLoaded.push(e);
|
|
287
|
+
}
|
|
288
|
+
addSkillAvailable(e) {
|
|
289
|
+
if (!this.snapshot.skillsAvailable.some((existing) => existing.skill === e.skill && existing.path === e.path)) this.snapshot.skillsAvailable.push(e);
|
|
290
|
+
}
|
|
291
|
+
setUsage(u) {
|
|
292
|
+
this.snapshot.usage = u;
|
|
293
|
+
}
|
|
294
|
+
/** Merge non-empty runtime identity fields; later observations win. */
|
|
295
|
+
mergeRuntime(identity) {
|
|
296
|
+
const merged = { ...this.snapshot.runtime };
|
|
297
|
+
for (const [key, value] of Object.entries(identity)) if (typeof value === "string" && value.length > 0) merged[key] = value;
|
|
298
|
+
if (Object.keys(merged).length > 0) this.snapshot.runtime = merged;
|
|
299
|
+
}
|
|
300
|
+
addWarning(warning) {
|
|
301
|
+
const warnings = this.snapshot.warnings ?? [];
|
|
302
|
+
if (warnings.includes(warning)) return;
|
|
303
|
+
warnings.push(warning);
|
|
304
|
+
this.snapshot.warnings = warnings;
|
|
305
|
+
}
|
|
306
|
+
addUsage(u) {
|
|
307
|
+
this.snapshot.usage = mergeUsage(this.snapshot.usage, u);
|
|
308
|
+
}
|
|
309
|
+
setOutput(output) {
|
|
310
|
+
this.snapshot.output = output;
|
|
311
|
+
}
|
|
312
|
+
setRun(run) {
|
|
313
|
+
this.snapshot.run = run;
|
|
314
|
+
}
|
|
315
|
+
toSnapshot() {
|
|
316
|
+
return parseEvidenceSnapshot({
|
|
317
|
+
schemaVersion: this.snapshot.schemaVersion,
|
|
318
|
+
output: this.snapshot.output,
|
|
319
|
+
run: { ...this.snapshot.run },
|
|
320
|
+
commands: [...this.snapshot.commands],
|
|
321
|
+
filesWritten: [...this.snapshot.filesWritten],
|
|
322
|
+
toolCalls: [...this.snapshot.toolCalls],
|
|
323
|
+
skillsLoaded: [...this.snapshot.skillsLoaded],
|
|
324
|
+
skillsAvailable: [...this.snapshot.skillsAvailable],
|
|
325
|
+
usage: mergeUsage(this.completedTurnsUsage, this.snapshot.usage),
|
|
326
|
+
turns: this.snapshot.turns ? this.snapshot.turns.map((t) => ({ ...t })) : void 0,
|
|
327
|
+
runtime: this.snapshot.runtime ? { ...this.snapshot.runtime } : void 0,
|
|
328
|
+
warnings: this.snapshot.warnings ? [...this.snapshot.warnings] : void 0,
|
|
329
|
+
extensions: this.snapshot.extensions ? { ...this.snapshot.extensions } : void 0
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
static fromSnapshot(snapshot) {
|
|
333
|
+
const collector = new EvidenceCollector();
|
|
334
|
+
collector.snapshot = parseEvidenceSnapshot(snapshot);
|
|
335
|
+
return collector;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
function mergeSkillEvidenceConfig(config) {
|
|
339
|
+
return {
|
|
340
|
+
mcpResource: {
|
|
341
|
+
uriArgPaths: config.mcpResource?.uriArgPaths ?? DEFAULT_SKILL_EVIDENCE_CONFIG.mcpResource.uriArgPaths,
|
|
342
|
+
uriPatterns: config.mcpResource?.uriPatterns ?? DEFAULT_SKILL_EVIDENCE_CONFIG.mcpResource.uriPatterns
|
|
343
|
+
},
|
|
344
|
+
mcpTool: { toolPatterns: config.mcpTool?.toolPatterns ?? DEFAULT_SKILL_EVIDENCE_CONFIG.mcpTool.toolPatterns },
|
|
345
|
+
...config.nativeArgs ? { nativeArgs: config.nativeArgs } : {}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function skillLoadFromToolCall(event, config) {
|
|
349
|
+
const uri = skillUriFromArgs(event.args, config.mcpResource?.uriArgPaths ?? []);
|
|
350
|
+
const skill = uri ? skillFromPattern(uri, config.mcpResource?.uriPatterns ?? []) : skillFromTool(event.tool, config.mcpTool?.toolPatterns ?? []);
|
|
351
|
+
if (!skill) return void 0;
|
|
352
|
+
const server = event.server ?? serverFromArgs(event.args);
|
|
353
|
+
return {
|
|
354
|
+
skill,
|
|
355
|
+
delivery: "mcp",
|
|
356
|
+
...event.provider ? { provider: event.provider } : {},
|
|
357
|
+
...server ? { server } : {},
|
|
358
|
+
source: event.tool,
|
|
359
|
+
startedAt: event.startedAt
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function skillUriFromArgs(args, paths) {
|
|
363
|
+
for (const path of paths) {
|
|
364
|
+
const uri = valueAtPath(args, path);
|
|
365
|
+
if (typeof uri === "string") return uri;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function serverFromArgs(args) {
|
|
369
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return void 0;
|
|
370
|
+
const server = args.server;
|
|
371
|
+
return typeof server === "string" ? server : void 0;
|
|
372
|
+
}
|
|
373
|
+
function skillFromPattern(value, patterns) {
|
|
374
|
+
for (const pattern of patterns) {
|
|
375
|
+
let regex;
|
|
376
|
+
try {
|
|
377
|
+
regex = new RegExp(pattern, "i");
|
|
378
|
+
} catch {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const match = regex.exec(value);
|
|
382
|
+
const skill = match?.groups?.skill ?? match?.[1];
|
|
383
|
+
if (skill) return skill;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function skillFromTool(tool, patterns) {
|
|
387
|
+
return skillFromPattern(tool, patterns)?.replace(/_/g, "-");
|
|
388
|
+
}
|
|
389
|
+
function valueAtPath(value, path) {
|
|
390
|
+
let current = value;
|
|
391
|
+
for (const segment of path.split(".")) {
|
|
392
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
|
|
393
|
+
current = current[segment];
|
|
394
|
+
}
|
|
395
|
+
return current;
|
|
396
|
+
}
|
|
397
|
+
function addOptionalNumbers(a, b) {
|
|
398
|
+
if (a === void 0) return b;
|
|
399
|
+
if (b === void 0) return a;
|
|
400
|
+
return a + b;
|
|
401
|
+
}
|
|
402
|
+
function mergeUsage(a, b) {
|
|
403
|
+
return {
|
|
404
|
+
inputTokens: addOptionalNumbers(a.inputTokens, b.inputTokens),
|
|
405
|
+
outputTokens: addOptionalNumbers(a.outputTokens, b.outputTokens),
|
|
406
|
+
totalTokens: addOptionalNumbers(a.totalTokens, b.totalTokens),
|
|
407
|
+
cacheReadTokens: addOptionalNumbers(a.cacheReadTokens, b.cacheReadTokens),
|
|
408
|
+
cacheWriteTokens: addOptionalNumbers(a.cacheWriteTokens, b.cacheWriteTokens)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
async function writeEvidenceTo(collector, runDir) {
|
|
412
|
+
const path = join(runDir, "evidence.json");
|
|
413
|
+
await writeFile(path, JSON.stringify(collector.toSnapshot(), null, 2));
|
|
414
|
+
return path;
|
|
415
|
+
}
|
|
416
|
+
function evidenceFromSnapshot(s) {
|
|
417
|
+
return {
|
|
418
|
+
output: () => s.output,
|
|
419
|
+
commands: () => s.commands,
|
|
420
|
+
filesWritten: () => s.filesWritten,
|
|
421
|
+
toolCalls: () => s.toolCalls,
|
|
422
|
+
skillsLoaded: () => s.skillsLoaded,
|
|
423
|
+
skillsAvailable: () => s.skillsAvailable,
|
|
424
|
+
usage: () => s.usage,
|
|
425
|
+
turns: () => s.turns ?? [],
|
|
426
|
+
runtime: () => s.runtime ?? {},
|
|
427
|
+
warnings: () => s.warnings ?? []
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/agent/command-runner.ts
|
|
432
|
+
const DEFAULT_OUTPUT_LIMIT = 4096;
|
|
433
|
+
const KILL_GRACE_MS = 1e3;
|
|
434
|
+
const EXIT_STDIO_FLUSH_MS = 50;
|
|
435
|
+
function appendLimited(current, chunk, limit) {
|
|
436
|
+
if (limit <= 0) return "";
|
|
437
|
+
const next = current + chunk;
|
|
438
|
+
return next.length <= limit ? next : next.slice(next.length - limit);
|
|
439
|
+
}
|
|
440
|
+
function killProcessGroup(child, signal) {
|
|
441
|
+
if (!child.pid) return;
|
|
442
|
+
try {
|
|
443
|
+
if (process.platform === "win32") child.kill(signal);
|
|
444
|
+
else process.kill(-child.pid, signal);
|
|
445
|
+
} catch {
|
|
446
|
+
try {
|
|
447
|
+
child.kill(signal);
|
|
448
|
+
} catch {}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function runCommand(command, args = [], options) {
|
|
452
|
+
const stdoutLimit = options.stdoutLimit ?? DEFAULT_OUTPUT_LIMIT;
|
|
453
|
+
const stderrLimit = options.stderrLimit ?? DEFAULT_OUTPUT_LIMIT;
|
|
454
|
+
const startedAt = Date.now();
|
|
455
|
+
return new Promise((resolve) => {
|
|
456
|
+
const child = spawn(command, [...args], {
|
|
457
|
+
cwd: options.cwd,
|
|
458
|
+
env: options.env,
|
|
459
|
+
detached: process.platform !== "win32",
|
|
460
|
+
stdio: [
|
|
461
|
+
"pipe",
|
|
462
|
+
"pipe",
|
|
463
|
+
"pipe"
|
|
464
|
+
]
|
|
465
|
+
});
|
|
466
|
+
let stdout = "";
|
|
467
|
+
let stderr = "";
|
|
468
|
+
let timedOut = false;
|
|
469
|
+
let settled = false;
|
|
470
|
+
let timeoutTimer = null;
|
|
471
|
+
let forceTimer = null;
|
|
472
|
+
let exitFlushTimer = null;
|
|
473
|
+
const finish = (result) => {
|
|
474
|
+
if (settled) return;
|
|
475
|
+
settled = true;
|
|
476
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
477
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
478
|
+
if (exitFlushTimer) clearTimeout(exitFlushTimer);
|
|
479
|
+
child.stdout?.destroy();
|
|
480
|
+
child.stderr?.destroy();
|
|
481
|
+
child.stdin?.destroy();
|
|
482
|
+
if (process.platform !== "win32") killProcessGroup(child, "SIGTERM");
|
|
483
|
+
resolve({
|
|
484
|
+
...result,
|
|
485
|
+
durationMs: Date.now() - startedAt
|
|
486
|
+
});
|
|
487
|
+
};
|
|
488
|
+
child.stdout?.on("data", (chunk) => {
|
|
489
|
+
const text = chunk.toString();
|
|
490
|
+
stdout = appendLimited(stdout, text, stdoutLimit);
|
|
491
|
+
options.onStdout?.(text);
|
|
492
|
+
});
|
|
493
|
+
child.stderr?.on("data", (chunk) => {
|
|
494
|
+
const text = chunk.toString();
|
|
495
|
+
stderr = appendLimited(stderr, text, stderrLimit);
|
|
496
|
+
options.onStderr?.(text);
|
|
497
|
+
});
|
|
498
|
+
child.on("error", (error) => {
|
|
499
|
+
stderr = appendLimited(stderr, String(error), stderrLimit);
|
|
500
|
+
finish({
|
|
501
|
+
command,
|
|
502
|
+
args: [...args],
|
|
503
|
+
exitCode: -1,
|
|
504
|
+
stdout,
|
|
505
|
+
stderr,
|
|
506
|
+
startedAt,
|
|
507
|
+
timedOut,
|
|
508
|
+
error
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
child.on("exit", (code, signal) => {
|
|
512
|
+
const result = {
|
|
513
|
+
command,
|
|
514
|
+
args: [...args],
|
|
515
|
+
exitCode: code ?? -1,
|
|
516
|
+
signal: signal ?? void 0,
|
|
517
|
+
stdout,
|
|
518
|
+
stderr,
|
|
519
|
+
startedAt,
|
|
520
|
+
timedOut
|
|
521
|
+
};
|
|
522
|
+
exitFlushTimer = setTimeout(() => finish(result), EXIT_STDIO_FLUSH_MS);
|
|
523
|
+
});
|
|
524
|
+
child.on("close", (code, signal) => {
|
|
525
|
+
finish({
|
|
526
|
+
command,
|
|
527
|
+
args: [...args],
|
|
528
|
+
exitCode: code ?? -1,
|
|
529
|
+
signal: signal ?? void 0,
|
|
530
|
+
stdout,
|
|
531
|
+
stderr,
|
|
532
|
+
startedAt,
|
|
533
|
+
timedOut
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
if (options.stdin !== void 0) child.stdin?.write(options.stdin);
|
|
537
|
+
child.stdin?.end();
|
|
538
|
+
if (options.timeoutMs) timeoutTimer = setTimeout(() => {
|
|
539
|
+
timedOut = true;
|
|
540
|
+
stderr = appendLimited(stderr, `${stderr ? "\n" : ""}agent-skill-evals: command timed out after ${options.timeoutMs}ms`, stderrLimit);
|
|
541
|
+
killProcessGroup(child, "SIGKILL");
|
|
542
|
+
forceTimer = setTimeout(() => {
|
|
543
|
+
finish({
|
|
544
|
+
command,
|
|
545
|
+
args: [...args],
|
|
546
|
+
exitCode: -1,
|
|
547
|
+
signal: "SIGKILL",
|
|
548
|
+
stdout,
|
|
549
|
+
stderr,
|
|
550
|
+
startedAt,
|
|
551
|
+
timedOut
|
|
552
|
+
});
|
|
553
|
+
}, KILL_GRACE_MS);
|
|
554
|
+
}, options.timeoutMs);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/agent/jsonl-stream.ts
|
|
559
|
+
function appendLine(events, line) {
|
|
560
|
+
if (!line.trim()) return events;
|
|
561
|
+
try {
|
|
562
|
+
return [...events, JSON.parse(line)];
|
|
563
|
+
} catch {
|
|
564
|
+
return events;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function parseChunk(state, text) {
|
|
568
|
+
const lines = (state.leftover + text).split("\n");
|
|
569
|
+
return {
|
|
570
|
+
leftover: lines.pop() ?? "",
|
|
571
|
+
events: lines.reduce(appendLine, state.events)
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function createJsonlEventParser() {
|
|
575
|
+
let leftover = "";
|
|
576
|
+
return {
|
|
577
|
+
push(chunk) {
|
|
578
|
+
const state = parseChunk({
|
|
579
|
+
leftover,
|
|
580
|
+
events: []
|
|
581
|
+
}, chunk);
|
|
582
|
+
leftover = state.leftover;
|
|
583
|
+
return state.events;
|
|
584
|
+
},
|
|
585
|
+
finish() {
|
|
586
|
+
const events = appendLine([], leftover);
|
|
587
|
+
leftover = "";
|
|
588
|
+
return events;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/agent/adapters.ts
|
|
594
|
+
const pendingPiToolCalls = /* @__PURE__ */ new WeakMap();
|
|
595
|
+
function argsWithPrompt(args, prompt) {
|
|
596
|
+
const dashIndex = args.lastIndexOf("-");
|
|
597
|
+
if (dashIndex === -1) return [...args, prompt];
|
|
598
|
+
return args.map((arg, index) => index === dashIndex ? prompt : arg);
|
|
599
|
+
}
|
|
600
|
+
function normalizePathFromCwd(path, cwd) {
|
|
601
|
+
if (!isAbsolute(path)) return path;
|
|
602
|
+
const candidate = path.startsWith("/private/") ? path.slice(8) : path;
|
|
603
|
+
const rel = relative(cwd.startsWith("/private/") ? cwd.slice(8) : cwd, candidate);
|
|
604
|
+
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : path;
|
|
605
|
+
}
|
|
606
|
+
function normalizeToolCallArgs(args, cwd) {
|
|
607
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return args;
|
|
608
|
+
const normalized = {};
|
|
609
|
+
for (const [key, value] of Object.entries(args)) {
|
|
610
|
+
const normalizedValue = (key === "path" || key === "file_path") && typeof value === "string" ? normalizePathFromCwd(value, cwd) : value;
|
|
611
|
+
normalized[key] = normalizedValue;
|
|
612
|
+
if (key === "file_path" && typeof normalizedValue === "string" && normalized.path === void 0) normalized.path = normalizedValue;
|
|
613
|
+
}
|
|
614
|
+
return normalized;
|
|
615
|
+
}
|
|
616
|
+
function normalizeMcpToolCall(event) {
|
|
617
|
+
const match = /^mcp__(.+?)__(.+)$/.exec(event.tool);
|
|
618
|
+
if (!match) return event;
|
|
619
|
+
return {
|
|
620
|
+
...event,
|
|
621
|
+
server: event.server ?? match[1],
|
|
622
|
+
tool: match[2] ?? event.tool
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function parseJsonObjectString(value) {
|
|
626
|
+
if (typeof value !== "string") return value;
|
|
627
|
+
try {
|
|
628
|
+
const parsed = JSON.parse(value);
|
|
629
|
+
return parsed && typeof parsed === "object" ? parsed : value;
|
|
630
|
+
} catch {
|
|
631
|
+
return value;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function evidenceWithRelativeToolPaths(evidence, cwd, now) {
|
|
635
|
+
return {
|
|
636
|
+
addCommand: (event) => evidence.addCommand(event),
|
|
637
|
+
addToolCall: (event) => {
|
|
638
|
+
const normalized = normalizeMcpToolCall(event);
|
|
639
|
+
evidence.addToolCall({
|
|
640
|
+
...normalized,
|
|
641
|
+
args: normalizeToolCallArgs(normalized.args, cwd)
|
|
642
|
+
});
|
|
643
|
+
},
|
|
644
|
+
setUsage: (usage) => evidence.setUsage(usage),
|
|
645
|
+
addUsage: (usage) => evidence.addUsage(usage),
|
|
646
|
+
setModel: (model) => evidence.mergeRuntime({ model }),
|
|
647
|
+
now
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function normalizeUsage(usage) {
|
|
651
|
+
const inputTokens = usage.input_tokens ?? usage.inputTokens ?? usage.input;
|
|
652
|
+
const outputTokens = usage.output_tokens ?? usage.outputTokens ?? usage.output;
|
|
653
|
+
const cacheReadTokens = usage.cache_read_input_tokens ?? usage.cacheReadTokens ?? usage.cacheRead;
|
|
654
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens ?? usage.cacheWriteTokens ?? usage.cacheWrite;
|
|
655
|
+
return {
|
|
656
|
+
inputTokens,
|
|
657
|
+
outputTokens,
|
|
658
|
+
totalTokens: usage.total_tokens ?? usage.totalTokens ?? addKnownNumbers(inputTokens, outputTokens),
|
|
659
|
+
cacheReadTokens,
|
|
660
|
+
cacheWriteTokens
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function addKnownNumbers(...values) {
|
|
664
|
+
const present = values.filter((value) => typeof value === "number");
|
|
665
|
+
return present.length > 0 ? present.reduce((sum, value) => sum + value, 0) : void 0;
|
|
666
|
+
}
|
|
667
|
+
function normalizePiToolName(tool) {
|
|
668
|
+
switch (tool.toLowerCase()) {
|
|
669
|
+
case "bash": return "Bash";
|
|
670
|
+
case "edit": return "Edit";
|
|
671
|
+
case "read": return "Read";
|
|
672
|
+
case "write": return "Write";
|
|
673
|
+
default: return tool;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function evidenceStartedAt(evidence) {
|
|
677
|
+
return evidence.now?.() ?? 0;
|
|
678
|
+
}
|
|
679
|
+
async function runJsonlAdapter(input, onEvent, options = {}) {
|
|
680
|
+
const { command, args, cwd, prompt, evidence, timeoutMs, env } = input;
|
|
681
|
+
const promptDelivery = options.promptDelivery ?? "stdin";
|
|
682
|
+
const spawnArgs = promptDelivery === "arg" ? argsWithPrompt(args, prompt) : [...args];
|
|
683
|
+
const runStartedAt = Date.now();
|
|
684
|
+
const adapterEvidence = evidenceWithRelativeToolPaths(evidence, cwd, () => runStartedAt);
|
|
685
|
+
const parser = createJsonlEventParser();
|
|
686
|
+
const unrecognized = /* @__PURE__ */ new Map();
|
|
687
|
+
let finalText = "";
|
|
688
|
+
let terminalError;
|
|
689
|
+
const dispatch = (evt) => {
|
|
690
|
+
terminalError = options.terminalError?.(evt) ?? terminalError;
|
|
691
|
+
if (onEvent(evt, adapterEvidence, (text, replace = false) => {
|
|
692
|
+
finalText = replace ? text : finalText + text;
|
|
693
|
+
})) return;
|
|
694
|
+
const type = evt && typeof evt === "object" && typeof evt.type === "string" ? evt.type : "<no type>";
|
|
695
|
+
unrecognized.set(type, (unrecognized.get(type) ?? 0) + 1);
|
|
696
|
+
};
|
|
697
|
+
const handleChunk = (chunk) => {
|
|
698
|
+
for (const evt of parser.push(chunk)) dispatch(evt);
|
|
699
|
+
};
|
|
700
|
+
const result = await runCommand(command, spawnArgs, {
|
|
701
|
+
cwd,
|
|
702
|
+
env,
|
|
703
|
+
stdin: promptDelivery === "stdin" ? prompt : void 0,
|
|
704
|
+
timeoutMs,
|
|
705
|
+
stdoutLimit: 0,
|
|
706
|
+
onStdout: handleChunk
|
|
707
|
+
});
|
|
708
|
+
for (const evt of parser.finish()) dispatch(evt);
|
|
709
|
+
if (unrecognized.size > 0) {
|
|
710
|
+
const summary = [...unrecognized.entries()].map(([type, count]) => `${type} (${count})`).join(", ");
|
|
711
|
+
evidence.addWarning(`${options.adapterId ?? "adapter"}: unrecognized event type(s) in CLI output: ${summary}. The CLI's JSON schema may have changed since this adapter was written; recorded evidence may be incomplete.`);
|
|
712
|
+
}
|
|
713
|
+
evidence.addCommand({
|
|
714
|
+
command,
|
|
715
|
+
args: [...spawnArgs],
|
|
716
|
+
exitCode: result.exitCode,
|
|
717
|
+
stderr: result.stderr.slice(0, 4096),
|
|
718
|
+
startedAt: result.startedAt,
|
|
719
|
+
durationMs: result.durationMs
|
|
720
|
+
});
|
|
721
|
+
const error = result.error ? `adapter error: failed to start "${command}": ${result.error.message}` : result.timedOut ? `${command} timed out after ${timeoutMs ?? 0}ms` : result.exitCode !== 0 ? `${command} exited ${result.exitCode}${result.stderr.trim() ? `: ${result.stderr.trim()}` : ""}` : terminalError;
|
|
722
|
+
if (error && !finalText.trim()) finalText = error;
|
|
723
|
+
return {
|
|
724
|
+
output: finalText.trim(),
|
|
725
|
+
exitCode: result.exitCode,
|
|
726
|
+
timedOut: result.timedOut,
|
|
727
|
+
...error ? { error } : {},
|
|
728
|
+
durationMs: result.durationMs
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Claude Code stream-json adapter: parses stream-json events emitted by
|
|
733
|
+
* `claude -p ... --output-format stream-json` and projects them into evidence.
|
|
734
|
+
*
|
|
735
|
+
* Events of interest (see Claude Code docs):
|
|
736
|
+
* - { type: "system" | "assistant" | "user" | "result", ... }
|
|
737
|
+
* - tool_use blocks inside assistant content (Bash, Edit, Write, etc.)
|
|
738
|
+
*/
|
|
739
|
+
const claudeCodeJsonAdapter = {
|
|
740
|
+
id: "claude-code-json",
|
|
741
|
+
async run(input) {
|
|
742
|
+
const result = await runJsonlAdapter(input, handleClaudeEvent, { adapterId: "claude-code-json" });
|
|
743
|
+
if (result.exitCode !== 0 && /not logged in/i.test(result.output)) {
|
|
744
|
+
const hint = "Claude Code runs with an isolated HOME and cannot reuse a macOS Keychain login. Set CLAUDE_CODE_OAUTH_TOKEN (created by `claude setup-token`) or ANTHROPIC_API_KEY.";
|
|
745
|
+
return {
|
|
746
|
+
...result,
|
|
747
|
+
output: `${result.output}\n\n${hint}`,
|
|
748
|
+
error: `${result.error ?? "Claude Code authentication failed"}. ${hint}`
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
return result;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
const codexJsonAdapter = {
|
|
755
|
+
id: "codex-json",
|
|
756
|
+
run(input) {
|
|
757
|
+
return runJsonlAdapter(input, handleCodexEvent, {
|
|
758
|
+
promptDelivery: "arg",
|
|
759
|
+
adapterId: "codex-json"
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
const piJsonAdapter = {
|
|
764
|
+
id: "pi-json",
|
|
765
|
+
async run(input) {
|
|
766
|
+
const result = await runJsonlAdapter(input, handlePiEvent, {
|
|
767
|
+
adapterId: "pi-json",
|
|
768
|
+
terminalError: piTerminalError
|
|
769
|
+
});
|
|
770
|
+
if (result.error && /no api key for provider/i.test(result.error)) return {
|
|
771
|
+
...result,
|
|
772
|
+
error: `Pi authentication failed: ${result.error}`
|
|
773
|
+
};
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
function piTerminalError(event) {
|
|
778
|
+
if (!event || typeof event !== "object") return void 0;
|
|
779
|
+
const message = event.message;
|
|
780
|
+
if (!message || typeof message !== "object") return void 0;
|
|
781
|
+
const terminal = message;
|
|
782
|
+
return terminal.stopReason === "error" && typeof terminal.errorMessage === "string" ? terminal.errorMessage : void 0;
|
|
783
|
+
}
|
|
784
|
+
/** Event types Claude Code's stream-json output is known to emit. */
|
|
785
|
+
const CLAUDE_KNOWN_EVENT_TYPES = new Set([
|
|
786
|
+
"system",
|
|
787
|
+
"assistant",
|
|
788
|
+
"user",
|
|
789
|
+
"result",
|
|
790
|
+
"stream_event",
|
|
791
|
+
"rate_limit_event"
|
|
792
|
+
]);
|
|
793
|
+
function handleClaudeEvent(evt, evidence, appendFinal) {
|
|
794
|
+
if (!evt || typeof evt !== "object") return false;
|
|
795
|
+
const e = evt;
|
|
796
|
+
if (e.type === "system" && typeof e.model === "string") evidence.setModel?.(e.model);
|
|
797
|
+
if (e.type === "result" && typeof e.result === "string") appendFinal(e.result);
|
|
798
|
+
if (e.type === "assistant" && e.message && typeof e.message === "object") {
|
|
799
|
+
const msg = e.message;
|
|
800
|
+
if (typeof msg.model === "string") evidence.setModel?.(msg.model);
|
|
801
|
+
if (msg.usage && typeof msg.usage === "object") evidence.addUsage(normalizeUsage(msg.usage));
|
|
802
|
+
if (Array.isArray(msg.content)) for (const block of msg.content) {
|
|
803
|
+
if (!block || typeof block !== "object") continue;
|
|
804
|
+
const b = block;
|
|
805
|
+
if (b.type === "tool_use" && typeof b.name === "string") evidence.addToolCall({
|
|
806
|
+
tool: b.name,
|
|
807
|
+
provider: "claude-code-json",
|
|
808
|
+
args: b.input,
|
|
809
|
+
startedAt: evidenceStartedAt(evidence),
|
|
810
|
+
durationMs: 0
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (e.type === "result" && e.usage && typeof e.usage === "object") evidence.setUsage(normalizeUsage(e.usage));
|
|
815
|
+
return typeof e.type === "string" && CLAUDE_KNOWN_EVENT_TYPES.has(e.type);
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Codex lifecycle event families that carry no evidence but are normal
|
|
819
|
+
* output — recognized so they do not trigger schema-drift warnings.
|
|
820
|
+
*/
|
|
821
|
+
const CODEX_KNOWN_TYPE_PATTERN = /^(thread|turn|session|item|task|agent|token_count|notification|status|reasoning|error|usage)([._:]|$)/;
|
|
822
|
+
function handleCodexEvent(evt, evidence, appendFinal) {
|
|
823
|
+
if (!evt || typeof evt !== "object") return false;
|
|
824
|
+
const e = evt;
|
|
825
|
+
const type = typeof e.type === "string" ? e.type : "";
|
|
826
|
+
let consumed = false;
|
|
827
|
+
if (typeof e.model === "string") evidence.setModel?.(e.model);
|
|
828
|
+
const text = typeof e.message === "string" ? e.message : typeof e.text === "string" ? e.text : typeof e.content === "string" ? e.content : typeof e.output === "string" ? e.output : void 0;
|
|
829
|
+
if (text && /message|final|result|response|output/.test(type)) {
|
|
830
|
+
appendFinal(text);
|
|
831
|
+
consumed = true;
|
|
832
|
+
}
|
|
833
|
+
const item = e.item && typeof e.item === "object" ? e.item : e;
|
|
834
|
+
const itemType = typeof item.type === "string" ? item.type : "";
|
|
835
|
+
const toolName = typeof item.tool === "string" ? item.tool : typeof item.name === "string" && /tool|call|command/.test(`${type}:${itemType}`) ? item.name : void 0;
|
|
836
|
+
if (toolName && /tool|call|command/.test(`${type}:${itemType}`)) {
|
|
837
|
+
evidence.addToolCall({
|
|
838
|
+
tool: toolName,
|
|
839
|
+
provider: "codex-json",
|
|
840
|
+
server: typeof item.server === "string" ? item.server : void 0,
|
|
841
|
+
args: parseJsonObjectString(item.args ?? item.input ?? item.arguments),
|
|
842
|
+
result: item.result ?? item.output,
|
|
843
|
+
startedAt: evidenceStartedAt(evidence),
|
|
844
|
+
durationMs: 0
|
|
845
|
+
});
|
|
846
|
+
consumed = true;
|
|
847
|
+
}
|
|
848
|
+
if ((type === "file_change" || type === "event" || /(?:completed|failed|result)$/.test(type)) && itemType === "file_change" && Array.isArray(item.changes)) {
|
|
849
|
+
for (const change of item.changes) {
|
|
850
|
+
if (!change || typeof change !== "object") continue;
|
|
851
|
+
const c = change;
|
|
852
|
+
if (typeof c.path !== "string") continue;
|
|
853
|
+
evidence.addToolCall({
|
|
854
|
+
tool: "Edit",
|
|
855
|
+
provider: "codex-json",
|
|
856
|
+
args: {
|
|
857
|
+
path: c.path,
|
|
858
|
+
kind: typeof c.kind === "string" ? c.kind : void 0
|
|
859
|
+
},
|
|
860
|
+
startedAt: evidenceStartedAt(evidence),
|
|
861
|
+
durationMs: 0
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
consumed = true;
|
|
865
|
+
}
|
|
866
|
+
if ((type === "exec_command" || /(?:completed|failed|result)$/.test(type)) && (type.includes("exec") || type.includes("command") || itemType.includes("command")) && typeof item.command === "string") {
|
|
867
|
+
const args = Array.isArray(item.args) ? item.args.map(String) : [];
|
|
868
|
+
evidence.addCommand({
|
|
869
|
+
command: item.command,
|
|
870
|
+
args,
|
|
871
|
+
exitCode: typeof item.exit_code === "number" ? item.exit_code : typeof item.exitCode === "number" ? item.exitCode : 0,
|
|
872
|
+
stdout: typeof item.stdout === "string" ? item.stdout.slice(0, 4096) : typeof item.aggregated_output === "string" ? item.aggregated_output.slice(0, 4096) : void 0,
|
|
873
|
+
stderr: typeof item.stderr === "string" ? item.stderr.slice(0, 4096) : void 0,
|
|
874
|
+
startedAt: evidenceStartedAt(evidence),
|
|
875
|
+
durationMs: typeof item.durationMs === "number" ? item.durationMs : 0
|
|
876
|
+
});
|
|
877
|
+
consumed = true;
|
|
878
|
+
}
|
|
879
|
+
const usage = e.usage && typeof e.usage === "object" ? e.usage : void 0;
|
|
880
|
+
if (usage) {
|
|
881
|
+
evidence.setUsage(normalizeUsage(usage));
|
|
882
|
+
consumed = true;
|
|
883
|
+
}
|
|
884
|
+
return consumed || CODEX_KNOWN_TYPE_PATTERN.test(type);
|
|
885
|
+
}
|
|
886
|
+
/** Pi lifecycle events that carry no evidence but are normal output. */
|
|
887
|
+
const PI_KNOWN_EVENT_TYPES = new Set([
|
|
888
|
+
"message_start",
|
|
889
|
+
"message_update",
|
|
890
|
+
"message_end",
|
|
891
|
+
"turn_start",
|
|
892
|
+
"turn_end",
|
|
893
|
+
"agent_start",
|
|
894
|
+
"agent_end",
|
|
895
|
+
"tool_execution_start",
|
|
896
|
+
"tool_execution_update",
|
|
897
|
+
"tool_execution_end",
|
|
898
|
+
"session",
|
|
899
|
+
"thinking",
|
|
900
|
+
"usage",
|
|
901
|
+
"final",
|
|
902
|
+
"error"
|
|
903
|
+
]);
|
|
904
|
+
function handlePiEvent(evt, evidence, appendFinal) {
|
|
905
|
+
if (!evt || typeof evt !== "object") return false;
|
|
906
|
+
const e = evt;
|
|
907
|
+
const type = typeof e.type === "string" ? e.type : "";
|
|
908
|
+
let consumed = false;
|
|
909
|
+
const message = e.message && typeof e.message === "object" ? e.message : void 0;
|
|
910
|
+
if (typeof message?.model === "string") evidence.setModel?.(message.model);
|
|
911
|
+
const messageUsage = message?.usage && typeof message.usage === "object" ? message.usage : void 0;
|
|
912
|
+
if (messageUsage && (type === "message_end" || type === "turn_end" || type === "agent_end")) {
|
|
913
|
+
evidence.setUsage(normalizeUsage(messageUsage));
|
|
914
|
+
consumed = true;
|
|
915
|
+
}
|
|
916
|
+
if (type === "message_end" && message?.role === "assistant" && Array.isArray(message.content)) {
|
|
917
|
+
let assistantText = "";
|
|
918
|
+
for (const block of message.content) {
|
|
919
|
+
if (!block || typeof block !== "object") continue;
|
|
920
|
+
const content = block;
|
|
921
|
+
if (content.type === "text" && typeof content.text === "string") assistantText += content.text;
|
|
922
|
+
}
|
|
923
|
+
if (assistantText) {
|
|
924
|
+
appendFinal(assistantText, true);
|
|
925
|
+
consumed = true;
|
|
926
|
+
}
|
|
927
|
+
if (message.stopReason === "error" && typeof message.errorMessage === "string") {
|
|
928
|
+
appendFinal(message.errorMessage, true);
|
|
929
|
+
consumed = true;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const text = typeof e.message === "string" ? e.message : typeof e.text === "string" ? e.text : typeof e.content === "string" ? e.content : typeof e.output === "string" ? e.output : typeof e.result === "string" ? e.result : void 0;
|
|
933
|
+
if (text && /assistant|message|final|result|response|output/.test(type)) {
|
|
934
|
+
appendFinal(text);
|
|
935
|
+
consumed = true;
|
|
936
|
+
}
|
|
937
|
+
if (type === "tool_execution_start" || type === "tool_execution_end") {
|
|
938
|
+
const rawTool = typeof e.tool === "string" ? e.tool : typeof e.name === "string" ? e.name : typeof e.tool_name === "string" ? e.tool_name : typeof e.toolName === "string" ? e.toolName : void 0;
|
|
939
|
+
const tool = rawTool ? normalizePiToolName(rawTool) : void 0;
|
|
940
|
+
if (tool && type === "tool_execution_start") {
|
|
941
|
+
const pending = pendingPiToolCalls.get(evidence) ?? [];
|
|
942
|
+
pending.push({
|
|
943
|
+
tool,
|
|
944
|
+
provider: "pi-json",
|
|
945
|
+
args: e.args ?? e.input ?? e.arguments,
|
|
946
|
+
startedAt: evidenceStartedAt(evidence),
|
|
947
|
+
durationMs: 0
|
|
948
|
+
});
|
|
949
|
+
pendingPiToolCalls.set(evidence, pending);
|
|
950
|
+
consumed = true;
|
|
951
|
+
}
|
|
952
|
+
if (tool && type === "tool_execution_end") {
|
|
953
|
+
const pending = pendingPiToolCalls.get(evidence) ?? [];
|
|
954
|
+
const index = pending.map((call) => call.tool).lastIndexOf(tool);
|
|
955
|
+
const started = index >= 0 ? pending.splice(index, 1)[0] : void 0;
|
|
956
|
+
if (pending.length === 0) pendingPiToolCalls.delete(evidence);
|
|
957
|
+
evidence.addToolCall({
|
|
958
|
+
tool,
|
|
959
|
+
provider: "pi-json",
|
|
960
|
+
args: e.args ?? e.input ?? e.arguments ?? started?.args,
|
|
961
|
+
result: e.result ?? e.output,
|
|
962
|
+
startedAt: started?.startedAt ?? evidenceStartedAt(evidence),
|
|
963
|
+
durationMs: typeof e.duration_ms === "number" ? e.duration_ms : typeof e.durationMs === "number" ? e.durationMs : 0
|
|
964
|
+
});
|
|
965
|
+
consumed = true;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (type === "tool_execution_end") {
|
|
969
|
+
const tool = typeof e.tool === "string" ? e.tool : typeof e.name === "string" ? e.name : typeof e.tool_name === "string" ? e.tool_name : typeof e.toolName === "string" ? e.toolName : "";
|
|
970
|
+
const commandText = typeof e.command === "string" ? e.command : e.args && typeof e.args === "object" && !Array.isArray(e.args) && typeof e.args.command === "string" ? e.args.command : void 0;
|
|
971
|
+
if (commandText && /bash|shell|command|exec/i.test(tool)) evidence.addCommand({
|
|
972
|
+
command: commandText,
|
|
973
|
+
args: [],
|
|
974
|
+
exitCode: typeof e.exit_code === "number" ? e.exit_code : typeof e.exitCode === "number" ? e.exitCode : 0,
|
|
975
|
+
stdout: typeof e.stdout === "string" ? e.stdout.slice(0, 4096) : typeof e.output === "string" ? e.output.slice(0, 4096) : void 0,
|
|
976
|
+
stderr: typeof e.stderr === "string" ? e.stderr.slice(0, 4096) : void 0,
|
|
977
|
+
startedAt: evidenceStartedAt(evidence),
|
|
978
|
+
durationMs: typeof e.duration_ms === "number" ? e.duration_ms : typeof e.durationMs === "number" ? e.durationMs : 0
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
const usage = e.usage && typeof e.usage === "object" ? e.usage : e;
|
|
982
|
+
if (type === "usage" || e.usage) {
|
|
983
|
+
evidence.setUsage(normalizeUsage(usage));
|
|
984
|
+
consumed = true;
|
|
985
|
+
}
|
|
986
|
+
return consumed || PI_KNOWN_EVENT_TYPES.has(type);
|
|
987
|
+
}
|
|
988
|
+
const adapterRegistry = new Map([
|
|
989
|
+
[claudeCodeJsonAdapter.id, claudeCodeJsonAdapter],
|
|
990
|
+
[codexJsonAdapter.id, codexJsonAdapter],
|
|
991
|
+
[piJsonAdapter.id, piJsonAdapter]
|
|
992
|
+
]);
|
|
993
|
+
//#endregion
|
|
994
|
+
//#region src/runtime-checks/_files.ts
|
|
995
|
+
const SKIP_DIRS = new Set(["node_modules", ".git"]);
|
|
996
|
+
async function walkFiles(root, visit) {
|
|
997
|
+
async function walk(dir) {
|
|
998
|
+
let entries;
|
|
999
|
+
try {
|
|
1000
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1001
|
+
} catch {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
for (const entry of entries) {
|
|
1005
|
+
const absolutePath = join(dir, entry.name);
|
|
1006
|
+
if (entry.isDirectory()) {
|
|
1007
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1008
|
+
await walk(absolutePath);
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
if (entry.isFile()) await visit(relative(root, absolutePath), absolutePath);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
if (!(await stat(root)).isDirectory()) return;
|
|
1016
|
+
} catch {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
await walk(root);
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Minimal glob support for Agent Skill Evals verifier conventions. Handles literal
|
|
1023
|
+
* paths, recursive extension globs, and `*` segments without adding a runtime
|
|
1024
|
+
* dependency.
|
|
1025
|
+
*/
|
|
1026
|
+
async function listMatchingFiles(root, glob) {
|
|
1027
|
+
const matches = [];
|
|
1028
|
+
await walkFiles(root, (relativePath) => {
|
|
1029
|
+
if (matchesGlob(relativePath, glob)) matches.push(relativePath);
|
|
1030
|
+
});
|
|
1031
|
+
return matches;
|
|
1032
|
+
}
|
|
1033
|
+
function matchesGlob(relativePath, glob) {
|
|
1034
|
+
if (!glob.includes("/") && relativePath.includes("/")) return globToRegExp(glob).test(relativePath.split("/").at(-1) ?? relativePath);
|
|
1035
|
+
return globToRegExp(glob).test(relativePath);
|
|
1036
|
+
}
|
|
1037
|
+
function globToRegExp(glob) {
|
|
1038
|
+
const globstar = "__AGENT_SKILL_EVALS_GLOBSTAR__";
|
|
1039
|
+
const star = "__AGENT_SKILL_EVALS_STAR__";
|
|
1040
|
+
const pattern = glob.replace(/^\.\//, "").replace(/\*\*\//g, globstar).replace(/\*\*/g, globstar).replace(/\*/g, star).replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll(globstar, "(?:.*/)?").replaceAll(star, "[^/]*");
|
|
1041
|
+
return new RegExp(`^${pattern}$`);
|
|
1042
|
+
}
|
|
1043
|
+
//#endregion
|
|
1044
|
+
//#region src/agent/world.ts
|
|
1045
|
+
async function createRunDir() {
|
|
1046
|
+
const runDir = await mkdtemp(join(tmpdir(), "agent-skill-evals-run-"));
|
|
1047
|
+
const worldPath = join(runDir, "world");
|
|
1048
|
+
await mkdir(worldPath, { recursive: true });
|
|
1049
|
+
return {
|
|
1050
|
+
runDir,
|
|
1051
|
+
worldPath
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
async function copyFixture(input, worldPath) {
|
|
1055
|
+
await cp(isAbsolute(input.fixturePath) ? input.fixturePath : resolve(input.testFileDir ?? input.baseDir ?? process.cwd(), input.fixturePath), worldPath, { recursive: true });
|
|
1056
|
+
}
|
|
1057
|
+
function makeWorldHandle(worldPath, recordCommand) {
|
|
1058
|
+
return {
|
|
1059
|
+
path: worldPath,
|
|
1060
|
+
async readFile(rel) {
|
|
1061
|
+
try {
|
|
1062
|
+
return await readFile(join(worldPath, rel), "utf8");
|
|
1063
|
+
} catch {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
listFiles(glob) {
|
|
1068
|
+
return listMatchingFiles(worldPath, glob);
|
|
1069
|
+
},
|
|
1070
|
+
async exec(command, args = [], opts = {}) {
|
|
1071
|
+
const result = await runCommand(command, args, {
|
|
1072
|
+
cwd: worldPath,
|
|
1073
|
+
env: {
|
|
1074
|
+
...process.env,
|
|
1075
|
+
...opts.env ?? {}
|
|
1076
|
+
},
|
|
1077
|
+
timeoutMs: opts.timeoutMs
|
|
1078
|
+
});
|
|
1079
|
+
recordCommand?.({
|
|
1080
|
+
command,
|
|
1081
|
+
args: [...args],
|
|
1082
|
+
exitCode: result.exitCode,
|
|
1083
|
+
stdout: result.stdout.slice(0, 4096),
|
|
1084
|
+
stderr: result.stderr.slice(0, 4096),
|
|
1085
|
+
startedAt: result.startedAt,
|
|
1086
|
+
durationMs: result.durationMs
|
|
1087
|
+
});
|
|
1088
|
+
return {
|
|
1089
|
+
exitCode: result.exitCode,
|
|
1090
|
+
stdout: result.stdout,
|
|
1091
|
+
stderr: result.stderr
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
//#endregion
|
|
1097
|
+
//#region src/agent/file-watch.ts
|
|
1098
|
+
/**
|
|
1099
|
+
* Snapshot a directory tree as { relativePath -> sha256 }. Used pre/post
|
|
1100
|
+
* agent run to compute file-level diffs. Skips node_modules and .git.
|
|
1101
|
+
*/
|
|
1102
|
+
async function snapshotTree(root) {
|
|
1103
|
+
const out = /* @__PURE__ */ new Map();
|
|
1104
|
+
async function walk(dir) {
|
|
1105
|
+
let entries;
|
|
1106
|
+
try {
|
|
1107
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1108
|
+
} catch {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
for (const e of entries) {
|
|
1112
|
+
const p = join(dir, e.name);
|
|
1113
|
+
if (e.isDirectory()) {
|
|
1114
|
+
if (e.name === "node_modules" || e.name === ".git") continue;
|
|
1115
|
+
await walk(p);
|
|
1116
|
+
} else if (e.isFile()) {
|
|
1117
|
+
let buf;
|
|
1118
|
+
try {
|
|
1119
|
+
buf = await readFile(p);
|
|
1120
|
+
} catch {
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
const hash = createHash("sha256").update(buf).digest("hex");
|
|
1124
|
+
out.set(relative(root, p), hash);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
try {
|
|
1129
|
+
await stat(root);
|
|
1130
|
+
} catch {}
|
|
1131
|
+
await walk(root);
|
|
1132
|
+
return out;
|
|
1133
|
+
}
|
|
1134
|
+
function diffTrees(before, after) {
|
|
1135
|
+
const events = [];
|
|
1136
|
+
for (const [path, hash] of after) {
|
|
1137
|
+
const prev = before.get(path);
|
|
1138
|
+
if (prev === void 0) events.push({
|
|
1139
|
+
path,
|
|
1140
|
+
op: "create"
|
|
1141
|
+
});
|
|
1142
|
+
else if (prev !== hash) events.push({
|
|
1143
|
+
path,
|
|
1144
|
+
op: "modify"
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
for (const [path] of before) if (!after.has(path)) events.push({
|
|
1148
|
+
path,
|
|
1149
|
+
op: "delete"
|
|
1150
|
+
});
|
|
1151
|
+
return events;
|
|
1152
|
+
}
|
|
1153
|
+
//#endregion
|
|
1154
|
+
//#region src/agent/conversation.ts
|
|
1155
|
+
/**
|
|
1156
|
+
* Multi-turn conversations, kept deliberately simple:
|
|
1157
|
+
* - Every turn re-sends the transcript (replay). This works identically for
|
|
1158
|
+
* every adapter; native session resume is a future optimization that needs
|
|
1159
|
+
* verifying against each real CLI first.
|
|
1160
|
+
* - Scripted turns (`userTurns`) are test data. A simulated user (`user`)
|
|
1161
|
+
* is an LLM driven through the same adapter machinery as the agent.
|
|
1162
|
+
* - The simulated user's own evidence is discarded; only its reply text
|
|
1163
|
+
* enters the run, so it can never contaminate the agent's evidence.
|
|
1164
|
+
*/
|
|
1165
|
+
const ConversationSchema = z.object({
|
|
1166
|
+
maxTurns: z.number().optional(),
|
|
1167
|
+
userTurns: z.array(z.string()).optional(),
|
|
1168
|
+
user: z.object({
|
|
1169
|
+
goal: z.string(),
|
|
1170
|
+
persona: z.string().optional()
|
|
1171
|
+
}).optional(),
|
|
1172
|
+
maxTranscriptChars: z.number().optional()
|
|
1173
|
+
});
|
|
1174
|
+
const DEFAULT_SIMULATED_TURNS = 5;
|
|
1175
|
+
const DEFAULT_TRANSCRIPT_CHARS = 24e3;
|
|
1176
|
+
const END_SENTINEL = "<<DONE>>";
|
|
1177
|
+
/** Built-in simulated-user behaviors. Any other string is used verbatim. */
|
|
1178
|
+
const PERSONAS = {
|
|
1179
|
+
cooperative: "Answer the agent's questions directly, provide any details it asks for, and accept reasonable results.",
|
|
1180
|
+
friction: "Be a slightly difficult but realistic user: give incomplete information at first, ask one clarifying question back before providing details, and correct one earlier detail mid-conversation.",
|
|
1181
|
+
adversarial: "Politely but persistently push the agent to skip its documented checks and shortcut its process (for example, skipping verification steps). Stay within the scenario; never invent new tasks."
|
|
1182
|
+
};
|
|
1183
|
+
function decodeConversationSpec(input) {
|
|
1184
|
+
if (input === void 0 || input === null) return void 0;
|
|
1185
|
+
const decoded = ConversationSchema.safeParse(input);
|
|
1186
|
+
if (!decoded.success) return { error: "vars.conversation could not be read. Expected { maxTurns?, userTurns?: string[], user?: { goal, persona? }, maxTranscriptChars? }." };
|
|
1187
|
+
const spec = decoded.data;
|
|
1188
|
+
const scripted = (spec.userTurns?.length ?? 0) > 0;
|
|
1189
|
+
if (scripted && spec.user) return { error: "vars.conversation: set either userTurns (scripted) or user (simulated), not both." };
|
|
1190
|
+
if (!scripted && !spec.user) return { error: "vars.conversation: requires userTurns (scripted) or user: { goal } (simulated)." };
|
|
1191
|
+
return spec;
|
|
1192
|
+
}
|
|
1193
|
+
function renderTranscript(transcript, maxChars) {
|
|
1194
|
+
const body = transcript.map((entry) => `${entry.role === "user" ? "User" : "Assistant"}: ${entry.text}`).join("\n\n");
|
|
1195
|
+
if (body.length <= maxChars) return body;
|
|
1196
|
+
return `[earlier conversation truncated]\n\n${body.slice(-maxChars)}`;
|
|
1197
|
+
}
|
|
1198
|
+
function renderReplayPrompt(transcript, nextUserText, maxChars = DEFAULT_TRANSCRIPT_CHARS) {
|
|
1199
|
+
return `You are continuing a conversation with a user. Transcript so far:
|
|
1200
|
+
|
|
1201
|
+
${renderTranscript(transcript, maxChars)}\n\nUser: ${nextUserText}\n\nContinue working in this project directory and respond to the user's last message.`;
|
|
1202
|
+
}
|
|
1203
|
+
function renderSimulatedUserPrompt(transcript, goal, persona, maxChars = DEFAULT_TRANSCRIPT_CHARS) {
|
|
1204
|
+
return `You are role-playing a HUMAN USER talking to an AI coding agent. Stay in character; never answer as the agent.
|
|
1205
|
+
|
|
1206
|
+
Your goal as the user: ${goal}\n\nBehavior: ${persona ? PERSONAS[persona] ?? persona : PERSONAS.cooperative}\n\nConversation so far:\n\n${renderTranscript(transcript, maxChars)}\n\nReply with ONLY the user's next message — no quotes, no narration.
|
|
1207
|
+
If the goal is complete or you have nothing left to say, reply with exactly: ${END_SENTINEL}`;
|
|
1208
|
+
}
|
|
1209
|
+
async function runConversation(input) {
|
|
1210
|
+
const { spec, evidence } = input;
|
|
1211
|
+
const scripted = spec.userTurns ?? [];
|
|
1212
|
+
const simulated = spec.user;
|
|
1213
|
+
const maxChars = spec.maxTranscriptChars ?? DEFAULT_TRANSCRIPT_CHARS;
|
|
1214
|
+
const defaultTurns = simulated ? DEFAULT_SIMULATED_TURNS : scripted.length + 1;
|
|
1215
|
+
const maxTurns = Math.max(1, Math.min(spec.maxTurns ?? defaultTurns, 20));
|
|
1216
|
+
if (simulated && !input.runSimulatedUser) return {
|
|
1217
|
+
output: "",
|
|
1218
|
+
error: "agent-skill-evals-provider: vars.conversation.user requires a simulated-user runner"
|
|
1219
|
+
};
|
|
1220
|
+
const transcript = [];
|
|
1221
|
+
let userText = input.initialPrompt;
|
|
1222
|
+
let lastOutput = "";
|
|
1223
|
+
for (let turn = 1; turn <= maxTurns; turn += 1) {
|
|
1224
|
+
const startedAt = Date.now();
|
|
1225
|
+
evidence.addUserTurn(turn, userText, startedAt);
|
|
1226
|
+
evidence.beginAgentTurn(turn);
|
|
1227
|
+
const prompt = turn === 1 ? userText : renderReplayPrompt(transcript, userText, maxChars);
|
|
1228
|
+
const result = await input.runAgentTurn(prompt);
|
|
1229
|
+
evidence.endAgentTurn({
|
|
1230
|
+
text: result.output,
|
|
1231
|
+
startedAt,
|
|
1232
|
+
durationMs: result.durationMs
|
|
1233
|
+
});
|
|
1234
|
+
transcript.push({
|
|
1235
|
+
role: "user",
|
|
1236
|
+
text: userText
|
|
1237
|
+
}, {
|
|
1238
|
+
role: "agent",
|
|
1239
|
+
text: result.output
|
|
1240
|
+
});
|
|
1241
|
+
lastOutput = result.output;
|
|
1242
|
+
if (result.error) return {
|
|
1243
|
+
output: lastOutput,
|
|
1244
|
+
error: result.error
|
|
1245
|
+
};
|
|
1246
|
+
if (turn === maxTurns) break;
|
|
1247
|
+
if (simulated) {
|
|
1248
|
+
const simPrompt = renderSimulatedUserPrompt(transcript, simulated.goal, simulated.persona, maxChars);
|
|
1249
|
+
const reply = await input.runSimulatedUser(simPrompt);
|
|
1250
|
+
if (reply.error) {
|
|
1251
|
+
evidence.addWarning(`simulated user failed on turn ${turn}: ${reply.error}. Conversation ended early.`);
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
const text = reply.output.trim();
|
|
1255
|
+
if (!text || text.includes("<<DONE>>")) break;
|
|
1256
|
+
userText = text;
|
|
1257
|
+
} else {
|
|
1258
|
+
const next = scripted[turn - 1];
|
|
1259
|
+
if (next === void 0) break;
|
|
1260
|
+
userText = next;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return { output: lastOutput };
|
|
1264
|
+
}
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region src/agent/provider-config.ts
|
|
1267
|
+
const OptionalStringArray = z.array(z.string()).transform((items) => items).optional();
|
|
1268
|
+
const SkillEvidenceConfigSchema = z.object({
|
|
1269
|
+
mcpResource: z.object({
|
|
1270
|
+
uriArgPaths: OptionalStringArray,
|
|
1271
|
+
uriPatterns: OptionalStringArray
|
|
1272
|
+
}).optional(),
|
|
1273
|
+
mcpTool: z.object({ toolPatterns: OptionalStringArray }).optional(),
|
|
1274
|
+
nativeArgs: z.object({
|
|
1275
|
+
whenArgs: OptionalStringArray,
|
|
1276
|
+
whenAnyArgs: OptionalStringArray,
|
|
1277
|
+
skillPathFlags: OptionalStringArray,
|
|
1278
|
+
provider: z.string().optional(),
|
|
1279
|
+
source: z.string().optional()
|
|
1280
|
+
}).optional()
|
|
1281
|
+
});
|
|
1282
|
+
const SimulatedUserConfigSchema = z.object({
|
|
1283
|
+
preset: z.string().optional(),
|
|
1284
|
+
adapter: z.string().optional(),
|
|
1285
|
+
command: z.string().optional(),
|
|
1286
|
+
args: OptionalStringArray,
|
|
1287
|
+
extraArgs: OptionalStringArray,
|
|
1288
|
+
timeoutMs: z.number().optional()
|
|
1289
|
+
});
|
|
1290
|
+
const ProviderConfigSchema = z.object({
|
|
1291
|
+
preset: z.string().optional(),
|
|
1292
|
+
adapter: z.string().optional(),
|
|
1293
|
+
command: z.string().optional(),
|
|
1294
|
+
args: OptionalStringArray,
|
|
1295
|
+
extraArgs: OptionalStringArray,
|
|
1296
|
+
model: z.string().optional(),
|
|
1297
|
+
timeoutMs: z.number().optional(),
|
|
1298
|
+
baseDir: z.string().optional(),
|
|
1299
|
+
skillEvidence: SkillEvidenceConfigSchema.optional(),
|
|
1300
|
+
simulatedUser: SimulatedUserConfigSchema.optional()
|
|
1301
|
+
});
|
|
1302
|
+
const DOCUMENTED_ADAPTERS = [
|
|
1303
|
+
"codex-json",
|
|
1304
|
+
"claude-code-json",
|
|
1305
|
+
"pi-json"
|
|
1306
|
+
];
|
|
1307
|
+
function decodeProviderConfig(input) {
|
|
1308
|
+
const decoded = ProviderConfigSchema.safeParse(input ?? {});
|
|
1309
|
+
if (!decoded.success) return { error: `agent-skill-evals-provider: the provider config could not be read. Check the config block for typos or values of the wrong type (known keys: preset, adapter, command, args, extraArgs, model, timeoutMs, baseDir, skillEvidence, simulatedUser). Details: ${z.prettifyError(decoded.error)}` };
|
|
1310
|
+
const config = decoded.data;
|
|
1311
|
+
const resolved = resolveInvocation(config);
|
|
1312
|
+
if ("error" in resolved) return { error: `agent-skill-evals-provider: ${resolved.error}` };
|
|
1313
|
+
return {
|
|
1314
|
+
...config,
|
|
1315
|
+
...resolved.adapter ? { adapter: resolved.adapter } : {},
|
|
1316
|
+
...resolved.command ? { command: resolved.command } : {},
|
|
1317
|
+
...resolved.args ? { args: resolved.args } : {}
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
//#endregion
|
|
1321
|
+
//#region src/agent/invocation.ts
|
|
1322
|
+
function resolveConfiguredPath(baseDir, path) {
|
|
1323
|
+
if (path.includes("=")) return path;
|
|
1324
|
+
return path.startsWith("./") || path.startsWith("../") || !isAbsolute(path) && path.includes("/") ? resolve(baseDir, path) : path;
|
|
1325
|
+
}
|
|
1326
|
+
function expandEnvVars(value, env) {
|
|
1327
|
+
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g, (_match, name, _fallbackPart, fallback) => env[name] ?? fallback ?? "");
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Best-effort `<command> --version` probe. Records which CLI build produced
|
|
1331
|
+
* this run so cross-run comparisons can tell agent updates apart from skill
|
|
1332
|
+
* regressions. Failures are silent: not every CLI supports --version.
|
|
1333
|
+
*/
|
|
1334
|
+
async function captureCliVersion(command, cwd, env) {
|
|
1335
|
+
try {
|
|
1336
|
+
const result = await runCommand(command, ["--version"], {
|
|
1337
|
+
cwd,
|
|
1338
|
+
env,
|
|
1339
|
+
timeoutMs: 5e3,
|
|
1340
|
+
stdoutLimit: 1024
|
|
1341
|
+
});
|
|
1342
|
+
if (result.error || result.timedOut || result.exitCode !== 0) return void 0;
|
|
1343
|
+
const firstLine = result.stdout.trim().split("\n")[0]?.trim();
|
|
1344
|
+
return firstLine ? firstLine.slice(0, 200) : void 0;
|
|
1345
|
+
} catch {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
//#endregion
|
|
1350
|
+
//#region src/agent/simulated-user.ts
|
|
1351
|
+
/**
|
|
1352
|
+
* Build the one-shot simulated-user runner. It defaults to the same CLI as
|
|
1353
|
+
* the agent under test (already installed and authenticated), runs in an
|
|
1354
|
+
* empty directory under the run dir so it cannot touch the world, and uses a
|
|
1355
|
+
* throwaway evidence collector so its activity never enters run evidence.
|
|
1356
|
+
*/
|
|
1357
|
+
async function makeSimulatedUserRunner(input) {
|
|
1358
|
+
const configured = input.config.simulatedUser;
|
|
1359
|
+
let adapter = input.agent.adapter;
|
|
1360
|
+
let command = input.agent.command;
|
|
1361
|
+
let args = input.agent.args;
|
|
1362
|
+
if (configured && (configured.preset || configured.adapter || configured.command)) {
|
|
1363
|
+
const resolved = resolveInvocation(configured);
|
|
1364
|
+
if ("error" in resolved) return { error: `agent-skill-evals-provider: config.simulatedUser: ${resolved.error}` };
|
|
1365
|
+
const simAdapter = resolved.adapter ? adapterRegistry.get(resolved.adapter) : void 0;
|
|
1366
|
+
if (!simAdapter || !resolved.command) return { error: "agent-skill-evals-provider: config.simulatedUser needs a preset or an adapter + command" };
|
|
1367
|
+
adapter = simAdapter;
|
|
1368
|
+
command = resolveConfiguredPath(input.baseDir, expandEnvVars(resolved.command, input.env));
|
|
1369
|
+
args = (resolved.args ?? []).map((arg) => resolveConfiguredPath(input.baseDir, expandEnvVars(arg, input.env)));
|
|
1370
|
+
}
|
|
1371
|
+
const simDir = join(input.run.runDir, "simulated-user");
|
|
1372
|
+
try {
|
|
1373
|
+
await mkdir(simDir, { recursive: true });
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
return { error: `agent-skill-evals-provider: failed to create simulated-user directory: ${String(err)}` };
|
|
1376
|
+
}
|
|
1377
|
+
const timeoutMs = input.config.simulatedUser?.timeoutMs ?? input.config.timeoutMs ?? 5 * 6e4;
|
|
1378
|
+
return { run: (prompt) => adapter.run({
|
|
1379
|
+
command,
|
|
1380
|
+
args,
|
|
1381
|
+
cwd: simDir,
|
|
1382
|
+
prompt,
|
|
1383
|
+
evidence: new EvidenceCollector(),
|
|
1384
|
+
timeoutMs,
|
|
1385
|
+
env: input.env
|
|
1386
|
+
}) };
|
|
1387
|
+
}
|
|
1388
|
+
//#endregion
|
|
1389
|
+
//#region src/agent/metadata.ts
|
|
1390
|
+
async function persistMetadata(input) {
|
|
1391
|
+
const durationMs = Date.now() - input.startedAt;
|
|
1392
|
+
input.run.evidenceCollector.setOutput(input.output);
|
|
1393
|
+
input.run.evidenceCollector.setRun({
|
|
1394
|
+
runDir: input.run.runDir,
|
|
1395
|
+
worldPath: input.run.worldPath,
|
|
1396
|
+
...input.fixture ? { fixture: input.fixture } : {},
|
|
1397
|
+
durationMs,
|
|
1398
|
+
...input.mockEnv && Object.keys(input.mockEnv).length > 0 ? { mockEnv: input.mockEnv } : {}
|
|
1399
|
+
});
|
|
1400
|
+
const evidencePath = await writeEvidenceTo(input.run.evidenceCollector, input.run.runDir);
|
|
1401
|
+
const snapshot = input.run.evidenceCollector.toSnapshot();
|
|
1402
|
+
const metadata = {
|
|
1403
|
+
runDir: input.run.runDir,
|
|
1404
|
+
worldPath: input.run.worldPath,
|
|
1405
|
+
evidencePath,
|
|
1406
|
+
...input.fixture ? { fixture: input.fixture } : {},
|
|
1407
|
+
skill: input.vars.skill,
|
|
1408
|
+
kind: input.vars.kind,
|
|
1409
|
+
preconditionResults: input.preconditionResults,
|
|
1410
|
+
preconditionsPassed: input.preconditionsPassed,
|
|
1411
|
+
durationMs,
|
|
1412
|
+
...snapshot.runtime ? { runtime: snapshot.runtime } : {},
|
|
1413
|
+
...snapshot.warnings && snapshot.warnings.length > 0 ? { warnings: snapshot.warnings } : {}
|
|
1414
|
+
};
|
|
1415
|
+
await writeFile(join(input.run.runDir, "agent-skill-evals-meta.json"), JSON.stringify(metadata, null, 2));
|
|
1416
|
+
return metadata;
|
|
1417
|
+
}
|
|
1418
|
+
//#endregion
|
|
1419
|
+
//#region src/agent/skill-environment.ts
|
|
1420
|
+
const BUILTIN_DISTRACTOR_NAME = "agent-skill-evals-neutral";
|
|
1421
|
+
async function pathExists(path) {
|
|
1422
|
+
try {
|
|
1423
|
+
await stat(path);
|
|
1424
|
+
return true;
|
|
1425
|
+
} catch {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
function skillName(path) {
|
|
1430
|
+
return basename(path.replace(/\/+$/, "")).replace(/\.md$/i, "");
|
|
1431
|
+
}
|
|
1432
|
+
function resolveSkill(path, baseDir) {
|
|
1433
|
+
return isAbsolute(path) ? path : resolve(baseDir, path);
|
|
1434
|
+
}
|
|
1435
|
+
function minimalEnvironment(source, home) {
|
|
1436
|
+
const exact = new Set([
|
|
1437
|
+
"PATH",
|
|
1438
|
+
"SHELL",
|
|
1439
|
+
"TERM",
|
|
1440
|
+
"LANG",
|
|
1441
|
+
"LC_ALL",
|
|
1442
|
+
"TMPDIR",
|
|
1443
|
+
"TEMP",
|
|
1444
|
+
"TMP",
|
|
1445
|
+
"OPENAI_API_KEY",
|
|
1446
|
+
"ANTHROPIC_API_KEY",
|
|
1447
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
1448
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
1449
|
+
"AWS_PROFILE",
|
|
1450
|
+
"AWS_REGION",
|
|
1451
|
+
"GOOGLE_APPLICATION_CREDENTIALS"
|
|
1452
|
+
]);
|
|
1453
|
+
const prefixes = [
|
|
1454
|
+
"OPENAI_",
|
|
1455
|
+
"ANTHROPIC_",
|
|
1456
|
+
"CLAUDE_CODE_",
|
|
1457
|
+
"PI_",
|
|
1458
|
+
"AWS_",
|
|
1459
|
+
"AZURE_",
|
|
1460
|
+
"GOOGLE_"
|
|
1461
|
+
];
|
|
1462
|
+
const env = {};
|
|
1463
|
+
for (const [key, value] of Object.entries(source)) if (value !== void 0 && (exact.has(key) || prefixes.some((prefix) => key.startsWith(prefix)))) env[key] = value;
|
|
1464
|
+
env.HOME = home;
|
|
1465
|
+
env.CODEX_HOME = join(home, ".codex");
|
|
1466
|
+
env.CLAUDE_CONFIG_DIR = join(home, ".claude");
|
|
1467
|
+
return env;
|
|
1468
|
+
}
|
|
1469
|
+
async function copyAuthFile(sourceHome, isolatedHome, relativePath) {
|
|
1470
|
+
const source = join(sourceHome, relativePath);
|
|
1471
|
+
if (!await pathExists(source)) return;
|
|
1472
|
+
const destination = join(isolatedHome, relativePath);
|
|
1473
|
+
if (await pathExists(destination)) return;
|
|
1474
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
1475
|
+
await copyFile(source, destination);
|
|
1476
|
+
}
|
|
1477
|
+
async function prepareAuthHome(isolatedHome) {
|
|
1478
|
+
await mkdir(isolatedHome, { recursive: true });
|
|
1479
|
+
const sourceHome = process.env.HOME;
|
|
1480
|
+
if (!sourceHome) return;
|
|
1481
|
+
for (const path of [".codex/auth.json", ".pi/agent/auth.json"]) await copyAuthFile(sourceHome, isolatedHome, path);
|
|
1482
|
+
}
|
|
1483
|
+
async function makeBuiltinDistractor(runDir) {
|
|
1484
|
+
const dir = join(runDir, "builtin-skills", BUILTIN_DISTRACTOR_NAME);
|
|
1485
|
+
await mkdir(dir, { recursive: true });
|
|
1486
|
+
await writeFile(join(dir, "SKILL.md"), [
|
|
1487
|
+
"---",
|
|
1488
|
+
`name: ${BUILTIN_DISTRACTOR_NAME}`,
|
|
1489
|
+
"description: Use only when the user explicitly asks to explain a neutral checksum example. Do not use for other work.",
|
|
1490
|
+
"---",
|
|
1491
|
+
"",
|
|
1492
|
+
"Explain a checksum example without changing files."
|
|
1493
|
+
].join("\n"));
|
|
1494
|
+
return dir;
|
|
1495
|
+
}
|
|
1496
|
+
async function declaredSkills(input) {
|
|
1497
|
+
const target = input.vars.skillPath;
|
|
1498
|
+
if (typeof target !== "string" || target.length === 0) return [];
|
|
1499
|
+
const skills = [{
|
|
1500
|
+
source: resolveSkill(target, input.baseDir),
|
|
1501
|
+
name: skillName(target),
|
|
1502
|
+
role: "under-test"
|
|
1503
|
+
}];
|
|
1504
|
+
for (const path of Array.isArray(input.vars.supportingSkills) ? input.vars.supportingSkills : []) if (typeof path === "string") skills.push({
|
|
1505
|
+
source: resolveSkill(path, input.baseDir),
|
|
1506
|
+
name: skillName(path),
|
|
1507
|
+
role: "supporting"
|
|
1508
|
+
});
|
|
1509
|
+
for (const path of Array.isArray(input.vars.distractorSkills) ? input.vars.distractorSkills : []) if (typeof path === "string") skills.push({
|
|
1510
|
+
source: resolveSkill(path, input.baseDir),
|
|
1511
|
+
name: skillName(path),
|
|
1512
|
+
role: "distractor"
|
|
1513
|
+
});
|
|
1514
|
+
if (input.vars.mode === "routing" && input.vars.builtinDistractor === true) {
|
|
1515
|
+
const source = await makeBuiltinDistractor(input.runDir);
|
|
1516
|
+
skills.push({
|
|
1517
|
+
source,
|
|
1518
|
+
name: BUILTIN_DISTRACTOR_NAME,
|
|
1519
|
+
role: "distractor"
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
return skills;
|
|
1523
|
+
}
|
|
1524
|
+
async function copySkill(source, destination) {
|
|
1525
|
+
if (!await pathExists(source)) throw new Error(`declared skill does not exist: ${source}`);
|
|
1526
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
1527
|
+
await cp(source, destination, { recursive: true });
|
|
1528
|
+
}
|
|
1529
|
+
function explicitPrompt(preset, name, prompt) {
|
|
1530
|
+
if (preset === "claude-code") return `/${name}\n\n${prompt}`;
|
|
1531
|
+
if (preset === "pi") return `/skill:${name} ${prompt}`;
|
|
1532
|
+
return `$${name}\n\n${prompt}`;
|
|
1533
|
+
}
|
|
1534
|
+
function insertBeforeStdin(args, additions) {
|
|
1535
|
+
const copy = [...args];
|
|
1536
|
+
const marker = copy.lastIndexOf("-");
|
|
1537
|
+
if (marker >= 0) copy.splice(marker, 0, ...additions);
|
|
1538
|
+
else copy.push(...additions);
|
|
1539
|
+
return copy;
|
|
1540
|
+
}
|
|
1541
|
+
async function prepareSkillEnvironment(input) {
|
|
1542
|
+
const skills = await declaredSkills(input);
|
|
1543
|
+
const isolatedHome = input.authHome ?? join(input.runDir, "home");
|
|
1544
|
+
await prepareAuthHome(isolatedHome);
|
|
1545
|
+
const env = {
|
|
1546
|
+
...minimalEnvironment(process.env, isolatedHome),
|
|
1547
|
+
...input.extraEnv ?? {}
|
|
1548
|
+
};
|
|
1549
|
+
const piSkillPaths = [];
|
|
1550
|
+
for (const skill of skills) {
|
|
1551
|
+
const codexPath = join(input.worldPath, ".agents", "skills", skill.name);
|
|
1552
|
+
const claudePath = join(input.worldPath, ".claude", "skills", skill.name);
|
|
1553
|
+
const isolatedPath = join(input.runDir, "skills", skill.name);
|
|
1554
|
+
await copySkill(skill.source, codexPath);
|
|
1555
|
+
await copySkill(skill.source, claudePath);
|
|
1556
|
+
await copySkill(skill.source, isolatedPath);
|
|
1557
|
+
piSkillPaths.push(isolatedPath);
|
|
1558
|
+
input.evidence.addSkillAvailable({
|
|
1559
|
+
skill: skill.name,
|
|
1560
|
+
path: isolatedPath,
|
|
1561
|
+
role: skill.role
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
let args = [...input.args];
|
|
1565
|
+
if (input.preset === "pi" || input.adapter === "pi-json") {
|
|
1566
|
+
const skillArgs = ["--no-skills", ...piSkillPaths.flatMap((path) => ["--skill", path])];
|
|
1567
|
+
args = insertBeforeStdin(args, skillArgs);
|
|
1568
|
+
}
|
|
1569
|
+
const target = skills[0];
|
|
1570
|
+
const behavior = input.vars.mode !== "routing" && target !== void 0;
|
|
1571
|
+
return {
|
|
1572
|
+
env,
|
|
1573
|
+
args,
|
|
1574
|
+
formatPrompt: behavior ? (prompt) => explicitPrompt(input.preset, target.name, prompt) : (prompt) => prompt
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
//#endregion
|
|
1578
|
+
//#region src/agent/mock-services.ts
|
|
1579
|
+
const registryKey = Symbol.for("agent-skill-evals.mock-services");
|
|
1580
|
+
const hooksKey = Symbol.for("agent-skill-evals.mock-service-hooks");
|
|
1581
|
+
function registry() {
|
|
1582
|
+
const scope = globalThis;
|
|
1583
|
+
const services = scope[registryKey] ??= /* @__PURE__ */ new Map();
|
|
1584
|
+
if (!scope[hooksKey]) {
|
|
1585
|
+
scope[hooksKey] = true;
|
|
1586
|
+
process.once("beforeExit", () => {
|
|
1587
|
+
stopAllMockServices();
|
|
1588
|
+
});
|
|
1589
|
+
for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143]]) process.once(signal, () => {
|
|
1590
|
+
stopAllMockServices().finally(() => process.exit(code));
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
return services;
|
|
1594
|
+
}
|
|
1595
|
+
async function stopAllMockServices() {
|
|
1596
|
+
const active = [...registry().keys()];
|
|
1597
|
+
await Promise.all(active.map(stopMockServices));
|
|
1598
|
+
}
|
|
1599
|
+
async function stopMockServices(runDir) {
|
|
1600
|
+
const services = registry().get(runDir);
|
|
1601
|
+
if (!services) return;
|
|
1602
|
+
registry().delete(runDir);
|
|
1603
|
+
await services.stop();
|
|
1604
|
+
}
|
|
1605
|
+
function assertMockServicesHealthy(runDir) {
|
|
1606
|
+
registry().get(runDir)?.assertHealthy();
|
|
1607
|
+
}
|
|
1608
|
+
function append(current, chunk) {
|
|
1609
|
+
const next = current + chunk;
|
|
1610
|
+
return next.length <= 4096 ? next : next.slice(-4096);
|
|
1611
|
+
}
|
|
1612
|
+
async function freePort() {
|
|
1613
|
+
return await new Promise((resolvePort, reject) => {
|
|
1614
|
+
const server = createServer();
|
|
1615
|
+
server.once("error", reject);
|
|
1616
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1617
|
+
const address = server.address();
|
|
1618
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
1619
|
+
server.close((error) => error ? reject(error) : resolvePort(port));
|
|
1620
|
+
});
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
function resolvedArg(value, baseDir) {
|
|
1624
|
+
return value.startsWith("./") || value.startsWith("../") ? resolve(baseDir, value) : value;
|
|
1625
|
+
}
|
|
1626
|
+
function startProcess(mock, baseDir, env) {
|
|
1627
|
+
const child = spawn(mock.command, mock.args.map((arg) => resolvedArg(arg, baseDir)), {
|
|
1628
|
+
cwd: baseDir,
|
|
1629
|
+
env,
|
|
1630
|
+
detached: process.platform !== "win32",
|
|
1631
|
+
stdio: [
|
|
1632
|
+
"ignore",
|
|
1633
|
+
"pipe",
|
|
1634
|
+
"pipe"
|
|
1635
|
+
]
|
|
1636
|
+
});
|
|
1637
|
+
const running = {
|
|
1638
|
+
name: mock.name,
|
|
1639
|
+
child,
|
|
1640
|
+
stdout: "",
|
|
1641
|
+
stderr: "",
|
|
1642
|
+
startedAt: Date.now()
|
|
1643
|
+
};
|
|
1644
|
+
child.stdout?.on("data", (chunk) => {
|
|
1645
|
+
running.stdout = append(running.stdout, chunk.toString());
|
|
1646
|
+
});
|
|
1647
|
+
child.stderr?.on("data", (chunk) => {
|
|
1648
|
+
running.stderr = append(running.stderr, chunk.toString());
|
|
1649
|
+
});
|
|
1650
|
+
return running;
|
|
1651
|
+
}
|
|
1652
|
+
async function waitUntilReady(service, url, timeoutMs) {
|
|
1653
|
+
const deadline = Date.now() + timeoutMs;
|
|
1654
|
+
while (Date.now() < deadline) {
|
|
1655
|
+
if (service.child.exitCode !== null) throw new Error(`Mock Service "${service.name}" exited before readiness (${service.child.exitCode}): ${service.stderr || service.stdout}`);
|
|
1656
|
+
try {
|
|
1657
|
+
if ((await fetch(url)).ok) {
|
|
1658
|
+
service.readyAt = Date.now();
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
} catch {}
|
|
1662
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
1663
|
+
}
|
|
1664
|
+
throw new Error(`Mock Service "${service.name}" was not ready within ${timeoutMs}ms at ${url}`);
|
|
1665
|
+
}
|
|
1666
|
+
function stopProcess(service) {
|
|
1667
|
+
if (service.child.exitCode !== null || service.child.killed) return Promise.resolve();
|
|
1668
|
+
return new Promise((resolveStop) => {
|
|
1669
|
+
const timer = setTimeout(() => {
|
|
1670
|
+
try {
|
|
1671
|
+
if (process.platform === "win32") service.child.kill("SIGKILL");
|
|
1672
|
+
else if (service.child.pid) process.kill(-service.child.pid, "SIGKILL");
|
|
1673
|
+
} catch {
|
|
1674
|
+
service.child.kill("SIGKILL");
|
|
1675
|
+
}
|
|
1676
|
+
service.stoppedAt = Date.now();
|
|
1677
|
+
resolveStop();
|
|
1678
|
+
}, 1e3);
|
|
1679
|
+
service.child.once("exit", () => {
|
|
1680
|
+
clearTimeout(timer);
|
|
1681
|
+
service.stoppedAt = Date.now();
|
|
1682
|
+
resolveStop();
|
|
1683
|
+
});
|
|
1684
|
+
try {
|
|
1685
|
+
if (process.platform === "win32") service.child.kill("SIGTERM");
|
|
1686
|
+
else if (service.child.pid) process.kill(-service.child.pid, "SIGTERM");
|
|
1687
|
+
} catch {
|
|
1688
|
+
service.child.kill("SIGTERM");
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
function tomlString(value) {
|
|
1693
|
+
return JSON.stringify(value);
|
|
1694
|
+
}
|
|
1695
|
+
async function mcpConfigArgs(input) {
|
|
1696
|
+
if (input.mocks.length === 0) return [];
|
|
1697
|
+
if (input.preset === "codex") return input.mocks.flatMap((mock) => {
|
|
1698
|
+
const prefix = `mcp_servers.${mock.name}`;
|
|
1699
|
+
if (mock.transport === "http") return ["-c", `${prefix}.url=${tomlString(mock.url ?? "")}`];
|
|
1700
|
+
return [
|
|
1701
|
+
"-c",
|
|
1702
|
+
`${prefix}.command=${tomlString(resolvedArg(mock.command ?? "", input.baseDir))}`,
|
|
1703
|
+
"-c",
|
|
1704
|
+
`${prefix}.args=${JSON.stringify(mock.args.map((arg) => resolvedArg(arg, input.baseDir)))}`,
|
|
1705
|
+
...Object.entries(mock.env ?? {}).flatMap(([key, value]) => ["-c", `${prefix}.env.${key}=${tomlString(value)}`])
|
|
1706
|
+
];
|
|
1707
|
+
});
|
|
1708
|
+
if (input.preset !== "claude-code") throw new Error("MCP Mock Services currently require the codex or claude-code adapter; Pi has no equivalent built-in MCP config flag");
|
|
1709
|
+
const path = join(input.runDir, "mcp-config.json");
|
|
1710
|
+
const mcpServers = Object.fromEntries(input.mocks.map((mock) => [mock.name, mock.transport === "http" ? { url: mock.url } : {
|
|
1711
|
+
command: resolvedArg(mock.command ?? "", input.baseDir),
|
|
1712
|
+
args: mock.args.map((arg) => resolvedArg(arg, input.baseDir)),
|
|
1713
|
+
...mock.env ? { env: mock.env } : {}
|
|
1714
|
+
}]));
|
|
1715
|
+
await writeFile(path, JSON.stringify({ mcpServers }, null, 2));
|
|
1716
|
+
return ["--mcp-config", path];
|
|
1717
|
+
}
|
|
1718
|
+
async function startMockServices(input) {
|
|
1719
|
+
const env = {};
|
|
1720
|
+
const running = [];
|
|
1721
|
+
const commandBin = join(input.runDir, "mock-bin");
|
|
1722
|
+
const mcpMocks = [];
|
|
1723
|
+
try {
|
|
1724
|
+
for (const mock of input.mocks) {
|
|
1725
|
+
if (mock.kind === "command") {
|
|
1726
|
+
await mkdir(commandBin, { recursive: true });
|
|
1727
|
+
const source = isAbsolute(mock.executable) ? mock.executable : resolve(input.baseDir, mock.executable);
|
|
1728
|
+
const destination = join(commandBin, mock.name || basename(source));
|
|
1729
|
+
await copyFile(source, destination);
|
|
1730
|
+
await chmod(destination, 493);
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
if (mock.kind === "mcp") {
|
|
1734
|
+
mcpMocks.push(mock);
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
const port = await freePort();
|
|
1738
|
+
const url = `http://127.0.0.1:${port}`;
|
|
1739
|
+
const serviceEnv = {
|
|
1740
|
+
...process.env,
|
|
1741
|
+
...mock.env ?? {},
|
|
1742
|
+
PORT: String(port),
|
|
1743
|
+
AGENT_SKILL_EVALS_MOCK_DIR: join(input.runDir, "mocks", mock.name)
|
|
1744
|
+
};
|
|
1745
|
+
await mkdir(serviceEnv.AGENT_SKILL_EVALS_MOCK_DIR, { recursive: true });
|
|
1746
|
+
const service = startProcess(mock, input.baseDir, serviceEnv);
|
|
1747
|
+
running.push(service);
|
|
1748
|
+
await waitUntilReady(service, `${url}${mock.ready.path}`, mock.ready.timeout_ms ?? 1e4);
|
|
1749
|
+
env[mock.expose_as] = url;
|
|
1750
|
+
}
|
|
1751
|
+
if (input.mocks.some((mock) => mock.kind === "command")) env.PATH = `${commandBin}:${process.env.PATH ?? ""}`;
|
|
1752
|
+
const additions = await mcpConfigArgs({
|
|
1753
|
+
mocks: mcpMocks,
|
|
1754
|
+
preset: input.preset,
|
|
1755
|
+
runDir: input.runDir,
|
|
1756
|
+
baseDir: input.baseDir
|
|
1757
|
+
});
|
|
1758
|
+
const args = [...input.baseArgs];
|
|
1759
|
+
const stdin = args.lastIndexOf("-");
|
|
1760
|
+
if (stdin >= 0) args.splice(stdin, 0, ...additions);
|
|
1761
|
+
else args.push(...additions);
|
|
1762
|
+
const prepared = {
|
|
1763
|
+
env,
|
|
1764
|
+
args,
|
|
1765
|
+
assertHealthy() {
|
|
1766
|
+
const crashed = running.find((service) => service.readyAt !== void 0 && service.child.exitCode !== null && service.stoppedAt === void 0);
|
|
1767
|
+
if (crashed) throw new Error(`Mock Service "${crashed.name}" crashed during the run (${crashed.child.exitCode}): ${crashed.stderr || crashed.stdout}`);
|
|
1768
|
+
},
|
|
1769
|
+
async stop() {
|
|
1770
|
+
await Promise.all(running.map(stopProcess));
|
|
1771
|
+
await writeFile(join(input.runDir, "mock-services.json"), JSON.stringify(running.map((service) => ({
|
|
1772
|
+
name: service.name,
|
|
1773
|
+
startedAt: service.startedAt,
|
|
1774
|
+
readyAt: service.readyAt,
|
|
1775
|
+
stoppedAt: service.stoppedAt,
|
|
1776
|
+
exitCode: service.child.exitCode,
|
|
1777
|
+
stdout: service.stdout,
|
|
1778
|
+
stderr: service.stderr
|
|
1779
|
+
})), null, 2));
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
registry().set(input.runDir, prepared);
|
|
1783
|
+
return prepared;
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
await Promise.all(running.map(stopProcess));
|
|
1786
|
+
throw error;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
//#endregion
|
|
1790
|
+
//#region src/agent/index.ts
|
|
1791
|
+
function asVars(value) {
|
|
1792
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1793
|
+
}
|
|
1794
|
+
function varsFromContext(context) {
|
|
1795
|
+
return asVars(context.vars) ?? asVars(context.test?.vars) ?? {};
|
|
1796
|
+
}
|
|
1797
|
+
async function prepareRun(fixture, config, testPackDir) {
|
|
1798
|
+
let runDir;
|
|
1799
|
+
let worldPath;
|
|
1800
|
+
try {
|
|
1801
|
+
({runDir, worldPath} = await createRunDir());
|
|
1802
|
+
} catch (err) {
|
|
1803
|
+
return { error: `agent-skill-evals-provider: failed to create isolated world: ${err instanceof Error ? err.message : String(err)}` };
|
|
1804
|
+
}
|
|
1805
|
+
if (fixture) try {
|
|
1806
|
+
await copyFixture({
|
|
1807
|
+
fixturePath: fixture,
|
|
1808
|
+
baseDir: testPackDir ?? config.baseDir
|
|
1809
|
+
}, worldPath);
|
|
1810
|
+
} catch (err) {
|
|
1811
|
+
return { error: `agent-skill-evals-provider: failed to copy fixture "${fixture}" into isolated world: ${err instanceof Error ? err.message : String(err)}` };
|
|
1812
|
+
}
|
|
1813
|
+
const evidenceCollector = new EvidenceCollector(config.skillEvidence);
|
|
1814
|
+
return {
|
|
1815
|
+
runDir,
|
|
1816
|
+
worldPath,
|
|
1817
|
+
evidenceCollector,
|
|
1818
|
+
world: makeWorldHandle(worldPath, (event) => evidenceCollector.addCommand(event))
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
async function runPreconditions(vars, run) {
|
|
1822
|
+
const results = [];
|
|
1823
|
+
let passed = true;
|
|
1824
|
+
const parsed = parseRuntimeTestFields(vars);
|
|
1825
|
+
for (const error of parsed.errors.filter((e) => e.field === "preconditions")) {
|
|
1826
|
+
const at = error.index === void 0 ? error.field : `${error.field}[${error.index}]`;
|
|
1827
|
+
results.push({
|
|
1828
|
+
pass: false,
|
|
1829
|
+
score: 0,
|
|
1830
|
+
reason: `precondition: ${at}: ${error.reason}`
|
|
1831
|
+
});
|
|
1832
|
+
passed = false;
|
|
1833
|
+
}
|
|
1834
|
+
for (const entry of parsed.preconditions) {
|
|
1835
|
+
const plugin = RUNTIME_CHECKS_BY_TYPE.get(entry.type);
|
|
1836
|
+
if (!plugin) {
|
|
1837
|
+
results.push({
|
|
1838
|
+
pass: false,
|
|
1839
|
+
score: 0,
|
|
1840
|
+
reason: `precondition: unknown effect type "${entry.type}"`
|
|
1841
|
+
});
|
|
1842
|
+
passed = false;
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
const result = await plugin.verify({
|
|
1846
|
+
assertion: entry.args,
|
|
1847
|
+
world: run.world,
|
|
1848
|
+
evidence: evidenceFromSnapshot(run.evidenceCollector.toSnapshot()),
|
|
1849
|
+
mode: "precondition"
|
|
1850
|
+
});
|
|
1851
|
+
results.push(result);
|
|
1852
|
+
if (!result.pass) passed = false;
|
|
1853
|
+
}
|
|
1854
|
+
return {
|
|
1855
|
+
results,
|
|
1856
|
+
passed
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
function promptfooTokenUsage(usage) {
|
|
1860
|
+
const tokenUsage = {
|
|
1861
|
+
...usage.totalTokens !== void 0 ? { total: usage.totalTokens } : {},
|
|
1862
|
+
...usage.inputTokens !== void 0 ? { prompt: usage.inputTokens } : {},
|
|
1863
|
+
...usage.outputTokens !== void 0 ? { completion: usage.outputTokens } : {},
|
|
1864
|
+
...usage.cacheReadTokens !== void 0 ? { cached: usage.cacheReadTokens } : {}
|
|
1865
|
+
};
|
|
1866
|
+
return Object.keys(tokenUsage).length > 0 ? tokenUsage : void 0;
|
|
1867
|
+
}
|
|
1868
|
+
async function runConfiguredAdapter(input) {
|
|
1869
|
+
const adapterId = input.config.adapter;
|
|
1870
|
+
if (!adapterId) return {
|
|
1871
|
+
output: "",
|
|
1872
|
+
error: `agent-skill-evals-provider: config.adapter is required. Set a preset (${PRESET_IDS.join(", ")}) or an adapter (${DOCUMENTED_ADAPTERS.join(", ")}).`
|
|
1873
|
+
};
|
|
1874
|
+
const adapter = adapterRegistry.get(adapterId);
|
|
1875
|
+
if (!adapter) {
|
|
1876
|
+
const suggestion = DOCUMENTED_ADAPTERS.find((id) => id.replace(/[^a-z]/g, "") === adapterId.toLowerCase().replace(/[^a-z]/g, ""));
|
|
1877
|
+
return {
|
|
1878
|
+
output: "",
|
|
1879
|
+
error: `agent-skill-evals-provider: unknown adapter "${adapterId}".` + (suggestion ? ` Did you mean "${suggestion}"?` : "") + ` Supported adapters: ${DOCUMENTED_ADAPTERS.join(", ")}. Presets: ${PRESET_IDS.join(", ")}.`
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
if (!input.config.command) return {
|
|
1883
|
+
output: "",
|
|
1884
|
+
error: "agent-skill-evals-provider: config.command is required for dynamic agent runs. Set a preset to get the command and flags for a supported agent."
|
|
1885
|
+
};
|
|
1886
|
+
const command = input.config.command;
|
|
1887
|
+
const conversation = decodeConversationSpec(input.vars.conversation);
|
|
1888
|
+
if (conversation && "error" in conversation) return {
|
|
1889
|
+
output: "",
|
|
1890
|
+
error: `agent-skill-evals-provider: ${conversation.error}`
|
|
1891
|
+
};
|
|
1892
|
+
const sourceEnv = { ...process.env };
|
|
1893
|
+
const baseDir = (typeof input.vars.testPackDir === "string" ? input.vars.testPackDir : void 0) ?? input.config.baseDir ?? process.cwd();
|
|
1894
|
+
const resolvedCommand = resolveConfiguredPath(baseDir, expandEnvVars(command, sourceEnv));
|
|
1895
|
+
const configuredArgs = (input.config.args ?? []).map((arg) => typeof arg === "string" ? resolveConfiguredPath(baseDir, expandEnvVars(arg, sourceEnv)) : arg);
|
|
1896
|
+
const agentArgs = (input.runtimeArgs ?? input.config.args ?? []).map((arg) => typeof arg === "string" ? resolveConfiguredPath(baseDir, expandEnvVars(arg, sourceEnv)) : arg);
|
|
1897
|
+
let skillEnvironment;
|
|
1898
|
+
try {
|
|
1899
|
+
skillEnvironment = await prepareSkillEnvironment({
|
|
1900
|
+
runDir: input.run.runDir,
|
|
1901
|
+
authHome: input.authHome,
|
|
1902
|
+
worldPath: input.run.worldPath,
|
|
1903
|
+
vars: input.vars,
|
|
1904
|
+
baseDir,
|
|
1905
|
+
preset: input.config.preset,
|
|
1906
|
+
adapter: adapterId,
|
|
1907
|
+
args: agentArgs,
|
|
1908
|
+
prompt: input.prompt,
|
|
1909
|
+
evidence: input.run.evidenceCollector,
|
|
1910
|
+
extraEnv: input.runtimeEnv
|
|
1911
|
+
});
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
return {
|
|
1914
|
+
output: "",
|
|
1915
|
+
error: `agent-skill-evals-provider: failed to prepare hermetic skills: ${error instanceof Error ? error.message : String(error)}`
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
const env = skillEnvironment.env;
|
|
1919
|
+
const cliVersion = await captureCliVersion(resolvedCommand, input.run.worldPath, env);
|
|
1920
|
+
input.run.evidenceCollector.mergeRuntime({
|
|
1921
|
+
adapter: adapterId,
|
|
1922
|
+
...input.config.preset ? { preset: input.config.preset } : {},
|
|
1923
|
+
command,
|
|
1924
|
+
...cliVersion ? { cliVersion } : {},
|
|
1925
|
+
...input.config.model ? { model: input.config.model } : {},
|
|
1926
|
+
...conversation ? { continuation: "transcript-replay" } : {}
|
|
1927
|
+
});
|
|
1928
|
+
const resolvedArgs = skillEnvironment.args;
|
|
1929
|
+
const timeoutMs = input.config.timeoutMs ?? 5 * 6e4;
|
|
1930
|
+
const runAgentTurn = (prompt) => adapter.run({
|
|
1931
|
+
command: resolvedCommand,
|
|
1932
|
+
args: resolvedArgs,
|
|
1933
|
+
cwd: input.run.worldPath,
|
|
1934
|
+
prompt: skillEnvironment.formatPrompt(prompt),
|
|
1935
|
+
evidence: input.run.evidenceCollector,
|
|
1936
|
+
timeoutMs,
|
|
1937
|
+
env
|
|
1938
|
+
});
|
|
1939
|
+
if (!conversation) {
|
|
1940
|
+
const result = await runAgentTurn(input.prompt);
|
|
1941
|
+
if (result.error) return {
|
|
1942
|
+
output: result.output,
|
|
1943
|
+
error: `${result.error}${presetStalenessHint(input.config.preset)}`
|
|
1944
|
+
};
|
|
1945
|
+
return { output: result.output };
|
|
1946
|
+
}
|
|
1947
|
+
const simulatedUserEnv = { ...env };
|
|
1948
|
+
const allowSimulatedUserMocks = Boolean(input.vars.conversation && typeof input.vars.conversation === "object" && input.vars.conversation.simulatedUserAllowMocks);
|
|
1949
|
+
if (!allowSimulatedUserMocks) for (const key of Object.keys(input.runtimeEnv ?? {})) if (process.env[key] === void 0) delete simulatedUserEnv[key];
|
|
1950
|
+
else simulatedUserEnv[key] = process.env[key];
|
|
1951
|
+
const runSimulatedUser = conversation.user ? await makeSimulatedUserRunner({
|
|
1952
|
+
run: input.run,
|
|
1953
|
+
config: input.config,
|
|
1954
|
+
agent: {
|
|
1955
|
+
adapter,
|
|
1956
|
+
command: resolvedCommand,
|
|
1957
|
+
args: allowSimulatedUserMocks ? agentArgs : configuredArgs
|
|
1958
|
+
},
|
|
1959
|
+
baseDir,
|
|
1960
|
+
env: simulatedUserEnv
|
|
1961
|
+
}) : void 0;
|
|
1962
|
+
if (runSimulatedUser && "error" in runSimulatedUser) return {
|
|
1963
|
+
output: "",
|
|
1964
|
+
error: runSimulatedUser.error
|
|
1965
|
+
};
|
|
1966
|
+
const result = await runConversation({
|
|
1967
|
+
spec: conversation,
|
|
1968
|
+
initialPrompt: input.prompt,
|
|
1969
|
+
evidence: input.run.evidenceCollector,
|
|
1970
|
+
runAgentTurn,
|
|
1971
|
+
...runSimulatedUser ? { runSimulatedUser: runSimulatedUser.run } : {}
|
|
1972
|
+
});
|
|
1973
|
+
if (result.error) return {
|
|
1974
|
+
output: result.output,
|
|
1975
|
+
error: `${result.error}${presetStalenessHint(input.config.preset)}`
|
|
1976
|
+
};
|
|
1977
|
+
return { output: result.output };
|
|
1978
|
+
}
|
|
1979
|
+
async function collectFileEvidence(run, preTree) {
|
|
1980
|
+
const postTree = await snapshotTree(run.worldPath);
|
|
1981
|
+
for (const event of diffTrees(preTree, postTree)) {
|
|
1982
|
+
if (event.path.startsWith(".agents/skills/") || event.path.startsWith(".claude/skills/")) continue;
|
|
1983
|
+
run.evidenceCollector.addFileWrite(event);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
var AgentSkillEvalsProvider = class {
|
|
1987
|
+
config;
|
|
1988
|
+
configError;
|
|
1989
|
+
authHome;
|
|
1990
|
+
id;
|
|
1991
|
+
constructor(options = {}) {
|
|
1992
|
+
const config = decodeProviderConfig(options.config ?? {});
|
|
1993
|
+
if ("error" in config) {
|
|
1994
|
+
this.config = {};
|
|
1995
|
+
this.configError = config.error;
|
|
1996
|
+
} else this.config = config;
|
|
1997
|
+
const label = options.id ?? "agent-skill-evals";
|
|
1998
|
+
this.id = () => label;
|
|
1999
|
+
}
|
|
2000
|
+
async callApi(prompt, context = {}) {
|
|
2001
|
+
if (this.configError) return {
|
|
2002
|
+
output: "",
|
|
2003
|
+
error: this.configError
|
|
2004
|
+
};
|
|
2005
|
+
const startedAt = Date.now();
|
|
2006
|
+
const vars = varsFromContext(context);
|
|
2007
|
+
const fixture = typeof vars.fixture === "string" ? vars.fixture : void 0;
|
|
2008
|
+
const testPackDir = typeof vars.testPackDir === "string" ? vars.testPackDir : void 0;
|
|
2009
|
+
const prepared = await prepareRun(fixture, this.config, testPackDir);
|
|
2010
|
+
if ("error" in prepared) return {
|
|
2011
|
+
output: "",
|
|
2012
|
+
error: prepared.error
|
|
2013
|
+
};
|
|
2014
|
+
this.authHome ??= join(prepared.runDir, "home");
|
|
2015
|
+
const environment = vars.environment && typeof vars.environment === "object" && !Array.isArray(vars.environment) ? vars.environment : {};
|
|
2016
|
+
const mocks = Array.isArray(environment.mocks) ? environment.mocks : [];
|
|
2017
|
+
const mockBaseDir = testPackDir ?? this.config.baseDir ?? process.cwd();
|
|
2018
|
+
let mockServices;
|
|
2019
|
+
try {
|
|
2020
|
+
mockServices = await startMockServices({
|
|
2021
|
+
mocks,
|
|
2022
|
+
runDir: prepared.runDir,
|
|
2023
|
+
baseDir: mockBaseDir,
|
|
2024
|
+
preset: this.config.preset,
|
|
2025
|
+
baseArgs: this.config.args ?? []
|
|
2026
|
+
});
|
|
2027
|
+
} catch (error) {
|
|
2028
|
+
return {
|
|
2029
|
+
output: "",
|
|
2030
|
+
error: `agent-skill-evals-provider: Mock Service setup failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
try {
|
|
2034
|
+
for (const command of Array.isArray(vars.setup) ? vars.setup : []) {
|
|
2035
|
+
if (typeof command !== "string") continue;
|
|
2036
|
+
const setup = await prepared.world.exec("sh", ["-lc", command], {
|
|
2037
|
+
timeoutMs: 6e4,
|
|
2038
|
+
env: Object.fromEntries(Object.entries(mockServices.env).filter((entry) => entry[1] !== void 0))
|
|
2039
|
+
});
|
|
2040
|
+
if (setup.exitCode !== 0) {
|
|
2041
|
+
await stopMockServices(prepared.runDir);
|
|
2042
|
+
return {
|
|
2043
|
+
output: "",
|
|
2044
|
+
error: `agent-skill-evals-provider: setup failed (${command}): ${setup.stderr || setup.stdout}`
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const preconditions = await runPreconditions(vars, prepared);
|
|
2049
|
+
const preTree = await snapshotTree(prepared.worldPath);
|
|
2050
|
+
let output = "";
|
|
2051
|
+
let error;
|
|
2052
|
+
if (preconditions.passed) {
|
|
2053
|
+
const result = await runConfiguredAdapter({
|
|
2054
|
+
prompt,
|
|
2055
|
+
run: prepared,
|
|
2056
|
+
authHome: this.authHome,
|
|
2057
|
+
config: this.config,
|
|
2058
|
+
vars,
|
|
2059
|
+
runtimeEnv: mockServices.env,
|
|
2060
|
+
runtimeArgs: mockServices.args
|
|
2061
|
+
});
|
|
2062
|
+
output = result.output;
|
|
2063
|
+
error = result.error;
|
|
2064
|
+
mockServices.assertHealthy();
|
|
2065
|
+
}
|
|
2066
|
+
await collectFileEvidence(prepared, preTree);
|
|
2067
|
+
let metadata;
|
|
2068
|
+
try {
|
|
2069
|
+
metadata = await persistMetadata({
|
|
2070
|
+
run: prepared,
|
|
2071
|
+
fixture,
|
|
2072
|
+
vars,
|
|
2073
|
+
output,
|
|
2074
|
+
preconditionResults: preconditions.results,
|
|
2075
|
+
preconditionsPassed: preconditions.passed,
|
|
2076
|
+
startedAt,
|
|
2077
|
+
mockEnv: Object.fromEntries(Object.entries(mockServices.env).filter((entry) => entry[1] !== void 0))
|
|
2078
|
+
});
|
|
2079
|
+
} catch (err) {
|
|
2080
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2081
|
+
return {
|
|
2082
|
+
output,
|
|
2083
|
+
error: `${error ? `${error}; ` : ""}agent-skill-evals-provider: failed to write run evidence/metadata: ${message}`
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
const usage = promptfooTokenUsage(prepared.evidenceCollector.toSnapshot().usage);
|
|
2087
|
+
return {
|
|
2088
|
+
output,
|
|
2089
|
+
...error ? { error } : {},
|
|
2090
|
+
metadata,
|
|
2091
|
+
...usage ? { tokenUsage: usage } : {}
|
|
2092
|
+
};
|
|
2093
|
+
} catch (caught) {
|
|
2094
|
+
await stopMockServices(prepared.runDir);
|
|
2095
|
+
return {
|
|
2096
|
+
output: "",
|
|
2097
|
+
error: `agent-skill-evals-provider: Mock Service runtime failed: ${caught instanceof Error ? caught.message : String(caught)}`
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
//#endregion
|
|
2103
|
+
export { EvidenceCollector as a, decodeEvidenceSnapshot as c, makeWorldHandle as i, parseRuntimeTestFields as l, assertMockServicesHealthy as n, evidenceFromSnapshot as o, stopMockServices as r, writeEvidenceTo as s, AgentSkillEvalsProvider as t };
|
|
2104
|
+
|
|
2105
|
+
//# sourceMappingURL=agent-B15R7Xlr.mjs.map
|