Skip to content

feat(acp-bridge): session/new 可选 model 锁定会话模型 + setModel 对象形态修复 (0.16 实测) - #52

Merged
tizerluo merged 2 commits into
tizerluo:mainfrom
jasonQin6:feat/acp-session-model-selection
Oct 3, 2026
Merged

tizerluo merged 2 commits into
tizerluo:mainfrom
jasonQin6:feat/acp-session-model-selection

Conversation

@jasonQin6

Copy link
Copy Markdown
Contributor

问题(0.16.1–0.16.9 实测, App 3.14.0/CLI 0.16.9)

1. setModel 透传形态与 0.16 后端 schema 不匹配

桥的 _on_session_set_model 发 {sessionId, modelId: "<string>"},而 0.16 后端 zod schema 要 model 对象,实测必报:

-32602 Invalid params — model: Invalid input: expected object, received undefined;
(root): Unrecognized key: "modelId"

即 0.16 时代 ACP 面的 session/setModel 从未真正可用(即使参数名对上,注册表里没有该 provider 也会报 Provider Registry 中不存在 Model)。

2. client 无法按会话指定模型(headless 刚需)

headless spawn 的 app-server,模型解析跟随 personal provider registry / App「上次使用」默认;config.json 静态 provider 不进注册表,凭证 env 三件套在 personal registry 存在时被无视。结果:App 里选了什么模型,脚本/编辑器拉起的会话就默认用什么,client 无从覆盖。

修复/新增

  • setModel:改为 {sessionId, model} 原样透传 ModelSelection 对象 {providerId, modelId, options?: {reasoningLevel}}。旧 modelId 字符串形态移除(在 0.16 后端从未通过 schema,保留只会让调用方误以为已生效)。
  • session/new 新增可选 model:
    • 字符串形态("GLM-5.3"):按 create 快照 settings.model.available catalog 解析,补 reasoningLevel = reasoning.defaultLevel;
    • 对象形态:原样透传;
    • create 成功后经 session/setModel 应用,失败则 session/new 整体报错(客户端明确知道模型没生效)。
    • 刻意不透传给 create 本身:0.16.9 实测 create 的初始 model 形参接受对象但丢 options,turn 阶段报 ModelProtocolError: Reasoning level is required for <provider>/<model>;setModel 路径无此问题。

实测(ZCode App 3.14.0 / CLI 0.16.9, personal provider bigmodel-api)

场景 结果
session/new {model: "GLM-5.3"} → session/prompt stopReason=end_turn,流式文本正常
session/new 素建 + session/setModel {model: {providerId, modelId, options:{reasoningLevel:max}}} → prompt setModel ok,turn end_turn
setModel 缺 options(reasoning 必需的模型) turn 阶段 ModelProtocolError(与 catalog defaultLevel 补齐后通过)
旧 {modelId} 形态 桥本地 -32602(替代原先透传后端必败)

测试

  • M5 改写:model 对象透传 / 缺参与非法形态 -32602(含旧 modelId 形态显式拒绝)
  • 新增 C4 系列 7 例:字符串 catalog 解析(补 defaultLevel)/无 reasoning 条目/对象透传/未知 modelId 不发 setModel/setModel 失败连带 new 失败/不带 model 向后兼容(零 setModel 调用)/非法类型
  • python3 -m pytest tests/ -q → 547 passed, 34 subtests passed

兼容性说明

  • {modelId} → {model} 是破坏性 ACP 参数变更,但旧形态对 0.16 后端本来就 100% 失败,无实际可用性损失;≤0.15 后端未验证(0.16 用户不受影响,老版本用户如需可反馈再兼容)。
  • 不带 model 的 session/new 行为逐字节不变(C4e 锁定)。

… 实测)

