Skip to content

Commit f3e35fd

Browse files
committed
chore: unify runtime
1 parent e7c460a commit f3e35fd

63 files changed

Lines changed: 8692 additions & 3126 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,4 @@ crates/**/Cargo.lock
6767
.beads/
6868
codex/vendor/**
6969
!codex/vendor/.gitkeep
70-
/.desloppify/config.json
71-
/.desloppify/config.json.bak
70+
/.desloppify

README.md

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ Python SDK for Codex with bundled `codex` binaries inside platform wheels.
44

55
This package exposes two supported APIs:
66

7-
- `Codex`: a simple, local, CLI-backed interface built on `codex exec`
7+
- `Codex`: a simple, local convenience interface backed by a private stdio app-server session
88
- `AppServerClient`: a richer app-server client for thread management, streaming events, approvals, and typed protocol access
99

10+
Canonical import paths:
11+
12+
- use `from codex import ...` for the high-level `Codex` facade
13+
- use `from codex.app_server import ...` for the raw app-server client and app-server option types
14+
1015
## Install
1116

1217
```bash
@@ -19,7 +24,9 @@ pip install codex-python
1924

2025
Use `Codex` when you want the smallest surface area for local automation:
2126

22-
- one process per run
27+
- one private local app-server session per `Codex` instance
28+
- stateless `run*()` convenience (fresh internal thread per call)
29+
- stateful thread workflows when needed via `start_thread()` / `resume_thread()`
2330
- simple request/response usage
2431
- optional streaming over the exec event stream
2532
- structured output via `TurnOptions(output_schema=...)`
@@ -40,18 +47,16 @@ Use `AppServerClient` when you want a deeper integration:
4047
from codex import Codex
4148

4249
client = Codex()
43-
thread = client.start_thread()
44-
45-
summary = thread.run_text("Diagnose the failing tests and propose a fix")
50+
summary = client.run_text("Diagnose the failing tests and propose a fix")
4651
print(summary)
4752
```
4853

49-
More exec-based examples: [docs/exec_api.md](docs/exec_api.md)
54+
More `Codex` examples: [docs/exec_api.md](docs/exec_api.md)
5055

5156
## Quickstart: `AppServerClient`
5257

5358
```python
54-
from codex import AppServerClient, AppServerClientInfo, AppServerInitializeOptions
59+
from codex.app_server import AppServerClient, AppServerClientInfo, AppServerInitializeOptions
5560

5661
initialize_options = AppServerInitializeOptions(
5762
client_info=AppServerClientInfo(
@@ -68,6 +73,7 @@ with AppServerClient.connect_stdio(initialize_options=initialize_options) as cli
6873
```
6974

7075
More app-server examples: [docs/app_server.md](docs/app_server.md)
76+
For websocket transport, install the optional extra: `pip install "codex-python[websocket]"`.
7177

7278
## Structured output
7379

@@ -84,8 +90,7 @@ schema = {
8490
}
8591

8692
client = Codex()
87-
thread = client.start_thread()
88-
payload = thread.run_json("Summarize repository status", TurnOptions(output_schema=schema))
93+
payload = client.run_json("Summarize repository status", TurnOptions(output_schema=schema))
8994
print(payload["summary"])
9095
```
9196

@@ -94,7 +99,7 @@ print(payload["summary"])
9499
```python
95100
from pydantic import BaseModel
96101

97-
from codex import AppServerClient
102+
from codex.app_server import AppServerClient, AppServerTurnOptions
98103

99104

100105
class Summary(BaseModel):
@@ -112,31 +117,35 @@ with AppServerClient.connect_stdio() as client:
112117

113118
`run_model()` uses `Summary` both as the validation model and, by default, as the output schema sent
114119
to Codex. If you want JSON back without validation, you can also pass the model class directly to
115-
`outputSchema`, for example `thread.run_json(..., outputSchema=Summary)`.
120+
`output_schema`, for example `thread.run_json(..., AppServerTurnOptions(output_schema=Summary))`.
116121

117122
## Streaming
118123

119-
### Exec stream
124+
### `Codex` stream
120125

121126
```python
122127
from codex import Codex
123128
from codex.protocol import types as protocol
124129

125130
client = Codex()
126-
thread = client.start_thread()
127-
128-
stream = thread.run("Investigate this bug")
131+
stream = client.run("Investigate this bug")
129132
for event in stream:
130-
if isinstance(event, protocol.AgentMessageDeltaEventMsg):
131-
print(event.delta, end="", flush=True)
133+
if isinstance(event, protocol.ItemAgentMessageDeltaNotification):
134+
print(event.params.delta, end="", flush=True)
132135

133136
print()
134137
```
135138

139+
`Codex.run*()` starts a fresh internal thread for each call. Use
140+
`start_thread()` or `resume_thread()` when you want later runs to share context.
141+
142+
High-level `Codex` helpers raise `ThreadRunError` on failed or interrupted terminal turns and
143+
preserve the final turn metadata on the exception for debugging and UI handling.
144+
136145
### App-server stream
137146

138147
```python
139-
from codex import AppServerClient
148+
from codex.app_server import AppServerClient
140149
from codex.protocol import types as protocol
141150

142151
with AppServerClient.connect_stdio() as client:
@@ -150,12 +159,13 @@ with AppServerClient.connect_stdio() as client:
150159
print()
151160
```
152161

153-
Advanced app-server usage: [docs/app_server_advanced.md](docs/app_server_advanced.md)
162+
Advanced app-server usage, including typed stable RPC domains such as `client.models` and the raw `client.rpc` fallback: [docs/app_server_advanced.md](docs/app_server_advanced.md)
154163

155164
## Examples
156165

157166
- [examples/basic_conversation.py](examples/basic_conversation.py): minimal `Codex` flow
158167
- [examples/app_server_conversation.py](examples/app_server_conversation.py): minimal app-server flow
168+
- [examples/app_server_websocket_conversation.py](examples/app_server_websocket_conversation.py): minimal websocket app-server flow
159169
- [examples/app_server_stream_events.py](examples/app_server_stream_events.py): protocol-native app-server streaming
160170
- [examples/app_server_tool_handler.py](examples/app_server_tool_handler.py): typed app-server request handling
161171

@@ -171,7 +181,7 @@ If the bundled binary is not present, for example in a source checkout, the SDK
171181
You can override the executable path with:
172182

173183
- `CodexOptions(codex_path_override=...)`
174-
- `AppServerProcessOptions(codex_path_override=...)`
184+
- `codex.app_server.AppServerProcessOptions(codex_path_override=...)`
175185

176186
## Development
177187

codex/__init__.py

Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,31 @@
11
"""Python SDK for embedding Codex via the bundled CLI binary."""
22

3-
from codex.app_server import (
4-
AppServerClient,
5-
AppServerClientInfo,
6-
AppServerClosedError,
7-
AppServerConnectionError,
8-
AppServerError,
9-
AppServerInitializeOptions,
10-
AppServerProcessOptions,
11-
AppServerProtocolError,
12-
AppServerRpcError,
13-
AppServerThread,
14-
AsyncAppServerClient,
15-
AsyncAppServerThread,
16-
AsyncRpcClient,
17-
AsyncTurnStream,
18-
GenericNotification,
19-
GenericServerRequest,
20-
RpcClient,
21-
TurnStream,
22-
)
3+
from __future__ import annotations
4+
235
from codex.codex import Codex
246
from codex.errors import CodexError, CodexExecError, CodexParseError, ThreadRunError
257
from codex.options import (
26-
ApprovalMode,
278
CancelSignal,
289
CodexConfigObject,
2910
CodexConfigValue,
3011
CodexOptions,
31-
ModelReasoningEffort,
32-
SandboxMode,
33-
ThreadOptions,
12+
ThreadResumeOptions,
13+
ThreadStartOptions,
3414
TurnOptions,
35-
WebSearchMode,
3615
)
37-
from codex.thread import ExecTurnStream, Input, Thread, UserInput
3816

3917
__version__ = "1.0.1"
4018

4119
__all__ = [
4220
"Codex",
43-
"AppServerClient",
44-
"AppServerThread",
45-
"AsyncAppServerClient",
46-
"AsyncAppServerThread",
47-
"RpcClient",
48-
"AsyncRpcClient",
49-
"TurnStream",
50-
"AsyncTurnStream",
51-
"AppServerError",
52-
"AppServerConnectionError",
53-
"AppServerClosedError",
54-
"AppServerProtocolError",
55-
"AppServerRpcError",
56-
"GenericNotification",
57-
"GenericServerRequest",
58-
"AppServerClientInfo",
59-
"AppServerInitializeOptions",
60-
"AppServerProcessOptions",
6121
"CodexError",
6222
"CodexExecError",
6323
"CodexParseError",
6424
"ThreadRunError",
65-
"Thread",
66-
"ExecTurnStream",
67-
"Input",
68-
"UserInput",
6925
"CodexOptions",
70-
"ThreadOptions",
26+
"ThreadStartOptions",
27+
"ThreadResumeOptions",
7128
"TurnOptions",
72-
"ApprovalMode",
73-
"SandboxMode",
74-
"ModelReasoningEffort",
75-
"WebSearchMode",
7629
"CodexConfigValue",
7730
"CodexConfigObject",
7831
"CancelSignal",

codex/_binary.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from codex.errors import CodexExecError
77

88

9+
class BundledCodexNotFoundError(CodexExecError):
10+
"""The platform wheel does not contain a bundled Codex binary."""
11+
12+
913
def resolve_target_triple(system_name: str | None = None, machine_name: str | None = None) -> str:
1014
system = (system_name or platform.system()).lower()
1115
machine = (machine_name or platform.machine()).lower()
@@ -35,7 +39,7 @@ def bundled_codex_path(target_triple: str | None = None) -> Path:
3539
binary_name = "codex.exe" if "windows" in triple else "codex"
3640
binary_path = package_root / "vendor" / triple / "codex" / binary_name
3741
if not binary_path.exists():
38-
raise CodexExecError(
42+
raise BundledCodexNotFoundError(
3943
"Bundled codex binary not found at "
4044
f"{binary_path}. Install a platform wheel or provide codex_path_override."
4145
)

codex/_config_types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from __future__ import annotations
2+
3+
type CodexConfigValue = (
4+
str | int | float | bool | list["CodexConfigValue"] | dict[str, "CodexConfigValue"]
5+
)
6+
type CodexConfigObject = dict[str, CodexConfigValue]

codex/_file_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import tempfile
5+
from pathlib import Path
6+
7+
8+
def atomic_write_text(path: Path, text: str, *, encoding: str = "utf-8") -> None:
9+
path.parent.mkdir(parents=True, exist_ok=True)
10+
fd, temp_path_str = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
11+
temp_path = Path(temp_path_str)
12+
try:
13+
with os.fdopen(fd, "w", encoding=encoding) as handle:
14+
handle.write(text)
15+
temp_path.replace(path)
16+
except Exception:
17+
temp_path.unlink(missing_ok=True)
18+
raise

0 commit comments

Comments
 (0)