fix(consult): treat pinched answers as failed and restore reply actions

Incomplete Flash generations were billed as completed consultations. Fail
those runs, keep the partial text, and reuse the rectification like/copy/rerun
bar on ordinary chat replies.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-19 19:40:50 +08:00
co-authored by Cursor
parent c38f11dbd3
commit 7d667fecbf
17 changed files with 487 additions and 89 deletions
+28
View File
@@ -42,6 +42,7 @@ import {
// calculation inside the 110s budget—so it is measured, not guessed.
export const AGENT_MAX_STEPS = 8;
export const AGENT_TIMEOUT_MS = 110_000;
export const CONSULTATION_MAX_OUTPUT_TOKENS = 8192;
const CONSULTATION_DOMAIN_DURATION_MS = 21_000;
const CONSULTATION_ANSWER_RESERVE_MS = 45_000;
export const CONSULTATION_DOMAIN_WALL_CLOCK_MS = AGENT_TIMEOUT_MS - CONSULTATION_ANSWER_RESERVE_MS;
@@ -50,6 +51,33 @@ export const MAX_CONSULTATION_DOMAINS = Math.max(
Math.floor(CONSULTATION_DOMAIN_WALL_CLOCK_MS / CONSULTATION_DOMAIN_DURATION_MS),
);
const thinkingDisabled = { thinking: { type: "disabled" as const } };
/**
* Visible-answer generation settings for one consult stream.
*
* DeepSeek V4 Flash thinks by default, and those hidden tokens share
* `max_tokens` with the spoken answer. Without an explicit visible budget and
* thinking turned off, a finished-looking stream can stop mid-heading with
* `finish_reason=length`. The provider id is repeated under `openai` because
* OpenAI-compatible adapters often look there first.
*/
export function consultationGenerationSettings(model?: unknown) {
const providerId = typeof model === "string"
? model
: model && typeof model === "object" && "providerId" in model && typeof model.providerId === "string"
? model.providerId
: undefined;
const providerOptions: Record<string, typeof thinkingDisabled> = {
openai: thinkingDisabled,
};
if (providerId) providerOptions[providerId] = thinkingDisabled;
return {
modelSettings: { maxOutputTokens: CONSULTATION_MAX_OUTPUT_TOKENS },
providerOptions,
};
}
// The raw plan bound stays at the registry default so a duplicate-heavy list
// canonicalizes instead of failing outright. The executable cap is enforced
// after canonicalization, where it can degrade and disclose rather than throw.