diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 51e9ce80..3df8e39a 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -2914,3 +2914,19 @@ - 防复发:任何服务器端创建 `birth_time_rectification` Session 的路径都必须在同一事务内得到可解析的持久化模型与版本;前端显示的默认模型不能替代数据库绑定。 - 相关记录:BUG-060、BUG-163、BUG-168 - 修复版本:待提交 + + +## BUG-170 | V9 Agent 开场未收到 Case ID 导致工具调用失败 + +- 状态:resolved(本地候选) +- 首次发现:2026-08-11 +- 最近更新:2026-08-11 +- 影响面:`POST /api/rectification/agent` 的 opening、message、read-only Agent 消息,以及所有要求 `caseId` 的 V9 rectification 工具调用。 +- 用户现象:接口已返回 `200 application/x-ndjson`,Skill 与 Case 均已加载,但回答正文报告 `invalid_case_id`,最后事件为 `run.failed`。 +- 触发条件:Agent 按指令调用 `rectification-read-case`,但服务端构造的 Agent 消息没有提供当前 Case ID。 +- 根因:请求路由和 `runV9AgentTurn` 已验证 Case/Session 绑定,但 `buildAgentMessages` 只传时间与用户消息;模型只能猜测工具所需的 `caseId`。 +- 修复:在共享 Agent 消息构造处加入服务端已验证的唯一 Case ID,并明确所有 rectification 工具必须原样使用;不放宽 UUID、所有权或 Case/Session 绑定校验。 +- 验证:新增回归测试捕获实际传给 Agent 的 opening 消息,要求包含精确服务端 Case ID;staging 需以原请求确认流以 `run.completed` 结束。 +- 防复发:任何由模型调用、但值由服务端拥有的工具引用,都必须在 Agent 上下文中显式提供,不能要求模型猜测。 +- 相关记录:BUG-163、BUG-169 +- 修复版本:待提交 diff --git a/frontend/src/lib/rectification-agentic/v9/agent-run.ts b/frontend/src/lib/rectification-agentic/v9/agent-run.ts index ea52be3b..5c7fd92d 100644 --- a/frontend/src/lib/rectification-agentic/v9/agent-run.ts +++ b/frontend/src/lib/rectification-agentic/v9/agent-run.ts @@ -337,18 +337,20 @@ function buildAgentMessages(options: V9AgentRunOptions, _attempt: number): unkno void _attempt; const timeContext = options.timeContext ?? `服务端当前时间(权威):${new Date().toISOString()}。涉及“现在、今天、今年、未来几个月”等相对时间时,以此为准。`; + const caseContext = `【服务端 Case ID】${options.caseId}。所有 rectification 工具调用的 caseId 必须原样使用此值。`; if (options.action === "opening") { return [{ role: "user", content: [ timeContext, + caseContext, "【服务端开场指令】这是本校正 Case 的首次开场,还没有用户输入。请先调用 skill 工具加载 jyotish-birth-time-rectification,再调用 rectification-read-case 读取服务端 Case 与证据摘要,然后用简体中文自然开场:说明你会通过已发生的人生事件来校正出生时间,并自然地提出第一个最有用的问题(只需一个问题)。", ].join("\n"), }]; } return [{ role: "user", - content: [timeContext, options.message ?? ""].join("\n"), + content: [timeContext, caseContext, options.message ?? ""].join("\n"), }]; } diff --git a/frontend/tests/rectification-v9-agent.test.ts b/frontend/tests/rectification-v9-agent.test.ts index dd23b05a..a4d3fa69 100644 --- a/frontend/tests/rectification-v9-agent.test.ts +++ b/frontend/tests/rectification-v9-agent.test.ts @@ -173,6 +173,35 @@ function runOptions(overrides: Partial = {}): { return { options: optionsValue, emitted, billing }; } +test("agent receives the exact server-owned case id for tool calls", async () => { + let observedMessages: unknown[] = []; + const agent = fakeAgentStream([ + chunk("start"), + chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }), + chunk("tool-result", { toolName: "skill" }), + chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }), + chunk("tool-result", { toolName: "rectification-read-case" }), + chunk("text-delta", { text: "你好,我是生时校正助手。" }), + chunk("finish"), + ]); + const { options } = runOptions({ + action: "opening", + message: null, + buildAgent: async () => ({ + ...agent, + stream: async (messages: unknown[]) => { + observedMessages = messages; + return agent.stream(); + }, + }) as never, + }); + + const result = await runV9AgentTurn(options); + + assert.equal(result.ok, true); + assert.match(JSON.stringify(observedMessages), new RegExp(CASE_ID)); +}); + test("first turn with no real skill evidence retries once then fails without saving success", async () => { const { options, emitted, billing } = runOptions({ accounting: fakeAccounting({