codex-python-sdk 0.1.0__py3-none-any.whl
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/__init__.py +57 -0
- codex_python_sdk/_shared.py +99 -0
- codex_python_sdk/async_client.py +1313 -0
- codex_python_sdk/errors.py +18 -0
- codex_python_sdk/examples/__init__.py +2 -0
- codex_python_sdk/examples/demo_smoke.py +304 -0
- codex_python_sdk/factory.py +25 -0
- codex_python_sdk/policy.py +636 -0
- codex_python_sdk/renderer.py +607 -0
- codex_python_sdk/sync_client.py +333 -0
- codex_python_sdk/types.py +48 -0
- codex_python_sdk-0.1.0.dist-info/METADATA +274 -0
- codex_python_sdk-0.1.0.dist-info/RECORD +17 -0
- codex_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- codex_python_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- codex_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- codex_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Any, Iterator
|
|
6
|
+
|
|
7
|
+
from .async_client import AsyncCodexAgenticClient
|
|
8
|
+
from .errors import CodexAgenticError
|
|
9
|
+
from .types import AgentResponse, ResponseEvent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CodexAgenticClient:
|
|
13
|
+
"""Synchronous facade over :class:`AsyncCodexAgenticClient`.
|
|
14
|
+
|
|
15
|
+
This wrapper owns an internal event loop and exposes blocking APIs for
|
|
16
|
+
scripts and notebooks that prefer non-async usage.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
20
|
+
self._loop = asyncio.new_event_loop()
|
|
21
|
+
self._client = AsyncCodexAgenticClient(**kwargs)
|
|
22
|
+
self._loop_started = threading.Event()
|
|
23
|
+
self._loop_thread = threading.Thread(target=self._loop_thread_main, name="codex-agentic-client-loop", daemon=True)
|
|
24
|
+
self._loop_thread.start()
|
|
25
|
+
self._loop_started.wait()
|
|
26
|
+
self._closed = False
|
|
27
|
+
|
|
28
|
+
def _loop_thread_main(self) -> None:
|
|
29
|
+
asyncio.set_event_loop(self._loop)
|
|
30
|
+
self._loop_started.set()
|
|
31
|
+
self._loop.run_forever()
|
|
32
|
+
|
|
33
|
+
def close(self) -> None:
|
|
34
|
+
if self._closed:
|
|
35
|
+
return
|
|
36
|
+
try:
|
|
37
|
+
self._run(self._client.close())
|
|
38
|
+
finally:
|
|
39
|
+
try:
|
|
40
|
+
if not self._loop.is_closed():
|
|
41
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
42
|
+
finally:
|
|
43
|
+
self._loop_thread.join(timeout=2.0)
|
|
44
|
+
# If the loop thread didn't exit, we cannot safely close the loop here.
|
|
45
|
+
if (not self._loop_thread.is_alive()) and (not self._loop.is_running()) and (not self._loop.is_closed()):
|
|
46
|
+
self._loop.close()
|
|
47
|
+
self._closed = True
|
|
48
|
+
|
|
49
|
+
def __enter__(self) -> "CodexAgenticClient":
|
|
50
|
+
try:
|
|
51
|
+
self._run(self._client.connect())
|
|
52
|
+
except Exception:
|
|
53
|
+
try:
|
|
54
|
+
self.close()
|
|
55
|
+
except Exception:
|
|
56
|
+
# Preserve the original connection failure.
|
|
57
|
+
pass
|
|
58
|
+
raise
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
62
|
+
self.close()
|
|
63
|
+
|
|
64
|
+
def _run(self, coro: Any) -> Any:
|
|
65
|
+
if self._closed:
|
|
66
|
+
raise CodexAgenticError("Client is closed.")
|
|
67
|
+
if threading.current_thread() is self._loop_thread:
|
|
68
|
+
raise CodexAgenticError("Cannot block on sync client from its own loop thread.")
|
|
69
|
+
fut = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
70
|
+
return fut.result()
|
|
71
|
+
|
|
72
|
+
def thread_start(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
params: dict[str, Any] | None = None,
|
|
76
|
+
) -> dict[str, Any]:
|
|
77
|
+
"""Create a new thread."""
|
|
78
|
+
|
|
79
|
+
return self._run(self._client.thread_start(params=params))
|
|
80
|
+
|
|
81
|
+
def thread_read(self, thread_id: str, *, include_turns: bool = False) -> dict[str, Any]:
|
|
82
|
+
"""Read one thread by id."""
|
|
83
|
+
|
|
84
|
+
return self._run(self._client.thread_read(thread_id, include_turns=include_turns))
|
|
85
|
+
|
|
86
|
+
def thread_list(self, limit: int = 50, *, sort_key: str = "updated_at") -> list[dict[str, Any]]:
|
|
87
|
+
"""List available threads."""
|
|
88
|
+
|
|
89
|
+
return self._run(self._client.thread_list(limit=limit, sort_key=sort_key))
|
|
90
|
+
|
|
91
|
+
def thread_archive(self, thread_id: str) -> dict[str, Any]:
|
|
92
|
+
"""Archive one thread."""
|
|
93
|
+
|
|
94
|
+
return self._run(self._client.thread_archive(thread_id))
|
|
95
|
+
|
|
96
|
+
def thread_fork(
|
|
97
|
+
self,
|
|
98
|
+
thread_id: str,
|
|
99
|
+
*,
|
|
100
|
+
params: dict[str, Any] | None = None,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
return self._run(self._client.thread_fork(thread_id, params=params))
|
|
103
|
+
|
|
104
|
+
def thread_name_set(self, thread_id: str, name: str) -> dict[str, Any]:
|
|
105
|
+
return self._run(self._client.thread_name_set(thread_id, name))
|
|
106
|
+
|
|
107
|
+
def thread_unarchive(self, thread_id: str) -> dict[str, Any]:
|
|
108
|
+
return self._run(self._client.thread_unarchive(thread_id))
|
|
109
|
+
|
|
110
|
+
def thread_compact_start(self, thread_id: str) -> dict[str, Any]:
|
|
111
|
+
return self._run(self._client.thread_compact_start(thread_id))
|
|
112
|
+
|
|
113
|
+
def thread_rollback(self, thread_id: str, num_turns: int) -> dict[str, Any]:
|
|
114
|
+
return self._run(self._client.thread_rollback(thread_id, num_turns))
|
|
115
|
+
|
|
116
|
+
def thread_loaded_list(self, *, limit: int | None = None, cursor: str | None = None) -> dict[str, Any]:
|
|
117
|
+
return self._run(self._client.thread_loaded_list(limit=limit, cursor=cursor))
|
|
118
|
+
|
|
119
|
+
def skills_list(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
cwds: list[str] | None = None,
|
|
123
|
+
force_reload: bool | None = None,
|
|
124
|
+
) -> dict[str, Any]:
|
|
125
|
+
return self._run(self._client.skills_list(cwds=cwds, force_reload=force_reload))
|
|
126
|
+
|
|
127
|
+
def skills_remote_read(self) -> dict[str, Any]:
|
|
128
|
+
return self._run(self._client.skills_remote_read())
|
|
129
|
+
|
|
130
|
+
def skills_remote_write(self, hazelnut_id: str, is_preload: bool) -> dict[str, Any]:
|
|
131
|
+
return self._run(self._client.skills_remote_write(hazelnut_id, is_preload))
|
|
132
|
+
|
|
133
|
+
def app_list(self, *, limit: int | None = None, cursor: str | None = None) -> dict[str, Any]:
|
|
134
|
+
return self._run(self._client.app_list(limit=limit, cursor=cursor))
|
|
135
|
+
|
|
136
|
+
def skills_config_write(self, path: str, enabled: bool) -> dict[str, Any]:
|
|
137
|
+
return self._run(self._client.skills_config_write(path, enabled))
|
|
138
|
+
|
|
139
|
+
def turn_interrupt(self, thread_id: str, turn_id: str) -> dict[str, Any]:
|
|
140
|
+
return self._run(self._client.turn_interrupt(thread_id, turn_id))
|
|
141
|
+
|
|
142
|
+
def turn_steer(self, thread_id: str, turn_id: str, prompt: str) -> dict[str, Any]:
|
|
143
|
+
return self._run(self._client.turn_steer(thread_id, turn_id, prompt))
|
|
144
|
+
|
|
145
|
+
def review_start(
|
|
146
|
+
self,
|
|
147
|
+
thread_id: str,
|
|
148
|
+
target: dict[str, Any],
|
|
149
|
+
*,
|
|
150
|
+
delivery: str | None = None,
|
|
151
|
+
) -> dict[str, Any]:
|
|
152
|
+
return self._run(self._client.review_start(thread_id, target, delivery=delivery))
|
|
153
|
+
|
|
154
|
+
def model_list(self, *, limit: int | None = None, cursor: str | None = None) -> dict[str, Any]:
|
|
155
|
+
return self._run(self._client.model_list(limit=limit, cursor=cursor))
|
|
156
|
+
|
|
157
|
+
def account_rate_limits_read(self) -> dict[str, Any]:
|
|
158
|
+
return self._run(self._client.account_rate_limits_read())
|
|
159
|
+
|
|
160
|
+
def account_read(self, *, refresh_token: bool | None = None) -> dict[str, Any]:
|
|
161
|
+
return self._run(self._client.account_read(refresh_token=refresh_token))
|
|
162
|
+
|
|
163
|
+
def command_exec(
|
|
164
|
+
self,
|
|
165
|
+
command: list[str],
|
|
166
|
+
*,
|
|
167
|
+
cwd: str | None = None,
|
|
168
|
+
timeout_ms: int | None = None,
|
|
169
|
+
sandbox_policy: dict[str, Any] | None = None,
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
return self._run(
|
|
172
|
+
self._client.command_exec(
|
|
173
|
+
command,
|
|
174
|
+
cwd=cwd,
|
|
175
|
+
timeout_ms=timeout_ms,
|
|
176
|
+
sandbox_policy=sandbox_policy,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def config_read(self, *, cwd: str | None = None, include_layers: bool = False) -> dict[str, Any]:
|
|
181
|
+
return self._run(self._client.config_read(cwd=cwd, include_layers=include_layers))
|
|
182
|
+
|
|
183
|
+
def config_value_write(
|
|
184
|
+
self,
|
|
185
|
+
key_path: str,
|
|
186
|
+
value: Any,
|
|
187
|
+
*,
|
|
188
|
+
merge_strategy: str = "upsert",
|
|
189
|
+
file_path: str | None = None,
|
|
190
|
+
expected_version: str | None = None,
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
return self._run(
|
|
193
|
+
self._client.config_value_write(
|
|
194
|
+
key_path,
|
|
195
|
+
value,
|
|
196
|
+
merge_strategy=merge_strategy,
|
|
197
|
+
file_path=file_path,
|
|
198
|
+
expected_version=expected_version,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def config_batch_write(
|
|
203
|
+
self,
|
|
204
|
+
edits: list[dict[str, Any]],
|
|
205
|
+
*,
|
|
206
|
+
file_path: str | None = None,
|
|
207
|
+
expected_version: str | None = None,
|
|
208
|
+
) -> dict[str, Any]:
|
|
209
|
+
return self._run(
|
|
210
|
+
self._client.config_batch_write(
|
|
211
|
+
edits,
|
|
212
|
+
file_path=file_path,
|
|
213
|
+
expected_version=expected_version,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def config_requirements_read(self) -> dict[str, Any]:
|
|
218
|
+
return self._run(self._client.config_requirements_read())
|
|
219
|
+
|
|
220
|
+
def config_mcp_server_reload(self) -> dict[str, Any]:
|
|
221
|
+
return self._run(self._client.config_mcp_server_reload())
|
|
222
|
+
|
|
223
|
+
def mcp_server_status_list(self, *, limit: int | None = None, cursor: str | None = None) -> dict[str, Any]:
|
|
224
|
+
return self._run(self._client.mcp_server_status_list(limit=limit, cursor=cursor))
|
|
225
|
+
|
|
226
|
+
def mcp_server_oauth_login(
|
|
227
|
+
self,
|
|
228
|
+
name: str,
|
|
229
|
+
*,
|
|
230
|
+
scopes: list[str] | None = None,
|
|
231
|
+
timeout_secs: int | None = None,
|
|
232
|
+
) -> dict[str, Any]:
|
|
233
|
+
return self._run(self._client.mcp_server_oauth_login(name, scopes=scopes, timeout_secs=timeout_secs))
|
|
234
|
+
|
|
235
|
+
def fuzzy_file_search(
|
|
236
|
+
self,
|
|
237
|
+
query: str,
|
|
238
|
+
*,
|
|
239
|
+
roots: list[str] | None = None,
|
|
240
|
+
cancellation_token: str | None = None,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
return self._run(
|
|
243
|
+
self._client.fuzzy_file_search(
|
|
244
|
+
query,
|
|
245
|
+
roots=roots,
|
|
246
|
+
cancellation_token=cancellation_token,
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def responses_events(
|
|
251
|
+
self,
|
|
252
|
+
*,
|
|
253
|
+
prompt: str,
|
|
254
|
+
session_id: str | None = None,
|
|
255
|
+
thread_params: dict[str, Any] | None = None,
|
|
256
|
+
turn_params: dict[str, Any] | None = None,
|
|
257
|
+
) -> Iterator[ResponseEvent]:
|
|
258
|
+
"""Run one prompt and yield structured events."""
|
|
259
|
+
|
|
260
|
+
async_iter = self._client.responses_events(
|
|
261
|
+
prompt=prompt,
|
|
262
|
+
session_id=session_id,
|
|
263
|
+
thread_params=thread_params,
|
|
264
|
+
turn_params=turn_params,
|
|
265
|
+
)
|
|
266
|
+
try:
|
|
267
|
+
while True:
|
|
268
|
+
try:
|
|
269
|
+
event = self._run(async_iter.__anext__())
|
|
270
|
+
except StopAsyncIteration:
|
|
271
|
+
break
|
|
272
|
+
else:
|
|
273
|
+
yield event
|
|
274
|
+
finally:
|
|
275
|
+
aclose = getattr(async_iter, "aclose", None)
|
|
276
|
+
if callable(aclose):
|
|
277
|
+
try:
|
|
278
|
+
self._run(aclose())
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
def responses_stream_text(
|
|
283
|
+
self,
|
|
284
|
+
*,
|
|
285
|
+
prompt: str,
|
|
286
|
+
session_id: str | None = None,
|
|
287
|
+
thread_params: dict[str, Any] | None = None,
|
|
288
|
+
turn_params: dict[str, Any] | None = None,
|
|
289
|
+
) -> Iterator[str]:
|
|
290
|
+
"""Run one prompt and yield text deltas only."""
|
|
291
|
+
|
|
292
|
+
async_iter = self._client.responses_stream_text(
|
|
293
|
+
prompt=prompt,
|
|
294
|
+
session_id=session_id,
|
|
295
|
+
thread_params=thread_params,
|
|
296
|
+
turn_params=turn_params,
|
|
297
|
+
)
|
|
298
|
+
try:
|
|
299
|
+
while True:
|
|
300
|
+
try:
|
|
301
|
+
chunk = self._run(async_iter.__anext__())
|
|
302
|
+
except StopAsyncIteration:
|
|
303
|
+
break
|
|
304
|
+
else:
|
|
305
|
+
yield chunk
|
|
306
|
+
finally:
|
|
307
|
+
aclose = getattr(async_iter, "aclose", None)
|
|
308
|
+
if callable(aclose):
|
|
309
|
+
try:
|
|
310
|
+
self._run(aclose())
|
|
311
|
+
except Exception:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
def responses_create(
|
|
315
|
+
self,
|
|
316
|
+
*,
|
|
317
|
+
prompt: str,
|
|
318
|
+
session_id: str | None = None,
|
|
319
|
+
thread_params: dict[str, Any] | None = None,
|
|
320
|
+
turn_params: dict[str, Any] | None = None,
|
|
321
|
+
include_events: bool = False,
|
|
322
|
+
) -> AgentResponse:
|
|
323
|
+
"""Run one prompt and return final aggregated response."""
|
|
324
|
+
|
|
325
|
+
return self._run(
|
|
326
|
+
self._client.responses_create(
|
|
327
|
+
prompt=prompt,
|
|
328
|
+
session_id=session_id,
|
|
329
|
+
thread_params=thread_params,
|
|
330
|
+
turn_params=turn_params,
|
|
331
|
+
include_events=include_events,
|
|
332
|
+
)
|
|
333
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ._shared import utc_now
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class AgentResponse:
|
|
11
|
+
"""Aggregated final response for one prompt execution.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
text: Final assistant text (message completion or merged deltas).
|
|
15
|
+
session_id: Thread id used by app-server.
|
|
16
|
+
request_id: Optional request id extracted from notifications.
|
|
17
|
+
tool_name: Backend label, currently always ``"app-server"``.
|
|
18
|
+
raw: Raw payload from the last observed event.
|
|
19
|
+
events: Optional full event list when ``include_events=True`` is used.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
text: str
|
|
23
|
+
session_id: str
|
|
24
|
+
request_id: str | None
|
|
25
|
+
tool_name: str
|
|
26
|
+
raw: Any
|
|
27
|
+
events: list["ResponseEvent"] | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ResponseEvent:
|
|
32
|
+
"""Normalized streaming event produced from app-server notifications."""
|
|
33
|
+
|
|
34
|
+
type: str
|
|
35
|
+
phase: str
|
|
36
|
+
text_delta: str | None = None
|
|
37
|
+
message_text: str | None = None
|
|
38
|
+
request_id: str | None = None
|
|
39
|
+
session_id: str | None = None
|
|
40
|
+
turn_id: str | None = None
|
|
41
|
+
item_id: str | None = None
|
|
42
|
+
summary_index: int | None = None
|
|
43
|
+
thread_name: str | None = None
|
|
44
|
+
token_usage: dict[str, Any] | None = None
|
|
45
|
+
plan: list[dict[str, Any]] | None = None
|
|
46
|
+
diff: str | None = None
|
|
47
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
48
|
+
timestamp: str = field(default_factory=lambda: utc_now())
|
|
@@ -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,17 @@
|
|
|
1
|
+
codex_python_sdk/__init__.py,sha256=aD7oFvTq-ZFk5RWlF_nd0ycXr0JXltz-LmeXReC7YmQ,1410
|
|
2
|
+
codex_python_sdk/_shared.py,sha256=0YwwZ1KeM9AvUJ8v_o3mjeFeUbtKk9j3WJZvUbGks_g,3028
|
|
3
|
+
codex_python_sdk/async_client.py,sha256=Wmc39b8jpHJi8mARVFRSAANriXaX6n0bjtFqj2hsFA0,54972
|
|
4
|
+
codex_python_sdk/errors.py,sha256=ndPfykdV4xFRZDppxyKlFCjHzXlP-SVifIlAExIEnJI,493
|
|
5
|
+
codex_python_sdk/factory.py,sha256=BenAH6fWAgI1HVvGpWDV_2dCYyz2K_XxLf9dLI2ipQo,597
|
|
6
|
+
codex_python_sdk/policy.py,sha256=63ssVhPQ-AGGbDY5-xMpBlRVFkPv7QlyDjpiktEzaVM,23115
|
|
7
|
+
codex_python_sdk/renderer.py,sha256=doE_adm-WpJ_C1ETbHoVhMfxIi19IYH_xuES7TMcY-s,23654
|
|
8
|
+
codex_python_sdk/sync_client.py,sha256=8kiym7uEveHBhl0savtDkAFo_oHARTTzJtKYcps8CKI,11508
|
|
9
|
+
codex_python_sdk/types.py,sha256=Kahi-p3WSOEeXG5ep0gCji8oG6__1GJ4wYcmE92Lmnc,1435
|
|
10
|
+
codex_python_sdk/examples/__init__.py,sha256=PHh4jqD7i0aL8W8hXThczKcT4xsNZiXanBxOH5IOW5I,36
|
|
11
|
+
codex_python_sdk/examples/demo_smoke.py,sha256=BHWVSbTSvbMKnVdnJNOPivzMS-SfHwf7SN2HkevEFoA,13569
|
|
12
|
+
codex_python_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=ww_lp8EcdsoJ9-K-8xXnJ_n8SX6FNiv7tKE8jk2Gzh0,1072
|
|
13
|
+
codex_python_sdk-0.1.0.dist-info/METADATA,sha256=dJRAGTgKZxaOgNoSZiskqMEbh2FzTVSZNXZk_zSD97E,8441
|
|
14
|
+
codex_python_sdk-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
15
|
+
codex_python_sdk-0.1.0.dist-info/entry_points.txt,sha256=YiQUxrLaU3Mwjp8X4kMNpCkhY1HXMN2TJIuYyQWt-l8,84
|
|
16
|
+
codex_python_sdk-0.1.0.dist-info/top_level.txt,sha256=Et4uySZ0xrY_Fog06H-5_jyj1ZOoTwKIfVvyYOeZQVI,17
|
|
17
|
+
codex_python_sdk-0.1.0.dist-info/RECORD,,
|