diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index e16b67ef..9e7858b1 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -4583,7 +4583,7 @@ - 修复:可见正文在 `length` 结束,或超时/中止时已有输出,改为 `run.failed` / `answer_truncated`,保留已流出文本,账务走 `cancel`。咨询流设置 `maxOutputTokens = 8192`,并对当前供应商与 `openai` 兼容键关闭 thinking。客户端保存半截助手消息并提示未完成、不会扣点,不再要求 `run.completed` 才落盘。 - 验证:`frontend/tests/consultation-agentic-runtime.test.ts` 覆盖 `length` 与超时半截不得 `run.completed`、不得调用 `onComplete`;`consultation-workflow-contract.test.ts` 锁定输出预算与 thinking disabled;`consultation-recovery.test.ts` 与 `chat-stream-layout.test.ts` 锁定半截落盘与提示;`agent-observability.test.ts` 将 `answer_truncated` 纳入已知错误码。 - 防复发:有可见正文不等于咨询完成。`finish_reason=length`、超时半截不得再映射为 `run.completed`。咨询生成必须显式保留可见 token 预算;不得依赖 Flash 默认 thinking 与提供方默认 `max_tokens`。公开回执仍不得带 `modelFinishReason`,失败码必须能单独说明夹断。 -- 相关记录:BUG-280、BUG-277 +- 相关记录:BUG-280、BUG-277、BUG-340 - 复发自:无 - 修复版本:待提交 @@ -5115,6 +5115,22 @@ - 复发自:BUG-335(把入口 portal 到页头时用了 effect 同步 setState) - 修复版本:f4619df2 +## BUG-340 | 生时纠正未关闭 thinking,半截回答仍可能被当成完成 + +- 状态:resolved +- 首次发现:2026-08-21 +- 最近更新:2026-08-21 +- 影响面:`POST /api/rectification/agent`、`runV9AgentTurn`、生时纠正与普通咨询等待态 +- 用户现象:Agent 写到一半停止,界面却像已经答完;等待期间只有「正在处理…」,看不出在做什么。 +- 触发条件:当前会话模型默认开启 thinking / reasoning;可见正文与隐藏推理共用输出预算,或墙钟超时后仍发出 `finish`。 +- 根因:BUG-305 只修了咨询路径。纠正 `agent.stream` 没有 `maxOutputTokens`、没有关闭 thinking,并把任意 `finish` 当成成功。进度事件 `tool.activity started` 被客户端丢掉,所以长计算期间用户只能干等。 +- 修复:咨询与纠正共用 `agentGenerationSettings`(8192 可见 token,thinking disabled)。纠正在 `finish_reason=length` 时走 `answer_truncated`、不扣点、不把半截写入成功 Turn;客户端保留已流出正文并提示未完成。等待态改为公开工具进度(正在比较候选时间等)和 8 秒后的已用时,不展示模型思维链。 +- 验证:`frontend/tests/rectification-v9-stream.test.ts` 的 length 夹断不得 `run.completed`;`frontend/tests/rectification-v9-agent.test.ts` 锁定纠正流的输出预算与 thinking disabled;`frontend/tests/agent-activity-progress.test.ts`、`frontend/tests/rectification-agentic-entry.test.ts`、`frontend/tests/consultation-workflow-contract.test.ts`。 +- 防复发:纠正与咨询必须走同一套 generation settings。不得为了等待体验打开 provider thinking。`tool.activity started` 必须驱动 Orb 文案。`finish_reason=length` 不得映射为 `run.completed`。 +- 相关记录:BUG-305、BUG-282、BUG-329 +- 复发自:BUG-305(咨询已修,纠正仍用默认 thinking 与任意 finish) +- 修复版本:待提交 + ## BUG-329 | 生时纠正 Agent 回答在结算后一次性出现,推理中无法停止 - 状态:resolved diff --git a/frontend/src/app/api/rectification/agent/route.ts b/frontend/src/app/api/rectification/agent/route.ts index 49944793..b153317a 100644 --- a/frontend/src/app/api/rectification/agent/route.ts +++ b/frontend/src/app/api/rectification/agent/route.ts @@ -290,6 +290,7 @@ export async function POST(request: Request) { emit: (event) => send(event), signal: request.signal, timeContext, + generationModel: selectedModel.model, buildAgent: (turnId, skillPackage, attemptId) => Promise.resolve( getRectificationV9Agent(selectedModel, { userId, diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 239a9477..1fb23f5d 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -241,8 +241,29 @@ button:disabled { cursor: default; opacity: .45; } } .markdown-table tr:last-child th, .markdown-table tr:last-child td { border-bottom: 0; } -.agent-activity-status { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-body-sm); line-height: 1.5; } +.agent-activity-status { + min-height: 24px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + color: var(--color-ink-tertiary); + font-size: var(--type-body-sm); + line-height: 1.5; +} +.agent-activity-status__row { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); min-width: 0; width: 100%; } +.agent-activity-status__live { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); min-width: 0; } .agent-activity-status canvas { flex: 0 0 auto; } +.agent-activity-status__elapsed { + flex: 0 0 auto; + color: var(--color-ink-tertiary); + font-variant-numeric: tabular-nums; +} +.agent-activity-status__trail { + margin: 0; + padding-left: 28px; + color: var(--color-ink-tertiary); +} .agent-activity-status__text { color: var(--color-ink-tertiary); background: linear-gradient( diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index c87ebd6f..fb6cfaf0 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -92,12 +92,20 @@ import { BALANCE_CHANGED_EVENT, membershipHref, } from "@/lib/membership"; -import { chatMessageViews, type AgentActivityView, type ChatMessage } from "@/lib/chat-message-view"; +import { chatMessageViews, nextActivityView, activityCompletedTrail, type AgentActivityView, type ChatMessage } from "@/lib/chat-message-view"; import { createNdjsonParser, type AgentExecutionReceipt, type ConsultationAgentPublicEvent, } from "@/lib/consultation-agent-events"; +import { + CONSULTATION_CHART_CALCULATION_LABEL, + CONSULTATION_COMPOSING_LABEL, + CONSULTATION_DONE_CHART_LABEL, + CONSULTATION_DONE_SKILL_LABEL, + CONSULTATION_EVIDENCE_VALIDATION_LABEL, + CONSULTATION_LOADING_METHOD_LABEL, +} from "@/lib/consultation-activity-labels"; import { writeChatSession } from "@/lib/chat-session-write-contract"; import { consultationReportMarkdown } from "@/lib/consultation-report-export"; import { @@ -3200,7 +3208,13 @@ export default function Home() { const updateStreamingAnswer = (activity?: AgentActivityView) => { const partialReply = parseAgentReply(answer).text; latestPartialReply = partialReply; - setStreamingReply({ sessionId, text: partialReply, activity }); + setStreamingReply((current) => ({ + sessionId, + text: partialReply, + activity: activity + ? nextActivityView(current?.sessionId === sessionId ? current.activity : undefined, activity) + : current?.sessionId === sessionId ? current.activity : undefined, + })); if (partialReply && pendingConsultation.current?.requestId === requestId) { pendingConsultation.current = { ...pendingConsultation.current, partialReply }; } @@ -3208,15 +3222,29 @@ export default function Home() { const updateActivity = (event: ConsultationAgentPublicEvent) => { let activity: AgentActivityView | undefined; if (event.type === "skill.started") { - activity = { phase: "loading-method", label: "正在读取印度占星分析规则…" }; - } else if (event.type === "tool.started") { - activity = { phase: "chart-calculation", label: "正在计算本命盘…" }; + activity = { phase: "loading-method", label: CONSULTATION_LOADING_METHOD_LABEL }; + } else if (event.type === "skill.completed" || event.type === "tool.started") { + activity = { + phase: "chart-calculation", + label: CONSULTATION_CHART_CALCULATION_LABEL, + completedTrail: activityCompletedTrail([CONSULTATION_DONE_SKILL_LABEL]), + }; } else if (event.type === "activity") { - activity = { phase: event.phase, label: event.label }; + activity = { + phase: event.phase, + label: event.label, + completedTrail: event.phase === "evidence-validation" + ? activityCompletedTrail([CONSULTATION_DONE_SKILL_LABEL, CONSULTATION_DONE_CHART_LABEL]) + : activityCompletedTrail([CONSULTATION_DONE_SKILL_LABEL]), + }; } else if (event.type === "tool.completed") { - activity = { phase: "evidence-validation", label: "正在核对可用证据…" }; + activity = { + phase: "evidence-validation", + label: CONSULTATION_EVIDENCE_VALIDATION_LABEL, + completedTrail: activityCompletedTrail([CONSULTATION_DONE_SKILL_LABEL, CONSULTATION_DONE_CHART_LABEL]), + }; } else if (event.type === "answer.delta") { - activity = { phase: "answer-composition", label: "正在组织回答…" }; + activity = { phase: "answer-composition", label: CONSULTATION_COMPOSING_LABEL }; } if (activity) updateStreamingAnswer(activity); }; diff --git a/frontend/src/components/agent-activity-status.tsx b/frontend/src/components/agent-activity-status.tsx index f377e743..3fcfa7d3 100644 --- a/frontend/src/components/agent-activity-status.tsx +++ b/frontend/src/components/agent-activity-status.tsx @@ -1,9 +1,11 @@ "use client"; +import { useEffect, useState } from "react"; import dynamic from "next/dynamic"; import type { OrbState } from "thinking-orbs"; import { prefetchOnIdle } from "@/components/chat-chunk-prefetch"; +import { activityElapsedLabel } from "@/lib/chat-message-view"; const labels = { working: "正在处理任务…", @@ -27,17 +29,40 @@ prefetchOnIdle(importThinkingOrb); export type AgentActivityState = OrbState; +function ActivityElapsed({ startedAt }: Readonly<{ startedAt: number }>) { + const [now, setNow] = useState(() => Date.now()); + useEffect(() => { + const timer = window.setInterval(() => setNow(Date.now()), 1000); + return () => window.clearInterval(timer); + }, []); + const label = activityElapsedLabel(startedAt, now); + if (!label) return null; + return ; +} + export function AgentActivityStatus({ state, label = labels[state], + startedAt, + completedTrail, }: Readonly<{ state: AgentActivityState; label?: string; + startedAt?: number; + completedTrail?: string; }>) { return ( -
-