Skip to content

Latest commit

 

History

History
72 lines (70 loc) · 20.3 KB

File metadata and controls

72 lines (70 loc) · 20.3 KB

pycodex AGENTS

Repo Priors

  • 这个仓库的目标是尽可能准确地复现上游 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-ws console script。
  • 双包发布时先发布 python-codex,成功后再发布 pycodex-ws,避免新 metapackage 短暂指向尚不存在的核心版本。
  • 包结构直接放在 repo 根目录下的 pycodex/,不使用 src/ layout。
  • Rust 到 Python 的主映射:submission_loop -> pycodex/runtime.pyrun_turn / run_sampling_request -> pycodex/agent.pyToolRouter -> pycodex/tools/base_tool.py + pycodex/tools/.
  • 会话协议先收敛到 4 类 item:UserMessageAssistantMessageToolCallToolResult;只有这层稳定后再扩 richer event model。
  • 优先保持主循环可测试、可替换:模型侧通过 ModelClient 协议接入;测试专用的 ScriptedModelClient 放在 tests/fakes.py,不要放进运行时包。
  • ResponsesModelClient 直接复用 ~/.codex/config.toml 的 provider 配置;当前已验证这里的 responses provider 需要 stream = true,否则会返回 400Stream must be set to true
  • 现在 ResponsesModelClient 默认会对流式断连做 provider 级自动重试(stream_max_retries 默认 5);写 CLI/REPL 测试时如果断言“先向用户报错,再靠下一句 go on 继续”,必须在测试 provider 配置里显式设 stream_max_retries = 0,否则测试可能一直等不到预期错误而卡住。
  • responses_server compat 层应透传请求里的 model;不要再做 “取 downstream /models 第一个 id 并强制覆盖请求模型” 这种兜底兼容。
  • model_provider = "vllm"responses_server 仍然走 /v1/chat/completions compat 路径,但要保留 reasoning:把 chat chunk 里的 reasoning / reasoning_content 翻回 Responses reasoning item,并把历史里的 Responses reasoning item 回放成下游 assistant message 的 reasoning 字段。
  • responses_server 不能把 terminal reasoning-only chat output 当成成功回复:如果 downstream 一轮结束时只返回 reasoning / reasoning_content,没有 assistant content 且没有 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、mock web_search follow-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 时,streaming prompt_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 with pycodex-ws --listen <host:port> --board <html>, not through pycodex CLI dispatch. CLI and web share pycodex.interactive_session.run_interactive_session; slash-command semantics such as /resume, /compact, /model, and /link belong 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.pyAgentLoop 只维护真实会话历史;每轮请求前由 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_window override,再回退到 vendored models.jsoncontext_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.mddocs/ 属于对齐工作的一部分:只要实现状态、对齐结论或使用方式发生实质变化,就应及时更新,不要让文档滞后于当前代码。
  • 新工具必须继承 BaseTool,然后通过 ToolRegistry.register(tool_instance) 接入;不要再给 registry 传散装 name/description/handler 参数。
  • request/tool schema 对齐应落在实际建模层(ToolSpec / BaseTool / request builder)本身,不要再引入 prompt 级别的 serialized_tools 旁路覆盖。
  • 当前已接入的内置工具子集:shellshell_commandexec_commandwrite_stdinexecwaitweb_searchupdate_planrequest_user_inputrequest_permissionsspawn_agentsend_inputresume_agentwait_agentclose_agentapply_patchgrep_filesread_filelist_dirview_image。后续继续新增时,仍然按原版 Codex 内置工具逐个补齐。
  • exec_command / write_stdin 的 unified-exec 对齐要注意两层默认截断语义:省略 max_output_tokens 时也要按 upstream 默认 10_000 token 预算裁剪 tool response;同时长时间未轮询的未读输出缓冲不能无限累积,需保留 upstream 同款 1 MiB head/tail。
  • 当前协议层已经支持两类原版工具载荷:普通 function tools,以及 apply_patch 这类 freeform/custom tools;工具结果也支持结构化 input_image content 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 request function_call_output 当前不带 success;而 GitHub openai/codex main 源码里的 FunctionCallOutputPayload / request_user_input handler 支持传递 success。写对齐结论时要明确区分“installed CLI live capture”与“upstream main 源码建模”。
  • 当前 exec / wait 已有一个最小 code-mode 实现:底层通过 Node 子进程运行 JavaScript,自带 text / image / store / load / exit / notify / yield_control helper,并允许通过 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,也应保留这两个核心状态语义。
  • 在当前 pycodex CLI 里,普通输入与 /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_server compat 层;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.configmodel/context/runtime 继续完全按 config_path.parent 读取 .envAGENTS.mdskills/;后续扩展时优先保持这个隔离边界,不要把分支判断散到运行时各模块里。
  • 当前存储服务原型契约固定为 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 现在会先做 /healthz preflight,再开始扫描/压缩目录;如果用户报“卡死”,先看目标 host:port 是否真有 storage server 在监听。默认打包策略也已经切到白名单:只带 config.toml.envAGENTS.mdAGENTS.override.mdskills/**,以及 config 里相对引用的 model_instructions_file,所以像 sessions/ 这类运行态目录不会再靠黑名单排除。
  • --call / portable storage paths must not rely on the process default text encoding. Always pass encoding="utf-8" when reading config, prompts, AGENTS files, skills, dotenv, and session history; for user-authored instructions/history, prefer errors="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_token through production code; when running tests/test_feishu_card.py locally, isolate HOME (for example HOME=/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 controls HOME.
  • lark_oapi.ws.client creates a module-level asyncio loop at import time and Client.start() always uses that global. For /link long-connection listeners, bind that SDK global to a listener-thread-owned loop before constructing the client, and stop it through private _disconnect() plus loop.stop() on /unlink; otherwise unlink/link can reuse a still-running SDK loop and fail with RuntimeError: This event loop is already running.
  • exec_command background completion auto-resume is intentionally Agent-idle-only: when a session exits, it may call Agent.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 in CliSubmissionQueue; direct Agent/IPython use should share the same Agent-level hook.
  • The tool description JSON fallbacks (pycodex/prompts/exec_tools.json and pycodex/prompts/subagent_tools.json) were deleted after moving schemas into class-level BaseTool specs. ToolSpec.serialize() intentionally skips function-tool output_schema, matching upstream ResponsesApiTool.output_schema #[serde(skip)]; keep output schemas as local metadata only unless upstream wire format changes.