/** * summarize — stage that compresses old loaded entries into a single * summary entry, preserving the most-recent N verbatim. * * Reads from scope: `loaded` * Writes to scope: `loaded` (mutated: oldest entries replaced by one * synthetic summary entry) * * Where this fits in the pipeline: * * loadRecent → summarize → pickByBudget → formatDefault * * After `summarize` runs, `loaded` is smaller (fewer entries, one of * which is a synthetic summary). The picker then selects from this * reduced set using the standard budget logic. * * ## Determinism contract (Anthropic-reviewer ask) * * For prompt caching to stay stable across runs, the summary content * MUST be the same each time for the same input. This requires: * * 1. Temperature = 0 on the provided LLM. * 2. A stable seed if the provider supports it (Anthropic: omit; * OpenAI: pass a fixed `seed` in the request). * 3. Same system prompt + message set produces same output. * * The stage CANNOT enforce this — it just calls the caller-supplied * `llm` function. Callers are responsible for configuring determinism. * Non-deterministic summarizers still work but invalidate prompt caches * on every turn (~5× token-cost increase on cache-enabled providers). * * ## When summarize does NOT fire * * - `loaded.length < triggerMinEntries` → no-op (not enough history). * - `loaded.length <= preserveRecent` → no-op (nothing to summarize; all * entries would be preserved verbatim anyway). * - LLM call throws → error propagates to the pipeline's executor * (fail-loud, per loadRecent / writeMessages convention). * * ## Config guidance * * Choose `triggerMinEntries - preserveRecent >= 2`. Below that, a firing * summarizer would compress just one entry — wasted LLM call with no * real compression. Defaults (trigger 20, preserve 5) summarize 15 * entries when firing, which is a meaningful compression ratio. * * ## Summary entry shape * * The synthetic entry replaces the summarized range. It has: * - `id`: `summary-{earliest_turn}-to-{latest_turn}` * - `value`: a `{role: 'system', content: summaryText}` message * - `source.turn`: the LATEST turn that was summarized (for sorting) * - `tier`: 'cold' (marks it as "condensed, may not be recent") * - `source.identity`: carried over from the summarized range's first entry */ import type { TypedScope } from 'footprintjs'; import type { LLMMessage as Message } from '../../adapters/types.js'; import type { MemoryState } from './types.js'; export interface SummarizeConfig { /** * LLM callback. Receives the chronological messages to summarize; * must return the summary text. Caller is responsible for configuring * the underlying model (temperature=0, seed, system prompt, etc.) to * keep the output deterministic — see "Determinism contract" above. */ readonly llm: (messages: readonly Message[]) => Promise; /** * Minimum `loaded.length` before summarization triggers. Below this, * no-op — the conversation is short enough to keep verbatim. Default 20. */ readonly triggerMinEntries?: number; /** * Number of most-recent entries to preserve verbatim (NOT summarized). * The oldest `loaded.length - preserveRecent` entries become a single * summary entry. Default 5 — keeps recent turns intact so the agent * can reference specific phrasing. */ readonly preserveRecent?: number; /** * Optional custom system prompt for the summarizer. Default is a * neutral "summarize the following conversation..." instruction. * Override for domain-specific summaries (e.g., "preserve all * refund-related details"). */ readonly systemPrompt?: string; } export declare function summarize(config: SummarizeConfig): (scope: TypedScope) => Promise;