问题 (0.16.1-0.16.9 实测):
1. setModel 透传形态与 0.16 后端 schema 不匹配: 桥发 {sessionId, modelId}
   字符串, 后端要 model 对象 → 必报 -32602 "expected object, received
   undefined; Unrecognized key: modelId"。即 0.16 时代 setModel 从未真正
   可用, ACP client 无法切换会话模型。
2. headless 场景模型解析跟随 personal provider registry / App「上次使用」,
   client 无法按会话指定模型: App 里选了什么, spawn 出的引擎就默认用什么
   (provider 目录不含 config.json 静态 provider, env 三件套在 personal
   registry 存在时被无视)。

修复/新增:
- setModel: 改为 {sessionId, model} 原样透传 ModelSelection 对象
  {providerId, modelId, options?: {reasoningLevel}}。旧 modelId 形态移除
  (在 0.16 后端从未通过 schema, 保留只会让调用方误以为已生效)。
- session/new 新增可选 model:
  * 字符串形态 ("GLM-5.3"): 按 create 快照 settings.model.available 的
    catalog 解析, 补 reasoningLevel=reasoning.defaultLevel;
  * 对象形态: 原样透传;
  * create 成功后经 session/setModel 应用, 失败则 session/new 整体报错。
  刻意不透传给 create 本身: 0.16.9 实测 create 的初始 model 形参接受对象
  但丢 options, turn 阶段报 ModelProtocolError "Reasoning level is
  required for <provider>/<model>"; setModel 路径无此问题。

实测 (ZCode App 3.14.0 / CLI 0.16.9, personal provider bigmodel-api):
- session/new {model: "GLM-5.3"} → session/prompt → stopReason=end_turn;
- setModel {model: {providerId, modelId, options:{reasoningLevel:max}}} →
  ok → prompt → end_turn;
- setModel 缺 options (无 reasoning 的模型除外) → turn 失败, 与 catalog
  defaultLevel 补齐后通过。

测试: M5 改写 (对象透传/缺参 -32602/旧形态拒绝) + C4 系列 7 例 (字符串
catalog 解析/无 reasoning 条目/对象透传/未知 modelId/setModel 失败连带
new 失败/不带 model 向后兼容/非法形态)。547 passed + 34 subtests。
@tizerluo

tizerluo commented Oct 3, 2026

Copy link
Copy Markdown
Owner

感谢贡献——这是本仓库第一个外部 PR,先说结论:质量很高,修后可合并。以下是维护者侧的独立审查(含活体验证);另有一条来自独立代码审查 agent 的意见见下一评论。

核心声明逐项活体验证(本机 App 3.14.4 / CLI 0.16.9,官方 app-server)

声明 验证方式 结果
旧 {modelId: string} 形态 0.16 后端必拒 活体 create 后发 setModel {modelId} ✅ 实测 -32602 ZodError: model expected object,与 PR 描述逐字吻合
新 {model: {providerId, modelId, options}} 形态可用 活体从 catalog 取条目下发 ✅ 接受,返回完整会话快照
create 快照 settings.model.available 形态与 resolver 匹配 活体 dump catalog ✅ 本机 470 条,{ref:{providerId,modelId}, reasoning:{defaultLevel}, ...} 与 _resolve_session_model 的读取路径完全一致
测试声明 本地 worktree 实跑 ✅ 547 passed(unittest discover),ruff 干净;与 main 基线 543 + 7×C4 − 3(security_review 基线差)对得上

另外确认:PR 与现行 main(cbc35ab)无冲突(MERGEABLE)。

意见

应修(合并前):

  1. 错误路径的孤儿会话(同下一评论的 P1,我独立读到同一点):session/create 成功、session_map 注册之后才做 model 归一/setModel——参数形状错误(C4f 那类)也会先真实创建后端会话再返 -32602;catalog 未命中/setModel 失败同样遗留活会话,而客户端拿不到 sessionId 无从回收,重试会累积。建议最小修:形状校验前移到 create 之前;session_map 写入移到 model 应用成功后;create 之后才失败的路径在错误信息里带上已创建的 sessionId。
  2. 文档三处漏同步:agent-help 的 key_methods 仍写 session/setModel {sessionId, modelId}(用户照它直连 app-server 会复现 -32602);README 版本兼容性表未给这条破坏性变更加注(≤0.15 未验证);按仓库惯例建议把三条实测证据落一份进 docs/(复测文档目前只有「空 params -32602 即存活」的方法论,恰好掩盖了 schema 形态问题——这个坑值得记档)。

