fix(consult): 第 0 步改回 auto,供应商 error 块不再被吞掉

BUG-937 撤回 thinking 模式下的 required toolChoice,只留 activeTools。BUG-938 让咨询流识别 Mastra error 块,公开码 calculation_failed,内部码进日志且不触发合同 retry。
This commit is contained in:
jesse-ux
2026-09-17 23:37:49 +08:00
parent b51e7aff43
commit 8b11ae7dab
11 changed files with 278 additions and 19 deletions
+2
View File
@@ -185,6 +185,8 @@ const knownErrorCodes = new Set([
"timeout",
"cancelled",
"settlement_failed",
"thinking_tool_choice_unsupported",
"provider_error",
]);
export function toAgentObservabilityErrorCode(error: unknown): string {
+38 -1
View File
@@ -20,7 +20,7 @@ import {
type PublicThinkingSection,
} from "./consultation-thinking-plan.ts";
type Chunk = { type?: string; payload?: Record<string, unknown>; data?: unknown };
type Chunk = { type?: string; payload?: Record<string, unknown>; data?: unknown; error?: unknown };
type ChunkStream = AsyncIterable<unknown> | ReadableStream<unknown>;
async function* readChunks(stream: ChunkStream): AsyncIterable<Chunk> {
@@ -90,11 +90,33 @@ function isTimeoutOrAbort(error: unknown) {
}
type RunFailedCode = "runtime_contract_incomplete" | "empty_answer" | "answer_truncated" | "calculation_failed";
type ProviderStreamErrorCode = "thinking_tool_choice_unsupported" | "provider_error";
function providerErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function classifyProviderStreamError(error: unknown): ProviderStreamErrorCode {
return providerErrorMessage(error).includes("Thinking mode does not support this tool_choice")
? "thinking_tool_choice_unsupported"
: "provider_error";
}
function chunkProviderError(chunk: Chunk): unknown {
if (chunk.error !== undefined) return chunk.error;
return chunk.payload?.error;
}
function runFailedCode(error: unknown, emitted: boolean): RunFailedCode {
if (error instanceof Error && error.message === "runtime_contract_incomplete") return "runtime_contract_incomplete";
if (error instanceof Error && error.message === "empty_answer") return "empty_answer";
if (error instanceof Error && error.message === "answer_truncated") return "answer_truncated";
if (
error instanceof Error
&& (error.message === "thinking_tool_choice_unsupported" || error.message === "provider_error")
) {
return "calculation_failed";
}
if (emitted && isTimeoutOrAbort(error)) return "answer_truncated";
return "calculation_failed";
}
@@ -383,6 +405,21 @@ export function streamAgentResponse(options: StreamAgentResponseOptions) {
const stepCountBeforeAttempt = options.state.modelStepCount;
try {
for await (const chunk of readChunks(stream)) {
if (chunk.type === "error") {
const raw = chunkProviderError(chunk);
const code = classifyProviderStreamError(raw);
appendConsultationRuntimeStep(options.state, {
kind: "validation",
name: "model-stream-error",
status: "failed",
});
console.error("[consult-provider-error]", {
requestId: options.requestId,
code,
messageHead: providerErrorMessage(raw).slice(0, 200),
});
throw new Error(code);
}
for (const event of mapChunk(chunk, options, startedAt, toolErrors)) send(controller, event);
flushThinkingPlan(controller);
if (chunk.type === "step-finish") options.state.modelStepCount += 1;
+7 -6
View File
@@ -60,10 +60,11 @@ export const CONSULTATION_NATAL_CALC_TOOL_ID = "run-jyotish-consultation";
export const CONSULTATION_WINDOW_CALC_TOOL_ID = "run-jyotish-window-consultation";
/**
* Step 0 must call the natal chart tool. Mastra/AI SDK types accept
* toolChoice "required" (`@mastra/core` Agent.stream options:
* `'auto' | 'none' | 'required'`). Later steps stay auto so the model can
* write after the request-scoped calculation is in hand.
* Step 0 exposes only the natal chart tool. Some thinking-mode providers
* reject named and required toolChoice (BUG-282, BUG-937); the first step
* therefore narrows with activeTools and leaves the choice as auto. Later
* steps stay auto so the model can write after the request-scoped
* calculation is in hand.
*
* Window and general agents must not share this hook: they do not own this tool.
*/
@@ -71,7 +72,7 @@ export function consultationNatalPrepareStep(input: { stepNumber: number }) {
return input.stepNumber === 0
? {
activeTools: [CONSULTATION_NATAL_CALC_TOOL_ID],
toolChoice: "required" as const,
toolChoice: "auto" as const,
}
: {
toolChoice: "auto" as const,
@@ -82,7 +83,7 @@ export function consultationWindowPrepareStep(input: { stepNumber: number }) {
return input.stepNumber === 0
? {
activeTools: [CONSULTATION_WINDOW_CALC_TOOL_ID],
toolChoice: "required" as const,
toolChoice: "auto" as const,
}
: {
toolChoice: "auto" as const,