codex-python 0.3.0__tar.gz → 1.0.0__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/PKG-INFO +136 -0
- codex_python-1.0.0/README.md +117 -0
- codex_python-1.0.0/codex/__init__.py +70 -0
- codex_python-1.0.0/codex/_binary.py +42 -0
- codex_python-1.0.0/codex/codex.py +22 -0
- codex_python-1.0.0/codex/errors.py +17 -0
- codex_python-1.0.0/codex/events.py +66 -0
- codex_python-1.0.0/codex/exec.py +118 -0
- codex_python-1.0.0/codex/items.py +84 -0
- codex_python-1.0.0/codex/options.py +27 -0
- codex_python-1.0.0/codex/output_schema_file.py +34 -0
- codex_python-1.0.0/codex/thread.py +170 -0
- codex_python-1.0.0/codex/vendor/.gitkeep +0 -0
- codex_python-1.0.0/crates/codex_native/Cargo.lock +204 -0
- codex_python-1.0.0/crates/codex_native/Cargo.toml +17 -0
- codex_python-1.0.0/crates/codex_native/codex/__init__.py +8 -0
- codex_python-1.0.0/crates/codex_native/src/lib.rs +12 -0
- {codex_python-0.3.0 → codex_python-1.0.0}/pyproject.toml +4 -6
- codex_python-0.3.0/PKG-INFO +0 -358
- codex_python-0.3.0/README.md +0 -338
- codex_python-0.3.0/codex/__init__.py +0 -31
- codex_python-0.3.0/codex/client.py +0 -410
- codex_python-0.3.0/codex/config.py +0 -326
- codex_python-0.3.0/codex/event.py +0 -29
- codex_python-0.3.0/codex/exec.py +0 -138
- codex_python-0.3.0/codex/native.py +0 -85
- codex_python-0.3.0/codex/protocol/_base_model.py +0 -8
- codex_python-0.3.0/codex/protocol/types.py +0 -1561
- codex_python-0.3.0/crates/codex_native/Cargo.lock +0 -3600
- codex_python-0.3.0/crates/codex_native/Cargo.toml +0 -48
- codex_python-0.3.0/crates/codex_native/src/bin/protocol_schema.rs +0 -170
- codex_python-0.3.0/crates/codex_native/src/lib.rs +0 -1176
- {codex_python-0.3.0 → codex_python-1.0.0}/LICENSE +0 -0
- {codex_python-0.3.0 → codex_python-1.0.0}/codex/py.typed +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codex-python
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Classifier: Programming Language :: Python :: 3
|
|
5
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Typing :: Typed
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Summary: Python SDK for Codex CLI with bundled platform binaries
|
|
12
|
+
Keywords: codex,sdk,cli,automation
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
15
|
+
Project-URL: Homepage, https://github.com/gersmann/codex-python
|
|
16
|
+
Project-URL: Issues, https://github.com/gersmann/codex-python/issues
|
|
17
|
+
Project-URL: Repository, https://github.com/gersmann/codex-python
|
|
18
|
+
|
|
19
|
+
# codex-python
|
|
20
|
+
|
|
21
|
+
Python SDK for Codex with bundled `codex` binaries inside platform wheels.
|
|
22
|
+
|
|
23
|
+
The SDK mirrors the TypeScript SDK behavior:
|
|
24
|
+
- Spawns `codex exec --experimental-json`
|
|
25
|
+
- Streams JSONL events
|
|
26
|
+
- Supports thread resume, structured output schemas, images, sandbox/model options
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install codex-python
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from codex import Codex
|
|
38
|
+
|
|
39
|
+
client = Codex()
|
|
40
|
+
thread = client.start_thread()
|
|
41
|
+
|
|
42
|
+
result = thread.run("Diagnose the failing tests and propose a fix")
|
|
43
|
+
print(result.final_response)
|
|
44
|
+
print(result.items)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Streaming
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from codex import Codex
|
|
51
|
+
|
|
52
|
+
client = Codex()
|
|
53
|
+
thread = client.start_thread()
|
|
54
|
+
|
|
55
|
+
stream = thread.run_streamed("Investigate this bug")
|
|
56
|
+
for event in stream.events:
|
|
57
|
+
if event["type"] == "item.completed":
|
|
58
|
+
print(event["item"])
|
|
59
|
+
elif event["type"] == "turn.completed":
|
|
60
|
+
print(event["usage"])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Structured output
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from codex import Codex, TurnOptions
|
|
67
|
+
|
|
68
|
+
schema = {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {"summary": {"type": "string"}},
|
|
71
|
+
"required": ["summary"],
|
|
72
|
+
"additionalProperties": False,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
client = Codex()
|
|
76
|
+
thread = client.start_thread()
|
|
77
|
+
result = thread.run("Summarize repository status", TurnOptions(output_schema=schema))
|
|
78
|
+
print(result.final_response)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Input with local images
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from codex import Codex
|
|
85
|
+
|
|
86
|
+
client = Codex()
|
|
87
|
+
thread = client.start_thread()
|
|
88
|
+
result = thread.run(
|
|
89
|
+
[
|
|
90
|
+
{"type": "text", "text": "Describe these screenshots"},
|
|
91
|
+
{"type": "local_image", "path": "./ui.png"},
|
|
92
|
+
{"type": "local_image", "path": "./diagram.jpg"},
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Resume a thread
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from codex import Codex
|
|
101
|
+
|
|
102
|
+
client = Codex()
|
|
103
|
+
thread = client.resume_thread("thread_123")
|
|
104
|
+
thread.run("Continue from previous context")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Options
|
|
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`
|
|
112
|
+
|
|
113
|
+
## Bundled binary behavior
|
|
114
|
+
|
|
115
|
+
By default, the SDK resolves the bundled binary at:
|
|
116
|
+
|
|
117
|
+
`codex/vendor/<target-triple>/codex/{codex|codex.exe}`
|
|
118
|
+
|
|
119
|
+
If the bundled binary is not present (for example in a source checkout), the SDK falls back to
|
|
120
|
+
`codex` on `PATH`.
|
|
121
|
+
|
|
122
|
+
You can always override with `CodexOptions(codex_path_override=...)`.
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
make lint
|
|
128
|
+
make test
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
If you want to test vendored-binary behavior locally, fetch binaries into `codex/vendor`:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
python scripts/fetch_codex_binary.py --target-triple x86_64-unknown-linux-musl
|
|
135
|
+
```
|
|
136
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# codex-python
|
|
2
|
+
|
|
3
|
+
Python SDK for Codex with bundled `codex` binaries inside platform wheels.
|
|
4
|
+
|
|
5
|
+
The SDK mirrors the TypeScript SDK behavior:
|
|
6
|
+
- Spawns `codex exec --experimental-json`
|
|
7
|
+
- Streams JSONL events
|
|
8
|
+
- Supports thread resume, structured output schemas, images, sandbox/model options
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install codex-python
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quickstart
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from codex import Codex
|
|
20
|
+
|
|
21
|
+
client = Codex()
|
|
22
|
+
thread = client.start_thread()
|
|
23
|
+
|
|
24
|
+
result = thread.run("Diagnose the failing tests and propose a fix")
|
|
25
|
+
print(result.final_response)
|
|
26
|
+
print(result.items)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Streaming
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from codex import Codex
|
|
33
|
+
|
|
34
|
+
client = Codex()
|
|
35
|
+
thread = client.start_thread()
|
|
36
|
+
|
|
37
|
+
stream = thread.run_streamed("Investigate this bug")
|
|
38
|
+
for event in stream.events:
|
|
39
|
+
if event["type"] == "item.completed":
|
|
40
|
+
print(event["item"])
|
|
41
|
+
elif event["type"] == "turn.completed":
|
|
42
|
+
print(event["usage"])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Structured output
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from codex import Codex, TurnOptions
|
|
49
|
+
|
|
50
|
+
schema = {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {"summary": {"type": "string"}},
|
|
53
|
+
"required": ["summary"],
|
|
54
|
+
"additionalProperties": False,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
client = Codex()
|
|
58
|
+
thread = client.start_thread()
|
|
59
|
+
result = thread.run("Summarize repository status", TurnOptions(output_schema=schema))
|
|
60
|
+
print(result.final_response)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Input with local images
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from codex import Codex
|
|
67
|
+
|
|
68
|
+
client = Codex()
|
|
69
|
+
thread = client.start_thread()
|
|
70
|
+
result = thread.run(
|
|
71
|
+
[
|
|
72
|
+
{"type": "text", "text": "Describe these screenshots"},
|
|
73
|
+
{"type": "local_image", "path": "./ui.png"},
|
|
74
|
+
{"type": "local_image", "path": "./diagram.jpg"},
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Resume a thread
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from codex import Codex
|
|
83
|
+
|
|
84
|
+
client = Codex()
|
|
85
|
+
thread = client.resume_thread("thread_123")
|
|
86
|
+
thread.run("Continue from previous context")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Options
|
|
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`
|
|
94
|
+
|
|
95
|
+
## Bundled binary behavior
|
|
96
|
+
|
|
97
|
+
By default, the SDK resolves the bundled binary at:
|
|
98
|
+
|
|
99
|
+
`codex/vendor/<target-triple>/codex/{codex|codex.exe}`
|
|
100
|
+
|
|
101
|
+
If the bundled binary is not present (for example in a source checkout), the SDK falls back to
|
|
102
|
+
`codex` on `PATH`.
|
|
103
|
+
|
|
104
|
+
You can always override with `CodexOptions(codex_path_override=...)`.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
make lint
|
|
110
|
+
make test
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
If you want to test vendored-binary behavior locally, fetch binaries into `codex/vendor`:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
python scripts/fetch_codex_binary.py --target-triple x86_64-unknown-linux-musl
|
|
117
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Python SDK for embedding Codex via the bundled CLI binary."""
|
|
2
|
+
|
|
3
|
+
from codex.codex import Codex
|
|
4
|
+
from codex.errors import CodexError, CodexExecError, CodexParseError, ThreadRunError
|
|
5
|
+
from codex.events import (
|
|
6
|
+
ItemCompletedEvent,
|
|
7
|
+
ItemStartedEvent,
|
|
8
|
+
ItemUpdatedEvent,
|
|
9
|
+
ThreadError,
|
|
10
|
+
ThreadErrorEvent,
|
|
11
|
+
ThreadEvent,
|
|
12
|
+
ThreadStartedEvent,
|
|
13
|
+
TurnCompletedEvent,
|
|
14
|
+
TurnFailedEvent,
|
|
15
|
+
TurnStartedEvent,
|
|
16
|
+
Usage,
|
|
17
|
+
)
|
|
18
|
+
from codex.items import (
|
|
19
|
+
AgentMessageItem,
|
|
20
|
+
CommandExecutionItem,
|
|
21
|
+
ErrorItem,
|
|
22
|
+
FileChangeItem,
|
|
23
|
+
McpToolCallItem,
|
|
24
|
+
ReasoningItem,
|
|
25
|
+
ThreadItem,
|
|
26
|
+
TodoListItem,
|
|
27
|
+
WebSearchItem,
|
|
28
|
+
)
|
|
29
|
+
from codex.options import ApprovalMode, CodexOptions, SandboxMode, ThreadOptions, TurnOptions
|
|
30
|
+
from codex.thread import Input, RunResult, RunStreamedResult, Thread, UserInput
|
|
31
|
+
|
|
32
|
+
__version__ = "1.0.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"Codex",
|
|
36
|
+
"CodexError",
|
|
37
|
+
"CodexExecError",
|
|
38
|
+
"CodexParseError",
|
|
39
|
+
"ThreadRunError",
|
|
40
|
+
"Thread",
|
|
41
|
+
"RunResult",
|
|
42
|
+
"RunStreamedResult",
|
|
43
|
+
"Input",
|
|
44
|
+
"UserInput",
|
|
45
|
+
"CodexOptions",
|
|
46
|
+
"ThreadOptions",
|
|
47
|
+
"TurnOptions",
|
|
48
|
+
"ApprovalMode",
|
|
49
|
+
"SandboxMode",
|
|
50
|
+
"ThreadEvent",
|
|
51
|
+
"ThreadStartedEvent",
|
|
52
|
+
"TurnStartedEvent",
|
|
53
|
+
"TurnCompletedEvent",
|
|
54
|
+
"TurnFailedEvent",
|
|
55
|
+
"ItemStartedEvent",
|
|
56
|
+
"ItemUpdatedEvent",
|
|
57
|
+
"ItemCompletedEvent",
|
|
58
|
+
"ThreadError",
|
|
59
|
+
"ThreadErrorEvent",
|
|
60
|
+
"Usage",
|
|
61
|
+
"ThreadItem",
|
|
62
|
+
"AgentMessageItem",
|
|
63
|
+
"ReasoningItem",
|
|
64
|
+
"CommandExecutionItem",
|
|
65
|
+
"FileChangeItem",
|
|
66
|
+
"McpToolCallItem",
|
|
67
|
+
"WebSearchItem",
|
|
68
|
+
"TodoListItem",
|
|
69
|
+
"ErrorItem",
|
|
70
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from codex.errors import CodexExecError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_target_triple(system_name: str | None = None, machine_name: str | None = None) -> str:
|
|
10
|
+
system = (system_name or platform.system()).lower()
|
|
11
|
+
machine = (machine_name or platform.machine()).lower()
|
|
12
|
+
|
|
13
|
+
if system in {"linux", "android"}:
|
|
14
|
+
if machine in {"x86_64", "amd64"}:
|
|
15
|
+
return "x86_64-unknown-linux-musl"
|
|
16
|
+
if machine in {"aarch64", "arm64"}:
|
|
17
|
+
return "aarch64-unknown-linux-musl"
|
|
18
|
+
elif system == "darwin":
|
|
19
|
+
if machine in {"x86_64", "amd64"}:
|
|
20
|
+
return "x86_64-apple-darwin"
|
|
21
|
+
if machine in {"aarch64", "arm64"}:
|
|
22
|
+
return "aarch64-apple-darwin"
|
|
23
|
+
elif system in {"windows", "win32"}:
|
|
24
|
+
if machine in {"x86_64", "amd64"}:
|
|
25
|
+
return "x86_64-pc-windows-msvc"
|
|
26
|
+
if machine in {"aarch64", "arm64"}:
|
|
27
|
+
return "aarch64-pc-windows-msvc"
|
|
28
|
+
|
|
29
|
+
raise CodexExecError(f"Unsupported platform: {system} ({machine})")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def bundled_codex_path(target_triple: str | None = None) -> Path:
|
|
33
|
+
triple = target_triple or resolve_target_triple()
|
|
34
|
+
package_root = Path(__file__).resolve().parent
|
|
35
|
+
binary_name = "codex.exe" if "windows" in triple else "codex"
|
|
36
|
+
binary_path = package_root / "vendor" / triple / "codex" / binary_name
|
|
37
|
+
if not binary_path.exists():
|
|
38
|
+
raise CodexExecError(
|
|
39
|
+
"Bundled codex binary not found at "
|
|
40
|
+
f"{binary_path}. Install a platform wheel or provide codex_path_override."
|
|
41
|
+
)
|
|
42
|
+
return binary_path
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from codex.exec import CodexExec
|
|
4
|
+
from codex.options import CodexOptions, ThreadOptions
|
|
5
|
+
from codex.thread import Thread
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Codex:
|
|
9
|
+
"""Main entrypoint for interacting with Codex threads."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, options: CodexOptions | None = None) -> None:
|
|
12
|
+
resolved = options or CodexOptions()
|
|
13
|
+
self._exec = CodexExec(resolved.codex_path_override)
|
|
14
|
+
self._options = resolved
|
|
15
|
+
|
|
16
|
+
def start_thread(self, options: ThreadOptions | None = None) -> Thread:
|
|
17
|
+
return Thread(self._exec, self._options, options or ThreadOptions())
|
|
18
|
+
|
|
19
|
+
def resume_thread(self, id: str, options: ThreadOptions | None = None) -> Thread:
|
|
20
|
+
if id == "":
|
|
21
|
+
raise ValueError("id must be non-empty")
|
|
22
|
+
return Thread(self._exec, self._options, options or ThreadOptions(), id)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CodexError(RuntimeError):
|
|
5
|
+
"""Base error for the Python Codex SDK."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CodexExecError(CodexError):
|
|
9
|
+
"""Raised when the Codex CLI process fails."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CodexParseError(CodexError):
|
|
13
|
+
"""Raised when streaming JSONL events cannot be parsed."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ThreadRunError(CodexError):
|
|
17
|
+
"""Raised when a run or stream fails before turn completion."""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, TypedDict
|
|
4
|
+
|
|
5
|
+
from codex.items import ThreadItem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ThreadStartedEvent(TypedDict):
|
|
9
|
+
type: Literal["thread.started"]
|
|
10
|
+
thread_id: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TurnStartedEvent(TypedDict):
|
|
14
|
+
type: Literal["turn.started"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Usage(TypedDict):
|
|
18
|
+
input_tokens: int
|
|
19
|
+
cached_input_tokens: int
|
|
20
|
+
output_tokens: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TurnCompletedEvent(TypedDict):
|
|
24
|
+
type: Literal["turn.completed"]
|
|
25
|
+
usage: Usage
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ThreadError(TypedDict):
|
|
29
|
+
message: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TurnFailedEvent(TypedDict):
|
|
33
|
+
type: Literal["turn.failed"]
|
|
34
|
+
error: ThreadError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ItemStartedEvent(TypedDict):
|
|
38
|
+
type: Literal["item.started"]
|
|
39
|
+
item: ThreadItem
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ItemUpdatedEvent(TypedDict):
|
|
43
|
+
type: Literal["item.updated"]
|
|
44
|
+
item: ThreadItem
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ItemCompletedEvent(TypedDict):
|
|
48
|
+
type: Literal["item.completed"]
|
|
49
|
+
item: ThreadItem
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ThreadErrorEvent(TypedDict):
|
|
53
|
+
type: Literal["error"]
|
|
54
|
+
message: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
ThreadEvent = (
|
|
58
|
+
ThreadStartedEvent
|
|
59
|
+
| TurnStartedEvent
|
|
60
|
+
| TurnCompletedEvent
|
|
61
|
+
| TurnFailedEvent
|
|
62
|
+
| ItemStartedEvent
|
|
63
|
+
| ItemUpdatedEvent
|
|
64
|
+
| ItemCompletedEvent
|
|
65
|
+
| ThreadErrorEvent
|
|
66
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
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}")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, NotRequired, TypedDict
|
|
4
|
+
|
|
5
|
+
CommandExecutionStatus = Literal["in_progress", "completed", "failed"]
|
|
6
|
+
PatchChangeKind = Literal["add", "delete", "update"]
|
|
7
|
+
PatchApplyStatus = Literal["completed", "failed"]
|
|
8
|
+
McpToolCallStatus = Literal["in_progress", "completed", "failed"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommandExecutionItem(TypedDict):
|
|
12
|
+
id: str
|
|
13
|
+
type: Literal["command_execution"]
|
|
14
|
+
command: str
|
|
15
|
+
aggregated_output: str
|
|
16
|
+
status: CommandExecutionStatus
|
|
17
|
+
exit_code: NotRequired[int]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FileUpdateChange(TypedDict):
|
|
21
|
+
path: str
|
|
22
|
+
kind: PatchChangeKind
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FileChangeItem(TypedDict):
|
|
26
|
+
id: str
|
|
27
|
+
type: Literal["file_change"]
|
|
28
|
+
changes: list[FileUpdateChange]
|
|
29
|
+
status: PatchApplyStatus
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class McpToolCallItem(TypedDict):
|
|
33
|
+
id: str
|
|
34
|
+
type: Literal["mcp_tool_call"]
|
|
35
|
+
server: str
|
|
36
|
+
tool: str
|
|
37
|
+
status: McpToolCallStatus
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AgentMessageItem(TypedDict):
|
|
41
|
+
id: str
|
|
42
|
+
type: Literal["agent_message"]
|
|
43
|
+
text: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ReasoningItem(TypedDict):
|
|
47
|
+
id: str
|
|
48
|
+
type: Literal["reasoning"]
|
|
49
|
+
text: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class WebSearchItem(TypedDict):
|
|
53
|
+
id: str
|
|
54
|
+
type: Literal["web_search"]
|
|
55
|
+
query: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ErrorItem(TypedDict):
|
|
59
|
+
id: str
|
|
60
|
+
type: Literal["error"]
|
|
61
|
+
message: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TodoItem(TypedDict):
|
|
65
|
+
text: str
|
|
66
|
+
completed: bool
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TodoListItem(TypedDict):
|
|
70
|
+
id: str
|
|
71
|
+
type: Literal["todo_list"]
|
|
72
|
+
items: list[TodoItem]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
ThreadItem = (
|
|
76
|
+
AgentMessageItem
|
|
77
|
+
| ReasoningItem
|
|
78
|
+
| CommandExecutionItem
|
|
79
|
+
| FileChangeItem
|
|
80
|
+
| McpToolCallItem
|
|
81
|
+
| WebSearchItem
|
|
82
|
+
| TodoListItem
|
|
83
|
+
| ErrorItem
|
|
84
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|