ocpipe 0.6.6 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,11 +76,11 @@ defaultModel: { backend: 'claude-code', modelID: 'sonnet' },
76
76
  claudeCode: { permissionMode: 'acceptEdits' },
77
77
  ```
78
78
 
79
- **Codex** - Uses `@openai/codex`. Install as a peer dependency.
79
+ **Codex** - Uses `@openai/codex-sdk`. Install as a peer dependency.
80
80
 
81
81
  ```typescript
82
82
  defaultModel: { backend: 'codex', modelID: 'gpt-5.4' },
83
- codex: { sandbox: 'read-only', ephemeral: true },
83
+ codex: { sandbox: 'read-only', reasoningEffort: 'high' },
84
84
  ```
85
85
 
86
86
  ### Requirements
@@ -96,10 +96,10 @@ codex: { sandbox: 'read-only', ephemeral: true },
96
96
  bun add @anthropic-ai/claude-agent-sdk
97
97
  ```
98
98
 
99
- **For Codex backend:** Install the Codex CLI package as a peer dependency:
99
+ **For Codex backend:** Install the Codex SDK package as a peer dependency:
100
100
 
101
101
  ```bash
102
- bun add @openai/codex
102
+ bun add @openai/codex-sdk
103
103
  ```
104
104
 
105
105
  ### Documentation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocpipe",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "SDK for LLM pipelines with OpenCode, Codex, and Zod",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -32,20 +32,21 @@
32
32
  "peerDependencies": {
33
33
  "zod": "4.4.3",
34
34
  "@anthropic-ai/claude-agent-sdk": "0.2.126",
35
- "@openai/codex": "0.128.0"
35
+ "@openai/codex-sdk": "0.130.0"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@anthropic-ai/claude-agent-sdk": {
39
39
  "optional": true
40
40
  },
41
- "@openai/codex": {
41
+ "@openai/codex-sdk": {
42
42
  "optional": true
43
43
  }
44
44
  },
45
45
  "devDependencies": {
46
46
  "@anthropic-ai/claude-agent-sdk": "^0.2.132",
47
- "@openai/codex": "^0.128.0",
48
47
  "@eslint/js": "^10.0.1",
48
+ "@openai/codex-sdk": "0.130.0",
49
+ "@typescript/native-preview": "^7.0.0-dev.20260506.1",
49
50
  "bun-types": "^1.3.13",
50
51
  "eslint": "^10.3.0",
51
52
  "globals": "^17.6.0",
@@ -53,7 +54,6 @@
53
54
  "prettier": "^3.8.3",
54
55
  "typescript": "^6.0.3",
55
56
  "typescript-eslint": "^8.59.2",
56
- "@typescript/native-preview": "^7.0.0-dev.20260506.1",
57
57
  "vitest": "^4.1.5"
58
58
  },
