codex-sdk-py 0.0.3__py3-none-any.whl
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.
- codex_sdk/__init__.py +135 -0
- codex_sdk/codex.py +79 -0
- codex_sdk/codex_options.py +53 -0
- codex_sdk/events.py +107 -0
- codex_sdk/exec.py +396 -0
- codex_sdk/items.py +175 -0
- codex_sdk/output_schema_file.py +74 -0
- codex_sdk/py.typed +0 -0
- codex_sdk/thread.py +256 -0
- codex_sdk/thread_options.py +79 -0
- codex_sdk/turn_options.py +24 -0
- codex_sdk_py-0.0.3.dist-info/METADATA +423 -0
- codex_sdk_py-0.0.3.dist-info/RECORD +14 -0
- codex_sdk_py-0.0.3.dist-info/WHEEL +4 -0
codex_sdk/exec.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Codex CLI execution management.
|
|
3
|
+
|
|
4
|
+
Corresponds to: src/exec.ts
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import AsyncGenerator
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .codex_options import CodexConfigObject, CodexConfigValue
|
|
21
|
+
from .thread_options import ApprovalMode, ModelReasoningEffort, SandboxMode, WebSearchMode
|
|
22
|
+
|
|
23
|
+
INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
|
|
24
|
+
PYTHON_SDK_ORIGINATOR = "codex_sdk_py"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CodexExecArgs:
|
|
29
|
+
"""Arguments for executing the Codex CLI."""
|
|
30
|
+
|
|
31
|
+
input: str
|
|
32
|
+
"""The prompt to send to the agent."""
|
|
33
|
+
|
|
34
|
+
base_url: str | None = None
|
|
35
|
+
api_key: str | None = None
|
|
36
|
+
thread_id: str | None = None
|
|
37
|
+
images: list[str] | None = None
|
|
38
|
+
|
|
39
|
+
# --model
|
|
40
|
+
model: str | None = None
|
|
41
|
+
# --sandbox
|
|
42
|
+
sandbox_mode: SandboxMode | None = None
|
|
43
|
+
# --cd
|
|
44
|
+
working_directory: str | None = None
|
|
45
|
+
# --add-dir
|
|
46
|
+
additional_directories: list[str] | None = None
|
|
47
|
+
# --skip-git-repo-check
|
|
48
|
+
skip_git_repo_check: bool | None = None
|
|
49
|
+
# --output-schema
|
|
50
|
+
output_schema_file: str | None = None
|
|
51
|
+
# --config model_reasoning_effort
|
|
52
|
+
model_reasoning_effort: ModelReasoningEffort | None = None
|
|
53
|
+
# Cancel event to cancel the execution
|
|
54
|
+
cancel_event: asyncio.Event | None = None
|
|
55
|
+
# --config sandbox_workspace_write.network_access
|
|
56
|
+
network_access_enabled: bool | None = None
|
|
57
|
+
# --config web_search
|
|
58
|
+
web_search_mode: WebSearchMode | None = None
|
|
59
|
+
# legacy --config features.web_search_request
|
|
60
|
+
web_search_enabled: bool | None = None
|
|
61
|
+
# --config approval_policy
|
|
62
|
+
approval_policy: ApprovalMode | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CodexExec:
|
|
66
|
+
"""
|
|
67
|
+
Internal class that spawns and manages the Codex CLI process.
|
|
68
|
+
|
|
69
|
+
Handles subprocess lifecycle, environment setup, and JSONL streaming.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
executable_path: str | None = None,
|
|
75
|
+
env: dict[str, str] | None = None,
|
|
76
|
+
config_overrides: CodexConfigObject | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Initialize the CodexExec instance.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
executable_path: Custom path to the Codex CLI binary.
|
|
83
|
+
env: Environment variables to pass to the subprocess.
|
|
84
|
+
config_overrides: Additional --config overrides.
|
|
85
|
+
"""
|
|
86
|
+
self._executable_path = executable_path or _find_codex_path()
|
|
87
|
+
self._env_override = env
|
|
88
|
+
self._config_overrides = config_overrides
|
|
89
|
+
|
|
90
|
+
async def run(self, args: CodexExecArgs) -> AsyncGenerator[str, None]:
|
|
91
|
+
"""
|
|
92
|
+
Execute the Codex CLI and yield JSONL lines from stdout.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
args: Execution arguments including prompt and options.
|
|
96
|
+
|
|
97
|
+
Yields:
|
|
98
|
+
JSONL lines from the CLI stdout.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
RuntimeError: If the CLI process fails.
|
|
102
|
+
asyncio.CancelledError: If cancelled via cancel_event.
|
|
103
|
+
"""
|
|
104
|
+
command_args = self._build_command_args(args)
|
|
105
|
+
env = self._build_env(args)
|
|
106
|
+
|
|
107
|
+
process = await asyncio.create_subprocess_exec(
|
|
108
|
+
self._executable_path,
|
|
109
|
+
*command_args,
|
|
110
|
+
stdin=asyncio.subprocess.PIPE,
|
|
111
|
+
stdout=asyncio.subprocess.PIPE,
|
|
112
|
+
stderr=asyncio.subprocess.PIPE,
|
|
113
|
+
env=env,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
stderr_chunks: list[bytes] = []
|
|
117
|
+
|
|
118
|
+
async def read_stderr() -> None:
|
|
119
|
+
"""Read stderr in background."""
|
|
120
|
+
if process.stderr:
|
|
121
|
+
while True:
|
|
122
|
+
chunk = await process.stderr.read(4096)
|
|
123
|
+
if not chunk:
|
|
124
|
+
break
|
|
125
|
+
stderr_chunks.append(chunk)
|
|
126
|
+
|
|
127
|
+
stderr_task = asyncio.create_task(read_stderr())
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
# Write input to stdin and close
|
|
131
|
+
if process.stdin:
|
|
132
|
+
process.stdin.write(args.input.encode("utf-8"))
|
|
133
|
+
await process.stdin.drain()
|
|
134
|
+
process.stdin.close()
|
|
135
|
+
await process.stdin.wait_closed()
|
|
136
|
+
|
|
137
|
+
# Stream stdout line by line
|
|
138
|
+
if process.stdout:
|
|
139
|
+
buffer = b""
|
|
140
|
+
while True:
|
|
141
|
+
# Check for cancellation
|
|
142
|
+
if args.cancel_event and args.cancel_event.is_set():
|
|
143
|
+
process.terminate()
|
|
144
|
+
raise asyncio.CancelledError("Turn cancelled by user")
|
|
145
|
+
|
|
146
|
+
chunk = await process.stdout.read(4096)
|
|
147
|
+
if not chunk:
|
|
148
|
+
# Process remaining buffer
|
|
149
|
+
if buffer:
|
|
150
|
+
yield buffer.decode("utf-8")
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
buffer += chunk
|
|
154
|
+
while b"\n" in buffer:
|
|
155
|
+
line, buffer = buffer.split(b"\n", 1)
|
|
156
|
+
decoded = line.decode("utf-8")
|
|
157
|
+
if decoded:
|
|
158
|
+
yield decoded
|
|
159
|
+
|
|
160
|
+
# Wait for process to complete
|
|
161
|
+
await process.wait()
|
|
162
|
+
await stderr_task
|
|
163
|
+
|
|
164
|
+
# Check for errors
|
|
165
|
+
if process.returncode != 0:
|
|
166
|
+
stderr_output = b"".join(stderr_chunks).decode("utf-8")
|
|
167
|
+
detail = f"code {process.returncode}"
|
|
168
|
+
raise RuntimeError(f"Codex Exec exited with {detail}: {stderr_output}")
|
|
169
|
+
|
|
170
|
+
finally:
|
|
171
|
+
stderr_task.cancel()
|
|
172
|
+
try:
|
|
173
|
+
await stderr_task
|
|
174
|
+
except asyncio.CancelledError:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
if process.returncode is None:
|
|
178
|
+
process.terminate()
|
|
179
|
+
try:
|
|
180
|
+
await asyncio.wait_for(process.wait(), timeout=5.0)
|
|
181
|
+
except asyncio.TimeoutError:
|
|
182
|
+
process.kill()
|
|
183
|
+
await process.wait()
|
|
184
|
+
|
|
185
|
+
def _build_command_args(self, args: CodexExecArgs) -> list[str]:
|
|
186
|
+
"""Build command line arguments for the CLI."""
|
|
187
|
+
command_args: list[str] = ["exec", "--experimental-json"]
|
|
188
|
+
|
|
189
|
+
# Add config overrides
|
|
190
|
+
if self._config_overrides:
|
|
191
|
+
for override in _serialize_config_overrides(self._config_overrides):
|
|
192
|
+
command_args.extend(["--config", override])
|
|
193
|
+
|
|
194
|
+
if args.model:
|
|
195
|
+
command_args.extend(["--model", args.model])
|
|
196
|
+
|
|
197
|
+
if args.sandbox_mode:
|
|
198
|
+
command_args.extend(["--sandbox", args.sandbox_mode])
|
|
199
|
+
|
|
200
|
+
if args.working_directory:
|
|
201
|
+
command_args.extend(["--cd", args.working_directory])
|
|
202
|
+
|
|
203
|
+
if args.additional_directories:
|
|
204
|
+
for dir_path in args.additional_directories:
|
|
205
|
+
command_args.extend(["--add-dir", dir_path])
|
|
206
|
+
|
|
207
|
+
if args.skip_git_repo_check:
|
|
208
|
+
command_args.append("--skip-git-repo-check")
|
|
209
|
+
|
|
210
|
+
if args.output_schema_file:
|
|
211
|
+
command_args.extend(["--output-schema", args.output_schema_file])
|
|
212
|
+
|
|
213
|
+
if args.model_reasoning_effort:
|
|
214
|
+
command_args.extend(
|
|
215
|
+
["--config", f'model_reasoning_effort="{args.model_reasoning_effort}"']
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if args.network_access_enabled is not None:
|
|
219
|
+
value = "true" if args.network_access_enabled else "false"
|
|
220
|
+
command_args.extend(
|
|
221
|
+
["--config", f"sandbox_workspace_write.network_access={value}"]
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if args.web_search_mode:
|
|
225
|
+
command_args.extend(["--config", f'web_search="{args.web_search_mode}"'])
|
|
226
|
+
elif args.web_search_enabled is True:
|
|
227
|
+
command_args.extend(["--config", 'web_search="live"'])
|
|
228
|
+
elif args.web_search_enabled is False:
|
|
229
|
+
command_args.extend(["--config", 'web_search="disabled"'])
|
|
230
|
+
|
|
231
|
+
if args.approval_policy:
|
|
232
|
+
command_args.extend(["--config", f'approval_policy="{args.approval_policy}"'])
|
|
233
|
+
|
|
234
|
+
if args.images:
|
|
235
|
+
for image in args.images:
|
|
236
|
+
command_args.extend(["--image", image])
|
|
237
|
+
|
|
238
|
+
if args.thread_id:
|
|
239
|
+
command_args.extend(["resume", args.thread_id])
|
|
240
|
+
|
|
241
|
+
return command_args
|
|
242
|
+
|
|
243
|
+
def _build_env(self, args: CodexExecArgs) -> dict[str, str]:
|
|
244
|
+
"""Build environment variables for the subprocess."""
|
|
245
|
+
env: dict[str, str] = {}
|
|
246
|
+
|
|
247
|
+
if self._env_override is not None:
|
|
248
|
+
env.update(self._env_override)
|
|
249
|
+
else:
|
|
250
|
+
for key, value in os.environ.items():
|
|
251
|
+
if value is not None:
|
|
252
|
+
env[key] = value
|
|
253
|
+
|
|
254
|
+
if INTERNAL_ORIGINATOR_ENV not in env:
|
|
255
|
+
env[INTERNAL_ORIGINATOR_ENV] = PYTHON_SDK_ORIGINATOR
|
|
256
|
+
|
|
257
|
+
if args.base_url:
|
|
258
|
+
env["OPENAI_BASE_URL"] = args.base_url
|
|
259
|
+
|
|
260
|
+
if args.api_key:
|
|
261
|
+
env["CODEX_API_KEY"] = args.api_key
|
|
262
|
+
|
|
263
|
+
return env
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _serialize_config_overrides(config_overrides: CodexConfigObject) -> list[str]:
|
|
267
|
+
"""Serialize config overrides to CLI format."""
|
|
268
|
+
overrides: list[str] = []
|
|
269
|
+
_flatten_config_overrides(config_overrides, "", overrides)
|
|
270
|
+
return overrides
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _flatten_config_overrides(
|
|
274
|
+
value: CodexConfigValue,
|
|
275
|
+
prefix: str,
|
|
276
|
+
overrides: list[str],
|
|
277
|
+
) -> None:
|
|
278
|
+
"""Flatten nested config dict to dotted paths."""
|
|
279
|
+
if not _is_plain_object(value):
|
|
280
|
+
if prefix:
|
|
281
|
+
overrides.append(f"{prefix}={_to_toml_value(value, prefix)}")
|
|
282
|
+
return
|
|
283
|
+
else:
|
|
284
|
+
raise ValueError("Codex config overrides must be a plain object")
|
|
285
|
+
|
|
286
|
+
entries = list(value.items())
|
|
287
|
+
if not prefix and len(entries) == 0:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
if prefix and len(entries) == 0:
|
|
291
|
+
overrides.append(f"{prefix}={{}}")
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
for key, child in entries:
|
|
295
|
+
if not key:
|
|
296
|
+
raise ValueError("Codex config override keys must be non-empty strings")
|
|
297
|
+
if child is None:
|
|
298
|
+
continue
|
|
299
|
+
path = f"{prefix}.{key}" if prefix else key
|
|
300
|
+
if _is_plain_object(child):
|
|
301
|
+
_flatten_config_overrides(child, path, overrides)
|
|
302
|
+
else:
|
|
303
|
+
overrides.append(f"{path}={_to_toml_value(child, path)}")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _to_toml_value(value: CodexConfigValue, path: str) -> str:
|
|
307
|
+
"""Serialize a value as a TOML literal."""
|
|
308
|
+
if isinstance(value, str):
|
|
309
|
+
return json.dumps(value)
|
|
310
|
+
elif isinstance(value, bool):
|
|
311
|
+
return "true" if value else "false"
|
|
312
|
+
elif isinstance(value, (int, float)):
|
|
313
|
+
is_finite = isinstance(value, int) or (
|
|
314
|
+
isinstance(value, float)
|
|
315
|
+
and value != float("inf")
|
|
316
|
+
and value != float("-inf")
|
|
317
|
+
and value == value # NaN check
|
|
318
|
+
)
|
|
319
|
+
if not is_finite:
|
|
320
|
+
raise ValueError(f"Codex config override at {path} must be a finite number")
|
|
321
|
+
return str(value)
|
|
322
|
+
elif isinstance(value, list):
|
|
323
|
+
rendered = [_to_toml_value(item, f"{path}[{i}]") for i, item in enumerate(value)]
|
|
324
|
+
return f"[{', '.join(rendered)}]"
|
|
325
|
+
elif _is_plain_object(value):
|
|
326
|
+
parts: list[str] = []
|
|
327
|
+
for key, child in value.items():
|
|
328
|
+
if not key:
|
|
329
|
+
raise ValueError("Codex config override keys must be non-empty strings")
|
|
330
|
+
if child is None:
|
|
331
|
+
continue
|
|
332
|
+
parts.append(f"{_format_toml_key(key)} = {_to_toml_value(child, f'{path}.{key}')}")
|
|
333
|
+
return "{" + ", ".join(parts) + "}"
|
|
334
|
+
elif value is None:
|
|
335
|
+
raise ValueError(f"Codex config override at {path} cannot be null")
|
|
336
|
+
else:
|
|
337
|
+
type_name = type(value).__name__
|
|
338
|
+
raise ValueError(f"Unsupported Codex config override value at {path}: {type_name}")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
TOML_BARE_KEY_PATTERN = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _format_toml_key(key: str) -> str:
|
|
345
|
+
"""Format a key for TOML syntax."""
|
|
346
|
+
if key and all(c in TOML_BARE_KEY_PATTERN for c in key):
|
|
347
|
+
return key
|
|
348
|
+
return json.dumps(key)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _is_plain_object(value: Any) -> bool:
|
|
352
|
+
"""Check if value is a plain object (dict)."""
|
|
353
|
+
return isinstance(value, dict)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _find_codex_path() -> str:
|
|
357
|
+
"""Find the Codex binary path for the current platform."""
|
|
358
|
+
system = platform.system().lower()
|
|
359
|
+
machine = platform.machine().lower()
|
|
360
|
+
|
|
361
|
+
target_triple: str | None = None
|
|
362
|
+
|
|
363
|
+
if system in ("linux", "android"):
|
|
364
|
+
if machine in ("x86_64", "amd64"):
|
|
365
|
+
target_triple = "x86_64-unknown-linux-musl"
|
|
366
|
+
elif machine in ("arm64", "aarch64"):
|
|
367
|
+
target_triple = "aarch64-unknown-linux-musl"
|
|
368
|
+
|
|
369
|
+
elif system == "darwin":
|
|
370
|
+
if machine in ("x86_64", "amd64"):
|
|
371
|
+
target_triple = "x86_64-apple-darwin"
|
|
372
|
+
elif machine in ("arm64", "aarch64"):
|
|
373
|
+
target_triple = "aarch64-apple-darwin"
|
|
374
|
+
|
|
375
|
+
elif system == "win32" or system == "windows":
|
|
376
|
+
if machine in ("x86_64", "amd64"):
|
|
377
|
+
target_triple = "x86_64-pc-windows-msvc"
|
|
378
|
+
elif machine in ("arm64", "aarch64"):
|
|
379
|
+
target_triple = "aarch64-pc-windows-msvc"
|
|
380
|
+
|
|
381
|
+
if not target_triple:
|
|
382
|
+
raise RuntimeError(f"Unsupported platform: {system} ({machine})")
|
|
383
|
+
|
|
384
|
+
# Try to find bundled binary
|
|
385
|
+
script_dir = Path(__file__).parent
|
|
386
|
+
vendor_root = script_dir / "vendor"
|
|
387
|
+
arch_root = vendor_root / target_triple
|
|
388
|
+
|
|
389
|
+
binary_name = "codex.exe" if sys.platform == "win32" else "codex"
|
|
390
|
+
binary_path = arch_root / "codex" / binary_name
|
|
391
|
+
|
|
392
|
+
if binary_path.exists():
|
|
393
|
+
return str(binary_path)
|
|
394
|
+
|
|
395
|
+
# Fall back to PATH
|
|
396
|
+
return "codex"
|
codex_sdk/items.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thread item type definitions.
|
|
3
|
+
|
|
4
|
+
Based on item types from codex-rs/exec/src/exec_events.rs
|
|
5
|
+
|
|
6
|
+
Corresponds to: src/items.ts
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Literal, NotRequired, TypedDict
|
|
12
|
+
|
|
13
|
+
# Status types
|
|
14
|
+
CommandExecutionStatus = Literal["in_progress", "completed", "failed"]
|
|
15
|
+
"""The status of a command execution."""
|
|
16
|
+
|
|
17
|
+
PatchChangeKind = Literal["add", "delete", "update"]
|
|
18
|
+
"""Indicates the type of the file change."""
|
|
19
|
+
|
|
20
|
+
PatchApplyStatus = Literal["completed", "failed"]
|
|
21
|
+
"""The status of a file change."""
|
|
22
|
+
|
|
23
|
+
McpToolCallStatus = Literal["in_progress", "completed", "failed"]
|
|
24
|
+
"""The status of an MCP tool call."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# MCP-related types
|
|
28
|
+
class McpContentBlock(TypedDict, total=False):
|
|
29
|
+
"""Content block from MCP tool result."""
|
|
30
|
+
|
|
31
|
+
type: str
|
|
32
|
+
text: str
|
|
33
|
+
data: str
|
|
34
|
+
mime_type: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class McpToolResult(TypedDict):
|
|
38
|
+
"""Result payload returned by the MCP server for successful calls."""
|
|
39
|
+
|
|
40
|
+
content: list[McpContentBlock]
|
|
41
|
+
structured_content: Any
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class McpToolError(TypedDict):
|
|
45
|
+
"""Error message reported for failed calls."""
|
|
46
|
+
|
|
47
|
+
message: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Item types
|
|
51
|
+
class CommandExecutionItem(TypedDict):
|
|
52
|
+
"""A command executed by the agent."""
|
|
53
|
+
|
|
54
|
+
id: str
|
|
55
|
+
type: Literal["command_execution"]
|
|
56
|
+
command: str
|
|
57
|
+
"""The command line executed by the agent."""
|
|
58
|
+
aggregated_output: str
|
|
59
|
+
"""Aggregated stdout and stderr captured while the command was running."""
|
|
60
|
+
status: CommandExecutionStatus
|
|
61
|
+
"""Current status of the command execution."""
|
|
62
|
+
exit_code: NotRequired[int]
|
|
63
|
+
"""Set when the command exits; omitted while still running."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FileUpdateChange(TypedDict):
|
|
67
|
+
"""A set of file changes by the agent."""
|
|
68
|
+
|
|
69
|
+
path: str
|
|
70
|
+
kind: PatchChangeKind
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class FileChangeItem(TypedDict):
|
|
74
|
+
"""A set of file changes by the agent. Emitted once the patch succeeds or fails."""
|
|
75
|
+
|
|
76
|
+
id: str
|
|
77
|
+
type: Literal["file_change"]
|
|
78
|
+
changes: list[FileUpdateChange]
|
|
79
|
+
"""Individual file changes that comprise the patch."""
|
|
80
|
+
status: PatchApplyStatus
|
|
81
|
+
"""Whether the patch ultimately succeeded or failed."""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class McpToolCallItem(TypedDict):
|
|
85
|
+
"""
|
|
86
|
+
Represents a call to an MCP tool.
|
|
87
|
+
|
|
88
|
+
The item starts when the invocation is dispatched and completes
|
|
89
|
+
when the MCP server reports success or failure.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
id: str
|
|
93
|
+
type: Literal["mcp_tool_call"]
|
|
94
|
+
server: str
|
|
95
|
+
"""Name of the MCP server handling the request."""
|
|
96
|
+
tool: str
|
|
97
|
+
"""The tool invoked on the MCP server."""
|
|
98
|
+
arguments: Any
|
|
99
|
+
"""Arguments forwarded to the tool invocation."""
|
|
100
|
+
status: McpToolCallStatus
|
|
101
|
+
"""Current status of the tool invocation."""
|
|
102
|
+
result: NotRequired[McpToolResult]
|
|
103
|
+
"""Result payload returned by the MCP server for successful calls."""
|
|
104
|
+
error: NotRequired[McpToolError]
|
|
105
|
+
"""Error message reported for failed calls."""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class AgentMessageItem(TypedDict):
|
|
109
|
+
"""
|
|
110
|
+
Response from the agent.
|
|
111
|
+
|
|
112
|
+
Either natural-language text or JSON when structured output is requested.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
id: str
|
|
116
|
+
type: Literal["agent_message"]
|
|
117
|
+
text: str
|
|
118
|
+
"""Either natural-language text or JSON when structured output is requested."""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ReasoningItem(TypedDict):
|
|
122
|
+
"""Agent's reasoning summary."""
|
|
123
|
+
|
|
124
|
+
id: str
|
|
125
|
+
type: Literal["reasoning"]
|
|
126
|
+
text: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class WebSearchItem(TypedDict):
|
|
130
|
+
"""Captures a web search request. Completes when results are returned to the agent."""
|
|
131
|
+
|
|
132
|
+
id: str
|
|
133
|
+
type: Literal["web_search"]
|
|
134
|
+
query: str
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ErrorItem(TypedDict):
|
|
138
|
+
"""Describes a non-fatal error surfaced as an item."""
|
|
139
|
+
|
|
140
|
+
id: str
|
|
141
|
+
type: Literal["error"]
|
|
142
|
+
message: str
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TodoItem(TypedDict):
|
|
146
|
+
"""An item in the agent's to-do list."""
|
|
147
|
+
|
|
148
|
+
text: str
|
|
149
|
+
completed: bool
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TodoListItem(TypedDict):
|
|
153
|
+
"""
|
|
154
|
+
Tracks the agent's running to-do list.
|
|
155
|
+
|
|
156
|
+
Starts when the plan is issued, updates as steps change,
|
|
157
|
+
and completes when the turn ends.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
id: str
|
|
161
|
+
type: Literal["todo_list"]
|
|
162
|
+
items: list[TodoItem]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Canonical union of thread items and their type-specific payloads
|
|
166
|
+
ThreadItem = (
|
|
167
|
+
AgentMessageItem
|
|
168
|
+
| ReasoningItem
|
|
169
|
+
| CommandExecutionItem
|
|
170
|
+
| FileChangeItem
|
|
171
|
+
| McpToolCallItem
|
|
172
|
+
| WebSearchItem
|
|
173
|
+
| TodoListItem
|
|
174
|
+
| ErrorItem
|
|
175
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output schema file utilities.
|
|
3
|
+
|
|
4
|
+
Corresponds to: src/outputSchemaFile.ts
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import shutil
|
|
11
|
+
import tempfile
|
|
12
|
+
from collections.abc import Awaitable, Callable
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class OutputSchemaFile:
|
|
20
|
+
"""Result of creating an output schema file."""
|
|
21
|
+
|
|
22
|
+
schema_path: str | None
|
|
23
|
+
"""Path to the temporary schema file, or None if no schema was provided."""
|
|
24
|
+
|
|
25
|
+
cleanup: Callable[[], Awaitable[None]]
|
|
26
|
+
"""Async function to clean up the temporary file."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_json_object(value: Any) -> bool:
|
|
30
|
+
"""Check if value is a plain JSON object (dict)."""
|
|
31
|
+
return isinstance(value, dict)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def create_output_schema_file(schema: Any | None) -> OutputSchemaFile:
|
|
35
|
+
"""
|
|
36
|
+
Create a temporary file containing the JSON schema.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
schema: The JSON schema to write, or None if no schema is needed.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
OutputSchemaFile with the path and cleanup function.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If schema is not a plain JSON object.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
async def noop_cleanup() -> None:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
if schema is None:
|
|
52
|
+
return OutputSchemaFile(schema_path=None, cleanup=noop_cleanup)
|
|
53
|
+
|
|
54
|
+
if not _is_json_object(schema):
|
|
55
|
+
raise ValueError("output_schema must be a plain JSON object")
|
|
56
|
+
|
|
57
|
+
# Create temporary directory
|
|
58
|
+
schema_dir = Path(tempfile.mkdtemp(prefix="codex-output-schema-"))
|
|
59
|
+
schema_path = schema_dir / "schema.json"
|
|
60
|
+
|
|
61
|
+
async def cleanup() -> None:
|
|
62
|
+
try:
|
|
63
|
+
shutil.rmtree(schema_dir, ignore_errors=True)
|
|
64
|
+
except Exception:
|
|
65
|
+
# suppress errors during cleanup
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Write schema to file
|
|
70
|
+
schema_path.write_text(json.dumps(schema), encoding="utf-8")
|
|
71
|
+
return OutputSchemaFile(schema_path=str(schema_path), cleanup=cleanup)
|
|
72
|
+
except Exception:
|
|
73
|
+
await cleanup()
|
|
74
|
+
raise
|
codex_sdk/py.typed
ADDED
|
File without changes
|