diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 82674b8b..53b2a828 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -5451,6 +5451,18 @@ - 复发自:BUG-354(关 thinking 后过程进正文)、BUG-357(用正则从正文里切两栏) - 修复版本:`04463e9a` +## BUG-360 | 生时纠正后段思考被写进第一段「思考」 - - +- 状态:resolved +- 首次发现:2026-08-23 +- 最近更新:2026-08-23 +- 影响面:生时纠正对话、`AgentActivityStatus`、流式 `thinking.delta` 与 `tool.activity` +- 用户现象:界面先列出「读取校正记录」「正在整理多条事件证据…」,再给一整块「思考」。工具之后的思维链被追加进第一段思考,而不是出现在「正在整理…」下面。 +- 触发条件:纠正回合先出现 `thinking.delta`,再 `tool.activity`,之后继续 `thinking.delta`。 +- 根因:`AgentActivityStatus` 先渲染 `completedTrail` 和当前活动,再渲染整段 `thinkingText`。流式处理把所有 `thinking.delta` 拼进同一个 `thinkingRaw`,无法按事件顺序把后段思考放到工具行之后。 +- 修复:纠正流按事件维护 `activityTrace`。工具开始会结束当前思考行;工具之后的 `thinking.delta` 开新的思考行。有轨迹时不再把拼接后的 `thinkingText` 单独渲在工具列表下面。 +- 验证:`frontend/tests/agent-activity-trace.test.ts`、`frontend/tests/chat-stream-layout.test.ts`、`frontend/tests/consultation-run-timeline.test.ts` +- 防复发:纠正流不得把全部 `thinking.delta` 拼成一块再画在活动列表下方。工具后的思考必须是新的 trace 行。咨询时间线同样不得把后段 `thinking.delta` upsert 回第一条思考。 +- 相关记录:BUG-095、BUG-357、BUG-359 +- 复发自:BUG-095(步骤轨迹只证明工具发生过,没有与思维链按时间交错) +- 修复版本:待本次提交 diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 485b8e73..d3f52931 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -271,6 +271,13 @@ button:disabled { cursor: default; opacity: .45; } gap: var(--space-2); min-height: 24px; } +.agent-thinking-step.is-think:has(.message-thinking) { + display: block; + min-height: 0; +} +.agent-thinking-step.is-think .message-thinking { + margin-bottom: 0; +} .agent-thinking-marker { width: 20px; height: 20px; diff --git a/frontend/src/components/agent-activity-status.tsx b/frontend/src/components/agent-activity-status.tsx index 0f56bb3a..ef548185 100644 --- a/frontend/src/components/agent-activity-status.tsx +++ b/frontend/src/components/agent-activity-status.tsx @@ -7,6 +7,7 @@ import type { OrbState } from "thinking-orbs"; import { prefetchOnIdle } from "@/components/chat-chunk-prefetch"; import { activityCompletedSteps, activityElapsedLabel } from "@/lib/chat-message-view"; +import type { AgentActivityTraceItem } from "@/lib/agent-activity-trace"; const labels = { working: "正在处理任务…", @@ -71,6 +72,7 @@ export function AgentActivityStatus({ startedAt, completedTrail, thinkingText, + activityTrace, hasAnswer = false, showLive = true, }: Readonly<{ @@ -79,11 +81,29 @@ export function AgentActivityStatus({ startedAt?: number; completedTrail?: string; thinkingText?: string; + activityTrace?: readonly AgentActivityTraceItem[]; hasAnswer?: boolean; showLive?: boolean; }>) { const completedSteps = activityCompletedSteps(completedTrail); const live = showLive && !hasAnswer; + const trace = activityTrace ?? []; + if (trace.length > 0) { + return ( +
+
    + {trace.map((item) => ( + + ))} +
