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.
Files changed (27) hide show
  1. codex_python_sdk-0.1.0/LICENSE +21 -0
  2. codex_python_sdk-0.1.0/PKG-INFO +274 -0
  3. codex_python_sdk-0.1.0/README.md +243 -0
  4. codex_python_sdk-0.1.0/codex_python_sdk/__init__.py +57 -0
  5. codex_python_sdk-0.1.0/codex_python_sdk/_shared.py +99 -0
  6. codex_python_sdk-0.1.0/codex_python_sdk/async_client.py +1313 -0
  7. codex_python_sdk-0.1.0/codex_python_sdk/errors.py +18 -0
  8. codex_python_sdk-0.1.0/codex_python_sdk/examples/__init__.py +2 -0
  9. codex_python_sdk-0.1.0/codex_python_sdk/examples/demo_smoke.py +304 -0
  10. codex_python_sdk-0.1.0/codex_python_sdk/factory.py +25 -0
  11. codex_python_sdk-0.1.0/codex_python_sdk/policy.py +636 -0
  12. codex_python_sdk-0.1.0/codex_python_sdk/renderer.py +607 -0
  13. codex_python_sdk-0.1.0/codex_python_sdk/sync_client.py +333 -0
  14. codex_python_sdk-0.1.0/codex_python_sdk/types.py +48 -0
  15. codex_python_sdk-0.1.0/codex_python_sdk.egg-info/PKG-INFO +274 -0
  16. codex_python_sdk-0.1.0/codex_python_sdk.egg-info/SOURCES.txt +25 -0
  17. codex_python_sdk-0.1.0/codex_python_sdk.egg-info/dependency_links.txt +1 -0
  18. codex_python_sdk-0.1.0/codex_python_sdk.egg-info/entry_points.txt +2 -0
  19. codex_python_sdk-0.1.0/codex_python_sdk.egg-info/requires.txt +11 -0
  20. codex_python_sdk-0.1.0/codex_python_sdk.egg-info/top_level.txt +1 -0
  21. codex_python_sdk-0.1.0/pyproject.toml +62 -0
  22. codex_python_sdk-0.1.0/setup.cfg +4 -0
  23. codex_python_sdk-0.1.0/tests/test_codex_python_sdk.py +2092 -0
  24. codex_python_sdk-0.1.0/tests/test_integration_real.py +113 -0
  25. codex_python_sdk-0.1.0/tests/test_policy_engine.py +619 -0
  26. codex_python_sdk-0.1.0/tests/test_renderer.py +28 -0
  27. 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
+