Skip to content

Commit 9aebfdd

Browse files
authored
feat: app-server protocol (#9)
1 parent 211e3bb commit 9aebfdd

70 files changed

Lines changed: 18403 additions & 1753 deletions

Some content is hidden

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

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Set up Python
2020
uses: actions/setup-python@v5
2121
with:
22-
python-version: "3.13"
22+
python-version: "3.12"
2323

2424
- name: Install uv
2525
uses: astral-sh/setup-uv@v4
@@ -43,6 +43,14 @@ jobs:
4343
- name: Test
4444
run: make test
4545

46+
- name: Upload coverage report
47+
if: always()
48+
uses: actions/upload-artifact@v4
49+
with:
50+
name: coverage-xml
51+
path: coverage.xml
52+
if-no-files-found: ignore
53+
4654
- name: Detect integration test availability
4755
id: integration
4856
env:

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ codex-proj/
5151

5252
# Ignore new markdown files by default
5353
*.md
54+
!docs/*.md
5455

5556
# Rust build outputs
5657
target/
@@ -66,3 +67,4 @@ crates/**/Cargo.lock
6667
.beads/
6768
codex/vendor/**
6869
!codex/vendor/.gitkeep
70+
/.desloppify

Makefile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
.PHONY: help venv fmt lint test build publish clean
1+
.PHONY: help venv fmt lint test build publish clean gen-protocol
22

33
help:
44
@echo "Common targets:"
55
@echo " make lint - Run ruff and mypy"
6-
@echo " make test - Run pytest"
6+
@echo " make test - Run pytest with coverage reporting and a 75% gate"
77
@echo " make build - Build sdist and wheel with uv"
88
@echo " make publish - Publish to PyPI via uv (uses PYPI_API_TOKEN)"
99
@echo " make clean - Remove build artifacts"
10+
@echo " make gen-protocol - Regenerate app-server protocol types from codex"
1011
@echo " make gen-stubs - Generate .pyi stubs for the wheel-tag shim module"
1112
@echo " make wheelhouse-linux - Prebuild manylinux & musllinux wheels (x86_64, aarch64)"
1213
@echo " make wheelhouse-clean - Remove wheelhouse/"
@@ -24,7 +25,7 @@ lint:
2425
uv run --group dev mypy codex
2526

2627
test:
27-
@bash -lc 'uv run --group dev pytest -q; ec=$$?; if [ $$ec -eq 5 ]; then echo "No tests collected"; exit 0; else exit $$ec; fi'
28+
@bash -lc 'uv run --group dev pytest --cov=codex --cov-report=term-missing --cov-report=xml; ec=$$?; if [ $$ec -eq 5 ]; then echo "No tests collected"; exit 0; else exit $$ec; fi'
2829

2930
build:
3031
uv build
@@ -54,6 +55,9 @@ publish: build
5455
clean:
5556
rm -rf build dist *.egg-info .pytest_cache .mypy_cache .ruff_cache
5657

58+
gen-protocol:
59+
uv run --group dev python scripts/generate_protocol_types.py --experimental
60+
5761
.PHONY: build-native dev-native
5862

5963
build-native:

README.md

Lines changed: 120 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,83 @@
22

33
Python SDK for Codex with bundled `codex` binaries inside platform wheels.
44

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
5+
This package exposes two supported APIs:
6+
7+
- `Codex`: a simple, local convenience interface backed by a private stdio app-server session
8+
- `AppServerClient`: a richer app-server client for thread management, streaming events, approvals, and typed protocol access
9+
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
914

1015
## Install
1116

1217
```bash
1318
pip install codex-python
1419
```
1520

16-
## Quickstart
21+
## Which API should I use?
22+
23+
### `Codex`
24+
25+
Use `Codex` when you want the smallest surface area for local automation:
26+
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()`
30+
- simple request/response usage
31+
- optional streaming over the exec event stream
32+
- structured output via `TurnOptions(output_schema=...)`
33+
34+
### `AppServerClient`
35+
36+
Use `AppServerClient` when you want a deeper integration:
37+
38+
- persistent app-server connection
39+
- thread objects and turn streams
40+
- protocol-native notifications
41+
- server-driven requests such as tool callbacks and approvals
42+
- typed protocol models and raw JSON-RPC access when needed
43+
44+
## Quickstart: `Codex`
1745

1846
```python
1947
from codex import Codex
2048

2149
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)
50+
summary = client.run_text("Diagnose the failing tests and propose a fix")
51+
print(summary)
2752
```
2853

29-
## Streaming
54+
More `Codex` examples: [docs/exec_api.md](docs/exec_api.md)
55+
56+
## Quickstart: `AppServerClient`
3057

3158
```python
32-
from codex import Codex
59+
from codex.app_server import AppServerClient, AppServerClientInfo, AppServerInitializeOptions
60+
61+
initialize_options = AppServerInitializeOptions(
62+
client_info=AppServerClientInfo(
63+
name="my_integration",
64+
title="My Integration",
65+
version="0.1.0",
66+
)
67+
)
3368

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"])
69+
with AppServerClient.connect_stdio(initialize_options=initialize_options) as client:
70+
thread = client.start_thread()
71+
summary = thread.run_text("Briefly summarize this repository's purpose.")
72+
print(summary)
4373
```
4474

75+
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]"`.
77+
4578
## Structured output
4679

80+
### `Codex`
81+
4782
```python
4883
from codex import Codex, TurnOptions
4984

@@ -55,71 +90,98 @@ schema = {
5590
}
5691

5792
client = Codex()
58-
thread = client.start_thread()
59-
result = thread.run("Summarize repository status", TurnOptions(output_schema=schema))
60-
print(result.final_response)
93+
payload = client.run_json("Summarize repository status", TurnOptions(output_schema=schema))
94+
print(payload["summary"])
6195
```
6296

63-
## Input with local images
97+
### `AppServerClient`
6498

6599
```python
66-
from codex import Codex
100+
from pydantic import BaseModel
67101

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-
)
102+
from codex.app_server import AppServerClient, AppServerTurnOptions
103+
104+
105+
class Summary(BaseModel):
106+
summary: str
107+
108+
109+
with AppServerClient.connect_stdio() as client:
110+
thread = client.start_thread()
111+
result = thread.run_model(
112+
"Summarize repository status",
113+
Summary,
114+
)
115+
print(result.summary)
77116
```
78117

79-
## Resume a thread
118+
`run_model()` uses `Summary` both as the validation model and, by default, as the output schema sent
119+
to Codex. If you want JSON back without validation, you can also pass the model class directly to
120+
`output_schema`, for example `thread.run_json(..., AppServerTurnOptions(output_schema=Summary))`.
121+
122+
## Streaming
123+
124+
### `Codex` stream
80125

81126
```python
82127
from codex import Codex
128+
from codex.protocol import types as protocol
83129

84130
client = Codex()
85-
thread = client.resume_thread("thread_123")
86-
thread.run("Continue from previous context")
131+
stream = client.run("Investigate this bug")
132+
for event in stream:
133+
if isinstance(event, protocol.ItemAgentMessageDeltaNotification):
134+
print(event.params.delta, end="", flush=True)
135+
136+
print()
87137
```
88138

89-
## Options
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.
90141

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`
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.
94144

95-
## Cancellation
145+
### App-server stream
96146

97147
```python
98-
import threading
148+
from codex.app_server import AppServerClient
149+
from codex.protocol import types as protocol
99150

100-
from codex import Codex, TurnOptions
151+
with AppServerClient.connect_stdio() as client:
152+
thread = client.start_thread()
153+
stream = thread.run("Investigate this bug")
101154

102-
cancel = threading.Event()
155+
for event in stream:
156+
if isinstance(event, protocol.ItemAgentMessageDeltaNotification):
157+
print(event.params.delta, end="", flush=True)
103158

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)
159+
print()
111160
```
112161

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)
163+
164+
## Examples
165+
166+
- [examples/basic_conversation.py](examples/basic_conversation.py): minimal `Codex` flow
167+
- [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
169+
- [examples/app_server_stream_events.py](examples/app_server_stream_events.py): protocol-native app-server streaming
170+
- [examples/app_server_tool_handler.py](examples/app_server_tool_handler.py): typed app-server request handling
171+
113172
## Bundled binary behavior
114173

115174
By default, the SDK resolves the bundled binary at:
116175

117176
`codex/vendor/<target-triple>/codex/{codex|codex.exe}`
118177

119-
If the bundled binary is not present (for example in a source checkout), the SDK falls back to
178+
If the bundled binary is not present, for example in a source checkout, the SDK falls back to
120179
`codex` on `PATH`.
121180

122-
You can always override with `CodexOptions(codex_path_override=...)`.
181+
You can override the executable path with:
182+
183+
- `CodexOptions(codex_path_override=...)`
184+
- `codex.app_server.AppServerProcessOptions(codex_path_override=...)`
123185

124186
## Development
125187

@@ -128,6 +190,9 @@ make lint
128190
make test
129191
```
130192

193+
`make test` emits a terminal coverage report, writes `coverage.xml`, and enforces the repository
194+
coverage gate.
195+
131196
If you want to test vendored-binary behavior locally, fetch binaries into `codex/vendor`:
132197

133198
```bash

0 commit comments

Comments
 (0)