From 6a44c778c3fa851273fb237d644146ce1aa1a376 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 21 Aug 2026 12:39:09 +0800 Subject: [PATCH] fix(web): show live agent work progress and fail truncated rectification answers Rectification dropped tool.activity started events and treated length finishes as completed. Share generation settings with consultation, keep the activity line through streaming, and name multi-domain chart calculation. Co-authored-by: Cursor --- docs/BUG_HISTORY.md | 18 +++- .../src/app/api/rectification/agent/route.ts | 1 + frontend/src/app/globals.css | 23 ++++- frontend/src/app/page.tsx | 44 +++++++-- .../src/components/agent-activity-status.tsx | 31 ++++++- frontend/src/components/chat-message-row.tsx | 7 +- .../components/completed-activity-receipt.tsx | 17 +--- .../components/rectification-agentic-chat.tsx | 76 +++++++++++++--- frontend/src/lib/agent-generation-settings.ts | 28 ++++++ frontend/src/lib/chat-message-view.ts | 35 +++++++ .../src/lib/consultation-activity-labels.ts | 11 +++ .../src/lib/rectification-activity-labels.ts | 64 +++++++++++++ .../lib/rectification-agentic/v9/agent-run.ts | 48 +++++++++- frontend/src/mastra/consultation-tools.ts | 45 +++------ .../tests/agent-activity-progress.test.ts | 91 +++++++++++++++++++ frontend/tests/chat-stream-layout.test.ts | 11 ++- .../consultation-workflow-contract.test.ts | 12 ++- .../rectification-activity-receipt.test.ts | 1 + .../tests/rectification-agentic-entry.test.ts | 18 +++- frontend/tests/rectification-v9-agent.test.ts | 4 + .../tests/rectification-v9-stream.test.ts | 35 +++++++ 21 files changed, 539 insertions(+), 81 deletions(-) create mode 100644 frontend/src/lib/agent-generation-settings.ts create mode 100644 frontend/src/lib/consultation-activity-labels.ts create mode 100644 frontend/src/lib/rectification-activity-labels.ts create mode 100644 frontend/tests/agent-activity-progress.test.ts 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 ( -
-