feat(consult): add an answer-step reasoning effort switch

The switch is off unless CONSULT_ANSWER_REASONING_EFFORT is set, so today's request body stays the same. A set value is sent as top-level reasoning_effort only on the answer step. The real A/B did not run: there is no model key.

Request-body probe, the named frontend tests (114 pass, 0 fail), tsc, and lint (0 errors) were run.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-15 08:31:19 +08:00
co-authored by Grok (agent loop) Cursor
parent 9274491681
commit ab7f66be14
13 changed files with 775 additions and 28 deletions
+22 -5
View File
@@ -75,6 +75,8 @@ import {
createConsultationRunClock,
consultationContinueGenerationSettings,
consultationGenerationSettings,
consultationGeneralPrepareStep,
consultAnswerEffortProviderOptions,
consultationNatalPrepareStep,
consultationWindowPrepareStep,
createConsultationAgentContext,
@@ -1288,6 +1290,8 @@ export async function POST(request: Request) {
prepareStep: (input: { stepNumber: number }) => consultationNatalPrepareStep({
stepNumber: input.stepNumber,
rectificationOffer: true,
answerReady: state.consultationToolCompleted,
model: selectedModel.model,
}),
};
const windowStreamOptions = {
@@ -1295,6 +1299,15 @@ export async function POST(request: Request) {
prepareStep: (input: { stepNumber: number }) => consultationWindowPrepareStep({
stepNumber: input.stepNumber,
rectificationOffer: true,
answerReady: state.consultationToolCompleted,
model: selectedModel.model,
}),
};
const generalStreamOptions = {
...streamOptions,
prepareStep: (input: { stepNumber: number }) => consultationGeneralPrepareStep({
stepNumber: input.stepNumber,
model: selectedModel.model,
}),
};
const answerPhaseSignal = runClock.answerSignal;
@@ -1305,10 +1318,10 @@ export async function POST(request: Request) {
agent: {
stream: (
messages: typeof baseMessages,
options: typeof streamOptions | typeof natalStreamOptions | typeof windowStreamOptions,
options: typeof streamOptions | typeof natalStreamOptions | typeof windowStreamOptions | typeof generalStreamOptions,
) => Promise<{ fullStream: AsyncIterable<unknown> | ReadableStream<unknown>; totalUsage: Promise<Usage> }>;
},
options: typeof streamOptions | typeof natalStreamOptions | typeof windowStreamOptions = streamOptions,
options: typeof streamOptions | typeof natalStreamOptions | typeof windowStreamOptions | typeof generalStreamOptions = streamOptions,
) {
try {
const result = await agent.stream(baseMessages, options);
@@ -1369,14 +1382,14 @@ export async function POST(request: Request) {
if (usesPublicDailyGeneralAgent(consultationMode, generalDailyContext)) {
state.workflowReceipt = workflowReceipt;
const agent = getGeneralJyotishAgent(selectedModel);
const result = await streamWithOverflowRetry(agent);
const result = await streamWithOverflowRetry(agent, generalStreamOptions);
// This mode has no calculation to require and no chart method to bind,
// so there is no contract for a retry to repair.
const retryForAnswer = async (retryHint?: string) => {
const retried = await agent.stream([
...baseMessages,
{ role: "user" as const, content: `上一轮没有输出任何回答文本。请直接给出这个问题的回答,不要只说明过程。${retryHint ? `\n${retryHint}` : ""}` },
], { ...streamOptions, abortSignal: answerPhaseSignal() });
], { ...generalStreamOptions, abortSignal: answerPhaseSignal() });
usages.push(retried.totalUsage);
return retried.fullStream;
};
@@ -1472,7 +1485,11 @@ export async function POST(request: Request) {
role: "user" as const,
content: `服务器窗口计算已经完成,但上一轮没有输出任何回答文本。请重新取回本次计算结果,然后直接给出回答;不要只描述过程或工具调用。${retryHint ? `\n${retryHint}` : ""}`,
},
], { ...streamOptions, abortSignal: answerPhaseSignal() });
], {
...streamOptions,
abortSignal: answerPhaseSignal(),
...consultAnswerEffortProviderOptions(selectedModel.model),
});
usages.push(retried.totalUsage);
return retried.fullStream;
};
@@ -16,6 +16,8 @@ export const AGENT_MAX_OUTPUT_TOKENS = AGENT_ANSWER_OUTPUT_TOKENS;
export type ThinkingMode = "enabled" | "disabled";
export type ReasoningEffort = "low" | "medium" | "high";
/** Top-level OpenAI-compatible `reasoning_effort`, including DeepSeek's `max`. */
export type ProviderReasoningEffort = "low" | "medium" | "high" | "max";
export type PromptCacheUsage = Readonly<{
readTokens: number;
@@ -100,6 +102,8 @@ export function agentGenerationSettings(
answerTokens?: number;
thinkingTokens?: number;
reasoningEffort?: ReasoningEffort;
/** Written beside `thinking`, not inside it, so compatible gateways emit `reasoning_effort`. */
providerReasoningEffort?: ProviderReasoningEffort;
} = {},
) {
const thinkingMode: ThinkingMode = options.thinking ?? "disabled";
@@ -110,6 +114,9 @@ export function agentGenerationSettings(
? { reasoningEffort: options.reasoningEffort }
: {}),
},
...(options.providerReasoningEffort
? { reasoningEffort: options.providerReasoningEffort }
: {}),
};
const providerId = typeof model === "string"
? model
+86 -5
View File
@@ -25,7 +25,7 @@ import { createConsultationPlan, type ConsultationPlan } from "../lib/consultati
import type { TechniqueAuditRow, WorkflowReceipt } from "../lib/consultation-agent-events.ts";
import { normalizeTechniqueAuditRows } from "../lib/consultation-technique-audit.ts";
import type { AgentModelFinishReason, EvidenceCardTelemetry } from "../lib/agent-observability.ts";
import { agentGenerationSettings, AGENT_SLICE_ANSWER_OUTPUT_TOKENS, AGENT_SLICE_THINKING_OUTPUT_TOKENS } from "../lib/agent-generation-settings.ts";
import { agentGenerationSettings, AGENT_SLICE_ANSWER_OUTPUT_TOKENS, AGENT_SLICE_THINKING_OUTPUT_TOKENS, type ProviderReasoningEffort } from "../lib/agent-generation-settings.ts";
import { chartCalculationProgressLabel, evidenceLookupActivityLabel } from "../lib/consultation-activity-labels.ts";
import { consultDomainConcurrency } from "../lib/consult/prep-flags.ts";
import {
@@ -176,6 +176,45 @@ export const CONSULTATION_EVIDENCE_LOOKUP_TOOL_ID = "read-consultation-evidence"
export const MAX_EVIDENCE_LOOKUPS_PER_TURN = 3;
export const CONSULTATION_WINDOW_CALC_TOOL_ID = "run-jyotish-window-consultation";
/**
* Answer-step reasoning effort. Unset or blank keeps today's request body.
* An illegal value warns once and is treated as unset.
*/
const CONSULT_ANSWER_REASONING_EFFORTS = ["low", "medium", "high", "max"] as const;
let consultAnswerReasoningEffortWarned = false;
export function consultAnswerReasoningEffort(): ProviderReasoningEffort | undefined {
const raw = process.env.CONSULT_ANSWER_REASONING_EFFORT;
if (raw === undefined || raw.trim() === "") return undefined;
const value = raw.trim();
if ((CONSULT_ANSWER_REASONING_EFFORTS as readonly string[]).includes(value)) {
return value as ProviderReasoningEffort;
}
if (!consultAnswerReasoningEffortWarned) {
consultAnswerReasoningEffortWarned = true;
console.warn(`CONSULT_ANSWER_REASONING_EFFORT=${value} is not low, medium, high, or max; leaving reasoning effort unset.`);
}
return undefined;
}
type AnswerEffortInput = {
answerReady?: boolean;
answerFromStart?: boolean;
model?: unknown;
};
function answerEffortOptions(input: AnswerEffortInput) {
const effort = consultAnswerReasoningEffort();
const answerStep = input.answerFromStart === true || input.answerReady === true;
if (!effort || !answerStep) return {};
return {
providerOptions: agentGenerationSettings(input.model, {
thinking: "enabled",
providerReasoningEffort: effort,
}).providerOptions,
};
}
/**
* Step 0 narrows with activeTools and leaves toolChoice auto. Thinking-mode
* providers reject named and required toolChoice (BUG-282, BUG-937).
@@ -185,35 +224,77 @@ export const CONSULTATION_WINDOW_CALC_TOOL_ID = "run-jyotish-window-consultation
* Later steps stay auto.
*
* Window and general agents must not share the natal hook.
*
* The answer step is any step that already holds a calculation (`answerReady`,
* the same flag as the answer clock) or a no-tool public-daily turn
* (`answerFromStart`). A retry starts a new stream at step 0 with the
* calculation already in hand, so the step number is not part of the check.
* The main loop's step 0 still has `answerReady` false.
*/
export function consultationNatalPrepareStep(input: { stepNumber: number; rectificationOffer?: boolean }) {
if (input.stepNumber !== 0) return { toolChoice: "auto" as const };
type ConsultPrepareInput = {
rectificationOffer?: boolean;
answerReady?: boolean;
answerFromStart?: boolean;
model?: unknown;
};
export function consultationNatalPrepareStep(input: { stepNumber: 0 } & ConsultPrepareInput): {
activeTools: string[];
toolChoice: "auto";
};
export function consultationNatalPrepareStep(input: { stepNumber: number } & ConsultPrepareInput): {
toolChoice: "auto";
activeTools?: string[];
};
export function consultationNatalPrepareStep(input: { stepNumber: number } & ConsultPrepareInput) {
const effort = answerEffortOptions(input);
if (input.stepNumber !== 0) return { toolChoice: "auto" as const, ...effort };
if (!input.rectificationOffer) {
return {
activeTools: [CONSULTATION_NATAL_CALC_TOOL_ID],
toolChoice: "auto" as const,
...effort,
};
}
return {
activeTools: [CONSULTATION_NATAL_CALC_TOOL_ID, RECTIFICATION_OFFER_TOOL_ID],
toolChoice: "auto" as const,
...effort,
};
}
export function consultationWindowPrepareStep(input: { stepNumber: number; rectificationOffer?: boolean }) {
if (input.stepNumber !== 0) return { toolChoice: "auto" as const };
export function consultationWindowPrepareStep(input: {
stepNumber: number;
rectificationOffer?: boolean;
answerReady?: boolean;
answerFromStart?: boolean;
model?: unknown;
}) {
const effort = answerEffortOptions(input);
if (input.stepNumber !== 0) return { toolChoice: "auto" as const, ...effort };
if (!input.rectificationOffer) {
return {
activeTools: [CONSULTATION_WINDOW_CALC_TOOL_ID],
toolChoice: "auto" as const,
...effort,
};
}
return {
activeTools: [CONSULTATION_WINDOW_CALC_TOOL_ID, RECTIFICATION_OFFER_TOOL_ID],
toolChoice: "auto" as const,
...effort,
};
}
export function consultAnswerEffortProviderOptions(model?: unknown) {
return answerEffortOptions({ answerReady: true, model });
}
/** Public daily has no chart tool. Its first step is already the answer. */
export function consultationGeneralPrepareStep(input: { stepNumber: number; model?: unknown }) {
return answerEffortOptions({ ...input, answerFromStart: true });
}
const CONSULTATION_DOMAIN_DURATION_MS = 31_000;
const CONSULTATION_ANSWER_RESERVE_MS = 45_000;
export const CONSULTATION_DOMAIN_WALL_CLOCK_MS = AGENT_TIMEOUT_MS - CONSULTATION_ANSWER_RESERVE_MS;