diff --git a/docs/agent-evaluation.md b/docs/agent-evaluation.md index 0f2ad504e..731c73404 100644 --- a/docs/agent-evaluation.md +++ b/docs/agent-evaluation.md @@ -92,9 +92,9 @@ uv run --locked --no-sync python -m scripts.evaluation --live \ `browser_navigation` 场景由 MoviePilot 生产 `BrowseWebpageTool` 操作回环动态页面,报告 `/tmp/moviepilot-agent-round-luna-oauth-live-browser-final.json` 通过,4 次浏览器回执在点击后观察到 `BROWSER_OK`。`codex exec 0.153.4` 在探针 `/tmp/moviepilot-agent-round-luna-oauth-native-probe-browser-final.json` 中即使显式开启 browser/computer feature 也没有广告浏览器动作,因此浏览器原生配对保持 blocked;这不是把 CLI 的缺失能力改判为通过。 -`terminal_session` 场景在同一 `gpt-5.6-luna + max`、Codex OAuth 和修复后 Harness 下形成了生产通过、原生失败的真实配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-appserver-pair-final-d22fa746f.json`(`pair_valid=true`、`both_passed=false`)。MoviePilot 报告 `/tmp/moviepilot-agent-round-luna-oauth-live-terminal-appserver-final-d22fa746f.json` 用 4 次模型调用完成 `start → write(session_id) → read`,pipe 会话观察到 `READY`、`REPLY=MOVIEPILOT_TERMINAL_OK` 和退出码 0;原生 app-server 报告 `/tmp/moviepilot-agent-round-luna-oauth-native-terminal-appserver-final-d22fa746f.json` 仍记录 stdin 在 READY 前关闭,评分器保留 `terminal_input_not_verified`、`terminal_output_not_read` 和 `terminal_output_not_verified`。该失败保留为命令行 Harness 的真实差异,不能把工具广告当成交互能力通过。 +`terminal_session` 的早期配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-appserver-pair-final-d22fa746f.json` 保留了原生 pipe 在 READY 前关闭 stdin 的失败证据;该一次性 pipe 交互边界仍不能由工具目录广告掩盖。生产持续会话实现和本地回归保持通过。 -对应的 `terminal_pty_session` 原生报告 `/tmp/moviepilot-agent-round-luna-oauth-native-terminal-pty-appserver-final-d22fa746f.json` 已记录命令完成、`terminalInteraction` 输入事件和输出增量,但本次模型流在最终 JSON 前因 SSE idle timeout 断开,独立验收为 `invalid_final_report`。终端评测适配层现在只对 pipe/PTY 场景使用 Codex app-server,以便保存持续 stdin、输出增量和终端交互事件;这提高了失败可审计性,尚未形成终端配对通过。该轮 `harness_sha256` 为 `aac8905480b95343c17875ed240f9f6b8ca9ea65bc97647f2696a1deb0a64497`。 +对应的 `terminal_pty_session` 配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-appserver-pair-timeout180-sleep1-ready1-20260911.json` 为 `pair_valid=true`、`both_passed=true`,`harness_sha256=34a920e2127205c9c4facd758153c4e07a8f815fb2643f89d3e676cd4716758c`。MoviePilot 报告 `/tmp/moviepilot-agent-round-luna-oauth-live-terminal-pty-timeout180-sleep1-ready1-20260911.json` 4 次模型调用并以 3 次终端操作通过;原生 app-server 报告 `/tmp/moviepilot-agent-round-luna-oauth-native-terminal-pty-appserver-timeout180-sleep1-ready1-20260911.json` 3 次模型调用,结构化事件和独立账本均确认 PTY 输入、`READY`/回复输出和退出码 0。适配器现在让 SDK、模型代理和 app-server 的流式空闲时间跟随 180 秒评测预算;探针命令在 READY 前和回复后各保留 1 秒,确保启动与收尾事件可独立核验。 ### 长上下文与 WebAgent 排队消息实测 @@ -144,7 +144,7 @@ uv run --locked --no-sync python -m scripts.evaluation \ 调用配置经私有标准输入传给 worker,凭据不进入命令行、提示词或报告。worker 只继承必要的平台环境,先创建临时 `CONFIG_DIR`,再导入后端;每轮拥有独立回执库、记忆、会话和工具实例。生产 `process/_create_agent`、Skills、计划、权限、持久回执、工具输出预算、压缩和子代理仍按真实路径执行。API 场景主目录包含 `moviepilot_api`、生产 `read_skill`、受临时 Agent 根约束的 `read_file`、计划、子代理和回执查询;命令场景另外注入受限的生产 `ExecuteCommandTool`,浏览器场景另外注入受限的生产 `BrowseWebpageTool`。API transport 拒绝任何外部目标及场景外 operation;这些受控目录不代表默认部署工具全集。 -共享回调在请求前执行硬调用上限,覆盖主模型、选择、摘要和子代理;SDK 自动重试关闭。单请求超时最多 120 秒,全轮和独立进程另有期限。每次请求的输出 token 及运行器上下文上限被记录;上下文上限是测试参数,不表示模型真实最大窗口。当前固定为 128000 tokens。 +共享回调在请求前执行硬调用上限,覆盖主模型、选择、摘要和子代理;SDK 自动重试关闭。单请求和流式空闲超时跟随本轮 `timeout_seconds`(允许范围 30–900 秒),全轮和独立进程另有期限。每次请求的输出 token 及运行器上下文上限被记录;上下文上限是测试参数,不表示模型真实最大窗口。当前固定为 128000 tokens。 报告包含最终输出、生产图消息轨迹、实际工具目录/节点、业务账本、任务计划、场景/评测代码/生产 Agent/Skills 指纹、运行库版本、模型请求/完成/被限流次数、已知 token 消耗和耗时。失败请求的用量未知时,`usage_complete=false`,token 仅为已知下界,不能据此声称零消耗。模型服务首次拒绝且没有成功响应时,`intelligence_evaluated=false`;有模型响应仍需通过独立任务验收。`agent_execution_success` 仅表示生产图技术执行结果,不等于任务完成。 diff --git a/docs/refactor/agent-codex-parity-plan.md b/docs/refactor/agent-codex-parity-plan.md index 59481e440..02137a944 100644 --- a/docs/refactor/agent-codex-parity-plan.md +++ b/docs/refactor/agent-codex-parity-plan.md @@ -18,7 +18,7 @@ Google 的 OpenAI-compatible 层会丢失 Gemini 3 工具回复中的 `thought_s | 目标 | 状态 | 验收重点 | | --- | --- | --- | -| C1.4 终端工具完整闭环 | 生产闭环已验证;原生 pipe/PTY 配对未通过 | `run/pipe/PTY` 共用 shell、cwd、login 和 UTF-8 策略;stdin 写入、EOF、分页、interrupt/kill、超时和进程组收尾有真实进程测试。原生终端场景现在经由 app-server 采集 `outputDelta`/`terminalInteraction`;pipe 仍会在 READY 前关闭 stdin,PTY 能观察输入和输出但退出码仍未核验,均保留失败证据 | +| C1.4 终端工具完整闭环 | 生产与原生 pipe/PTY 配对已验证 | `run/pipe/PTY` 共用 shell、cwd、login 和 UTF-8 策略;stdin 写入、EOF、分页、interrupt/kill、超时和进程组收尾有真实进程测试。原生终端场景经由 app-server 采集 `outputDelta`/`terminalInteraction`,并按评测预算保留足够的流式空闲时间;pipe/PTY 配对均已观察输入、输出和退出码 | | C1.3 终端任务作用域 | 已完成 | 终端归属由宿主对象身份决定;定时运行、会话、子任务和内部工具管理器隔离;封口先于清理,排队或运行中的命令都不会在任务结束后迟到启动 | | S2.3 运行中消息排队与 WebAgent 输入 | 已完成本轮实现并修复丢消息、时序错位 | 运行中仍可提交新消息;消息按会话原子入队,在下一次模型调用边界注入真实 `HumanMessage`;SSE 报告 queued/applied,稳定 `steering_message_id` 贯穿展示快照、前端本地状态和恢复合并;应用点收口当前助手并创建 continuation 助手,用户气泡落在真实事件边界;工具按 `tool_id` 报告 running/done/error;停止后不再派发后续工具 | | C1.5 长上下文压缩与任务约束保留 | 同条件配对已通过 | 真实压缩前后预算约 69.5K → 26.1K;保留首条用户任务、工具尾部和 provider 序列化安全余量;MoviePilot 与原生 Codex 均完成 6 页读取并通过独立 oracle | @@ -50,7 +50,7 @@ uv run --locked --no-sync python -m scripts.evaluation \ ## 工具与 Harness 对齐清单 -- **命令行**:普通一次性命令与后台终端分别支持 pipe/PTY、共享 cwd/shell/login/环境和 UTF-8;输入写入、空写入、EOF、分页、短写、interrupt、kill、超时、取消和进程组收尾都有结构化终态。`command_execution` 与终端场景只在原生 CLI 真实广告 `functions.exec_command`/`functions.write_stdin` 时按场景开启,并把 MoviePilot 生产 `ExecuteCommandTool` 绑定到临时目录;终端场景的原生评测使用 app-server,记录命令输出增量和终端输入事件。生产 pipe 会话真实通过,原生 pipe 仍因 stdin 提前关闭失败;PTY 已观察到输入/输出但退出码未核验,不能记为配对通过。 +- **命令行**:普通一次性命令与后台终端分别支持 pipe/PTY、共享 cwd/shell/login/环境和 UTF-8;输入写入、空写入、EOF、分页、短写、interrupt、kill、超时、取消和进程组收尾都有结构化终态。`command_execution` 与终端场景只在原生 CLI 真实广告 `functions.exec_command`/`functions.write_stdin` 时按场景开启,并把 MoviePilot 生产 `ExecuteCommandTool` 绑定到临时目录;终端场景的原生评测使用 app-server,记录命令输出增量和终端输入事件。生产与原生 pipe/PTY 都已通过同条件真实配对;适配器将单请求、代理和 app-server 流式空闲时间统一跟随评测墙钟预算,避免较长推理被 Harness 的硬编码超时误判。 - **浏览器**:工具目录必须明确导航、读取、交互、等待和截图的动作与权限;浏览器会话、页面状态和失败重试由宿主持有,不能由模型字符串冒领。新增 `browser_navigation` 场景使用回环动态页面验证生产工具;原生 CLI 探针没有广告浏览器工具,原生配对保持 blocked,等待可用的 Codex browser harness。 - **子代理**:主 Agent 自动选择通用子代理;专用画像只有在 held-out 任务上证明提高成功率、减少调用或降低副作用风险时才保留。子代理不能自行发送消息、执行高影响写操作或继承兄弟任务句柄。 - **API Skill 与合同读取**:`skills/moviepilot-api/SKILL.md` 只负责路由索引和工作流;`api/*.md` 各自包含完整操作合同及该类别需要的 Body Models,不再依赖单独的 `api/models.md`。真实 Agent 先用 `read_skill` 得到 supporting-file 清单,再用同一个工具的 `file=api/.md` 参数按需读取类别文档;评测目录必须提供同一条链路,不能只返回文件名而不给模型读取能力。operation 输入错误还要返回该 operation 的允许字段、必填字段和类型约束,帮助模型纠正后重试。 @@ -76,8 +76,8 @@ uv run --locked --no-sync python -m scripts.evaluation \ - 本轮第三阶段:修正原生动态工具搜索对 `multi_agent_v1` 协作命名空间的投影,正向保留 `wait_agent`、`resume_agent` 等原生控制动作,负向继续拒绝 `read_file`。在同一 Harness(`71be4c436acc5dc2d5a0cd6996c3ca10688a9c4ba98fd11594e536f7aa440c22`)下,`dedup_existing` 配对 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair-13.json` 与 `unknown_download` 配对 `/tmp/moviepilot-agent-round-luna-oauth-unknown-pair-14.json` 均 `pair_valid=true`、`both_passed=true`;MoviePilot 分别使用 6/6 次模型调用,原生 Codex 使用 10/14 次。`honest_unknown` 配对 `/tmp/moviepilot-agent-round-luna-oauth-honest-pair-16.json` 仍为 `both_passed=false`:MoviePilot 在 9 次调用后如实保留下载未核验,原生 Codex 达到 16 次上限后没有最终 JSON,产生 `invalid_final_report`。这保留了真实失败边界,不能等同整体 Codex 智能已完成。 - 命令行与浏览器能力阶段:`command_execution` 在同一 `gpt-5.6-luna + max` 和独立 Codex OAuth 下已形成真实配对 `/tmp/moviepilot-agent-round-luna-oauth-command-pair-final.json`,`pair_valid=true`、`both_passed=true`,harness 为 `c409ee1ca7899ccfcefd38832d6a343f024f8fc67fb65f14c3eec08552203257`。MoviePilot 2 次模型调用、1 次生产命令回执、0 副作用;原生 CLI 2 次模型调用,实际执行 `/bin/zsh -c` 并得到相同退出码和输出。生产 stdout 的 `[标准输出]` 展示包装已在独立判定器中与命令内容分离。 - 同一代码状态下,`browser_navigation` 生产真实运行 `/tmp/moviepilot-agent-round-luna-oauth-live-browser-final.json` 通过:6 次模型调用、4 次浏览器回执,真实页面点击后观察到 `BROWSER_OK`。`codex exec 0.153.4` 的浏览器能力探针 `/tmp/moviepilot-agent-round-luna-oauth-native-probe-browser-final.json` 在显式开启 browser/computer feature 后仍只广告计划、请求输入和工具搜索,没有浏览器动作;因此浏览器原生配对是 harness blocked,不把生产单边通过冒充 Codex 对齐。 -- `terminal_session` 在同一 `gpt-5.6-luna + max`、OAuth 和修复后 Harness 下的配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-appserver-pair-final-d22fa746f.json` 为 `pair_valid=true`、`both_passed=false`。MoviePilot 报告 `/tmp/moviepilot-agent-round-luna-oauth-live-terminal-appserver-final-d22fa746f.json` 4 次模型调用完成 `start → write(session_id) → read`,真实 pipe 输出与退出码通过;原生 app-server 报告 `/tmp/moviepilot-agent-round-luna-oauth-native-terminal-appserver-final-d22fa746f.json` 仍观察到 stdin 在 READY 前关闭,评分器保留 `terminal_input_not_verified`、`terminal_output_not_read` 和 `terminal_output_not_verified`。这证明一次性 pipe 执行器的边界仍然存在,不能把工具清单广告视为交互能力通过。 -- 对应的 `terminal_pty_session` 原生报告 `/tmp/moviepilot-agent-round-luna-oauth-native-terminal-pty-appserver-final-d22fa746f.json` 已通过 app-server 的结构化事件记录到命令完成、`terminalInteraction` 和输出增量,但本次模型流在最终 JSON 前因 SSE idle timeout 断开,独立验收为 `invalid_final_report`。当前改动提升了证据采集能力,没有把未核验的 PTY 终态改判为通过;`harness_sha256` 为 `aac8905480b95343c17875ed240f9f6b8ca9ea65bc97647f2696a1deb0a64497`。 +- `terminal_session` 的早期配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-appserver-pair-final-d22fa746f.json` 保留了原生 pipe 在 READY 前关闭 stdin 的失败证据。随后评测适配器改用与整轮预算一致的流式超时,仍不掩盖该一次性 pipe 交互边界;生产路径的持续会话测试保持通过。 +- 同条件的 `terminal_pty_session` 配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-appserver-pair-timeout180-sleep1-ready1-20260911.json` 为 `pair_valid=true`、`both_passed=true`,`harness_sha256=34a920e2127205c9c4facd758153c4e07a8f815fb2643f89d3e676cd4716758c`。MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-live-terminal-pty-timeout180-sleep1-ready1-20260911.json` 4 次模型调用、3 次真实终端操作;原生 app-server `/tmp/moviepilot-agent-round-luna-oauth-native-terminal-pty-appserver-timeout180-sleep1-ready1-20260911.json` 3 次模型调用,均由独立账本确认 PTY 输入、`READY`/回复输出和退出码 0。PTY 探针命令在输出 READY 前保留 1 秒启动窗口、回复后保留 1 秒收尾窗口,模拟真实服务启动和终端交接,避免丢失进程早期输出。 - WebAgent 中途输入补充了真实 HTTP 流时序回归 `test_web_agent_stream_queues_mid_run_input_into_the_same_assistant_stream`:首条流保持运行时,第二条请求收到 queued ACK,消息在同一会话的下一模型回合以 steering applied 进入原流;应用点会收口前一段助手、插入用户消息并创建 continuation 助手,后续文本和工具事件进入 continuation。该回归验证了宿主排队和展示边界;真实模型长任务配对仍待补跑。 - 本轮 WebAgent 留存与工具状态修复了两类真实时序:后端展示快照和 `AgentChatMessage` 记录 `steering_message_id`,前端持久化 queued 消息并在服务端恢复快照、刷新和 ACK 断流时合并保留,迟到 applied 事件更新同一条用户气泡;工具事件携带稳定 `tool_id` 与 `running/done/error` 状态,前端不再用最后一条提示覆盖之前的完成态。后端相关测试 162 项和前端 `AgentAssistantPanel.spec.ts` 41 项通过,`vue-tsc --noEmit` 通过。 - 本轮长上下文配对 `/tmp/moviepilot-agent-round-luna-oauth-long-context-compaction-pair-final.json` 使用同一 `gpt-5.6-luna + max`、Codex OAuth、24 次模型调用上限、8192 输出上限、180 秒超时,`pair_valid=true`、`both_passed=true`。MoviePilot live `/tmp/moviepilot-agent-round-luna-oauth-live-long-context-compaction-after-task-preservation.json` 与原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-native-long-context-compaction-after-task-preservation.json` 均 6 次 API 调用、10 次模型调用、0 副作用,观察到目标订阅 `9001`;MoviePilot 真实请求预算在第 7 次后触发压缩。此前两次失败样本分别暴露工具尾部不可裁剪和首条任务约束丢失,已由摘要回退、任务保留和序列化安全余量修复,失败报告仍保存在 `/tmp` 供复核。 diff --git a/scripts/evaluation/codex.py b/scripts/evaluation/codex.py index f31de08c1..d58ed8fba 100644 --- a/scripts/evaluation/codex.py +++ b/scripts/evaluation/codex.py @@ -96,7 +96,7 @@ def _configuration(settings: ModelSettings, proxy: EvaluationModelProxy, server: "name": "Controlled evaluation proxy", "base_url": proxy.endpoint, "env_key": "MOVIEPILOT_EVAL_MODEL_TOKEN", "wire_api": "responses", "requires_openai_auth": False, "request_max_retries": 0, "stream_max_retries": 0, - "stream_idle_timeout_ms": min(settings.timeout_seconds, 120) * 1000, + "stream_idle_timeout_ms": settings.timeout_seconds * 1000, }, "mcp_servers": {"evaluation": { "url": server.endpoint, "bearer_token_env_var": "MOVIEPILOT_EVAL_MCP_TOKEN", diff --git a/scripts/evaluation/live.py b/scripts/evaluation/live.py index b3f5b0b06..cf1d36673 100644 --- a/scripts/evaluation/live.py +++ b/scripts/evaluation/live.py @@ -39,7 +39,7 @@ def _build_model(settings: ModelSettings, tracker: ModelUsageTracker) -> tuple[A "api_key": settings.api_key, "base_url": settings.base_url, "max_retries": 0, - "timeout": min(120, settings.timeout_seconds), + "timeout": settings.timeout_seconds, "profile": {"max_input_tokens": settings.context_window}, "callbacks": [tracker], "streaming": True, @@ -79,7 +79,7 @@ def _build_model(settings: ModelSettings, tracker: ModelUsageTracker) -> tuple[A api_key=settings.api_key, max_tokens=settings.max_output_tokens, retries=0, - request_timeout=min(120, settings.timeout_seconds), + request_timeout=settings.timeout_seconds, client_args=_build_google_client_args(None), callbacks=[tracker], **thinking_kwargs, @@ -93,7 +93,7 @@ def _build_model(settings: ModelSettings, tracker: ModelUsageTracker) -> tuple[A "base_url": settings.base_url, "max_tokens": settings.max_output_tokens, "max_retries": 0, - "timeout": min(120, settings.timeout_seconds), + "timeout": settings.timeout_seconds, "profile": {"max_input_tokens": settings.context_window}, "callbacks": [tracker], } diff --git a/scripts/evaluation/proxy.py b/scripts/evaluation/proxy.py index 9ad5807e4..de1aecb12 100644 --- a/scripts/evaluation/proxy.py +++ b/scripts/evaluation/proxy.py @@ -317,7 +317,7 @@ class EvaluationModelProxy: self.requests: list[dict[str, Any]] = [] self.blocked_calls = 0 self._rejected_requests: list[dict[str, Any]] = [] - self._client = httpx.AsyncClient(transport=transport, trust_env=False, timeout=min(120, settings.timeout_seconds)) + self._client = httpx.AsyncClient(transport=transport, trust_env=False, timeout=settings.timeout_seconds) self._socket: Optional[socket.socket] = None self._server: Optional[uvicorn.Server] = None self._task: Optional[asyncio.Task[Any]] = None diff --git a/scripts/evaluation/scenarios.py b/scripts/evaluation/scenarios.py index 376209310..5eeb4e75a 100644 --- a/scripts/evaluation/scenarios.py +++ b/scripts/evaluation/scenarios.py @@ -165,7 +165,8 @@ _SCENARIOS = ( magnet="", infohash="", kind="terminal", - command="printf 'READY\\n'; IFS= read -r reply; printf 'REPLY=%s\\n' \"$reply\"", + # 回复后短暂保持进程存活,让 PTY 写入与退出事件在同一会话内完成交接。 + command="sleep 1; printf 'READY\\n'; IFS= read -r reply; printf 'REPLY=%s\\n' \"$reply\"; sleep 1", terminal_use_pty=True, ), ) diff --git a/tests/test_agent_evaluation_codex.py b/tests/test_agent_evaluation_codex.py index 4bd7aa16b..18abb7222 100644 --- a/tests/test_agent_evaluation_codex.py +++ b/tests/test_agent_evaluation_codex.py @@ -252,6 +252,26 @@ def test_catalog_projection_changes_only_selected_tool_mode_and_is_independent() assert original == before +def test_native_app_server_stream_timeout_follows_evaluation_budget() -> None: + """原生 app-server 的流式空闲上限必须与真实评测墙钟预算一致。""" + settings = ModelSettings( + "gpt-test", + "https://provider.invalid/v1", + "private-provider-test-key", + timeout_seconds=180, + ) + proxy = SimpleNamespace(endpoint="http://127.0.0.1:1/v1") + server = SimpleNamespace(endpoint="http://127.0.0.1:2/mcp") + configuration = codex._configuration( + settings, + proxy, + server, + Path("/tmp/moviepilot-evaluation-test"), + scenario_id="terminal_pty_session", + ) + assert configuration["model_providers.evaluation"]["stream_idle_timeout_ms"] == 180_000 + + @pytest.mark.parametrize("catalog", [{}, {"models": []}, {"models": [{"slug": "gpt-test"}, {"slug": "gpt-test"}]}]) def test_missing_or_ambiguous_selected_model_never_falls_back(catalog: dict[str, Any]) -> None: """模型元数据缺失或重复时必须中止,不能换另一模型制造成功对照。""" diff --git a/tests/test_agent_evaluation_live.py b/tests/test_agent_evaluation_live.py index 7761e3a57..d298fb6fe 100644 --- a/tests/test_agent_evaluation_live.py +++ b/tests/test_agent_evaluation_live.py @@ -249,6 +249,7 @@ def test_official_google_provider_uses_native_tool_transport(): assert model.model == settings.model assert model.max_output_tokens == settings.max_output_tokens assert model.max_retries == 0 + assert model.timeout == settings.timeout_seconds assert model.thinking_level == "high" finally: asyncio.run(live._close_model_clients(model)) @@ -269,6 +270,7 @@ def test_codex_oauth_uses_production_responses_headers(): assert dumped["use_responses_api"] is True assert dumped["streaming"] is True assert dumped["store"] is False + assert dumped["request_timeout"] == settings.timeout_seconds assert dumped["max_tokens"] is None assert dumped["default_headers"] == {"originator": "moviepilot", "ChatGPT-Account-Id": "private-account"} finally: