python-codex 0.2.7__py3-none-any.whl → 0.3.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.
Files changed (84) hide show
  1. pycodex/__init__.py +14 -14
  2. pycodex/agent.py +465 -499
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -510
  5. pycodex/compat.py +19 -5
  6. pycodex/context.py +222 -212
  7. pycodex/doctor.py +52 -48
  8. pycodex/events.py +857 -0
  9. pycodex/feishu_card.py +217 -163
  10. pycodex/feishu_link.py +43 -83
  11. pycodex/model.py +324 -253
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +76 -45
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +245 -983
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -176
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +1 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +58 -25
  23. pycodex/tools/close_agent_tool.py +2 -2
  24. pycodex/tools/code_mode_manager.py +77 -64
  25. pycodex/tools/exec_command_tool.py +26 -11
  26. pycodex/tools/exec_tool.py +4 -4
  27. pycodex/tools/grep_files_tool.py +12 -10
  28. pycodex/tools/ipython_tool.py +10 -13
  29. pycodex/tools/list_dir_tool.py +13 -9
  30. pycodex/tools/read_file_tool.py +29 -17
  31. pycodex/tools/request_permissions_tool.py +15 -5
  32. pycodex/tools/request_user_input_tool.py +13 -104
  33. pycodex/tools/resume_agent_tool.py +2 -2
  34. pycodex/tools/send_input_tool.py +11 -8
  35. pycodex/tools/shell_command_tool.py +7 -5
  36. pycodex/tools/shell_tool.py +7 -5
  37. pycodex/tools/spawn_agent_tool.py +7 -4
  38. pycodex/tools/unified_exec_manager.py +102 -69
  39. pycodex/tools/update_plan_tool.py +8 -5
  40. pycodex/tools/view_image_tool.py +7 -5
  41. pycodex/tools/wait_agent_tool.py +27 -4
  42. pycodex/tools/wait_tool.py +5 -4
  43. pycodex/tools/web_search_tool.py +4 -2
  44. pycodex/tools/write_stdin_tool.py +12 -11
  45. pycodex/utils/__init__.py +2 -17
  46. pycodex/utils/compactor.py +41 -72
  47. pycodex/utils/debug.py +2 -2
  48. pycodex/utils/dotenv.py +6 -7
  49. pycodex/utils/event_helpers.py +190 -0
  50. pycodex/utils/get_env.py +27 -70
  51. pycodex/{image_utils.py → utils/image_utils.py} +8 -11
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +217 -163
  54. pycodex/utils/truncation.py +21 -45
  55. python_codex-0.3.0.dist-info/METADATA +704 -0
  56. python_codex-0.3.0.dist-info/RECORD +90 -0
  57. responses_server/__init__.py +1 -5
  58. responses_server/__main__.py +0 -1
  59. responses_server/app.py +36 -31
  60. responses_server/config.py +23 -23
  61. responses_server/messages_api.py +51 -53
  62. responses_server/payload_processors.py +25 -20
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +101 -98
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +36 -14
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +461 -375
  71. workspace_server/workspace.html +852 -228
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +137 -79
  74. pycodex/collaboration.py +0 -20
  75. pycodex/interactive_session.py +0 -415
  76. pycodex/prompts/collaboration_default.md +0 -11
  77. pycodex/prompts/collaboration_plan.md +0 -128
  78. pycodex/utils/toolcall_visualize.py +0 -713
  79. pycodex/utils/visualize.py +0 -560
  80. python_codex-0.2.7.dist-info/METADATA +0 -455
  81. python_codex-0.2.7.dist-info/RECORD +0 -93
  82. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,704 @@
