// V12b / V12b-T — top-level `Err` formatting and chain attribution at the // slash-dispatch boundary. // // This module owns the renderer that turns a top-level `QueryError` — returned // to the slash-dispatch boundary by a directly-slash-invoked theta (prompt OR // subagent mode) — into the single-line `theta-system-note` string Pi appends to // the user's session: // // - SLSH-3 — a top-level `Err` at the slash-dispatch boundary renders ONE // line; for a directly-slash-invoked subagent-mode theta it is the sole // user-facing surface for the failure (the transcript stays private). // - SLSH-4 — the per-kind note templates (SNK-a … SNK-k) render verbatim; the // renderer is total over any unlisted `kind` in the `QueryError` union via // the SNK-k catch-all row. // - SLSH-5 — chain attribution: when the leaf failure cascaded out of an // `invoke`d child via `?` (the rendered error is `invoke_callee`, possibly // nested), the renderer appends ` from invoked at // :` per `invoke_callee` hop, leaf-first (innermost hop // first). The leaf `kind` (the innermost non-`invoke_callee` variant) drives // the per-kind row; `` is the wrapper's `callee_path`, and the // `:` provenance is consumed from V15g's per-frame // invocation record — this leaf renders from that record, it does not // derive source positions here. // // V12b-T (tests-task) declares this seam and stubs the two render entries // inertly / non-compliantly (they return a fixed sentinel, ignoring the SLSH-4 // templates and the SLSH-5 chain suffix), so the failing V12b-T tests red on // their own primary string-equality assertions rather than on a compile error, // a missing fixture, or a harness throw. The paired V12b implementation fills // in the per-kind templates and the leaf-first chain walk. // // Spec: slash-invocation.md (SLSH-3, SLSH-4, SNK-a…SNK-k, SLSH-5), // errors-and-results/queryerror-variants.md (the nine-variant union). import type { CodeToolError, ContextOverflowError, InvokeCalleeError, InvokeInfraError, ModelToolError, QueryError, ToolLoopExhaustedError, TransportError, ValidationError, } from "./query-error"; import type { InvocationRecord } from "./invoke-provenance"; /** * The SLSH-4 template separator: em-dash U+2014. The registry/template cells * carry the literal em-dash; only the readability backticks are stripped. */ const DASH = "\u2014"; /** * One `invoke_callee` hop's rendering inputs. Each hop pairs the wrapper's * `callee_path` (the invoked child, SLSH-5 ``) with the V15g * per-frame invocation record that carries the call-site provenance * (`:`). The provenance is consumed from the record, never * re-derived here (V12b-T asserts rendering from the record; V15g is its * producer). */ export interface ChainHop { /** * The wrapper `InvokeCalleeError.callee_path` — the post-`realpath` absolute * path of the invoked child at this hop (SLSH-5 ``). */ readonly calleePath: string; /** * V15g's per-frame invocation record for this hop: the parent theta's * post-`realpath` path and the 1-indexed call-site line (SLSH-5 * `` and ``). */ readonly record: InvocationRecord; } /** * Inputs to the top-level `Err`-note renderer (SLSH-3/SLSH-4/SLSH-5). */ export interface ErrNoteInput { /** The theta's slash name (its filename stem), e.g. `entry`. SLSH ``. */ readonly thetaName: string; /** * The top-level `QueryError` returned to the slash-dispatch boundary. May be * an `invoke_callee` wrapper (possibly nested); the renderer recurses through * `inner` to the leaf variant, which drives the per-kind row. */ readonly error: QueryError; /** * One `ChainHop` per `invoke_callee` hop, in the order the hops are * encountered walking `inner` from the top-level error inward (OUTERMOST hop * first). Empty for a non-cascaded (non-`invoke_callee`) top-level error. The * renderer emits the hop suffixes leaf-first (reversing this order) per * SLSH-5. */ readonly chain: readonly ChainHop[]; } /** * SLSH-4 per-kind leaf rendering: render the single-line per-`kind` note for a * leaf (non-`invoke_callee`) `QueryError`, verbatim per the SNK-a … SNK-k rows, * total over any unlisted `kind` via the SNK-k catch-all. No chain suffix. * * The V12b-T stub returns the sentinel so the per-kind string assertions red. */ export function renderLeafKindNote(thetaName: string, leaf: QueryError): string { const prefix = `theta /${thetaName}`; // `kind` is typed `string` (ERR-15 discriminator openness), so the union // members are not TS-discriminable by tag; each branch casts to the variant // whose fields it reads. Any tag outside the theta 1.0.0 set falls to SNK-k, // making the renderer total over the open discriminator. switch (leaf.kind) { case "validation": { const e = leaf as ValidationError; // SNK-a (schema_validation) / SNK-b (empty_template) share `kind`, // keyed on `cause`. if (e.cause === "empty_template") { // SNK-b return `${prefix} returned Err: rendered query template was empty ${DASH} no provider turn was issued`; } // SNK-a return `${prefix} returned Err: model failed schema after ${e.attempts} respond-repair attempts`; } case "transport": { // SNK-c const e = leaf as TransportError; return `${prefix} returned Err: transport ${DASH} ${e.message}`; } case "model_tool": { // SNK-d const e = leaf as ModelToolError; return `${prefix} returned Err: tool ${e.tool_name} failed ${DASH} ${e.message}`; } case "context_overflow": { // SNK-e void (leaf as ContextOverflowError); return `${prefix} returned Err: context overflow`; } case "cancelled": { // SNK-f return `${prefix} cancelled`; } case "code_tool": { // SNK-g const e = leaf as CodeToolError; return `${prefix} returned Err: tool ${e.tool_name} call failed (${e.cause}) ${DASH} ${e.message}`; } case "tool_loop_exhausted": { // SNK-h — `last_tool_name` renders as the literal `respond` when null // (defensive forward-compat rendering; no theta 1.0-reachable null case). const e = leaf as ToolLoopExhaustedError; const lastTool = e.last_tool_name ?? "respond"; return `${prefix} returned Err: tool-call loop exhausted after ${e.rounds} rounds (last tool: ${lastTool})`; } case "invoke_infra": { // SNK-i const e = leaf as InvokeInfraError; return `${prefix} returned Err: invoke of ${e.callee_path} failed (${e.cause})`; } default: { // SNK-k catch-all — total over any unlisted `kind` in the open union. return `${prefix} returned Err: ${leaf.kind} ${DASH} ${leaf.message}`; } } } /** * SLSH-3/SLSH-4/SLSH-5 top-level renderer: render the one-line `theta-system-note` * string for a top-level `Err` at the slash-dispatch boundary. Recurses through * any `invoke_callee` wrapper to the leaf variant (which drives the per-kind * row), then appends the SLSH-5 chain suffix leaf-first. * * The V12b-T stub returns the sentinel so the SLSH-3/SLSH-4/SLSH-5 string * assertions red on their own primary comparison. */ export function renderTopLevelErrNote(input: ErrNoteInput): string { // Walk `inner` through any `invoke_callee` wrapper(s) to the leaf variant; // the leaf `kind` drives the per-kind row (SLSH-5), never the wrapper. let leaf: QueryError = input.error; while (isInvokeCalleeError(leaf)) { leaf = leaf.inner; } const row = renderLeafKindNote(input.thetaName, leaf); // SLSH-5 chain suffix: one ` from invoked at :` // per hop, emitted leaf-first (innermost hop first). `input.chain` is supplied // OUTERMOST-first, so reverse it to render leaf-first. const suffix = input.chain .slice() .reverse() .map( (h) => ` from ${h.calleePath} invoked at ${h.record.parentPath}:${h.record.callSiteLine}`, ) .join(""); return row + suffix; } /** * Narrowing helper the renderer (and its tests) use to recognise the * `invoke_callee` wrapper variant. Exported so the paired V12b implementation * and the V12b-T tests share one recogniser rather than re-checking the tag * inline. Non-behavioural (pure discriminator check), so it is compliant as-is. */ export function isInvokeCalleeError(error: QueryError): error is InvokeCalleeError { return error.kind === "invoke_callee"; }