建议:

  1. 同名 modelId 跨 provider 的歧义在真实环境成立:本机 catalog 470 条、同一 modelId 出现在多个 provider 下(如账号级与自定义各一份)。「字符串取第一个匹配」docstring 已声明,但哪个 provider 排先是注册表决定的,调用方无法预期——建议 README 也写明,并引导需要精确控制的调用方直接传对象形态。
  2. 测试小缺口:C4/C4a/C4b 可补 params["sessionId"] 断言;C4e 可断言 create 参数与旧基线完全一致(真正锁住「逐字节不变」)。

与仓库现状的呼应

你的问题陈述(「凭证 env 三件套在 personal registry 存在时被无视、模型跟随 App 上次使用」)与我们在 #55 的独立调研结论完全一致——这是同一问题的 app-server 侧视角。你给出的会话级解法(session/new 锁定模型)与 #55 的配置级引导互补,正好补上 ACP 调用方的刚需。

合并顺序:建议本 PR 先行(#53 叠在其上,需要 rebase)。

@tizerluo

tizerluo commented Oct 3, 2026

Copy link
Copy Markdown
Owner

独立代码审查(code-reviewer agent)意见——由维护者代发,与上一条维护者审查互不参考。

结论:⚠️ 需小改(无 P0;1 条 P1 错误路径处置 + 4 条 P2 文档/测试同步)

P1(应修)

  • packages/acp-bridge/zcode-acp-bridge:1349-1387 — 错误路径会产生孤儿会话且客户端拿不到 sessionId。session/create 先执行、session_map 先注册,之后才做 model 归一/应用;于是:
    • 纯参数形状错误(C4f 的 {}/""/42)也会先实打实创建一个后端会话再返 -32602——这类校验完全可以提到 create 之前;
    • 未知 modelId、setModel 失败、setModel 超时三条路径同样留下一个活的后端会话(持久化、出现在 session/list),而返回体只有 error,客户端不知道 sessionId 无法回收,每次重试再积一个;
    • self.session_map 也留下一个客户端从未见过的映射项。
      建议修法(按成本从低到高):① 形状校验前移到 create 之前;② session_map 注册移到 model 应用成功之后(session/prompt 有 or acp_sid 兜底,移位无副作用);③ create 之后才失败的路径在错误信息带上已创建的 sessionId(如「已创建会话 <sid> 但 model 未生效: …」)并在 README 写明该已知行为;④ 未来后端暴露会话删除方法时在此分支加回收。

P2(建议)

  1. README 版本兼容性表未随破坏性变更同步(README.md:310-322):PR 自述「≤0.15 后端未验证」,但兼容性章节仍是「对旧版完全向后兼容」、0.15.x 行仍「✅ 全部」。建议加注(可选设计:桥已记录 protocol_version,可对无 protocol 块的旧协议保留旧形态透传)。
  2. agent-help 第三处文档面漏改(zcode-agent-help:537):key_methods 仍写 session/setModel 参数 sessionId, modelId——正是本 PR 否定掉的形态,用户照 agent-help 直连 app-server 会复现 -32602。
  3. docs/ 缺实测落档(仓库惯例):三条核心证据(旧形态 -32602 原文、create 初始 model 丢 options、catalog 形态)只在 PR 描述/代码注释/测试 fixture 里。docs/recheck-3.14.0.md §5 的「空 params 返 -32602 即存活」方法论恰好掩盖了 schema 问题,建议补一段「存活性判定 ≠ 参数形态验证」+ 本次探测结果。
  4. 测试缺口:未覆盖 setModel 后端返 -32601 的分支(_passthrough_error 首次从核心路径 handler 内被调用,会得到「已移除」文案,与 D7 钉死的行为是刻意区分,值得显式钉住);C4/C4a/C4b 未断言 params["sessionId"];C4d 未断言文案可区分「setModel 这一步失败」;C4e 建议断言 create 参数与旧基线完全一致。

P3(提示)

  • 同一参数两种错误码语义:字符串形态失败桥本地返 -32602,对象形态的后端 -32602(如缺 providerId)被 _passthrough_error 改写成 -32603(既有约定)——调用方按码区分「参数错」在对象形态上失效;最小修法是桥侧校验 providerId/modelId 非空字符串 → -32602,或文档写明。
  • _resolve_session_model 假定 catalog 条目为 dict,后端给非 dict 条目会 AttributeError(主循环兜成 -32603 带异常原文,提示很差);加 isinstance 守卫即可。
  • 旧协议/无 catalog 的 create 结果下,「不在本会话的模型目录中」文案会误导(根本没有目录);可区分「catalog 不可用」与「未命中」。
  • _passthrough_error docstring 的「只用于扩展/透传 handler」边界描述已过时(session/new 现在也调它),建议补一句。
  • setModel docstring 建议「改用 session/new 的字符串形态」对已存在的会话不可执行;可考虑按 session 缓存 create 快照让 setModel 也支持字符串(后续增量),或改写措辞。
  • README/SKILL.md「补默认 reasoningLevel」是绝对化表述(代码只在 defaultLevel 存在时补,正确);SKILL.md 的脚本示例段未展示新的可选 model。

亮点

  • 关键不变量被测试钉死:C4 用 assertNotIn("model", create_params) +「恰好一次 setModel」同时锁住「不透传给 create」和「必须走 setModel」——这正是后续重构最容易被打回的点。
  • 失败即报错、不静默降级的设计与 C4d/C4e 配套;C4e 保住老调用方行为。
  • 注释写的是「代码看不出来的约束」(create 丢 options、为何走 setModel、catalog 语义),符合仓库风格。
  • 声明可信:静态统计 547 个测试与作者自报一致;docs/recheck-3.14.0.md 只做过空参存活性探测,恰好解释「旧 schema 从未验证」,无内在矛盾。

未发现问题的维度:安全性(ModelSelection 不含凭证,无新增泄漏面);性能(带 model 的 new 多一次 15s 上限 RPC,无退化);并发(桥主循环串行,无新竞态)。


给贡献者的一句话:这是一份质量高于平均线的贡献——问题定位有实测原文、修复方向克制、文档/测试同步意识强。剩余问题集中在错误路径的会话生命周期处置和三处文档面,均为小改;改完即可合并,欢迎继续贡献。

在 jasonQin6 PR tizerluo#52 基础上按 R1 审查清单修复, 保持原方案 (setModel 对象
形态 + session/new 可选 model) 不变, 只修审查指出的问题。

