Files
Jyotisha/TASK-rectification-question-in-message-20260902.md
T
Jesse_Chen 24a1cf3777 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
2026-09-02 09:22:09 +00:00

16 KiB
Raw Blame History

任务书 · 题目进消息:一条助手消息承载正文+题干+选项,Agent 自己写题干,删除问题槽(2026-09-02)

基线:origin/staging @ 33d55b3a(含 BUG-480488 全部)。

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。前端 messagesFromTurnsrectification-agentic-chat.tsx:267)只能重建纯文本。

新增(一条 migrationfrontend/supabase/migrations/):

  1. focuses 表加 asked_turn_id uuid null references agentic_rectification_turns(id) on delete set nullanswer_option text null check (answer_option is null or answer_option in ('A','B','C','D','stop'))
  2. 创建 focus 的 RPCset_v10_conversation_focus 或现名,按符号找)接受 p_asked_turn_idresolve 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_atcreated_at 时间关系推断归属。
  4. current_question / choice_card 保留为提交契约question_idfocus_idprobe_idcase_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-focusmastra/rectification-v9-tools.ts:1173)增加输入 spokenPrompt: string(必填,8–120 字,单句,可含一个问号)。服务端校验:不得含选项字面("A."、"选项")、不得含"唯一/确切的出生分钟"、不得含内部 tokenprobe/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.promptcollect)或 choice.promptchoice)。projectCurrentQuestion / choiceCardFromCaseDossier 已优先读这里,不用改语义;serverOwnedChoiceCopy 只在 Agent 没写时使用。

  3. 记证据类工具(rectification-record-evidence-batch / confirm / revise)的返回已带 agentVisibleLatestProjectioncurrent_questionmethod_followup_plan.next_followup;确认返回里含服务端已决定的下一问的结构信息(探针 id、年份/期间、域、choice_frame 的选项文本、why),足够 Agent 写题干。缺什么补什么,不新造第二套投影。

  4. 系统指令(mastra/agentic-rectification.ts 第 4 条)改写为:

    每轮在记录证据后,用 rectification-set-focusspokenPrompt 写出服务端给你的下一问:用自己的话、结合用户刚说的事,问出同一个年份/期间和同一个事件家族;不得改年份、不得改选项含义、不得合并两道题。正文只做承接(2-4 句),不提问、不复述题干、不预告选项——题干会作为同一条消息的下一段自动出现。开场轮:先 set-focus 写采集题的 spokenPrompt,正文只打招呼。没有下一问(服务端返回 next_followup=null)时不要自拟问题。

    frontend/docs/VOICE.md 同步:加 3 组"服务端探针 → Agent 题干"好/坏对照(坏:逐字复读模板;坏:改了年份;好:接着用户上一句、同年份同家族)。

  5. 只读重写(regenerate)不得改 focus;重生成后的消息仍挂同一个 questionregenerate-turn.ts 已有回贴逻辑,改为按 asked_turn_id 回贴)。

4. 服务端兜底:模板题干,同一条消息

Agent 没调 set-focus(或两次校验失败)时:轮末 persistNextInterviewIfIdle 照常用 USER_COLLECT_QUESTION / serverOwnedChoiceCopy 创建 focus带当前 turnId。消息渲染规则与 Agent 写的题干完全相同(都是"这条 turn 的 question"),所以兜底也在同一个气泡里。

删除:

  • shouldPersistFocusPromptTurnturn-exit.ts:82-107 把题干写成第二条 assistant turn 的整段逻辑。题干不再是 turn 文本,而是 turn 的 question
  • composeCollectSpokenAssistantText / detachCollectSpokenAssistantTextagent-run.ts:507-531 的"把题干拼进 p_assistant_message"BUG-487)。assistant_message 只存正文;已存在的历史 turn 里拼过题干的文本按精确后缀一次性 detach(读取时,asked_turn_id 命中且文本以 \n\n${prompt} 结尾才剥,仅此一处允许精确字符串比较,且只作用于本次迁移前的旧数据)。

顺带修掉 39c30b55 引入的回归:finalizeSuccessfulTurnExitfocusCreated 早退,导致 agent-run 预先建好的 choice focus 在 message 路径上从不写题干 turn。新合同下这个问题自然消失(题干不依赖 turn 文本),但要加不变量测试锁住(§7 第 2 条)。

5. 点选路径

点选是"immediate"确定性执行,没有模型轮。本轮保持确定性applyRectificationChoicepersistV9DeterministicTurn(正文 = 现有 hostNarration 里的承接/进度句)→ 下一问 focus 带 askedTurnId + 模板题干。resolve 时写 answer_option

用户可见效果:点 A 之后,上一条消息原地变成已答态,紧接着出现一条新消息"记下了…(进度)+ 下一题干 + 选项"。

可选升级(本轮不做,留接口):点选后跑一次小预算模型轮只写 spokenPrompt,让区分题链也带 Agent 口吻。数据合同与 UI 不需要为此再改。PR 里记为 follow-up。

6. 前端

rectification-agentic-chat.tsx

  1. 删除 .rectification-question-slot(三处渲染 :1253/:1281showQuestionSlotshowMissingQuestion/showUnavailableQuestion 文案)、collectSpokenPrompt 合成、attachCollectSpokenStembubbleText 拼接。
  2. RenderMessage 增加 question?: MessageQuestion(形状同 §2.3)。messagesFromTurnsturn.question 直接重建;choiceAttachment 并入 questionanswer_option 非空即已答)。
  3. 气泡:ChatMessageRow 之内(同一个 <article>)渲染 message.question:题干段落 + 可选 RectificationChoiceCard(改为无外框/无 legend 的"选项组"变体,题干由段落承担,不再重复)。动作图标在整条消息之下。
  4. 流式结束:run.completedloadCaseSnapshot() → 用返回的 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-insetBUG-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 不 bumpskill 文本"正文只做承接与解释"与本合同兼容)。
  4. ./node_modules/.bin/tsc --noEmit 通过(不要用 npx tsc);npm run lint --prefix frontend 0 errorrectification-* / consultation-* / consult-* / chat-* fail=0。
  5. migration 必须可在已有数据上执行(新列可空);旧 focus 无 asked_turn_id 的历史消息退化为纯文本,不报错。
  6. 无凭据不得声称真实环境验证;PR 贴走查 case 形状的前后对照(在线截图形状 + 刷新后重建)。

9. 开工前置

git fetch origin --prune
git worktree add -b codex/rectification-question-in-message-20260902 \
  ../.worktrees/rectification-question-in-message-20260902 origin/staging

frontend/AGENTS.mdfrontend/docs/VOICE.mddocs/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=0BUG_HISTORY 两条落库。
  5. 真实环境人工清单(部署后):开场一条消息(打招呼+采集题);答一件事后一条消息(承接+区分题+选项);点 A 后上一条原地变已答;打字答选择题后上一条仍在;刷新后全程一致;复制文本含题干。