import type { Message } from '../../types/message/index.js'; export interface RepeatCallThresholds { /** Repeats at which a first, mild notice is attached. */ readonly notifyAfter: number; /** Repeats at which the wording escalates. */ readonly escalateAfter: number; /** * Consecutive identical FAILURES after which the next identical call is * refused rather than run. A success in between resets the count. */ readonly refuseFailedAfter: number; } export declare const DEFAULT_REPEAT_THRESHOLDS: RepeatCallThresholds; export interface RepeatCallNotice { readonly toolName: string; readonly count: number; readonly level: 'notice' | 'escalated'; readonly text: string; } /** * Turn-scoped, like `ToolGrantSet` and for the same reason: a count carried * into a later turn is a statement about work nobody repeated. */ export declare class RepeatCallTracker { private readonly thresholds; private readonly counts; /** Which keys have already been reported at which level, so one repeat * does not produce the same sentence on every subsequent turn. */ private readonly announced; /** Consecutive failures per key; a success deletes the entry. */ private readonly failures; constructor(thresholds?: RepeatCallThresholds); /** * Records one call and returns a notice when this is the repeat that * crosses a threshold, `undefined` otherwise. `outcome.failed` is what * the refusal counts; a call recorded without an outcome counts as a * repeat but never towards a refusal. */ record(toolName: string, input: unknown, outcome?: { readonly failed: boolean; }): RepeatCallNotice | undefined; /** Repeats seen for one call, for a host that wants to render it. */ countOf(toolName: string, input: unknown): number; /** * The refusal for a call that has failed identically too many times in * a row, or `undefined` when the call may run. Asked BEFORE execution; * the refused call is still recorded afterwards, as a failure, so the * refusal holds until the model changes something. */ refusal(toolName: string, input: unknown): string | undefined; } /** * Rides the notice out on the last `tool_result` of the batch, same slot * steering uses: a `tool_use` block must be answered by a `tool_result` with * the same id, so a user message wedged between them is rejected by the * provider outright. * * That slot only exists when the trailing result's content is plain text. A * result answered with structured content (an image, a document, an MCP * block) has a shape the model reads positionally, and appending a string to * it is either dropped or corrupts the block — this used to mean the notice * was simply dropped, on the theory that an advisory costs nothing to lose. * It costs more than a refusal would: `RepeatCallTracker.record` already * marked the threshold as announced the moment it fired, so a notice lost * here never comes back, unlike steering, which can requeue and wait for a * later plain-text result. The fallback instead rides out as its own * `runtime-context` message placed AFTER the complete tool-result batch — * never between a `tool_use` and its `tool_result`, so provider-required * adjacency still holds — carrying that provenance so it is never mistaken * for operator input (see `isOperatorUserMessage` in `steering.ts`). */ export declare function attachRepeatNotice(messages: readonly Message[], notices: readonly RepeatCallNotice[]): readonly Message[]; //# sourceMappingURL=repeat-call.d.ts.map