Files
Jyotisha/docs/tasks/TASK-consult-followup-tool-contract-20260917.md
T

127 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 任务书 · 咨询追问轮不调排盘工具即整轮失败:提示词与运行合同对齐(2026-09-17)
## 0. 基线
- 基线 commit:`1b508d5e`(`origin/staging` head)。
- 分支:`codex/consult-followup-tool-contract-20260917`,`git worktree add -b codex/consult-followup-tool-contract-20260917 .worktrees/consult-followup-tool-contract-20260917 origin/staging`。
- 范围:只有前端(`frontend/src/mastra/index.ts`、`frontend/src/mastra/consultation-tools.ts`、`frontend/src/app/api/consult/route.ts` 与对应测试)。不动 Python、不动迁移、不动 Skill、不动生时校正 Agent。
- BUG 段:**BUG-922 起**(基线 `docs/BUG_HISTORY.md` 最大号 BUG-921,开工时再核对一次)。
## 1. 事故实证
产品负责人 2026-09-17 在 staging 一个申报出生时段(`declared_birth_window`)会话里连发三轮,三轮都没有得到回答。第三轮的事件流原样如下(已去掉会话与用户标识):
| 事件 | 内容 |
| --- | --- |
| 请求 | `consultationMode: declared_birth_window`,`theme: general`,`question: 你在说什么鬼`;服务端历史只有两条用户消息「请帮我看看我家庭关系的整体模式和特点」「?」,**没有任何助手消息** |
| `skill.started` / `skill.completed` | `jyotish-vedic-astrology` |
| `activity` | `phase: loading-method`,「正在补齐方法与计算步骤」 |
| `run.failed` | `code: runtime_contract_incomplete`,「Agent 未完成必要的方法与计算步骤,本次不会扣点。」 |
| 回执 `steps` | 只有两步:`skill` 与 `validation: runtime-contract-retry`,**没有任何 `tool` 步骤** |
| 回执 `workflow` | `route: declared-birth-window`,`status: blocked`,`techniqueTruth: declared-window` |
代码定位(按符号):
- 门禁:`frontend/src/lib/stream-agent-response.ts` 的 `contractReady()`。`requireTool` 为真时要求 `consultationToolCompleted && consultationToolSuccessCount === 1`。第一轮不满足则调用 `options.retry()` 再跑一轮,仍不满足即 `throw new Error("runtime_contract_incomplete")`,模型已写出的文字全部丢弃(`consumeAttempt` 在契约未绿时不释放文本,见 BUG-286 与测试「text written before the contract completes is dropped」)。
- 申报时段路线:`frontend/src/app/api/consult/route.ts` 中 `shouldRunDeclaredWindowWorkflow` 分支调用 `streamAgentResponse({ requireTool: true, retry, ... })`,`retry` 的追问原文是「运行合同不完整:本次尚未取得声明窗口计算结果。请调用 run-jyotish-window-consultation 完成计算,再据此回答」。
- 窗口 Agent 系统指令:`frontend/src/mastra/index.ts` 的 `windowJyotishInstructions`,含这一句:**「For questions that need personal chart structure, call run-jyotish-window-consultation before answering. Simple conversational follow-ups may use the existing packet.」**
- 本命 Agent 系统指令:同文件 `jyotishInstructions`(第 17 行起,含 `natalSpokenReportContract` 之后的第 6 行)含同构的一句:**「For questions that require a new chart claim, call run-jyotish-consultation before answering. Simple conversational follow-ups may use the existing context.」**
- 工具选择:两条路线都是 `toolChoice: "auto"`。本命路线 `consultationNatalPrepareStep`(`frontend/src/mastra/consultation-tools.ts`)在第 0 步只把 `activeTools` 收窄到排盘工具,但没有强制调用;申报时段路线用的是不带 `prepareStep` 的 `streamOptions`。
- 历史为什么只剩用户消息:用户消息在扣点预留 RPC 里就写库(`route.ts` 的 `p_question_message: { role: "user", text: visibleQuestion }`),运行失败不补助手消息。所以前两轮「家庭关系」「?」也都失败了。
## 2. 根因
**系统提示词与运行合同互相矛盾。** 提示词明确允许模型对「简单的对话式追问」不调排盘工具、复用「已有 packet / context」;服务端合同却要求每一次请求都必须恰好一次成功的排盘工具调用,否则整轮作废。像「?」「你在说什么鬼」这类追问,模型按提示词判断为对话式追问、直接用文字回答,门禁判定合同未完成;服务端追问一句后模型仍按同一套系统指令行事,第二轮也不调工具,于是 `runtime_contract_incomplete`。
提示词里的「existing packet / existing context」还是**事实错误**:排盘结果只在单次请求内缓存(`createWindowConsultationTools` / `createConsultationTools` 的闭包变量 `calculation`),跨请求根本没有可复用的 packet。模型信了这句话就会跳过工具。
这两句是 2026-08-21 提交 `9958e00a` 写进去的,门禁是 2026-08-17 提交 `7886629b` 定的,两者从未对齐。Bug 历史里 `runtime_contract_incomplete` 的既有记录(BUG-205 缓存拒绝、BUG-214 失败次数计入门禁、BUG-286 契约未绿文本丢弃)都不是这个原因,防复发措施均仍在,本单是新问题,不是复发。
第一轮「请帮我看看我家庭关系的整体模式和特点」是正经解盘问题,它为什么失败**本单不下结论**,见 T4。
## 3. 决策记录
- 产品负责人 2026-09-17 拍板 **方案 1**:删掉「简单追问可以不调工具」的例外,**每一轮回答前都必须调用排盘工具**。理由:工具在单次请求内有缓存、代价小;每轮都有当轮证据,符合「解盘必须有原始数据」红线。
- **否决方案 2**(保留提示词、让没调工具的轮次走不扣点的纯对话回复):追问轮会没有星盘证据,与 AGENTS.md Part B 的证据要求冲突。
- 不推翻任何既有红线:`contractReady()` 的「恰好一次成功调用」口径不放宽;BUG-214 允许失败尝试后重试成功的语义保持;BUG-286 契约未绿即丢弃文本的语义保持。
- 生时校正 Agent(`agentic-rectification.ts`)不在本单范围,它有自己的合同。
## 4. 硬红线
1. 不改 `contractReady()`;不改 `retry` / `retryForAnswer` 的触发条件;不给失败轮扣点。
2. `page.tsx` 与 `jyotish_api_server.py` 不动(AGENTS.md §6)。
3. 既有测试断言不得静默弱化;改任何断言写「原值 / 新值 / 原因」三栏。
4. 不顺手改 Skill 文件、不 bump Skill 版本(本单只改 Agent 系统指令与 SDK 调用参数)。
5. Bug 历史与进度记录不得写入会话 ID、用户 ID、出生资料。
## 5. 任务分解
### T1 删掉两处「简单追问可跳过工具」例外(BUG-922)
- `windowJyotishInstructions`:把「For questions that need personal chart structure, call run-jyotish-window-consultation before answering. Simple conversational follow-ups may use the existing packet.」改为「Call run-jyotish-window-consultation before answering **every** turn, including short follow-ups, clarifications, and complaints; the packet is request-scoped and is never carried over from an earlier turn.」
- `jyotishInstructions`:同样改「For questions that require a new chart claim… Simple conversational follow-ups may use the existing context.」为「Call run-jyotish-consultation before answering every turn, including short follow-ups; the calculation is request-scoped and is never carried over from an earlier turn.」
- 全文再 grep 一次 `follow-ups may use`,确认 `frontend/src` 无残留。
- 验收:
- 在 `frontend/tests/consultation-birth-time-mode.test.ts`(它已经读窗口 Agent)或 `consultation-agentic-runtime.test.ts` 加契约:两段指令 `doesNotMatch(/follow-ups may use the existing/)` 且 `match(/before answering every turn/)`。
- 既有「personal Agent exposes the Jyotish Skill and named server tool」等测试原样通过。
### T2 第 0 步强制调用排盘工具(BUG-923)
提示词只是软约束,门禁是硬约束,中间应该有一层确定性的 SDK 参数把二者钉在一起。
- `consultationNatalPrepareStep`:第 0 步在 `activeTools: [CONSULTATION_NATAL_CALC_TOOL_ID]` 之外把 `toolChoice` 改为 `"required"`;后续步骤保持 `"auto"`。
- 申报时段路线:新增 `consultationWindowPrepareStep`(同文件、同形状:第 0 步 `activeTools: ["run-jyotish-window-consultation"]` + `toolChoice: "required"`),并在 `route.ts` 的申报时段分支把 `streamWithOverflowRetry(agent)` 与 `retry` 改为使用带该 `prepareStep` 的选项对象(照本命分支 `natalStreamOptions` 的写法建 `windowStreamOptions`)。
- `retryForAnswer` / `continueAfterLength` / `composeSection` 不带强制(它们是在合同已绿之后跑的,本命分支的 `composeSection` 明确 `toolChoice: "none"`,不动)。
- 开工先确认本仓 Mastra / AI SDK 版本的 `prepareStep` 接受 `toolChoice: "required"`(查 `node_modules/@mastra/core` 与 `ai` 的类型定义,写进进度记录)。
- 验收:
- `consultation-agentic-runtime.test.ts` 既有「natal first step exposes only the chart calculation tool」扩成断言第 0 步 `toolChoice === "required"`、第 1 步 `"auto"`;为窗口 `prepareStep` 加对称的一条。
- 加一条 `route.ts` 源码契约:申报时段分支的 `agent.stream(` 调用与 `retry` 使用带 `prepareStep` 的选项(对齐 `consultation-stream-recovery.test.ts` 那类结构断言的写法)。
- 已有的「a calculation that succeeds only after failed attempts still satisfies the contract」「text written before the contract completes is dropped」原样绿。
### T3 记录
- `docs/BUG_HISTORY.md` 新增 BUG-922、BUG-923:现象、触发条件(申报时段 / 本命模式下发短追问)、根因(提示词例外 vs 合同)、修复、验证、防复发(契约测试 + 第 0 步 required)、关联 BUG-205 / 214 / 286。
- `CHANGELOG.md` 一条:追问、反问、短句也会先重新取回本轮星盘证据再回答,不再整轮失败。
- `docs/tasks/PROGRESS-consult-followup-tool-contract-20260917.md`:测试数字、失败清单与基线逐条比对、gzip 前后、`prepareStep` 类型核对结果。
### T4 第一轮失败原因取证(不占 BUG 号,写进进度记录)
第一轮是正经解盘问题,不能用 T1 的根因解释。回执只写到 web 容器控制台(`frontend/src/lib/agent-observability.ts` 的 `console.info("[agent-observability]", …)`),不进数据库。执行方**做不了**(无 VPS 登录态),把下面的取证方法原样写进进度记录,留给产品负责人或部署侧:
```bash
# 在 VPS 上,以 deploy 用户
cd /opt/jyotisha-production
docker compose -f deploy/docker-compose.server.yml logs web --since 2026-09-17T00:00:00 \
| grep agent-observability | grep '<该会话的 sessionId>' | head
```
三条 `run.failed` 各自的 `steps` 若都没有 `tool` 步骤,即同一根因、本单覆盖;若第一轮有 `tool` 步骤且 `status: failed`,另立单,不得并进本单。
## 6. 让步顺序
1. T2 若本仓 SDK 版本的 `prepareStep` 不接受 `toolChoice: "required"`(类型或运行时报错),退而只做 T1,并在进度记录与 `BLOCKED.md` 写明版本与报错原文;不得顺手升级依赖。
2. T2 若 `required` 让本命第 0 步在 `generalDailyContext`(首页今日)路径误触发(那条路径 `requireTool: false`,用的是 `getGeneralJyotishAgent`,理论上不经过 `natalStreamOptions`),先用测试证明,再决定是否只对申报时段路线加 required。
3. T1、T3 不可让步。
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/consult-followup-tool-contract-20260917 .worktrees/consult-followup-tool-contract-20260917 origin/staging
cd .worktrees/consult-followup-tool-contract-20260917/frontend
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -20 # 记下基线总数与失败清单
grep -o "BUG-[0-9]\+" ../docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1 # 应为 BUG-921
grep -rn "follow-ups may use" src # 应恰好 2 处
```
验收口径:`tsc` 0 错;lint 0 error;`npm test` 失败清单与基线逐条一致(无 Docker 的那组不算新增)且总数不减;`next build` 后 `/` 仍 Static;首屏 gzip ±2%(本单不动客户端 bundle,应为 0 变化)。
## 8. 环境缺口(留给产品负责人真机)
部署后(先核对 `/api/health` 的 `deployment.gitCommit` 等于本单合入 staging 的 SHA)在申报时段模式的会话里:
1. 先问一个正经问题(例如「看看我事业上的整体特点」),应正常出回答。
2. 紧接着发「?」,再发「你在说什么鬼」。两轮都应先出现「正在比较声明出生窗口内的稳定层」再出回答,**不得**出现「Agent 未完成必要的方法与计算步骤」。
3. 换到本命模式(有出生分钟)的会话重复第 2 步,同样不得整轮失败。
4. 同一会话刷新页面后,历史里应能看到助手回复,不再是只有用户消息。