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:
co-authored by
Grok (agent loop)
Cursor
parent
9274491681
commit
ab7f66be14
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user