python-codex 0.1.3__tar.gz → 0.1.4__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.
- {python_codex-0.1.3 → python_codex-0.1.4}/AGENTS.md +6 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/PKG-INFO +18 -3
- {python_codex-0.1.3 → python_codex-0.1.4}/README.md +17 -2
- {python_codex-0.1.3 → python_codex-0.1.4}/README_ZH.md +12 -2
- {python_codex-0.1.3 → python_codex-0.1.4}/docs/ALIGNMENT.md +26 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/docs/responses_server/README.md +1 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/agent.py +51 -11
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/cli.py +109 -3
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/context.py +23 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/model.py +362 -23
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/models.json +30 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/apply_patch_tool.py +2 -2
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/utils/__init__.py +4 -0
- python_codex-0.1.4/pycodex/utils/compactor.py +189 -0
- python_codex-0.1.4/pycodex/utils/session_persist.py +483 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/utils/visualize.py +119 -5
- {python_codex-0.1.3 → python_codex-0.1.4}/pyproject.toml +1 -1
- {python_codex-0.1.3 → python_codex-0.1.4}/responses_server/app.py +3 -1
- {python_codex-0.1.3 → python_codex-0.1.4}/responses_server/payload_processors.py +10 -1
- {python_codex-0.1.3 → python_codex-0.1.4}/responses_server/stream_router.py +25 -6
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/responses_server/test_server.py +87 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/test_agent.py +30 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/test_builtin_tools.py +1 -4
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/test_cli.py +1025 -12
- python_codex-0.1.4/tests/test_compactor.py +64 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/test_context.py +16 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/test_model.py +266 -3
- {python_codex-0.1.3 → python_codex-0.1.4}/.github/workflows/publish.yml +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/.github/workflows/test.yml +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/.gitignore +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/LICENSE +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/docs/CONTEXT.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/__init__.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/collaboration.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/compat.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/doctor.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/portable.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/portable_server.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/collaboration_default.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/collaboration_plan.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/default_base_instructions.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/exec_tools.json +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/prompts/subagent_tools.json +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/protocol.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/runtime.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/runtime_services.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/__init__.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/agent_tool_schemas.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/base_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/close_agent_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/code_mode_manager.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/exec_command_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/exec_runtime.js +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/exec_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/grep_files_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/list_dir_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/read_file_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/request_permissions_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/request_user_input_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/resume_agent_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/send_input_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/shell_command_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/shell_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/spawn_agent_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/unified_exec_manager.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/update_plan_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/view_image_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/wait_agent_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/wait_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/web_search_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/tools/write_stdin_tool.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/utils/dotenv.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/utils/get_env.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/pycodex/utils/random_ids.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/responses_server/__init__.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/responses_server/__main__.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/responses_server/config.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/responses_server/server.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/responses_server/session_store.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/responses_server/tools/__init__.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/responses_server/tools/custom_adapter.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/responses_server/tools/web_search.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/TESTS.md +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/__init__.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/compare_request_user_input_roundtrip.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/compare_steer_request_bodies.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/compare_tool_schemas.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/fake_responses_server.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/fakes.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/responses_server/fake_chat_completions_server.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/test_doctor.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/test_fake_responses_server.py +0 -0
- {python_codex-0.1.3 → python_codex-0.1.4}/tests/test_portable.py +0 -0
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
- 会话协议先收敛到 4 类 item:`UserMessage`、`AssistantMessage`、`ToolCall`、`ToolResult`;只有这层稳定后再扩 richer event model。
|
|
10
10
|
- 优先保持主循环可测试、可替换:模型侧通过 `ModelClient` 协议接入;测试专用的 `ScriptedModelClient` 放在 `tests/fakes.py`,不要放进运行时包。
|
|
11
11
|
- `ResponsesModelClient` 直接复用 `~/.codex/config.toml` 的 provider 配置;当前已验证这里的 responses provider 需要 `stream = true`,否则会返回 `400` 和 `Stream must be set to true`。
|
|
12
|
+
- 现在 `ResponsesModelClient` 默认会对流式断连做 provider 级自动重试(`stream_max_retries` 默认 5);写 CLI/REPL 测试时如果断言“先向用户报错,再靠下一句 `go on` 继续”,必须在测试 provider 配置里显式设 `stream_max_retries = 0`,否则测试可能一直等不到预期错误而卡住。
|
|
12
13
|
- `responses_server` compat 层应透传请求里的 `model`;不要再做 “取 downstream /models 第一个 id 并强制覆盖请求模型” 这种兜底兼容。
|
|
13
14
|
- 对 `model_provider = "vllm"`,`responses_server` 仍然走 `/v1/chat/completions` compat 路径,但要保留 reasoning:把 chat chunk 里的 `reasoning` / `reasoning_content` 翻回 Responses `reasoning` item,并把历史里的 Responses `reasoning` item 回放成下游 assistant message 的 `reasoning` 字段。
|
|
14
15
|
- `responses_server` 的 provider-specific chat payload 定制统一放在 `responses_server/payload_processors.py`:使用 `CompatServerConfig.model_provider` 选择 `provider_name -> proc_fn(outcomming_request)` 映射,并且只在真正发出 downstream `/v1/chat/completions` 前 post-process;`StreamRouter` 内部继续保留 canonical payload,避免 tool hydration / mock web_search follow-up 被 provider 改写污染。
|
|
15
16
|
- `pycodex` 默认是最小交互 CLI;无 prompt 时进入 REPL,并通过 `AgentRuntime` 跑外层提交循环。当前会显示最小事件流、assistant 流式输出、简单 title/history(`/title`, `/history`),并默认注册一组与原版一一对应的本地工具子集。
|
|
16
17
|
- 交互 CLI 的事件流展示优先表达用户可感知的阶段(例如工具开始/完成、模型回看工具结果),不要直接把内部 `iteration` 计数暴露成主要状态文案;`iterations` 应继续保留在 `TurnResult` 等程序化结果里。
|
|
17
18
|
- prompt/context 相关逻辑统一放在 `pycodex/context.py`:`AgentLoop` 只维护真实会话历史;每轮请求前由 `ContextManager` 注入 base instructions、developer message、`AGENTS.md` 指令和 `<environment_context>`,且这些注入项不写回 history。
|
|
19
|
+
- 对需要 model-specific prompt 的本地 model slug,直接在 vendored `pycodex/prompts/models.json` 补条目;当前 `step-3.5-flash` / `step-3.5-flash-2603` 已按这个方式接入。
|
|
20
|
+
- 交互 REPL 的 context 用量提示也应尽量贴近上游语义:展示“剩余 context 百分比”而不是原始 token 数;计算时按上游同款 `BASELINE_TOKENS=12000` 做归一化,并在模型元数据只有 `context_window` 时默认按 `95%` effective window 处理。只要当前模型能解析出 context window,初始 prompt 就先显示 `100%`,等首个 usage 回来后再刷新成真实值。
|
|
21
|
+
- 对交互 REPL 的 context 指示器,`model_context_window` 的取值优先级也要贴近上游:先吃 `config.toml` / profile 里的 `model_context_window` override,再回退到 vendored `models.json` 的 `context_window`;effective percent 继续沿用模型元数据,没有时默认 `95%`。
|
|
18
22
|
- `AgentLoop` 的 turn-loop 语义要跟上游 `codex-rs/core/src/codex.rs` 一致:按 follow-up / tool handoff 自然收敛,不要加固定 12 轮之类的 hard cap,也不要保留本地专用的 iteration-limit 参数。
|
|
19
23
|
- `README.md` 和 `docs/` 属于对齐工作的一部分:只要实现状态、对齐结论或使用方式发生实质变化,就应及时更新,不要让文档滞后于当前代码。
|
|
20
24
|
- 新工具必须继承 `BaseTool`,然后通过 `ToolRegistry.register(tool_instance)` 接入;不要再给 registry 传散装 name/description/handler 参数。
|
|
@@ -42,3 +46,5 @@
|
|
|
42
46
|
- `--put` 的 CLI UX 现在约定为“先打印打包文件清单和上传目标,再打印结果”,并且最后一行保留为可直接执行的 `pycodex --call SECRET-CALLID@<host:port>` 一键启动命令;后续如果再改输出,尽量保留这个末行语义。
|
|
43
47
|
- `--put` 现在不是“只上传就结束”:上传成功后还会立刻跑一次真实的 `--call` 下载/解包 round-trip 测试,只有这个测试也成功才算整个 `--put` 成功。排障时如果上传成功但 CLI 仍退出非零,要继续看 call 路径而不只看 put 端。
|
|
44
48
|
- `--put` 现在会先做 `/healthz` preflight,再开始扫描/压缩目录;如果用户报“卡死”,先看目标 `host:port` 是否真有 storage server 在监听。默认打包策略也已经切到白名单:只带 `config.toml`、`.env`、`AGENTS.md`、`AGENTS.override.md`、`skills/**`,以及 config 里相对引用的 `model_instructions_file`,所以像 `sessions/` 这类运行态目录不会再靠黑名单排除。
|
|
49
|
+
- 对接真实 `~/.codex/sessions/.../rollout-*.jsonl` 时,不要假设它一定是严格的一行一个 JSON object:本机样本可能包含 pretty-printed 多行对象,且文件尾部偶尔带未完成记录。恢复历史时用 concatenated-JSON 方式读取,并容忍尾部残缺。
|
|
50
|
+
- `pycodex` 本地 session 保存现在也按上游思路走:新 session 一开始就分配稳定的 uuidv7 thread/session id,并把历史增量追加到 `CODEX_HOME/sessions/.../rollout-*.jsonl`;`/resume` 列表应只展示至少有真实 user message 的 rollout,避免空白新 session 污染恢复列表。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-codex
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A minimal Python extraction of Codex's main agent loop
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.6.2
|
|
@@ -72,7 +72,7 @@ Intentionally not included yet:
|
|
|
72
72
|
|
|
73
73
|
- TUI / streaming incremental rendering
|
|
74
74
|
- MCP / connectors / sandbox / approvals
|
|
75
|
-
- memory / compact /
|
|
75
|
+
- memory / compact / review mode
|
|
76
76
|
- a full production OpenAI adapter surface
|
|
77
77
|
|
|
78
78
|
All of those can be layered on later. For now, the project is focused on
|
|
@@ -174,9 +174,24 @@ Current behavior:
|
|
|
174
174
|
- interactive mode shows a compact event stream for user-visible phases such as
|
|
175
175
|
tool execution and model follow-up after tool results
|
|
176
176
|
- assistant text is printed from streaming deltas directly
|
|
177
|
-
- interactive mode supports `/history`, `/title`, and `/
|
|
177
|
+
- interactive mode supports `/history`, `/title`, `/model`, `/resume`, and `/compact`
|
|
178
178
|
- `/model <name>` switches the model used by later turns in the current
|
|
179
179
|
interactive session; `/model` shows the current model and available choices
|
|
180
|
+
- `/resume` with no argument lists the currently resumable sessions by their
|
|
181
|
+
first user-message preview; `/resume 1` resumes the first listed session
|
|
182
|
+
- `/resume <number>` replaces the in-memory history with the selected recorded
|
|
183
|
+
Codex rollout from `CODEX_HOME/sessions`
|
|
184
|
+
- `/compact` synthesizes a local handoff summary, replaces the in-memory
|
|
185
|
+
conversation history with the compacted view, and appends a compacted-history
|
|
186
|
+
entry to the rollout so later `/resume` sees the same state
|
|
187
|
+
- new sessions are now recorded under `CODEX_HOME/sessions/.../rollout-*.jsonl`
|
|
188
|
+
with a stable session/thread id and per-item append+flush semantics so
|
|
189
|
+
`/resume` reads back the same rollout format
|
|
190
|
+
- if `TURN_HOOK.md` exists in the workspace root and is non-empty, each
|
|
191
|
+
completed turn also forks the just-finished history into a temporary,
|
|
192
|
+
non-persisted follow-up session and submits the file contents as the next
|
|
193
|
+
user instruction; this is intended for side-effect follow-ups such as
|
|
194
|
+
Feishu notifications
|
|
180
195
|
- steer is enabled by default in interactive mode: normal input goes into the
|
|
181
196
|
runtime steer path, the current request stops at the next safe boundary, and
|
|
182
197
|
later steer text is appended to the next model request's `input` in order;
|
|
@@ -51,7 +51,7 @@ Intentionally not included yet:
|
|
|
51
51
|
|
|
52
52
|
- TUI / streaming incremental rendering
|
|
53
53
|
- MCP / connectors / sandbox / approvals
|
|
54
|
-
- memory / compact /
|
|
54
|
+
- memory / compact / review mode
|
|
55
55
|
- a full production OpenAI adapter surface
|
|
56
56
|
|
|
57
57
|
All of those can be layered on later. For now, the project is focused on
|
|
@@ -153,9 +153,24 @@ Current behavior:
|
|
|
153
153
|
- interactive mode shows a compact event stream for user-visible phases such as
|
|
154
154
|
tool execution and model follow-up after tool results
|
|
155
155
|
- assistant text is printed from streaming deltas directly
|
|
156
|
-
- interactive mode supports `/history`, `/title`, and `/
|
|
156
|
+
- interactive mode supports `/history`, `/title`, `/model`, `/resume`, and `/compact`
|
|
157
157
|
- `/model <name>` switches the model used by later turns in the current
|
|
158
158
|
interactive session; `/model` shows the current model and available choices
|
|
159
|
+
- `/resume` with no argument lists the currently resumable sessions by their
|
|
160
|
+
first user-message preview; `/resume 1` resumes the first listed session
|
|
161
|
+
- `/resume <number>` replaces the in-memory history with the selected recorded
|
|
162
|
+
Codex rollout from `CODEX_HOME/sessions`
|
|
163
|
+
- `/compact` synthesizes a local handoff summary, replaces the in-memory
|
|
164
|
+
conversation history with the compacted view, and appends a compacted-history
|
|
165
|
+
entry to the rollout so later `/resume` sees the same state
|
|
166
|
+
- new sessions are now recorded under `CODEX_HOME/sessions/.../rollout-*.jsonl`
|
|
167
|
+
with a stable session/thread id and per-item append+flush semantics so
|
|
168
|
+
`/resume` reads back the same rollout format
|
|
169
|
+
- if `TURN_HOOK.md` exists in the workspace root and is non-empty, each
|
|
170
|
+
completed turn also forks the just-finished history into a temporary,
|
|
171
|
+
non-persisted follow-up session and submits the file contents as the next
|
|
172
|
+
user instruction; this is intended for side-effect follow-ups such as
|
|
173
|
+
Feishu notifications
|
|
159
174
|
- steer is enabled by default in interactive mode: normal input goes into the
|
|
160
175
|
runtime steer path, the current request stops at the next safe boundary, and
|
|
161
176
|
later steer text is appended to the next model request's `input` in order;
|
|
@@ -47,7 +47,7 @@ uv run pycodex
|
|
|
47
47
|
|
|
48
48
|
- TUI / 流式增量渲染
|
|
49
49
|
- MCP / connectors / sandbox / approvals
|
|
50
|
-
- memory / compact /
|
|
50
|
+
- memory / compact / review mode
|
|
51
51
|
- 真实 OpenAI 适配器
|
|
52
52
|
|
|
53
53
|
这些都可以后续继续往上叠,但当前项目先把最核心的“工具增强推理主循环”钉住。
|
|
@@ -129,8 +129,18 @@ pycodex doctor
|
|
|
129
129
|
- 交互模式下支持 `/exit` 和 `/quit`
|
|
130
130
|
- 交互模式下会显示简洁阶段事件流,例如工具执行状态和模型回看工具结果
|
|
131
131
|
- assistant 文本会按流式 delta 直接打印
|
|
132
|
-
- 交互模式下支持 `/history`、`/title` 和 `/
|
|
132
|
+
- 交互模式下支持 `/history`、`/title`、`/model` 和 `/resume`
|
|
133
133
|
- `/model <name>` 会切换当前交互会话后续请求使用的模型;`/model` 会显示当前模型和可选模型
|
|
134
|
+
- `/resume` 不带参数时会按首条用户消息预览列出当前可恢复的 session;`/resume 1`
|
|
135
|
+
会恢复列表里的第 1 个 session
|
|
136
|
+
- `/resume <数字>` 会从 `CODEX_HOME/sessions` 读取选中的已记录 Codex rollout,
|
|
137
|
+
并直接替换当前内存里的会话 history
|
|
138
|
+
- 新 session 现在会自动保存到 `CODEX_HOME/sessions/.../rollout-*.jsonl`,
|
|
139
|
+
使用稳定的 session/thread id,并按 item 级别 append + flush,和 `/resume`
|
|
140
|
+
读取的 rollout 格式保持一致
|
|
141
|
+
- 如果 workspace 根目录存在非空的 `TURN_HOOK.md`,每个已完成 turn 之后都会把
|
|
142
|
+
刚结束的 history fork 成一个不落盘的临时 follow-up 会话,并把文件内容作为下一条
|
|
143
|
+
user 指令提交;适合做 Feishu 通知这类副作用收尾动作
|
|
134
144
|
- 交互模式默认支持 steer:普通输入会走 runtime 的 steer 路径,当前请求会在下一个安全边界尽快停下,后续 steer 文本会按顺序并入下一次模型请求的 `input`;如需明确排队可用 `/queue <message>`,会打印 `[steer] queued: ...`,随后等该 turn 真正开始时再打印 `[steer] inserted: ...`
|
|
135
145
|
- 当前默认注册一组与原版 Codex 一一对应的本地工具子集:`shell`、`shell_command`、`exec_command`、`write_stdin`、`exec`、`wait`、`web_search`、`update_plan`、`request_user_input`、`request_permissions`、`spawn_agent`、`send_input`、`resume_agent`、`wait_agent`、`close_agent`、`apply_patch`、`grep_files`、`read_file`、`list_dir`、`view_image`
|
|
136
146
|
- `--vllm-endpoint http://host:port` 会自动拉起一个本地 `responses_server` compat 层;当 path 为空时会内部补 `/v1`,继续把 `/responses` 请求转到下游 `/v1/chat/completions`。当前对 `model_provider = "vllm"` 已补上 reasoning 兼容:会把 chat chunk 里的 `reasoning` / `reasoning_content` 翻回 Responses `reasoning` item,并把历史里的 `reasoning` item 回放成下游 assistant message 的 `reasoning` 字段;同时会向 vLLM 请求 streaming usage,并在最终 `response.completed.response.usage` 中回传
|
|
@@ -549,3 +549,29 @@ Those are the next alignment target after the prompt/context pass.
|
|
|
549
549
|
- 因此,下一次请求体里的 `input` 现在可以把多个 steer 文本按顺序并到 history 尾部,并且继续沿用同一个 `turn_id`;这一点已经明显比旧版 `cancel + 单条新 turn` 更接近 upstream
|
|
550
550
|
- 通过 `tests/compare_steer_request_bodies.py` 的 fake/proxy capture 对比,当前 steer 首轮/次轮 request body 在忽略 `prompt_cache_key` 后已与本机 installed `codex-cli 0.115.0` 对齐;这里比较的是“默认 steer”路径,因此脚本会先去掉本机用户配置里的顶层 `service_tier`,避免把本地 fast-mode 设置误记成 steer 差异。同一 steer turn 的 follow-up request 仍需继续带 `workspaces`
|
|
551
551
|
- 仍未完全一致的点主要是内部控制流:本地实现仍是在 runtime 层结束一次 `run_turn(...)` 再启动下一次;upstream 则更倾向于在同一个 active turn 里继续 follow-up
|
|
552
|
+
|
|
553
|
+
## timeout / interrupt 对齐现状
|
|
554
|
+
|
|
555
|
+
- `pycodex` 现在已经补上最小的 provider 级 stream retry:`ResponsesProviderConfig`
|
|
556
|
+
支持 `stream_max_retries` / `stream_idle_timeout_ms`,默认值对齐 upstream 的
|
|
557
|
+
`5` 次重试和 `300_000 ms` SSE idle timeout;代码在 `pycodex/model.py`
|
|
558
|
+
- 当前实现会把 `response.failed`、stream 在 `response.completed` 前断开、以及
|
|
559
|
+
`requests` 侧的读流异常统一视为 retryable stream error,并在
|
|
560
|
+
`ResponsesModelClient.complete(...)` 里按 backoff 重试;重试前会向外发
|
|
561
|
+
`ModelStreamEvent(kind="stream_error")`,CLI 会显示 `[status] Reconnecting...`
|
|
562
|
+
- 这一点已经明显更接近 upstream 在 `run_sampling_request(...)` 里对
|
|
563
|
+
`CodexErr::Stream(...)` / `CodexErr::Timeout` 的 backoff retry + `StreamError`
|
|
564
|
+
前端通知语义;对应参考仍在 `core/src/model_provider_info.rs`、
|
|
565
|
+
`codex-api/src/sse/responses.rs`、`core/src/codex.rs`
|
|
566
|
+
- 还没对齐的点主要有两类:
|
|
567
|
+
- retry 目前是在 Python `ResponsesModelClient` 内部完成,不像 upstream 那样放在更外层的
|
|
568
|
+
sampling loop,并复用统一的 `CodexErr::is_retryable()` 分类
|
|
569
|
+
- 还没有 upstream 的 WebSocket transport / HTTP fallback,因此也没有
|
|
570
|
+
“超过 WS retry 预算后切到 HTTP” 的那层行为
|
|
571
|
+
- 中断语义也还不一样:upstream 普通“steer/user input”优先走
|
|
572
|
+
`inject_input(...)` -> `pending_input`,只有显式 interrupt 才会真正取消当前 task;
|
|
573
|
+
取消时会通过 `CancellationToken` 中止活跃请求/工具,终止 unified-exec 进程,并把
|
|
574
|
+
`<turn_aborted>` marker 持久化到 history
|
|
575
|
+
- `pycodex` 当前 steer 仍主要靠 `AgentLoop.interrupt_asap` 在 loop 边界抛
|
|
576
|
+
`TurnInterrupted`;它不会主动打断正在阻塞的模型流读取或正在运行的 tool,也不会写
|
|
577
|
+
`<turn_aborted>` marker,因此 interrupt 语义仍明显弱于 upstream
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
- vLLM chat-completions `reasoning` / `reasoning_content` -> Responses `reasoning` item 适配
|
|
26
26
|
- vLLM 历史 `reasoning` item -> assistant message `reasoning` 字段回放
|
|
27
27
|
- vLLM streaming `usage` -> final `response.completed.response.usage`
|
|
28
|
+
- 下游 chat stream 如果半路断开,会转成上游可解析的 `response.failed` 事件,而不是直接截断 HTTP body
|
|
28
29
|
- 普通 function tools
|
|
29
30
|
- custom tools 的 function-wrapper 兼容适配
|
|
30
31
|
- mock `web_search` 接口对齐(返回空结果)
|
|
@@ -20,6 +20,9 @@ from .tools import ToolContext, ToolRegistry
|
|
|
20
20
|
from .utils import uuid7_string
|
|
21
21
|
import typing
|
|
22
22
|
|
|
23
|
+
if typing.TYPE_CHECKING:
|
|
24
|
+
from .utils.session_persist import SessionRolloutRecorder
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
EventHandler = Callable[[AgentEvent], None]
|
|
25
28
|
NOOP_EVENT_HANDLER: 'EventHandler' = lambda _event: None
|
|
@@ -46,6 +49,7 @@ class AgentLoop:
|
|
|
46
49
|
parallel_tool_calls: 'bool' = True,
|
|
47
50
|
event_handler: 'EventHandler' = NOOP_EVENT_HANDLER,
|
|
48
51
|
initial_history: 'typing.Tuple[ConversationItem, ...]' = (),
|
|
52
|
+
rollout_recorder: 'typing.Union[SessionRolloutRecorder, None]' = None,
|
|
49
53
|
) -> 'None':
|
|
50
54
|
self._model_client = model_client
|
|
51
55
|
self._tool_registry = tool_registry
|
|
@@ -53,6 +57,7 @@ class AgentLoop:
|
|
|
53
57
|
self._parallel_tool_calls = parallel_tool_calls
|
|
54
58
|
self._event_handler = event_handler
|
|
55
59
|
self._history: 'typing.List[ConversationItem]' = list(initial_history)
|
|
60
|
+
self._rollout_recorder = rollout_recorder
|
|
56
61
|
self.interrupt_asap = False
|
|
57
62
|
|
|
58
63
|
@property
|
|
@@ -64,6 +69,18 @@ class AgentLoop:
|
|
|
64
69
|
) -> 'None':
|
|
65
70
|
self._event_handler = event_handler
|
|
66
71
|
|
|
72
|
+
def replace_history(
|
|
73
|
+
self,
|
|
74
|
+
history: 'typing.Iterable[ConversationItem]',
|
|
75
|
+
) -> 'None':
|
|
76
|
+
self._history = list(history)
|
|
77
|
+
|
|
78
|
+
def set_rollout_recorder(
|
|
79
|
+
self,
|
|
80
|
+
rollout_recorder: 'typing.Union[SessionRolloutRecorder, None]',
|
|
81
|
+
) -> 'None':
|
|
82
|
+
self._rollout_recorder = rollout_recorder
|
|
83
|
+
|
|
67
84
|
def _raise_if_interrupt_requested(
|
|
68
85
|
self,
|
|
69
86
|
turn_id: 'str',
|
|
@@ -83,8 +100,9 @@ class AgentLoop:
|
|
|
83
100
|
) -> 'TurnResult':
|
|
84
101
|
turn_id = turn_id or uuid7_string()
|
|
85
102
|
self.interrupt_asap = False
|
|
86
|
-
for text in texts
|
|
87
|
-
|
|
103
|
+
new_user_messages = [UserMessage(text=text) for text in texts]
|
|
104
|
+
self._history.extend(new_user_messages)
|
|
105
|
+
self._persist_history_items(new_user_messages)
|
|
88
106
|
|
|
89
107
|
self._emit(
|
|
90
108
|
"turn_started",
|
|
@@ -131,12 +149,15 @@ class AgentLoop:
|
|
|
131
149
|
)
|
|
132
150
|
|
|
133
151
|
tool_calls: 'typing.List[ToolCall]' = []
|
|
152
|
+
persisted_response_items: 'typing.List[ConversationItem]' = []
|
|
134
153
|
for item in response.items:
|
|
135
154
|
self._history.append(item)
|
|
155
|
+
persisted_response_items.append(item)
|
|
136
156
|
if isinstance(item, AssistantMessage):
|
|
137
157
|
last_assistant_message = item.text
|
|
138
158
|
elif isinstance(item, ToolCall):
|
|
139
159
|
tool_calls.append(item)
|
|
160
|
+
self._persist_history_items(persisted_response_items)
|
|
140
161
|
|
|
141
162
|
if not tool_calls:
|
|
142
163
|
self._raise_if_interrupt_requested(
|
|
@@ -160,7 +181,10 @@ class AgentLoop:
|
|
|
160
181
|
|
|
161
182
|
tool_results = await self._execute_tool_batch(turn_id, tool_calls)
|
|
162
183
|
self._history.extend(tool_results)
|
|
163
|
-
self.
|
|
184
|
+
self._persist_history_items(tool_results)
|
|
185
|
+
follow_up_messages = self._build_follow_up_messages(tool_results)
|
|
186
|
+
self._history.extend(follow_up_messages)
|
|
187
|
+
self._persist_history_items(follow_up_messages)
|
|
164
188
|
self._raise_if_interrupt_requested(
|
|
165
189
|
turn_id,
|
|
166
190
|
iteration,
|
|
@@ -226,7 +250,12 @@ class AgentLoop:
|
|
|
226
250
|
call: 'ToolCall',
|
|
227
251
|
prior_results: 'typing.Tuple[ToolResult, ...]' = (),
|
|
228
252
|
) -> 'ToolResult':
|
|
229
|
-
|
|
253
|
+
payload: 'typing.Dict[str, object]' = {
|
|
254
|
+
"tool_name": call.name,
|
|
255
|
+
"call_id": call.call_id,
|
|
256
|
+
"call": call,
|
|
257
|
+
}
|
|
258
|
+
self._emit("tool_started", turn_id, **payload)
|
|
230
259
|
result = await self._tool_registry.execute(
|
|
231
260
|
call,
|
|
232
261
|
ToolContext(
|
|
@@ -235,13 +264,8 @@ class AgentLoop:
|
|
|
235
264
|
collaboration_mode=self._context_manager.collaboration_mode,
|
|
236
265
|
),
|
|
237
266
|
)
|
|
238
|
-
payload
|
|
239
|
-
|
|
240
|
-
"call_id": call.call_id,
|
|
241
|
-
"is_error": result.is_error,
|
|
242
|
-
"call": call,
|
|
243
|
-
"result": result,
|
|
244
|
-
}
|
|
267
|
+
payload["result"] = result
|
|
268
|
+
payload["is_error"] = result.is_error
|
|
245
269
|
self._emit("tool_completed", turn_id, **payload)
|
|
246
270
|
return result
|
|
247
271
|
|
|
@@ -250,11 +274,27 @@ class AgentLoop:
|
|
|
250
274
|
AgentEvent(kind=kind, turn_id=turn_id, payload=dict(payload))
|
|
251
275
|
)
|
|
252
276
|
|
|
277
|
+
def _persist_history_items(
|
|
278
|
+
self,
|
|
279
|
+
items: 'typing.Iterable[ConversationItem]',
|
|
280
|
+
) -> 'None':
|
|
281
|
+
recorder = self._rollout_recorder
|
|
282
|
+
if recorder is None:
|
|
283
|
+
return
|
|
284
|
+
try:
|
|
285
|
+
recorder.append_history_items(items)
|
|
286
|
+
except Exception: # pragma: no cover - persistence should not break turns
|
|
287
|
+
return
|
|
288
|
+
|
|
253
289
|
def _handle_model_stream_event(self, turn_id: 'str', event: 'ModelStreamEvent') -> 'None':
|
|
254
290
|
if event.kind == "assistant_delta":
|
|
255
291
|
self._emit("assistant_delta", turn_id, **event.payload)
|
|
256
292
|
elif event.kind == "tool_call":
|
|
257
293
|
self._emit("tool_called", turn_id, **event.payload)
|
|
294
|
+
elif event.kind == "token_count":
|
|
295
|
+
self._emit("token_count", turn_id, **event.payload)
|
|
296
|
+
elif event.kind == "stream_error":
|
|
297
|
+
self._emit("stream_error", turn_id, **event.payload)
|
|
258
298
|
|
|
259
299
|
def _build_follow_up_messages(
|
|
260
300
|
self,
|
|
@@ -20,7 +20,15 @@ from .portable import bootstrap_called_home, upload_codex_home
|
|
|
20
20
|
from .protocol import AgentEvent
|
|
21
21
|
from .runtime import AgentRuntime
|
|
22
22
|
from .runtime_services import RuntimeEnvironment, create_runtime_environment
|
|
23
|
-
from .utils import CliSessionView, load_codex_dotenv
|
|
23
|
+
from .utils import CliSessionView, load_codex_dotenv, uuid7_string
|
|
24
|
+
from .utils.compactor import compact_agent_loop
|
|
25
|
+
from .utils.session_persist import (
|
|
26
|
+
SessionRolloutRecorder,
|
|
27
|
+
conversation_history_to_turns,
|
|
28
|
+
list_resumable_sessions,
|
|
29
|
+
load_resumed_session,
|
|
30
|
+
resolve_codex_home,
|
|
31
|
+
)
|
|
24
32
|
import typing
|
|
25
33
|
|
|
26
34
|
EXIT_COMMANDS = {"/exit", "/quit"}
|
|
@@ -28,6 +36,8 @@ HISTORY_COMMAND = "/history"
|
|
|
28
36
|
TITLE_COMMAND = "/title"
|
|
29
37
|
MODEL_COMMAND = "/model"
|
|
30
38
|
QUEUE_COMMAND = "/queue"
|
|
39
|
+
RESUME_COMMAND = "/resume"
|
|
40
|
+
COMPACT_COMMAND = "/compact"
|
|
31
41
|
CliSessionMode = Literal["exec", "tui"]
|
|
32
42
|
LOCAL_RESPONSES_SERVER_API_KEY_ENV = "PYCODEX_LOCAL_RESPONSES_SERVER_KEY"
|
|
33
43
|
CLI_ORIGINATOR = "codex-tui"
|
|
@@ -284,6 +294,9 @@ def build_runtime(
|
|
|
284
294
|
collaboration_mode=collaboration_mode,
|
|
285
295
|
include_collaboration_instructions=use_tui_context,
|
|
286
296
|
)
|
|
297
|
+
session_id = getattr(client, "_session_id", None) or uuid7_string()
|
|
298
|
+
if hasattr(client, "_session_id"):
|
|
299
|
+
client._session_id = session_id
|
|
287
300
|
subagent_context_manager = ContextManager.from_codex_config(
|
|
288
301
|
config_path,
|
|
289
302
|
profile,
|
|
@@ -293,6 +306,14 @@ def build_runtime(
|
|
|
293
306
|
runtime_environment = create_runtime_environment()
|
|
294
307
|
runtime_environment.request_user_input_manager.set_handler(None)
|
|
295
308
|
runtime_environment.request_permissions_manager.set_handler(None)
|
|
309
|
+
rollout_recorder = SessionRolloutRecorder.create(
|
|
310
|
+
resolve_codex_home(config_path),
|
|
311
|
+
session_id,
|
|
312
|
+
context_manager.cwd,
|
|
313
|
+
getattr(client, "_originator", CLI_ORIGINATOR),
|
|
314
|
+
getattr(getattr(client, "_config", None), "provider_name", None),
|
|
315
|
+
context_manager.resolve_base_instructions(),
|
|
316
|
+
)
|
|
296
317
|
|
|
297
318
|
def make_subagent_runtime_builder(base_client):
|
|
298
319
|
def build_subagent_runtime(
|
|
@@ -330,7 +351,10 @@ def build_runtime(
|
|
|
330
351
|
)
|
|
331
352
|
return AgentRuntime(
|
|
332
353
|
AgentLoop(
|
|
333
|
-
client,
|
|
354
|
+
client,
|
|
355
|
+
get_tools(runtime_environment, exec_mode=True),
|
|
356
|
+
context_manager,
|
|
357
|
+
rollout_recorder=rollout_recorder,
|
|
334
358
|
),
|
|
335
359
|
runtime_environment=runtime_environment,
|
|
336
360
|
)
|
|
@@ -498,10 +522,14 @@ async def prompt_request_permissions(
|
|
|
498
522
|
async def run_interactive_session(
|
|
499
523
|
runtime: 'AgentRuntime',
|
|
500
524
|
json_mode: 'bool',
|
|
525
|
+
config_path: 'typing.Union[str, None]' = None,
|
|
501
526
|
) -> 'int':
|
|
502
527
|
worker = asyncio.create_task(runtime.run_forever())
|
|
528
|
+
context_window_tokens = runtime._agent_loop._context_manager.resolve_model_context_window()
|
|
503
529
|
view = CliSessionView()
|
|
530
|
+
view.set_context_window_tokens(context_window_tokens)
|
|
504
531
|
model_client = runtime._agent_loop._model_client
|
|
532
|
+
codex_home = resolve_codex_home(config_path)
|
|
505
533
|
runtime.set_event_handler(view.handle_event)
|
|
506
534
|
pending_turn_tasks: 'typing.Set[asyncio.Task[None]]' = set()
|
|
507
535
|
runtime_environment = runtime.runtime_environment
|
|
@@ -515,7 +543,7 @@ async def run_interactive_session(
|
|
|
515
543
|
lambda payload: prompt_request_permissions(view, payload)
|
|
516
544
|
)
|
|
517
545
|
view.write_line("pycodex interactive mode. Type /exit to quit.")
|
|
518
|
-
view.write_line("Extra commands: /history, /title, /model")
|
|
546
|
+
view.write_line("Extra commands: /history, /title, /model, /resume, /compact")
|
|
519
547
|
try:
|
|
520
548
|
|
|
521
549
|
def has_pending_turn_tasks() -> 'bool':
|
|
@@ -524,6 +552,39 @@ async def run_interactive_session(
|
|
|
524
552
|
)
|
|
525
553
|
return bool(pending_turn_tasks)
|
|
526
554
|
|
|
555
|
+
async def run_manual_compact() -> 'None':
|
|
556
|
+
agent_loop = runtime._agent_loop
|
|
557
|
+
if not agent_loop.history:
|
|
558
|
+
view.write_line("Nothing to compact.")
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
compact_turn_id = uuid7_string()
|
|
562
|
+
|
|
563
|
+
def handle_compact_stream_event(event) -> 'None':
|
|
564
|
+
if event.kind not in {"token_count", "stream_error"}:
|
|
565
|
+
return
|
|
566
|
+
view.handle_event(
|
|
567
|
+
AgentEvent(
|
|
568
|
+
kind=event.kind,
|
|
569
|
+
turn_id=compact_turn_id,
|
|
570
|
+
payload=dict(event.payload),
|
|
571
|
+
)
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
view.write_line("Compacting conversation history...")
|
|
575
|
+
compact_result = await compact_agent_loop(
|
|
576
|
+
agent_loop,
|
|
577
|
+
handle_compact_stream_event,
|
|
578
|
+
)
|
|
579
|
+
if compact_result is None:
|
|
580
|
+
view.write_line("Nothing to compact.")
|
|
581
|
+
return
|
|
582
|
+
view.load_session_history(
|
|
583
|
+
getattr(view, "_title", None),
|
|
584
|
+
conversation_history_to_turns(compact_result.history),
|
|
585
|
+
)
|
|
586
|
+
view.write_line(compact_result.display_text())
|
|
587
|
+
|
|
527
588
|
async def wait_for_turn_result(future) -> 'None':
|
|
528
589
|
try:
|
|
529
590
|
result = await future
|
|
@@ -556,6 +617,50 @@ async def run_interactive_session(
|
|
|
556
617
|
if prompt_text == TITLE_COMMAND:
|
|
557
618
|
view.show_title()
|
|
558
619
|
continue
|
|
620
|
+
if prompt_text == RESUME_COMMAND:
|
|
621
|
+
sessions = list_resumable_sessions(codex_home)
|
|
622
|
+
if not sessions:
|
|
623
|
+
view.write_line("No resumable sessions found.")
|
|
624
|
+
continue
|
|
625
|
+
view.write_line("Available sessions:")
|
|
626
|
+
for index, session in enumerate(sessions, start=1):
|
|
627
|
+
view.write_line(f"[{index}] {session['preview']}")
|
|
628
|
+
continue
|
|
629
|
+
if prompt_text.startswith(f"{RESUME_COMMAND} "):
|
|
630
|
+
if has_pending_turn_tasks():
|
|
631
|
+
view.write_line(
|
|
632
|
+
"Cannot resume while work is running or queued."
|
|
633
|
+
)
|
|
634
|
+
continue
|
|
635
|
+
resume_target = prompt_text[len(RESUME_COMMAND) :].strip()
|
|
636
|
+
try:
|
|
637
|
+
resumed = load_resumed_session(codex_home, resume_target)
|
|
638
|
+
runtime._agent_loop.replace_history(resumed["history"])
|
|
639
|
+
if hasattr(model_client, "_session_id"):
|
|
640
|
+
model_client._session_id = str(resumed["session_id"])
|
|
641
|
+
runtime._agent_loop.set_rollout_recorder(
|
|
642
|
+
SessionRolloutRecorder.resume(resumed["rollout_path"])
|
|
643
|
+
)
|
|
644
|
+
view.load_session_history(
|
|
645
|
+
str(resumed["title"]),
|
|
646
|
+
tuple(resumed["turns"]),
|
|
647
|
+
)
|
|
648
|
+
view.write_line(f"Resumed session: {resumed['title']}")
|
|
649
|
+
view.show_history()
|
|
650
|
+
except Exception as exc: # pragma: no cover - defensive surface
|
|
651
|
+
view.show_error(str(exc))
|
|
652
|
+
continue
|
|
653
|
+
if prompt_text == COMPACT_COMMAND:
|
|
654
|
+
if has_pending_turn_tasks():
|
|
655
|
+
view.write_line(
|
|
656
|
+
"Cannot compact while work is running or queued."
|
|
657
|
+
)
|
|
658
|
+
continue
|
|
659
|
+
try:
|
|
660
|
+
await run_manual_compact()
|
|
661
|
+
except Exception as exc: # pragma: no cover - defensive surface
|
|
662
|
+
view.show_error(str(exc))
|
|
663
|
+
continue
|
|
559
664
|
if prompt_text.startswith(f"{QUEUE_COMMAND} "):
|
|
560
665
|
queued_text = prompt_text[len(QUEUE_COMMAND) :].strip()
|
|
561
666
|
if not queued_text:
|
|
@@ -663,6 +768,7 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
|
|
|
663
768
|
return await run_interactive_session(
|
|
664
769
|
runtime,
|
|
665
770
|
args.json,
|
|
771
|
+
args.config,
|
|
666
772
|
)
|
|
667
773
|
else:
|
|
668
774
|
prompt_text = resolve_prompt_text(args.prompt)
|
|
@@ -30,6 +30,7 @@ DEFAULT_COLLABORATION_INSTRUCTIONS_PATH = (
|
|
|
30
30
|
PLAN_COLLABORATION_INSTRUCTIONS_PATH = (
|
|
31
31
|
Path(__file__).resolve().parent / "prompts" / "collaboration_plan.md"
|
|
32
32
|
)
|
|
33
|
+
DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT = 95
|
|
33
34
|
PERMISSIONS_SANDBOX_PROMPTS_PATH = (
|
|
34
35
|
Path(__file__).resolve().parent / "prompts" / "permissions" / "sandbox_mode"
|
|
35
36
|
)
|
|
@@ -76,6 +77,7 @@ class ContextConfig:
|
|
|
76
77
|
codex_home: 'typing.Union[Path, None]' = None
|
|
77
78
|
project_doc_max_bytes: 'typing.Union[int, None]' = None
|
|
78
79
|
model: 'typing.Union[str, None]' = None
|
|
80
|
+
model_context_window: 'typing.Union[int, None]' = None
|
|
79
81
|
personality: 'typing.Union[str, None]' = None
|
|
80
82
|
approval_policy: 'typing.Union[str, None]' = None
|
|
81
83
|
sandbox_mode: 'typing.Union[str, None]' = None
|
|
@@ -117,6 +119,7 @@ class ContextConfig:
|
|
|
117
119
|
codex_home=codex_home,
|
|
118
120
|
project_doc_max_bytes=_normalize_int(selected.get("project_doc_max_bytes")),
|
|
119
121
|
model=_normalize_text(selected.get("model")),
|
|
122
|
+
model_context_window=_normalize_int(selected.get("model_context_window")),
|
|
120
123
|
personality=_normalize_text(selected.get("personality")),
|
|
121
124
|
approval_policy=_normalize_text(selected.get("approval_policy")),
|
|
122
125
|
sandbox_mode=_normalize_text(selected.get("sandbox_mode")),
|
|
@@ -240,6 +243,26 @@ class ContextManager:
|
|
|
240
243
|
return resolved
|
|
241
244
|
return self._default_base_instructions
|
|
242
245
|
|
|
246
|
+
def resolve_model_context_window(self) -> 'typing.Union[int, None]':
|
|
247
|
+
model_metadata = None
|
|
248
|
+
model_slug = self._config.model
|
|
249
|
+
if model_slug is not None:
|
|
250
|
+
model_metadata = _load_models_by_slug().get(model_slug)
|
|
251
|
+
|
|
252
|
+
context_window = self._config.model_context_window
|
|
253
|
+
if context_window is None and model_metadata is not None:
|
|
254
|
+
context_window = _normalize_int(model_metadata.get("context_window"))
|
|
255
|
+
if context_window is None:
|
|
256
|
+
return None
|
|
257
|
+
effective_percent = None
|
|
258
|
+
if model_metadata is not None:
|
|
259
|
+
effective_percent = _normalize_int(
|
|
260
|
+
model_metadata.get("effective_context_window_percent")
|
|
261
|
+
)
|
|
262
|
+
if effective_percent is None:
|
|
263
|
+
effective_percent = DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT
|
|
264
|
+
return context_window * max(effective_percent, 0) // 100
|
|
265
|
+
|
|
243
266
|
def _resolve_model_instructions(self) -> 'typing.Union[str, None]':
|
|
244
267
|
model_slug = self._config.model
|
|
245
268
|
if model_slug is None:
|