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