fix(web): stop consultation thinking from pinching the answer

Disable provider thinking so Flash CoT cannot fill max_tokens, raise the
spoken budget to 16384, emit a server-owned step tree, and continue once
when the body ends on length.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-22 21:19:00 +08:00
co-authored by Cursor
parent d2a5f0f5bf
commit 0ca7da997f
26 changed files with 1013 additions and 88 deletions
+39 -3
View File
@@ -36,6 +36,7 @@ import { createServerSupabaseClient } from "@/lib/supabase/server";
import { streamTextResponse } from "@/lib/stream-text-response";
import { streamAgentResponse } from "@/lib/stream-agent-response";
import type { AgentExecutionReceipt, WorkflowReceipt } from "@/lib/consultation-agent-events";
import { consultationContinuePrompt, type PublicThinkingSection } from "@/lib/consultation-thinking-plan";
import {
AGENT_MAX_STEPS,
AGENT_TIMEOUT_MS,
@@ -501,6 +502,7 @@ export async function POST(request: Request) {
workflowReceipt: WorkflowReceipt,
agentExecutionReceipt?: AgentExecutionReceipt,
thinkingText?: string,
thinkingSections?: PublicThinkingSection[],
): Promise<AgentSettlementResult> {
try {
const reply = parseAgentReply(
@@ -513,6 +515,7 @@ export async function POST(request: Request) {
role: "assistant" as const,
text: reply.text,
...(persistedThinking ? { thinkingText: persistedThinking } : {}),
...(thinkingSections?.length ? { thinkingSections } : {}),
techniqueTruth,
workflowReceipt,
...(agentExecutionReceipt ? { agentExecutionReceipt } : {}),
@@ -724,6 +727,15 @@ export async function POST(request: Request) {
usages.push(retried.totalUsage);
return retried.fullStream;
};
const continueAfterLength = async (output: string) => {
const continued = await agent.stream([
...baseMessages,
{ role: "assistant" as const, content: output },
{ role: "user" as const, content: consultationContinuePrompt(output) },
], streamOptions);
usages.push(continued.totalUsage);
return continued.fullStream;
};
const executionReceipt = (): AgentExecutionReceipt => ({
runId: requestId,
runtime: "mastra-agentic",
@@ -748,6 +760,7 @@ export async function POST(request: Request) {
stream: result.fullStream,
requireTool: false,
retryForAnswer,
continueAfterLength,
continueAfterDisconnect: true,
transformText: createBirthTimeModeOutputGuard(
generalDailyContext ? "general_no_birth_time" : consultationMode,
@@ -758,13 +771,14 @@ export async function POST(request: Request) {
headers: { "x-jyotish-birth-time-mode": consultationMode },
onFirstActivity: markFirstActivity,
onFirstOutput: markFirstText,
onComplete: (output, agentExecutionReceipt, thinkingText) => settleRun(() => completeResponse(
onComplete: (output, agentExecutionReceipt, thinkingText, thinkingSections) => settleRun(() => completeResponse(
output,
mergeUsage(usages),
generalDailyContext ? "public-panchanga-only" : "not-applicable",
workflowReceipt,
agentExecutionReceipt,
thinkingText,
thinkingSections,
), undefined),
onError: (error) => settleRun(
cancel,
@@ -812,6 +826,15 @@ export async function POST(request: Request) {
usages.push(retried.totalUsage);
return retried.fullStream;
};
const continueAfterLength = async (output: string) => {
const continued = await agent.stream([
...baseMessages,
{ role: "assistant" as const, content: output },
{ role: "user" as const, content: consultationContinuePrompt(output) },
], streamOptions);
usages.push(continued.totalUsage);
return continued.fullStream;
};
const executionReceipt = (): AgentExecutionReceipt => ({
runId: requestId,
runtime: "mastra-agentic",
@@ -837,6 +860,7 @@ export async function POST(request: Request) {
requireTool: true,
retry,
retryForAnswer,
continueAfterLength,
continueAfterDisconnect: true,
transformText: createBirthTimeModeOutputGuard(consultationMode, false),
toolStatus: () => workflowStatus(state.workflowReceipt?.status),
@@ -844,13 +868,14 @@ export async function POST(request: Request) {
headers: { "x-jyotish-birth-time-mode": consultationMode },
onFirstActivity: markFirstActivity,
onFirstOutput: markFirstText,
onComplete: (output, agentExecutionReceipt, thinkingText) => settleRun(() => completeResponse(
onComplete: (output, agentExecutionReceipt, thinkingText, thinkingSections) => settleRun(() => completeResponse(
output,
mergeUsage(usages),
state.techniqueTruth ?? "declared-window",
state.workflowReceipt ?? workflowReceipt,
agentExecutionReceipt,
thinkingText,
thinkingSections,
), undefined),
onError: (error) => settleRun(
cancel,
@@ -900,6 +925,15 @@ export async function POST(request: Request) {
usages.push(retried.totalUsage);
return retried.fullStream;
};
const continueAfterLength = async (output: string) => {
const continued = await agent.stream([
...baseMessages,
{ role: "assistant" as const, content: output },
{ role: "user" as const, content: consultationContinuePrompt(output) },
], streamOptions);
usages.push(continued.totalUsage);
return continued.fullStream;
};
const executionReceipt = (): AgentExecutionReceipt => ({
runId: requestId,
runtime: "mastra-agentic",
@@ -925,6 +959,7 @@ export async function POST(request: Request) {
requireTool: true,
retry,
retryForAnswer,
continueAfterLength,
continueAfterDisconnect: true,
transformText: (text) => createBirthTimeModeOutputGuard(
consultationMode,
@@ -935,13 +970,14 @@ export async function POST(request: Request) {
headers: { "x-jyotish-birth-time-mode": consultationMode },
onFirstActivity: markFirstActivity,
onFirstOutput: markFirstText,
onComplete: (output, agentExecutionReceipt, thinkingText) => settleRun(() => completeResponse(
onComplete: (output, agentExecutionReceipt, thinkingText, thinkingSections) => settleRun(() => completeResponse(
output,
mergeUsage(usages),
state.techniqueTruth ?? "unknown",
state.workflowReceipt ?? workflowReceipt,
agentExecutionReceipt,
thinkingText,
thinkingSections,
), undefined),
onError: (error) => settleRun(
cancel,
+31
View File
@@ -844,6 +844,37 @@ button:disabled { cursor: default; opacity: .45; }
line-height: 1.55;
white-space: pre-wrap;
}
.consultation-step-tree__intent {
margin: 0 0 var(--space-2);
color: var(--color-ink-secondary);
font-size: 13px;
line-height: 1.5;
}
.agent-thinking-marker.is-pending {
background: var(--color-canvas-muted);
box-shadow: inset 0 0 0 1px var(--color-border);
}
.agent-thinking-step.is-more {
color: var(--color-ink-tertiary);
}
.consultation-thinking-report {
display: grid;
gap: var(--space-5);
}
.consultation-report-block {
display: grid;
gap: var(--space-2);
}
.consultation-report-analysis__label {
margin: 0 0 var(--space-2);
color: var(--color-ink);
font-size: 13px;
font-weight: 600;
line-height: 1.5;
}
.consultation-report-analysis .message-answer {
margin-top: 0;
}
.rectification-message-entry { min-width: 0; }
.message-actions {
display: flex;
+42 -15
View File
@@ -106,6 +106,12 @@ import {
type AgentExecutionReceipt,
type ConsultationAgentPublicEvent,
} from "@/lib/consultation-agent-events";
import {
applyThinkingSectionProgress,
parsePublicThinkingSections,
upsertThinkingSection,
type PublicThinkingSection,
} from "@/lib/consultation-thinking-plan";
import {
CONSULTATION_CHART_CALCULATION_LABEL,
CONSULTATION_COMPOSING_LABEL,
@@ -220,7 +226,13 @@ type ReplyOutcome = {
readonly phase: Extract<ChatReplyPhase, "completed" | "stopped" | "failed">;
readonly replyOrdinal: number;
};
type StreamingReply = { sessionId: string; text: string; activity?: AgentActivityView; thinkingText?: string };
type StreamingReply = {
sessionId: string;
text: string;
activity?: AgentActivityView;
thinkingText?: string;
thinkingSections?: PublicThinkingSection[];
};
type BirthPlace = {
label: string;
lat: number;
@@ -780,10 +792,12 @@ function readSessions(value: unknown, catalog: PublicLanguageModelCatalog | null
const thinkingText = typeof stored.thinkingText === "string" && stored.thinkingText.trim()
? stored.thinkingText.slice(0, 4000)
: undefined;
const thinkingSections = parsePublicThinkingSections(stored.thinkingSections);
return [{
role: stored.role,
text: stored.text.slice(0, 12000),
...(thinkingText ? { thinkingText } : {}),
...(thinkingSections.length ? { thinkingSections } : {}),
...(typeof stored.techniqueTruth === "string" ? { techniqueTruth: stored.techniqueTruth } : {}),
...(stored.agentExecutionReceipt ? { agentExecutionReceipt: stored.agentExecutionReceipt } : {}),
...(stored.workflowReceipt ? { workflowReceipt: stored.workflowReceipt } : {}),
@@ -1197,6 +1211,9 @@ export default function Home() {
const activeStreamingThinking = streamingReply && streamingReply.sessionId === activeSession?.id
? streamingReply.thinkingText
: undefined;
const activeStreamingSections = streamingReply && streamingReply.sessionId === activeSession?.id
? streamingReply.thinkingSections
: undefined;
const activeReplyOutcome = replyOutcome && replyOutcome.sessionId === activeSession?.id ? replyOutcome : null;
const replyPhase: ChatReplyPhase = isLoading
? consultationPhase === "recovering" ? "recovering" : "generating"
@@ -2015,6 +2032,7 @@ export default function Home() {
role: message.role,
text: message.text,
thinkingText: message.thinkingText,
thinkingSections: message.thinkingSections,
techniqueTruth: message.techniqueTruth,
agentExecutionReceipt: message.agentExecutionReceipt,
workflowReceipt: message.workflowReceipt,
@@ -3249,7 +3267,7 @@ export default function Home() {
}
setStreamingReply({ sessionId, text: "" });
let latestPartialReply = "";
let thinking = "";
let thinkingSections: PublicThinkingSection[] = [];
try {
const response = await fetch("/api/consult", {
method: "POST",
@@ -3314,10 +3332,11 @@ export default function Home() {
const updateStreamingAnswer = (activity?: AgentActivityView) => {
const partialReply = parseAgentReply(answer).text;
latestPartialReply = partialReply;
thinkingSections = applyThinkingSectionProgress(thinkingSections, partialReply);
setStreamingReply((current) => ({
sessionId,
text: partialReply,
thinkingText: current?.sessionId === sessionId ? current.thinkingText : undefined,
thinkingSections: thinkingSections.length ? thinkingSections : undefined,
activity: activity
? nextActivityView(current?.sessionId === sessionId ? current.activity : undefined, activity)
: current?.sessionId === sessionId ? current.activity : undefined,
@@ -3359,12 +3378,20 @@ export default function Home() {
if ((response.headers.get("content-type") ?? "").includes("application/x-ndjson")) {
const parser = createNdjsonParser((event) => {
if (event.type === "answer.delta") answer += event.text;
if (event.type === "thinking.delta") {
thinking = `${thinking}${event.text}`.slice(0, 4_000);
if (event.type === "thinking.section") {
thinkingSections = applyThinkingSectionProgress(
upsertThinkingSection(thinkingSections, {
id: event.id,
title: event.title,
heading: event.heading,
steps: event.steps,
}),
parseAgentReply(answer).text,
);
setStreamingReply((current) => ({
sessionId,
text: current?.sessionId === sessionId ? current.text : parseAgentReply(answer).text,
thinkingText: thinking,
thinkingSections,
activity: current?.sessionId === sessionId ? current.activity : undefined,
}));
}
@@ -3403,7 +3430,7 @@ export default function Home() {
messages: [...userSession.messages, {
role: "assistant",
text: reply.text,
thinkingText: thinking || undefined,
...(thinkingSections.length ? { thinkingSections } : {}),
techniqueTruth,
workflowReceipt,
agentExecutionReceipt,
@@ -3429,8 +3456,8 @@ export default function Home() {
if (!runCompleted && !truncatedFailure) {
throw new ConsultationResponseError(
502,
thinking.trim()
? "这次还没有生成可显示的回答。思考过程已保留,可以直接继续问。"
thinkingSections.length
? "这次还没有生成可显示的回答。思考步骤已保留,可以直接继续问。"
: "Agent 回答未完成,本次不会保存为成功咨询。",
);
}
@@ -3446,8 +3473,8 @@ export default function Home() {
if (controller.signal.aborted) return Boolean(latestPartialReply);
const reply = parseAgentReply(answer);
if (!reply.text) {
throw thinking.trim()
? new ConsultationResponseError(502, "这次还没有生成可显示的回答。思考过程已保留,可以直接继续问。")
throw thinkingSections.length
? new ConsultationResponseError(502, "这次还没有生成可显示的回答。思考步骤已保留,可以直接继续问。")
: new Error("Agent 没有返回可显示的回答,请重试。");
}
@@ -3464,7 +3491,7 @@ export default function Home() {
messages: [...userSession.messages, {
role: "assistant",
text: reply.text,
thinkingText: thinking || undefined,
...(thinkingSections.length ? { thinkingSections } : {}),
techniqueTruth,
workflowReceipt,
agentExecutionReceipt,
@@ -3508,13 +3535,13 @@ export default function Home() {
if (restore) {
updateSession(sessionId, () => restore);
void persistSession(restore).catch(() => {});
} else if (thinking.trim() || latestPartialReply) {
} else if (thinkingSections.length || latestPartialReply) {
const failedSession: ChatSession = {
...userSession,
messages: [...userSession.messages, {
role: "assistant",
text: latestPartialReply,
thinkingText: thinking.trim() || undefined,
...(thinkingSections.length ? { thinkingSections } : {}),
}],
updatedAt: timestamp(),
};
@@ -3950,7 +3977,7 @@ export default function Home() {
</div>
) : (
<div className="message-list" aria-busy={isLoading}>
{chatMessageViews(activeSession.messages, isLoading, activeStreamingText, activeStreamingActivity, activeStreamingThinking).map((message, index, views) => {
{chatMessageViews(activeSession.messages, isLoading, activeStreamingText, activeStreamingActivity, activeStreamingThinking, activeStreamingSections).map((message, index, views) => {
const showActions = message.role === "assistant"
&& message.state === "settled"
&& Boolean(message.text);