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:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user