/** * window/turns — where a turn boundary is, and which turns may leave. * * Pattern: Pure functions over the window (no scope, no I/O, no clock). * Role: core/ layer. THE refusal engine. Every window strategy — the three * that ship, and any a consumer writes — decides what leaves the * window by calling these functions and nothing else, because the * whole safety argument of the family lives here: * * a removal that splits an assistant's `tool_use` from its * `tool_result` produces a request the vendor rejects, and a * removal that swallows an unanswered question destroys the * referent of the answer that has not arrived yet. * * Strategies never import this module. `WindowStrategyInput` hands * them `planRemoval` already bound to this iteration's turns and * guards, so a strategy CANNOT skip the refusal rules — that is a * property of the seam, not of the documentation. * Emits: N/A. * * Testable on its own — see `test/core/window-turns.test.ts`. */ import type { LLMMessage } from '../../../adapters/types.js'; import type { ToolResultPin } from './lastToolResult.js'; import type { WindowObservations, WindowRefusal, WindowRefusalReason } from './types.js'; /** * One turn: a `user` / `assistant` / `system` message plus every `tool` * message that answers it. Tool results belong to the assistant turn that * requested them — that pairing is the thing a removal must never break. */ export interface Turn { /** Index of this turn in the segmentation. */ readonly index: number; /** Index of the turn's FIRST message in the window. */ readonly start: number; /** Number of messages in the turn. */ readonly length: number; readonly messages: readonly LLMMessage[]; } /** * Segment a window into turns. * * A new turn starts at any non-`tool` message; `tool` messages join the turn * in progress. A leading `tool` message (only reachable from a hand-built * history) starts its own turn rather than being silently dropped. */ export declare function segmentTurns(history: readonly LLMMessage[]): readonly Turn[]; /** * Context a removal decision needs beyond the turn itself. * * Internal: the stage builds it from scope and binds it into the * `planRemoval` a strategy is handed, so no strategy has to know it exists. */ export interface RemovalGuards { /** Every `toolCallId` answered anywhere in the window. */ readonly answeredCallIds: ReadonlySet; /** * Index in the window of the CURRENT REQUEST — the message this run is * executing (9.55.0). The turn holding it refuses with `'current-request'`. * * Resolved by `currentRequestIndexOf`, which the stage calls with the run's * own `scope.userMessage`. Absent (or `-1`) when the window holds no * identifiable request, and then this rule simply does not apply — which is * how a window that never had one behaves exactly as it did before the rule * existed. */ readonly currentRequestIndex?: number; /** The tool call this run is paused on, when it is paused. */ readonly pausedToolCallId?: string; /** True when the pause is a check-in (human consent) rather than askHuman. */ readonly pausedCheckIn?: boolean; /** * Candidate LAST-TOOL-RESULT pins (9.57.0), newest first, from * `toolResultPinsOf`. An INPUT to `planRemoval`, not to `refusalFor`: the * ceiling can only be spent once the keep window is known, so `planRemoval` * admits some of these and hands `refusalFor` the answer. */ readonly toolResultPins?: readonly ToolResultPin[]; /** The ceiling on admitted pins (`keepLastToolResults`). 0 = the pin is off. */ readonly keepLastToolResults?: number; /** * The ADMITTED pins, by turn index — what `refusalFor` actually reads. * Derived by `planRemoval`; a caller building guards by hand may set it * directly to ask `refusalFor` about one turn. */ readonly pinnedTurnIndexes?: ReadonlySet; } /** Every tool_call id that has a matching `role: 'tool'` message. */ export declare function answeredCallIds(history: readonly LLMMessage[]): ReadonlySet; /** * Why this turn may NOT leave the window, or `undefined` when it may. * * Order matters only for which reason gets reported first; every check is * independent. `paused-tool` / `pending-check-in` are separated from * `unresolved-tool-call` on purpose: they are the same shape but a different * fact about the world, and "we are waiting on a human" is what the person * reading the trace needs to see. */ export declare function refusalFor(turn: Turn, guards: RemovalGuards): WindowRefusalReason | undefined; /** The span a removal will take, plus every refusal it had to name to get there. */ export interface RemovalPlan { /** First turn index in the span; -1 when nothing may be removed. */ readonly from: number; /** Last turn index in the span (inclusive); -1 when nothing may be removed. */ readonly to: number; readonly refusals: readonly WindowRefusal[]; /** * What the last-tool-result pin did on this plan (9.57.0) — which turns it * held and what the ceiling turned away. Absent when it held nothing, so a * window with no pinnable result plans exactly as it did before. */ readonly observations?: WindowObservations; } /** * Choose the removal span: the LONGEST CONTIGUOUS run of removable candidate * turns, starting at the oldest removable one. * * Contiguity is not fussiness — it is what keeps the conversation in order. * A fold replaces its span with ONE summary message; if the span skipped over * an unremovable turn, that turn would end up after a summary of things that * happened before it. So an unremovable turn at the front is stepped over (the * span "takes the next oldest instead") and an unremovable turn in the middle * ends the span. Everything not removed keeps its position. * * The drop strategies take the same span for a second reason: it is what makes * a refusal reason mean the same thing under every strategy. A turn that ends * the span this iteration is retried the next one — by which time the tool * result it was waiting on has usually arrived. * * @param turns the window's turn segmentation * @param keepRecent how many trailing turns are off-limits * @param guards what must not leave (unanswered calls, the pause) * @param isExistingSummary optional: true for a turn that is a summary a prior * fold wrote. When the whole span is one such turn, the plan refuses with * `only-existing-summary` — re-summarizing a summary spends an LLM call to * lose detail. The drop strategies omit it: a drop spends nothing, so there * is nothing to protect against. */ export declare function planRemoval(turns: readonly Turn[], keepRecent: number, guards: RemovalGuards, isExistingSummary?: (turn: Turn) => boolean): RemovalPlan; /** Total characters of message content in a window. Exact; not tokens. */ export declare function windowChars(history: readonly LLMMessage[]): number;