59
59
  "scripts": {
package/src/agent.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ocpipe agent integration.
3
3
  *
4
- * Dispatches to OpenCode CLI, Claude Code SDK, or Codex CLI based on backend
4
+ * Dispatches to OpenCode CLI, Claude Code SDK, or Codex SDK based on backend
5
5
  * configuration.
6
6
  */
7
7
 
package/src/codex.ts CHANGED
@@ -1,14 +1,22 @@
1
1
  /**
2
- * ocpipe Codex CLI integration.
2
+ * ocpipe Codex SDK integration.
3
3
  *
4
- * Runs Codex non-interactively through `codex exec`.
4
+ * Runs Codex through @openai/codex-sdk threads.
5
5
  */
6
6
 
7
- import { spawn } from 'child_process'
8
- import { mkdir, readFile, unlink } from 'fs/promises'
9
- import { join } from 'path'
10
- import { PROJECT_ROOT, TMP_DIR } from './paths.js'
11
- import type { RunAgentOptions, RunAgentResult } from './types.js'
7
+ import {
8
+ Codex,
9
+ type CodexOptions as CodexSdkClientOptions,
10
+ type RunResult,
11
+ type ThreadOptions,
12
+ } from '@openai/codex-sdk'
13
+ import { PROJECT_ROOT } from './paths.js'
14
+ import type {
15
+ CodexOptions,
16
+ CodexRunSummary,
17
+ RunAgentOptions,
18
+ RunAgentResult,
19
+ } from './types.js'
12
20
 
13
21
  class CodexLogFilter {
14
22
  private buf = ''
@@ -79,157 +87,200 @@ export async function runCodexAgent(
79
87
  signal,
80
88
  } = options
81
89
 
82
- if (sessionId) {
83
- throw new Error('Codex backend does not support session resume yet')
84
- }
85
90
  if (signal?.aborted) {
86
91
  throw new Error('Request aborted')
87
92
  }
88
93
 
89
94
  const cwd = workdir ?? PROJECT_ROOT
90
- await mkdir(TMP_DIR, { recursive: true })
91
- const stamp = `${Date.now()}_${Math.random().toString(36).slice(2)}`
92
- const outputFile = join(TMP_DIR, `codex_output_${stamp}.txt`)
93
-
94
- const cmd = codex?.pathToCodexExecutable ?? 'codex'
95
- const args = [
96
- 'exec',
97
- '--color',
98
- 'never',
99
- '--model',
100
- model.modelID,
101
- '--sandbox',
102
- codex?.sandbox ?? 'read-only',
103
- '--cd',
104
- cwd,
105
- '--output-last-message',
106
- outputFile,
107
- ]
108
-
109
- if (codex?.ephemeral ?? true) {
110
- args.push('--ephemeral')
111
- }
112
- if (codex?.ignoreUserConfig) {
113
- args.push('--ignore-user-config')
114
- }
115
- if (codex?.ignoreRules) {
116
- args.push('--ignore-rules')
117
- }
118
- if (codex?.reasoningEffort) {
119
- args.push('-c', `model_reasoning_effort="${codex.reasoningEffort}"`)
120
- }
121
- for (const dir of codex?.addDirs ?? []) {
122
- args.push('--add-dir', dir)
123
- }
124
- for (const [key, value] of Object.entries(codex?.config ?? {})) {
125
- args.push('-c', `${key}=${value}`)
126
- }
127
- args.push('-')
95
+ const client = new Codex(buildCodexClientOptions(codex))
96
+ const threadOptions = buildCodexThreadOptions(model.modelID, cwd, codex)
97
+ const thread =
98
+ sessionId && !codex?.ephemeral ?
99
+ client.resumeThread(sessionId, threadOptions)
100
+ : client.startThread(threadOptions)
128
101
 
129
102
  const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
103
+ const sessionInfo =
104
+ sessionId && !codex?.ephemeral ? `[thread:${sessionId}]` : '[new thread]'
130
105
  console.error(
131
- `\n>>> Codex [${model.modelID}] [new session]: ${promptPreview}...`,
106
+ `\n>>> Codex SDK [${model.modelID}] ${sessionInfo}: ${promptPreview}...`,
132
107
  )
133
108
 
134
- return new Promise((resolve, reject) => {
135
- const proc = spawn(cmd, args, {
136
- cwd,
137
- stdio: ['pipe', 'pipe', 'pipe'],
138
- })
139
-
140
- const stderrChunks: string[] = []
141
- const stdoutFilter = new CodexLogFilter()
142
- const stderrFilter = new CodexLogFilter()
143
- let aborted = false
109
+ const abort = new AbortController()
110
+ let timedOut = false
111
+ const abortHandler = () => abort.abort()
112
+ signal?.addEventListener('abort', abortHandler, { once: true })
113
+ const timeout =
114
+ timeoutSec > 0 ?
115
+ setTimeout(() => {
116
+ timedOut = true
117
+ abort.abort()
118
+ }, timeoutSec * 1000)
119
+ : null
144
120
 
145
- const cleanup = async () => {
146
- await unlink(outputFile).catch(() => {})
121
+ try {
122
+ const result = await thread.run(prompt, { signal: abort.signal })
123
+ const response = result.finalResponse.trim()
124
+ if (!response) {
125
+ throw new Error('Codex returned an empty response')
147
126
  }
148
127
 
149
- const abortHandler = () => {
150
- if (aborted) return
151
- aborted = true
152
- console.error('\n[abort] Killing Codex subprocess...')
153
- proc.kill('SIGTERM')
154
- setTimeout(() => {
155
- if (!proc.killed) proc.kill('SIGKILL')
156
- }, 1000)
157
- void cleanup()
158
- reject(new Error('Request aborted'))
128
+ const nextSessionId = codex?.ephemeral ? '' : (thread.id ?? '')
129
+ const sessionStr = nextSessionId || 'none'
130
+ const runSummary = buildCodexRunSummary(result)
131
+ console.error(
132
+ `<<< Codex SDK done [thread:${sessionStr}]\n${formatCodexRunSummary(runSummary)}`,
133
+ )
134
+
135
+ return {
136
+ text: response,
137
+ sessionId: nextSessionId,
138
+ runSummary,
139
+ }
140
+ } catch (err) {
141
+ if (timedOut) {
142
+ throw new Error(`Timeout after ${timeoutSec}s`, { cause: err })
159
143
  }
160
- signal?.addEventListener('abort', abortHandler, { once: true })
144
+ if (signal?.aborted) {
145
+ throw new Error('Request aborted', { cause: err })
146
+ }
147
+ throw err
148
+ } finally {
149
+ if (timeout) clearTimeout(timeout)
150
+ signal?.removeEventListener('abort', abortHandler)
151
+ }
152
+ }
161
153
 
162
- proc.stdout.on('data', (data: Buffer) => {
163
- const text = stdoutFilter.write(data.toString())
164
- if (text) {
165
- process.stderr.write(text)
166
- }
167
- })
168
- proc.stderr.on('data', (data: Buffer) => {
169
- const text = stderrFilter.write(data.toString())
170
- stderrChunks.push(text)
171
- if (text) {
172
- process.stderr.write(text)
173
- }
174
- })
175
-
176
- const timeout =
177
- timeoutSec > 0 ?
178
- setTimeout(() => {
179
- proc.kill()
180
- void cleanup()
181
- reject(new Error(`Timeout after ${timeoutSec}s`))
182
- }, timeoutSec * 1000)
183
- : null
184
-
185
- proc.stdin.end(prompt)
186
-
187
- proc.on('close', async (code) => {
188
- if (timeout) clearTimeout(timeout)
189
- signal?.removeEventListener('abort', abortHandler)
190
- if (aborted) return
191
-
192
- const stdoutTail = stdoutFilter.flush()
193
- if (stdoutTail) {
194
- process.stderr.write(stdoutTail)
195
- }
196
- const stderrTail = stderrFilter.flush()
197
- if (stderrTail) {
198
- stderrChunks.push(stderrTail)
199
- process.stderr.write(stderrTail)
200
- }
201
- const stderr = stderrChunks.join('').trim()
202
- if (code !== 0) {
203
- await cleanup()
204
- const detail =
205
- stderr ? `\n${stderr.split('\n').slice(-10).join('\n')}` : ''
206
- reject(new Error(`Codex exited with code ${code}${detail}`))
207
- return
208
- }
154
+ /** buildCodexRunSummary projects a Codex turn into a parent-readable summary. */
155
+ export function buildCodexRunSummary(result: RunResult): CodexRunSummary {
156
+ const commands: CodexRunSummary['commands'] = []
157
+ const fileChanges: CodexRunSummary['fileChanges'] = []
158
+ let errorMessage = ''
159
+ let finalMessage = result.finalResponse.trim()
209
160
 
210
- try {
211
- const response = (await readFile(outputFile, 'utf8')).trim()
212
- await cleanup()
213
- if (!response) {
214
- reject(new Error('Codex returned an empty response'))
215
- return
216
- }
217
- console.error(`<<< Codex done (${response.length} chars)`)
218
- resolve({
219
- text: response,
220
- sessionId: '',
161
+ for (const item of result.items) {
162
+ switch (item.type) {
163
+ case 'agent_message':
164
+ if (item.text) finalMessage = item.text
165
+ break
166
+ case 'command_execution':
167
+ commands.push({
168
+ command: item.command,
169
+ status: item.status,
170
+ exitCode: item.exit_code ?? null,
221
171
  })
222
- } catch (err) {
223
- await cleanup()
224
- reject(err)
225
- }
226
- })
227
-
228
- proc.on('error', async (err) => {
229
- if (timeout) clearTimeout(timeout)
230
- signal?.removeEventListener('abort', abortHandler)
231
- await cleanup()
232
- reject(err)
233
- })
234
- })
172
+ break
173
+ case 'file_change':
174
+ for (const change of item.changes) {
175
+ fileChanges.push({
176
+ path: change.path,
177
+ kind: change.kind,
178
+ status: item.status,
179
+ })
180
+ }
181
+ break
182
+ case 'error':
183
+ errorMessage = item.message
184
+ break
185
+ }
186
+ }
187
+
188
+ const usage = result.usage
189
+ return {
190
+ status: errorMessage ? 'failed' : 'completed',
191
+ finalMessage: finalMessage.trim(),
192
+ errorMessage,
193
+ commands,
194
+ fileChanges,
195
+ tokens:
196
+ usage ?
197
+ {
198
+ input: usage.input_tokens,
199
+ cached: usage.cached_input_tokens,
200
+ output: usage.output_tokens,
201
+ reasoning: usage.reasoning_output_tokens,
202
+ }
203
+ : null,
204
+ }
205
+ }
206
+
207
+ /** formatCodexRunSummary renders a Codex run summary as a clean text block. */
208
+ export function formatCodexRunSummary(summary: CodexRunSummary): string {
209
+ const lines: string[] = [`status: ${summary.status}`]
210
+ if (summary.errorMessage) {
211
+ lines.push(`error: ${summary.errorMessage}`)
212
+ }
213
+ if (summary.commands.length > 0) {
214
+ const failed = summary.commands.filter(isFailedCommand).length
215
+ lines.push(
216
+ `commands: ${summary.commands.length} completed, ${failed} failed`,
217
+ )
218
+ }
219
+ if (summary.fileChanges.length > 0) {
220
+ let add = 0
221
+ let update = 0
222
+ let del = 0
223
+ for (const change of summary.fileChanges) {
224
+ if (change.kind === 'add') add++
225
+ else if (change.kind === 'delete') del++
226
+ else update++
227
+ }
228
+ lines.push(`files: add=${add} update=${update} delete=${del}`)
229
+ }
230
+ if (summary.tokens) {
231
+ const t = summary.tokens
232
+ lines.push(
233
+ `tokens: input=${t.input} cached=${t.cached} output=${t.output} reasoning=${t.reasoning}`,
234
+ )
235
+ }
236
+ if (summary.finalMessage) {
237
+ lines.push(`final_message:\n${summary.finalMessage}`)
238
+ }
239
+ return lines.join('\n')
240
+ }
241
+
242
+ function isFailedCommand(command: CodexRunSummary['commands'][number]): boolean {
243
+ return (
244
+ command.status === 'failed' ||
245
+ (command.exitCode !== null && command.exitCode !== 0)
246
+ )
247
+ }
248
+
249
+ function buildCodexClientOptions(
250
+ codex: CodexOptions | undefined,
251
+ ): CodexSdkClientOptions {
252
+ return {
253
+ ...(codex?.pathToCodexExecutable ?
254
+ { codexPathOverride: codex.pathToCodexExecutable }
255
+ : {}),
256
+ ...(codex?.baseUrl ? { baseUrl: codex.baseUrl } : {}),
257
+ ...(codex?.apiKey ? { apiKey: codex.apiKey } : {}),
258
+ ...(codex?.env ? { env: codex.env } : {}),
259
+ ...(codex?.config ? { config: codex.config } : {}),
260
+ }
261
+ }
262
+
263
+ function buildCodexThreadOptions(
264
+ modelID: string,
265
+ cwd: string,
266
+ codex: CodexOptions | undefined,
267
+ ): ThreadOptions {
268
+ return {
269
+ model: modelID,
270
+ workingDirectory: cwd,
271
+ skipGitRepoCheck: true,
272
+ sandboxMode: codex?.sandbox ?? 'read-only',
273
+ ...(codex?.reasoningEffort ?
274
+ { modelReasoningEffort: codex.reasoningEffort }
275
+ : {}),
276
+ ...(codex?.addDirs ? { additionalDirectories: codex.addDirs } : {}),
277
+ ...(codex?.approvalPolicy ? { approvalPolicy: codex.approvalPolicy } : {}),
278
+ ...(codex?.networkAccessEnabled !== undefined ?
279
+ { networkAccessEnabled: codex.networkAccessEnabled }
280
+ : {}),
281
+ ...(codex?.webSearchMode ? { webSearchMode: codex.webSearchMode } : {}),
282
+ ...(codex?.webSearchEnabled !== undefined ?
283
+ { webSearchEnabled: codex.webSearchEnabled }
284
+ : {}),
285
+ }
235
286
  }
package/src/index.ts CHANGED
@@ -50,6 +50,7 @@ export { createSessionId, createBaseState, extendBaseState } from './state.js'
50
50
 
51
51
  // Agent integration
52
52
  export { runAgent, logStep } from './agent.js'
53
+ export { buildCodexRunSummary, formatCodexRunSummary } from './codex.js'
53
54
 
54
55
  // Response parsing
55
56
  export {
@@ -91,7 +92,12 @@ export type {
91
92
  BackendType,
92
93
  PermissionMode,
93
94
  ClaudeCodeOptions,
95
+ CodexApprovalPolicy,
96
+ CodexConfigValue,
94
97
  CodexOptions,
98
+ CodexReasoningEffort,
99
+ CodexSandboxMode,
100
+ CodexWebSearchMode,
95
101
  ModelConfig,
96
102
  ExecutionContext,
97
103
  StepResult,
@@ -111,6 +117,10 @@ export type {
111
117
  // Agent types
112
118
  RunAgentOptions,
113
119
  RunAgentResult,
120
+ CodexRunSummary,
121
+ CodexRunCommand,
122
+ CodexRunFileChange,
123
+ CodexRunTokens,
114
124
  // Correction types
115
125
  CorrectionMethod,
116
126
  CorrectionConfig,
package/src/types.ts CHANGED
@@ -11,6 +11,38 @@ import type { z } from 'zod/v4'
11
11
  /** Backend type for running agents. */
12
12
  export type BackendType = 'opencode' | 'claude-code' | 'codex'
13
13
 
14
+ /** Reasoning effort for Codex SDK threads. */
15
+ export type CodexReasoningEffort =
16
+ | 'minimal'
17
+ | 'low'
18
+ | 'medium'
19
+ | 'high'
20
+ | 'xhigh'
21
+
22
+ /** Codex sandbox mode. */
23
+ export type CodexSandboxMode =
24
+ | 'read-only'
25
+ | 'workspace-write'
26
+ | 'danger-full-access'
27
+
28
+ /** Codex approval policy. */
29
+ export type CodexApprovalPolicy =
30
+ | 'never'
31
+ | 'on-request'
32
+ | 'on-failure'
33
+ | 'untrusted'
34
+
35
+ /** Codex web search mode. */
36
+ export type CodexWebSearchMode = 'disabled' | 'cached' | 'live'
37
+
38
+ /** Codex SDK config override value. */
39
+ export type CodexConfigValue =
40
+ | string
41
+ | number
42
+ | boolean
43
+ | CodexConfigValue[]
44
+ | { [key: string]: CodexConfigValue }
45
+
14
46
  /** Permission mode for Claude Code sessions. */
15
47
  export type PermissionMode =
16
48
  | 'default'
@@ -57,24 +89,34 @@ export interface ClaudeCodeOptions {
57
89
  allowedTools?: string[]
58
90
  }
59
91
 
60
- /** Codex CLI specific session options. */
92
+ /** Codex SDK specific session options. */
61
93
  export interface CodexOptions {
62
- /** Path to Codex executable (default: `codex` from PATH). */
94
+ /** Path override for the Codex executable used by the SDK. */
63
95
  pathToCodexExecutable?: string
64
- /** Sandbox mode for `codex exec` (default: `read-only`). */
65
- sandbox?: 'read-only' | 'workspace-write' | 'danger-full-access'
66
- /** Extra config overrides passed as repeated `-c key=value` flags. */
67
- config?: Record<string, string>
68
- /** Reasoning effort passed to Codex (for example: `low`, `medium`, `high`, `xhigh`). */
69
- reasoningEffort?: string
70
- /** Run without persisting Codex session files (default: true). */
96
+ /** Base URL passed to the Codex SDK. */
97
+ baseUrl?: string
98
+ /** API key passed to the Codex SDK. */
99
+ apiKey?: string
100
+ /** Environment variables passed to the Codex subprocess. */
101
+ env?: Record<string, string>
102
+ /** Sandbox mode for Codex (default: `read-only`). */
103
+ sandbox?: CodexSandboxMode
104
+ /** Extra config overrides passed through the Codex SDK. */
105
+ config?: { [key: string]: CodexConfigValue }
106
+ /** Reasoning effort passed to Codex. */
107
+ reasoningEffort?: CodexReasoningEffort
108
+ /** Start a new SDK thread instead of resuming the provided session ID. */
71
109
  ephemeral?: boolean
72
- /** Ignore user config for deterministic automation (default: false). */
73
- ignoreUserConfig?: boolean
74
- /** Ignore user and project execpolicy rules (default: false). */
75
- ignoreRules?: boolean
76
110
  /** Additional directories Codex may access alongside the working directory. */
77
111
  addDirs?: string[]
112
+ /** Approval policy passed to Codex. */
113
+ approvalPolicy?: CodexApprovalPolicy
114
+ /** Enable network access in workspace-write sandboxes. */
115
+ networkAccessEnabled?: boolean
116
+ /** Web search mode passed to Codex. */
117
+ webSearchMode?: CodexWebSearchMode
118
+ /** Legacy boolean web search control passed to Codex. */
119
+ webSearchEnabled?: boolean
78
120
  }
79
121
 
80
122
  /** Model configuration for LLM backends. */
@@ -108,9 +150,9 @@ export interface ExecutionContext {
108
150
  workdir?: string
109
151
  /** Claude Code specific options. */
110
152
  claudeCode?: ClaudeCodeOptions
111
- /** Codex CLI specific options. */
153
+ /** Codex SDK specific options. */
112
154
  codex?: CodexOptions
113
- /** AbortSignal for cancelling requests. When aborted, kills subprocesses. */
155
+ /** AbortSignal for cancelling in-flight backend requests. */
114
156
  signal?: AbortSignal
115
157
  }
116
158
 
@@ -328,7 +370,7 @@ export interface PipelineConfig {
328
370
  workdir?: string
329
371
  /** Claude Code specific options. */
330
372
  claudeCode?: ClaudeCodeOptions
331
- /** Codex CLI specific options. */
373
+ /** Codex SDK specific options. */
332
374
  codex?: CodexOptions
333
375
  }
334
376
 
@@ -364,9 +406,9 @@ export interface RunAgentOptions {
364
406
  workdir?: string
365
407
  /** Claude Code specific options. */
366
408
  claudeCode?: ClaudeCodeOptions
367
- /** Codex CLI specific options. */
409
+ /** Codex SDK specific options. */
368
410
  codex?: CodexOptions
369
- /** AbortSignal for cancelling the request. When aborted, kills the subprocess. */
411
+ /** AbortSignal for cancelling the request. */
370
412
  signal?: AbortSignal
371
413
  }
372
414
 
@@ -376,4 +418,38 @@ export interface RunAgentResult {
376
418
  text: string
377
419
  /** Session ID (for continuing the conversation). */
378
420
  sessionId: string
421
+ /** Clean run summary, set for the Codex backend. */
422
+ runSummary?: CodexRunSummary
423
+ }
424
+
425
+ /** CodexRunCommand records one command the Codex agent executed. */
426
+ export interface CodexRunCommand {
427
+ command: string
428
+ status: string
429
+ exitCode: number | null
430
+ }
431
+
432
+ /** CodexRunFileChange records one file the Codex agent changed. */
433
+ export interface CodexRunFileChange {
434
+ path: string
435
+ kind: 'add' | 'update' | 'delete'
436
+ status: string
437
+ }
438
+
439
+ /** CodexRunTokens records token usage for a Codex turn. */
440
+ export interface CodexRunTokens {
441
+ input: number
442
+ cached: number
443
+ output: number
444
+ reasoning: number
445
+ }
446
+
447
+ /** CodexRunSummary is the parent-readable projection of a Codex turn. */
448
+ export interface CodexRunSummary {
449
+ status: 'completed' | 'failed' | 'unknown'
450
+ finalMessage: string
451
+ errorMessage: string
452
+ commands: CodexRunCommand[]
453
+ fileChanges: CodexRunFileChange[]
454
+ tokens: CodexRunTokens | null
379
455
  }