codex-python-sdk 0.1.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_sdk-0.1.0/LICENSE +21 -0
- codex_python_sdk-0.1.0/PKG-INFO +274 -0
- codex_python_sdk-0.1.0/README.md +243 -0
- codex_python_sdk-0.1.0/codex_python_sdk/__init__.py +57 -0
- codex_python_sdk-0.1.0/codex_python_sdk/_shared.py +99 -0
- codex_python_sdk-0.1.0/codex_python_sdk/async_client.py +1313 -0
- codex_python_sdk-0.1.0/codex_python_sdk/errors.py +18 -0
- codex_python_sdk-0.1.0/codex_python_sdk/examples/__init__.py +2 -0
- codex_python_sdk-0.1.0/codex_python_sdk/examples/demo_smoke.py +304 -0
- codex_python_sdk-0.1.0/codex_python_sdk/factory.py +25 -0
- codex_python_sdk-0.1.0/codex_python_sdk/policy.py +636 -0
- codex_python_sdk-0.1.0/codex_python_sdk/renderer.py +607 -0
- codex_python_sdk-0.1.0/codex_python_sdk/sync_client.py +333 -0
- codex_python_sdk-0.1.0/codex_python_sdk/types.py +48 -0
- codex_python_sdk-0.1.0/codex_python_sdk.egg-info/PKG-INFO +274 -0
- codex_python_sdk-0.1.0/codex_python_sdk.egg-info/SOURCES.txt +25 -0
- codex_python_sdk-0.1.0/codex_python_sdk.egg-info/dependency_links.txt +1 -0
- codex_python_sdk-0.1.0/codex_python_sdk.egg-info/entry_points.txt +2 -0
- codex_python_sdk-0.1.0/codex_python_sdk.egg-info/requires.txt +11 -0
- codex_python_sdk-0.1.0/codex_python_sdk.egg-info/top_level.txt +1 -0
- codex_python_sdk-0.1.0/pyproject.toml +62 -0
- codex_python_sdk-0.1.0/setup.cfg +4 -0
- codex_python_sdk-0.1.0/tests/test_codex_python_sdk.py +2092 -0
- codex_python_sdk-0.1.0/tests/test_integration_real.py +113 -0
- codex_python_sdk-0.1.0/tests/test_policy_engine.py +619 -0
- codex_python_sdk-0.1.0/tests/test_renderer.py +28 -0
- codex_python_sdk-0.1.0/tests/test_shared.py +46 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Henry_spdcoding
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codex-python-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python wrapper for Codex app-server JSON-RPC interface
|
|
5
|
+
Author: Henry_spdcoding
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/spdcoding/codex-python-sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/spdcoding/codex-python-sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/spdcoding/codex-python-sdk/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/spdcoding/codex-python-sdk/blob/HEAD/docs/tutorial.md
|
|
11
|
+
Project-URL: Codex-App-Server, https://developers.openai.com/codex/app-server/
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: <4.0,>=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest>=8.2; extra == "test"
|
|
23
|
+
Requires-Dist: pytest-timeout; extra == "test"
|
|
24
|
+
Requires-Dist: rich; extra == "test"
|
|
25
|
+
Provides-Extra: build
|
|
26
|
+
Requires-Dist: setuptools>=61; extra == "build"
|
|
27
|
+
Requires-Dist: wheel; extra == "build"
|
|
28
|
+
Requires-Dist: build; extra == "build"
|
|
29
|
+
Requires-Dist: twine; extra == "build"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# codex-python-sdk
|
|
33
|
+
|
|
34
|
+
[GitHub](https://github.com/spdcoding/codex-python-sdk) | [English](./README.md) | [简体中文](./README.zh-CN.md) | [Docs](./docs)
|
|
35
|
+
|
|
36
|
+
Production-focused Python SDK for running Codex agents through `codex app-server`.
|
|
37
|
+
|
|
38
|
+
`codex-python-sdk` gives you a stable Python interface over Codex JSON-RPC so you can automate agent workflows, stream structured runtime events, and enforce runtime policy from your own applications.
|
|
39
|
+
|
|
40
|
+
## Why This SDK
|
|
41
|
+
|
|
42
|
+
- Script-first API: built for automation pipelines, not only interactive CLI sessions.
|
|
43
|
+
- Sync + async parity: same mental model and similar method names in both clients.
|
|
44
|
+
- Structured streaming: consume normalized `ResponseEvent` objects for observability and UI.
|
|
45
|
+
- Predictable failures: explicit error types such as `NotAuthenticatedError` and `SessionNotFoundError`.
|
|
46
|
+
- Policy control: approval/file-change/tool-input/tool-call hooks and policy engine integration.
|
|
47
|
+
- Thin protocol wrapper: close to `codex app-server` behavior, easier to reason about and debug.
|
|
48
|
+
|
|
49
|
+
## 30-Second Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from codex_python_sdk import create_client
|
|
53
|
+
|
|
54
|
+
with create_client() as client:
|
|
55
|
+
result = client.responses_create(prompt="Reply with exactly: READY")
|
|
56
|
+
print(result.session_id)
|
|
57
|
+
print(result.text)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Core Workflows
|
|
61
|
+
|
|
62
|
+
### Stream events (for logs/UI)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from codex_python_sdk import create_client, render_exec_style_events
|
|
66
|
+
|
|
67
|
+
with create_client() as client:
|
|
68
|
+
events = client.responses_events(prompt="Summarize this repository")
|
|
69
|
+
render_exec_style_events(events)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Async flow
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
from codex_python_sdk import create_async_client
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def main() -> None:
|
|
80
|
+
async with create_async_client() as client:
|
|
81
|
+
result = await client.responses_create(prompt="Reply with exactly: ASYNC_READY")
|
|
82
|
+
print(result.text)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
asyncio.run(main())
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Smoke and demo runner
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Quick health check (default mode)
|
|
92
|
+
codex-python-sdk-demo --mode smoke
|
|
93
|
+
|
|
94
|
+
# Stable API showcase
|
|
95
|
+
codex-python-sdk-demo --mode demo
|
|
96
|
+
|
|
97
|
+
# Demo + unstable remote/interrupt/compact paths
|
|
98
|
+
codex-python-sdk-demo --mode full
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Note: the demo runner uses permissive hooks (`accept` for command/file approvals and empty tool-input answers) so it can run unattended.
|
|
102
|
+
Use stricter hooks or policy engines in production.
|
|
103
|
+
|
|
104
|
+
## Mental Model: How It Works
|
|
105
|
+
|
|
106
|
+
`codex app-server` is Codex CLI's local JSON-RPC runtime over stdio.
|
|
107
|
+
|
|
108
|
+
One `responses_create(prompt=...)` call is essentially:
|
|
109
|
+
|
|
110
|
+
1. `create_client()` creates a sync facade (`CodexAgenticClient`).
|
|
111
|
+
2. Sync call forwards to `AsyncCodexAgenticClient` via a dedicated event-loop thread.
|
|
112
|
+
3. `connect()` starts `codex app-server` and performs `initialize/initialized`.
|
|
113
|
+
4. `_request(method, params)` handles all JSON-RPC request/response plumbing.
|
|
114
|
+
5. `responses_events()` streams notifications; `responses_create()` aggregates them into final text.
|
|
115
|
+
|
|
116
|
+
For a deeper walkthrough, see `docs/core_mechanism.md`.
|
|
117
|
+
|
|
118
|
+
## Safety Defaults (Important)
|
|
119
|
+
|
|
120
|
+
Default behavior without hooks/policy:
|
|
121
|
+
- Command approval: `accept`
|
|
122
|
+
- File change approval: `accept`
|
|
123
|
+
- Tool user input: empty answers
|
|
124
|
+
- Tool call: failure response with explanatory text
|
|
125
|
+
|
|
126
|
+
This is convenient for unattended demos, but not production-safe.
|
|
127
|
+
|
|
128
|
+
Recommended safer setup: enable LLM-judge policy with strict fallback decisions.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from codex_python_sdk import PolicyJudgeConfig, create_client
|
|
132
|
+
|
|
133
|
+
rubric = {
|
|
134
|
+
"system_rubric": "Allow read-only operations. Decline unknown write operations.",
|
|
135
|
+
"use_llm_judge": True,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
judge_cfg = PolicyJudgeConfig(
|
|
139
|
+
timeout_seconds=8.0,
|
|
140
|
+
model="gpt-5",
|
|
141
|
+
effort="low",
|
|
142
|
+
fallback_command_decision="decline",
|
|
143
|
+
fallback_file_change_decision="decline",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
with create_client(
|
|
147
|
+
policy_rubric=rubric,
|
|
148
|
+
policy_judge_config=judge_cfg,
|
|
149
|
+
) as client:
|
|
150
|
+
result = client.responses_create(prompt="Show git status.")
|
|
151
|
+
print(result.text)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Note: LLM-judge requires a real Codex runtime/account; for deterministic local tests, use `RuleBasedPolicyEngine`.
|
|
155
|
+
|
|
156
|
+
## Install
|
|
157
|
+
|
|
158
|
+
### Prerequisites
|
|
159
|
+
|
|
160
|
+
- Python `3.9+` (recommended: `3.12`)
|
|
161
|
+
- `uv` (recommended for development workflows)
|
|
162
|
+
- `codex` CLI installed and runnable
|
|
163
|
+
- Authentication completed via `codex login`
|
|
164
|
+
|
|
165
|
+
### Install from PyPI
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
uv add codex-python-sdk
|
|
169
|
+
uv run codex-python-sdk-demo --help
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
or
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pip install codex-python-sdk
|
|
176
|
+
codex-python-sdk-demo --help
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Developer setup (for contributors)
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
./uv-sync.sh
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
This bootstraps a local `.venv` and installs project/test/build dependencies.
|
|
186
|
+
|
|
187
|
+
## API Snapshot
|
|
188
|
+
|
|
189
|
+
Factory:
|
|
190
|
+
- `create_client(**kwargs) -> CodexAgenticClient`
|
|
191
|
+
- `create_async_client(**kwargs) -> AsyncCodexAgenticClient`
|
|
192
|
+
|
|
193
|
+
High-frequency response APIs:
|
|
194
|
+
- `responses_create(...) -> AgentResponse`
|
|
195
|
+
- `responses_events(...) -> Iterator[ResponseEvent] / AsyncIterator[ResponseEvent]`
|
|
196
|
+
- `responses_stream_text(...) -> Iterator[str] / AsyncIterator[str]`
|
|
197
|
+
|
|
198
|
+
Thread basics:
|
|
199
|
+
- `thread_start`, `thread_read`, `thread_list`, `thread_archive`
|
|
200
|
+
|
|
201
|
+
Account basics:
|
|
202
|
+
- `account_read`, `account_rate_limits_read`
|
|
203
|
+
|
|
204
|
+
## Documentation Map
|
|
205
|
+
|
|
206
|
+
English:
|
|
207
|
+
- `docs/tutorial.md`: practical workflows and end-to-end usage
|
|
208
|
+
- `docs/core_mechanism.md`: architecture-level core control flow
|
|
209
|
+
- `docs/config.md`: server/thread/turn configuration model
|
|
210
|
+
- `docs/api.md`: full API reference (sync + async)
|
|
211
|
+
- `docs/policy.md`: hooks and policy engine integration
|
|
212
|
+
- `docs/app_server.md`: app-server concepts and protocol mapping
|
|
213
|
+
|
|
214
|
+
简体中文:
|
|
215
|
+
- `docs/zh/tutorial.md`
|
|
216
|
+
- `docs/zh/core_mechanism.md`
|
|
217
|
+
- `docs/zh/config.md`
|
|
218
|
+
- `docs/zh/api.md`
|
|
219
|
+
- `docs/zh/policy.md`
|
|
220
|
+
- `docs/zh/app_server.md`
|
|
221
|
+
|
|
222
|
+
## Notes
|
|
223
|
+
|
|
224
|
+
- After `AppServerConnectionError`, recreate the client instead of relying on implicit reconnect behavior.
|
|
225
|
+
- Internal app-server `stderr` buffering keeps only the latest 500 lines in SDK-captured diagnostics.
|
|
226
|
+
- When using low-level server request handlers, method names must be exactly `item`, `tool`, or `requestUserInput`.
|
|
227
|
+
- Policy LLM-judge parsing is strict JSON-only: judge output must be a pure JSON object; embedded JSON snippets in free text are rejected.
|
|
228
|
+
- Invalid command/file policy decision values (allowed: `accept`, `acceptForSession`, `decline`, `cancel`) raise `CodexAgenticError`.
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
./uv-sync.sh
|
|
234
|
+
uv run python3 -m pytest -q -m "not real"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Release
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# Default: test + build + twine check (no upload)
|
|
241
|
+
./build.sh
|
|
242
|
+
|
|
243
|
+
# Build only
|
|
244
|
+
./build.sh build
|
|
245
|
+
|
|
246
|
+
# Release to pypi (upload enabled explicitly)
|
|
247
|
+
TWINE_UPLOAD=1 ./build.sh release --repo pypi
|
|
248
|
+
|
|
249
|
+
# Release to testpypi
|
|
250
|
+
TWINE_UPLOAD=1 ./build.sh release --repo testpypi
|
|
251
|
+
|
|
252
|
+
# Upload existing artifacts only
|
|
253
|
+
./build.sh upload --repo pypi
|
|
254
|
+
|
|
255
|
+
# Help
|
|
256
|
+
./build.sh help
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Recommended upload auth: `~/.pypirc` with API token.
|
|
260
|
+
|
|
261
|
+
## Project Layout
|
|
262
|
+
|
|
263
|
+
- `codex_python_sdk/`: SDK source code
|
|
264
|
+
- `codex_python_sdk/examples/`: runnable demo code
|
|
265
|
+
- `tests/`: unit and real-runtime integration tests
|
|
266
|
+
- `uv-sync.sh`: dev environment bootstrap
|
|
267
|
+
- `build.sh`: build/release script
|
|
268
|
+
|
|
269
|
+
## Error Types
|
|
270
|
+
|
|
271
|
+
- `CodexAgenticError`: base SDK error
|
|
272
|
+
- `AppServerConnectionError`: app-server transport/setup failure
|
|
273
|
+
- `SessionNotFoundError`: unknown thread/session id
|
|
274
|
+
- `NotAuthenticatedError`: auth unavailable or invalid
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# codex-python-sdk
|
|
2
|
+
|
|
3
|
+
[GitHub](https://github.com/spdcoding/codex-python-sdk) | [English](./README.md) | [简体中文](./README.zh-CN.md) | [Docs](./docs)
|
|
4
|
+
|
|
5
|
+
Production-focused Python SDK for running Codex agents through `codex app-server`.
|
|
6
|
+
|
|
7
|
+
`codex-python-sdk` gives you a stable Python interface over Codex JSON-RPC so you can automate agent workflows, stream structured runtime events, and enforce runtime policy from your own applications.
|
|
8
|
+
|
|
9
|
+
## Why This SDK
|
|
10
|
+
|
|
11
|
+
- Script-first API: built for automation pipelines, not only interactive CLI sessions.
|
|
12
|
+
- Sync + async parity: same mental model and similar method names in both clients.
|
|
13
|
+
- Structured streaming: consume normalized `ResponseEvent` objects for observability and UI.
|
|
14
|
+
- Predictable failures: explicit error types such as `NotAuthenticatedError` and `SessionNotFoundError`.
|
|
15
|
+
- Policy control: approval/file-change/tool-input/tool-call hooks and policy engine integration.
|
|
16
|
+
- Thin protocol wrapper: close to `codex app-server` behavior, easier to reason about and debug.
|
|
17
|
+
|
|
18
|
+
## 30-Second Quick Start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from codex_python_sdk import create_client
|
|
22
|
+
|
|
23
|
+
with create_client() as client:
|
|
24
|
+
result = client.responses_create(prompt="Reply with exactly: READY")
|
|
25
|
+
print(result.session_id)
|
|
26
|
+
print(result.text)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Core Workflows
|
|
30
|
+
|
|
31
|
+
### Stream events (for logs/UI)
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from codex_python_sdk import create_client, render_exec_style_events
|
|
35
|
+
|
|
36
|
+
with create_client() as client:
|
|
37
|
+
events = client.responses_events(prompt="Summarize this repository")
|
|
38
|
+
render_exec_style_events(events)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Async flow
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import asyncio
|
|
45
|
+
from codex_python_sdk import create_async_client
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def main() -> None:
|
|
49
|
+
async with create_async_client() as client:
|
|
50
|
+
result = await client.responses_create(prompt="Reply with exactly: ASYNC_READY")
|
|
51
|
+
print(result.text)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Smoke and demo runner
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Quick health check (default mode)
|
|
61
|
+
codex-python-sdk-demo --mode smoke
|
|
62
|
+
|
|
63
|
+
# Stable API showcase
|
|
64
|
+
codex-python-sdk-demo --mode demo
|
|
65
|
+
|
|
66
|
+
# Demo + unstable remote/interrupt/compact paths
|
|
67
|
+
codex-python-sdk-demo --mode full
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Note: the demo runner uses permissive hooks (`accept` for command/file approvals and empty tool-input answers) so it can run unattended.
|
|
71
|
+
Use stricter hooks or policy engines in production.
|
|
72
|
+
|
|
73
|
+
## Mental Model: How It Works
|
|
74
|
+
|
|
75
|
+
`codex app-server` is Codex CLI's local JSON-RPC runtime over stdio.
|
|
76
|
+
|
|
77
|
+
One `responses_create(prompt=...)` call is essentially:
|
|
78
|
+
|
|
79
|
+
1. `create_client()` creates a sync facade (`CodexAgenticClient`).
|
|
80
|
+
2. Sync call forwards to `AsyncCodexAgenticClient` via a dedicated event-loop thread.
|
|
81
|
+
3. `connect()` starts `codex app-server` and performs `initialize/initialized`.
|
|
82
|
+
4. `_request(method, params)` handles all JSON-RPC request/response plumbing.
|
|
83
|
+
5. `responses_events()` streams notifications; `responses_create()` aggregates them into final text.
|
|
84
|
+
|
|
85
|
+
For a deeper walkthrough, see `docs/core_mechanism.md`.
|
|
86
|
+
|
|
87
|
+
## Safety Defaults (Important)
|
|
88
|
+
|
|
89
|
+
Default behavior without hooks/policy:
|
|
90
|
+
- Command approval: `accept`
|
|
91
|
+
- File change approval: `accept`
|
|
92
|
+
- Tool user input: empty answers
|
|
93
|
+
- Tool call: failure response with explanatory text
|
|
94
|
+
|
|
95
|
+
This is convenient for unattended demos, but not production-safe.
|
|
96
|
+
|
|
97
|
+
Recommended safer setup: enable LLM-judge policy with strict fallback decisions.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from codex_python_sdk import PolicyJudgeConfig, create_client
|
|
101
|
+
|
|
102
|
+
rubric = {
|
|
103
|
+
"system_rubric": "Allow read-only operations. Decline unknown write operations.",
|
|
104
|
+
"use_llm_judge": True,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
judge_cfg = PolicyJudgeConfig(
|
|
108
|
+
timeout_seconds=8.0,
|
|
109
|
+
model="gpt-5",
|
|
110
|
+
effort="low",
|
|
111
|
+
fallback_command_decision="decline",
|
|
112
|
+
fallback_file_change_decision="decline",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
with create_client(
|
|
116
|
+
policy_rubric=rubric,
|
|
117
|
+
policy_judge_config=judge_cfg,
|
|
118
|
+
) as client:
|
|
119
|
+
result = client.responses_create(prompt="Show git status.")
|
|
120
|
+
print(result.text)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Note: LLM-judge requires a real Codex runtime/account; for deterministic local tests, use `RuleBasedPolicyEngine`.
|
|
124
|
+
|
|
125
|
+
## Install
|
|
126
|
+
|
|
127
|
+
### Prerequisites
|
|
128
|
+
|
|
129
|
+
- Python `3.9+` (recommended: `3.12`)
|
|
130
|
+
- `uv` (recommended for development workflows)
|
|
131
|
+
- `codex` CLI installed and runnable
|
|
132
|
+
- Authentication completed via `codex login`
|
|
133
|
+
|
|
134
|
+
### Install from PyPI
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
uv add codex-python-sdk
|
|
138
|
+
uv run codex-python-sdk-demo --help
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
or
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pip install codex-python-sdk
|
|
145
|
+
codex-python-sdk-demo --help
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Developer setup (for contributors)
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
./uv-sync.sh
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
This bootstraps a local `.venv` and installs project/test/build dependencies.
|
|
155
|
+
|
|
156
|
+
## API Snapshot
|
|
157
|
+
|
|
158
|
+
Factory:
|
|
159
|
+
- `create_client(**kwargs) -> CodexAgenticClient`
|
|
160
|
+
- `create_async_client(**kwargs) -> AsyncCodexAgenticClient`
|
|
161
|
+
|
|
162
|
+
High-frequency response APIs:
|
|
163
|
+
- `responses_create(...) -> AgentResponse`
|
|
164
|
+
- `responses_events(...) -> Iterator[ResponseEvent] / AsyncIterator[ResponseEvent]`
|
|
165
|
+
- `responses_stream_text(...) -> Iterator[str] / AsyncIterator[str]`
|
|
166
|
+
|
|
167
|
+
Thread basics:
|
|
168
|
+
- `thread_start`, `thread_read`, `thread_list`, `thread_archive`
|
|
169
|
+
|
|
170
|
+
Account basics:
|
|
171
|
+
- `account_read`, `account_rate_limits_read`
|
|
172
|
+
|
|
173
|
+
## Documentation Map
|
|
174
|
+
|
|
175
|
+
English:
|
|
176
|
+
- `docs/tutorial.md`: practical workflows and end-to-end usage
|
|
177
|
+
- `docs/core_mechanism.md`: architecture-level core control flow
|
|
178
|
+
- `docs/config.md`: server/thread/turn configuration model
|
|
179
|
+
- `docs/api.md`: full API reference (sync + async)
|
|
180
|
+
- `docs/policy.md`: hooks and policy engine integration
|
|
181
|
+
- `docs/app_server.md`: app-server concepts and protocol mapping
|
|
182
|
+
|
|
183
|
+
简体中文:
|
|
184
|
+
- `docs/zh/tutorial.md`
|
|
185
|
+
- `docs/zh/core_mechanism.md`
|
|
186
|
+
- `docs/zh/config.md`
|
|
187
|
+
- `docs/zh/api.md`
|
|
188
|
+
- `docs/zh/policy.md`
|
|
189
|
+
- `docs/zh/app_server.md`
|
|
190
|
+
|
|
191
|
+
## Notes
|
|
192
|
+
|
|
193
|
+
- After `AppServerConnectionError`, recreate the client instead of relying on implicit reconnect behavior.
|
|
194
|
+
- Internal app-server `stderr` buffering keeps only the latest 500 lines in SDK-captured diagnostics.
|
|
195
|
+
- When using low-level server request handlers, method names must be exactly `item`, `tool`, or `requestUserInput`.
|
|
196
|
+
- Policy LLM-judge parsing is strict JSON-only: judge output must be a pure JSON object; embedded JSON snippets in free text are rejected.
|
|
197
|
+
- Invalid command/file policy decision values (allowed: `accept`, `acceptForSession`, `decline`, `cancel`) raise `CodexAgenticError`.
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
./uv-sync.sh
|
|
203
|
+
uv run python3 -m pytest -q -m "not real"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Release
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Default: test + build + twine check (no upload)
|
|
210
|
+
./build.sh
|
|
211
|
+
|
|
212
|
+
# Build only
|
|
213
|
+
./build.sh build
|
|
214
|
+
|
|
215
|
+
# Release to pypi (upload enabled explicitly)
|
|
216
|
+
TWINE_UPLOAD=1 ./build.sh release --repo pypi
|
|
217
|
+
|
|
218
|
+
# Release to testpypi
|
|
219
|
+
TWINE_UPLOAD=1 ./build.sh release --repo testpypi
|
|
220
|
+
|
|
221
|
+
# Upload existing artifacts only
|
|
222
|
+
./build.sh upload --repo pypi
|
|
223
|
+
|
|
224
|
+
# Help
|
|
225
|
+
./build.sh help
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Recommended upload auth: `~/.pypirc` with API token.
|
|
229
|
+
|
|
230
|
+
## Project Layout
|
|
231
|
+
|
|
232
|
+
- `codex_python_sdk/`: SDK source code
|
|
233
|
+
- `codex_python_sdk/examples/`: runnable demo code
|
|
234
|
+
- `tests/`: unit and real-runtime integration tests
|
|
235
|
+
- `uv-sync.sh`: dev environment bootstrap
|
|
236
|
+
- `build.sh`: build/release script
|
|
237
|
+
|
|
238
|
+
## Error Types
|
|
239
|
+
|
|
240
|
+
- `CodexAgenticError`: base SDK error
|
|
241
|
+
- `AppServerConnectionError`: app-server transport/setup failure
|
|
242
|
+
- `SessionNotFoundError`: unknown thread/session id
|
|
243
|
+
- `NotAuthenticatedError`: auth unavailable or invalid
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio as asyncio
|
|
4
|
+
|
|
5
|
+
from .async_client import (
|
|
6
|
+
DEFAULT_APP_SERVER_ARGS,
|
|
7
|
+
DEFAULT_CLI_COMMAND,
|
|
8
|
+
AsyncCodexAgenticClient,
|
|
9
|
+
)
|
|
10
|
+
from .errors import (
|
|
11
|
+
AppServerConnectionError,
|
|
12
|
+
CodexAgenticError,
|
|
13
|
+
NotAuthenticatedError,
|
|
14
|
+
SessionNotFoundError,
|
|
15
|
+
)
|
|
16
|
+
from .factory import create_async_client, create_client
|
|
17
|
+
from .policy import (
|
|
18
|
+
DEFAULT_POLICY_RUBRIC,
|
|
19
|
+
DefaultPolicyEngine,
|
|
20
|
+
LlmRubricPolicyEngine,
|
|
21
|
+
PolicyContext,
|
|
22
|
+
PolicyEngine,
|
|
23
|
+
PolicyJudgeConfig,
|
|
24
|
+
PolicyRubric,
|
|
25
|
+
RuleBasedPolicyEngine,
|
|
26
|
+
build_policy_engine_from_rubric,
|
|
27
|
+
)
|
|
28
|
+
from .renderer import ExecStyleRenderer, render_exec_style_events
|
|
29
|
+
from .sync_client import CodexAgenticClient
|
|
30
|
+
from .types import AgentResponse, ResponseEvent
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"AgentResponse",
|
|
34
|
+
"AppServerConnectionError",
|
|
35
|
+
"AsyncCodexAgenticClient",
|
|
36
|
+
"CodexAgenticClient",
|
|
37
|
+
"CodexAgenticError",
|
|
38
|
+
"DEFAULT_APP_SERVER_ARGS",
|
|
39
|
+
"DEFAULT_CLI_COMMAND",
|
|
40
|
+
"ExecStyleRenderer",
|
|
41
|
+
"DEFAULT_POLICY_RUBRIC",
|
|
42
|
+
"DefaultPolicyEngine",
|
|
43
|
+
"LlmRubricPolicyEngine",
|
|
44
|
+
"NotAuthenticatedError",
|
|
45
|
+
"PolicyContext",
|
|
46
|
+
"PolicyEngine",
|
|
47
|
+
"PolicyJudgeConfig",
|
|
48
|
+
"PolicyRubric",
|
|
49
|
+
"ResponseEvent",
|
|
50
|
+
"RuleBasedPolicyEngine",
|
|
51
|
+
"SessionNotFoundError",
|
|
52
|
+
"asyncio",
|
|
53
|
+
"build_policy_engine_from_rubric",
|
|
54
|
+
"create_async_client",
|
|
55
|
+
"create_client",
|
|
56
|
+
"render_exec_style_events",
|
|
57
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def utc_now() -> str:
|
|
8
|
+
return datetime.now(tz=timezone.utc).isoformat()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def first_nonempty_text(value: Any) -> str | None:
|
|
12
|
+
if isinstance(value, str):
|
|
13
|
+
text = value.strip()
|
|
14
|
+
return text or None
|
|
15
|
+
if isinstance(value, list):
|
|
16
|
+
out: list[str] = []
|
|
17
|
+
for item in value:
|
|
18
|
+
text = first_nonempty_text(item)
|
|
19
|
+
if text:
|
|
20
|
+
out.append(text)
|
|
21
|
+
if out:
|
|
22
|
+
return "\n".join(out)
|
|
23
|
+
return None
|
|
24
|
+
if isinstance(value, dict):
|
|
25
|
+
for key in ("message", "text", "delta", "error", "content"):
|
|
26
|
+
if key in value:
|
|
27
|
+
text = first_nonempty_text(value.get(key))
|
|
28
|
+
if text:
|
|
29
|
+
return text
|
|
30
|
+
for nested in value.values():
|
|
31
|
+
if isinstance(nested, (dict, list)):
|
|
32
|
+
text = first_nonempty_text(nested)
|
|
33
|
+
if text:
|
|
34
|
+
return text
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def token_usage_total_tokens(value: Any) -> int | None:
|
|
39
|
+
if not isinstance(value, dict):
|
|
40
|
+
return None
|
|
41
|
+
total = value.get("totalTokens")
|
|
42
|
+
if isinstance(total, int):
|
|
43
|
+
return total
|
|
44
|
+
total = value.get("total_tokens")
|
|
45
|
+
if isinstance(total, int):
|
|
46
|
+
return total
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def token_usage_summary(token_usage: dict[str, Any]) -> str | None:
|
|
51
|
+
last = token_usage.get("last")
|
|
52
|
+
total = token_usage.get("total")
|
|
53
|
+
last_total = token_usage_total_tokens(last)
|
|
54
|
+
total_total = token_usage_total_tokens(total)
|
|
55
|
+
if last_total is None and total_total is None:
|
|
56
|
+
return None
|
|
57
|
+
if last_total is None:
|
|
58
|
+
return f"total={total_total}"
|
|
59
|
+
if total_total is None:
|
|
60
|
+
return f"last={last_total}"
|
|
61
|
+
return f"last={last_total}, total={total_total}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def diff_change_counts(diff: str) -> tuple[int, int]:
|
|
65
|
+
added = 0
|
|
66
|
+
removed = 0
|
|
67
|
+
for line in diff.splitlines():
|
|
68
|
+
if line.startswith("+++") or line.startswith("---"):
|
|
69
|
+
continue
|
|
70
|
+
if line.startswith("+"):
|
|
71
|
+
added += 1
|
|
72
|
+
elif line.startswith("-"):
|
|
73
|
+
removed += 1
|
|
74
|
+
return added, removed
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def turn_plan_summary(params: dict[str, Any]) -> str | None:
|
|
78
|
+
plan = params.get("plan")
|
|
79
|
+
if not isinstance(plan, list):
|
|
80
|
+
return first_nonempty_text(params.get("explanation"))
|
|
81
|
+
|
|
82
|
+
status_counts: dict[str, int] = {}
|
|
83
|
+
for item in plan:
|
|
84
|
+
if not isinstance(item, dict):
|
|
85
|
+
continue
|
|
86
|
+
status = item.get("status")
|
|
87
|
+
if isinstance(status, str):
|
|
88
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
89
|
+
|
|
90
|
+
parts: list[str] = []
|
|
91
|
+
explanation = params.get("explanation")
|
|
92
|
+
if isinstance(explanation, str) and explanation.strip():
|
|
93
|
+
parts.append(explanation.strip())
|
|
94
|
+
parts.append(f"steps={len(plan)}")
|
|
95
|
+
for status in ("inProgress", "pending", "completed"):
|
|
96
|
+
if status in status_counts:
|
|
97
|
+
parts.append(f"{status}={status_counts[status]}")
|
|
98
|
+
return ", ".join(parts)
|
|
99
|
+
|