import type { Api } from "../types"; import type { AbortSourceTracker } from "../utils/abort"; import type { CapturedHttpErrorResponse, RawHttpRequestDump } from "../utils/http-inspector"; import { classify, classifyMessage, status } from "./flags"; import { formatMessage } from "./format"; /** Context a provider catch block hands to {@link finalize}. */ export interface FinalizeOptions { /** Wire API, for api-specific text classification (e.g. stale-responses items). */ api?: Api; /** Provider id; forwarded to the message formatter for copilot rewrites. */ provider?: string; /** Caller signal, for providers that don't run an abort tracker. */ signal?: AbortSignal; /** Abort tracker, preferred over `signal`: distinguishes caller vs. local aborts. */ abortTracker?: AbortSourceTracker; /** Raw request, dumped into the message for 400-class failures. */ rawRequestDump?: RawHttpRequestDump; /** Captured non-2xx response body, used for status fallback and message detail. */ capturedErrorResponse?: CapturedHttpErrorResponse; } /** The full bundle a provider assigns onto its `AssistantMessage` error fields. */ export interface FinalizeResult { /** Structured flag id from {@link classify}. */ id: number; /** HTTP status, from the error or the captured response. */ status: number | undefined; /** `"aborted"` when the caller cancelled, otherwise `"error"`. */ stopReason: "aborted" | "error"; /** User-facing message from {@link formatMessage}, or a local abort reason. */ message: string; } /** * Build the complete error bundle for a provider catch block, replacing the * `stopReason` / `errorStatus` / `errorId` / `errorMessage` boilerplate. * * `stopReason` comes from the abort tracker (caller intent dominates) or, when * no tracker is supplied, the raw `signal.aborted`. A local abort reason (e.g. a * first-event timeout) supersedes the formatted message. Message formatting is * wrapped so a formatter throw can never skip the caller's `stream.end()`. */ export async function finalize(error: unknown, opts: FinalizeOptions = {}): Promise { const aborted = opts.abortTracker ? opts.abortTracker.wasCallerAbort() : opts.signal?.aborted === true; const currentStatus = status(error) ?? opts.capturedErrorResponse?.status; let message: string; try { const localReason = opts.abortTracker?.getLocalAbortReason(); message = localReason?.message ?? (await formatMessage(error, opts)); } catch { message = error instanceof Error ? error.message : String(error); } const id = classifyMessage({ api: opts.api, errorId: classify(error, opts.api), errorMessage: message, errorStatus: currentStatus, }); return { id, status: currentStatus, stopReason: aborted ? "aborted" : "error", message, }; }