feat: P1 能力补齐——Plan Mode + MCP 工具懒加载(ToolSearch) - #570
Conversation
Plan Mode / MCP 工具懒加载 / MCP OAuth / 浏览器自动化 / 语义检索代码库索引 的总体开发计划、排期、验收基线与待拍板决策。
- 新增 ExitPlanMode 内置工具:挂起/落定/超时(10min 按未批准)/中止模型, 与 AskUserQuestion/工具审批同构;批准触发续轮自动入队,拒绝带反馈留在计划模式 - 注册表组装层直接裁掉非只读工具(模型不可见,省 token 无泄漏面), resolveToolGate 加后备拦截兜旁路;子代理经 forceReadonly 强制 readonly, 显式 worktree 按参数错误拒绝(fail-closed,不静默降级) - ChatRuntimeControls.planModeEnabled:归一化仅显式 true 生效,跨端合并 只能收紧(同 strictestCommandSafetyMode 取向);composer 新增「计划」chip - system prompt 段 turn 级冻结注入(保护前缀缓存),轨迹数组同口径记录 - PlanModeCard 共享卡片(markdown 预览+倒计时+批准/要求修改),GUI 直连 挂起表,WebUI 经 chat_queue.plan_decision(action 字符串,零 proto 改动) - 分享脱敏清单(TS/Rust)同步 ExitPlanMode;i18n 双端补全 测试:plan-mode-tools 10 用例、validate forceReadonly、registry 过滤; 全量 2457/2457(含 cargo),三端 tsc 零错误,biome 与 main 基线持平。 设计文档 docs/design/plan-mode.md;详见 2026h2-capability-roadmap.md P1-①
- spike 定论(附真实 pi-agent-core 集成测试):loop 快照使执行层必须全量 注册,但 runner 的 filterRequestTools 每轮重估——「执行层可见、请求层隐藏」 即可实现轮中激活,零上游依赖风险 - MCP 工具 schema 估算(tokenLedger 同口径)超 12k 时:注入 ToolSearch, 未激活的 MCP 工具不进模型请求;检索命中/直呼均激活,会话内持续有效 - 检索为轻量加权子串评分(name×3/server×2/description×1),默认 5 条上限 10 - runner 新增 requestToolFilter 参数;registry 返回 mcpToolDeferralActive; plan mode 下不注册(MCP 工具本就不在表内);会话销毁清理激活集 - 目录/i18n/分享脱敏清单(TS+Rust)同步 测试:tool-search 5 用例、runner 轮中激活集成测试、registry 判定口径; 全量 2464/2464,三端 tsc 零错误,biome 与 main 基线持平。 设计文档 docs/design/mcp-tool-search.md;详见 roadmap P1-②
|
PR governance checks passed. Awaiting human review. |
feat(ui): 计划模式入口迁入 + 菜单,计划卡任务卡化直出 历史库修复(根因): - 1.x 老库的 chatHistory.context_json(NOT NULL 无默认值)在分段持久化上线后 已无写方,但 CREATE TABLE IF NOT EXISTS 不改建既有表、列 ensure 只加不删, 导致每次 INSERT 被 NOT NULL 约束拒绝:聊天历史/任务清单持久化全部失败, 轨迹分段随之因外键刷屏报错。v4 迁移 DROP COLUMN + 迁移测试;实机验证 修复后历史行/任务清单正常落库 - 「批准后续轮先入队」为既有队列语义:run 进行中入队、结束由 drain 自动发送 计划模式 UI(按 Codex 参照重做): - 入口迁入 + 菜单(「添加」分节:文件和文件夹 / 计划模式,附动作提示文案), 开启后 composer 显示可点击关闭的「计划」pill;composer chip 与安全模式 菜单合并方案全部撤销,入口唯一 - 计划卡任务卡化:不进折叠壳、无摘要行,整卡直出(自带头部+状态+倒计时), 正文不限高与消息同滚动;附件按钮改为 + 触发器(Paperclip 项收入菜单) 测试:v4 迁移用例、布局约束测试更新;全量 2464/2464、cargo 943、三端 tsc 零错误
实测截图与增量修复(80bb156)持久化报错根因已修「任务清单持久化失败(重试两次均报错)」与轨迹 FOREIGN KEY 刷屏的根因:1.x 老库遗留的 计划模式 UI 按 Codex 参照重做
Closes #571 |
用户观察准确:此前批准后模型还要跑一轮'收尾话',run 才结束、队列才放行,
体验上是'批准→等收尾→才执行'。改为批准落定即 terminate:
- runner 新增 resolveToolTermination 工具级终止谓词(复用 pi-agent-core
afterToolCall terminate,provider 原生搜索桥同机制)
- ExitPlanMode 批准时标记该调用(approvedToolCallIds),turn 层谓词命中即
结束 run——不再消耗收尾轮 token,续轮由 drain 立即自动发送
- 工具描述/system prompt 段同步('Approval ends this turn immediately')
- cancel 兜底清理标记集
测试:批准标记/拒绝不标记用例、runner terminate 集成测试(单模型轮断言);
全量 2465/2465
- gateway test/webui/web-settings.test.mjs 四处期望字面量补 planModeEnabled:false (与 agent-gui settings/normalization 同批适配,此前遗漏 webui 侧) - docs/images/pr570/: 计划模式 + 菜单/pill/计划卡直出/端到端实测截图, 供 PR body Screenshots 小节引用(governance 检查要求)
- handleSend 优先分派:会话存在挂起的 ExitPlanMode 时,输入的消息直接作为 'reject + feedback'落到计划卡(模型立即收到意见继续完善),不再排进队列 干等审批——消除'卡在 ExitPlanMode、消息只能进队列'的体验 - 续轮提示词去系统腔:'开始执行已批准的计划:先用 TaskCreate 建任务清单…' (删除'见上方 ExitPlanMode 结果'的内部指涉) - plan mode system 段明确禁止提议把计划存成 plan.md/询问保存路径 (计划载体是卡片,规划期也根本写不了文件) 实测:挂起时输入'内容改成 X'→ 卡片落定为已退回并展示反馈 → 新版计划 自动提交 → 批准 → 本轮即终止 → 续轮自动执行 → 文件内容为 X。 测试:getPendingPlanDecisionToolCallId 用例;全量 2466/2466
- 批准卡新增'同时保存计划到文件'勾选(路径可改,预填 plan.md,相对工作区):
savePath 经 PlanDecisionAnswer 契约(仅批准时保留,超长截断)透传到宿主,
续轮提示词首条即'先把计划原文保存到 {path}'——落盘发生在执行轮
(全工具+checkpoint 可回滚),规划轮自身无写能力
- 执行续轮不再入队:批准暂存续轮提示,批准即终止使 run 立刻结束,effect 在
running 集合消失后直接 send(带 planModeEnabled:false 覆盖)。用户不再看到
队列行,也无队列轮询参与;计划执行完对话自然结束
实测:批准勾选存盘 → 无队列行 → plan.md 落盘 + hello.txt 改写 → 对话结束。
测试:savePath 契约(批准保留/拒绝忽略/截断)、回调透传;全量 2466/2466
用户反馈三连:挂起转圈难看、'同意'打字无效必须点按钮、保存计划应走对话。
按 Codex plan mode 范式整体重做:
- ExitPlanMode 不再挂起:提交即登记'待决计划'并立即返回,runner 终止谓词
使规划 run 就地结束——无转圈等待、无 10min 审批超时、无 deadline 盖章
- 应答即对话:输入纯批准短语('同意/开始/ok/lgtm'等 26 个中英短语)= 批准
(等同点按钮);其他输入 = 普通消息(修改意见),模型修订后重新提交,
新提交覆盖旧登记。'保存计划到文件'同样走对话——模型把落盘写进计划首步
- 宿主动作经 registerPlanDecisionHandlers 单例注册:批准 = 关 plan 开关 +
run 结束后直发执行续轮(不进队列);退回 = 反馈作为普通消息发送。
卡片按钮/批准短语/WebUI plan_decision 三入口共用
- 计划卡瘦身:标题+正文+单个'批准并开始执行'按钮+'输入同意即批准'提示;
待决/已批准态 GUI 订阅登记表,WebUI 由参数标记(__exitPlanModePending/
Approved,gateway preview 盖章)响应
- 契约清理:删除 deadline/savePath/倒计时全套;details 仅 kind+plan
实测:提交即停 → 打'同意'回车即批准('同意'不进会话) → 续轮自动执行 →
文件落地 → 对话自然结束。测试重写 10 用例;全量 2465/2465,三端 tsc 零错误
唯一冲突为 ChatPage handleSend 的 useCallback 依赖数组:合并双方新增项 (composerRef + requestQueuedChatTurnProcessing);计划续轮 effect 的刻意 触发依赖按 main 新启用的 useExhaustiveDependencies 规则补 biome-ignore。 合并后全量 2473/2473,三端 tsc(TS7) 零错误
审查 PR#570 发现的问题逐项修复: 阻断级: - ToolSearch 延迟判定按 groupId find 命中先注册的 McpManager(同 groupId "mcp"),永远低于阈值,特性在生产完全失效。改为持有 MCP 业务工具 bundle 的直接引用做判定与目录构建(builtinRegistry),并补超阈值形态回归测试。 - Plan mode 只读承诺被子代理穿透:selectReadOnlyTools 放行 isReadOnly:false 的 MCP 业务工具。新增 strictReadOnly(forceReadonly 时启用),plan mode 下子代理不再继承变更类 MCP 工具;补拒绝断言测试。 - 批准后执行续轮被静默丢弃:续轮 send 可因 isSending 竞态返回 false, 之前删了暂存就直发。改为暂存→冲刷→失败重新暂存+限次兜底重试。 - 拒绝反馈同病(onReject 直发不看结果),统一走同一条暂存冲刷路径。 严重级: - buildMcpRequestToolFilter 按裸 groupId 隐藏 McpManager(不在延迟目录, 永远无法激活)。判定收窄为 kind==="mcp";直呼激活分支同口径。 - ExitPlanMode 与并行调用同批时批终止失效(pi-agent-core all-or-nothing 语义):终止谓词按批铺展,同批调用照常执行后 run 就地终止;补测试。 - WebUI 按轮 plan 开关有名无实:proto ChatRuntimeControls 新增 plan_mode_enabled(4),web 序列化/adapters/Go body 双向/Rust 解构/GUI 事件类型全链路铺通,桌面端"只能收紧"合并即刻生效,不再依赖 settings 同步竞态。 - 空闲 prune 杀死跨 run 存活的待决计划:计划审批从 prune 清理中移除 (设计上就要跨 run 存活),仅会话删除时清;两条 prune 路径(ChatPage 与 useConversationHistoryActions)统一共用同一瞬态清理回调。 次要级: - "+"菜单触发键不再被 uploadDisabled 连坐(plan 开关不依赖 workdir/上传); 上传限制下沉到上传菜单项自身。 - WebUI 计划卡永久陈旧:审批发生在 run 终止后,无事件翻转参数标记。 planModeBridge 增加本地落定 overlay(批准/退回/桌面回报 not_found 均 落定),usePlanDecisionState 合并 overlay 与标记;answerPlanDecision 增加结构化 code 并经 error_code 直通到远端。 - approvedToolCallIds 进程级泄漏:批准先清 pending 导致清理反查不到。 按会话另记批准集,cancel 时整组删除;补清理断言。 - 删除不存在的"10 分钟超时"承诺:i18n 两端文案、设计文档、共享类型的 timedOut/cancelled 死字段一并清除。 - 测试传入被静默忽略的 onPlanApproved 参数删除(审批路由由 registerPlanDecisionHandlers 承接,registry 不再有该回调)。
代码审查:发现 12 项缺陷,已在
|
待决计划跨 run 存活(设计如此),但此前 handleSend 的短语批准只看 "有无待决计划":用户提交计划后手动关掉「计划」pill 弃置,之后在同一 会话随口回一句"好的/ok",被弃置的旧计划会瞬间获批并自动直发执行续轮。 修复:短语批准要求 plan 开关仍开着(planModeEnabledRef 点击时采样, handleSend 刻意不依赖 settings)。正常流程不受影响——提交后开关保持 开启,批准才关;关掉 pill 后批准短语回归普通消息。显式批准仍可走 卡片按钮(不受开关限制),WebUI plan_decision 通道同理。 测试:plan-mode-tools 补 ChatPage 源码门禁断言;GUI 2467/2467、 tsc 零错误、biome 基线持平。 Co-authored-by: Cursor <cursoragent@cursor.com>
Closes #571
概述
按 2026 H2 能力路线图 交付 P1 前两项:Plan Mode(只读规划→审批→自动执行)与 ToolSearch(MCP 工具懒加载)。每项均附设计文档与完整测试。
P1-① Plan Mode(设计文档)
ExitPlanMode内置工具:挂起/落定/超时(10min 按未批准)/中止模型,与 AskUserQuestion/工具审批同构;批准 → 自动入队"开始执行"续轮,拒绝 → 带反馈留在计划模式继续完善resolveToolGate后备拦截兜旁路;子代理强制 readonly(显式 worktree 按参数错误拒绝,fail-closed 不静默降级)ChatRuntimeControls.planModeEnabled:归一化仅显式 true 生效;跨端合并只能收紧(同strictestCommandSafetyMode取向)+菜单(「添加」分节:文件和文件夹 / 计划模式),开启后显示可点击关闭的「计划」pill;计划卡任务卡化直出(不折叠、自带头部/状态/倒计时)chat_queue.plan_decision(action 字符串 + request_json,零 proto/Go/Rust 改动——三层 relay 均已核实透传)P1-② ToolSearch / MCP 懒加载(设计文档)
filterRequestTools每轮重估——「执行层可见、请求层隐藏」即可轮中激活,零上游依赖风险。附真实 pi-agent-core 集成测试requestToolFilter参数,复用 provider 原生搜索的既有隐藏机制附带修复:历史库持久化全灭(实测中发现)
1.x 老库遗留的
chatHistory.context_json(NOT NULL 无默认值)在分段持久化上线后已无写方,但CREATE TABLE IF NOT EXISTS不改建既有表、列 ensure 只加不删——老库每次 INSERT 被 NOT NULL 约束拒绝:聊天历史/任务清单持久化全部失败,轨迹分段随之因外键刷屏报错。v4 迁移 DROP COLUMN + 迁移测试;实机验证修复后正常落库。测试与基线
已知边界(详见各设计文档)
ChatRuntimeControls.plan_mode_enabled字段 + buf codegen;远程审批已完整可用)Screenshots / preview
+菜单)计划卡(任务卡风格直出,批准/要求修改 + 倒计时):
端到端实测(批准 → 本轮即终止 → 队列自动发出执行续轮 → 文件落地):