docs(rectification): add question-in-message task brief
Product decision 2026-09-02: every rectification question lives inside the latest assistant message (body + agent-authored stem + server options); the question slot is removed. Records which BUG-441/461/471/485/487/488 anti-recurrence clauses are revoked and which stay. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# 任务书 · 题目进消息:一条助手消息承载正文+题干+选项,Agent 自己写题干,删除问题槽(2026-09-02)
|
||||
|
||||
基线:`origin/staging` @ `33d55b3a`(含 BUG-480~488 全部)。
|
||||
|
||||
## 0. 决策记录(产品负责人 2026-09-02 拍板)
|
||||
|
||||
产品负责人的原话:"我要的是这部分是一条消息出来,而不是一条消息加下面的卡片;这些问题不能让 Agent 直接说出来吗?那是不是就不需要这个槽了。"
|
||||
|
||||
**结论:不需要槽。** 生时校正的每一道题(采集题、区分题、前事核对)都必须作为**最新一条助手消息本身**的一部分出现——正文(Agent 承接)→ 题干(Agent 用自己的话写)→ 选项(服务端,画在同一个气泡里)。已答之后这条消息原地保留(选项变灰、标出选的那个),刷新后从 turn 重建一模一样。`.rectification-question-slot` 整体删除。
|
||||
|
||||
这次显式**废止**下列防复发条款中与"槽"或"正文不得含题干"绑定的部分(其余条款保留):
|
||||
|
||||
| 条款 | 废止内容 | 保留内容 |
|
||||
|---|---|---|
|
||||
| BUG-441 | "不得为无选项的问题新造视觉容器" | 同一焦点在所有投影里形态一致;不得用正文判断"有没有问过" |
|
||||
| BUG-461 | "题干只留在正文"/legend sr-only | 问题与已答卡共用 `--assistant-content-inset` |
|
||||
| BUG-471 | "口述采集真源是 GET 直播槽,必须在问题槽可见" | 有选项时题干必须可见;不得用正文判断"有没有问过" |
|
||||
| BUG-485 | "口述采集直播题干只由问题槽展示" | 开场正文不得并行提问;不得追加第二条 opening 题干消息 |
|
||||
| BUG-487/488 | "仅在最新正文尚未带该题干时渲染采集槽"/"模型仍不自己提问" | 题干必须在同一条已完成助手消息里、刷新后仍在;不得用 `includes`/正则扫正文 |
|
||||
| `agentic-rectification.ts` 指令第 4 条 | "题干与选项完全由结构化槽位和 UI 承担,正文不得提问" | 见 §3 新指令 |
|
||||
|
||||
**不变的红线**:问哪道题、年份、选项语义、答案→候选映射、计分,全部服务端所有;确认门恒 fail-closed;不得用正文字符串判断任何状态;Python 引擎不动;不 bump Skill 10.0.14。
|
||||
|
||||
## 1. 目标形态
|
||||
|
||||
```
|
||||
┌ 助手气泡(一条消息,一个 <article>)────────────────┐
|
||||
│ 2016 年 9 月上大学,记下了——这种带月份的节点对校正 │ ← 正文:Agent 承接(2-4 句,不提问)
|
||||
│ 特别有用。接下来想核对一下感情这边。 │
|
||||
│ │
|
||||
│ 2023 年前后,你有没有一段认真开始或结束的关系? │ ← 题干:Agent 通过工具写的句子(focus.prompt)
|
||||
│ ┌────────────────────────────────────────┐ │
|
||||
│ │ A 明确发生且时间吻合 │ │ ← 选项:服务端,choice 才有
|
||||
│ │ B 发生过但时间偏了 │ │
|
||||
│ │ C 没有这回事 D 记不清 │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────┘
|
||||
👍 👎 📋 🔄 ← 动作图标在整条消息之下
|
||||
```
|
||||
|
||||
- 采集题(`collect_spoken`)= 同一结构,没有选项框;输入框占位由 `current_question` 驱动,不变。
|
||||
- 已答:同一条消息不移动、不复制;选项禁用,选中项高亮;用户点选的那行 "A. …" 仍不作为独立用户气泡出现(保留 `isStructuredChoiceUserText` 隐藏语义)。
|
||||
- 流式期间:正文先流出;题干+选项在 `run.completed` 后随 Case 快照进入**同一条**消息(沿用 BUG-488 的 `bubbleMessage` 合成思路,扩展到 choice)。不允许在流式中出现第二个气泡或临时槽。
|
||||
|
||||
## 2. 数据合同:题目挂在 turn 上,刷新后可重建
|
||||
|
||||
现状:`agentic_rectification_conversation_focuses` 已持久化 `question_id / intent / expected_answer_schema(prompt, choice.options…) / status / asked_at`,但**没有与 turn 的链接,也没有记录用户选了哪个**。`agentic_rectification_turns` 只有 `user_message / assistant_message`。前端 `messagesFromTurns`(`rectification-agentic-chat.tsx:267`)只能重建纯文本。
|
||||
|
||||
新增(一条 migration,`frontend/supabase/migrations/`):
|
||||
|
||||
1. focuses 表加 `asked_turn_id uuid null references agentic_rectification_turns(id) on delete set null`、`answer_option text null check (answer_option is null or answer_option in ('A','B','C','D','stop'))`。
|
||||
2. 创建 focus 的 RPC(`set_v10_conversation_focus` 或现名,按符号找)接受 `p_asked_turn_id`;`resolve` RPC 接受 `p_answer_option`。
|
||||
3. dossier / GET `/api/rectification/cases/[caseId]` 的 `turns[]` 每条 assistant turn 增加 `question` 字段(null 或 `{ focus_id, question_id, kind: "choice"|"collect_spoken"|"reverse_verify", prompt, options[] | null, status, answer_option, probe_id }`),由服务端按 `asked_turn_id` 连接生成。**禁止**按 `asked_at` 与 `created_at` 时间关系推断归属。
|
||||
4. `current_question` / `choice_card` 保留为**提交契约**(`question_id`、`focus_id`、`probe_id`、`case_revision`),不再驱动任何独立 UI。
|
||||
|
||||
链接时机:
|
||||
- Agent 路径:turn 行在 run 开始就已创建(`appendV9Turn` 返回 `turnId`),工具创建 focus 时直接带 `askedTurnId`;服务端兜底(§4)同样带当前 `turnId`。
|
||||
- 点选/确定性路径:先 `persistV9DeterministicTurn` 拿到 `turnId`,再创建下一问 focus 并链接;或创建后 update。二选一,PR 说明。
|
||||
- 采用后前事核对(BUG-483 的 reverse_verify)走同一规则。
|
||||
|
||||
## 3. Agent 协议:题干由 Agent 写,但通过工具而不是正文
|
||||
|
||||
原则:**Agent 写题干,服务端出选项和计分;"是否问过"的信号是工具调用,不是正文。** 正文继续不提问——题干不是正文的一部分,而是同一条消息里正文之后的独立段落,其文本来自 focus。这样正文里永远不会出现"两句相近的问法",且不需要读正文。
|
||||
|
||||
1. `rectification-set-focus`(`mastra/rectification-v9-tools.ts:1173`)增加输入 `spokenPrompt: string`(必填,8–120 字,单句,可含一个问号)。服务端校验:不得含选项字面("A."、"选项")、不得含"唯一/确切的出生分钟"、不得含内部 token(probe/focus/candidate id 字样)与机器腔词表词条(词表在 `tests/agent-voice-copy-contract.test.ts`,抽成可复用常量)、choice 时年份/域必须与 `method_followup_plan.next_followup` 的探针一致(结构字段比对,不是正文)。校验失败 → 工具返回 `invalid_spoken_prompt`,Agent 重写一次;两次失败落服务端模板(§4)。
|
||||
2. 通过校验的 `spokenPrompt` 写入 `expected_answer_schema.prompt`(collect)或 `choice.prompt`(choice)。`projectCurrentQuestion` / `choiceCardFromCaseDossier` 已优先读这里,不用改语义;`serverOwnedChoiceCopy` 只在 Agent 没写时使用。
|
||||
3. 记证据类工具(`rectification-record-evidence-batch` / `confirm` / `revise`)的返回已带 `agentVisibleLatestProjection` 的 `current_question` 与 `method_followup_plan.next_followup`;确认返回里含**服务端已决定的下一问的结构信息**(探针 id、年份/期间、域、choice_frame 的选项文本、`why`),足够 Agent 写题干。缺什么补什么,不新造第二套投影。
|
||||
4. 系统指令(`mastra/agentic-rectification.ts` 第 4 条)改写为:
|
||||
|
||||
> 每轮在记录证据后,用 `rectification-set-focus` 的 `spokenPrompt` 写出服务端给你的下一问:用自己的话、结合用户刚说的事,问出**同一个年份/期间和同一个事件家族**;不得改年份、不得改选项含义、不得合并两道题。正文只做承接(2-4 句),不提问、不复述题干、不预告选项——题干会作为同一条消息的下一段自动出现。开场轮:先 set-focus 写采集题的 `spokenPrompt`,正文只打招呼。没有下一问(服务端返回 `next_followup=null`)时不要自拟问题。
|
||||
|
||||
`frontend/docs/VOICE.md` 同步:加 3 组"服务端探针 → Agent 题干"好/坏对照(坏:逐字复读模板;坏:改了年份;好:接着用户上一句、同年份同家族)。
|
||||
5. 只读重写(regenerate)不得改 focus;重生成后的消息仍挂同一个 `question`(`regenerate-turn.ts` 已有回贴逻辑,改为按 `asked_turn_id` 回贴)。
|
||||
|
||||
## 4. 服务端兜底:模板题干,同一条消息
|
||||
|
||||
Agent 没调 set-focus(或两次校验失败)时:轮末 `persistNextInterviewIfIdle` 照常用 `USER_COLLECT_QUESTION` / `serverOwnedChoiceCopy` 创建 focus,**带当前 `turnId`**。消息渲染规则与 Agent 写的题干完全相同(都是"这条 turn 的 question"),所以兜底也在同一个气泡里。
|
||||
|
||||
删除:
|
||||
- `shouldPersistFocusPromptTurn` 及 `turn-exit.ts:82-107` 把题干写成**第二条** assistant turn 的整段逻辑。题干不再是 turn 文本,而是 turn 的 `question`。
|
||||
- `composeCollectSpokenAssistantText` / `detachCollectSpokenAssistantText` 及 `agent-run.ts:507-531` 的"把题干拼进 `p_assistant_message`"(BUG-487)。`assistant_message` 只存正文;已存在的历史 turn 里拼过题干的文本按精确后缀一次性 detach(读取时,`asked_turn_id` 命中且文本以 `\n\n${prompt}` 结尾才剥,仅此一处允许精确字符串比较,且只作用于本次迁移前的旧数据)。
|
||||
|
||||
顺带修掉 `39c30b55` 引入的回归:`finalizeSuccessfulTurnExit` 以 `focusCreated` 早退,导致 agent-run 预先建好的 choice focus 在 message 路径上从不写题干 turn。新合同下这个问题自然消失(题干不依赖 turn 文本),但要加不变量测试锁住(§7 第 2 条)。
|
||||
|
||||
## 5. 点选路径
|
||||
|
||||
点选是"immediate"确定性执行,没有模型轮。本轮**保持确定性**:`applyRectificationChoice` → `persistV9DeterministicTurn`(正文 = 现有 `hostNarration` 里的承接/进度句)→ 下一问 focus 带 `askedTurnId` + 模板题干。`resolve` 时写 `answer_option`。
|
||||
|
||||
用户可见效果:点 A 之后,上一条消息原地变成已答态,紧接着出现一条新消息"记下了…(进度)+ 下一题干 + 选项"。
|
||||
|
||||
**可选升级(本轮不做,留接口)**:点选后跑一次小预算模型轮只写 `spokenPrompt`,让区分题链也带 Agent 口吻。数据合同与 UI 不需要为此再改。PR 里记为 follow-up。
|
||||
|
||||
## 6. 前端
|
||||
|
||||
`rectification-agentic-chat.tsx`:
|
||||
|
||||
1. 删除 `.rectification-question-slot`(三处渲染 `:1253/:1281`、`showQuestionSlot`、`showMissingQuestion`/`showUnavailableQuestion` 文案)、`collectSpokenPrompt` 合成、`attachCollectSpokenStem`、`bubbleText` 拼接。
|
||||
2. `RenderMessage` 增加 `question?: MessageQuestion`(形状同 §2.3)。`messagesFromTurns` 从 `turn.question` 直接重建;`choiceAttachment` 并入 `question`(`answer_option` 非空即已答)。
|
||||
3. 气泡:`ChatMessageRow` 之内(同一个 `<article>`)渲染 `message.question`:题干段落 + 可选 `RectificationChoiceCard`(改为无外框/无 legend 的"选项组"变体,题干由段落承担,不再重复)。动作图标在整条消息之下。
|
||||
4. 流式结束:`run.completed` → `loadCaseSnapshot()` → 用返回的 `turns[last].question` 写入最新消息(不是拼文本)。`send()` 与 `submitStructuredChoice` 共用一个 `markQuestionAnswered(focusId, option | "typed")`:打字回答选择题时,上一条消息的 question 标 `answer_option: null, status: "resolved"`(灰掉选项,不高亮),不得让它消失。
|
||||
5. 缺题状态:"当前没有可回答的问题,正在等待服务端更新"只在**输入框上方**以一行状态显示(`role=status`),不再是消息列表里的块。
|
||||
6. 复制:复制文本 = 正文 + 空行 + 题干(+ 选项行)。
|
||||
7. `globals.css`:删除 `.rectification-question-slot*`;已答/未答选项组共用 `--assistant-content-inset`(BUG-461 保留条款)。
|
||||
|
||||
## 7. 不变量与测试(fail=0)
|
||||
|
||||
1. **一条消息**:任意 active focus ⇒ 最新 assistant turn 的 `question.focus_id` 等于它;DOM 中题干与选项在同一个 `article` 内;不存在 `.rectification-question-slot`。
|
||||
2. **历史必有题**:opening / message / answer_choice / accept 后建立的每个 focus 都有 `asked_turn_id`,且 GET `turns[]` 里恰有一条 assistant turn 带该 `question`(覆盖 `39c30b55` 回归形状:message 路径下 agent-run 预建 choice focus)。
|
||||
3. **刷新等价**:以走查 case `a17efc37` 形状跑完整链(opening → 2 采集 → 3 区分点选 → 1 打字答区分 → 采用 → 前事核对),`messagesFromTurns(GET.turns)` 渲染结果与在线会话的消息列表结构相同(题干、选项、已答标记、条数)。
|
||||
4. **Agent 题干校验**:年份/域与探针不符、含选项字面、含"唯一出生分钟"、超长 → `invalid_spoken_prompt`;两次失败落模板且仍挂在同一 turn。
|
||||
5. **点选行隐藏不变**:`isStructuredChoiceUserText` 语义保留;已答消息里选中项可见。
|
||||
6. **正文不提问**:`agent-voice-copy-contract` 的开场/承接锁保留(正文样本不得含问号结尾的题干句——这是对**模型输出样本**的测试断言,不是运行时判定)。
|
||||
7. 既有 `rectification-spoken-collect` / `rectification-collect-prompt` / `rectification-walkthrough-polish` 中锁槽位与拼接的断言:逐条三栏(旧→新→保留语义)迁移到新合同,不得整文件删除。
|
||||
|
||||
## 8. 硬红线
|
||||
|
||||
1. 问哪道题、年份、选项、计分、候选/采用/确认门语义不变;确认门恒 fail-closed。
|
||||
2. 运行时不得读正文字符串做任何判定(§4 的旧数据 detach 是唯一例外,且只在读取旧 turn 时)。
|
||||
3. Python 引擎不动;Skill 10.0.14 不 bump(skill 文本"正文只做承接与解释"与本合同兼容)。
|
||||
4. `./node_modules/.bin/tsc --noEmit` 通过(不要用 `npx tsc`);`npm run lint --prefix frontend` 0 error;`rectification-*` / `consultation-*` / `consult-*` / `chat-*` fail=0。
|
||||
5. migration 必须可在已有数据上执行(新列可空);旧 focus 无 `asked_turn_id` 的历史消息退化为纯文本,不报错。
|
||||
6. 无凭据不得声称真实环境验证;PR 贴走查 case 形状的前后对照(在线截图形状 + 刷新后重建)。
|
||||
|
||||
## 9. 开工前置
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/rectification-question-in-message-20260902 \
|
||||
../.worktrees/rectification-question-in-message-20260902 origin/staging
|
||||
```
|
||||
|
||||
读 `frontend/AGENTS.md`、`frontend/docs/VOICE.md`、`docs/BUG_HISTORY.md` 的 BUG-441/449/456/461/469/471/480/485/487/488 链(本任务书 §0 已列出各条废止/保留)。**行号是线索,按符号名定位。** BUG_HISTORY 新条目编号从 BUG-489 起(先 grep 确认最大号);本任务建议拆两条:一条记"题目进消息+删槽"的结构变更(引用 §0 决策记录),一条记 `39c30b55` 的 message 路径题干丢失回归。
|
||||
|
||||
## 10. 验收标准
|
||||
|
||||
1. §7 七条不变量全绿,改动断言逐条三栏说明。
|
||||
2. 走查 case 形状前后对照:改后每一道题都在助手气泡内;刷新后历史与在线一致;打字回答区分题后上一题不消失。
|
||||
3. Agent 题干样本(PR 贴 ≥5 道:2 采集、2 区分、1 前事核对):同年份同家族、接着用户上一句、无模板复读。
|
||||
4. tsc + lint + 四组测试 fail=0;BUG_HISTORY 两条落库。
|
||||
5. 真实环境人工清单(部署后):开场一条消息(打招呼+采集题);答一件事后一条消息(承接+区分题+选项);点 A 后上一条原地变已答;打字答选择题后上一条仍在;刷新后全程一致;复制文本含题干。
|
||||
Reference in New Issue
Block a user