import type { AgentMessage } from "@elyracode/agent-core"; import type { Model } from "@elyracode/ai"; import type { CacheWarmingSettings } from "./settings-manager.js"; /** * Keeps the provider's prompt cache warm while the user is idle. * * Anthropic's default cache TTL is 5 minutes from the last read or write. After a turn ends, * the warmer waits `delayMs`, checks that nothing has changed, and asks the session to send a * minimal request that replays the same prefix (system, tools, history) with `max_tokens: 1`. * That read refreshes the TTL for one cache-read price. It repeats up to `maxPings` times, * then gives up until the next real turn. * * The warmer only decides *when*; the session owns *how* (`ping`) so this stays free of * agent internals and is easy to test with fake timers. */ export interface CacheWarmerDeps { getSettings: () => CacheWarmingSettings; /** True when the agent is not streaming, compacting, or otherwise busy. */ isIdle: () => boolean; getModel: () => Model | undefined; /** Resolved credential for the model, used to exclude OAuth (subscription) accounts. */ getApiKey: (model: Model) => Promise; getMessages: () => readonly AgentMessage[]; /** Perform the warm request. Errors are caught and end the current idle period. */ ping: (signal: AbortSignal) => Promise; /** Optional observer for tests and diagnostics. */ onEvent?: (event: CacheWarmerEvent) => void; } export type CacheWarmerSkipReason = "disabled" | "busy" | "no-model" | "unsupported-model" | "no-api-key" | "oauth" | "small-prefix" | "messages-changed" | "max-pings"; export type CacheWarmerEvent = { type: "armed"; delayMs: number; ping: number; } | { type: "ping-start"; ping: number; } | { type: "ping-end"; ping: number; ok: boolean; error?: string; } | { type: "skipped"; reason: CacheWarmerSkipReason; } | { type: "disarmed"; }; /** Only Anthropic's Messages API exposes the TTL-refresh-on-read behaviour this relies on. */ export declare function supportsCacheWarming(model: Model | undefined): model is Model; /** * Anthropic OAuth tokens (Claude subscriptions) are billed against rate limits rather than * dollars, so a warm ping spends quota the user probably cares more about than cache misses. */ export declare function isOAuthApiKey(apiKey: string): boolean; /** * Size of the prefix the next request would replay, taken from the last assistant message's * usage. `input + cacheRead + cacheWrite` is the full prompt the provider saw for that call. */ export declare function estimatePrefixTokens(messages: readonly AgentMessage[]): number; export declare class CacheWarmer { private readonly deps; private timer; private abort; private pingsThisIdle; private fingerprint; private disposed; constructor(deps: CacheWarmerDeps); /** * Start (or restart) the idle countdown. Call after a real turn ends. * Resets the per-idle ping counter. */ arm(): void; /** Cancel any pending or in-flight ping. Call when a turn starts, the model changes, or on dispose. */ disarm(emit?: boolean): void; dispose(): void; /** Whether a ping is scheduled or running. */ get isArmed(): boolean; private schedule; private fire; } //# sourceMappingURL=cache-warmer.d.ts.map