/** * Concrete finish reasons for rectification Agent runs. * * Incomplete turns must not be described only as “请稍后再试”. The code is * durable; the Chinese copy is for the live client and is not persisted as a * chat message. */ export const RECTIFICATION_FINISH_REASONS = [ "stop", "tool_calls", "length", "timeout", "max_steps", "provider_error", "aborted", "unknown", ] as const; export type RectificationFinishReason = (typeof RECTIFICATION_FINISH_REASONS)[number]; export type RectificationRunDiagnostic = Readonly<{ runId: string; modelId: string; finishReason: RectificationFinishReason; inputTokens: number | null; reasoningTokens: number | null; outputTokens: number | null; stepCount: number; toolCallCount: number; readCasePayloadBytes: number | null; elapsedMs: number; lastCompletedTool: string | null; stateMutationCommitted: boolean; expectedWrite: "evidence" | "none" | "unknown" | null; collectIntent: "classified" | "unclassified" | null; }>; const USER_COPY: Readonly> = { answer_truncated: "模型输出达到上限,状态已记录。", length: "模型输出达到上限,状态已记录。", run_timeout: "服务端运行超时,状态已记录。", timeout: "服务端运行超时,状态已记录。", stream_aborted: "服务端运行超时,状态已记录。", stream_unfinished: "服务端运行超时,状态已记录。", max_steps: "本轮达到步骤上限,状态已记录。", tool_calls: "本轮达到步骤上限,状态已记录。", provider_error: "上游模型连接失败,状态已记录。", run_failed: "上游模型连接失败,状态已记录。", empty_stream: "本轮没有生成可展示的回复,状态已记录。", evidence_not_written: "这件我还没记上。请再说一次大概年月和发生的事。", }; export function finishReasonFromErrorCode(errorCode: string | null | undefined): RectificationFinishReason { if (errorCode === "answer_truncated") return "length"; if (errorCode === "run_timeout" || errorCode === "stream_unfinished") return "timeout"; if (errorCode === "stream_aborted") return "aborted"; if (errorCode === "max_steps") return "max_steps"; if (errorCode === "provider_error" || errorCode === "run_failed") return "provider_error"; return "unknown"; } export function userFacingRunFailure(errorCode: string | null | undefined): string { if (!errorCode) return "本轮没有完成,状态已记录。"; return USER_COPY[errorCode] ?? "本轮没有完成,状态已记录。"; } export function isIncompleteRunBanner(text: string): boolean { return /本轮处理未完成|当前进度已保留|请稍后再试/.test(text); } export function mapModelFinishToErrorCode(input: { finishReason: string | null; aborted: boolean; timedOut: boolean; answerText: string; stepCount: number; maxSteps: number; }): string | null { if (input.timedOut) return "run_timeout"; if (input.aborted) return "stream_aborted"; if (input.finishReason === "length") return "answer_truncated"; if (input.finishReason === "error") return "provider_error"; if ( (input.finishReason === "tool-calls" || input.finishReason === "tool_calls") && !input.answerText.trim() ) { return "max_steps"; } if (input.stepCount >= input.maxSteps && !input.answerText.trim()) return "max_steps"; return null; }