agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
77 lines (76 loc) • 4.03 kB
TypeScript
/**
* window/strategies/drop — the mechanic both drop strategies share.
*
* Pattern: One pure function over a `WindowStrategyInput`.
* Role: core/ layer. `slidingWindow` and `tokenBudget` differ ONLY in when
* they engage and what they file; what actually leaves the window is
* decided here, once, so a refusal reason means the same thing under
* both — and under compaction, which asks the same `planRemoval`.
* Emits: N/A.
*
* A drop makes no LLM call and writes no summary. What it must still do is
* keep the request valid and keep the record honest:
*
* • the span comes from the shared refusal engine, so the CURRENT REQUEST,
* an unanswered tool call, the paused tool, a pending check-in and the
* recent turns never leave, and each refusal is named;
* • when the span reaches the front of what may leave, an authored notice
* takes that position — the window must open on a user turn (see
* notice.ts), and a message we are forced to author should tell the
* truth. That position is the window's HEAD in the ordinary case, and the
* slot just after the kept request when the request was the head;
* • when it does not, nothing is inserted: the original opening turn is
* still there, so there is no wire problem to solve.
*
* ## The notice is owed to the WIRE, not to politeness (9.57.0)
*
* That distinction used to be missing here, and it produced a livelock. The
* notice was authored whenever the removal reached the front of what may
* leave, and the whole removal was abandoned when the notice was not smaller
* than the span — but the obligation being guarded is narrower: **the window
* must open on a user turn.** When the message that would BECOME the head is
* already a user turn (the kept request, or an older user turn from a restored
* conversation), no notice is owed at all, and its 245–358 characters had no
* business vetoing a legitimate drop.
*
* Because the span is the longest CONTIGUOUS removable run, a blocker one turn
* along caps it at the same small span at every boundary — so the same verdict
* came back for ever and the window grew without bound. Two locally-correct
* rules composing into non-progress.
*
* So the decision is a LADDER: the rich notice, else the plain one, else no
* notice at all — and `'replacement-not-smaller'` only when the wire genuinely
* needs a message in that position and none of them is smaller than the span.
*/
import type { LLMMessage } from '../../../../provider-contract/types.js';
import type { WindowEviction, WindowStrategyInput } from '../strategy.js';
import type { WindowRefusal } from '../types.js';
/** What a drop attempt did, in the terms a record needs. */
export interface DropOutcome {
/** Every turn that refused to leave, named. */
readonly refusals: readonly WindowRefusal[];
/** The new window; absent when nothing was dropped. */
readonly window?: readonly LLMMessage[];
readonly rebase?: {
readonly headCount: number;
readonly keptTailCount: number;
readonly insertedAtMs?: number;
};
readonly removedStageIds: readonly string[];
readonly removedMessageCount: number;
readonly evictions: readonly WindowEviction[];
readonly windowCharsBefore: number;
readonly windowCharsAfter: number;
/** Turns in the window before / after. Counted from the segmentation. */
readonly turnsBefore: number;
readonly turnsAfter: number;
}
/**
* Drop the oldest contiguous removable span, if there is one.
*
* @param input the strategy's input, with the bound refusal engine
* @param keepRecentTurns how many trailing turns are off-limits
* @param strategyName named in the authored notice, so a reader of the
* window itself can tell which policy removed things
*/
export declare function dropOldestSpan(input: WindowStrategyInput, keepRecentTurns: number, strategyName: string): DropOutcome;