+
+ ); + } if (!live && completedSteps.length === 0 && !thinkingText?.trim()) return null; return ( @@ -115,3 +135,45 @@ export function AgentActivityStatus({ ); } + +function TraceStep({ + item, + state, + hasAnswer, +}: Readonly<{ + item: AgentActivityTraceItem; + state: AgentActivityState; + hasAnswer: boolean; +}>) { + if (item.kind === "think") { + return ( +
  • + {item.text?.trim() ? ( + + ) : ( + <> + + {item.label} + + )} +
  • + ); + } + return ( +
  • + + {item.status === "live" ? ( + + {item.label} + {item.startedAt ? : null} + + ) : ( + {item.label} + )} +
  • + ); +} diff --git a/frontend/src/components/chat-message-row.tsx b/frontend/src/components/chat-message-row.tsx index 191cf184..fec5ae32 100644 --- a/frontend/src/components/chat-message-row.tsx +++ b/frontend/src/components/chat-message-row.tsx @@ -64,9 +64,10 @@ export function ChatMessageRow({ const hasAnswer = Boolean(message.text.trim()); const consultTimeline = message.timeline; const thinkingSections = message.thinkingSections ?? []; + const hasTrace = (message.activityTrace?.length ?? 0) > 0; const showReport = consultTimeline === undefined && thinkingSections.length > 0; const showLiveActivity = showActivity && !showReport && consultTimeline === undefined; - const showThinkingPanel = showLiveActivity || (!showReport && consultTimeline === undefined && Boolean(message.thinkingText?.trim())); + const showThinkingPanel = showLiveActivity || (!showReport && consultTimeline === undefined && (Boolean(message.thinkingText?.trim()) || hasTrace)); const showSpokenAnswer = !showReport && Boolean(message.text); const stackedThinkingAndAnswer = showThinkingPanel && showSpokenAnswer; @@ -101,7 +102,8 @@ export function ChatMessageRow({ label={activityLabel} startedAt={message.activity?.startedAt} completedTrail={message.activity?.completedTrail} - thinkingText={message.thinkingText} + thinkingText={hasTrace ? undefined : message.thinkingText} + activityTrace={message.activityTrace} hasAnswer={hasAnswer} showLive={showLiveActivity} /> diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx index 79809a3c..23e684c8 100644 --- a/frontend/src/components/rectification-agentic-chat.tsx +++ b/frontend/src/components/rectification-agentic-chat.tsx @@ -6,6 +6,16 @@ import { createPortal } from "react-dom"; import { parseAgentReply } from "@/lib/agent-reply"; import { nextActivityView, type ChatMessage, type ChatMessageView } from "@/lib/chat-message-view"; import { + appendActivityTraceThinking, + completeActivityTrace, + completeActivityTraceStep, + emptyActivityTrace, + freezeLiveThink, + startActivityTraceStep, + type AgentActivityTraceItem, +} from "@/lib/agent-activity-trace"; +import { + RECTIFICATION_TOOL_DONE_LABELS, RECTIFICATION_TOOL_PROGRESS_LABELS, rectificationCompletedTrail, rectificationToolActivityPhase, @@ -151,6 +161,7 @@ type RenderMessage = ChatMessageView & { completedReceipt?: CompletedActivityReceiptView; failed?: boolean; turnId?: string; + activityTrace?: readonly AgentActivityTraceItem[]; }; function turnOfferedSelection(message: RenderMessage): boolean { @@ -383,7 +394,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { ...(action === "message" ? [{ role: "user", text: trimmed, renderKey: userRenderKey, state: "settled" } satisfies RenderMessage] : []), - { role: "assistant", text: "", renderKey: assistantRenderKey, state: "thinking", activity: { + { role: "assistant", text: "", renderKey: assistantRenderKey, state: "thinking", activityTrace: emptyActivityTrace(), activity: { phase: "evidence-validation", label: "正在处理…", startedAt: Date.now(), @@ -393,6 +404,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { let raw = ""; let thinkingRaw = ""; + let activityTrace: readonly AgentActivityTraceItem[] = emptyActivityTrace(); let activityReceiptState = createRectificationActivityReceiptState(); let completedReceipt = receiptFromRectificationActivityState(activityReceiptState); let completedTurnId: string | undefined; @@ -464,6 +476,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { if (typeof event.type !== "string") continue; if (event.type === "answer.delta" && typeof event.text === "string") { raw += event.text; + activityTrace = freezeLiveThink(activityTrace); const settled = settleRectificationSpokenAndThinking(raw, thinkingRaw); const parsed = parseAgentReply(settled.spoken); setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey @@ -471,6 +484,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { ...message, text: parsed.text, thinkingText: settled.thinking.trim() || undefined, + activityTrace, state: parsed.text ? "streaming" : "thinking", activity: nextActivityView(message.activity, { phase: "answer-composition", @@ -480,6 +494,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { : message)); } else if (event.type === "thinking.delta" && typeof event.text === "string") { thinkingRaw += event.text; + activityTrace = appendActivityTraceThinking(activityTrace, event.text); const settled = settleRectificationSpokenAndThinking(raw, thinkingRaw); const parsed = parseAgentReply(settled.spoken); setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey @@ -487,12 +502,14 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { ...message, text: parsed.text, thinkingText: settled.thinking.trim() || undefined, + activityTrace, state: parsed.text ? "streaming" : "thinking", } : message)); } else if (event.type === "attempt.reset") { raw = ""; thinkingRaw = ""; + activityTrace = emptyActivityTrace(); activityReceiptState = createRectificationActivityReceiptState(); completedReceipt = receiptFromRectificationActivityState(activityReceiptState); completedTurnId = undefined; @@ -501,6 +518,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { ...message, text: "", thinkingText: undefined, + activityTrace, state: "thinking", completedReceipt: undefined, failed: false, @@ -523,9 +541,15 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { const tool = isPublicRectificationTool(event.tool) ? event.tool : null; if (!tool) continue; if (event.status === "started") { + activityTrace = startActivityTraceStep( + activityTrace, + tool, + RECTIFICATION_TOOL_PROGRESS_LABELS[tool], + ); setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey ? { ...message, + activityTrace, activity: nextActivityView(message.activity, { phase: rectificationToolActivityPhase(tool), label: RECTIFICATION_TOOL_PROGRESS_LABELS[tool], @@ -536,6 +560,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { continue; } if (event.status !== "completed" && event.status !== "failed") continue; + activityTrace = completeActivityTraceStep( + activityTrace, + tool, + RECTIFICATION_TOOL_DONE_LABELS[tool], + ); activityReceiptState = reduceRectificationActivityReceipt(activityReceiptState, { tool, status: event.status, @@ -544,6 +573,18 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { : [], }); completedReceipt = receiptFromRectificationActivityState(activityReceiptState); + setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey + ? { + ...message, + activityTrace, + completedReceipt, + activity: nextActivityView(message.activity, { + phase: rectificationToolActivityPhase(tool), + label: RECTIFICATION_TOOL_DONE_LABELS[tool], + completedTrail: rectificationCompletedTrail(activityReceiptState.completedSteps), + }), + } + : message)); } } } @@ -558,6 +599,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { ...message, text: parsed.text, thinkingText: settled.thinking.trim() || undefined, + activityTrace: completeActivityTrace(activityTrace), state: "settled", completedReceipt, failed: false, @@ -570,6 +612,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { ...message, text: parsed.text, thinkingText: settled.thinking.trim() || undefined, + activityTrace: completeActivityTrace(activityTrace), state: "settled", completedReceipt, failed: true, @@ -607,6 +650,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { ...message, text: parsed.text, thinkingText: settled.thinking.trim() || undefined, + activityTrace: completeActivityTrace(activityTrace), state: "settled", completedReceipt, failed: false, diff --git a/frontend/src/lib/agent-activity-trace.ts b/frontend/src/lib/agent-activity-trace.ts new file mode 100644 index 00000000..83139b52 --- /dev/null +++ b/frontend/src/lib/agent-activity-trace.ts @@ -0,0 +1,107 @@ +export type AgentActivityTraceKind = "activity" | "think"; +export type AgentActivityTraceStatus = "live" | "done"; + +export type AgentActivityTraceItem = Readonly<{ + id: string; + kind: AgentActivityTraceKind; + status: AgentActivityTraceStatus; + label: string; + text?: string; + startedAt?: number; + tool?: string; +}>; + +const THINKING_TEXT_LIMIT = 8_000; + +export function emptyActivityTrace(): readonly AgentActivityTraceItem[] { + return []; +} + +export function appendActivityTraceThinking( + rows: readonly AgentActivityTraceItem[], + text: string, +): readonly AgentActivityTraceItem[] { + if (!text) return rows; + const last = rows.at(-1); + if (last?.kind === "think" && last.status === "live") { + return rows.map((row, index) => ( + index === rows.length - 1 + ? { ...row, text: `${row.text ?? ""}${text}`.slice(0, THINKING_TEXT_LIMIT) } + : row + )); + } + const frozen = freezeLiveThink(rows); + const thinkCount = frozen.filter((row) => row.kind === "think").length; + return [ + ...frozen, + { + id: `think-${thinkCount + 1}`, + kind: "think", + status: "live", + label: "思考", + text: text.slice(0, THINKING_TEXT_LIMIT), + }, + ]; +} + +export function startActivityTraceStep( + rows: readonly AgentActivityTraceItem[], + tool: string, + label: string, + startedAt = Date.now(), +): readonly AgentActivityTraceItem[] { + const frozen = freezeLiveThink(rows); + const last = frozen.at(-1); + if (last?.kind === "activity" && last.status === "live" && last.tool === tool) { + return frozen; + } + const closed = frozen.map((row) => ( + row.kind === "activity" && row.status === "live" ? { ...row, status: "done" as const } : row + )); + const toolCount = closed.filter((row) => row.kind === "activity" && row.tool === tool).length; + return [ + ...closed, + { + id: `activity-${tool}-${toolCount + 1}`, + kind: "activity", + status: "live", + label, + startedAt, + tool, + }, + ]; +} + +export function completeActivityTraceStep( + rows: readonly AgentActivityTraceItem[], + tool: string, + label?: string, +): readonly AgentActivityTraceItem[] { + for (let index = rows.length - 1; index >= 0; index -= 1) { + const row = rows[index]; + if (row?.kind === "activity" && row.tool === tool && row.status === "live") { + return rows.map((item, current) => ( + current === index + ? { ...item, status: "done" as const, label: label ?? item.label } + : item + )); + } + } + return rows; +} + +export function freezeLiveThink( + rows: readonly AgentActivityTraceItem[], +): readonly AgentActivityTraceItem[] { + return rows.map((row) => ( + row.kind === "think" && row.status === "live" ? { ...row, status: "done" as const } : row + )); +} + +export function completeActivityTrace( + rows: readonly AgentActivityTraceItem[], +): readonly AgentActivityTraceItem[] { + return freezeLiveThink(rows).map((row) => ( + row.status === "live" ? { ...row, status: "done" as const } : row + )); +} diff --git a/frontend/src/lib/chat-message-view.ts b/frontend/src/lib/chat-message-view.ts index 5efdbdd1..c97943af 100644 --- a/frontend/src/lib/chat-message-view.ts +++ b/frontend/src/lib/chat-message-view.ts @@ -1,3 +1,4 @@ +import type { AgentActivityTraceItem } from "./agent-activity-trace.ts"; import type { AgentExecutionReceipt, PublicActivityPhase, WorkflowReceipt } from "./consultation-agent-events.ts"; import type { PublicThinkingSection } from "./consultation-thinking-plan.ts"; import { @@ -66,6 +67,7 @@ export type ChatMessageView = ChatMessage & { readonly renderKey: string; readonly state: "settled" | "streaming" | "thinking"; readonly activity?: AgentActivityView; + readonly activityTrace?: readonly AgentActivityTraceItem[]; readonly timeline?: readonly ConsultationTimelineRow[]; }; diff --git a/frontend/src/lib/consultation-run-timeline.ts b/frontend/src/lib/consultation-run-timeline.ts index 7798ea35..689c4547 100644 --- a/frontend/src/lib/consultation-run-timeline.ts +++ b/frontend/src/lib/consultation-run-timeline.ts @@ -34,7 +34,6 @@ export type ConsultationTimelineState = Readonly<{ const METHOD_ID = "method"; const CALCULATE_ID = "calculate"; -const OPEN_THINK_ID = "think-open"; export function emptyConsultationTimeline(): ConsultationTimelineState { return { rows: [], answer: "" }; @@ -97,7 +96,7 @@ export function reduceConsultationTimeline( steps: event.steps, }; const withCalc = enrichCalculate(completeRow(state, CALCULATE_ID, CONSULTATION_DONE_CHART_LABEL), section); - const withoutOpen = completeRow(withCalc, OPEN_THINK_ID); + const withoutOpen = completeLiveThink(withCalc); return upsertRow(withoutOpen, { id: `think-${section.id}`, kind: "think", @@ -106,15 +105,19 @@ export function reduceConsultationTimeline( }); } if (event.type === "thinking.delta") { - const think = lastRow(state.rows, (row) => row.kind === "think" && row.status === "live") - ?? { - id: OPEN_THINK_ID, - kind: "think" as const, - status: "live" as const, - label: "正在分析…", - }; - const thinkingText = `${think.thinkingText ?? ""}${event.text}`.slice(0, 4_000); - return upsertRow(state, { ...think, status: "live", thinkingText }); + const think = lastRow(state.rows, (row) => row.kind === "think" && row.status === "live"); + if (think) { + const thinkingText = `${think.thinkingText ?? ""}${event.text}`.slice(0, 4_000); + return upsertRow(state, { ...think, status: "live", thinkingText }); + } + const thinkCount = state.rows.filter((row) => row.kind === "think").length; + return upsertRow(state, { + id: `think-open-${thinkCount + 1}`, + kind: "think", + status: "live", + label: "正在分析…", + thinkingText: event.text.slice(0, 4_000), + }); } if (event.type === "answer.delta") { const next = { ...completeLiveThink(state), answer: `${state.answer}${event.text}` }; diff --git a/frontend/tests/agent-activity-trace.test.ts b/frontend/tests/agent-activity-trace.test.ts new file mode 100644 index 00000000..1bab9147 --- /dev/null +++ b/frontend/tests/agent-activity-trace.test.ts @@ -0,0 +1,57 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + appendActivityTraceThinking, + completeActivityTrace, + completeActivityTraceStep, + emptyActivityTrace, + startActivityTraceStep, +} from "../src/lib/agent-activity-trace.ts"; + +test("thinking after a tool starts a new think row instead of filling the first", () => { + let rows = appendActivityTraceThinking(emptyActivityTrace(), "先确认用户说的是入职当天。"); + rows = startActivityTraceStep(rows, "rectification-record-evidence-batch", "正在整理多条事件证据…", 1_000); + rows = appendActivityTraceThinking(rows, "再把 2024-04-07 写成经历证据。"); + + assert.deepEqual(rows.map((row) => row.kind), ["think", "activity", "think"]); + assert.equal(rows[0]?.status, "done"); + assert.equal(rows[0]?.text, "先确认用户说的是入职当天。"); + assert.equal(rows[1]?.label, "正在整理多条事件证据…"); + assert.equal(rows[1]?.status, "live"); + assert.equal(rows[2]?.status, "live"); + assert.equal(rows[2]?.text, "再把 2024-04-07 写成经历证据。"); + assert.notEqual(rows[0]?.id, rows[2]?.id); +}); + +test("completing a tool then thinking starts a new think under the done activity", () => { + let rows = appendActivityTraceThinking(emptyActivityTrace(), "先读取校正记录。"); + rows = startActivityTraceStep(rows, "rectification-record-evidence-batch", "正在整理多条事件证据…", 1_000); + rows = completeActivityTraceStep(rows, "rectification-record-evidence-batch", "整理事件证据"); + rows = appendActivityTraceThinking(rows, "用户说转做开发就是 2024-04-07。"); + + assert.deepEqual(rows.map((row) => `${row.kind}:${row.status}`), [ + "think:done", + "activity:done", + "think:live", + ]); + assert.equal(rows[1]?.label, "整理事件证据"); + assert.equal(rows[2]?.text, "用户说转做开发就是 2024-04-07。"); +}); + +test("the same live tool is not duplicated, and a later tool follows the second think", () => { + let rows = startActivityTraceStep(emptyActivityTrace(), "rectification-read-case", "正在读取校正记录…", 10); + rows = startActivityTraceStep(rows, "rectification-read-case", "正在读取校正记录…", 20); + assert.equal(rows.length, 1); + assert.equal(rows[0]?.startedAt, 10); + + rows = completeActivityTraceStep(rows, "rectification-read-case", "读取校正记录"); + rows = startActivityTraceStep(rows, "rectification-record-evidence-batch", "正在整理多条事件证据…", 30); + assert.deepEqual(rows.map((row) => `${row.kind}:${row.label}`), [ + "activity:读取校正记录", + "activity:正在整理多条事件证据…", + ]); + + const settled = completeActivityTrace(rows); + assert.ok(settled.every((row) => row.status === "done")); +}); diff --git a/frontend/tests/chat-stream-layout.test.ts b/frontend/tests/chat-stream-layout.test.ts index 959290c2..385494a2 100644 --- a/frontend/tests/chat-stream-layout.test.ts +++ b/frontend/tests/chat-stream-layout.test.ts @@ -101,8 +101,12 @@ test("shows honest agent activity states before and during streamed text", () => assert.match(pageSource, /streamedThinking/); assert.match(messageRowSource, /showLiveActivity/); assert.match(messageRowSource, /ConsultationThinkingReport/); - assert.match(messageRowSource, /thinkingText=\{message\.thinkingText\}/); - assert.match(messageRowSource, /showLiveActivity \|\| \(!showReport && consultTimeline === undefined && Boolean\(message\.thinkingText/); + assert.match(messageRowSource, /thinkingText=\{hasTrace \? undefined : message\.thinkingText\}/); + assert.match(messageRowSource, /activityTrace=\{message\.activityTrace\}/); + assert.match(messageRowSource, /showLiveActivity \|\| \(!showReport && consultTimeline === undefined && \(Boolean\(message\.thinkingText/); + assert.match(activitySource, /activityTrace \?\? \[\]/); + assert.match(activitySource, /function TraceStep/); + assert.match(globalStyles, /\.agent-thinking-step\.is-think:has\(\.message-thinking\)/); assert.match(reportSource, /caption="思考"/); assert.match(reportSource, /reasoning=\{thinkingText\}/); assert.match(reportSource, /aria-label="分析"/); @@ -179,6 +183,10 @@ test("ordinary consultation replies reuse the shared Agent action bar", () => { assert.match(pageSource, /function regenerateLatestAnswer\(renderKey: string\)/); assert.match(pageSource, /messages: session\.messages\.slice\(0, -1\)/); assert.match(pageSource, /restoreOnFailure: session/); + assert.match(rectificationChat, /appendActivityTraceThinking/); + assert.match(rectificationChat, /startActivityTraceStep/); + assert.match(rectificationChat, /completeActivityTraceStep/); + assert.match(rectificationChat, /activityTrace = appendActivityTraceThinking\(activityTrace, event\.text\)/); assert.match(rectificationChat, / { + const state = reduceConsultationTimelineEvents([ + { type: "thinking.delta", text: "先确认用户说的是入职当天。" }, + { + type: "tool.started", + callId: "tool-1", + tool: "run-jyotish-consultation", + label: "正在计算本命盘…", + }, + { type: "thinking.delta", text: "再按宫位写。" }, + ]); + + assert.deepEqual(state.rows.map((row) => row.kind), ["think", "calculate", "think"]); + assert.equal(state.rows[0]?.status, "done"); + assert.equal(state.rows[0]?.thinkingText, "先确认用户说的是入职当天。"); + assert.equal(state.rows[2]?.status, "live"); + assert.equal(state.rows[2]?.thinkingText, "再按宫位写。"); + assert.notEqual(state.rows[0]?.id, state.rows[2]?.id); +}); + test("idle chat without a natal calculation has no calculate row", () => { const [section] = generalConsultationThinkingPlan(); assert.ok(section); diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts index ac5e1d82..71566593 100644 --- a/frontend/tests/rectification-agentic-entry.test.ts +++ b/frontend/tests/rectification-agentic-entry.test.ts @@ -313,6 +313,10 @@ test("rectification keeps receipts for the varga sentence and shows live tool pr assert.match(chat, /回答未完成,已保留现有内容;本次不会扣点/); assert.doesNotMatch(chat, /reasoning-delta|chain-of-thought/); assert.match(chat, /event.type === "thinking.delta"/); + assert.match(chat, /appendActivityTraceThinking/); + assert.match(chat, /startActivityTraceStep/); + assert.match(chat, /completeActivityTraceStep/); + assert.match(activityStatus, /activityTrace/); assert.match(activityStatus, /className="message-thinking"/); assert.match(activityStatus, />思考