codex-python 1.0.0__tar.gz → 1.0.0b3__tar.gz
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_python-1.0.0 → codex_python-1.0.0b3}/PKG-INFO +22 -4
- {codex_python-1.0.0 → codex_python-1.0.0b3}/README.md +21 -3
- {codex_python-1.0.0 → codex_python-1.0.0b3}/codex/__init__.py +18 -2
- {codex_python-1.0.0 → codex_python-1.0.0b3}/codex/codex.py +5 -1
- codex_python-1.0.0b3/codex/exec.py +313 -0
- {codex_python-1.0.0 → codex_python-1.0.0b3}/codex/items.py +26 -0
- codex_python-1.0.0b3/codex/options.py +57 -0
- {codex_python-1.0.0 → codex_python-1.0.0b3}/codex/thread.py +14 -14
- {codex_python-1.0.0 → codex_python-1.0.0b3}/crates/codex_native/Cargo.lock +1 -1
- {codex_python-1.0.0 → codex_python-1.0.0b3}/crates/codex_native/Cargo.toml +1 -1
- {codex_python-1.0.0 → codex_python-1.0.0b3}/pyproject.toml +1 -0
- codex_python-1.0.0/codex/exec.py +0 -118
- codex_python-1.0.0/codex/options.py +0 -27
- {codex_python-1.0.0 → codex_python-1.0.0b3}/LICENSE +0 -0
- {codex_python-1.0.0 → codex_python-1.0.0b3}/codex/_binary.py +0 -0
- {codex_python-1.0.0 → codex_python-1.0.0b3}/codex/errors.py +0 -0
- {codex_python-1.0.0 → codex_python-1.0.0b3}/codex/events.py +0 -0
- {codex_python-1.0.0 → codex_python-1.0.0b3}/codex/output_schema_file.py +0 -0
- {codex_python-1.0.0 → codex_python-1.0.0b3}/codex/py.typed +0 -0
- {codex_python-1.0.0 → codex_python-1.0.0b3}/codex/vendor/.gitkeep +0 -0
- {codex_python-1.0.0 → codex_python-1.0.0b3}/crates/codex_native/codex/__init__.py +0 -0
- {codex_python-1.0.0 → codex_python-1.0.0b3}/crates/codex_native/src/lib.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0b3
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -106,9 +106,27 @@ thread.run("Continue from previous context")
|
|
|
106
106
|
|
|
107
107
|
## Options
|
|
108
108
|
|
|
109
|
-
- `CodexOptions`: `codex_path_override`, `base_url`, `api_key`
|
|
110
|
-
- `ThreadOptions`: `model`, `sandbox_mode`, `working_directory`, `skip_git_repo_check`
|
|
111
|
-
- `TurnOptions`: `output_schema`
|
|
109
|
+
- `CodexOptions`: `codex_path_override`, `base_url`, `api_key`, `config`, `env`
|
|
110
|
+
- `ThreadOptions`: `model`, `sandbox_mode`, `working_directory`, `skip_git_repo_check`, `model_reasoning_effort`, `network_access_enabled`, `web_search_mode`, `web_search_enabled`, `approval_policy`, `additional_directories`
|
|
111
|
+
- `TurnOptions`: `output_schema`, `signal`
|
|
112
|
+
|
|
113
|
+
## Cancellation
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import threading
|
|
117
|
+
|
|
118
|
+
from codex import Codex, TurnOptions
|
|
119
|
+
|
|
120
|
+
cancel = threading.Event()
|
|
121
|
+
|
|
122
|
+
client = Codex()
|
|
123
|
+
thread = client.start_thread()
|
|
124
|
+
stream = thread.run_streamed("Long running task", TurnOptions(signal=cancel))
|
|
125
|
+
|
|
126
|
+
cancel.set()
|
|
127
|
+
for event in stream.events:
|
|
128
|
+
print(event)
|
|
129
|
+
```
|
|
112
130
|
|
|
113
131
|
## Bundled binary behavior
|
|
114
132
|
|
|
@@ -88,9 +88,27 @@ thread.run("Continue from previous context")
|
|
|
88
88
|
|
|
89
89
|
## Options
|
|
90
90
|
|
|
91
|
-
- `CodexOptions`: `codex_path_override`, `base_url`, `api_key`
|
|
92
|
-
- `ThreadOptions`: `model`, `sandbox_mode`, `working_directory`, `skip_git_repo_check`
|
|
93
|
-
- `TurnOptions`: `output_schema`
|
|
91
|
+
- `CodexOptions`: `codex_path_override`, `base_url`, `api_key`, `config`, `env`
|
|
92
|
+
- `ThreadOptions`: `model`, `sandbox_mode`, `working_directory`, `skip_git_repo_check`, `model_reasoning_effort`, `network_access_enabled`, `web_search_mode`, `web_search_enabled`, `approval_policy`, `additional_directories`
|
|
93
|
+
- `TurnOptions`: `output_schema`, `signal`
|
|
94
|
+
|
|
95
|
+
## Cancellation
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import threading
|
|
99
|
+
|
|
100
|
+
from codex import Codex, TurnOptions
|
|
101
|
+
|
|
102
|
+
cancel = threading.Event()
|
|
103
|
+
|
|
104
|
+
client = Codex()
|
|
105
|
+
thread = client.start_thread()
|
|
106
|
+
stream = thread.run_streamed("Long running task", TurnOptions(signal=cancel))
|
|
107
|
+
|
|
108
|
+
cancel.set()
|
|
109
|
+
for event in stream.events:
|
|
110
|
+
print(event)
|
|
111
|
+
```
|
|
94
112
|
|
|
95
113
|
## Bundled binary behavior
|
|
96
114
|
|
|
@@ -26,10 +26,21 @@ from codex.items import (
|
|
|
26
26
|
TodoListItem,
|
|
27
27
|
WebSearchItem,
|
|
28
28
|
)
|
|
29
|
-
from codex.options import
|
|
29
|
+
from codex.options import (
|
|
30
|
+
ApprovalMode,
|
|
31
|
+
CancelSignal,
|
|
32
|
+
CodexConfigObject,
|
|
33
|
+
CodexConfigValue,
|
|
34
|
+
CodexOptions,
|
|
35
|
+
ModelReasoningEffort,
|
|
36
|
+
SandboxMode,
|
|
37
|
+
ThreadOptions,
|
|
38
|
+
TurnOptions,
|
|
39
|
+
WebSearchMode,
|
|
40
|
+
)
|
|
30
41
|
from codex.thread import Input, RunResult, RunStreamedResult, Thread, UserInput
|
|
31
42
|
|
|
32
|
-
__version__ = "1.0.0"
|
|
43
|
+
__version__ = "1.0.0-beta.3"
|
|
33
44
|
|
|
34
45
|
__all__ = [
|
|
35
46
|
"Codex",
|
|
@@ -47,6 +58,11 @@ __all__ = [
|
|
|
47
58
|
"TurnOptions",
|
|
48
59
|
"ApprovalMode",
|
|
49
60
|
"SandboxMode",
|
|
61
|
+
"ModelReasoningEffort",
|
|
62
|
+
"WebSearchMode",
|
|
63
|
+
"CodexConfigValue",
|
|
64
|
+
"CodexConfigObject",
|
|
65
|
+
"CancelSignal",
|
|
50
66
|
"ThreadEvent",
|
|
51
67
|
"ThreadStartedEvent",
|
|
52
68
|
"TurnStartedEvent",
|
|
@@ -10,7 +10,11 @@ class Codex:
|
|
|
10
10
|
|
|
11
11
|
def __init__(self, options: CodexOptions | None = None) -> None:
|
|
12
12
|
resolved = options or CodexOptions()
|
|
13
|
-
self._exec = CodexExec(
|
|
13
|
+
self._exec = CodexExec(
|
|
14
|
+
resolved.codex_path_override,
|
|
15
|
+
env_override=resolved.env,
|
|
16
|
+
config_overrides=resolved.config,
|
|
17
|
+
)
|
|
14
18
|
self._options = resolved
|
|
15
19
|
|
|
16
20
|
def start_thread(self, options: ThreadOptions | None = None) -> Thread:
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from codex._binary import bundled_codex_path
|
|
14
|
+
from codex.errors import CodexExecError
|
|
15
|
+
from codex.options import (
|
|
16
|
+
ApprovalMode,
|
|
17
|
+
CancelSignal,
|
|
18
|
+
CodexConfigObject,
|
|
19
|
+
CodexConfigValue,
|
|
20
|
+
ModelReasoningEffort,
|
|
21
|
+
SandboxMode,
|
|
22
|
+
SupportsAborted,
|
|
23
|
+
SupportsIsSet,
|
|
24
|
+
WebSearchMode,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
|
|
28
|
+
PYTHON_SDK_ORIGINATOR = "codex_sdk_py"
|
|
29
|
+
TOML_BARE_KEY = re.compile(r"^[A-Za-z0-9_-]+$")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True, frozen=True)
|
|
33
|
+
class CodexExecArgs:
|
|
34
|
+
input: str
|
|
35
|
+
base_url: str | None = None
|
|
36
|
+
api_key: str | None = None
|
|
37
|
+
thread_id: str | None = None
|
|
38
|
+
images: list[str] | None = None
|
|
39
|
+
model: str | None = None
|
|
40
|
+
sandbox_mode: SandboxMode | None = None
|
|
41
|
+
working_directory: str | None = None
|
|
42
|
+
additional_directories: list[str] | None = None
|
|
43
|
+
skip_git_repo_check: bool = False
|
|
44
|
+
output_schema_file: str | None = None
|
|
45
|
+
model_reasoning_effort: ModelReasoningEffort | None = None
|
|
46
|
+
signal: CancelSignal | None = None
|
|
47
|
+
network_access_enabled: bool | None = None
|
|
48
|
+
web_search_mode: WebSearchMode | None = None
|
|
49
|
+
web_search_enabled: bool | None = None
|
|
50
|
+
approval_policy: ApprovalMode | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CodexExec:
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
executable_path: str | None = None,
|
|
57
|
+
env_override: dict[str, str] | None = None,
|
|
58
|
+
config_overrides: CodexConfigObject | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
if executable_path is not None:
|
|
61
|
+
path = Path(executable_path)
|
|
62
|
+
else:
|
|
63
|
+
try:
|
|
64
|
+
path = bundled_codex_path()
|
|
65
|
+
except CodexExecError as bundled_error:
|
|
66
|
+
system_codex = shutil.which("codex")
|
|
67
|
+
if system_codex is None:
|
|
68
|
+
raise CodexExecError(
|
|
69
|
+
f"{bundled_error} Also failed to find `codex` on PATH."
|
|
70
|
+
) from bundled_error
|
|
71
|
+
path = Path(system_codex)
|
|
72
|
+
self.executable_path = str(path)
|
|
73
|
+
self._env_override = env_override
|
|
74
|
+
self._config_overrides = config_overrides
|
|
75
|
+
|
|
76
|
+
def run(self, args: CodexExecArgs) -> Iterator[str]:
|
|
77
|
+
if is_signal_aborted(args.signal):
|
|
78
|
+
raise CodexExecError("Codex exec aborted before start")
|
|
79
|
+
|
|
80
|
+
command_args: list[str] = ["exec", "--experimental-json"]
|
|
81
|
+
|
|
82
|
+
if self._config_overrides is not None:
|
|
83
|
+
for override in serialize_config_overrides(self._config_overrides):
|
|
84
|
+
command_args.extend(["--config", override])
|
|
85
|
+
|
|
86
|
+
if args.model is not None:
|
|
87
|
+
command_args.extend(["--model", args.model])
|
|
88
|
+
if args.sandbox_mode is not None:
|
|
89
|
+
command_args.extend(["--sandbox", args.sandbox_mode])
|
|
90
|
+
if args.working_directory is not None:
|
|
91
|
+
command_args.extend(["--cd", args.working_directory])
|
|
92
|
+
if args.additional_directories:
|
|
93
|
+
for directory in args.additional_directories:
|
|
94
|
+
command_args.extend(["--add-dir", directory])
|
|
95
|
+
if args.skip_git_repo_check:
|
|
96
|
+
command_args.append("--skip-git-repo-check")
|
|
97
|
+
if args.output_schema_file is not None:
|
|
98
|
+
command_args.extend(["--output-schema", args.output_schema_file])
|
|
99
|
+
if args.model_reasoning_effort is not None:
|
|
100
|
+
command_args.extend(
|
|
101
|
+
[
|
|
102
|
+
"--config",
|
|
103
|
+
f'model_reasoning_effort="{args.model_reasoning_effort}"',
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
if args.network_access_enabled is not None:
|
|
107
|
+
command_args.extend(
|
|
108
|
+
[
|
|
109
|
+
"--config",
|
|
110
|
+
(
|
|
111
|
+
"sandbox_workspace_write.network_access="
|
|
112
|
+
f"{format_toml_bool(args.network_access_enabled)}"
|
|
113
|
+
),
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
if args.web_search_mode is not None:
|
|
117
|
+
command_args.extend(["--config", f'web_search="{args.web_search_mode}"'])
|
|
118
|
+
elif args.web_search_enabled is True:
|
|
119
|
+
command_args.extend(["--config", 'web_search="live"'])
|
|
120
|
+
elif args.web_search_enabled is False:
|
|
121
|
+
command_args.extend(["--config", 'web_search="disabled"'])
|
|
122
|
+
if args.approval_policy is not None:
|
|
123
|
+
command_args.extend(["--config", f'approval_policy="{args.approval_policy}"'])
|
|
124
|
+
if args.thread_id is not None:
|
|
125
|
+
command_args.extend(["resume", args.thread_id])
|
|
126
|
+
if args.images is not None:
|
|
127
|
+
for image in args.images:
|
|
128
|
+
command_args.extend(["--image", image])
|
|
129
|
+
|
|
130
|
+
env = self.build_env(base_url=args.base_url, api_key=args.api_key)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
child = subprocess.Popen(
|
|
134
|
+
[self.executable_path, *command_args],
|
|
135
|
+
stdin=subprocess.PIPE,
|
|
136
|
+
stdout=subprocess.PIPE,
|
|
137
|
+
stderr=subprocess.PIPE,
|
|
138
|
+
text=True,
|
|
139
|
+
encoding="utf-8",
|
|
140
|
+
env=env,
|
|
141
|
+
)
|
|
142
|
+
except OSError as exc:
|
|
143
|
+
raise CodexExecError(
|
|
144
|
+
f"Failed to spawn codex executable at '{self.executable_path}': {exc}"
|
|
145
|
+
) from exc
|
|
146
|
+
|
|
147
|
+
if child.stdin is None:
|
|
148
|
+
terminate_child(child)
|
|
149
|
+
raise CodexExecError("Child process has no stdin")
|
|
150
|
+
if child.stdout is None:
|
|
151
|
+
terminate_child(child)
|
|
152
|
+
raise CodexExecError("Child process has no stdout")
|
|
153
|
+
if child.stderr is None:
|
|
154
|
+
terminate_child(child)
|
|
155
|
+
raise CodexExecError("Child process has no stderr")
|
|
156
|
+
|
|
157
|
+
if is_signal_aborted(args.signal):
|
|
158
|
+
terminate_child(child)
|
|
159
|
+
_ = child.stderr.read()
|
|
160
|
+
child.stderr.close()
|
|
161
|
+
raise CodexExecError("Codex exec aborted before start")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
child.stdin.write(args.input)
|
|
165
|
+
child.stdin.close()
|
|
166
|
+
except OSError as exc:
|
|
167
|
+
terminate_child(child)
|
|
168
|
+
raise CodexExecError(f"Failed to write input to codex process: {exc}") from exc
|
|
169
|
+
|
|
170
|
+
if is_signal_aborted(args.signal):
|
|
171
|
+
terminate_child(child)
|
|
172
|
+
_ = child.stderr.read()
|
|
173
|
+
child.stderr.close()
|
|
174
|
+
raise CodexExecError("Codex exec aborted")
|
|
175
|
+
|
|
176
|
+
aborted = False
|
|
177
|
+
try:
|
|
178
|
+
for line in child.stdout:
|
|
179
|
+
if is_signal_aborted(args.signal):
|
|
180
|
+
aborted = True
|
|
181
|
+
break
|
|
182
|
+
yield line.rstrip("\r\n")
|
|
183
|
+
except GeneratorExit:
|
|
184
|
+
terminate_child(child)
|
|
185
|
+
child.stderr.close()
|
|
186
|
+
raise
|
|
187
|
+
finally:
|
|
188
|
+
child.stdout.close()
|
|
189
|
+
|
|
190
|
+
if aborted:
|
|
191
|
+
terminate_child(child)
|
|
192
|
+
stderr = child.stderr.read()
|
|
193
|
+
child.stderr.close()
|
|
194
|
+
raise CodexExecError(build_abort_message(stderr))
|
|
195
|
+
|
|
196
|
+
exit_code = child.wait()
|
|
197
|
+
stderr = child.stderr.read()
|
|
198
|
+
child.stderr.close()
|
|
199
|
+
|
|
200
|
+
if exit_code != 0:
|
|
201
|
+
raise CodexExecError(f"Codex exec exited with code {exit_code}: {stderr}")
|
|
202
|
+
|
|
203
|
+
def build_env(self, base_url: str | None, api_key: str | None) -> dict[str, str]:
|
|
204
|
+
env: dict[str, str]
|
|
205
|
+
if self._env_override is None:
|
|
206
|
+
env = os.environ.copy()
|
|
207
|
+
else:
|
|
208
|
+
env = dict(self._env_override)
|
|
209
|
+
|
|
210
|
+
if INTERNAL_ORIGINATOR_ENV not in env:
|
|
211
|
+
env[INTERNAL_ORIGINATOR_ENV] = PYTHON_SDK_ORIGINATOR
|
|
212
|
+
if base_url is not None:
|
|
213
|
+
env["OPENAI_BASE_URL"] = base_url
|
|
214
|
+
if api_key is not None:
|
|
215
|
+
env["CODEX_API_KEY"] = api_key
|
|
216
|
+
return env
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def terminate_child(child: subprocess.Popen[str]) -> None:
|
|
220
|
+
try:
|
|
221
|
+
child.kill()
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
try:
|
|
225
|
+
child.wait()
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def build_abort_message(stderr: str) -> str:
|
|
231
|
+
if stderr == "":
|
|
232
|
+
return "Codex exec aborted"
|
|
233
|
+
return f"Codex exec aborted: {stderr}"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def is_signal_aborted(signal: CancelSignal | None) -> bool:
|
|
237
|
+
if signal is None:
|
|
238
|
+
return False
|
|
239
|
+
if isinstance(signal, SupportsAborted):
|
|
240
|
+
return signal.aborted
|
|
241
|
+
if isinstance(signal, SupportsIsSet):
|
|
242
|
+
return signal.is_set()
|
|
243
|
+
raise TypeError("signal must expose `aborted` or `is_set()`")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def serialize_config_overrides(config_overrides: CodexConfigObject) -> list[str]:
|
|
247
|
+
overrides: list[str] = []
|
|
248
|
+
flatten_config_overrides(config_overrides, "", overrides)
|
|
249
|
+
return overrides
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def flatten_config_overrides(
|
|
253
|
+
value: CodexConfigValue | CodexConfigObject, prefix: str, overrides: list[str]
|
|
254
|
+
) -> None:
|
|
255
|
+
if not isinstance(value, dict):
|
|
256
|
+
if prefix == "":
|
|
257
|
+
raise ValueError("Codex config overrides must be a plain object")
|
|
258
|
+
overrides.append(f"{prefix}={to_toml_value(value, prefix)}")
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
entries = list(value.items())
|
|
262
|
+
if prefix == "" and not entries:
|
|
263
|
+
return
|
|
264
|
+
if prefix != "" and not entries:
|
|
265
|
+
overrides.append(f"{prefix}={{}}")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
for key, child in entries:
|
|
269
|
+
if not isinstance(key, str) or key == "":
|
|
270
|
+
raise ValueError("Codex config override keys must be non-empty strings")
|
|
271
|
+
path = f"{prefix}.{key}" if prefix else key
|
|
272
|
+
if isinstance(child, dict):
|
|
273
|
+
flatten_config_overrides(child, path, overrides)
|
|
274
|
+
else:
|
|
275
|
+
overrides.append(f"{path}={to_toml_value(child, path)}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def to_toml_value(value: CodexConfigValue, path: str) -> str:
|
|
279
|
+
if isinstance(value, str):
|
|
280
|
+
return json.dumps(value)
|
|
281
|
+
if isinstance(value, bool):
|
|
282
|
+
return format_toml_bool(value)
|
|
283
|
+
if isinstance(value, int):
|
|
284
|
+
return f"{value}"
|
|
285
|
+
if isinstance(value, float):
|
|
286
|
+
if not math.isfinite(value):
|
|
287
|
+
raise ValueError(f"Codex config override at {path} must be a finite number")
|
|
288
|
+
return f"{value}"
|
|
289
|
+
if isinstance(value, list):
|
|
290
|
+
rendered_items = [
|
|
291
|
+
to_toml_value(item, f"{path}[{index}]") for index, item in enumerate(value)
|
|
292
|
+
]
|
|
293
|
+
return f"[{', '.join(rendered_items)}]"
|
|
294
|
+
if isinstance(value, dict):
|
|
295
|
+
parts: list[str] = []
|
|
296
|
+
for key, child in value.items():
|
|
297
|
+
if not isinstance(key, str) or key == "":
|
|
298
|
+
raise ValueError("Codex config override keys must be non-empty strings")
|
|
299
|
+
parts.append(f"{format_toml_key(key)} = {to_toml_value(child, f'{path}.{key}')}")
|
|
300
|
+
return f"{{{', '.join(parts)}}}"
|
|
301
|
+
if value is None:
|
|
302
|
+
raise ValueError(f"Codex config override at {path} cannot be null")
|
|
303
|
+
raise ValueError(f"Unsupported Codex config override value at {path}: {type(value).__name__}")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def format_toml_key(key: str) -> str:
|
|
307
|
+
if TOML_BARE_KEY.match(key):
|
|
308
|
+
return key
|
|
309
|
+
return json.dumps(key)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def format_toml_bool(value: bool) -> str:
|
|
313
|
+
return "true" if value else "false"
|
|
@@ -29,11 +29,37 @@ class FileChangeItem(TypedDict):
|
|
|
29
29
|
status: PatchApplyStatus
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
class McpTextContent(TypedDict):
|
|
33
|
+
type: Literal["text"]
|
|
34
|
+
text: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class McpImageContent(TypedDict):
|
|
38
|
+
type: Literal["image"]
|
|
39
|
+
data: str
|
|
40
|
+
mimeType: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
McpContentBlock = McpTextContent | McpImageContent | dict[str, object]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class McpToolCallResult(TypedDict):
|
|
47
|
+
content: list[McpContentBlock]
|
|
48
|
+
structured_content: object
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class McpToolCallError(TypedDict):
|
|
52
|
+
message: str
|
|
53
|
+
|
|
54
|
+
|
|
32
55
|
class McpToolCallItem(TypedDict):
|
|
33
56
|
id: str
|
|
34
57
|
type: Literal["mcp_tool_call"]
|
|
35
58
|
server: str
|
|
36
59
|
tool: str
|
|
60
|
+
arguments: NotRequired[object]
|
|
61
|
+
result: NotRequired[McpToolCallResult]
|
|
62
|
+
error: NotRequired[McpToolCallError]
|
|
37
63
|
status: McpToolCallStatus
|
|
38
64
|
|
|
39
65
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal, Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
ApprovalMode = Literal["never", "on-request", "on-failure", "untrusted"]
|
|
7
|
+
SandboxMode = Literal["read-only", "workspace-write", "danger-full-access"]
|
|
8
|
+
ModelReasoningEffort = Literal["minimal", "low", "medium", "high", "xhigh"]
|
|
9
|
+
WebSearchMode = Literal["disabled", "cached", "live"]
|
|
10
|
+
|
|
11
|
+
type CodexConfigValue = (
|
|
12
|
+
str | int | float | bool | list["CodexConfigValue"] | dict[str, "CodexConfigValue"]
|
|
13
|
+
)
|
|
14
|
+
type CodexConfigObject = dict[str, CodexConfigValue]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@runtime_checkable
|
|
18
|
+
class SupportsIsSet(Protocol):
|
|
19
|
+
def is_set(self) -> bool: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class SupportsAborted(Protocol):
|
|
24
|
+
@property
|
|
25
|
+
def aborted(self) -> bool: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
type CancelSignal = SupportsIsSet | SupportsAborted
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True, frozen=True)
|
|
32
|
+
class CodexOptions:
|
|
33
|
+
codex_path_override: str | None = None
|
|
34
|
+
base_url: str | None = None
|
|
35
|
+
api_key: str | None = None
|
|
36
|
+
config: CodexConfigObject | None = None
|
|
37
|
+
env: dict[str, str] | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(slots=True, frozen=True)
|
|
41
|
+
class ThreadOptions:
|
|
42
|
+
model: str | None = None
|
|
43
|
+
sandbox_mode: SandboxMode | None = None
|
|
44
|
+
working_directory: str | None = None
|
|
45
|
+
skip_git_repo_check: bool = False
|
|
46
|
+
model_reasoning_effort: ModelReasoningEffort | None = None
|
|
47
|
+
network_access_enabled: bool | None = None
|
|
48
|
+
web_search_mode: WebSearchMode | None = None
|
|
49
|
+
web_search_enabled: bool | None = None
|
|
50
|
+
approval_policy: ApprovalMode | None = None
|
|
51
|
+
additional_directories: list[str] | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(slots=True, frozen=True)
|
|
55
|
+
class TurnOptions:
|
|
56
|
+
output_schema: dict[str, object] | None = None
|
|
57
|
+
signal: CancelSignal | None = None
|
|
@@ -3,16 +3,20 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from collections.abc import Iterator, Mapping, Sequence
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Any, Literal, TypedDict, cast
|
|
6
|
+
from typing import Any, Literal, Protocol, TypedDict, cast
|
|
7
7
|
|
|
8
8
|
from codex.errors import CodexParseError, ThreadRunError
|
|
9
9
|
from codex.events import ThreadEvent, Usage
|
|
10
|
-
from codex.exec import
|
|
10
|
+
from codex.exec import CodexExecArgs
|
|
11
11
|
from codex.items import ThreadItem
|
|
12
12
|
from codex.options import CodexOptions, ThreadOptions, TurnOptions
|
|
13
13
|
from codex.output_schema_file import create_output_schema_file
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class ExecRunner(Protocol):
|
|
17
|
+
def run(self, args: CodexExecArgs) -> Iterator[str]: ...
|
|
18
|
+
|
|
19
|
+
|
|
16
20
|
class TextInput(TypedDict):
|
|
17
21
|
type: Literal["text"]
|
|
18
22
|
text: str
|
|
@@ -42,7 +46,7 @@ class RunStreamedResult:
|
|
|
42
46
|
class Thread:
|
|
43
47
|
def __init__(
|
|
44
48
|
self,
|
|
45
|
-
exec_runner:
|
|
49
|
+
exec_runner: ExecRunner,
|
|
46
50
|
options: CodexOptions,
|
|
47
51
|
thread_options: ThreadOptions,
|
|
48
52
|
thread_id: str | None = None,
|
|
@@ -77,8 +81,15 @@ class Thread:
|
|
|
77
81
|
model=options.model,
|
|
78
82
|
sandbox_mode=options.sandbox_mode,
|
|
79
83
|
working_directory=options.working_directory,
|
|
84
|
+
additional_directories=options.additional_directories,
|
|
80
85
|
skip_git_repo_check=options.skip_git_repo_check,
|
|
81
86
|
output_schema_file=schema_file.schema_path,
|
|
87
|
+
model_reasoning_effort=options.model_reasoning_effort,
|
|
88
|
+
signal=effective_turn_options.signal,
|
|
89
|
+
network_access_enabled=options.network_access_enabled,
|
|
90
|
+
web_search_mode=options.web_search_mode,
|
|
91
|
+
web_search_enabled=options.web_search_enabled,
|
|
92
|
+
approval_policy=options.approval_policy,
|
|
82
93
|
)
|
|
83
94
|
try:
|
|
84
95
|
for item in self._exec.run(exec_args):
|
|
@@ -95,7 +106,6 @@ class Thread:
|
|
|
95
106
|
final_response = ""
|
|
96
107
|
usage: Usage | None = None
|
|
97
108
|
turn_failure: str | None = None
|
|
98
|
-
saw_turn_complete = False
|
|
99
109
|
for event in generator:
|
|
100
110
|
event_dict = cast(dict[str, Any], event)
|
|
101
111
|
event_type = event_dict.get("type")
|
|
@@ -111,7 +121,6 @@ class Thread:
|
|
|
111
121
|
usage_value = event_dict.get("usage")
|
|
112
122
|
if isinstance(usage_value, dict):
|
|
113
123
|
usage = cast(Usage, usage_value)
|
|
114
|
-
saw_turn_complete = True
|
|
115
124
|
elif event_type == "turn.failed":
|
|
116
125
|
error_value = event_dict.get("error")
|
|
117
126
|
if isinstance(error_value, dict):
|
|
@@ -119,17 +128,8 @@ class Thread:
|
|
|
119
128
|
if isinstance(message, str):
|
|
120
129
|
turn_failure = message
|
|
121
130
|
break
|
|
122
|
-
elif event_type == "error":
|
|
123
|
-
message_value = event_dict.get("message")
|
|
124
|
-
if isinstance(message_value, str):
|
|
125
|
-
turn_failure = message_value
|
|
126
|
-
break
|
|
127
131
|
if turn_failure is not None:
|
|
128
132
|
raise ThreadRunError(turn_failure)
|
|
129
|
-
if not saw_turn_complete:
|
|
130
|
-
raise ThreadRunError(
|
|
131
|
-
"stream disconnected before completion: missing turn.completed event"
|
|
132
|
-
)
|
|
133
133
|
return RunResult(items=items, final_response=final_response, usage=usage)
|
|
134
134
|
|
|
135
135
|
|
codex_python-1.0.0/codex/exec.py
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import shutil
|
|
5
|
-
import subprocess
|
|
6
|
-
from collections.abc import Iterator
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
from codex._binary import bundled_codex_path
|
|
11
|
-
from codex.errors import CodexExecError
|
|
12
|
-
from codex.options import SandboxMode
|
|
13
|
-
|
|
14
|
-
INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
|
|
15
|
-
PYTHON_SDK_ORIGINATOR = "codex_sdk_py"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@dataclass(slots=True, frozen=True)
|
|
19
|
-
class CodexExecArgs:
|
|
20
|
-
input: str
|
|
21
|
-
base_url: str | None = None
|
|
22
|
-
api_key: str | None = None
|
|
23
|
-
thread_id: str | None = None
|
|
24
|
-
images: list[str] | None = None
|
|
25
|
-
model: str | None = None
|
|
26
|
-
sandbox_mode: SandboxMode | None = None
|
|
27
|
-
working_directory: str | None = None
|
|
28
|
-
skip_git_repo_check: bool = False
|
|
29
|
-
output_schema_file: str | None = None
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class CodexExec:
|
|
33
|
-
def __init__(self, executable_path: str | None = None) -> None:
|
|
34
|
-
if executable_path is not None:
|
|
35
|
-
path = Path(executable_path)
|
|
36
|
-
else:
|
|
37
|
-
try:
|
|
38
|
-
path = bundled_codex_path()
|
|
39
|
-
except CodexExecError as bundled_error:
|
|
40
|
-
system_codex = shutil.which("codex")
|
|
41
|
-
if system_codex is None:
|
|
42
|
-
raise CodexExecError(
|
|
43
|
-
f"{bundled_error} Also failed to find `codex` on PATH."
|
|
44
|
-
) from bundled_error
|
|
45
|
-
path = Path(system_codex)
|
|
46
|
-
self.executable_path = str(path)
|
|
47
|
-
|
|
48
|
-
def run(self, args: CodexExecArgs) -> Iterator[str]:
|
|
49
|
-
command_args: list[str] = ["exec", "--experimental-json"]
|
|
50
|
-
|
|
51
|
-
if args.model is not None:
|
|
52
|
-
command_args.extend(["--model", args.model])
|
|
53
|
-
if args.sandbox_mode is not None:
|
|
54
|
-
command_args.extend(["--sandbox", args.sandbox_mode])
|
|
55
|
-
if args.working_directory is not None:
|
|
56
|
-
command_args.extend(["--cd", args.working_directory])
|
|
57
|
-
if args.skip_git_repo_check:
|
|
58
|
-
command_args.append("--skip-git-repo-check")
|
|
59
|
-
if args.output_schema_file is not None:
|
|
60
|
-
command_args.extend(["--output-schema", args.output_schema_file])
|
|
61
|
-
if args.images is not None:
|
|
62
|
-
for image in args.images:
|
|
63
|
-
command_args.extend(["--image", image])
|
|
64
|
-
if args.thread_id:
|
|
65
|
-
command_args.extend(["resume", args.thread_id])
|
|
66
|
-
|
|
67
|
-
env = os.environ.copy()
|
|
68
|
-
if INTERNAL_ORIGINATOR_ENV not in env:
|
|
69
|
-
env[INTERNAL_ORIGINATOR_ENV] = PYTHON_SDK_ORIGINATOR
|
|
70
|
-
if args.base_url is not None:
|
|
71
|
-
env["OPENAI_BASE_URL"] = args.base_url
|
|
72
|
-
if args.api_key is not None:
|
|
73
|
-
env["CODEX_API_KEY"] = args.api_key
|
|
74
|
-
|
|
75
|
-
try:
|
|
76
|
-
child = subprocess.Popen(
|
|
77
|
-
[self.executable_path, *command_args],
|
|
78
|
-
stdin=subprocess.PIPE,
|
|
79
|
-
stdout=subprocess.PIPE,
|
|
80
|
-
stderr=subprocess.PIPE,
|
|
81
|
-
text=True,
|
|
82
|
-
encoding="utf-8",
|
|
83
|
-
env=env,
|
|
84
|
-
)
|
|
85
|
-
except OSError as exc:
|
|
86
|
-
raise CodexExecError(
|
|
87
|
-
f"Failed to spawn codex executable at '{self.executable_path}': {exc}"
|
|
88
|
-
) from exc
|
|
89
|
-
|
|
90
|
-
if child.stdin is None:
|
|
91
|
-
child.kill()
|
|
92
|
-
raise CodexExecError("Child process has no stdin")
|
|
93
|
-
if child.stdout is None:
|
|
94
|
-
child.kill()
|
|
95
|
-
raise CodexExecError("Child process has no stdout")
|
|
96
|
-
if child.stderr is None:
|
|
97
|
-
child.kill()
|
|
98
|
-
raise CodexExecError("Child process has no stderr")
|
|
99
|
-
|
|
100
|
-
try:
|
|
101
|
-
child.stdin.write(args.input)
|
|
102
|
-
child.stdin.close()
|
|
103
|
-
except OSError as exc:
|
|
104
|
-
child.kill()
|
|
105
|
-
raise CodexExecError(f"Failed to write input to codex process: {exc}") from exc
|
|
106
|
-
|
|
107
|
-
try:
|
|
108
|
-
for line in child.stdout:
|
|
109
|
-
yield line.rstrip("\r\n")
|
|
110
|
-
finally:
|
|
111
|
-
child.stdout.close()
|
|
112
|
-
|
|
113
|
-
exit_code = child.wait()
|
|
114
|
-
stderr = child.stderr.read()
|
|
115
|
-
child.stderr.close()
|
|
116
|
-
|
|
117
|
-
if exit_code != 0:
|
|
118
|
-
raise CodexExecError(f"Codex exec exited with code {exit_code}: {stderr}")
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import Literal
|
|
5
|
-
|
|
6
|
-
ApprovalMode = Literal["never", "on-request", "on-failure", "untrusted"]
|
|
7
|
-
SandboxMode = Literal["read-only", "workspace-write", "danger-full-access"]
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@dataclass(slots=True, frozen=True)
|
|
11
|
-
class CodexOptions:
|
|
12
|
-
codex_path_override: str | None = None
|
|
13
|
-
base_url: str | None = None
|
|
14
|
-
api_key: str | None = None
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass(slots=True, frozen=True)
|
|
18
|
-
class ThreadOptions:
|
|
19
|
-
model: str | None = None
|
|
20
|
-
sandbox_mode: SandboxMode | None = None
|
|
21
|
-
working_directory: str | None = None
|
|
22
|
-
skip_git_repo_check: bool = False
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass(slots=True, frozen=True)
|
|
26
|
-
class TurnOptions:
|
|
27
|
-
output_schema: dict[str, object] | None = None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|