@lesliechan721/agent-skill-eval 0.1.0-beta.7 → 0.1.0-beta.9

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.
Files changed (98) hide show
  1. package/dist/bin.js +149 -39
  2. package/dist/bin.js.map +1 -1
  3. package/dist/codex-eval-app-server.d.ts +199 -0
  4. package/dist/codex-eval-app-server.js +1366 -0
  5. package/dist/codex-eval-app-server.js.map +1 -0
  6. package/dist/codex-eval-cases.d.ts +43 -0
  7. package/dist/codex-eval-cases.js +327 -0
  8. package/dist/codex-eval-cases.js.map +1 -0
  9. package/dist/codex-eval-isolation.d.ts +61 -0
  10. package/dist/codex-eval-isolation.js +318 -0
  11. package/dist/codex-eval-isolation.js.map +1 -0
  12. package/dist/codex-eval-process-tree.d.ts +38 -0
  13. package/dist/codex-eval-process-tree.js +803 -0
  14. package/dist/codex-eval-process-tree.js.map +1 -0
  15. package/dist/codex-eval-runtime-config.d.ts +43 -0
  16. package/dist/codex-eval-runtime-config.js +607 -0
  17. package/dist/codex-eval-runtime-config.js.map +1 -0
  18. package/dist/codex-trigger-eval.d.ts +82 -0
  19. package/dist/codex-trigger-eval.js +789 -0
  20. package/dist/codex-trigger-eval.js.map +1 -0
  21. package/dist/codex-workflow.d.ts +188 -0
  22. package/dist/codex-workflow.js +1790 -0
  23. package/dist/codex-workflow.js.map +1 -0
  24. package/dist/credentials.d.ts +5 -0
  25. package/dist/credentials.js +30 -0
  26. package/dist/credentials.js.map +1 -0
  27. package/dist/custom-process.d.ts +58 -0
  28. package/dist/custom-process.js +409 -0
  29. package/dist/custom-process.js.map +1 -0
  30. package/dist/eval-audit.d.ts +38 -0
  31. package/dist/eval-audit.js +71 -0
  32. package/dist/eval-audit.js.map +1 -0
  33. package/dist/eval-file.d.ts +9 -0
  34. package/dist/eval-file.js +15 -0
  35. package/dist/eval-file.js.map +1 -0
  36. package/dist/eval-result.d.ts +64 -0
  37. package/dist/eval-result.js +144 -0
  38. package/dist/eval-result.js.map +1 -0
  39. package/dist/eval-run.d.ts +23 -0
  40. package/dist/eval-run.js +539 -0
  41. package/dist/eval-run.js.map +1 -0
  42. package/dist/eval-spec.d.ts +57 -0
  43. package/dist/eval-spec.js +120 -0
  44. package/dist/eval-spec.js.map +1 -0
  45. package/dist/eval-suite-binding.d.ts +4 -0
  46. package/dist/eval-suite-binding.js +19 -0
  47. package/dist/eval-suite-binding.js.map +1 -0
  48. package/dist/index.d.ts +34 -9
  49. package/dist/index.js +18 -6
  50. package/dist/index.js.map +1 -1
  51. package/dist/json-schema.js +75 -5
  52. package/dist/json-schema.js.map +1 -1
  53. package/dist/output-path.js +2 -2
  54. package/dist/process-tree.d.ts +19 -1
  55. package/dist/process-tree.js +321 -66
  56. package/dist/process-tree.js.map +1 -1
  57. package/dist/process.d.ts +0 -2
  58. package/dist/process.js +76 -25
  59. package/dist/process.js.map +1 -1
  60. package/dist/provider-doctor.d.ts +33 -0
  61. package/dist/provider-doctor.js +86 -0
  62. package/dist/provider-doctor.js.map +1 -0
  63. package/dist/providers.d.ts +82 -0
  64. package/dist/providers.js +630 -0
  65. package/dist/providers.js.map +1 -0
  66. package/dist/run-paths.d.ts +8 -0
  67. package/dist/run-paths.js +71 -0
  68. package/dist/run-paths.js.map +1 -0
  69. package/dist/runtime-skill.d.ts +6 -0
  70. package/dist/runtime-skill.js +87 -3
  71. package/dist/runtime-skill.js.map +1 -1
  72. package/dist/schemas/digests.json +3 -3
  73. package/dist/schemas/eval-audit.schema.json +64 -0
  74. package/dist/schemas/eval-result.schema.json +202 -0
  75. package/dist/schemas/eval-spec.schema.json +109 -0
  76. package/dist/schemas.d.ts +1 -3
  77. package/dist/schemas.js +0 -17
  78. package/dist/schemas.js.map +1 -1
  79. package/dist/selected-cases.d.ts +30 -0
  80. package/dist/selected-cases.js +388 -0
  81. package/dist/selected-cases.js.map +1 -0
  82. package/dist/types.d.ts +2 -34
  83. package/package.json +6 -1
  84. package/dist/bundle.d.ts +0 -93
  85. package/dist/bundle.js +0 -216
  86. package/dist/bundle.js.map +0 -1
  87. package/dist/capabilities.d.ts +0 -2
  88. package/dist/capabilities.js +0 -38
  89. package/dist/capabilities.js.map +0 -1
  90. package/dist/plan.d.ts +0 -79
  91. package/dist/plan.js +0 -285
  92. package/dist/plan.js.map +0 -1
  93. package/dist/run.d.ts +0 -2
  94. package/dist/run.js +0 -592
  95. package/dist/run.js.map +0 -1
  96. package/dist/schemas/aw-skill-eval-execution-plan.schema.json +0 -220
  97. package/dist/schemas/eval-run-bundle.schema.json +0 -408
  98. package/dist/schemas/eval-runtime-capabilities.schema.json +0 -85
