/** * Recovering tool calls a HOST failed to parse. * * A model emits tool calls in its own dialect; the serving host is supposed to parse that into the * OpenAI `message.tool_calls` field. NIM and OpenRouter do. Some free hosts do not, and hand back * the raw envelope as ordinary assistant TEXT. Before this module the consequence was silent and * total: no `tool_calls` ⇒ `stop_reason: end_turn` ⇒ zero `tool_use` blocks ⇒ the validator finds * nothing malformed and PASSES ⇒ repair never engages ⇒ the client receives markup it treats as a * final answer. Measured 2026-08-08: a task wrote its output correctly and then emitted * `` as its entire visible response. * See docs/tool-call-dialect-leak.md. * * ⚠ This is PARSING, not inference — the same side of the repair boundary as fixing args that * violate a schema. The envelope set is CLOSED (same reasoning as the effort vocabulary in * sync-tiers.mjs and the alias list in authEnv.ts): prose that merely mentions a tool name is not a * tool call, and promoting it to one would be fabricating intent, which is the one thing the repair * path must never do. A dialect we do not recognize yields `detected` at most, never a guess. */ /** JSON Schema fragment, only the parts used to coerce a stringly-typed parameter. */ interface SchemaLike { type?: unknown; properties?: Record; items?: SchemaLike; } /** * The relay-owned error code for a dialect-rescue destructive refusal, on every surface that can * carry one: the buffered error bodies, the mid-stream SSE `error` event, and the commit probe's * classifier. * * It has ONE job beyond naming the failure: the probe must read it as RELAY-authored, not * upstream. An in-band error is provenance `upstream` by default and therefore retriable, which * would let a streamed pre-commit refusal reroll onto the next candidate while the buffered lanes * treat the same refusal as terminal — two paths, one policy. See * docs/history/dialect-rescue-destructive-refusal-2026-08-24.md §3. */ export declare const DIALECT_REFUSED_DESTRUCTIVE_CODE = "tool_dialect_refused_destructive"; /** * Proof that the refusal event on a stream is the RELAY's, set by the wrapper that emitted it. * * ⚠ The code above is not that proof. It travels on the wire, so a stream can carry it without the * relay having written it — on an `anthropic`-kind target the body is a byte passthrough and the * dialect wrapper never runs at all, so EVERY occurrence there is the upstream's. Classifying on * the bytes let a counterparty mint `local` provenance for itself, which suppresses failover AND * exempts it from breaker accounting (`relay-mapper-defect` outcomes are dropped): a hostile member * could black-hole a request a healthy sibling would have served, and take no health hit for it. * * So provenance is DECLARED, never inferred from the counterparty's bytes — the same rule * `credentialState()` follows for credential containment. Only the wrapper that pushed the event * sets `refused`, and `stream-commit.ts` requires BOTH the flag and the code before it will call a * dead verdict `local`. */ export interface DialectRefusalSignal { refused: boolean; } /** A fresh, un-refused signal for one wrapped stream. */ export declare function dialectRefusalSignal(): DialectRefusalSignal; /** * Render the refused tool names for an error message, bounded. * * The names are model-authored: an envelope's `name="…"` attribute, admitted only because it * MATCHED the operator's list — and a prefix pattern (`git_*`) admits arbitrary text after the * prefix. So the wording is bounded here rather than at each of the four seams, the same reasoning * as `stream-commit.ts` `boundedError`. One definition, because a message that is truncated on * three lanes and unbounded on the fourth is the asymmetry this whole change exists to remove. */ export declare function describeRefused(refused: readonly string[]): string; export interface DialectToolCall { name: string; input: Record; } export type DialectOutcome = /** No tool-call framing present. The text is just text. */ { status: "none"; } /** Framing found and fully parsed. `text` is what remains after removing the envelope. */ | { status: "parsed"; calls: DialectToolCall[]; text: string; dialect: string; } /** * Framing markers present but unparseable — truncated mid-stream, or a dialect variant we do not * model. NEVER guessed at: the caller fails clean so failover reaches a host that parses. */ | { status: "detected"; dialect: string; } /** * Framing parsed, but a recovered call names a tool the operator listed as destructive. * * A backend emitting native `tool_calls` has stated its own protocol intent and the destructive * list has never governed that. Rescue is the relay deciding that model TEXT is a tool call — * for `Bash`/`Write`/`Edit` under `--dangerously-skip-permissions` that is the relay authoring a * destructive call the host never made, which "refused, never fabricated" forbids. * * Refused WHOLE, never partially: committing the surviving calls and dropping this one would * silently change the model's intent, the same reasoning `guardReshaped`'s structural * conservation rests on. `refused` carries the offending names — they come from the operator's * own configured list, so announcing them leaks nothing; the recovered ARGUMENTS never travel. */ | { status: "refused-destructive"; dialect: string; refused: string[]; }; export declare function detectDialect(text: string): string | null; /** * How much of a partial text stream is safe to forward, and whether a marker has landed. * * The streaming-needle problem: a marker arrives split across SSE deltas, so forwarding each delta * as it comes would emit the first half of an envelope before we know it is one. `safeLen` is * everything except the longest trailing run that could still GROW into a marker, so the caller can * stream normally while never emitting into an envelope it is about to capture. * * ⚠ The holdback is bounded by the longest marker, so ordinary prose containing `<` streams with at * most that many characters of lag — it does not degrade into buffering the whole response, which * would trade this bug for a latency regression on every tool-bearing request. */ export declare function scanForMarker(text: string): { safeLen: number; hit: boolean; }; /** * Index at which a marker begins, for splitting emitted prose from a captured envelope. * * ⚠ Backs up over the tag opener. Markers deliberately omit the `<` / `, isDestructive: (name: string) => boolean): DialectOutcome; export {};