import type { RunawayConfig } from "./config.js"; /** * Runaway guard: a model that degenerates mid-reply repeats the same block of text over and over without calling a tool, * and nothing else stops it before the token limit. The guard reads the stream, counts identical blocks with code only, * and stops the run when a block repeats past the threshold. No request leaves the machine. * * Calibration over 675 local sessions (2634 assistant text blocks of 300+ characters): the most repeated paragraph in an * ordinary reply occurs twice at most; the one runaway repeated its block 28 times. Thinking drafts code and repeats * paragraphs up to 7 times legitimately, so it has its own higher threshold. */ export type StreamKind = "text" | "thinking"; export interface RepeatSignal { count: number; /** Normalised block that repeated; empty when nothing qualified. */ block: string; } export interface RunawayVerdict { kind: StreamKind; count: number; block: string; /** Characters streamed for this kind when the run was stopped. */ chars: number; /** block: an identical paragraph; phrase: the text ends with one unit repeated back to back. */ signal: "block" | "phrase"; } /** Paragraphs shorter than this (normalised) are ignored: code fences, list markers, and short labels repeat legitimately. */ export declare const BLOCK_MIN_CHARS = 24; /** Length of the trailing phrase whose recurrence is counted. */ export declare const PHRASE_CHARS = 60; /** Characters streamed between two checks; keeps the per-token cost to a string append. */ export declare const CHECK_EVERY = 256; /** The paragraph (blank-line separated, normalised, at least `minChars`) that occurs most often. */ export declare function repeatedBlock(text: string, minChars?: number): RepeatSignal; /** * Run-on repetition without paragraph breaks: the text ends with one unit repeated back to back. The trailing `chars` * locate the period (distance to their previous occurrence); the whole unit must match on every step back, so templated * sentences that share only a suffix ("item 3 … see below.", "item 4 … see below.") count once. Units shorter than * `minPeriod` (table separators, ellipses) are ignored. */ export declare function repeatedTail(text: string, chars?: number, minPeriod?: number): RepeatSignal; /** The stronger of the two signals for one stream. */ export declare function findRepeats(text: string): RepeatSignal & { signal: "block" | "phrase"; }; interface StreamEvent { type: string; delta?: string; } /** Follows one assistant message as it streams. `feed` is called per token and only appends; `check` runs every CHECK_EVERY characters. */ export declare class RunawayMonitor { private streams; private stoppedKind; reset(): void; /** True once this message triggered a stop; later deltas from the same stream are ignored. */ get stopped(): boolean; /** Text seen so far for one stream kind. */ streamed(kind: StreamKind): string; /** Feeds one AssistantMessageEvent; returns the stream kind that has grown enough for a check, else undefined. */ feed(event: StreamEvent): StreamKind | undefined; /** Judges one stream against the config; at most one verdict per message. */ check(kind: StreamKind, config: RunawayConfig): RunawayVerdict | undefined; } /** Follow-up for the agent after its run was stopped. `recover` true: this message starts the next turn. */ export declare function runawayNudge(verdict: RunawayVerdict, recover: boolean): string; export declare function formatRunaway(verdict: RunawayVerdict, recovering: boolean, template?: string): string; export {};