From ee10fb4d8a777d64f79073aa38bd36f5903af8e4 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 30 Jul 2026 03:15:06 +0800 Subject: [PATCH] feat: persist rectification analysis traces --- docs/BUG_HISTORY.md | 16 + frontend/src/app/globals.css | 55 +++ .../src/components/rectification-v4-panel.tsx | 141 +++++- .../src/lib/rectification-agent/contracts.ts | 7 +- .../lib/rectification-agent/orchestrator.ts | 107 ++++- .../lib/rectification-agent/reasoner-agent.ts | 66 ++- .../src/lib/rectification-v4/case-service.ts | 6 +- .../src/lib/rectification-v4/contracts.ts | 38 ++ .../src/lib/rectification-v4/memory-store.ts | 13 +- frontend/src/lib/rectification-v4/store.ts | 6 +- .../lib/rectification-v4/supabase-store.ts | 38 +- ...ersational-rectification-component.test.ts | 121 ++++- frontend/tests/rectification-agent-v5.test.ts | 14 +- .../rectification-analysis-trace.test.ts | 432 ++++++++++++++++++ ...ectification-v5-migration-contract.test.ts | 6 + skills/birth-time-rectification/SKILL.md | 13 + .../references/failure-policy.md | 9 + .../references/output-contract.md | 16 + .../references/technique-policy.md | 12 + 19 files changed, 1084 insertions(+), 32 deletions(-) create mode 100644 frontend/tests/rectification-analysis-trace.test.ts diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 5e4ae6c7..19119f3e 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -1692,3 +1692,19 @@ - 相关记录:BUG-090、BUG-091、BUG-092、BUG-093 - 复发自:无 - 修复版本:`birth-time-rectification-v6` / `rectification-agent-v6-1` + + +## BUG-095 | 生时校正“思考中”无法证明实际执行步骤且刷新后不可溯源 + +- 状态:resolved +- 首次发现:2026-07-29 +- 最近更新:2026-07-29 +- 影响面:V4/V5 生时校正运行状态、历史助手消息、候选计算与诊断的测试可观测性 +- 用户现象:页面只显示“正在核对星盘信息……”动画,无法判断本轮实际执行了哪些阶段、工具和技法;任务结束或刷新后也无法回看。 +- 根因:Job 只保存一个会被后续步骤覆盖的当前 `phase`,不能还原阶段历史;已持久化的 Public Message 与 Agent Run 工具记录没有被 Case API 投影到对应 Turn;UI 的 thinking 状态只是客户端进度动画,不是模型 reasoning,也不是服务端执行收据。 +- 修复:将“分析过程”定义为与 Turn 关联、可持久化和刷新后可恢复的服务端执行收据;只投影实际发生的阶段、工具调用和 allowlist 技法。供应商显式返回的 reasoning 内容只有通过服务端来源校验与安全过滤后才可作为可选摘要,缺失或不安全时直接省略,不伪造且不读取 hidden chain-of-thought。 +- 安全边界:不公开分数、权重、贡献矩阵、内部 ID/字段、候选分钟、工具参数或原始结果、Prompt、模型内部信息和用户敏感原文;D60 不展示且不驱动结论;未执行、不可用或仅供参考的技法不得显示为已执行。 +- 兼容边界:历史无收据记录继续读取;`v4_legacy` 与 `v5_shadow` 保持原有用户可见回复,shadow 仅持久化新产物而不展示分析轨迹;completed-job replay、原子 completion、`canConfirmExactMinute === false` 和禁止自动写入 `profiles.active_birth_time` 保持不变。 +- 防复发:API 与组件测试必须锁定 Turn 关联、刷新恢复、运行中真实 phase、仅展示实际工具/技法、无安全 reasoning 时不补写摘要,以及旧记录、legacy、shadow 的兼容行为。 +- 相关记录:BUG-011、BUG-086、BUG-087、BUG-094 +- 修复版本:`birth-time-rectification-v6` / `rectification-agent-v6-1` diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index e665c9a2..46b5d654 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -601,6 +601,56 @@ button:disabled { cursor: default; opacity: .45; } .rectification-message-actions button:focus-visible { outline: 2px solid color-mix(in srgb, var(--color-focus) 52%, transparent); outline-offset: 1px; } .rectification-message-actions button:disabled { cursor: default; opacity: 0.32; } .rectification-message-actions svg { width: 13px; height: 13px; stroke-width: 1.65; } +.rectification-analysis { + width: min(620px, calc(100% - 42px)); + margin: -2px 0 var(--space-1) 42px; + color: var(--color-ink-secondary); + font-size: var(--type-caption); +} +.rectification-analysis > summary { + width: fit-content; + display: flex; + align-items: center; + gap: var(--space-2); + min-height: 30px; + padding: 0 var(--space-2); + border-radius: var(--radius-sm); + color: var(--color-ink-tertiary); + cursor: pointer; + list-style: none; + transition: background-color 120ms ease-out, color 120ms ease-out; +} +.rectification-analysis > summary::-webkit-details-marker { display: none; } +.rectification-analysis > summary::before { + width: 6px; + height: 6px; + border-right: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; + content: ""; + transform: rotate(-45deg); + transition: transform 120ms ease-out; +} +.rectification-analysis[open] > summary::before { transform: rotate(45deg) translate(-1px, -1px); } +.rectification-analysis > summary:hover { background: var(--color-canvas-muted); color: var(--color-ink-secondary); } +.rectification-analysis > summary:focus-visible { outline: 2px solid color-mix(in srgb, var(--color-focus) 52%, transparent); outline-offset: 1px; } +.rectification-analysis > summary small { color: var(--color-ink-muted); font-size: inherit; } +.rectification-analysis-content { + display: grid; + gap: var(--space-3); + margin: var(--space-1) 0 var(--space-2); + padding: var(--space-3) var(--space-4); + border: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent); + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--color-canvas-muted) 54%, transparent); +} +.rectification-analysis-content section { display: grid; gap: var(--space-2); } +.rectification-analysis-content h4 { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 600; } +.rectification-analysis-content ol, +.rectification-analysis-content ul { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; } +.rectification-analysis-content li { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); } +.rectification-analysis-content li span { min-width: 0; color: var(--color-ink-secondary); } +.rectification-analysis-content li small { flex: 0 0 auto; color: var(--color-ink-muted); } +.rectification-analysis-content p { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.55; } .message p, .message-markdown { color: var(--color-ink-strong); font-size: var(--type-body-md); line-height: 1.65; text-wrap: pretty; word-break: auto-phrase; } .message-evidence-status { margin: var(--space-3) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--color-border); color: var(--color-ink-muted); font-size: var(--type-body-sm); line-height: 1.5; } .message-user p { line-height: 1.55; color: var(--color-ink); font-size: var(--type-body-sm); } @@ -1539,6 +1589,11 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background: max-width: 88%; } + .rectification-analysis { + width: calc(100% - 38px); + margin-left: 38px; + } + .conversation:not(.is-empty):not(.is-rectification) + .composer-wrap { padding-top: var(--space-3); padding-bottom: var(--space-3); diff --git a/frontend/src/components/rectification-v4-panel.tsx b/frontend/src/components/rectification-v4-panel.tsx index 8a42dcb5..97bc302d 100644 --- a/frontend/src/components/rectification-v4-panel.tsx +++ b/frontend/src/components/rectification-v4-panel.tsx @@ -2,11 +2,12 @@ import { ArrowUp, Check, Copy, RotateCcw, ThumbsDown, ThumbsUp } from "lucide-react"; import { useEffect, useRef, useState } from "react"; +import { AgentActivityStatus } from "@/components/agent-activity-status"; import { useRectificationV4 } from "@/hooks/use-rectification-v4"; import type { ChatMessageView } from "@/lib/chat-message-view"; import type { PublicLanguageModel } from "@/lib/public-models"; -import type { RectificationV4ApiResponse } from "@/lib/rectification-v4/contracts"; -import { ChatMessageRow } from "./chat-message-row"; +import type { RectificationAnalysisItem, RectificationAnalysisTrace, RectificationV4ApiResponse } from "@/lib/rectification-v4/contracts"; +import { AgentAvatar, ChatMessageRow } from "./chat-message-row"; import { ModelSelector } from "./model-selector"; import { Button } from "./ui/button"; import { Textarea } from "./ui/textarea"; @@ -29,6 +30,115 @@ type RectificationV4PanelProps = Readonly<{ onContinueOriginalQuestion?: (continuation: RectificationV4Continuation) => void; }>; +type RectificationChatMessageView = ChatMessageView & Readonly<{ + analysisTrace?: RectificationAnalysisTrace; +}>; + +const phaseLabels = { + collecting_evidence: "正在准备继续收集经历…", + extracting_evidence: "正在整理你刚才提到的经历…", + scoring_candidates: "正在扫描候选时间…", + checking_robustness: "正在检查候选范围的稳定性…", + planning_question: "正在选择下一条最有信息量的问题…", + reasoning: "正在结合上下文决定下一步…", + rendering: "正在组织下一条回复…", + complete: "分析已完成", +} as const; + +export function rectificationPhaseLabel( + phase: NonNullable["phase"], +): string { + return phaseLabels[phase]; +} + +function durationLabel(durationMs: number | null): string | null { + if (durationMs === null || durationMs < 0) return null; + return durationMs < 1_000 ? `${durationMs} 毫秒` : `${(durationMs / 1_000).toFixed(1)} 秒`; +} + +function publicStatusLabel(status: string): string { + return ({ + completed: "已完成", + succeeded: "已完成", + running: "进行中", + failed: "未完成", + skipped: "已跳过", + legacy: "历史记录", + } as Record)[status] ?? "已记录"; +} + +function RectificationAnalysisDetails({ trace }: Readonly<{ trace: RectificationAnalysisTrace }>) { + return ( +
+ + 分析过程 + {publicStatusLabel(trace.status)} + +
+ {trace.stages.length > 0 && ( +
+

执行阶段

+
    + {trace.stages.map((stage, index) => { + const duration = durationLabel(stage.durationMs); + return ( +
  1. + {stage.label} + {publicStatusLabel(stage.status)}{duration ? ` · ${duration}` : ""} +
  2. + ); + })} +
+
+ )} + {trace.toolCalls.length > 0 && ( +
+

实际调用

+
    + {trace.toolCalls.map((toolCall, index) => { + const duration = durationLabel(toolCall.durationMs); + return ( +
  • + {toolCall.label} + {publicStatusLabel(toolCall.outcome)}{duration ? ` · ${duration}` : ""} +
  • + ); + })} +
+
+ )} + {trace.techniques.length > 0 && ( +
+

实际使用的技法

+

{trace.techniques.join("、")}

+
+ )} + {trace.reasoningSource === "provider_summary" && trace.reasoningSummary && ( +
+

推理摘要

+

{trace.reasoningSummary}

+
+ )} +
+
+ ); +} + +function RectificationMessageRow({ message }: Readonly<{ message: RectificationChatMessageView }>) { + if (message.state !== "thinking" || !message.text) return ; + + return ( +
+ +
+
+ +
+
+
+ ); +} + export function toggleRectificationFeedback( current: "up" | "down" | undefined, requested: "up" | "down", @@ -55,7 +165,7 @@ export function rectificationV4ChatMessages( data: RectificationV4ApiResponse | null, processing: boolean, pendingConsultationQuestion?: string | null, -): readonly ChatMessageView[] { +): readonly RectificationChatMessageView[] { if (!data) { return [{ role: "assistant", @@ -65,7 +175,13 @@ export function rectificationV4ChatMessages( }]; } - const messages: ChatMessageView[] = []; + const messages: RectificationChatMessageView[] = []; + const analysis = data.case.deploymentMode === "v5_agent" + ? (data as RectificationV4ApiResponse & { + readonly analysis?: readonly RectificationAnalysisItem[]; + }).analysis ?? [] + : []; + const analysisBySourceTurnId = new Map(analysis.map((item) => [item.sourceTurnId, item.trace])); if (pendingConsultationQuestion?.trim()) { messages.push({ role: "assistant", @@ -75,12 +191,14 @@ export function rectificationV4ChatMessages( }); } + let previousTurnId: string | null = null; for (const turn of data.turns) { messages.push({ role: "assistant", text: turn.question, renderKey: `rectification-question-${turn.id}`, state: "settled", + analysisTrace: previousTurnId ? analysisBySourceTurnId.get(previousTurnId) : undefined, }); if (turn.answer) { messages.push({ @@ -90,16 +208,20 @@ export function rectificationV4ChatMessages( state: "settled", }); } + previousTurnId = turn.id; } const caseValue = data.case; const primary = caseValue.latestSnapshot?.clusters[0]; + const latestTurnTrace = analysisBySourceTurnId.get(data.turns.at(-1)?.id ?? ""); + const terminalTrace = caseValue.currentQuestion ? undefined : latestTurnTrace; if (caseValue.acceptedRange) { messages.push({ role: "assistant", text: `候选范围已保存为 ${caseValue.acceptedRange.start}–${caseValue.acceptedRange.end}。这是校正得到的候选范围,原出生时间没有被自动改写。`, renderKey: `rectification-accepted-${caseValue.version}`, state: "settled", + analysisTrace: terminalTrace, }); } else if (caseValue.status === "range_ready" && primary) { messages.push({ @@ -107,6 +229,7 @@ export function rectificationV4ChatMessages( text: `根据目前这些经历,可以先把范围稳定缩小到 ${primary.startTime}–${primary.endTime}。这是候选范围,不是已确认的出生分钟;你可以保存它,也可以继续补充经历。`, renderKey: `rectification-range-${caseValue.version}`, state: "settled", + analysisTrace: terminalTrace, }); } @@ -116,13 +239,14 @@ export function rectificationV4ChatMessages( text: caseValue.currentQuestion.prompt, renderKey: `rectification-current-${caseValue.currentQuestion.id}`, state: "settled", + analysisTrace: latestTurnTrace, }); } if (processing) { messages.push({ role: "assistant", - text: "", + text: rectificationPhaseLabel(data.job?.phase ?? caseValue.phase), renderKey: `rectification-processing-${data.job?.id ?? caseValue.version}`, state: "thinking", }); @@ -132,6 +256,7 @@ export function rectificationV4ChatMessages( text: "进度已经保存。准备好后,我们可以从这里继续。", renderKey: `rectification-paused-${caseValue.version}`, state: "settled", + analysisTrace: terminalTrace, }); } else if (caseValue.status === "abandoned") { messages.push({ @@ -139,6 +264,7 @@ export function rectificationV4ChatMessages( text: "这次校正已经结束,原出生时间没有被改写。", renderKey: `rectification-abandoned-${caseValue.version}`, state: "settled", + analysisTrace: terminalTrace, }); } @@ -254,9 +380,12 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) { }); return (
- + {message.role === "assistant" && message.state === "settled" && message.analysisTrace && ( + + )} {showActions && !regenerating && (