1
+ Metadata-Version: 2.1
2
+ Name: python-codex
3
+ Version: 0.3.0
4
+ Summary: A minimal Python extraction of Codex's main agent loop
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.6.2
7
+ Requires-Dist: dataclasses>=0.8; python_version < '3.7'
8
+ Requires-Dist: fastapi<0.84,>=0.83.0; python_version < '3.7'
9
+ Requires-Dist: fastapi>=0.83.0; python_version >= '3.7'
10
+ Requires-Dist: importlib-metadata>=4.8.3; python_version < '3.8'
11
+ Requires-Dist: loguru>=0.7.3
12
+ Requires-Dist: pillow>=8.4.0; python_version < '3.7'
13
+ Requires-Dist: pillow>=9.0.0; python_version >= '3.7'
14
+ Requires-Dist: prompt-toolkit>=3.0.36
15
+ Requires-Dist: pycryptodomex>=3.20
16
+ Requires-Dist: pyyaml>=6.0
17
+ Requires-Dist: requests>=2.27.1
18
+ Requires-Dist: tomli<2,>=1.2.3; python_version < '3.11'
19
+ Requires-Dist: typing-extensions>=4.1.1; python_version < '3.8'
20
+ Requires-Dist: uvicorn<0.17,>=0.16.0; python_version < '3.7'
21
+ Requires-Dist: uvicorn>=0.16.0; python_version >= '3.7'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # pycodex
25
+
26
+ English README. Chinese version: `README_ZH.md`
27
+
28
+ 0.3.0 release preparation and Python API migration: `docs/RELEASE_0.3.0.md`.
29
+
30
+ PyPI distributions:
31
+
32
+ - Primary package: `python-codex`
33
+ - Workspace install alias: `pycodex-ws`
34
+
35
+ The import path remains `pycodex`; the CLI commands are `pycodex` and
36
+ `pycodex-ws`.
37
+
38
+ `pycodex-ws` is a thin metapackage that depends on the exact matching
39
+ `python-codex` version. The implementation and console script remain owned by
40
+ `python-codex`, so the two distributions never install duplicate modules.
41
+
42
+ This repository extracts the core Codex agent loop from upstream Codex
43
+ (`https://github.com/openai/codex`) into a deliberately small Python version,
44
+ while preserving the two most important layers:
45
+
46
+ - `submission_loop`: sequentially consumes submitted operations.
47
+ - `run_turn`: keeps executing `model sample -> tool call -> feed tool result
48
+ back into the model` inside a single turn until a final answer is reached.
49
+
50
+ Relevant Rust reference points:
51
+
52
+ - `codex-rs/core/src/codex.rs` -> `submission_loop`
53
+ - `codex-rs/core/src/codex.rs` -> `run_turn`
54
+ - `codex-rs/core/src/codex.rs` -> `run_sampling_request`
55
+ - `codex-rs/core/src/tools/router.rs` -> `ToolRouter`
56
+ - `codex-rs/core/src/stream_events_utils.rs` -> `handle_output_item_done`
57
+
58
+ ## Quick Start
59
+
60
+ Install the full package or the workspace-oriented alias:
61
+
62
+ ```bash
63
+ pip install python-codex
64
+ pip install pycodex-ws
65
+ ```
66
+
67
+ Install dependencies first:
68
+
69
+ ```bash
70
+ uv sync
71
+ ```
72
+
73
+ Try the real entry points:
74
+
75
+ ```bash
76
+ uv run pycodex "Reply with exactly OK."
77
+ uv run pycodex
78
+ ```
79
+
80
+ ## Design Tradeoffs
81
+
82
+ This is not a 1:1 port of the Rust implementation. The current goal is a
83
+ minimal reusable kernel that converges on the upstream behavior over time:
84
+
85
+ 1. Use a thin `ModelClient` protocol to abstract the model side.
86
+ 2. Use `ToolRegistry` to manage tool specs and executors.
87
+ 3. Use `AgentLoop` to implement the core closed loop.
88
+ 4. Use `AgentRuntime` to preserve the outer submission queue so it can keep
89
+ converging toward Rust's `submission_loop` later.
90
+
91
+ Intentionally not included yet:
92
+
93
+ - TUI / streaming incremental rendering
94
+ - MCP / connectors / sandbox / approvals
95
+ - memory / compact / review mode
96
+ - a full production OpenAI adapter surface
97
+
98
+ All of those can be layered on later. For now, the project is focused on
99
+ nailing the core tool-augmented reasoning loop first.
100
+
101
+ ## Layout
102
+
103
+ - `pycodex/protocol.py`: minimal conversation item / prompt protocol
104
+ - `pycodex/events.py`: typed events, plain-text views and stateful presentation
105
+ - `pycodex/utils/event_helpers.py`: stateless text, color, summary and result-formatting helpers
106
+ - `pycodex/utils/image_utils.py`: image loading, resizing and data-URL preparation
107
+ - `pycodex/model.py`: model client protocol and Responses API adapter
108
+ - `pycodex/cli.py`: single-turn/interactive entry points, terminal I/O executor and input loop
109
+ - `pycodex/bootstrap.py`: frontend-independent model, tools, Agent and session assembly
110
+ - `pycodex/tools/base_tool.py`: `BaseTool`, `ToolRegistry`, `ToolContext`
111
+ - `pycodex/tools/`: concrete tool implementations
112
+ - `pycodex/agent.py`: inner turn loop
113
+ - `pycodex/runtime.py`: session commands, submission queues and frontend event subscriptions
114
+ - `tests/test_agent.py`: core behavior tests
115
+
116
+ ## Current Alignment Status
117
+
118
+ Current progress is easiest to read in layers:
119
+
120
+ - prompt/context alignment:
121
+ - the 2026-09-23 audit uses Codex CLI 0.153.4: shared context matches for
122
+ `gpt-5.4` first/resume/tool-follow-up requests and suffixed Astra first/resume
123
+ requests, with explicit exclusions recorded in `docs/CONTEXT.md`;
124
+ - this layer is now mainly handled by `pycodex/context.py` plus vendored
125
+ prompt data; model lookup uses the longest matching slug prefix without
126
+ rewriting the requested model name.
127
+ - turn-loop semantic alignment:
128
+ - `AgentLoop` no longer uses a fixed 12-iteration cap by default;
129
+ - like upstream, it now converges naturally based on whether there is still
130
+ follow-up work or tool handoff to do;
131
+ - the local iteration-limit parameter is gone.
132
+ - request-level alignment:
133
+ - full raw request parity is **not** claimed: client-generated message/result
134
+ IDs, the tool catalog, new upstream telemetry, and some permission profiles
135
+ still differ;
136
+ - the default CLI keeps the `codex-tui` client identity, but intentionally
137
+ omits collaboration-mode developer instructions;
138
+ - `User-Agent` and rollout `cli_version` use the fixed upstream alignment
139
+ version `0.153.4`; session creation does not run `codex --version`;
140
+ - `tests/compare_context_requests.py` keeps raw differences separate from
141
+ shared-context exclusions; older interactive captures are not a fresh
142
+ certification of 0.153.4 interactive behavior.
143
+ - structured user input:
144
+ - `request_user_input` remains declared, but calls return
145
+ `request_user_input is unavailable in Default mode` without opening a question;
146
+ - attaching a frontend or input handler does not enable it. The generic runtime
147
+ input and permission services remain available;
148
+ - local tool-result `success` metadata is omitted from API requests, including
149
+ when replaying answers from older rollouts.
150
+
151
+ See `docs/ALIGNMENT.md` for more detailed notes.
152
+
153
+ `PYCODEX_DUMP` records downstream `stream_completed` and `stream_error_type`
154
+ alongside usage, finish reason, and exact token IDs. Failed attempts remain
155
+ in the dump, including failures before the first chunk. Chat SSE `error`
156
+ payloads propagate as `response.failed`, never as an empty completed response.
157
+ Chained failures also retain `stream_error_cause_type` and, for HTTP errors,
158
+ `stream_error_http_status`; exception messages, headers and URLs are not
159
+ copied into these diagnostic fields.
160
+
161
+ ## Live Model Integration
162
+
163
+ If this machine already has a Codex CLI configuration, `pycodex` can reuse the
164
+ `model`, `model_provider`, `base_url`, and `env_key` from
165
+ `~/.codex/config.toml` directly:
166
+
167
+ ```python
168
+ from pycodex import ResponsesModelClient
169
+
170
+ client = ResponsesModelClient.from_codex_config()
171
+ ```
172
+
173
+ The current implementation uses the streaming OpenAI-compatible `/responses`
174
+ endpoint. This path has already been validated against the local
175
+ `~/.codex/config.toml` setup.
176
+
177
+ When launched through the CLI, `pycodex` also loads `.env` from the same
178
+ configuration directory before reading config (typically `~/.codex/.env`), so
179
+ provider keys and similar environment variables can live there. To match
180
+ upstream Codex, variables starting with `CODEX_` are not imported from `.env`.
181
+
182
+ ## pycodex CLI
183
+
184
+ `pycodex` now defaults to a minimal interactive entry point. Internally it uses
185
+ `AgentRuntime` to drive the turn submission loop and reuses
186
+ `~/.codex/config.toml` by default:
187
+
188
+ ```bash
189
+ pycodex
190
+ pycodex "Summarize this repo in one sentence."
191
+ printf 'Reply with exactly OK.' | pycodex
192
+ pycodex --json "Reply with exactly OK."
193
+ pycodex --profile model_proxy "Reply with exactly OK."
194
+ pycodex --profile opus --use-messages "Reply with exactly OK."
195
+ pycodex --vllm-endpoint http://127.0.0.1:18000 "Reply with exactly OK."
196
+ pycodex --put @127.0.0.1:5577
197
+ pycodex --put /data/.codex/@127.0.0.1:5577
198
+ pycodex --call SECRET-CALLID@127.0.0.1:5577 "Reply with exactly OK."
199
+ pycodex doctor
200
+ pycodex-ws --listen 0.0.0.0:6007 --workspace-config ./workspaces.json
201
+ pycodex-ws --listen 0.0.0.0:6007 --workspace-config ./workspaces.json --password 12345
202
+ ```
203
+
204
+ Current behavior:
205
+
206
+ - with no argv prompt and a TTY stdin, enter interactive mode
207
+ - with an argv prompt or piped stdin, run a single turn
208
+ - `pycodex-ws` starts the standalone browser workspace manager and serves each
209
+ workspace with a board pane and a pycodex session pane
210
+ - interactive mode exits through `/exit`, `/quit`, Ctrl+D on an empty prompt,
211
+ or a single Ctrl+C; accepted work finishes before cleanup and normal exit.
212
+ A `[closing]` message explains the wait. Press Ctrl+C again while closing
213
+ to immediately exit the CLI with status 130. This skips remaining cleanup;
214
+ external tool processes may keep running.
215
+ - interactive mode shows a compact event stream for user-visible phases such as
216
+ tool execution and model follow-up after tool results
217
+ - assistant text is printed from streaming deltas directly
218
+ - interactive mode supports `/history`, `/title`, `/model`, `/resume`, `/compact`,
219
+ and `/fork`
220
+ - `/model <name>` switches the model used by later turns in the current
221
+ interactive session; `/model` shows the current model and available choices
222
+ - `/resume` with no argument lists the currently resumable sessions by their
223
+ first user-message preview; `/resume 1` resumes the first listed session
224
+ - `/resume <number>` replaces the in-memory history with the selected recorded
225
+ Codex rollout from `CODEX_HOME/sessions`. Tool calls without recorded results
226
+ are omitted during restore; later saved messages and completed tools remain
227
+ available, and the source file is left intact.
228
+ - `/compact` synthesizes a local handoff summary, replaces the in-memory
229
+ conversation history with the compacted view, and appends a compacted-history
230
+ entry to the rollout so later `/resume` sees the same state. The handoff remains
231
+ model context, but is omitted from frontend conversation blocks and `/history`;
232
+ real replies after compaction remain visible, including after resume or fork.
233
+ - `/fork` allocates a new Agent/provider session id and lazy rollout while
234
+ preserving current history and the workspace tab; the original rollout stays intact
235
+ - workspace tabs follow the fork's rollout once it is written. Before that,
236
+ restart restores the source recording and creates a fresh fork, preserving
237
+ history and title without appending to the source file.
238
+ - `model_auto_compact_token_limit = <tokens>` in `config.toml` enables the same
239
+ compaction path automatically when the latest reported usage reaches that
240
+ threshold before a follow-up sampling request or the next user turn
241
+ - `service_tier = "fast"` enables Fast mode for models whose vendored metadata
242
+ advertises the `priority` service tier; pycodex follows upstream Codex by
243
+ sending `service_tier = "priority"` on the Responses request, while
244
+ `service_tier = "default"` or unsupported tiers are omitted
245
+ - if a model request fails with `context_length_exceeded`, pycodex now treats
246
+ the provider-reported requested token count as a failed-request usage sample,
247
+ triggers the same compact path immediately, and retries the request once; if
248
+ the compact request is also over the limit, it repeatedly drops the oldest
249
+ tool response plus its matching tool call before retrying compact
250
+ - new sessions are now recorded under `CODEX_HOME/sessions/.../rollout-*.jsonl`
251
+ with a stable session/thread id and per-item append+flush semantics so
252
+ `/resume` reads back the same rollout format
253
+ - if `TURN_HOOK.md` exists in the workspace root and is non-empty, each
254
+ completed turn also forks the just-finished history into a temporary,
255
+ non-persisted follow-up session and submits the file contents as the next
256
+ user instruction; this is intended for side-effect follow-ups such as
257
+ Feishu notifications
258
+ - `/link <feishu-email|open_id|chat_id>` attaches the current interactive
259
+ session to a Feishu card; multiple sessions in the same pycodex process share
260
+ one Feishu long-connection listener and route card actions by message id
261
+ - Feishu user OAuth stores its refresh token only in
262
+ `~/.codex/.feishu_refresh_token` (mode `0600`). Run
263
+ `env -u VIRTUAL_ENV uv run python tools/feishu_oauth.py` from this repository
264
+ to authorize; app credentials can stay in `~/.codex/.env`. Each refresh reads
265
+ the latest token from the dedicated file and saves its replacement there.
266
+ `FEISHU_REFRESH_TOKEN` in `.env` or the environment is no longer used and
267
+ can be removed. Refresh is triggered by API calls, with no idle keepalive.
268
+ - `pycodex-ws --workspace-config workspaces.json` serves workspace boards from
269
+ one process. The JSON file can be either a list or
270
+ `{"workspaces": [...]}`; each entry uses `board` and `work_dir`, with optional
271
+ display/URL `id`, and is exposed at `/w/<id>/`. Internally the resolved
272
+ `board` path is the workspace identity, so adding the same board path twice is
273
+ rejected even with a different name. Relative `board` and `work_dir` paths
274
+ resolve from the config file directory. The root path `/` shows a workspaces
275
+ management page with `add_workspace(name=None, dir="./", board=None)` and
276
+ `delete(name)` controls; omitted names become `workspace-1`, `workspace-2`,
277
+ etc. If `board` is omitted when adding a workspace, pycodex assigns a random
278
+ writable `/tmp/pcws-*.html` board path. Add/delete actions and later
279
+ session-state saves refresh the JSON file. Board HTML can reference local
280
+ images beside the board (including nested paths) with relative URLs; only
281
+ `image/*` files contained by the board directory are served.
282
+ Assistant Markdown supports KaTeX formulas with `$...$`, `$$...$$`,
283
+ `\(...\)`, and `\[...\]` delimiters.
284
+ `--password <value>` enables a password-only login page for workspace pages,
285
+ APIs, and websocket connections. After login, the browser returns to the
286
+ requested workspace path with its query parameters.
287
+ Questions and permission requests appear below the conversation only while
288
+ waiting for input; they disappear when answered, cancelled, or timed out,
289
+ leaving the continuing assistant output at the bottom.
290
+ Queued enqueue/steer messages become user conversation blocks when their
291
+ turns start, in execution order. Small capsules to the left of the status
292
+ pill preview pending Steer (`↑`) and Queue (`◷`) text. Long text is truncated;
293
+ hover shows the full pending content. Each disappears when that type has
294
+ no waiting input.
295
+ - steer is enabled by default in interactive mode: normal input goes into the
296
+ runtime steer path, the current request stops at the next safe boundary, and
297
+ later steer text is appended to the next model request's `input` in order;
298
+ for explicit queueing, use `/queue <message>`, which prints
299
+ `[steer] queued: ...` and later `[steer] inserted: ...`
300
+ - the default local tool set includes the upstream-aligned subset plus the
301
+ pycodex `clock` extension: `shell`, `shell_command`, `exec_command`,
302
+ `write_stdin`, `clock`, `exec`, `wait`, `web_search`, `update_plan`,
303
+ `request_user_input`, `request_permissions`, `spawn_agent`, `send_input`,
304
+ `resume_agent`, `wait_agent`, `close_agent`, `apply_patch`, `grep_files`,
305
+ `read_file`, `list_dir`, `view_image`
306
+ - `clock(period_m)` sets one periodic clock for the current Agent session;
307
+ `null` cancels it. The countdown restarts after each reply and wakes the
308
+ Agent with a `<clock_tick>` message containing the current timezone-aware
309
+ time when it expires.
310
+ - while a background command or clock is pending, the idle status is
311
+ `idle: sleeping`
312
+ - only the active workspace tab shows its close button; closing it restores
313
+ the selected tab's conversation, draft, and scroll position immediately
314
+ after its snapshot loads
315
+ - `--vllm-endpoint http://host:port` automatically launches a local
316
+ `responses_server` compatibility layer; when the URL path is empty it is
317
+ normalized to `/v1`, and `/responses` requests are still forwarded to the
318
+ downstream `/v1/chat/completions` endpoint. This local compat path always
319
+ uses the canonical Responses request shape, even when the selected model's
320
+ metadata enables `responses_lite`. With `--vllm-endpoint`, startup also reads
321
+ `/v1/models` and uses the last returned model id for the downstream request.
322
+ For `model_provider = "vllm"`, reasoning is preserved across this path:
323
+ chat chunks with `reasoning` or `reasoning_content` are translated back into
324
+ Responses `reasoning` items, and
325
+ historical `reasoning` items are replayed into downstream assistant messages
326
+ via the `reasoning` field. Streaming token usage is also requested from vLLM
327
+ and forwarded to the final `response.completed.response.usage`. If a
328
+ downstream chat stream terminates after emitting only reasoning, with no
329
+ assistant content and no tool call, the compat layer discards that partial
330
+ reasoning, retries the same downstream request once, and only then emits
331
+ `response.failed` with `type = "model_output_invalid"` if the retry is still
332
+ reasoning-only
333
+ - standalone `responses_server` now also supports downstream `/v1/messages`
334
+ backends via `--outcomming-api messages`, while keeping the internal
335
+ canonical request/route logic in chat-completions shape
336
+ - `pycodex doctor` checks config, `.env`, API keys, DNS, TCP/TLS, and an
337
+ optional live Responses API request
338
+
339
+ Current primary uses:
340
+
341
+ - verify provider / model / auth configuration
342
+ - debug `ResponsesModelClient`
343
+ - run minimal single-turn and multi-turn smoke tests
344
+
345
+ `doctor` examples:
346
+
347
+ ```bash
348
+ pycodex doctor
349
+ pycodex doctor --skip-live
350
+ pycodex doctor --json
351
+ ```
352
+
353
+ ## Portable Mode
354
+
355
+ `Portable Mode` is the quickest way to bring your usual `pycodex` setup into a
356
+ fresh machine, container, or debug image.
357
+
358
+ Use it like this:
359
+
360
+ ```bash
361
+ pycodex --put @127.0.0.1:5577
362
+ pycodex --put /data/.codex/@127.0.0.1:5577
363
+ ```
364
+
365
+ - `--put` prints a reusable `SECRET-CALLID@host:port` plus a final one-line
366
+ `pycodex --call ...` command
367
+ - on the new environment or image, run that printed `--call` command directly
368
+ - quickly restoring your usual `config.toml`, `.env`, `AGENTS.md`, and
369
+ `skills/` into a clean debug environment
370
+ - keeping a new image focused on the bug you are debugging instead of spending
371
+ time rebuilding local Codex setup by hand
372
+ - bootstrapping `pycodex` even when the target environment does not already
373
+ have a populated `~/.codex`
374
+ - bare `--put` uses the current user's `~/.codex`
375
+ - `--put /path/.codex/@host:port` lets you publish a different Codex home
376
+
377
+ ## Example
378
+
379
+ ```python
380
+ import asyncio
381
+ from pathlib import Path
382
+
383
+ from pycodex import (
384
+ Agent,
385
+ BaseTool,
386
+ ContextConfig,
387
+ ResponsesModelClient,
388
+ ToolRegistry,
389
+ )
390
+
391
+
392
+ class EchoTool(BaseTool):
393
+ name = "echo"
394
+ description = "Echo the provided text."
395
+ input_schema = {
396
+ "type": "object",
397
+ "properties": {"text": {"type": "string"}},
398
+ "required": ["text"],
399
+ }
400
+
401
+ async def run(self, context, args):
402
+ del context
403
+ return args["text"]
404
+
405
+
406
+ async def main() -> None:
407
+ config_path = Path.home() / ".codex" / "config.toml"
408
+ model = ResponsesModelClient.from_codex_config(config_path)
409
+ context_config = ContextConfig.from_codex_config(config_path)
410
+
411
+ tools = ToolRegistry()
412
+ tools.register(EchoTool())
413
+
414
+ agent = Agent(model, tools, context_config)
415
+ result = await agent.run_turn(
416
+ ["Call the echo tool with text=hello, then tell me what it returned."]
417
+ )
418
+ print(result.output_text)
419
+
420
+
421
+ asyncio.run(main())
422
+ ```
423
+
424
+ ### Turn lifecycle
425
+
426
+ `Agent` requires a model client, tool registry, and `ContextConfig`. It creates
427
+ its own `ContextManager`; cwd, instruction overrides and
428
+ extra contextual messages belong in that config. The client's model identifier
429
+ is authoritative for the Agent's context. Standalone context construction uses
430
+ `ContextManager(config)`; load files through `ContextConfig.from_codex_config`.
431
+
432
+ Collaboration modes and their prompt templates are removed. There is no mode
433
+ switch in `ContextConfig`, `ToolContext` or `build_agent`; normal interaction,
434
+ `update_plan`, structured user input and sub-agents remain independent features.
435
+
436
+ Dependencies are ordinary attributes: `model_client`, `tool_registry`,
437
+ `context_manager` and `event_handler`. The Agent has no Runtime/Queue reference;
438
+ `AgentRuntime` calls its public methods and subscribes to its events.
439
+ Pure getter/setter wrappers are removed. `history` remains a tuple snapshot,
440
+ and `model_name` and lifecycle properties remain derived values.
441
+
442
+ `session_file_path=None`, the default, means **keep history in memory without
443
+ recording**. Passing a file path creates an internal rollout recorder. Every
444
+ Agent still receives a stable UUIDv7 session id unless an explicit id is supplied.
445
+ CLI, Web and Feishu use `build_agent`, which assigns an id and a matching path
446
+ under the configured Codex home's `sessions/` directory. Sub-agents leave the path
447
+ as `None`, so their turns, compaction, forks and reopening stay in memory.
448
+
449
+ When a path is supplied, construction does not create the file or its parent
450
+ directories. The first history append or successful compaction creates it,
451
+ writing metadata and the current initial history before the new records.
452
+ An existing destination raises `FileExistsError`; exclusive creation also
453
+ prevents overwriting a file that appears before the first write.
454
+ `agent.session_id` and the read-only `agent.session_file_path` expose the identity
455
+ and optional recording path without exposing the recorder.
456
+
457
+ ```python
458
+ agent = Agent(model, tools, context_config) # In-memory session.
459
+ agent.resume("~/.codex/sessions/2026/09/22/rollout-example.jsonl")
460
+ ```
461
+
462
+ `resume(path)` reads an existing file, restores history and session identity, and
463
+ continues appending to that same file. Paths accept `~`; the existing
464
+ concatenated-JSON and compact-checkpoint loader is reused. Constructing an Agent
465
+ and immediately resuming another file leaves no unused rollout behind. Initial
466
+ history is recorded only when the new recorded session first writes. `history`
467
+ is read-only; there is no public `replace_history()` interface.
468
+
469
+ An `Agent` executes one turn or manual compaction at a time.
470
+ There are two public turn interfaces:
471
+
472
+ - `agent.ask(text)` blocks and returns the turn result.
473
+ - `await agent.run_turn(texts, turn_id=None)` executes the ordinary coroutine
474
+ and returns the turn result.
475
+
476
+ The Agent does not create, hold or return a per-turn Task. Creating a coroutine
477
+ does not start it or mark the Agent busy. A host that needs concurrent UI/input
478
+ handling can schedule the coroutine with `asyncio.create_task`; synchronous code
479
+ can use `ask` or `asyncio.run(agent.run_turn([...]))`. `start_turn` is removed.
480
+
481
+ `agent.is_running` reflects actual execution. One idle event backs both that
482
+ state and `await agent.wait_until_idle()`; `finally` releases it on exit.
483
+ Starting an overlapping operation raises `RuntimeError`; use `AgentRuntime`
484
+ for queued or steer submissions. `maybe_invoke` skips a busy or closed Agent;
485
+ when idle, it directly awaits the same `run_turn` path and propagates errors.
486
+ It returns `True` after that turn finishes, not immediately after scheduling.
487
+
488
+ `runtime.is_busy` includes active Agent work and pending submissions, including
489
+ direct/background turns that bypass the queue. CLI, web, and Feishu use this
490
+ state instead of tracking another busy flag. There is no Agent cancellation API:
491
+ `agent.cancel()`, `runtime.cancel_current()` and Task listeners are removed.
492
+
493
+ Steer asks `agent.stop_asap()` to end the current execution at a safe boundary.
494
+ The in-flight request and all issued tools finish first; the Agent records their
495
+ results, emits `turn_interrupted` and raises `TurnInterrupted`, without knowing
496
+ why the stop was requested. Runtime alone settles submission futures and starts
497
+ the next batch. Runtime-owned steer retains its logical turn id, but starts a new
498
+ `run_turn` execution with a fresh iteration count. `/queue` does not request a stop.
499
+ Checks after compaction prevent an extra sample when steer arrives during it.
500
+ Direct Agent calls never consume Runtime queues; their caller observes
501
+ `TurnInterrupted`, and pending input waits for the Runtime worker.
502
+
503
+ The core loop has one threshold-compaction entry, then samples, commits output
504
+ and executes tools or finishes. Before the first sample, compaction precedes new
505
+ user input; later it includes completed tool results. Context-overflow recovery
506
+ stays inside sampling: compact and retry once without counting another iteration
507
+ or replaying tools. Stops are checked before each request attempt and after the
508
+ issued tool batch, without a separate preparation wrapper. Network retries remain
509
+ inside the provider.
510
+
511
+ `await agent.compact()` computes a summary and commits the replacement history
512
+ only after the summary and rollout checkpoint succeed. Failed compaction leaves
513
+ the active history unchanged, including tool results pruned from retry prompts.
514
+ History persistence failures are surfaced rather than silently ignored.
515
+ Manual and automatic compaction share the same execution/observation path.
516
+
517
+ Sub-agent status comes from Agent lifecycle events and current busy/queue state,
518
+ including direct and background invocations. Bare `Agent.shutdown()` rejects new
519
+ calls and disables background hooks, but lets the current turn finish. `runtime.close()`,
520
+ `close_agent`, and workspace close wait for accepted work and child workers to
521
+ finish naturally; a stuck request can therefore keep close waiting.
522
+ `resume_agent` explicitly reopens the child. No turn-abort or synthetic
523
+ interruption-result recovery is performed.
524
+
525
+ Queue lifecycle uses only `start()` and `close()`. Closing switches off admission;
526
+ the worker exits when the queues are empty, without a special shutdown request.
527
+ Background invocations obey the same admission switch. Tool shutdown hooks run
528
+ once, after draining accepted work. Concurrent or repeated close calls share the
529
+ worker's completion and errors; cancelling a caller does not cancel its Agent turn.
530
+
531
+ CLI, web and Feishu restore through the backend's `runtime.resume(session_file_path)`,
532
+ which delegates to `agent.resume(session_file_path)`. It rejects
533
+ active or queued work, loads history, restores supported provider session
534
+ identity, switches its internal recorder, and clears stale usage in one
535
+ synchronous operation. Load failures leave the existing session unchanged.
536
+ The backend broadcasts restored history/title/identity to all attached views.
537
+ `agent.resume()` without a path reopens the same in-memory Agent and rebinds its
538
+ tool callbacks, without reading or writing a file or changing history, identity,
539
+ recorder or usage. It returns `None` and also rejects active or queued work.
540
+ Sub-agent services use this form to reopen the child's in-memory history.
541
+ There is no separate `reopen()` method.
542
+ Model switching and history replacement remain explicit operations because they
543
+ enforce state consistency, not merely assign a field.
544
+
545
+ ### Shared session backend
546
+
547
+ CLI, Web and Feishu all use `AgentRuntime.submit_input`: not just `/model`
548
+ and `/resume`, but every session command, steer/queue admission, interactive
549
+ questions and permissions, state notifications and shutdown. Frontends only
550
+ adapt input and render events. Web no longer imports or runs the CLI shell.
551
+ IPython is the deliberate exception: `ipython_agent()` still returns a bare Agent.
552
+
553
+ ```python
554
+ from pycodex.bootstrap import build_agent, build_model, build_runtime
555
+
556
+ async def run_session():
557
+ runtime = build_runtime(build_agent(build_model()))
558
+ await runtime.start()
559
+ observer = runtime.attach(lambda event: print(event.kind))
560
+ try:
561
+ receipt = await runtime.submit_input("/model", sender="application")
562
+ result = await receipt.future
563
+ return result
564
+ finally:
565
+ await runtime.close()
566
+ runtime.detach(observer)
567
+ ```
568
+
569
+ Attach immediately supplies a state snapshot; subsequent events synchronize
570
+ all views. Detach does not close the backend. Plain input steers at the next
571
+ sampling boundary, `/queue` waits for its own turn, and busy Feishu cards still
572
+ accept steer or question answers. Unknown slash commands report errors instead
573
+ of silently entering model history. See [runtime contracts](docs/RUNTIME.md)
574
+ for command receipts, structured answers and session ownership.
575
+
576
+ Python observers receive typed `Event` dataclasses from `pycodex.events`, such
577
+ as `TurnStartedEvent` and `ToolCompletedEvent`, not generic payload dictionaries.
578
+ Custom model clients emit `ModelEvent` variants such as `AssistantDeltaEvent`.
579
+ Events provide reusable plain text through `event.visualize()`, or render through
580
+ `event.render(display)`. Each CLI view and Feishu card owns an `EventDisplay` with
581
+ independent stream/queue state and log/status/prompt callbacks. Events own colors,
582
+ flushing, status transitions and prompt text; frontends execute I/O or update their
583
+ display fields. Feishu disables colors and shows a bounded recent transcript plus
584
+ the live stream, rather than a separate last-answer/last-turn projection.
585
+ There is no display-handler mapping. Web retains its own projection and unchanged
586
+ event JSON format.
587
+
588
+ ### Internal contracts
589
+
590
+ - Tools implement async `BaseTool.run`; synchronous implementations are not
591
+ implicitly accepted. Tool-specific follow-up messages come from
592
+ `BaseTool.follow_up_messages`, not tool-name checks in the Agent loop.
593
+ - Completed tool results are committed individually. Explicitly parallel tool
594
+ batches wait for all their operations before propagating a commit failure;
595
+ they do not cancel remaining operations or invent interruption results.
596
+ - Background tools bind their own callbacks through `BaseTool.bind_agent`.
597
+ The Agent does not reach into specific tools' private managers.
598
+ - Event observers report failures to the asyncio exception handler without
599
+ changing the execution result. Provider stream callbacks run on the owning
600
+ event loop and stop delivering when their request is finished or cancelled.
601
+ - `ModelClient.model` is a required read-only model identifier, also supplied by
602
+ test clients. `Agent.model_name` reads it directly, without `getattr` fallback.
603
+ `ModelClient.complete` returns a `ModelResponse` containing only supported
604
+ output item types. Context overflow is reported as `ContextLengthExceeded`;
605
+ provider-specific text classification stays inside the Responses client.
606
+ - A terminal `response.incomplete` is not a disconnected stream and is not
607
+ retried. For `max_output_tokens`, done assistant/reasoning items are retained
608
+ for the next turn, but uncommitted text deltas and unresolved tool calls are not.
609
+ - Interactive clients expose the `ModelControl` interface in `pycodex.model`.
610
+ Use `agent.set_model(name)` to update provider metadata, context instructions,
611
+ context limits, and usage state together.
612
+ - Runtime services belong to the tool registry. Pass an explicit environment to
613
+ `ToolRegistry(environment)` or `get_tools(environment)` when needed; the Agent
614
+ uses `agent.tool_registry.runtime_environment`, not a forwarding Agent property.
615
+ The global `get_agent_runtime_environment()` and
616
+ separate `Agent(..., runtime_environment=...)` entry points are removed.
617
+ - Each sub-agent gets its own context manager and runtime services. The sub-agent
618
+ service retains terminal event status, rather than inspecting Task results.
619
+ - Every Agent records its session, including direct and child Agents; external
620
+ callers no longer construct or inject `SessionRolloutRecorder`. The outer queue
621
+ remains optional and no worker starts implicitly. Empty initial history and a
622
+ no-op event callback remain valid defaults.
623
+
624
+ See `docs/RUNTIME.md` for state ownership and failure semantics.
625
+
626
+ ## Alignment Checklist
627
+
628
+ See `docs/ALIGNMENT.md` for more detail. This section keeps a high-level
629
+ checklist for quick status scanning.
630
+
631
+ ### Tool Alignment
632
+
633
+ Official upstream tools:
634
+
635
+ - [x] `shell` - run shell commands in argv form.
636
+ - [x] `shell_command` - run shell scripts in string form.
637
+ - [x] `exec_command` - start long-running commands with a session.
638
+ - [x] `write_stdin` - write stdin to an existing execution session or poll
639
+ output.
640
+ - [x] `web_search` - expose provider-native web search capability.
641
+ - [x] `update_plan` - update the task plan and maintain step status.
642
+ - [x] `request_user_input` - retain the declaration and return the upstream
643
+ Default-mode unavailable response.
644
+ - [x] `request_permissions` - request extra permissions before continuing.
645
+ - [x] `spawn_agent` - create and start a sub-agent.
646
+ - [x] `send_input` - continue feeding input to an existing sub-agent.
647
+ - [x] `resume_agent` - reopen a closed sub-agent.
648
+ - [x] `wait_agent` - wait for a sub-agent to reach a terminal state.
649
+ - [x] `close_agent` - close a sub-agent that is no longer needed.
650
+ - [x] `apply_patch` - edit files precisely with a freeform patch.
651
+ - [x] `grep_files` - search file contents by pattern.
652
+ - [x] `read_file` - read file slices while preserving line-number semantics.
653
+ - [x] `list_dir` - list directory tree slices.
654
+ - [x] `view_image` - turn a local image into model-visible input.
655
+
656
+ Upstream low-frequency / special-mode tools not yet modeled separately:
657
+
658
+ - [ ] `wait_infinite` - long blocking wait for external events or later input.
659
+ - [ ] `spawn_agents_on_csv` - create sub-agent jobs in bulk from CSV.
660
+ - [ ] `report_agent_job_result` - report batch agent job results.
661
+ - [ ] `js_repl` - JavaScript REPL / code-mode primary entry point.
662
+ - [ ] `js_repl_reset` - reset `js_repl` state.
663
+ - [ ] `artifacts` - generate or manage structured artifact outputs.
664
+ - [ ] `list_mcp_resources` - list MCP resources.
665
+ - [ ] `list_mcp_resource_templates` - list MCP resource templates.
666
+ - [ ] `read_mcp_resource` - read MCP resource contents.
667
+ - [ ] `multi_tool_use.parallel` - parallel wrapper around multiple developer
668
+ tool calls.
669
+
670
+ Repository-specific compatibility / transition tools:
671
+
672
+ - [x] `clock` - pycodex periodic Agent wake-up extension.
673
+ - [x] `exec` - current local approximation of code mode.
674
+ - [x] `wait` - current local approximation of code-mode waiting behavior.
675
+
676
+ ### Behavior Alignment
677
+
678
+ - [x] `AgentLoop` / `AgentRuntime` main loop skeleton - turn loop and submission
679
+ queue are in place.
680
+ - [x] non-interactive `exec` `instructions` alignment - base instructions match
681
+ upstream.
682
+ - [x] shared `exec` context content - the audited first/resume/tool-follow-up
683
+ scenarios match after documented exclusions; raw item identity is not equal.
684
+ - [x] developer/contextual-user message shape alignment - message/content shape
685
+ matches upstream.
686
+ - [x] `AGENTS.md` + `<environment_context>` injection alignment - context
687
+ assembly order matches upstream.
688
+ - [ ] complete 0.153.4 tool catalog alignment - local `clock`, legacy sub-agent
689
+ tools, and upstream goal/deferred/code-mode-v2 surfaces still differ.
690
+ - [x] `include = ["reasoning.encrypted_content"]` - reasoning include field is
691
+ aligned.
692
+ - [x] `prompt_cache_key` - request-level prompt cache key is implemented.
693
+ - [x] `x-client-request-id` - request id header is implemented.
694
+ - [x] `x-codex-turn-metadata` - turn id / sandbox header is implemented.
695
+ - [x] `originator` - mode-aware originator header is implemented.
696
+ - [ ] current identity/telemetry header parity - local identity remains
697
+ unchanged; the audit records upstream's newer headers and `client_metadata`.
698
+ - [x] field-by-field upstream exec-mode tool schema alignment - aligned tools
699
+ use class-level specs; `clock` is documented separately as an extension.
700
+ - [ ] full interactive-mode and non-`exec` behavior alignment - the non-exec
701
+ first-turn context is now on the `codex-tui` path, but continuous REPL
702
+ multi-turn behavior is not fully verified yet.
703
+ - [ ] sandbox / approvals / compact / memory and other outer behavior alignment
704
+ - these systems are still in later scope.