@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,1790 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import fs from 'fs-extra';
5
+ import { CodexEvalAttemptControl, EvalAppServerSession, extractAppServerThreadId, } from './codex-eval-app-server.js';
6
+ import { applyCodexEvalProviderEnvironment, createCodexEvalTemporaryRoot, materializeCodexEvalRuntimeHome, redactCodexEvalValue, } from './codex-eval-runtime-config.js';
7
+ import { runProviderProcess } from './process.js';
8
+ import { assertProviderRunPaths } from './run-paths.js';
9
+ import { parseEvalProviderGrade, } from './custom-process.js';
10
+ const MAX_ADAPTER_OUTPUT_BYTES = 4 * 1024 * 1024;
11
+ const MAX_REQUEST_TEXT_BYTES = 200_000;
12
+ const MAX_ROLE_COUNT = 32;
13
+ const MAX_ROLE_DESCRIPTION_BYTES = 20_000;
14
+ const MAX_ROLLOUT_BYTES = 64 * 1024 * 1024;
15
+ const MAX_ROLLOUT_LINES = 100_000;
16
+ const MAX_V1_EXEC_INPUT_BYTES = MAX_REQUEST_TEXT_BYTES + 16_384;
17
+ const V1_CODE_MODE_SPAWN_MARKER = 'multi_agent_v1__spawn_agent';
18
+ export const CODEX_WORKFLOW_ARTIFACTS = [
19
+ { path: 'workflow-evidence.json', role: 'evidence' },
20
+ { path: 'workflow-trace.json', role: 'trace' },
21
+ { path: 'model-output.txt', role: 'output' },
22
+ ];
23
+ function isRecord(value) {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
26
+ function exactKeys(value, required, optional, label) {
27
+ for (const key of required) {
28
+ if (!Object.hasOwn(value, key))
29
+ throw new Error(`${label} is missing ${key}.`);
30
+ }
31
+ const allowed = new Set([...required, ...optional]);
32
+ const unknown = Object.keys(value).find((key) => !allowed.has(key));
33
+ if (unknown)
34
+ throw new Error(`${label} has unknown field ${unknown}.`);
35
+ }
36
+ function boundedString(value, label, maxBytes) {
37
+ if (typeof value !== 'string' || value.trim() === '') {
38
+ throw new Error(`${label} must be a non-empty string.`);
39
+ }
40
+ if (Buffer.byteLength(value) > maxBytes)
41
+ throw new Error(`${label} is too large.`);
42
+ return value;
43
+ }
44
+ function relativeWorkspacePath(value, label) {
45
+ const result = boundedString(value, label, 4_096);
46
+ if (result.includes('\\')
47
+ || result.includes('\0')
48
+ || path.posix.isAbsolute(result)
49
+ || path.win32.isAbsolute(result)
50
+ || path.posix.normalize(result) !== result
51
+ || result.split('/').some((segment) => segment === '' || segment === '.' || segment === '..'))
52
+ throw new Error(`${label} must be a normalized workspace-relative POSIX path.`);
53
+ return result;
54
+ }
55
+ async function assertWorkspaceRoleFile(workspace, relative) {
56
+ let current = workspace;
57
+ for (const segment of relative.split('/')) {
58
+ current = path.join(current, segment);
59
+ const info = await fs.lstat(current).catch(() => null);
60
+ if (!info)
61
+ throw new Error(`workflow role configFile does not exist: ${relative}`);
62
+ if (info.isSymbolicLink()) {
63
+ throw new Error(`workflow role configFile must not contain symbolic links: ${relative}`);
64
+ }
65
+ }
66
+ if (!(await fs.lstat(current)).isFile()) {
67
+ throw new Error(`workflow role configFile must be a regular file: ${relative}`);
68
+ }
69
+ }
70
+ export async function parseCodexWorkflowRequest(value, allowedModels, workspace) {
71
+ if (!isRecord(value))
72
+ throw new Error('workflow prepare stdout must be one JSON object.');
73
+ exactKeys(value, ['version', 'prompt', 'sandbox', 'model', 'developerInstructions', 'roles', 'multiAgent'], [], 'workflow prepare stdout');
74
+ if (value.version !== 1)
75
+ throw new Error('workflow prepare stdout version must be 1.');
76
+ if (value.sandbox !== 'read-only' && value.sandbox !== 'workspace-write') {
77
+ throw new Error('workflow request sandbox must be read-only or workspace-write.');
78
+ }
79
+ if (value.model !== null && (typeof value.model !== 'string' || !allowedModels.includes(value.model))) {
80
+ throw new Error('workflow request model must be null or a member of provider.allowedModels.');
81
+ }
82
+ if (!Array.isArray(value.roles) || value.roles.length > MAX_ROLE_COUNT) {
83
+ throw new Error(`workflow request roles must be an array with at most ${MAX_ROLE_COUNT} entries.`);
84
+ }
85
+ const roles = await Promise.all(value.roles.map(async (entry, index) => {
86
+ if (!isRecord(entry))
87
+ throw new Error(`workflow request roles[${index}] must be an object.`);
88
+ exactKeys(entry, ['name', 'description', 'configFile'], [], `workflow request roles[${index}]`);
89
+ const name = boundedString(entry.name, `workflow request roles[${index}].name`, 256);
90
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) {
91
+ throw new Error(`workflow request roles[${index}].name must be a safe role name.`);
92
+ }
93
+ const configFile = relativeWorkspacePath(entry.configFile, `workflow request roles[${index}].configFile`);
94
+ await assertWorkspaceRoleFile(workspace, configFile);
95
+ return {
96
+ name,
97
+ description: boundedString(entry.description, `workflow request roles[${index}].description`, MAX_ROLE_DESCRIPTION_BYTES),
98
+ configFile,
99
+ };
100
+ }));
101
+ if (new Set(roles.map((role) => role.name)).size !== roles.length) {
102
+ throw new Error('workflow request roles must contain unique names.');
103
+ }
104
+ if (!isRecord(value.multiAgent))
105
+ throw new Error('workflow request multiAgent must be an object.');
106
+ exactKeys(value.multiAgent, ['protocol', 'maxDepth', 'hideSpawnAgentMetadata', 'exposeSpawnAgentModelOverrides'], [], 'workflow request multiAgent');
107
+ const multiAgent = value.multiAgent;
108
+ if (multiAgent.protocol !== 'v1' && multiAgent.protocol !== 'v2') {
109
+ throw new Error('workflow request multiAgent.protocol must be v1 or v2.');
110
+ }
111
+ if (!Number.isSafeInteger(multiAgent.maxDepth) || multiAgent.maxDepth < 1
112
+ || multiAgent.maxDepth > 4) {
113
+ throw new Error('workflow request multiAgent.maxDepth must be an integer from 1 to 4.');
114
+ }
115
+ if (typeof multiAgent.hideSpawnAgentMetadata !== 'boolean'
116
+ || typeof multiAgent.exposeSpawnAgentModelOverrides !== 'boolean')
117
+ throw new Error('workflow request multiAgent metadata fields must be boolean.');
118
+ if (multiAgent.protocol === 'v1'
119
+ && (multiAgent.hideSpawnAgentMetadata || multiAgent.exposeSpawnAgentModelOverrides))
120
+ throw new Error('workflow request V1 metadata fields must be false.');
121
+ return {
122
+ version: 1,
123
+ prompt: boundedString(value.prompt, 'workflow request prompt', MAX_REQUEST_TEXT_BYTES),
124
+ sandbox: value.sandbox,
125
+ model: value.model,
126
+ developerInstructions: boundedString(value.developerInstructions, 'workflow request developerInstructions', MAX_REQUEST_TEXT_BYTES),
127
+ roles,
128
+ multiAgent: {
129
+ protocol: multiAgent.protocol,
130
+ maxDepth: multiAgent.maxDepth,
131
+ hideSpawnAgentMetadata: multiAgent.hideSpawnAgentMetadata,
132
+ exposeSpawnAgentModelOverrides: multiAgent.exposeSpawnAgentModelOverrides,
133
+ },
134
+ };
135
+ }
136
+ function parseJsonObject(stdout, label) {
137
+ let value;
138
+ try {
139
+ value = JSON.parse(stdout);
140
+ }
141
+ catch (error) {
142
+ throw new Error(`${label} must be one JSON object: ${error.message}`);
143
+ }
144
+ if (!isRecord(value))
145
+ throw new Error(`${label} must be one JSON object.`);
146
+ return value;
147
+ }
148
+ export function codexWorkflowAdapterEnvironment(sourceEnv, evalRoot, phase) {
149
+ const env = {};
150
+ for (const name of ['PATH', 'LANG', 'LC_ALL']) {
151
+ if (sourceEnv[name] !== undefined)
152
+ env[name] = sourceEnv[name];
153
+ }
154
+ const home = path.join(evalRoot, `.workflow-adapter-${phase}-home`);
155
+ const temporary = path.join(evalRoot, `.workflow-adapter-${phase}-tmp`);
156
+ return {
157
+ ...env,
158
+ HOME: home,
159
+ USERPROFILE: home,
160
+ XDG_CACHE_HOME: path.join(home, '.cache'),
161
+ XDG_CONFIG_HOME: path.join(home, '.config'),
162
+ XDG_STATE_HOME: path.join(home, '.state'),
163
+ TMPDIR: temporary,
164
+ TEMP: temporary,
165
+ TMP: temporary,
166
+ NO_COLOR: '1',
167
+ };
168
+ }
169
+ async function runAdapter(options) {
170
+ const env = codexWorkflowAdapterEnvironment(options.sourceEnv, options.evalRoot, options.input.phase);
171
+ await Promise.all([fs.mkdir(env.HOME, { mode: 0o700 }), fs.mkdir(env.TMPDIR, { mode: 0o700 })]);
172
+ try {
173
+ const result = await runProviderProcess({
174
+ executable: options.command[0],
175
+ args: [...options.command.slice(1)],
176
+ cwd: options.evalRoot,
177
+ env,
178
+ stdin: JSON.stringify(options.input),
179
+ timeoutMs: options.timeoutMs,
180
+ maxBufferBytes: MAX_ADAPTER_OUTPUT_BYTES,
181
+ ...(options.signal ? { signal: options.signal } : {}),
182
+ }, {}, {
183
+ containment: 'verified-observed',
184
+ processTree: options.processTree,
185
+ });
186
+ if (result.timedOut)
187
+ throw new Error(`workflow ${options.input.phase} timed out.`);
188
+ if (result.cancelled)
189
+ throw new Error(`workflow ${options.input.phase} was aborted.`);
190
+ if (result.maxBufferExceeded)
191
+ throw new Error(`workflow ${options.input.phase} output is too large.`);
192
+ if (!result.gradingEvidenceAccepted) {
193
+ throw new Error(`workflow ${options.input.phase} process containment was not verified.`);
194
+ }
195
+ if (result.signal || result.exitCode !== 0) {
196
+ const diagnostic = result.stderr.trim();
197
+ throw new Error(`workflow ${options.input.phase} failed (exit=${String(result.exitCode)}, `
198
+ + `signal=${String(result.signal)})${diagnostic ? `: ${diagnostic}` : '.'}`);
199
+ }
200
+ return parseJsonObject(result.stdout, `workflow ${options.input.phase} stdout`);
201
+ }
202
+ finally {
203
+ const cleanup = await Promise.allSettled([fs.remove(env.HOME), fs.remove(env.TMPDIR)]);
204
+ if (cleanup.some((result) => result.status === 'rejected')) {
205
+ throw new Error(`workflow ${options.input.phase} could not remove isolated adapter state.`);
206
+ }
207
+ }
208
+ }
209
+ async function snapshotWorkspace(root) {
210
+ const snapshot = new Map();
211
+ async function visit(current) {
212
+ const entries = await fs.readdir(current, { withFileTypes: true });
213
+ for (const entry of entries) {
214
+ const absolute = path.join(current, entry.name);
215
+ const relative = path.relative(root, absolute).split(path.sep).join('/');
216
+ if (entry.isDirectory()) {
217
+ snapshot.set(`${relative}/`, { kind: 'directory', digest: 'directory' });
218
+ await visit(absolute);
219
+ }
220
+ else if (entry.isFile()) {
221
+ snapshot.set(relative, {
222
+ kind: 'file',
223
+ digest: createHash('sha256').update(await fs.readFile(absolute)).digest('hex'),
224
+ });
225
+ }
226
+ else if (entry.isSymbolicLink()) {
227
+ snapshot.set(relative, { kind: 'symlink', digest: await fs.readlink(absolute) });
228
+ }
229
+ else
230
+ snapshot.set(relative, { kind: 'other', digest: 'other' });
231
+ }
232
+ }
233
+ await visit(root);
234
+ return snapshot;
235
+ }
236
+ function workspaceDiff(before, after) {
237
+ const added = [];
238
+ const removed = [];
239
+ const modified = [];
240
+ const paths = [...new Set([...before.keys(), ...after.keys()])].sort();
241
+ for (const entry of paths) {
242
+ if (!before.has(entry))
243
+ added.push(entry);
244
+ else if (!after.has(entry))
245
+ removed.push(entry);
246
+ else if (JSON.stringify(before.get(entry)) !== JSON.stringify(after.get(entry)))
247
+ modified.push(entry);
248
+ }
249
+ return { added, removed, modified };
250
+ }
251
+ function buildEngineEnvironment(sourceEnv, root, codexHome, runtime) {
252
+ const env = {};
253
+ for (const name of [
254
+ 'PATH', 'LANG', 'LC_ALL', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
255
+ 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
256
+ ]) {
257
+ if (sourceEnv[name] !== undefined)
258
+ env[name] = sourceEnv[name];
259
+ }
260
+ const home = path.join(root, 'home');
261
+ const temporary = path.join(root, 'tmp');
262
+ return applyCodexEvalProviderEnvironment({
263
+ ...env,
264
+ HOME: home,
265
+ USERPROFILE: home,
266
+ CODEX_HOME: codexHome,
267
+ XDG_CACHE_HOME: path.join(root, 'xdg-cache'),
268
+ XDG_CONFIG_HOME: path.join(root, 'xdg-config'),
269
+ XDG_STATE_HOME: path.join(root, 'xdg-state'),
270
+ TMPDIR: temporary,
271
+ TEMP: temporary,
272
+ TMP: temporary,
273
+ NO_COLOR: '1',
274
+ GIT_OPTIONAL_LOCKS: '0',
275
+ }, runtime);
276
+ }
277
+ function configValue(value) {
278
+ return JSON.stringify(value);
279
+ }
280
+ function workflowConfig(request, workspace) {
281
+ const v2 = request.multiAgent.protocol === 'v2';
282
+ return [
283
+ 'features.multi_agent=true',
284
+ 'features.executed_tool_call_metadata=true',
285
+ `features.multi_agent_v2.enabled=${String(v2)}`,
286
+ `features.multi_agent_v2.hide_spawn_agent_metadata=${String(request.multiAgent.hideSpawnAgentMetadata)}`,
287
+ `features.multi_agent_v2.expose_spawn_agent_model_overrides=${String(request.multiAgent.exposeSpawnAgentModelOverrides)}`,
288
+ `agents.max_depth=${request.multiAgent.maxDepth}`,
289
+ `developer_instructions=${configValue(request.developerInstructions)}`,
290
+ ...request.roles.flatMap((role) => [
291
+ `agents.${role.name}.description=${configValue(role.description)}`,
292
+ `agents.${role.name}.config_file=${configValue(path.join(workspace, role.configFile))}`,
293
+ ]),
294
+ ];
295
+ }
296
+ export function codexWorkflowFrameThreadId(frame) {
297
+ const params = isRecord(frame.params) ? frame.params : {};
298
+ if (typeof params.threadId === 'string')
299
+ return params.threadId;
300
+ for (const key of ['thread', 'turn', 'item']) {
301
+ const nested = isRecord(params[key]) ? params[key] : {};
302
+ if (typeof nested.threadId === 'string')
303
+ return nested.threadId;
304
+ if (key === 'thread' && typeof nested.id === 'string')
305
+ return nested.id;
306
+ }
307
+ return null;
308
+ }
309
+ export function codexWorkflowFrameIsForRoot(frame, rootThreadId) {
310
+ return codexWorkflowFrameThreadId(frame) === rootThreadId;
311
+ }
312
+ export function codexWorkflowMissingFinalMessageDiagnostic(options) {
313
+ let rootFinalAgentMessageCount = 0;
314
+ let otherThreadFinalAgentMessageCount = 0;
315
+ for (const value of options.frames) {
316
+ if (!isRecord(value) || value.method !== 'item/completed')
317
+ continue;
318
+ const params = isRecord(value.params) ? value.params : {};
319
+ const item = isRecord(params.item) ? params.item : null;
320
+ if (!item || item.type !== 'agentMessage' || item.phase !== 'final_answer')
321
+ continue;
322
+ const threadId = codexWorkflowFrameThreadId(value);
323
+ if (threadId === options.rootThreadId)
324
+ rootFinalAgentMessageCount += 1;
325
+ else if (threadId !== null)
326
+ otherThreadFinalAgentMessageCount += 1;
327
+ }
328
+ return {
329
+ rootTurnStatus: options.turnStatus,
330
+ rootFinalAgentMessageCount,
331
+ otherThreadFinalAgentMessageCount,
332
+ usagePresent: options.usage !== null && options.usage !== undefined,
333
+ frameCount: options.frames.length,
334
+ };
335
+ }
336
+ function hash(value) {
337
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
338
+ }
339
+ function parseSerialized(value) {
340
+ if (typeof value !== 'string')
341
+ return value;
342
+ try {
343
+ return JSON.parse(value);
344
+ }
345
+ catch {
346
+ return value;
347
+ }
348
+ }
349
+ function skipWhitespace(source, start) {
350
+ let index = start;
351
+ while (/\s/u.test(source[index] ?? ''))
352
+ index += 1;
353
+ return index;
354
+ }
355
+ function parseJsonStringToken(source, start) {
356
+ if (source[start] !== '"')
357
+ throw new Error('Codex V1 code-mode spawn object has an invalid string.');
358
+ let escaped = false;
359
+ for (let index = start + 1; index < source.length; index += 1) {
360
+ const character = source[index];
361
+ if (escaped) {
362
+ escaped = false;
363
+ continue;
364
+ }
365
+ if (character === '\\') {
366
+ escaped = true;
367
+ continue;
368
+ }
369
+ if (character !== '"')
370
+ continue;
371
+ let value;
372
+ try {
373
+ value = JSON.parse(source.slice(start, index + 1));
374
+ }
375
+ catch {
376
+ throw new Error('Codex V1 code-mode spawn object has an invalid string.');
377
+ }
378
+ if (typeof value !== 'string') {
379
+ throw new Error('Codex V1 code-mode spawn object has an invalid string.');
380
+ }
381
+ return { value, end: index + 1 };
382
+ }
383
+ throw new Error('Codex V1 code-mode spawn object has an unterminated string.');
384
+ }
385
+ function parseTemplateLiteralToken(source, start) {
386
+ if (source[start] !== '`') {
387
+ throw new Error('Codex V1 code-mode spawn object has an invalid template literal.');
388
+ }
389
+ let value = '';
390
+ for (let index = start + 1; index < source.length; index += 1) {
391
+ const character = source[index];
392
+ if (character === '`')
393
+ return { value, end: index + 1 };
394
+ if (character === '$' && source[index + 1] === '{') {
395
+ throw new Error('Codex V1 code-mode spawn template literal interpolation is unsupported.');
396
+ }
397
+ if (character === '\r') {
398
+ if (source[index + 1] === '\n')
399
+ index += 1;
400
+ value += '\n';
401
+ continue;
402
+ }
403
+ if (character !== '\\') {
404
+ value += character;
405
+ continue;
406
+ }
407
+ const escaped = source[index + 1];
408
+ if (escaped === undefined) {
409
+ throw new Error('Codex V1 code-mode spawn object has an unterminated template literal.');
410
+ }
411
+ index += 1;
412
+ if (escaped === '\n')
413
+ continue;
414
+ if (escaped === '\r') {
415
+ if (source[index + 1] === '\n')
416
+ index += 1;
417
+ continue;
418
+ }
419
+ if (escaped === '\u2028' || escaped === '\u2029')
420
+ continue;
421
+ const commonEscapes = {
422
+ b: '\b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v',
423
+ '0': '\0', '\\': '\\', '`': '`',
424
+ };
425
+ if (Object.hasOwn(commonEscapes, escaped)) {
426
+ if (escaped === '0' && /[0-9]/u.test(source[index + 1] ?? '')) {
427
+ throw new Error('Codex V1 code-mode spawn template literal has an invalid numeric escape.');
428
+ }
429
+ value += commonEscapes[escaped];
430
+ continue;
431
+ }
432
+ if (escaped === 'x') {
433
+ const digits = source.slice(index + 1, index + 3);
434
+ if (!/^[0-9A-Fa-f]{2}$/u.test(digits)) {
435
+ throw new Error('Codex V1 code-mode spawn template literal has an invalid hex escape.');
436
+ }
437
+ value += String.fromCharCode(Number.parseInt(digits, 16));
438
+ index += 2;
439
+ continue;
440
+ }
441
+ if (escaped === 'u') {
442
+ if (source[index + 1] === '{') {
443
+ const close = source.indexOf('}', index + 2);
444
+ const digits = close === -1 ? '' : source.slice(index + 2, close);
445
+ const codePoint = /^[0-9A-Fa-f]{1,6}$/u.test(digits)
446
+ ? Number.parseInt(digits, 16)
447
+ : Number.NaN;
448
+ if (!Number.isInteger(codePoint) || codePoint > 0x10FFFF) {
449
+ throw new Error('Codex V1 code-mode spawn template literal has an invalid Unicode escape.');
450
+ }
451
+ value += String.fromCodePoint(codePoint);
452
+ index = close;
453
+ continue;
454
+ }
455
+ const digits = source.slice(index + 1, index + 5);
456
+ if (!/^[0-9A-Fa-f]{4}$/u.test(digits)) {
457
+ throw new Error('Codex V1 code-mode spawn template literal has an invalid Unicode escape.');
458
+ }
459
+ value += String.fromCharCode(Number.parseInt(digits, 16));
460
+ index += 4;
461
+ continue;
462
+ }
463
+ if (/[1-9]/u.test(escaped)) {
464
+ throw new Error('Codex V1 code-mode spawn template literal has an invalid numeric escape.');
465
+ }
466
+ value += escaped;
467
+ }
468
+ throw new Error('Codex V1 code-mode spawn object has an unterminated template literal.');
469
+ }
470
+ function parseV1SpawnObject(source, start) {
471
+ let index = skipWhitespace(source, start);
472
+ if (source[index] !== '{')
473
+ throw new Error('Codex V1 code-mode spawn arguments must be an object.');
474
+ index += 1;
475
+ const value = {};
476
+ while (true) {
477
+ index = skipWhitespace(source, index);
478
+ if (source[index] === '}')
479
+ return { value, end: index + 1 };
480
+ let key;
481
+ if (source[index] === '"') {
482
+ const parsed = parseJsonStringToken(source, index);
483
+ key = parsed.value;
484
+ index = parsed.end;
485
+ }
486
+ else {
487
+ const match = source.slice(index).match(/^[A-Za-z_$][A-Za-z0-9_$]*/u);
488
+ if (!match)
489
+ throw new Error('Codex V1 code-mode spawn object has an invalid key.');
490
+ key = match[0];
491
+ index += key.length;
492
+ }
493
+ if (Object.hasOwn(value, key)) {
494
+ throw new Error('Codex V1 code-mode spawn object has a duplicate key.');
495
+ }
496
+ index = skipWhitespace(source, index);
497
+ if (source[index] !== ':')
498
+ throw new Error('Codex V1 code-mode spawn object is missing a colon.');
499
+ index = skipWhitespace(source, index + 1);
500
+ if (source[index] === '"') {
501
+ const parsed = parseJsonStringToken(source, index);
502
+ value[key] = parsed.value;
503
+ index = parsed.end;
504
+ }
505
+ else if (source[index] === '`') {
506
+ const parsed = parseTemplateLiteralToken(source, index);
507
+ value[key] = parsed.value;
508
+ index = parsed.end;
509
+ }
510
+ else if (source.startsWith('true', index)) {
511
+ value[key] = true;
512
+ index += 4;
513
+ }
514
+ else if (source.startsWith('false', index)) {
515
+ value[key] = false;
516
+ index += 5;
517
+ }
518
+ else {
519
+ throw new Error('Codex V1 code-mode spawn object has an unsupported value.');
520
+ }
521
+ index = skipWhitespace(source, index);
522
+ if (source[index] === '}')
523
+ return { value, end: index + 1 };
524
+ if (source[index] !== ',')
525
+ throw new Error('Codex V1 code-mode spawn object is missing a comma.');
526
+ index = skipWhitespace(source, index + 1);
527
+ if (source[index] === '}')
528
+ return { value, end: index + 1 };
529
+ }
530
+ }
531
+ function canonicalV1SpawnArguments(value) {
532
+ if (!isRecord(value))
533
+ throw new Error('Codex V1 spawn arguments must be an object.');
534
+ const allowed = new Set(['agent_type', 'message', 'fork_context', 'model', 'reasoning_effort']);
535
+ const unknown = Object.keys(value).find((key) => !allowed.has(key));
536
+ if (unknown)
537
+ throw new Error(`Codex V1 spawn arguments have unknown field ${unknown}.`);
538
+ const agentType = Object.hasOwn(value, 'agent_type')
539
+ ? boundedString(value.agent_type, 'Codex V1 spawn agent_type', 256)
540
+ : undefined;
541
+ const message = boundedString(value.message, 'Codex V1 spawn message', MAX_REQUEST_TEXT_BYTES);
542
+ if (typeof value.fork_context !== 'boolean') {
543
+ throw new Error('Codex V1 spawn fork_context evidence must be boolean.');
544
+ }
545
+ const model = value.model === undefined
546
+ ? undefined
547
+ : boundedString(value.model, 'Codex V1 spawn model', 256);
548
+ const reasoningEffort = value.reasoning_effort === undefined
549
+ ? undefined
550
+ : boundedString(value.reasoning_effort, 'Codex V1 spawn reasoning_effort', 256);
551
+ return {
552
+ ...(agentType === undefined ? {} : { agent_type: agentType }),
553
+ message,
554
+ ...(model === undefined ? {} : { model }),
555
+ ...(reasoningEffort === undefined ? {} : { reasoning_effort: reasoningEffort }),
556
+ fork_context: value.fork_context,
557
+ };
558
+ }
559
+ function v1PrefixWrapperFingerprint(source) {
560
+ const pragma = /^\s*\/\/\s*@exec\b/u.test(source);
561
+ const withoutPragma = source.replace(/^\s*\/\/\s*@exec\b[^\r\n]*(?:\r?\n|$)/u, '');
562
+ const codeFence = /^\s*```[^\r\n]*(?:\r?\n|$)/u.test(withoutPragma);
563
+ const structuralSource = withoutPragma.replace(/^\s*```[^\r\n]*(?:\r?\n|$)/u, '');
564
+ const declaration = structuralSource.match(/^\s*(const|let|var)\b/u)?.[1];
565
+ const declarationPrefix = /^\s*(?:const|let|var)\s+/u;
566
+ const bindingSource = structuralSource.replace(declarationPrefix, '');
567
+ const bindingForm = /^[A-Za-z_$][A-Za-z0-9_$]*\s*=/u.test(bindingSource)
568
+ ? 'identifier'
569
+ : /^\{[^}\r\n]*\}\s*=/u.test(bindingSource)
570
+ ? 'object-destructure'
571
+ : /^\[[^\]\r\n]*\]\s*=/u.test(bindingSource)
572
+ ? 'array-destructure'
573
+ : 'other';
574
+ const assignmentAwait = /^\s*(?:const|let|var)\s+[^\r\n]*?=\s*await\b/u;
575
+ const directTextAwait = /^\s*text\s*\(\s*await\s+tools\.multi_agent_v1__spawn_agent\s*\(/u;
576
+ const callee = structuralSource.match(/^\s*(?:(?:const|let|var)\s+[^\r\n]*?=\s*|text\s*\(\s*)(?:await\s+)?([A-Za-z_$][A-Za-z0-9_$]*)\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/u);
577
+ const receiver = callee?.[1] === 'tools' ? 'tools' : 'other';
578
+ const calleeName = callee?.[2];
579
+ const calleeFamily = calleeName === 'multi_agent_v1__spawn_agent'
580
+ ? 'exact-v1-snake'
581
+ : calleeName?.startsWith('multi_agent_v1__') === true
582
+ ? 'v1-namespaced-other'
583
+ : calleeName !== undefined && /(?:spawn_agent|spawnAgent)$/u.test(calleeName)
584
+ ? 'spawn-family-other'
585
+ : 'other';
586
+ return {
587
+ phase: 'prefix',
588
+ pragma: pragma ? 'yes' : 'no',
589
+ codeFence: codeFence ? 'yes' : 'no',
590
+ declaration: declaration === 'const' || declaration === 'let' || declaration === 'var'
591
+ ? declaration
592
+ : 'none',
593
+ bindingForm,
594
+ await: assignmentAwait.test(structuralSource) || directTextAwait.test(structuralSource)
595
+ ? 'yes'
596
+ : 'no',
597
+ receiver,
598
+ calleeFamily,
599
+ exactSpawnCallee: receiver === 'tools' && calleeFamily === 'exact-v1-snake' ? 'yes' : 'no',
600
+ outputForm: directTextAwait.test(structuralSource) ? 'direct-text-await' : 'other',
601
+ };
602
+ }
603
+ function v1SuffixWrapperFingerprint(source, variable) {
604
+ const escapedVariable = variable.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
605
+ const textOutput = new RegExp(`^\\s*\\)\\s*;?\\s*text\\s*\\(\\s*${escapedVariable}\\s*\\)\\s*;?`, 'u');
606
+ const returnTextOutput = new RegExp(`^\\s*\\)\\s*;?\\s*return\\s+text\\s*\\(\\s*${escapedVariable}\\s*\\)\\s*;?`, 'u');
607
+ const output = source.match(textOutput) ?? source.match(returnTextOutput);
608
+ return {
609
+ phase: 'suffix',
610
+ spawnCallSemicolon: /^\s*\)\s*;/u.test(source) ? 'yes' : 'no',
611
+ outputForm: textOutput.test(source)
612
+ ? 'text'
613
+ : returnTextOutput.test(source) ? 'return-text' : 'other',
614
+ extraStatements: output !== null && source.slice(output[0].length).trim() !== '' ? 'yes' : 'no',
615
+ };
616
+ }
617
+ function invalidV1Wrapper(fingerprint) {
618
+ return new Error(`Codex V1 code-mode spawn ${fingerprint.phase} is invalid. `
619
+ + `fingerprint=${JSON.stringify(fingerprint)}`);
620
+ }
621
+ function parseV1CodeModeSpawnInput(value) {
622
+ if (typeof value !== 'string' || Buffer.byteLength(value) > MAX_V1_EXEC_INPUT_BYTES) {
623
+ throw new Error('Codex V1 code-mode spawn input must be a bounded string.');
624
+ }
625
+ const prefix = value.match(/^\s*const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:await\s+)?tools\.multi_agent_v1__spawn_agent\s*\(\s*/u);
626
+ if (!prefix)
627
+ throw invalidV1Wrapper(v1PrefixWrapperFingerprint(value));
628
+ const parsed = parseV1SpawnObject(value, prefix[0].length);
629
+ const variable = prefix[1];
630
+ const suffix = value.slice(parsed.end);
631
+ const suffixPattern = new RegExp(`^\\s*\\)\\s*;\\s*text\\s*\\(\\s*${variable}\\s*\\)\\s*;?\\s*$`, 'u');
632
+ if (!suffixPattern.test(suffix)) {
633
+ throw invalidV1Wrapper(v1SuffixWrapperFingerprint(suffix, variable));
634
+ }
635
+ return canonicalV1SpawnArguments(parsed.value);
636
+ }
637
+ function isV1CodeModeExecCall(call) {
638
+ return call?.tool === 'exec'
639
+ && isRecord(call.arguments)
640
+ && typeof call.arguments.input === 'string'
641
+ && call.arguments.input.includes(V1_CODE_MODE_SPAWN_MARKER);
642
+ }
643
+ function responseItem(event) {
644
+ return event.type === 'response_item' && isRecord(event.payload) ? event.payload : null;
645
+ }
646
+ function contentText(payload) {
647
+ if (!Array.isArray(payload.content))
648
+ return '';
649
+ return payload.content.filter(isRecord)
650
+ .filter((entry) => entry.type === 'input_text' && typeof entry.text === 'string')
651
+ .map((entry) => entry.text)
652
+ .join('\n');
653
+ }
654
+ function encryptedContent(payload) {
655
+ if (!Array.isArray(payload.content))
656
+ return null;
657
+ const content = payload.content.filter(isRecord);
658
+ const envelope = content.find((item) => item.type === 'input_text' && typeof item.text === 'string');
659
+ if (!String(envelope?.text ?? '').includes('Message Type: NEW_TASK'))
660
+ return null;
661
+ const entry = content.find((item) => item.type === 'encrypted_content' && typeof item.encrypted_content === 'string');
662
+ return entry ? entry.encrypted_content : null;
663
+ }
664
+ function agentMessagePayload(text) {
665
+ const marker = text.match(/^Payload:\s*$/mu);
666
+ if (!marker || !Number.isInteger(marker.index))
667
+ return null;
668
+ return text
669
+ .slice(marker.index + marker[0].length)
670
+ .replace(/^\r?\n/u, '')
671
+ .trim() || null;
672
+ }
673
+ function sanitizeTraceValue(value, runtime, isolatedPaths, redactSpawnMessages) {
674
+ if (typeof value === 'string') {
675
+ const redactedPath = isolatedPaths
676
+ .filter((entry) => entry.path !== '')
677
+ .sort((left, right) => right.path.length - left.path.length)
678
+ .reduce((text, entry) => text.replaceAll(entry.path, entry.marker), value);
679
+ return redactCodexEvalValue(redactedPath, runtime);
680
+ }
681
+ if (Array.isArray(value)) {
682
+ return value.map((entry) => sanitizeTraceValue(entry, runtime, isolatedPaths, redactSpawnMessages));
683
+ }
684
+ if (!isRecord(value))
685
+ return value;
686
+ if (value.type === 'custom_tool_call'
687
+ && value.name === 'exec'
688
+ && typeof value.input === 'string'
689
+ && value.input.includes(V1_CODE_MODE_SPAWN_MARKER)) {
690
+ let input = '[redacted-v1-code-wrapper]';
691
+ try {
692
+ input = {
693
+ namespace: 'multi_agent_v1',
694
+ tool: 'spawn_agent',
695
+ arguments: parseV1CodeModeSpawnInput(value.input),
696
+ };
697
+ }
698
+ catch {
699
+ // A linked terminal event is validated separately. An unlinked malformed wrapper stays private.
700
+ }
701
+ return sanitizeTraceValue({ ...value, input }, runtime, isolatedPaths, redactSpawnMessages);
702
+ }
703
+ if (value.type === 'message' && value.role === 'developer') {
704
+ const developerText = contentText(value);
705
+ return {
706
+ type: 'message',
707
+ role: 'developer',
708
+ developerContext: {
709
+ present: true,
710
+ sha256: hash(developerText),
711
+ bytes: Buffer.byteLength(developerText),
712
+ },
713
+ };
714
+ }
715
+ if (typeof value.name === 'string'
716
+ && normalizeToolName(value.name).tool === 'spawn_agent'
717
+ && redactSpawnMessages) {
718
+ const argumentsValue = parseSerialized(value.arguments ?? value.input);
719
+ if (isRecord(argumentsValue) && typeof argumentsValue.message === 'string') {
720
+ const argumentKey = value.arguments === undefined ? 'input' : 'arguments';
721
+ return sanitizeTraceValue({
722
+ ...value,
723
+ [argumentKey]: {
724
+ ...argumentsValue,
725
+ message: { kind: 'opaque', sha256: hash(argumentsValue.message) },
726
+ },
727
+ }, runtime, isolatedPaths, redactSpawnMessages);
728
+ }
729
+ }
730
+ const result = {};
731
+ for (const [key, entry] of Object.entries(value)) {
732
+ if (key === 'agentPath' || key === 'agent_path' || key === 'sender_agent_path')
733
+ continue;
734
+ if (key === 'encrypted_content' && typeof entry === 'string') {
735
+ result[key] = { redacted: true, sha256: hash(entry) };
736
+ continue;
737
+ }
738
+ if (/^(?:authorization|credentials?|api[_-]?key|access[_-]?token|refresh[_-]?token|secret|ciphertext)$/iu.test(key)) {
739
+ result[key] = '[redacted]';
740
+ continue;
741
+ }
742
+ result[key] = sanitizeTraceValue(entry, runtime, isolatedPaths, redactSpawnMessages);
743
+ }
744
+ return result;
745
+ }
746
+ async function listRollouts(root) {
747
+ const result = [];
748
+ async function visit(current) {
749
+ for (const entry of await fs.readdir(current, { withFileTypes: true }).catch((error) => {
750
+ if (error.code === 'ENOENT')
751
+ return [];
752
+ throw error;
753
+ })) {
754
+ const target = path.join(current, entry.name);
755
+ if (entry.isDirectory())
756
+ await visit(target);
757
+ else if (entry.isFile() && target.endsWith('.jsonl'))
758
+ result.push(target);
759
+ else if (entry.isSymbolicLink())
760
+ throw new Error('Codex rollout tree contains a symbolic link.');
761
+ }
762
+ }
763
+ await visit(path.join(root, 'sessions'));
764
+ return result.sort();
765
+ }
766
+ function rolloutIdentity(events) {
767
+ for (const event of events) {
768
+ if (event.type !== 'session_meta' || !isRecord(event.payload))
769
+ continue;
770
+ const payload = event.payload;
771
+ if (typeof payload.id !== 'string' || payload.id === '') {
772
+ throw new Error('Codex rollout session_meta is missing its thread id.');
773
+ }
774
+ const source = isRecord(payload.source) ? payload.source : {};
775
+ const subagent = isRecord(source.subagent) ? source.subagent : {};
776
+ const spawn = isRecord(subagent.thread_spawn) ? subagent.thread_spawn : {};
777
+ return {
778
+ threadId: payload.id,
779
+ parentThreadId: typeof spawn.parent_thread_id === 'string' ? spawn.parent_thread_id : null,
780
+ agentPath: typeof spawn.agent_path === 'string' ? spawn.agent_path : null,
781
+ agentRole: typeof spawn.agent_role === 'string' ? spawn.agent_role : null,
782
+ };
783
+ }
784
+ throw new Error('Codex rollout is missing session_meta.');
785
+ }
786
+ function eventTimestamp(event, file, line) {
787
+ return `${typeof event.timestamp === 'string' ? event.timestamp : ''}\0${file}\0${String(line).padStart(8, '0')}`;
788
+ }
789
+ function callFromPayload(options) {
790
+ const { payload, identity } = options;
791
+ if (payload.type !== 'function_call' && payload.type !== 'custom_tool_call')
792
+ return null;
793
+ if (typeof payload.name !== 'string')
794
+ return null;
795
+ const argumentsValue = payload.type === 'custom_tool_call'
796
+ && payload.name === 'exec'
797
+ && typeof payload.input === 'string'
798
+ ? { input: payload.input }
799
+ : parseSerialized(payload.arguments ?? payload.input ?? null);
800
+ return {
801
+ threadId: identity.threadId,
802
+ parentThreadId: identity.parentThreadId,
803
+ callId: typeof payload.call_id === 'string' ? payload.call_id : null,
804
+ namespace: typeof payload.namespace === 'string' ? payload.namespace : null,
805
+ tool: payload.name,
806
+ arguments: argumentsValue,
807
+ output: null,
808
+ status: null,
809
+ order: eventTimestamp(options.event, options.file, options.line),
810
+ };
811
+ }
812
+ function agentMessageCall(options) {
813
+ if (options.payload.type !== 'agent_message')
814
+ return null;
815
+ const text = contentText(options.payload);
816
+ const messageType = text.match(/^Message Type:\s*(MESSAGE|FINAL_ANSWER)\s*$/mu)?.[1];
817
+ if (!messageType)
818
+ return null;
819
+ return {
820
+ threadId: options.identity.threadId,
821
+ parentThreadId: options.identity.parentThreadId,
822
+ callId: null,
823
+ namespace: 'collaboration',
824
+ tool: 'agent_message',
825
+ arguments: null,
826
+ output: {
827
+ messageType,
828
+ status: text.match(/\bstatus=(progress|blocked)\b/u)?.[1] ?? null,
829
+ taskName: text.match(/^Task name:\s*(.+)\s*$/mu)?.[1] ?? null,
830
+ senderAgentPath: text.match(/^Sender:\s*(.+)\s*$/mu)?.[1] ?? null,
831
+ payload: messageType === 'FINAL_ANSWER' ? agentMessagePayload(text) : null,
832
+ },
833
+ status: 'completed',
834
+ order: eventTimestamp(options.event, options.file, options.line),
835
+ };
836
+ }
837
+ function normalizeToolName(value) {
838
+ const separator = value.indexOf('__');
839
+ return separator < 0
840
+ ? { namespace: null, tool: value }
841
+ : { namespace: value.slice(0, separator), tool: value.slice(separator + 2) };
842
+ }
843
+ function normalizeAppServerCollaborationTool(value) {
844
+ switch (value) {
845
+ case 'spawnAgent':
846
+ case 'spawn_agent':
847
+ return 'spawn_agent';
848
+ case 'wait':
849
+ case 'wait_agent':
850
+ return 'wait_agent';
851
+ case 'closeAgent':
852
+ case 'close_agent':
853
+ return 'close_agent';
854
+ default:
855
+ return value;
856
+ }
857
+ }
858
+ function appServerCollaborationCalls(frames, records, descendants) {
859
+ const parentByThread = new Map(records.map((record) => [record.threadId, record.parentThreadId]));
860
+ const recordByThread = new Map(records.map((record) => [record.threadId, record]));
861
+ const rolloutCallsById = new Map();
862
+ for (const call of records.flatMap((record) => record.calls)) {
863
+ if (!call.callId)
864
+ continue;
865
+ const matches = rolloutCallsById.get(call.callId) ?? [];
866
+ matches.push(call);
867
+ rolloutCallsById.set(call.callId, matches);
868
+ }
869
+ const calls = [];
870
+ for (const [index, value] of frames.entries()) {
871
+ if (!isRecord(value) || value.method !== 'item/completed')
872
+ continue;
873
+ const params = isRecord(value.params) ? value.params : {};
874
+ const item = isRecord(params.item) ? params.item : null;
875
+ if (!item
876
+ || (item.type !== 'collabToolCall'
877
+ && item.type !== 'collabAgentToolCall'
878
+ && item.type !== 'collab_tool_call'))
879
+ continue;
880
+ const threadId = typeof item.senderThreadId === 'string'
881
+ ? item.senderThreadId
882
+ : typeof item.sender_thread_id === 'string'
883
+ ? item.sender_thread_id
884
+ : codexWorkflowFrameThreadId(value);
885
+ if (!threadId || !descendants.has(threadId))
886
+ continue;
887
+ const callId = typeof item.id === 'string' && item.id !== '' ? item.id : null;
888
+ const tool = typeof item.tool === 'string' && item.tool !== '' ? item.tool : null;
889
+ const status = typeof item.status === 'string' && item.status !== '' ? item.status : null;
890
+ const receiverThreadIds = Array.isArray(item.receiverThreadIds)
891
+ && item.receiverThreadIds.every((entry) => typeof entry === 'string')
892
+ ? item.receiverThreadIds
893
+ : Array.isArray(item.receiver_thread_ids)
894
+ && item.receiver_thread_ids.every((entry) => typeof entry === 'string')
895
+ ? item.receiver_thread_ids
896
+ : typeof item.receiverThreadId === 'string'
897
+ ? [item.receiverThreadId]
898
+ : typeof item.receiver_thread_id === 'string'
899
+ ? [item.receiver_thread_id]
900
+ : typeof item.newThreadId === 'string'
901
+ ? [item.newThreadId]
902
+ : typeof item.new_thread_id === 'string'
903
+ ? [item.new_thread_id]
904
+ : null;
905
+ const agentsStates = item.agentsStates
906
+ ?? item.agents_states
907
+ ?? item.agentStatus
908
+ ?? item.agent_status;
909
+ if (!callId || !tool || !status || !receiverThreadIds || agentsStates === undefined) {
910
+ throw new Error('Codex app-server collaboration item is missing required terminal evidence.');
911
+ }
912
+ const matchingRolloutCalls = rolloutCallsById.get(callId) ?? [];
913
+ const rolloutCall = matchingRolloutCalls.length === 1 ? matchingRolloutCalls[0] : undefined;
914
+ const rolloutArguments = isRecord(rolloutCall?.arguments) ? rolloutCall.arguments : {};
915
+ const normalizedTool = normalizeAppServerCollaborationTool(tool);
916
+ let callArguments = rolloutCall?.arguments ?? null;
917
+ let effectiveRuntime;
918
+ let callOutput = {
919
+ agentsStates,
920
+ ...(item.result !== undefined ? { result: item.result } : {}),
921
+ ...(item.error !== undefined ? { error: item.error } : {}),
922
+ };
923
+ if (normalizedTool === 'spawn_agent') {
924
+ if (matchingRolloutCalls.length !== 1 || rolloutCall?.tool !== 'spawn_agent') {
925
+ throw new Error('Codex V1 spawn evidence must have one exact rollout call id and tool match.');
926
+ }
927
+ const spawnArguments = canonicalV1SpawnArguments(rolloutCall.arguments);
928
+ if (receiverThreadIds.length !== 1) {
929
+ throw new Error('Codex V1 spawn evidence must identify exactly one receiver thread.');
930
+ }
931
+ const receiverThreadId = receiverThreadIds[0];
932
+ const receiver = recordByThread.get(receiverThreadId);
933
+ if (!receiver
934
+ || receiver.parentThreadId !== threadId
935
+ || !receiver.agentRole
936
+ || !receiver.initialUserMessage)
937
+ throw new Error('Codex V1 spawn evidence is not linked to a plaintext child thread.');
938
+ if (item.prompt !== undefined && item.prompt !== null && item.prompt !== receiver.initialUserMessage) {
939
+ throw new Error('Codex V1 spawn prompt does not match the linked child thread.');
940
+ }
941
+ if (spawnArguments.agent_type !== undefined
942
+ && spawnArguments.agent_type !== receiver.agentRole)
943
+ throw new Error('Codex V1 spawn role does not match the linked child thread.');
944
+ if (spawnArguments.message !== receiver.initialUserMessage)
945
+ throw new Error('Codex V1 spawn message does not match the linked child thread.');
946
+ const stateRecord = isRecord(agentsStates) ? agentsStates : null;
947
+ if (!stateRecord || !Object.hasOwn(stateRecord, receiverThreadId)) {
948
+ throw new Error('Codex V1 spawn evidence is missing the linked child state.');
949
+ }
950
+ const model = item.model;
951
+ const reasoningEffort = item.reasoningEffort
952
+ ?? item.reasoning_effort;
953
+ callArguments = spawnArguments;
954
+ if (typeof model === 'string' && model !== ''
955
+ || typeof reasoningEffort === 'string' && reasoningEffort !== '') {
956
+ effectiveRuntime = {
957
+ ...(typeof model === 'string' && model !== '' ? { model } : {}),
958
+ ...(typeof reasoningEffort === 'string' && reasoningEffort !== ''
959
+ ? { reasoningEffort }
960
+ : {}),
961
+ };
962
+ }
963
+ callOutput = {
964
+ agent_id: receiverThreadId,
965
+ status: stateRecord[receiverThreadId],
966
+ };
967
+ }
968
+ else if (normalizedTool === 'wait_agent') {
969
+ if (!isRecord(agentsStates)) {
970
+ throw new Error('Codex V1 wait evidence agentsStates must be an object.');
971
+ }
972
+ if (receiverThreadIds.some((receiver) => !descendants.has(receiver)
973
+ || !recordByThread.has(receiver)
974
+ || !Object.hasOwn(agentsStates, receiver))) {
975
+ throw new Error('Codex V1 wait evidence is missing a linked receiver state.');
976
+ }
977
+ const timeoutMs = rolloutArguments.timeout_ms;
978
+ if (timeoutMs !== undefined && (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0)) {
979
+ throw new Error('Codex V1 wait timeout_ms evidence must be a non-negative integer.');
980
+ }
981
+ callArguments = {
982
+ receiver_thread_ids: receiverThreadIds,
983
+ ...(timeoutMs === undefined ? {} : { timeout_ms: timeoutMs }),
984
+ };
985
+ const rawOutput = isRecord(rolloutCall?.output) ? rolloutCall.output : null;
986
+ const rawTimedOut = rawOutput?.timed_out === true
987
+ || rawOutput?.timedOut === true
988
+ || rawOutput?.status === 'timeout'
989
+ || rawOutput?.status === 'timed_out';
990
+ const result = isRecord(item.result) ? item.result : null;
991
+ const statusByReceiver = {};
992
+ for (const receiver of receiverThreadIds) {
993
+ const receiverResult = isRecord(result?.[receiver]) ? result[receiver] : result;
994
+ statusByReceiver[receiver] = receiverResult && typeof receiverResult.completed === 'string'
995
+ ? { completed: receiverResult.completed }
996
+ : agentsStates[receiver];
997
+ }
998
+ const inferredTimedOut = status === 'completed'
999
+ && receiverThreadIds.every((receiver) => agentsStates[receiver] === 'running');
1000
+ callOutput = {
1001
+ ...(rawTimedOut || inferredTimedOut ? { timed_out: true } : {}),
1002
+ status: statusByReceiver,
1003
+ };
1004
+ }
1005
+ else if (normalizedTool === 'close_agent') {
1006
+ if (receiverThreadIds.length === 0
1007
+ || receiverThreadIds.some((receiver) => !descendants.has(receiver)
1008
+ || !recordByThread.has(receiver)))
1009
+ throw new Error('Codex V1 close evidence is missing a linked receiver thread.');
1010
+ callArguments = { receiver_thread_ids: receiverThreadIds };
1011
+ }
1012
+ calls.push({
1013
+ threadId,
1014
+ parentThreadId: parentByThread.get(threadId) ?? null,
1015
+ ...(normalizedTool === 'wait_agent'
1016
+ && receiverThreadIds.length === 1
1017
+ && recordByThread.get(receiverThreadIds[0])?.parentThreadId
1018
+ ? { linkedThreadId: receiverThreadIds[0] }
1019
+ : {}),
1020
+ callId,
1021
+ namespace: 'collaboration',
1022
+ tool: normalizedTool,
1023
+ arguments: callArguments,
1024
+ ...(effectiveRuntime === undefined ? {} : { effectiveRuntime }),
1025
+ output: callOutput,
1026
+ status,
1027
+ order: `0000-app-server\0${String(index).padStart(8, '0')}`,
1028
+ });
1029
+ }
1030
+ return calls;
1031
+ }
1032
+ function v1CodeModeSpawnCall(options) {
1033
+ const event = options.events[options.index];
1034
+ const eventPayload = event.type === 'event_msg' && isRecord(event.payload) ? event.payload : null;
1035
+ const item = eventPayload?.type === 'item_completed' && isRecord(eventPayload.item)
1036
+ ? eventPayload.item
1037
+ : null;
1038
+ if (!item || !['CollabAgentToolCall', 'collabAgentToolCall', 'collab_tool_call'].includes(typeof item.type === 'string' ? item.type : ''))
1039
+ return null;
1040
+ if (normalizeAppServerCollaborationTool(String(item.tool ?? '')) !== 'spawn_agent')
1041
+ return null;
1042
+ const previous = options.events[options.index - 1];
1043
+ const next = options.events[options.index + 1];
1044
+ const exec = previous ? responseItem(previous) : null;
1045
+ const output = next ? responseItem(next) : null;
1046
+ if (!exec
1047
+ || exec.type !== 'custom_tool_call'
1048
+ || exec.name !== 'exec'
1049
+ || typeof exec.call_id !== 'string'
1050
+ || exec.call_id === ''
1051
+ || typeof exec.input !== 'string'
1052
+ || !output
1053
+ || output.type !== 'custom_tool_call_output'
1054
+ || output.call_id !== exec.call_id
1055
+ || typeof item.id !== 'string'
1056
+ || item.id === ''
1057
+ || (item.sender_thread_id ?? item.senderThreadId) !== options.identity.threadId)
1058
+ throw new Error('Codex V1 code-mode spawn event is not linked to its exec wrapper and output.');
1059
+ const argumentsValue = parseV1CodeModeSpawnInput(exec.input);
1060
+ if (typeof item.prompt !== 'string' || item.prompt !== argumentsValue.message) {
1061
+ throw new Error('Codex V1 code-mode spawn prompt conflicts with its wrapper arguments.');
1062
+ }
1063
+ return {
1064
+ threadId: options.identity.threadId,
1065
+ parentThreadId: options.identity.parentThreadId,
1066
+ callId: item.id,
1067
+ namespace: 'multi_agent_v1',
1068
+ tool: 'spawn_agent',
1069
+ arguments: argumentsValue,
1070
+ output: null,
1071
+ status: typeof item.status === 'string' ? item.status : null,
1072
+ order: eventTimestamp(event, options.file, options.index + 1),
1073
+ };
1074
+ }
1075
+ function rolloutCalls(events, identity, file) {
1076
+ const calls = [];
1077
+ const byId = new Map();
1078
+ const pendingOutputs = new Map();
1079
+ for (const [index, event] of events.entries()) {
1080
+ const payload = responseItem(event);
1081
+ if (payload) {
1082
+ const update = agentMessageCall({ payload, identity, file, line: index + 1, event });
1083
+ if (update)
1084
+ calls.push(update);
1085
+ const call = callFromPayload({ payload, identity, file, line: index + 1, event });
1086
+ const v1CodeModeWrapper = isV1CodeModeExecCall(call);
1087
+ if (call && !v1CodeModeWrapper && (!call.callId || !byId.has(call.callId))) {
1088
+ if (call.callId && pendingOutputs.has(call.callId)) {
1089
+ call.output = pendingOutputs.get(call.callId);
1090
+ call.status = 'completed';
1091
+ }
1092
+ calls.push(call);
1093
+ if (call.callId)
1094
+ byId.set(call.callId, call);
1095
+ }
1096
+ if ((payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output')
1097
+ && typeof payload.call_id === 'string') {
1098
+ const output = parseSerialized(payload.output);
1099
+ const target = byId.get(payload.call_id);
1100
+ if (target) {
1101
+ target.output = output;
1102
+ target.status = 'completed';
1103
+ }
1104
+ else
1105
+ pendingOutputs.set(payload.call_id, output);
1106
+ }
1107
+ const metadata = isRecord(payload.internal_chat_message_metadata_passthrough)
1108
+ ? payload.internal_chat_message_metadata_passthrough
1109
+ : {};
1110
+ if (Array.isArray(metadata.executed_tool_calls)) {
1111
+ if (!metadata.executed_tool_calls.every(isRecord)) {
1112
+ throw new Error('Codex executed tool-call metadata contains an invalid entry.');
1113
+ }
1114
+ if (typeof payload.call_id !== 'string' || payload.call_id === '') {
1115
+ throw new Error('Codex executed tool-call metadata is missing its output call id.');
1116
+ }
1117
+ const output = parseSerialized(payload.output);
1118
+ if (output === undefined || output === null) {
1119
+ throw new Error('Codex executed tool-call metadata is missing its linked output.');
1120
+ }
1121
+ for (const [nestedIndex, entry] of metadata.executed_tool_calls.entries()) {
1122
+ if (typeof entry.name !== 'string' || entry.name === '' || !Object.hasOwn(entry, 'arguments')) {
1123
+ throw new Error('Codex executed tool-call metadata is missing its name or arguments.');
1124
+ }
1125
+ const tool = normalizeToolName(entry.name);
1126
+ const direct = byId.get(payload.call_id);
1127
+ if (direct?.tool === tool.tool
1128
+ && (tool.namespace === null || direct.namespace === tool.namespace))
1129
+ continue;
1130
+ calls.push({
1131
+ threadId: identity.threadId,
1132
+ parentThreadId: identity.parentThreadId,
1133
+ callId: metadata.executed_tool_calls.length === 1
1134
+ ? payload.call_id
1135
+ : `${payload.call_id}:executed:${nestedIndex + 1}`,
1136
+ namespace: tool.namespace,
1137
+ tool: tool.tool,
1138
+ arguments: parseSerialized(entry.arguments),
1139
+ output,
1140
+ status: 'completed',
1141
+ order: eventTimestamp(event, file, index + 1),
1142
+ });
1143
+ }
1144
+ }
1145
+ }
1146
+ const v1Spawn = v1CodeModeSpawnCall({ events, index, identity, file });
1147
+ if (v1Spawn)
1148
+ calls.push(v1Spawn);
1149
+ const eventPayload = event.type === 'event_msg' && isRecord(event.payload) ? event.payload : null;
1150
+ const completedItem = eventPayload?.type === 'item_completed' && isRecord(eventPayload.item)
1151
+ ? eventPayload.item
1152
+ : null;
1153
+ if (!completedItem)
1154
+ continue;
1155
+ if (completedItem.type === 'CommandExecution' || completedItem.type === 'command_execution') {
1156
+ const raw = Array.isArray(completedItem.command)
1157
+ ? completedItem.command.at(-1)
1158
+ : completedItem.command;
1159
+ if (typeof raw !== 'string')
1160
+ continue;
1161
+ calls.push({
1162
+ threadId: identity.threadId,
1163
+ parentThreadId: identity.parentThreadId,
1164
+ callId: typeof completedItem.id === 'string' ? `command:${completedItem.id}` : null,
1165
+ namespace: null,
1166
+ tool: 'exec_command',
1167
+ arguments: { cmd: raw },
1168
+ output: {
1169
+ exitCode: Number.isInteger(completedItem.exit_code) ? completedItem.exit_code : null,
1170
+ },
1171
+ status: typeof completedItem.status === 'string' ? completedItem.status : 'completed',
1172
+ order: eventTimestamp(event, file, index + 1),
1173
+ });
1174
+ }
1175
+ }
1176
+ return calls;
1177
+ }
1178
+ function initialMessages(events) {
1179
+ let user = null;
1180
+ let opaque = null;
1181
+ const developer = [];
1182
+ for (const event of events) {
1183
+ const payload = responseItem(event);
1184
+ if (!payload)
1185
+ continue;
1186
+ if (payload.type === 'message' && payload.role === 'developer')
1187
+ developer.push(contentText(payload));
1188
+ if (user === null && payload.type === 'message' && payload.role === 'user') {
1189
+ const candidate = contentText(payload);
1190
+ if (candidate && !/^<environment_context>[\s\S]*<\/environment_context>$/u.test(candidate.trim())) {
1191
+ user = candidate;
1192
+ }
1193
+ }
1194
+ if (opaque === null && payload.type === 'agent_message')
1195
+ opaque = encryptedContent(payload);
1196
+ }
1197
+ return {
1198
+ user,
1199
+ opaque,
1200
+ developer: developer.join('\n'),
1201
+ developerMessageCount: developer.length,
1202
+ };
1203
+ }
1204
+ function opaqueSpawnArguments(calls, protocol) {
1205
+ if (protocol !== 'v2')
1206
+ return;
1207
+ for (const call of calls) {
1208
+ if (call.tool !== 'spawn_agent' || !isRecord(call.arguments))
1209
+ continue;
1210
+ if (typeof call.arguments.message !== 'string')
1211
+ continue;
1212
+ call.arguments = {
1213
+ ...call.arguments,
1214
+ message: { kind: 'opaque', sha256: hash(call.arguments.message) },
1215
+ };
1216
+ }
1217
+ }
1218
+ function linkedOpaqueCall(calls, parentThreadId, digest) {
1219
+ const call = calls.find((entry) => entry.threadId === parentThreadId
1220
+ && entry.tool === 'spawn_agent'
1221
+ && isRecord(entry.arguments)
1222
+ && isRecord(entry.arguments.message)
1223
+ && entry.arguments.message.sha256 === digest);
1224
+ return call?.callId ?? null;
1225
+ }
1226
+ export async function normalizeCodexWorkflowRollouts(options) {
1227
+ const records = [];
1228
+ let totalBytes = 0;
1229
+ let totalLines = 0;
1230
+ for (const file of await listRollouts(options.codexHome)) {
1231
+ const bytes = await fs.readFile(file);
1232
+ totalBytes += bytes.length;
1233
+ if (totalBytes > MAX_ROLLOUT_BYTES)
1234
+ throw new Error('Codex rollout evidence is too large.');
1235
+ const lines = bytes.toString('utf8').split('\n').filter((line) => line.trim() !== '');
1236
+ totalLines += lines.length;
1237
+ if (totalLines > MAX_ROLLOUT_LINES)
1238
+ throw new Error('Codex rollout evidence has too many events.');
1239
+ const events = lines.map((line, index) => {
1240
+ let value;
1241
+ try {
1242
+ value = JSON.parse(line);
1243
+ }
1244
+ catch {
1245
+ throw new Error(`Codex rollout contains invalid JSON at line ${index + 1}.`);
1246
+ }
1247
+ if (!isRecord(value))
1248
+ throw new Error(`Codex rollout event ${index + 1} must be an object.`);
1249
+ return value;
1250
+ });
1251
+ const identity = rolloutIdentity(events);
1252
+ const messages = initialMessages(events);
1253
+ records.push({
1254
+ ...identity,
1255
+ initialUserMessage: messages.user,
1256
+ initialOpaqueMessage: messages.opaque,
1257
+ developerText: messages.developer,
1258
+ developerMessageCount: messages.developerMessageCount,
1259
+ calls: rolloutCalls(events, identity, path.relative(options.codexHome, file)),
1260
+ trace: events,
1261
+ });
1262
+ }
1263
+ const root = records.find((record) => record.threadId === options.rootThreadId);
1264
+ if (!root)
1265
+ throw new Error('Codex raw rollout does not contain the app-server root thread.');
1266
+ const descendants = new Set([options.rootThreadId]);
1267
+ let changed = true;
1268
+ while (changed) {
1269
+ changed = false;
1270
+ for (const record of records) {
1271
+ if (record.parentThreadId && descendants.has(record.parentThreadId) && !descendants.has(record.threadId)) {
1272
+ descendants.add(record.threadId);
1273
+ changed = true;
1274
+ }
1275
+ }
1276
+ }
1277
+ const selected = records.filter((record) => descendants.has(record.threadId));
1278
+ if (selected.some((record) => record.developerText === '')) {
1279
+ throw new Error('Codex raw rollout is missing developer context for a workflow thread.');
1280
+ }
1281
+ const rolloutCallList = selected.flatMap((record) => record.calls);
1282
+ const appServerCallList = options.request.multiAgent.protocol === 'v1'
1283
+ ? appServerCollaborationCalls(options.appServerFrames ?? [], records, descendants)
1284
+ : [];
1285
+ const appServerCallIds = new Set(appServerCallList.map((call) => call.callId));
1286
+ const calls = [
1287
+ ...appServerCallList,
1288
+ ...rolloutCallList.filter((call) => !call.callId || !appServerCallIds.has(call.callId)),
1289
+ ];
1290
+ const isolatedPaths = options.isolatedPaths
1291
+ ?? [{ path: options.codexHome, marker: '[codex-home]' }];
1292
+ for (const call of calls) {
1293
+ if (options.request.multiAgent.protocol === 'v1'
1294
+ && isV1CodeModeExecCall(call)
1295
+ && isRecord(call.arguments)
1296
+ && typeof call.arguments.input === 'string') {
1297
+ call.arguments = {
1298
+ input: {
1299
+ namespace: 'multi_agent_v1',
1300
+ tool: 'spawn_agent',
1301
+ arguments: parseV1CodeModeSpawnInput(call.arguments.input),
1302
+ },
1303
+ };
1304
+ }
1305
+ call.arguments = sanitizeTraceValue(call.arguments, options.runtime, isolatedPaths, options.request.multiAgent.protocol === 'v2');
1306
+ call.output = sanitizeTraceValue(call.output, options.runtime, isolatedPaths, options.request.multiAgent.protocol === 'v2');
1307
+ }
1308
+ opaqueSpawnArguments(calls, options.request.multiAgent.protocol);
1309
+ calls.sort((left, right) => left.order.localeCompare(right.order));
1310
+ const collaborationCalls = calls.map(({ order: _order, ...call }, index) => ({
1311
+ ...call,
1312
+ ordinal: index + 1,
1313
+ }));
1314
+ const descendantThreadIds = new Set(selected
1315
+ .filter((record) => record.threadId !== options.rootThreadId)
1316
+ .map((record) => record.threadId));
1317
+ for (const call of collaborationCalls) {
1318
+ if (call.tool !== 'wait_agent' || call.linkedThreadId !== undefined)
1319
+ continue;
1320
+ const receiverIds = new Set();
1321
+ if (isRecord(call.arguments) && Array.isArray(call.arguments.receiver_thread_ids)) {
1322
+ for (const receiver of call.arguments.receiver_thread_ids) {
1323
+ if (typeof receiver === 'string')
1324
+ receiverIds.add(receiver);
1325
+ }
1326
+ }
1327
+ if (isRecord(call.output) && isRecord(call.output.status)) {
1328
+ for (const receiver of Object.keys(call.output.status))
1329
+ receiverIds.add(receiver);
1330
+ }
1331
+ if (receiverIds.size !== 1)
1332
+ continue;
1333
+ const receiver = [...receiverIds][0];
1334
+ if (descendantThreadIds.has(receiver))
1335
+ call.linkedThreadId = receiver;
1336
+ }
1337
+ const descendantByAgentPath = new Map();
1338
+ for (const record of selected) {
1339
+ if (!record.parentThreadId || !record.agentPath)
1340
+ continue;
1341
+ descendantByAgentPath.set(record.agentPath, descendantByAgentPath.has(record.agentPath) ? null : record.threadId);
1342
+ }
1343
+ for (const call of collaborationCalls) {
1344
+ if (call.tool !== 'agent_message' || !isRecord(call.output))
1345
+ continue;
1346
+ const senderAgentPath = call.output.senderAgentPath;
1347
+ delete call.output.senderAgentPath;
1348
+ if (call.threadId !== options.rootThreadId
1349
+ || call.output.messageType !== 'FINAL_ANSWER'
1350
+ || typeof senderAgentPath !== 'string')
1351
+ continue;
1352
+ const linkedThreadId = descendantByAgentPath.get(senderAgentPath);
1353
+ if (linkedThreadId)
1354
+ call.linkedThreadId = linkedThreadId;
1355
+ }
1356
+ const threadGraph = selected.map((record) => {
1357
+ let initialMessage = null;
1358
+ if (record.initialOpaqueMessage) {
1359
+ const digest = hash(record.initialOpaqueMessage);
1360
+ const linkedCallId = record.parentThreadId
1361
+ ? linkedOpaqueCall(calls, record.parentThreadId, digest)
1362
+ : null;
1363
+ if (!linkedCallId)
1364
+ throw new Error('Codex V2 child opaque message is not linked to a spawn call.');
1365
+ initialMessage = { kind: 'opaque-link', sha256: digest, linkedCallId };
1366
+ }
1367
+ else if (record.initialUserMessage) {
1368
+ initialMessage = { kind: 'plaintext', text: record.initialUserMessage };
1369
+ }
1370
+ if (record.parentThreadId
1371
+ && options.request.multiAgent.protocol === 'v1'
1372
+ && initialMessage?.kind !== 'plaintext')
1373
+ throw new Error('Codex V1 child rollout is missing its plaintext initial message.');
1374
+ if (record.parentThreadId
1375
+ && options.request.multiAgent.protocol === 'v2'
1376
+ && options.request.multiAgent.hideSpawnAgentMetadata
1377
+ && initialMessage?.kind !== 'opaque-link')
1378
+ throw new Error('Codex V2 child rollout is missing its opaque spawn link.');
1379
+ return {
1380
+ threadId: record.threadId,
1381
+ parentThreadId: record.parentThreadId,
1382
+ agentRole: record.agentRole,
1383
+ initialMessage,
1384
+ developerContext: {
1385
+ present: true,
1386
+ messageCount: record.developerMessageCount,
1387
+ sha256: hash(record.developerText),
1388
+ hasMultiAgentModeGate: record.developerText.includes('<multi_agent_mode>'),
1389
+ hasNestedAgentCapability: record.developerText.includes('Child agents can also spawn their own sub-agents'),
1390
+ hasCodexExploreDeveloperAuthorization: record.developerText.includes('### Codex Explore 子代理授权'),
1391
+ },
1392
+ };
1393
+ });
1394
+ threadGraph.sort((left, right) => {
1395
+ if (left.threadId === options.rootThreadId)
1396
+ return -1;
1397
+ if (right.threadId === options.rootThreadId)
1398
+ return 1;
1399
+ return left.threadId.localeCompare(right.threadId);
1400
+ });
1401
+ const skillLoads = [];
1402
+ const seenLoads = new Set();
1403
+ for (const call of collaborationCalls) {
1404
+ const serialized = JSON.stringify(call.arguments).replaceAll('\\', '/');
1405
+ for (const match of serialized.matchAll(/\.agents\/skills\/([^/"\\]+)\/SKILL\.md/gu)) {
1406
+ const key = `${call.threadId}\0${match[1]}`;
1407
+ if (seenLoads.has(key))
1408
+ continue;
1409
+ seenLoads.add(key);
1410
+ skillLoads.push({ threadId: call.threadId, skill: match[1], callId: call.callId });
1411
+ }
1412
+ }
1413
+ return {
1414
+ threadGraph,
1415
+ collaborationCalls,
1416
+ leafCalls: collaborationCalls.filter((call) => call.threadId !== options.rootThreadId
1417
+ || (call.linkedThreadId !== undefined && descendants.has(call.linkedThreadId))),
1418
+ skillLoads,
1419
+ trace: selected.flatMap((record) => record.trace.map((event) => sanitizeTraceValue(event, options.runtime, [
1420
+ ...isolatedPaths,
1421
+ ...selected.flatMap((entry) => entry.agentPath
1422
+ ? [{ path: entry.agentPath, marker: '[agent-path]' }]
1423
+ : []),
1424
+ ], options.request.multiAgent.protocol === 'v2'))),
1425
+ };
1426
+ }
1427
+ function sandboxPolicy(request) {
1428
+ return request.sandbox === 'read-only'
1429
+ ? { type: 'readOnly', networkAccess: true }
1430
+ : {
1431
+ type: 'workspaceWrite',
1432
+ writableRoots: [],
1433
+ networkAccess: true,
1434
+ excludeTmpdirEnvVar: false,
1435
+ excludeSlashTmp: false,
1436
+ };
1437
+ }
1438
+ const defaultTransport = {
1439
+ async run(input) {
1440
+ const control = new CodexEvalAttemptControl({
1441
+ label: 'codex-workflow',
1442
+ timeoutMs: input.timeoutMs,
1443
+ ...(input.signal ? { signal: input.signal } : {}),
1444
+ });
1445
+ const temporaryRoot = await createCodexEvalTemporaryRoot('codex-workflow-');
1446
+ const codexHome = path.join(temporaryRoot.root, 'codex-home');
1447
+ const env = buildEngineEnvironment(input.sourceEnv, temporaryRoot.root, codexHome, input.codexRuntime);
1448
+ let session;
1449
+ let cleanupConfirmed = false;
1450
+ let codexHomeRemoved = false;
1451
+ const startedAt = Date.now();
1452
+ const frames = [];
1453
+ let removeHandler;
1454
+ let removeFailureHandler;
1455
+ try {
1456
+ await Promise.all([
1457
+ fs.ensureDir(env.HOME),
1458
+ fs.ensureDir(env.TMPDIR),
1459
+ materializeCodexEvalRuntimeHome(input.codexRuntime, codexHome, {
1460
+ trustedProject: input.input.workspacePath,
1461
+ }),
1462
+ ]);
1463
+ const before = await snapshotWorkspace(input.input.workspacePath);
1464
+ session = new EvalAppServerSession({
1465
+ codexBin: input.codexBin,
1466
+ cwd: input.input.workspacePath,
1467
+ env,
1468
+ modelProvider: input.codexRuntime.modelProvider,
1469
+ sensitiveEnvironmentNames: input.codexRuntime.sensitiveEnvironmentNames,
1470
+ isolation: 'standard',
1471
+ processManagement: {
1472
+ mode: 'managed',
1473
+ inspectionAvailable: true,
1474
+ userAcknowledged: false,
1475
+ descendantCleanupGuaranteed: true,
1476
+ },
1477
+ configOverrides: workflowConfig(input.request, input.input.workspacePath),
1478
+ });
1479
+ await session.initialize(control.rpcTimeout('initialize'));
1480
+ const threadResult = await session.sendRequest('thread/start', {
1481
+ cwd: input.input.workspacePath,
1482
+ approvalPolicy: 'never',
1483
+ sandbox: input.request.sandbox,
1484
+ ephemeral: false,
1485
+ ...(input.request.model ? { model: input.request.model } : {}),
1486
+ }, control.rpcTimeout('thread/start'));
1487
+ const rootThreadId = extractAppServerThreadId(threadResult);
1488
+ let finalMessage = null;
1489
+ let usage = null;
1490
+ let turnStatus = null;
1491
+ let resolveTurn;
1492
+ let rejectTurn;
1493
+ const turnDone = new Promise((resolve, reject) => {
1494
+ resolveTurn = resolve;
1495
+ rejectTurn = reject;
1496
+ });
1497
+ void turnDone.catch(() => undefined);
1498
+ removeFailureHandler = session.onFailure((error) => rejectTurn?.(error));
1499
+ removeHandler = session.onFrame((frame) => {
1500
+ const isolatedPaths = [
1501
+ { path: codexHome, marker: '[codex-home]' },
1502
+ { path: env.HOME, marker: '[isolated-home]' },
1503
+ { path: temporaryRoot.root, marker: '[runtime-root]' },
1504
+ ];
1505
+ frames.push(sanitizeTraceValue(frame, input.codexRuntime, isolatedPaths, input.request.multiAgent.protocol === 'v2'));
1506
+ const method = typeof frame.method === 'string' ? frame.method : '';
1507
+ const params = isRecord(frame.params) ? frame.params : {};
1508
+ if (method === 'turn/completed') {
1509
+ if (!codexWorkflowFrameIsForRoot(frame, rootThreadId))
1510
+ return;
1511
+ const turn = isRecord(params.turn) ? params.turn : {};
1512
+ turnStatus = typeof turn.status === 'string' ? turn.status : 'unknown';
1513
+ resolveTurn?.();
1514
+ return;
1515
+ }
1516
+ if (!codexWorkflowFrameIsForRoot(frame, rootThreadId))
1517
+ return;
1518
+ if (method === 'item/completed' && isRecord(params.item)) {
1519
+ const item = params.item;
1520
+ if (item.type === 'agentMessage'
1521
+ && item.phase === 'final_answer'
1522
+ && typeof item.text === 'string')
1523
+ finalMessage = item.text;
1524
+ }
1525
+ if (method === 'thread/tokenUsage/updated')
1526
+ usage = params.tokenUsage ?? usage;
1527
+ if (method === 'item/tool/requestUserInput') {
1528
+ rejectTurn?.(new Error('codex-workflow root requested interactive user input.'));
1529
+ }
1530
+ if (method === 'error')
1531
+ rejectTurn?.(new Error(`codex-workflow app-server error: ${JSON.stringify(params)}`));
1532
+ });
1533
+ await session.sendRequest('turn/start', {
1534
+ threadId: rootThreadId,
1535
+ input: [{ type: 'text', text: input.request.prompt, text_elements: [] }],
1536
+ cwd: input.input.workspacePath,
1537
+ approvalPolicy: 'never',
1538
+ sandboxPolicy: sandboxPolicy(input.request),
1539
+ ...(input.request.model ? { model: input.request.model } : {}),
1540
+ }, control.rpcTimeout('turn/start'));
1541
+ let abortTurnWait;
1542
+ try {
1543
+ await Promise.race([
1544
+ turnDone,
1545
+ new Promise((_resolve, reject) => {
1546
+ abortTurnWait = () => reject(control.stopError('turn wait'));
1547
+ if (control.signal.aborted)
1548
+ abortTurnWait();
1549
+ else
1550
+ control.signal.addEventListener('abort', abortTurnWait, { once: true });
1551
+ }),
1552
+ ]);
1553
+ }
1554
+ finally {
1555
+ if (abortTurnWait)
1556
+ control.signal.removeEventListener('abort', abortTurnWait);
1557
+ }
1558
+ control.throwIfStopped('turn wait');
1559
+ if (turnStatus !== 'completed') {
1560
+ throw new Error(`codex-workflow root turn ended with status ${String(turnStatus)}.`);
1561
+ }
1562
+ if (!finalMessage) {
1563
+ const diagnostic = codexWorkflowMissingFinalMessageDiagnostic({
1564
+ frames,
1565
+ rootThreadId,
1566
+ turnStatus,
1567
+ usage,
1568
+ });
1569
+ throw new Error(`codex-workflow root turn did not produce a final message: ${JSON.stringify(diagnostic)}.`);
1570
+ }
1571
+ const isolatedPaths = [
1572
+ { path: codexHome, marker: '[codex-home]' },
1573
+ { path: env.HOME, marker: '[isolated-home]' },
1574
+ { path: temporaryRoot.root, marker: '[runtime-root]' },
1575
+ ];
1576
+ const rollout = await normalizeCodexWorkflowRollouts({
1577
+ codexHome,
1578
+ rootThreadId,
1579
+ request: input.request,
1580
+ runtime: input.codexRuntime,
1581
+ appServerFrames: frames,
1582
+ isolatedPaths,
1583
+ });
1584
+ removeHandler();
1585
+ removeHandler = undefined;
1586
+ removeFailureHandler();
1587
+ removeFailureHandler = undefined;
1588
+ await session.cleanup();
1589
+ cleanupConfirmed = true;
1590
+ const after = await snapshotWorkspace(input.input.workspacePath);
1591
+ await temporaryRoot.cleanup();
1592
+ codexHomeRemoved = true;
1593
+ const evidence = {
1594
+ version: 1,
1595
+ rootThreadId,
1596
+ completion: {
1597
+ status: 'completed',
1598
+ turnStatus,
1599
+ durationMs: Date.now() - startedAt,
1600
+ },
1601
+ termination: {
1602
+ reason: 'completed',
1603
+ appServerProcessTreeCleanupConfirmed: true,
1604
+ },
1605
+ usage,
1606
+ finalMessage,
1607
+ workspaceDiff: workspaceDiff(before, after),
1608
+ skillLoads: rollout.skillLoads,
1609
+ threadGraph: rollout.threadGraph,
1610
+ collaborationCalls: rollout.collaborationCalls,
1611
+ leafCalls: rollout.leafCalls,
1612
+ cleanup: { isolatedCodexHomeRemoved: true },
1613
+ };
1614
+ return {
1615
+ evidence: session.scrub(redactCodexEvalValue(evidence, input.codexRuntime)),
1616
+ trace: session.scrub(redactCodexEvalValue([
1617
+ ...frames.map((frame) => sanitizeTraceValue(frame, input.codexRuntime, isolatedPaths, input.request.multiAgent.protocol === 'v2')),
1618
+ ...rollout.trace,
1619
+ ], input.codexRuntime)),
1620
+ modelOutput: redactCodexEvalValue(finalMessage, input.codexRuntime),
1621
+ usage: redactCodexEvalValue(usage, input.codexRuntime),
1622
+ };
1623
+ }
1624
+ finally {
1625
+ removeHandler?.();
1626
+ removeFailureHandler?.();
1627
+ let cleanupError;
1628
+ if (!cleanupConfirmed) {
1629
+ try {
1630
+ await session?.cleanup();
1631
+ }
1632
+ catch (error) {
1633
+ cleanupError = error;
1634
+ }
1635
+ }
1636
+ if (!codexHomeRemoved) {
1637
+ try {
1638
+ await temporaryRoot.cleanup();
1639
+ }
1640
+ catch (error) {
1641
+ cleanupError ??= error;
1642
+ }
1643
+ }
1644
+ control.dispose();
1645
+ if (cleanupError) {
1646
+ throw new Error(`codex-workflow cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
1647
+ }
1648
+ }
1649
+ },
1650
+ };
1651
+ function standardUsage(value) {
1652
+ if (!isRecord(value))
1653
+ return null;
1654
+ const candidates = [value, ...['total', 'totalUsage', 'usage', 'tokenUsage']
1655
+ .map((key) => value[key]).filter(isRecord)];
1656
+ for (const candidate of candidates) {
1657
+ const input = candidate.inputTokens ?? candidate.input_tokens;
1658
+ const output = candidate.outputTokens ?? candidate.output_tokens;
1659
+ if (Number.isSafeInteger(input) && input >= 0
1660
+ && Number.isSafeInteger(output) && output >= 0)
1661
+ return { inputTokens: input, outputTokens: output };
1662
+ }
1663
+ return null;
1664
+ }
1665
+ async function writeWorkflowArtifacts(outputDir, execution) {
1666
+ await Promise.all([
1667
+ fs.writeFile(path.join(outputDir, CODEX_WORKFLOW_ARTIFACTS[0].path), `${JSON.stringify(execution.evidence, null, 2)}\n`, { flag: 'wx' }),
1668
+ fs.writeFile(path.join(outputDir, CODEX_WORKFLOW_ARTIFACTS[1].path), `${JSON.stringify(execution.trace, null, 2)}\n`, { flag: 'wx' }),
1669
+ fs.writeFile(path.join(outputDir, CODEX_WORKFLOW_ARTIFACTS[2].path), `${execution.modelOutput}\n`, { flag: 'wx' }),
1670
+ ]);
1671
+ }
1672
+ async function artifactDigests(outputDir) {
1673
+ return Promise.all(CODEX_WORKFLOW_ARTIFACTS.map(async (artifact) => hash(await fs.readFile(path.join(outputDir, artifact.path)))));
1674
+ }
1675
+ export async function executeCodexWorkflowProvider(options) {
1676
+ const platform = options.platform ?? options.processTree?.platform ?? process.platform;
1677
+ if (platform !== 'darwin' && platform !== 'linux') {
1678
+ throw new Error(`codex-workflow is unsupported on ${platform}; no model was started.`);
1679
+ }
1680
+ const paths = await assertProviderRunPaths({
1681
+ evalRoot: options.evalRoot,
1682
+ workspacePath: options.input.workspacePath,
1683
+ fixturesPath: options.input.fixturesPath,
1684
+ outputDir: options.input.outputDir,
1685
+ });
1686
+ const sourceEnv = options.sourceEnv ?? process.env;
1687
+ const control = new CodexEvalAttemptControl({
1688
+ label: 'codex-workflow provider',
1689
+ timeoutMs: options.input.timeoutMs,
1690
+ ...(options.input.signal ? { signal: options.input.signal } : {}),
1691
+ });
1692
+ try {
1693
+ const common = {
1694
+ version: 1,
1695
+ case: options.input.case,
1696
+ variant: options.input.variant,
1697
+ attempt: options.input.attempt,
1698
+ };
1699
+ let prepared;
1700
+ try {
1701
+ prepared = await runAdapter({
1702
+ command: options.provider.command,
1703
+ evalRoot: paths.evalRoot,
1704
+ input: {
1705
+ ...common,
1706
+ phase: 'prepare',
1707
+ paths: {
1708
+ workspace: paths.workspacePath,
1709
+ fixtures: paths.fixturesPath,
1710
+ output: paths.outputDir,
1711
+ },
1712
+ },
1713
+ sourceEnv,
1714
+ timeoutMs: control.remaining('prepare'),
1715
+ signal: control.signal,
1716
+ processTree: options.processTree,
1717
+ });
1718
+ }
1719
+ catch (error) {
1720
+ control.throwIfStopped('prepare');
1721
+ throw error;
1722
+ }
1723
+ const request = await parseCodexWorkflowRequest(prepared, options.provider.allowedModels, paths.workspacePath);
1724
+ let execution;
1725
+ try {
1726
+ execution = await (options.transport ?? defaultTransport).run({
1727
+ request,
1728
+ input: { ...options.input, ...paths, timeoutMs: control.remaining('Codex execution') },
1729
+ evalRoot: paths.evalRoot,
1730
+ codexRuntime: options.codexRuntime,
1731
+ codexBin: options.codexBin ?? sourceEnv.AW_SKILL_EVAL_CODEX_BIN ?? 'codex',
1732
+ sourceEnv,
1733
+ timeoutMs: control.remaining('Codex execution'),
1734
+ signal: control.signal,
1735
+ });
1736
+ }
1737
+ catch (error) {
1738
+ control.throwIfStopped('Codex execution');
1739
+ throw error;
1740
+ }
1741
+ control.throwIfStopped('evidence collection');
1742
+ const sanitized = redactCodexEvalValue(execution, options.codexRuntime);
1743
+ const usage = standardUsage(sanitized.usage);
1744
+ if (!usage || !standardUsage(sanitized.evidence.usage)) {
1745
+ throw new Error('codex-workflow completed without required token usage evidence.');
1746
+ }
1747
+ await writeWorkflowArtifacts(paths.outputDir, sanitized);
1748
+ const beforeGrade = await artifactDigests(paths.outputDir);
1749
+ let gradeValue;
1750
+ try {
1751
+ gradeValue = await runAdapter({
1752
+ command: options.provider.command,
1753
+ evalRoot: paths.evalRoot,
1754
+ input: {
1755
+ ...common,
1756
+ phase: 'grade',
1757
+ paths: {
1758
+ workspace: paths.workspacePath,
1759
+ fixtures: paths.fixturesPath,
1760
+ output: paths.outputDir,
1761
+ evidence: path.join(paths.outputDir, CODEX_WORKFLOW_ARTIFACTS[0].path),
1762
+ modelOutput: path.join(paths.outputDir, CODEX_WORKFLOW_ARTIFACTS[2].path),
1763
+ },
1764
+ },
1765
+ sourceEnv,
1766
+ timeoutMs: control.remaining('grade'),
1767
+ signal: control.signal,
1768
+ processTree: options.processTree,
1769
+ });
1770
+ }
1771
+ catch (error) {
1772
+ control.throwIfStopped('grade');
1773
+ throw error;
1774
+ }
1775
+ const grade = parseEvalProviderGrade(gradeValue, 'workflow grade stdout');
1776
+ const afterGrade = await artifactDigests(paths.outputDir);
1777
+ if (JSON.stringify(beforeGrade) !== JSON.stringify(afterGrade)) {
1778
+ throw new Error('workflow grade modified Engine-owned attempt artifacts.');
1779
+ }
1780
+ return {
1781
+ usage,
1782
+ grade,
1783
+ artifacts: CODEX_WORKFLOW_ARTIFACTS.map((artifact) => ({ ...artifact })),
1784
+ };
1785
+ }
1786
+ finally {
1787
+ control.dispose();
1788
+ }
1789
+ }
1790
+ //# sourceMappingURL=codex-workflow.js.map