- 这个仓库的目标是尽可能准确地复现上游 Codex(
https://github.com/openai/codex),不是发明一个“类似 Codex”的简化替代品;凡是行为差异都应优先朝原版 Codex 收敛。 - PyPI 主 distribution name 用
python-codex;代码包目录、import路径和 CLI 命令仍保持pycodex。 pycodex-ws是与python-codex锁步同版本的薄 metapackage,只依赖精确同版本的python-codex;不要在两个 wheel 中重复打包pycodex/、workspace_server/或重复声明pycodex-wsconsole script。- 双包发布时先发布
python-codex,成功后再发布pycodex-ws,避免新 metapackage 短暂指向尚不存在的核心版本。 - 包结构直接放在 repo 根目录下的
pycodex/,不使用src/layout。 - Rust 到 Python 的主映射:
submission_loop->pycodex/runtime.py,run_turn/run_sampling_request->pycodex/agent.py,ToolRouter->pycodex/tools/base_tool.py+pycodex/tools/. - 会话协议先收敛到 4 类 item:
UserMessage、AssistantMessage、ToolCall、ToolResult;只有这层稳定后再扩 richer event model。 - 优先保持主循环可测试、可替换:模型侧通过
ModelClient协议接入;测试专用的ScriptedModelClient放在tests/fakes.py,不要放进运行时包。 ResponsesModelClient直接复用~/.codex/config.toml的 provider 配置;当前已验证这里的 responses provider 需要stream = true,否则会返回400和Stream must be set to true。- 现在
ResponsesModelClient默认会对流式断连做 provider 级自动重试(stream_max_retries默认 5);写 CLI/REPL 测试时如果断言“先向用户报错,再靠下一句go on继续”,必须在测试 provider 配置里显式设stream_max_retries = 0,否则测试可能一直等不到预期错误而卡住。 responses_servercompat 层应透传请求里的model;不要再做 “取 downstream /models 第一个 id 并强制覆盖请求模型” 这种兜底兼容。- 对
model_provider = "vllm",responses_server仍然走/v1/chat/completionscompat 路径,但要保留 reasoning:把 chat chunk 里的reasoning/reasoning_content翻回 Responsesreasoningitem,并把历史里的 Responsesreasoningitem 回放成下游 assistant message 的reasoning字段。 responses_server不能把 terminal reasoning-only chat output 当成成功回复:如果 downstream 一轮结束时只返回reasoning/reasoning_content,没有 assistantcontent且没有 tool call,先丢弃本次 partial reasoning 并用原样 downstream request 静默重试一次;若仍然 reasoning-only,再发response.failed(type=model_output_invalid),避免把 partial reasoning 写进 rollout 后在下一轮变成 chat 后端拒绝的裸 assistant message。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 改写污染。responses_server如果要兼容下游/v1/messages,也优先保持这条边界:内部继续用 canonical chat request / chat-like chunk 流,只有真正发请求和读取 SSE 时才做 messages 适配,这样 tool hydration、mockweb_searchfollow-up、provider payload post-process 都能复用。- 真实 vLLM
0.19.0的/v1/messages会对缺失max_tokens直接返回400;messages 适配层必须总是补这个字段。当前约定是优先透传请求里的max_output_tokens/max_tokens,否则回退到默认32000。 - 对 vLLM chat-completions 打开
return_token_ids=true时,streamingprompt_token_ids只出现在首个 chunk,后续每个 chunk 的choices[*].token_ids都是 decode delta;要在responses_server侧导出 trajectory 时,按“首个prompt_token_ids+ 按序拼接所有 chunk 的token_ids”重建即可。 pycodex默认是最小交互 CLI;无 prompt 时进入 REPL,并通过AgentRuntime跑外层提交循环。当前会显示最小事件流、assistant 流式输出、简单 title/history(/title,/history),并默认注册一组与原版一一对应的本地工具子集。- Web workspace lives in the standalone
workspace_server/package and is launched withpycodex-ws --listen <host:port> --board <html>, not throughpycodexCLI dispatch. CLI and web sharepycodex.interactive_session.run_interactive_session; slash-command semantics such as/resume,/compact,/model, and/linkbelong to that shared interactive shell loop, while workspace only supplies a web view/input adapter and tab/session lifecycle. - Relative images in board HTML resolve to HTTP siblings such as
/w/<workspace-id>/<image>(or/<image>in single-workspace mode), not to/board/.... Keep the image fallback routes after concrete workspace/API routes, resolve files from the board's directory, and reject non-image/*files or paths that escape that directory. - 交互 CLI 的事件流展示优先表达用户可感知的阶段(例如工具开始/完成、模型回看工具结果),不要直接把内部
iteration计数暴露成主要状态文案;iterations应继续保留在TurnResult等程序化结果里。 - 在交互 CLI 里,
stream_error表示当前 Responses stream attempt 失败且模型客户端可能马上自动重试;不要在这个事件上finish_stream()输出当前 assistant delta buffer,否则第一次失败 attempt 的文本和重试成功后的最终回复会重复显示。真正 fatal 的失败仍由turn_failed走通用 flush,保留 partial 输出。 - prompt/context 相关逻辑统一放在
pycodex/context.py:AgentLoop只维护真实会话历史;每轮请求前由ContextManager注入 base instructions、developer message、AGENTS.md指令和<environment_context>,且这些注入项不写回 history。 - 对需要 model-specific prompt 的本地 model slug,直接在 vendored
pycodex/prompts/models.json补条目;当前step-3.5-flash/step-3.5-flash-2603/step-3.6已按这个方式接入。 - 交互 REPL 的 context 用量提示也应尽量贴近上游语义:展示“剩余 context 百分比”而不是原始 token 数;计算时按上游同款
BASELINE_TOKENS=12000做归一化,并在模型元数据只有context_window时默认按95%effective window 处理。只要当前模型能解析出 context window,初始 prompt 就先显示100%,等首个 usage 回来后再刷新成真实值。 - 对交互 REPL 的 context 指示器,
model_context_window的取值优先级也要贴近上游:先吃config.toml/ profile 里的model_context_windowoverride,再回退到 vendoredmodels.json的context_window;effective percent 继续沿用模型元数据,没有时默认95%。 pyco(<percent>)正常只来自模型流里最近一次response.completed.response.usage.total_tokens;如果大 tool output 之后的下一次请求被下游context_length_exceeded拒绝,rollout 不会单独记录 usage。遇到这类错误时应从错误文案的requested ... tokens (... in the messages, ... in the completion)提取真实请求 token,作为失败请求的token_count事件回灌,并立即触发 compact 后重试一次。若服务端只返回Your input exceeds the context window...这类无 token 数的response.failed,仍应触发 compact+retry,只是不要伪造token_count。若 compact 请求本身也超长,先循环删除最旧的ToolResult及其配对ToolCall再重试 compact。AgentLoop的 turn-loop 语义要跟上游codex-rs/core/src/codex.rs一致:按 follow-up / tool handoff 自然收敛,不要加固定 12 轮之类的 hard cap,也不要保留本地专用的 iteration-limit 参数。README.md和docs/属于对齐工作的一部分:只要实现状态、对齐结论或使用方式发生实质变化,就应及时更新,不要让文档滞后于当前代码。- 新工具必须继承
BaseTool,然后通过ToolRegistry.register(tool_instance)接入;不要再给 registry 传散装 name/description/handler 参数。 - request/tool schema 对齐应落在实际建模层(
ToolSpec/BaseTool/ request builder)本身,不要再引入 prompt 级别的serialized_tools旁路覆盖。 - 当前已接入的内置工具子集:
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。后续继续新增时,仍然按原版 Codex 内置工具逐个补齐。 exec_command/write_stdin的 unified-exec 对齐要注意两层默认截断语义:省略max_output_tokens时也要按 upstream 默认10_000token 预算裁剪 tool response;同时长时间未轮询的未读输出缓冲不能无限累积,需保留 upstream 同款1 MiBhead/tail。- 当前协议层已经支持两类原版工具载荷:普通 function tools,以及
apply_patch这类 freeform/custom tools;工具结果也支持结构化input_imagecontent items,用于view_image这类会把图片喂回模型的工具。 - 当前本地 runtime 已支持一个最小的 in-process sub-agent 管理层:
spawn_agent/send_input/resume_agent/wait_agent/close_agent通过共享的SubAgentManager驱动新的AgentRuntime实例,不依赖 CLI harness 自带的多 agent 基础设施。 - 当前交互工具
request_user_input/request_permissions已接到pycodex交互 CLI:它们会在一次 turn 内直接复用同一个 stdin/input_fn 向用户提问,因此只有交互模式下有完整体验;非交互调用默认会返回取消/不可用类错误。 request_user_input对齐上游时要注意两层:Default mode 默认返回固定错误request_user_input is unavailable in Default mode;Plan mode happy path 则要求每个问题都有非空options、handler 会补isOther=true,并把结构化答案序列化成 JSON 字符串加success=true塞回function_call_output。- 在
/data/pycodex本机已安装的codex-cli 0.115.0上,用tests/compare_request_user_input_roundtrip.py做 Plan-mode deterministic proxy live capture 时,upstream 的 second requestfunction_call_output当前不带success;而 GitHubopenai/codexmain源码里的FunctionCallOutputPayload/request_user_inputhandler 支持传递success。写对齐结论时要明确区分“installed CLI live capture”与“upstream main 源码建模”。 - 当前
exec/wait已有一个最小 code-mode 实现:底层通过 Node 子进程运行 JavaScript,自带text/image/store/load/exit/notify/yield_controlhelper,并允许通过tools.<name>(...)调回本地已注册工具;web_search当前作为 Responses API provider-native tool declaration 暴露给模型,不经过本地 ToolRegistry 执行。 - 我们支持的 tools 必须和原版 Codex 内置 tools 一一对应:名称、定位、参数形状、交互模型、输出语义都应尽量对齐;不要混合多个原版工具的语义做一个“折中工具”。
- 代码风格上不要使用
*定义 keyword-only 参数;接口默认允许位置参数。 - runtime 包和会在 import 阶段执行的测试辅助代码需要保持 Python 3.6.2 语法兼容;不要引入 walrus
:=、match等仅 3.8+/3.10+ 可解析的新语法,哪怕分支在运行时不会走到。 - 本仓库使用
uv;本地默认没有预装测试依赖,开始工作前先跑uv sync --dev,验证用uv run pytest。 - 上游 Codex 的
steer目前是 TUI 交互层能力:开启后Enter会立即提交,Tab才是排队。对齐时优先盯“下一次发出去的请求体”而不是内部控制流;当前pycodex已把 steer/queue 语义下沉到AgentRuntime,并让AgentLoop.run_turn(texts)一次接收一批 user texts,这样下一次请求的input可以把多个 steer 文本按顺序并到 history 尾部。CliSessionView.handle_event()把 spinner 再次拉起。要保证输入不被遮挡,需要让 view 知道“当前正在输入”,并在 input-active 期间抑制这些事件对 spinner 的 resume。 - steer 的最小交互反馈已经约定为两条显式状态文案:入队时打印
[steer] queued: <prompt>,该条 queued turn 真正开始执行时打印[steer] inserted: <prompt>。后续如果补更复杂的 queue UI,也应保留这两个核心状态语义。 - 在当前
pycodexCLI 里,普通输入与/queue <message>只负责选择 runtime queue;真正的 steer/queue 差别由AgentRuntime.enqueue_user_turn(..., queue=...)决定。runtime 内部也应保持成两个同构 queue,而不是一个普通 queue 再叠一个 steer 专用旁路状态机。 - 对上游 steer 语义要非常谨慎:正常 active-turn steer 首先走的是
inject_input(...)+pending_input,不是立刻spawn_task(...)/TurnAbortReason::Replaced。更准确的理解是“在最近一次 sampling 边界插入”,而不是“任意时刻硬打断当前模型/工具调用”。 - 用
tests/fake_responses_server.py做 steer 时序对比时,不要把 proxy capture 文件的生成时刻当成“请求已到达 upstream”的信号;build_proxy_handler(...)会等整条 upstream response 读完后才write_capture(...)。如果要在第一条 request 仍未完成时注入 steer,应该同步等待 fake origin 自己收到第 1 条 POST。 --use-chat-completion已废弃为 CLI flag,改为从~/.codex/config.toml的 provider 段读取持久配置:在对应[model_providers.<name>]下加use_chat_completion = true即可对该 provider 默认启用本地responses_servercompat 层;CLI 仍可显式传--use-chat-completion覆盖配置值。- 在本机做 steer fake-server 对比时,不要把用户本地
config.toml里的service_tier/ fast-mode 设置混进“默认 steer”结论。tests/compare_steer_request_bodies.py现在会给 upstream 和pycodex都生成临时 config,并去掉顶层service_tier后再比较 request body。 x-codex-turn-metadata.workspaces的时机不是“整个 session 只发第一条请求”。当前对齐结论是:首个 turn 的后续 steer/follow-up request 也继续带workspaces;切到后续新 turn 才省略。- 远端 Codex home 存储模式当前仍刻意只挂在
pycodex/cli.py启动前:--put/--call只负责上传或落本地CODEX_HOME并重写args.config,model/context/runtime继续完全按config_path.parent读取.env、AGENTS.md、skills/;后续扩展时优先保持这个隔离边界,不要把分支判断散到运行时各模块里。 - 当前存储服务原型契约固定为
POST /v1/storage/put上传客户端本地加密后的 bundle,并返回/使用SECRET-CALLID@<host:port>这种 call token;GET /v1/storage/call/<call_id>只下载密文,bundle 的解密和解压都在 client 侧完成。做 smoke 或排障时,优先先验证这两个 endpoint 和 call token 形状。 --put的 CLI UX 现在约定为“先打印打包文件清单和上传目标,再打印结果”,并且最后一行保留为可直接执行的pycodex --call SECRET-CALLID@<host:port>一键启动命令;后续如果再改输出,尽量保留这个末行语义。--put现在不是“只上传就结束”:上传成功后还会立刻跑一次真实的--call下载/解包 round-trip 测试,只有这个测试也成功才算整个--put成功。排障时如果上传成功但 CLI 仍退出非零,要继续看 call 路径而不只看 put 端。--put现在会先做/healthzpreflight,再开始扫描/压缩目录;如果用户报“卡死”,先看目标host:port是否真有 storage server 在监听。默认打包策略也已经切到白名单:只带config.toml、.env、AGENTS.md、AGENTS.override.md、skills/**,以及 config 里相对引用的model_instructions_file,所以像sessions/这类运行态目录不会再靠黑名单排除。--call/ portable storage paths must not rely on the process default text encoding. Always passencoding="utf-8"when reading config, prompts, AGENTS files, skills, dotenv, and session history; for user-authored instructions/history, prefererrors="replace"so a Windows GBK locale cannot crash on UTF-8 punctuation such as U+2264 or em dash.- 对接真实
~/.codex/sessions/.../rollout-*.jsonl时,不要假设它一定是严格的一行一个 JSON object:本机样本可能包含 pretty-printed 多行对象,且文件尾部偶尔带未完成记录。恢复历史时用 concatenated-JSON 方式读取,并容忍尾部残缺。 pycodex本地 session 保存现在也按上游思路走:新 session 一开始就分配稳定的 uuidv7 thread/session id,并把历史增量追加到CODEX_HOME/sessions/.../rollout-*.jsonl;/resume列表应只展示至少有真实 user message 的 rollout,避免空白新 session 污染恢复列表。- auto-compact 对齐上游配置名
model_auto_compact_token_limit;为空时关闭,触发依据是最近一次模型上报的usage.total_tokens,pre-turn 压缩上一轮历史,mid-turn 压缩工具 follow-up 前的当前历史,并继续复用现有 compacted rollout 记录。 - Responses streaming 里的
response.incomplete不是连接断开:不要让ResponsesModelClient把它当 retryable incomplete stream 反复重连。普通 turn 应明确报response.incomplete;compact 请求如果已经收到 assistant partial summary,可以用这个 partial summary 完成 replacement history,避免 midturn auto-compact 卡在 5 次 retry。 - 上游 Codex Responses 请求当前不传模型级
max_output_tokens,也没有读取model_max_output_tokens这个 config key;这个名字在上游主要用于工具输出截断,不要为了上游对齐把它加进模型请求。 service_tier = "fast"在 config 中是 Fast mode 的持久写法,但 Responses wire 值是priority;只对模型 metadata 的service_tiers明确包含该 id 的模型发送,default和不支持的 tier 都省略。- 普通 turn 遇到
ResponsesIncompleteError(reason="max_output_tokens")时,上游语义是保留异常前已经收到的response.output_item.done;pycodex 因为模型客户端按整轮返回,需要在异常路径把这些 done assistant/reasoning items 写入 history 和 rollout,才能让用户下一句continue接上。不要把纯response.output_text.delta合成 history,也不要持久化没有 tool result 的 tool call。 - Feishu card tests read
~/.codex/.feishu_refresh_tokenthrough production code; when runningtests/test_feishu_card.pylocally, isolate HOME (for exampleHOME=/tmp/pycodex-empty-home env -u VIRTUAL_ENV uv run pytest tests/test_feishu_card.py tests/test_feishu_link.py) unless the test itself controlsHOME. lark_oapi.ws.clientcreates a module-level asyncioloopat import time andClient.start()always uses that global. For/linklong-connection listeners, bind that SDK global to a listener-thread-owned loop before constructing the client, and stop it through private_disconnect()plusloop.stop()on/unlink; otherwise unlink/link can reuse a still-running SDK loop and fail withRuntimeError: This event loop is already running.exec_commandbackground completion auto-resume is intentionally Agent-idle-only: when a session exits, it may callAgent.maybe_invoke(...)and start a synthetic<exec_command_completed>turn only if that Agent is not already running a turn. Do not enqueue/cache these events inCliSubmissionQueue; direct Agent/IPython use should share the same Agent-level hook.- The tool description JSON fallbacks (
pycodex/prompts/exec_tools.jsonandpycodex/prompts/subagent_tools.json) were deleted after moving schemas into class-levelBaseToolspecs.ToolSpec.serialize()intentionally skips function-tooloutput_schema, matching upstreamResponsesApiTool.output_schema #[serde(skip)]; keep output schemas as local metadata only unless upstream wire format changes.