@@ -0,0 +1,789 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { computeSkillDigest } from './runtime-skill.js';
5
+ import fs from 'fs-extra';
6
+ import { CodexEvalAttemptControl, EvalAppServerSession, extractAppServerThreadId, inspectAppServerConfigWarning, } from './codex-eval-app-server.js';
7
+ import { assertCodexEvalCanStartWork, applyCodexEvalProviderEnvironment, createCodexEvalTemporaryRoot, materializeCodexEvalRuntimeHome, } from './codex-eval-runtime-config.js';
8
+ async function createTriggerTemporaryRoot(workRoot, strict = false) {
9
+ if (strict || !workRoot)
10
+ return createCodexEvalTemporaryRoot('aw-trigger-eval-');
11
+ await fs.ensureDir(workRoot);
12
+ return createCodexEvalTemporaryRoot('trigger-', {
13
+ allocate: () => fs.mkdtemp(path.join(workRoot, 'trigger-')),
14
+ });
15
+ }
16
+ const PASS_THRESHOLD = 0.5;
17
+ const PASSTHROUGH_ENV = new Set([
18
+ 'PATH',
19
+ 'LANG',
20
+ 'LC_ALL',
21
+ 'HTTP_PROXY',
22
+ 'HTTPS_PROXY',
23
+ 'ALL_PROXY',
24
+ 'NO_PROXY',
25
+ 'SSL_CERT_FILE',
26
+ 'SSL_CERT_DIR',
27
+ 'NODE_EXTRA_CA_CERTS',
28
+ ]);
29
+ const PROTECTED_ENV = new Set([
30
+ 'HOME',
31
+ 'USERPROFILE',
32
+ 'CODEX_HOME',
33
+ 'XDG_CACHE_HOME',
34
+ 'XDG_CONFIG_HOME',
35
+ 'XDG_STATE_HOME',
36
+ 'TMPDIR',
37
+ 'TEMP',
38
+ 'TMP',
39
+ 'PATH',
40
+ ]);
41
+ function isRecord(value) {
42
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
43
+ }
44
+ function sha256(value) {
45
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
46
+ }
47
+ function stableJson(value) {
48
+ if (Array.isArray(value)) {
49
+ return `[${value.map(stableJson).join(',')}]`;
50
+ }
51
+ if (isRecord(value)) {
52
+ return `{${Object.entries(value)
53
+ .sort(([left], [right]) => left.localeCompare(right))
54
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
55
+ .join(',')}}`;
56
+ }
57
+ return JSON.stringify(value);
58
+ }
59
+ function buildIsolatedEnv(sourceEnv, runRoot, codexHome, runtime) {
60
+ const env = {};
61
+ for (const name of PASSTHROUGH_ENV) {
62
+ if (sourceEnv[name] !== undefined) {
63
+ env[name] = sourceEnv[name];
64
+ }
65
+ }
66
+ const home = path.join(runRoot, 'home');
67
+ const temp = path.join(runRoot, 'tmp');
68
+ return applyCodexEvalProviderEnvironment({
69
+ ...env,
70
+ HOME: home,
71
+ USERPROFILE: home,
72
+ CODEX_HOME: codexHome,
73
+ XDG_CACHE_HOME: path.join(runRoot, 'xdg-cache'),
74
+ XDG_CONFIG_HOME: path.join(runRoot, 'xdg-config'),
75
+ XDG_STATE_HOME: path.join(runRoot, 'xdg-state'),
76
+ TMPDIR: temp,
77
+ TEMP: temp,
78
+ TMP: temp,
79
+ NO_COLOR: '1',
80
+ }, runtime);
81
+ }
82
+ async function snapshotWorkspace(root) {
83
+ const snapshot = {};
84
+ async function visit(current) {
85
+ const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
86
+ for (const entry of entries) {
87
+ if (entry.name === '.git') {
88
+ continue;
89
+ }
90
+ const absolutePath = path.join(current, entry.name);
91
+ if (entry.isDirectory()) {
92
+ await visit(absolutePath);
93
+ }
94
+ else if (entry.isFile()) {
95
+ const relativePath = path.relative(root, absolutePath).split(path.sep).join('/');
96
+ snapshot[relativePath] = sha256(await fs.readFile(absolutePath));
97
+ }
98
+ }
99
+ }
100
+ await visit(root);
101
+ return snapshot;
102
+ }
103
+ function explicitSkillRequested(query, skillName) {
104
+ const escaped = skillName.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
105
+ return new RegExp(`(?:^|\\s)\\$${escaped}(?:\\s|$)`, 'u').test(query);
106
+ }
107
+ function expectedLoadedSkills(suite, evalCase) {
108
+ if (evalCase.expected_loaded_skills) {
109
+ return [...evalCase.expected_loaded_skills];
110
+ }
111
+ return evalCase.should_load_skill ? [suite.skill] : [];
112
+ }
113
+ function sameSkillSet(left, right) {
114
+ return left.length === right.length && left.every((entry) => right.includes(entry));
115
+ }
116
+ function routeLabel(skills) {
117
+ if (skills.length === 0)
118
+ return 'neither';
119
+ return [...skills].sort((left, right) => left.localeCompare(right)).join('+');
120
+ }
121
+ function normalizedCommandReadsSkill(command, skillPath) {
122
+ const normalize = (value) => {
123
+ let normalized = value.replaceAll('\\', '/').replaceAll('"', '').replaceAll("'", '');
124
+ for (let index = 0; index < 3; index += 1) {
125
+ normalized = normalized.replace(/(^|[\s/])\.\/(?=\S)/gu, '$1');
126
+ }
127
+ return normalized;
128
+ };
129
+ const normalized = normalize(command);
130
+ if (!/(?:^|[\s;&|/])(?:cat|sed|head|tail|less|more|bat|awk|grep|rg|type|get-content)\b/iu.test(normalized)) {
131
+ return false;
132
+ }
133
+ const normalizedSkillPath = normalize(skillPath);
134
+ const normalizedSkillDir = path.posix.dirname(normalizedSkillPath);
135
+ const managedSkillDir = `.agents/skills/${path.basename(path.dirname(skillPath))}`;
136
+ if (normalized.includes(normalizedSkillPath) ||
137
+ normalized.includes(`${managedSkillDir}/SKILL.md`)) {
138
+ return true;
139
+ }
140
+ return ((normalized.includes(normalizedSkillDir) || normalized.includes(managedSkillDir)) &&
141
+ /(?:^|[\s;&|/])SKILL\.md(?:$|[\s;&|])/u.test(normalized));
142
+ }
143
+ function commandOutputContainsSkill(item, skillPath) {
144
+ if (typeof item.aggregatedOutput !== 'string')
145
+ return false;
146
+ const skillName = path.basename(path.dirname(skillPath));
147
+ const escaped = skillName.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
148
+ return new RegExp(`^name:\\s*["']?${escaped}["']?\\s*$`, 'mu').test(item.aggregatedOutput);
149
+ }
150
+ export function inspectTriggerAppServerFrame(frame, skillPath) {
151
+ const method = typeof frame.method === 'string' ? frame.method : '';
152
+ const params = isRecord(frame.params) ? frame.params : {};
153
+ const item = isRecord(params.item) ? params.item : null;
154
+ if (item?.type === 'commandExecution' && typeof item.command === 'string') {
155
+ const completed = method === 'item/completed';
156
+ const statusPassed = item.status === undefined || item.status === 'completed';
157
+ const exitPassed = item.exitCode === undefined || item.exitCode === 0;
158
+ const outputProvesRead = commandOutputContainsSkill(item, skillPath);
159
+ if (completed &&
160
+ ((statusPassed && exitPassed) || outputProvesRead) &&
161
+ normalizedCommandReadsSkill(item.command, skillPath)) {
162
+ return { loaded: true, loadEvidence: item.command };
163
+ }
164
+ }
165
+ if ((method === 'item/started' || method === 'item/completed') &&
166
+ item?.type === 'subAgentActivity' &&
167
+ item.kind === 'started') {
168
+ const agentPath = typeof item.agentPath === 'string' ? item.agentPath : null;
169
+ const agentThreadId = typeof item.agentThreadId === 'string' ? item.agentThreadId : null;
170
+ return {
171
+ spawned: true,
172
+ spawnEvidence: `app-server:${agentPath ?? agentThreadId ?? String(item.id ?? 'started')}`,
173
+ spawnCall: {
174
+ source: 'app_server_sub_agent_activity',
175
+ event_id: typeof item.id === 'string' ? item.id : null,
176
+ kind: 'started',
177
+ agent_thread_id: agentThreadId,
178
+ agent_path: agentPath,
179
+ },
180
+ };
181
+ }
182
+ if (method === 'item/completed' &&
183
+ item?.type === 'agentMessage' &&
184
+ item.phase !== 'commentary' &&
185
+ typeof item.text === 'string') {
186
+ return { finalMessage: item.text };
187
+ }
188
+ if (method === 'thread/tokenUsage/updated' && params.tokenUsage !== undefined) {
189
+ return { usage: params.tokenUsage };
190
+ }
191
+ if (method === 'item/tool/requestUserInput') {
192
+ return { requestUserInput: true };
193
+ }
194
+ if (method === 'turn/completed') {
195
+ const turn = isRecord(params.turn) ? params.turn : {};
196
+ return { turnStatus: typeof turn.status === 'string' ? turn.status : 'unknown' };
197
+ }
198
+ return {};
199
+ }
200
+ export function inspectTriggerAppServerConfigWarning(frame) {
201
+ return inspectAppServerConfigWarning(frame);
202
+ }
203
+ function resolveCodexExecutable(codexBin) {
204
+ if (process.platform !== 'win32') {
205
+ return codexBin;
206
+ }
207
+ if (!codexBin.toLowerCase().endsWith('.exe') || !path.win32.isAbsolute(codexBin)) {
208
+ throw new Error('On Windows, --codex-bin must be an absolute path to the native codex.exe.');
209
+ }
210
+ return codexBin;
211
+ }
212
+ function expectedEarlyStop(suite, evalCase, observed, loadedSkills) {
213
+ if (evalCase.expected_first_loaded_skill) {
214
+ return loadedSkills[0] === evalCase.expected_first_loaded_skill
215
+ ? 'expected_first_skill_route_observed'
216
+ : null;
217
+ }
218
+ if (evalCase.expected_loaded_skills) {
219
+ const expected = expectedLoadedSkills(suite, evalCase);
220
+ if (suite.forbidMultipleSkillLoads && loadedSkills.length > 1) {
221
+ return 'unexpected_multiple_skill_loads';
222
+ }
223
+ const routeResolved = expected.length === suite.skills.length && sameSkillSet(loadedSkills, expected);
224
+ if (suite.observeSpawnAgent && !evalCase.should_spawn_agent && observed.spawned) {
225
+ return 'unexpected_spawn';
226
+ }
227
+ if (routeResolved &&
228
+ suite.observeSpawnAgent &&
229
+ evalCase.should_spawn_agent &&
230
+ observed.spawned) {
231
+ return 'expected_outcome_observed';
232
+ }
233
+ if (routeResolved && !suite.observeSpawnAgent) {
234
+ return 'expected_skill_route_observed';
235
+ }
236
+ return null;
237
+ }
238
+ if (!evalCase.should_load_skill && observed.loaded) {
239
+ return 'unexpected_skill_load';
240
+ }
241
+ if (suite.observeSpawnAgent && !evalCase.should_spawn_agent && observed.spawned) {
242
+ return 'unexpected_spawn';
243
+ }
244
+ if (!suite.observeSpawnAgent && evalCase.should_load_skill && observed.loaded) {
245
+ return 'expected_skill_load';
246
+ }
247
+ if (suite.observeSpawnAgent &&
248
+ evalCase.should_load_skill &&
249
+ evalCase.should_spawn_agent &&
250
+ observed.loaded &&
251
+ observed.spawned) {
252
+ return 'expected_outcome_observed';
253
+ }
254
+ return null;
255
+ }
256
+ async function loadFixtureModule(pluginRoot, suite) {
257
+ if (!suite.fixtureModule) {
258
+ return null;
259
+ }
260
+ const modulePath = path.resolve(pluginRoot, suite.fixtureModule.replace(/^\.\//u, ''));
261
+ const loaded = await import(pathToFileURL(modulePath).href);
262
+ if (typeof loaded.setupTriggerWorkspace !== 'function') {
263
+ throw new Error(`Trigger fixture module for suite "${suite.id}" must export setupTriggerWorkspace().`);
264
+ }
265
+ return loaded;
266
+ }
267
+ function mergeFixtureEnvironment(baseEnv, result) {
268
+ if (!result?.environment) {
269
+ return baseEnv;
270
+ }
271
+ if (!isRecord(result.environment)) {
272
+ throw new Error('Trigger fixture environment must be an object.');
273
+ }
274
+ const additions = {};
275
+ for (const [name, value] of Object.entries(result.environment)) {
276
+ if (PROTECTED_ENV.has(name)) {
277
+ throw new Error(`Trigger fixture cannot override protected environment variable ${name}.`);
278
+ }
279
+ if (typeof value !== 'string') {
280
+ throw new Error(`Trigger fixture environment variable ${name} must be a string.`);
281
+ }
282
+ additions[name] = value;
283
+ }
284
+ return { ...baseEnv, ...additions };
285
+ }
286
+ function protectedEnvironmentSnapshot(env) {
287
+ return Object.fromEntries([...PROTECTED_ENV].map((name) => [name, env[name]]));
288
+ }
289
+ function assertProtectedEnvironmentUnchanged(env, before) {
290
+ for (const [name, value] of Object.entries(before)) {
291
+ if (env[name] !== value) {
292
+ throw new Error(`Trigger fixture cannot mutate protected environment variable ${name}.`);
293
+ }
294
+ }
295
+ }
296
+ async function captureState(fixture, context) {
297
+ const state = fixture?.captureTriggerWorkspaceState
298
+ ? await fixture.captureTriggerWorkspaceState(context)
299
+ : await snapshotWorkspace(context.workspaceRoot);
300
+ if (state === undefined || stableJson(state) === undefined) {
301
+ throw new Error('Trigger fixture state must be JSON-serializable.');
302
+ }
303
+ return state;
304
+ }
305
+ async function isolateTriggerWorkspaceSkills(workspaceRoot, skillNames) {
306
+ const skillsRoot = path.join(workspaceRoot, '.agents', 'skills');
307
+ const allowed = new Set(skillNames);
308
+ const entries = await fs.readdir(skillsRoot).catch((error) => {
309
+ if (error.code === 'ENOENT')
310
+ return [];
311
+ throw error;
312
+ });
313
+ await Promise.all(entries.map(async (entry) => {
314
+ if (allowed.has(entry))
315
+ return;
316
+ await fs.remove(path.join(skillsRoot, entry));
317
+ }));
318
+ }
319
+ async function isolateTriggerWorkspaceRules(workspaceRoot, useProjectRules) {
320
+ if (useProjectRules === true)
321
+ return;
322
+ await fs.remove(path.join(workspaceRoot, '.codex', 'rules'));
323
+ }
324
+ async function snapshotStrictControlPlane(workspaceRoot) {
325
+ const snapshot = await snapshotWorkspace(workspaceRoot);
326
+ return Object.fromEntries(Object.entries(snapshot).filter(([entry]) => entry === '.agents' ||
327
+ entry.startsWith('.agents/') ||
328
+ entry === '.codex' ||
329
+ entry.startsWith('.codex/')));
330
+ }
331
+ async function runTriggerOnce(options) {
332
+ assertCodexEvalCanStartWork();
333
+ const control = new CodexEvalAttemptControl({
334
+ label: 'trigger evaluation',
335
+ ...(options.manageTimeout === false ? {} : { timeoutMs: options.timeoutMs }),
336
+ ...(options.signal ? { signal: options.signal } : {}),
337
+ });
338
+ const temporaryRoot = await createTriggerTemporaryRoot(options.workRoot, options.suite.isolation === 'strict');
339
+ const runRoot = temporaryRoot.root;
340
+ const workspaceRoot = path.join(runRoot, 'project');
341
+ const codexHome = path.join(runRoot, 'codex-home');
342
+ const startedAt = Date.now();
343
+ const diagnostics = [];
344
+ const eventTrace = [];
345
+ let finalMessage = null;
346
+ let usage = null;
347
+ let session;
348
+ let removeHandler;
349
+ let removeFailureHandler;
350
+ let removeAbortHandler;
351
+ try {
352
+ await fs.copy(options.subjectRoot, workspaceRoot, { dereference: false });
353
+ await isolateTriggerWorkspaceRules(workspaceRoot, options.evalCase.use_project_rules);
354
+ if (options.suite.isolation === 'strict') {
355
+ await isolateTriggerWorkspaceSkills(workspaceRoot, options.suite.skills);
356
+ }
357
+ const declaredSkillPaths = new Map(options.suite.skills.map((skillName) => [
358
+ skillName,
359
+ path.join(workspaceRoot, '.agents', 'skills', skillName, 'SKILL.md'),
360
+ ]));
361
+ const skillPaths = new Map();
362
+ for (const [skillName, skillPath] of declaredSkillPaths) {
363
+ if (await fs.pathExists(skillPath)) {
364
+ skillPaths.set(skillName, skillPath);
365
+ }
366
+ else if (!options.allowMissingSkills) {
367
+ throw new Error(`Materialized trigger Skill is missing: ${skillName} (${skillPath})`);
368
+ }
369
+ }
370
+ const skillDigests = new Map(await Promise.all([...skillPaths].map(async ([skillName, skillPath]) => [
371
+ skillName,
372
+ await computeSkillDigest(path.dirname(skillPath)),
373
+ ])));
374
+ const ownerSkillPath = declaredSkillPaths.get(options.suite.skill);
375
+ let env = buildIsolatedEnv(options.sourceEnv, runRoot, codexHome, options.codexRuntime);
376
+ await Promise.all([
377
+ fs.ensureDir(env.HOME),
378
+ fs.ensureDir(env.TMPDIR),
379
+ materializeCodexEvalRuntimeHome(options.codexRuntime, codexHome, {
380
+ trustedProject: workspaceRoot,
381
+ }),
382
+ ]);
383
+ const initialContext = {
384
+ workspaceRoot,
385
+ evalCase: options.evalCase,
386
+ env,
387
+ };
388
+ const protectedEnvironment = protectedEnvironmentSnapshot(env);
389
+ const controlPlaneBefore = options.suite.isolation === 'strict'
390
+ ? await snapshotStrictControlPlane(workspaceRoot)
391
+ : null;
392
+ const fixtureResult = options.fixture
393
+ ? await options.fixture.setupTriggerWorkspace(initialContext)
394
+ : undefined;
395
+ assertProtectedEnvironmentUnchanged(env, protectedEnvironment);
396
+ env = mergeFixtureEnvironment(env, fixtureResult);
397
+ if (controlPlaneBefore &&
398
+ stableJson(controlPlaneBefore) !== stableJson(await snapshotStrictControlPlane(workspaceRoot))) {
399
+ throw new Error('Trigger fixture modified strict eval control-plane files.');
400
+ }
401
+ const context = {
402
+ ...initialContext,
403
+ env,
404
+ };
405
+ for (const [skillName, skillPath] of skillPaths) {
406
+ if (await computeSkillDigest(path.dirname(skillPath)) !== skillDigests.get(skillName)) {
407
+ throw new Error(`Trigger fixture modified materialized participant Skill "${skillName}".`);
408
+ }
409
+ }
410
+ const beforeState = await captureState(options.fixture, context);
411
+ const allLoadedSkills = options.suite.skills.filter((skillName) => explicitSkillRequested(options.evalCase.query, skillName));
412
+ const loadEvidenceBySkill = Object.fromEntries(allLoadedSkills.map((skillName) => [
413
+ skillName,
414
+ `explicit $${skillName} invocation`,
415
+ ]));
416
+ const observed = {
417
+ loaded: allLoadedSkills.includes(options.suite.skill),
418
+ spawned: false,
419
+ loadEvidence: loadEvidenceBySkill[options.suite.skill] ?? null,
420
+ spawnEvidence: null,
421
+ };
422
+ let requestUserInputCalled = false;
423
+ let waitingOnUserInput = false;
424
+ let finalResponseEmitted = false;
425
+ const recordSkillLoad = (skillName, evidence) => {
426
+ if (!allLoadedSkills.includes(skillName))
427
+ allLoadedSkills.push(skillName);
428
+ if (evidence && !loadEvidenceBySkill[skillName]) {
429
+ loadEvidenceBySkill[skillName] = evidence;
430
+ }
431
+ if (skillName === options.suite.skill) {
432
+ observed.loaded = true;
433
+ observed.loadEvidence ??= evidence ?? null;
434
+ }
435
+ };
436
+ const spawnCalls = [];
437
+ const spawnCallIds = new Set();
438
+ let rootThreadId;
439
+ let turnStatus;
440
+ let finishRun = () => undefined;
441
+ let rejectRun = () => undefined;
442
+ let runSettled = false;
443
+ const runDone = new Promise((resolve, reject) => {
444
+ finishRun = (termination) => {
445
+ if (runSettled)
446
+ return;
447
+ runSettled = true;
448
+ resolve(termination);
449
+ };
450
+ rejectRun = (error) => {
451
+ if (runSettled)
452
+ return;
453
+ runSettled = true;
454
+ reject(error);
455
+ };
456
+ });
457
+ void runDone.catch(() => undefined);
458
+ const abort = () => {
459
+ finishRun(control.timeoutExpired ? 'timeout' : 'aborted');
460
+ void session?.cleanup().catch((error) => {
461
+ rejectRun(error instanceof Error ? error : new Error(String(error)));
462
+ });
463
+ };
464
+ if (control.signal.aborted)
465
+ abort();
466
+ else {
467
+ control.signal.addEventListener('abort', abort, { once: true });
468
+ removeAbortHandler = () => control.signal.removeEventListener('abort', abort);
469
+ }
470
+ control.throwIfStopped('initialize');
471
+ assertCodexEvalCanStartWork();
472
+ session = new EvalAppServerSession({
473
+ codexBin: resolveCodexExecutable(options.codexBin),
474
+ cwd: workspaceRoot,
475
+ env,
476
+ modelProvider: options.codexRuntime.modelProvider,
477
+ sensitiveEnvironmentNames: options.codexRuntime.sensitiveEnvironmentNames,
478
+ isolation: options.suite.isolation,
479
+ processManagement: options.processManagement,
480
+ configOverrides: options.suite.observeSpawnAgent
481
+ ? [
482
+ 'features.multi_agent=true',
483
+ 'features.multi_agent_v2.enabled=true',
484
+ 'features.multi_agent_v2.hide_spawn_agent_metadata=true',
485
+ 'features.multi_agent_v2.expose_spawn_agent_model_overrides=true',
486
+ ]
487
+ : [],
488
+ });
489
+ control.throwIfStopped('initialize');
490
+ removeFailureHandler = session.onFailure(rejectRun);
491
+ removeHandler = session.onFrame((frame) => {
492
+ const method = typeof frame.method === 'string' ? frame.method : '';
493
+ const params = isRecord(frame.params) ? frame.params : {};
494
+ if (!method.startsWith('remoteControl/') && !method.startsWith('account/')) {
495
+ eventTrace.push(frame);
496
+ }
497
+ const configWarning = options.suite.isolation === 'strict'
498
+ ? inspectTriggerAppServerConfigWarning(frame)
499
+ : null;
500
+ if (configWarning) {
501
+ diagnostics.push(`Strict Codex eval received a config warning: ${configWarning}`);
502
+ finishRun('strict_isolation_error');
503
+ return;
504
+ }
505
+ const frameThreadId = typeof params.threadId === 'string' ? params.threadId : undefined;
506
+ if (rootThreadId && frameThreadId && frameThreadId !== rootThreadId) {
507
+ return;
508
+ }
509
+ const evidence = inspectTriggerAppServerFrame(frame, ownerSkillPath);
510
+ for (const [skillName, skillPath] of skillPaths) {
511
+ const skillEvidence = inspectTriggerAppServerFrame(frame, skillPath);
512
+ if (skillEvidence.loaded) {
513
+ recordSkillLoad(skillName, skillEvidence.loadEvidence);
514
+ }
515
+ }
516
+ if (evidence.spawned) {
517
+ observed.spawned = true;
518
+ observed.spawnEvidence ??= evidence.spawnEvidence ?? null;
519
+ }
520
+ if (evidence.spawnCall) {
521
+ const identity = `${String(evidence.spawnCall.event_id)}:${String(evidence.spawnCall.agent_thread_id)}`;
522
+ if (!spawnCallIds.has(identity)) {
523
+ spawnCallIds.add(identity);
524
+ spawnCalls.push(evidence.spawnCall);
525
+ }
526
+ }
527
+ if (evidence.finalMessage !== undefined) {
528
+ finalMessage = evidence.finalMessage;
529
+ finalResponseEmitted = true;
530
+ }
531
+ if (evidence.usage !== undefined) {
532
+ usage = evidence.usage;
533
+ }
534
+ if (evidence.turnStatus !== undefined) {
535
+ turnStatus = evidence.turnStatus;
536
+ finishRun('completed');
537
+ return;
538
+ }
539
+ if (method === 'error') {
540
+ diagnostics.push(`app-server error: ${JSON.stringify(params)}`);
541
+ finishRun('app_server_error');
542
+ return;
543
+ }
544
+ if (evidence.requestUserInput) {
545
+ requestUserInputCalled = true;
546
+ waitingOnUserInput = true;
547
+ finishRun('waiting_on_user_input');
548
+ return;
549
+ }
550
+ const reason = expectedEarlyStop(options.suite, options.evalCase, observed, allLoadedSkills);
551
+ if (reason)
552
+ finishRun(reason);
553
+ });
554
+ await session.initialize(control.rpcTimeout('initialize'));
555
+ control.throwIfStopped('initialize');
556
+ const threadResult = await session.sendRequest('thread/start', {
557
+ cwd: workspaceRoot,
558
+ approvalPolicy: 'never',
559
+ sandbox: 'read-only',
560
+ ephemeral: false,
561
+ ...(options.model ? { model: options.model } : {}),
562
+ }, control.rpcTimeout('thread/start'));
563
+ rootThreadId = extractAppServerThreadId(threadResult);
564
+ control.throwIfStopped('turn/start');
565
+ await session.sendRequest('turn/start', {
566
+ threadId: rootThreadId,
567
+ input: [{ type: 'text', text: options.evalCase.query, text_elements: [] }],
568
+ cwd: workspaceRoot,
569
+ approvalPolicy: 'never',
570
+ sandboxPolicy: { type: 'readOnly', networkAccess: true },
571
+ ...(options.model ? { model: options.model } : {}),
572
+ }, control.rpcTimeout('turn/start'));
573
+ const termination = await runDone;
574
+ control.throwIfStopped('turn wait');
575
+ removeHandler();
576
+ removeHandler = undefined;
577
+ removeFailureHandler();
578
+ removeFailureHandler = undefined;
579
+ let cleanupFailed = false;
580
+ try {
581
+ await session.cleanup();
582
+ }
583
+ catch (error) {
584
+ cleanupFailed = true;
585
+ diagnostics.push(`Trigger eval app-server cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
586
+ }
587
+ if (!cleanupFailed)
588
+ control.throwIfStopped('cleanup');
589
+ const afterState = await captureState(options.fixture, context);
590
+ let skillsUnchanged = true;
591
+ for (const [skillName, skillPath] of skillPaths) {
592
+ const unchanged = await fs.pathExists(skillPath) &&
593
+ await computeSkillDigest(path.dirname(skillPath)) === skillDigests.get(skillName);
594
+ if (!unchanged) {
595
+ skillsUnchanged = false;
596
+ diagnostics.push(`Materialized participant Skill "${skillName}" changed during trigger evaluation.`);
597
+ }
598
+ }
599
+ const fixtureUnchanged = stableJson(beforeState) === stableJson(afterState) && skillsUnchanged;
600
+ if (!fixtureUnchanged) {
601
+ diagnostics.push('Trigger evaluation workspace state changed.');
602
+ }
603
+ const expectedStop = new Set([
604
+ 'expected_skill_load',
605
+ 'expected_outcome_observed',
606
+ 'unexpected_skill_load',
607
+ 'unexpected_spawn',
608
+ 'unexpected_multiple_skill_loads',
609
+ 'expected_skill_route_observed',
610
+ 'waiting_on_user_input',
611
+ ]).has(termination);
612
+ const executionError = termination === 'timeout' ||
613
+ termination === 'aborted' ||
614
+ termination === 'strict_isolation_error' ||
615
+ termination === 'app_server_error' ||
616
+ (!expectedStop && termination === 'completed' && turnStatus !== 'completed') ||
617
+ !fixtureUnchanged ||
618
+ cleanupFailed;
619
+ return session.scrub({
620
+ loaded: observed.loaded,
621
+ first_loaded_skill: allLoadedSkills[0] ?? null,
622
+ all_loaded_skills: allLoadedSkills,
623
+ load_evidence_by_skill: loadEvidenceBySkill,
624
+ spawned: observed.spawned,
625
+ load_evidence: observed.loadEvidence,
626
+ spawn_evidence: observed.spawnEvidence,
627
+ spawn_calls: spawnCalls,
628
+ termination,
629
+ exit_code: null,
630
+ signal: null,
631
+ execution_error: executionError,
632
+ fixture_unchanged: fixtureUnchanged,
633
+ request_user_input_called: requestUserInputCalled,
634
+ waiting_on_user_input: waitingOnUserInput,
635
+ final_response_emitted: finalResponseEmitted,
636
+ ...(session.stderr.trim() ? { stderr: session.stderr.trim() } : {}),
637
+ final_message: finalMessage,
638
+ duration_ms: Date.now() - startedAt,
639
+ usage,
640
+ event_trace: eventTrace,
641
+ diagnostics,
642
+ });
643
+ }
644
+ catch (error) {
645
+ const evidence = {
646
+ loaded: false,
647
+ first_loaded_skill: null,
648
+ all_loaded_skills: [],
649
+ load_evidence_by_skill: {},
650
+ spawned: false,
651
+ load_evidence: null,
652
+ spawn_evidence: null,
653
+ spawn_calls: [],
654
+ termination: control.timeoutExpired ? 'timeout' : 'setup_error',
655
+ exit_code: null,
656
+ signal: null,
657
+ execution_error: true,
658
+ fixture_unchanged: false,
659
+ request_user_input_called: false,
660
+ waiting_on_user_input: false,
661
+ final_response_emitted: finalMessage !== null,
662
+ final_message: finalMessage,
663
+ duration_ms: Date.now() - startedAt,
664
+ usage,
665
+ event_trace: eventTrace,
666
+ diagnostics: [error instanceof Error ? error.stack ?? error.message : String(error)],
667
+ };
668
+ return session ? session.scrub(evidence) : evidence;
669
+ }
670
+ finally {
671
+ removeHandler?.();
672
+ removeFailureHandler?.();
673
+ removeAbortHandler?.();
674
+ try {
675
+ await session?.cleanup();
676
+ }
677
+ catch (error) {
678
+ throw new Error(`Trigger eval app-server cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
679
+ }
680
+ finally {
681
+ try {
682
+ await temporaryRoot.cleanup();
683
+ }
684
+ finally {
685
+ control.dispose();
686
+ }
687
+ }
688
+ }
689
+ }
690
+ export async function runCodexTriggerProviderCase(options) {
691
+ const fixture = await loadFixtureModule(options.fixtureRoot, options.suite);
692
+ return runTriggerOnce({
693
+ suite: options.suite,
694
+ evalCase: options.evalCase,
695
+ subjectRoot: options.subjectRoot,
696
+ fixture,
697
+ codexBin: options.codexBin,
698
+ ...(options.model ? { model: options.model } : {}),
699
+ codexRuntime: options.codexRuntime,
700
+ timeoutMs: options.timeoutMs ?? options.suite.perRunTimeoutMs,
701
+ sourceEnv: options.sourceEnv ?? process.env,
702
+ processManagement: options.processManagement ?? {
703
+ mode: 'unmanaged-confirmed',
704
+ inspectionAvailable: false,
705
+ userAcknowledged: true,
706
+ descendantCleanupGuaranteed: false,
707
+ limitation: 'The AwSkillEval engine owns the provider process tree and timeout.',
708
+ },
709
+ allowMissingSkills: true,
710
+ manageTimeout: options.manageTimeout ?? false,
711
+ ...(options.workRoot ? { workRoot: options.workRoot } : {}),
712
+ ...(options.signal ? { signal: options.signal } : {}),
713
+ });
714
+ }
715
+ function ratePasses(rate, expected, strictNegative) {
716
+ if (expected) {
717
+ return rate > PASS_THRESHOLD;
718
+ }
719
+ return strictNegative ? rate === 0 : rate < PASS_THRESHOLD;
720
+ }
721
+ export function summarizeTriggerCase(suite, evalCase, runs) {
722
+ const loadRate = runs.filter((run) => run.loaded).length / runs.length;
723
+ const skillLoadRates = Object.fromEntries(suite.skills.map((skillName) => [
724
+ skillName,
725
+ runs.filter((run) => run.all_loaded_skills.includes(skillName)).length / runs.length,
726
+ ]));
727
+ const spawnRate = runs.filter((run) => run.spawned).length / runs.length;
728
+ const executionErrors = runs.filter((run) => run.execution_error).length;
729
+ const strictNegative = suite.strictNegativeSplits.includes(evalCase.split);
730
+ const usesRouteExpectation = evalCase.expected_loaded_skills !== undefined;
731
+ const usesFirstRouteExpectation = evalCase.expected_first_loaded_skill !== undefined;
732
+ const expectedSkills = expectedLoadedSkills(suite, evalCase);
733
+ const routeMatches = runs.filter((run) => sameSkillSet(run.all_loaded_skills, expectedSkills)).length;
734
+ const routeMatchRate = routeMatches / runs.length;
735
+ const firstRouteMatches = usesFirstRouteExpectation
736
+ ? runs.filter((run) => run.first_loaded_skill === evalCase.expected_first_loaded_skill).length
737
+ : 0;
738
+ const firstRouteMatchRate = firstRouteMatches / runs.length;
739
+ const multipleSkillLoads = runs.filter((run) => run.all_loaded_skills.length > 1).length;
740
+ const loadPassed = usesFirstRouteExpectation
741
+ ? firstRouteMatchRate >= (suite.minimumPassRate ?? PASS_THRESHOLD)
742
+ : usesRouteExpectation
743
+ ? strictNegative && expectedSkills.length === 0
744
+ ? routeMatchRate === 1
745
+ : routeMatchRate >= (suite.minimumPassRate ?? PASS_THRESHOLD)
746
+ : ratePasses(loadRate, Boolean(evalCase.should_load_skill), strictNegative);
747
+ const routePassed = usesFirstRouteExpectation
748
+ || !suite.forbidMultipleSkillLoads
749
+ || multipleSkillLoads === 0;
750
+ const spawnPassed = suite.observeSpawnAgent
751
+ ? ratePasses(spawnRate, Boolean(evalCase.should_spawn_agent), strictNegative)
752
+ : true;
753
+ const complete = executionErrors === 0;
754
+ const routeCounts = Object.fromEntries([...new Set(runs.map((run) => routeLabel(run.all_loaded_skills)))].map((route) => [
755
+ route,
756
+ runs.filter((run) => routeLabel(run.all_loaded_skills) === route).length,
757
+ ]));
758
+ return {
759
+ id: evalCase.id,
760
+ split: evalCase.split,
761
+ query: evalCase.query,
762
+ expected: {
763
+ ...(usesFirstRouteExpectation
764
+ ? { first_loaded_skill: evalCase.expected_first_loaded_skill }
765
+ : usesRouteExpectation
766
+ ? { loaded_skills: expectedSkills }
767
+ : { load_skill: Boolean(evalCase.should_load_skill) }),
768
+ ...(suite.observeSpawnAgent ? { spawn_agent: Boolean(evalCase.should_spawn_agent) } : {}),
769
+ },
770
+ observed: {
771
+ load_rate: loadRate,
772
+ skill_load_rates: skillLoadRates,
773
+ ...(usesRouteExpectation ? {
774
+ route_match_rate: routeMatchRate,
775
+ route_counts: routeCounts,
776
+ } : {}),
777
+ ...(usesFirstRouteExpectation ? {
778
+ first_route_match_rate: firstRouteMatchRate,
779
+ } : {}),
780
+ multiple_skill_loads: multipleSkillLoads,
781
+ ...(suite.observeSpawnAgent ? { spawn_rate: spawnRate } : {}),
782
+ execution_errors: executionErrors,
783
+ },
784
+ complete,
785
+ passed: complete && loadPassed && routePassed && spawnPassed,
786
+ runs,
787
+ };
788
+ }
789
+ //# sourceMappingURL=codex-trigger-eval.js.map