A (P1 孤儿会话):
- model 形状校验前移到 session/create 之前 — 非法形态本地 -32602 且不
  触碰后端, 不再先真实建会话再报错;
- create 后失败 (catalog 未命中/不可用、setModel 失败、setModel 超时)
  的错误文案带已创建的 sessionId (「已创建会话 <sid> 但 model 未生效:
  <原因>」) 供客户端收编; 后端无会话删除方法, 不尝试回收;
- session_map 注册后移到 model 应用成功之后, 失败会话不登记、不能被
  session/prompt 使用。消费方确认: session/prompt/session/cancel 用
  .get(acp_sid) 严格取, 扩展 handler 用 .get(acp_sid) or acp_sid 兜底;
  acp_sid 恒等于 zcode_sid 且登记先于响应返回, 移位对成功路径无行为差异;
- _on_session_new docstring 同步失败语义。

B (P3):
- 对象形态 providerId/modelId 须为非空字符串 (本地 -32602, 消除透传后端
  -32602 被 _passthrough_error 改写成 -32603 的不一致);
- catalog 条目/ref/reasoning 加 isinstance 守卫 (畸形条目不抛 AttributeError);
- 「catalog 不可用」与「未命中」文案区分;
- _passthrough_error 加 prefix 参数 + docstring 注记 session/new 嵌套
  setModel 是核心 handler 走本 helper 的例外场景;
