fix(web): isolate rectification answers from tool-step planning text

Mastra intermediate text-delta was published as answer.delta, then set-focus domain errors reset the attempt and replayed evidence. Publish only the terminal no-tool step, persist the next probe on the server, and ground batch quotes in the source turn.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-24 21:26:57 +08:00
parent 167bdad20d
commit fe87a9ecdb
30 changed files with 1373 additions and 237 deletions
@@ -468,6 +468,8 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
action,
modelId: selectedModelId,
...(action === "message" ? { message: trimmed } : {}),
origin: "typed",
clientActionId: requestId,
}),
});
if (!response.ok) {
@@ -527,14 +529,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
raw += event.text;
activityTrace = freezeLiveThink(activityTrace);
const settled = settleRectificationSpokenAndThinking(raw, thinkingRaw);
const parsed = parseAgentReply(settled.spoken);
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
? {
...message,
text: parsed.text,
text: settled.spoken,
thinkingText: settled.thinking.trim() || undefined,
activityTrace,
state: parsed.text ? "streaming" : "thinking",
state: settled.spoken ? "streaming" : "thinking",
activity: nextActivityView(message.activity, {
phase: "answer-composition",
label: "正在组织回答…",
@@ -639,7 +640,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}
const settled = settleRectificationSpokenAndThinking(raw, thinkingRaw);
const parsed = parseAgentReply(settled.spoken);
const parsed = completed && !streamFailed ? parseAgentReply(settled.spoken) : { text: "", title: undefined };
const succeeded = completed && !streamFailed && Boolean(parsed.text);
setMessages((current) => current.flatMap((message): RenderMessage[] => {
if (message.renderKey !== assistantRenderKey) return [message];
@@ -656,10 +657,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
activity: undefined,
}];
}
if (streamFailed || hasActivityReceipt(completedReceipt) || parsed.text) {
if (streamFailed || hasActivityReceipt(completedReceipt) || settled.spoken.trim()) {
return [{
...message,
text: parsed.text,
text: settled.spoken,
thinkingText: settled.thinking.trim() || undefined,
activityTrace: completeActivityTrace(activityTrace),
state: "settled",
@@ -670,7 +671,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}
return [];
}));
if (!succeeded && parsed.text) {
if (!succeeded && settled.spoken.trim()) {
setError((current) => current || "回答未完成,已保留现有内容;本次不会扣点。");
} else if (!succeeded && runFailedMessage) {
setError((current) => current || runFailedMessage);
@@ -691,18 +692,17 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
: caught instanceof Error && caught.name === "AbortError";
if (aborted) {
const settled = settleRectificationSpokenAndThinking(raw, thinkingRaw);
const parsed = parseAgentReply(settled.spoken);
setMessages((current) => current.flatMap((message): RenderMessage[] => {
if (message.renderKey !== assistantRenderKey) return [message];
if (parsed.text || settled.thinking.trim()) {
if (settled.spoken.trim() || settled.thinking.trim() || hasActivityReceipt(completedReceipt)) {
return [{
...message,
text: parsed.text,
text: settled.spoken,
thinkingText: settled.thinking.trim() || undefined,
activityTrace: completeActivityTrace(activityTrace),
state: "settled",
completedReceipt,
failed: false,
failed: true,
turnId: completedTurnId,
}];
}
@@ -784,6 +784,8 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
probeId: choiceCard.probe_id,
optionId: optionId === "stop" ? undefined : optionId,
expectedRevision: choiceCard.case_revision ?? 0,
origin: "choice_click",
clientActionId: actionId,
}),
});
const payload = await response.json().catch(() => null);