/** * Map a provider's native finish-reason value onto the neutral vocabulary. * * @param {string|null|undefined} raw - the provider's native value (`stop_reason` / `finish_reason` / * `finishReason` / `done_reason`). Absent or non-string ⇒ `null` (pre-BA-6 behavior). * @param {'anthropic'|'openai'|'gemini'|'ollama'} provider - which table to read. * @param {{hasToolCalls?: boolean}} [ctx] - what the round actually CARRIED. See below: two providers * cannot express "stopped to call a tool" in their finish-reason field at all, so the round's own * content is the only place that fact exists. * @returns {string|null} a neutral value, an unrecognized value passed through verbatim, or `null`. */ export function normalizeStopReason(raw: string | null | undefined, provider: "anthropic" | "openai" | "gemini" | "ollama", ctx?: { hasToolCalls?: boolean; }): string | null; /** * Did this round get CUT OFF at the output-token cap? * * The one predicate the Loop acts on. Deliberately narrow: `context_exceeded`, `refusal` and * `pause_turn` are all "not a normal finish" but they are NOT output-cap truncations and must not be * folded in here — `pause_turn` in particular is a RESUMABLE state, and erroring on it would break * server-side tool flows that are working exactly as designed. * * Retained for back-compat (it was the Loop's original BA-6 gate). The Loop now routes through * {@link classifyStopReason} instead — `isTruncated(x)` is exactly `classifyStopReason(x) === 'truncated'`. * * @param {string|null|undefined} stopReason - a NEUTRAL value (post-{@link normalizeStopReason}). * @returns {boolean} */ export function isTruncated(stopReason: string | null | undefined): boolean; /** * @param {string|null|undefined} stopReason - a NEUTRAL value (post-{@link normalizeStopReason}). * @returns {'truncated'|'refusal'|'context_exceeded'|'resume'|null} */ export function classifyStopReason(stopReason: string | null | undefined): "truncated" | "refusal" | "context_exceeded" | "resume" | null;