- _on_session_set_model docstring 改写 (需要按 catalog 解析的调用方应在
  session/new 指定; 本方法要求调用方提供完整对象)。

C (文档):
- README 版本兼容性加注: setModel 对象形态仅 0.16.x 验证 (0.16.9 实测),
  ≤0.15 未验证;
- README/SKILL 的 session/new 说明: 「补默认 reasoningLevel」条件化为
  「catalog 条目带 reasoning.defaultLevel 时补」, 补 model:null 语义;
- agent-help key_methods: setModel 参数改对象形态 + 0.16 注记;
- docs/recheck-3.14.0.md 追加补遗 (旧形态 -32602 原文/新对象形态实测/
  create 快照 catalog 形态/空参探存活的方法论教训)。

D (测试 547→551):
- C4/C4a/C4b 补 sessionId 断言; C4d 补 setModel 文案 + 超时分支 + 失败
  不登记; C4e 锁定 create 参数逐字段不变;
- 新增 C4g (嵌套 setModel -32601 → 「已移除 (session/setModel)」文案)、
  C4h (对象缺字段 → 本地 -32602 且不发起后端调用)、C4i (catalog 不可用
  文案)、C4j (畸形 catalog 条目不抛);
- C4c/C4f 补「不发起 create」「不登记 session_map」断言。

验证: python3 -m unittest discover -s tests → 551 passed; ruff check 干净。
@tizerluo
tizerluo merged commit 79dbbe7 into tizerluo:main Oct 3, 2026
@tizerluo

tizerluo commented Oct 3, 2026

Copy link
Copy Markdown
Owner

感谢这份高质量贡献,也先道个歉:这两个 PR 在仓库里搁了近两周才被我们看到——是我们的问题,不是你的问题;为了不再耽误,我们在你的分支上直接代为修复后合并了(你勾选了「允许维护者修改」,固此路可行)。

处理经过

  1. 两轮独立审查(维护者 + 独立代码审查):核心结论是「修后可合并」——问题定位实测扎实、修复方向克制、测试纪律好;
  2. 按审查意见代修(提交 a28f1b5,在你的 7855e19 之上):孤儿会话处置(形状校验前移到 create 之前、session_map 注册后移到 model 生效后、create 后失败路径错误信息带上已创建的 sessionId)+ 文档三处同步(README 兼容表加注、agent-help key_methods、docs/recheck-3.14.0.md 补遗——顺带把你发现的「空 params 探存活 ≠ 参数形态验证」这个方法论坑记了档)+ 测试补强(-32601 分支/sessionId 断言/逐字段基线等);
  3. 修复后活体验证(真实 app-server 0.16.9):旧 {modelId} 形态 -32602 实锤、新对象形态可用、catalog 形态与 resolver 完全一致;已合并。

贡献记录

你的提交 7855e19 原样保留在 main 历史中(合并提交 79dbbe7),作者署名完整。session/new 的 model 参数与 setModel 对象形态修复均以你为源头记档(docs 补遗与提交历史可查)。

你发现的问题(「凭证 env 在 personal registry 存在时被无视、模型跟随 App 上次使用」)与我们同期在 #55 的独立调研结论完全一致——你是从 app-server 侧独立撞出来的,这个视角非常宝贵。欢迎继续贡献;后续 PR 我们不会再让它们晾着了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants