/** * errors.ts — Structured error types for consistent, actionable CLI diagnostics. * * Purpose: every error path in the CLI and headless orchestrator should be * able to emit context that helps users (and future debuggers) understand * *what* failed, *where*, and *what to try next* — without depending on * heavy error-handling libraries. * * Consumer: cli.ts, headless.ts, and any extension that surfaces user-facing * failures. The types are plain data so they serialize cleanly to stderr, * JSON batch output, and trace spans. */ // --------------------------------------------------------------------------- // Core structured error type // --------------------------------------------------------------------------- /** * A user-facing or machine-readable error record with rich context. * * All fields are optional except `message` so that call-sites can incrementally * adopt structured errors without rewriting every catch block at once. */ export interface StructuredError { /** Human-readable description of what went wrong. */ message: string; /** The high-level operation that was in progress when the error occurred, * e.g. "graph build", "session resume", "model validation". */ operation?: string; /** The file path most relevant to the failure (the file being read, * written, or expected). */ file?: string; /** The line number inside `file` if known (e.g. from a parser error). */ line?: number; /** Actionable guidance for the user — what to check or try next. */ guidance?: string; /** Whether retrying the same operation (with the same inputs) might * succeed, e.g. transient network failures. */ retry?: boolean; /** The underlying cause, if this error wraps another. Kept as `unknown` * so callers aren't forced to coerce to Error. */ cause?: unknown; } // --------------------------------------------------------------------------- // Convenience constructors // --------------------------------------------------------------------------- /** * Create a {@link StructuredError} from a message and optional context. * * Purpose: reduce boilerplate at catch sites where we want to enrich a raw * exception with operation/file context before logging or returning it. * * Consumer: cli.ts catch blocks, headless.ts event handlers. */ export function error( message: string, ctx?: Omit, ): StructuredError { return { message, ...ctx }; } // --------------------------------------------------------------------------- // Formatters // --------------------------------------------------------------------------- /** * Format a {@link StructuredError} as plain text suitable for stderr. * * Output shape (fields omitted when undefined): * [sf] Error: * Operation: * File: : * Guidance: * Retryable: yes|no */ export function formatStructuredError( err: StructuredError, prefix = "[sf]", ): string { const parts: string[] = [`${prefix} Error: ${err.message}`]; if (err.operation) { parts.push(` Operation: ${err.operation}`); } if (err.file) { const line = err.line !== undefined ? `:${err.line}` : ""; parts.push(` File: ${err.file}${line}`); } if (err.guidance) { parts.push(` Guidance: ${err.guidance}`); } if (err.retry !== undefined) { parts.push(` Retryable: ${err.retry ? "yes" : "no"}`); } return parts.join("\n") + "\n"; } /** * Format a {@link StructuredError} as a JSON object. * * Purpose: headless --output-format json mode can embed structured errors * in the result payload instead of interleaving free-form text on stderr. */ export function errorToJson(err: StructuredError): Record { const out: Record = { message: err.message }; if (err.operation !== undefined) out.operation = err.operation; if (err.file !== undefined) out.file = err.file; if (err.line !== undefined) out.line = err.line; if (err.guidance !== undefined) out.guidance = err.guidance; if (err.retry !== undefined) out.retry = err.retry; if (err.cause !== undefined) { out.cause = err.cause instanceof Error ? { message: err.cause.message, name: err.cause.name } : String(err.cause); } return out; } // --------------------------------------------------------------------------- // Predicates // --------------------------------------------------------------------------- /** * Narrow an `unknown` value to a {@link StructuredError}. * * Purpose: safe type guards at catch boundaries where the thrown value may * be a plain Error, a StructuredError, or something else entirely. */ export function isStructuredError(val: unknown): val is StructuredError { return ( typeof val === "object" && val !== null && "message" in val && typeof (val as Record).message === "string" ); }