/** * The LIVE half: judge each turn the moment it settles. * * This adds no control flow. `createChatTurnRoutes` already exposes a * `lifecycle` seam that fires exactly one of `onTurnComplete`/`onTurnError` * after a turn settles, and already swallows hook errors so telemetry cannot * fail a turn. That seam was shipped and then wired by nobody, which is a fair * description of why the outage lasted 17 days. This function fills it. * * The shape is declared structurally rather than imported from * `/chat-routes`, so `/turn-health` stays free of the server chat vertical and * can be used by any turn driver that reports the same three moments. */ import { type AlertSink } from './sink.js'; /** Structural mirror of `/chat-routes`' `ChatTurnLifecycle` complete payload. */ export interface TurnHealthCompleteInfo { finalText: string; usage?: { outputTokens?: number | null; } | null; durationMs: number; threadId?: string; turnStreamId?: string; executionId?: string; /** `/chat-routes` sets this when a `contextGate` short-circuited the turn and * the producer never ran. */ gated?: boolean; } /** Structural mirror of the lifecycle error payload. */ export interface TurnHealthErrorInfo { error: unknown; durationMs: number; threadId?: string; turnStreamId?: string; executionId?: string; } /** What {@link createTurnHealthLifecycle} returns — assignable to * `createChatTurnRoutes`' `lifecycle` option. */ export interface TurnHealthLifecycle { onTurnComplete(info: TurnHealthCompleteInfo): Promise; onTurnError(info: TurnHealthErrorInfo): Promise; } export interface TurnHealthLifecycleOptions { /** Names the product in every alert. */ product: string; sink: AlertSink; /** Called for every verdict, healthy or not — the hook for a counter or a * metrics push. Alerts are for humans; this is for graphs. */ onVerdict?(verdict: { product: string; healthy: boolean; kinds: string[]; durationMs: number; }): void; /** Page when a turn was answered by a gate instead of the model. * * Default `false`, and the default is the honest one: gating is a legitimate * design and a product that gates its intake would otherwise page on every * healthy turn. Whether the rate is pathological is domain knowledge, so it * stays a product decision — the verdict is ALWAYS reported through * {@link TurnHealthLifecycleOptions.onVerdict} so a counter can watch the * rate even when nobody is paged. */ alertOnGatedTurn?: boolean; } /** * Build the lifecycle hooks that page on a turn which succeeded at nothing. * * The live lane sees `finalText` and usage but not the persisted parts, so it * catches the blank-completion and hard-failure shapes immediately. The * parts-dependent shapes (a tool call whose arguments never parsed, a tool * call that left no effect) are caught by {@link sweepSilentFailures}, which * reads what was actually written to the store — the honest place to ask * whether an effect persisted. */ export declare function createTurnHealthLifecycle(options: TurnHealthLifecycleOptions): TurnHealthLifecycle;