{"version":3,"sources":["../src/budget.ts","../src/auto-summarize.ts"],"names":["approximateCounter","messages","total","m","perMessage","a","b","compact","options","counter","keepRecent","toNum","v","tail","head","droppable","keptHead","summary","withSummary","next","afterTokens","createAutoSummarizingMemory","backing","result"],"mappings":"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","file":"auto-summarize.cjs","sourcesContent":["import type { Message } from './types/message'\nimport type { TokenCounter } from './types/token-counter'\nimport type { ToolDefinition } from './types/tool'\n\nexport type BudgetStrategy = 'drop-oldest' | 'sliding-window' | 'summarize'\n\nexport interface CompileBudgetInput {\n  /** Hard upper bound (model context limit - reserveForOutput). */\n  budget: number\n  messages: Message[]\n  systemPrompt?: string\n  tools?: ToolDefinition[]\n  /** Token counter. Defaults to `approximateCounter` (chars/4 heuristic). */\n  counter?: TokenCounter\n  /** Trimming strategy. Default 'drop-oldest'. */\n  strategy?: BudgetStrategy\n  /** Required when strategy === 'summarize'. */\n  summarizer?: (dropped: Message[]) => Message | Promise<Message>\n  /** Tokens reserved for the model's output. Subtracted from budget. */\n  reserveForOutput?: number\n  /**\n   * Minimum number of recent messages to keep regardless of strategy.\n   * Protects against dropping the turn that actually matters. Default 1.\n   */\n  keepRecent?: number\n}\n\nexport interface CompileBudgetResult {\n  messages: Message[]\n  systemPrompt?: string\n  tokens: {\n    system: number\n    messages: number\n    tools: number\n    total: number\n    budget: number\n  }\n  dropped: Message[]\n  fits: boolean\n  strategy: BudgetStrategy\n}\n\n/**\n * Zero-dependency approximate token counter. Rule of thumb: ~4 chars\n * per token. Good enough for budget planning; swap for a real\n * tokenizer (tiktoken etc.) via the `counter` option in prod.\n */\nexport const approximateCounter: TokenCounter = {\n  name: 'approximate',\n  count(messages) {\n    let total = 0\n    for (const m of messages) {\n      total += Math.ceil(((m.role?.length ?? 0) + (m.content?.length ?? 0)) / 4) + 2\n    }\n    return total\n  },\n  countDetailed(messages) {\n    const perMessage = messages.map(m => Math.ceil(((m.role?.length ?? 0) + (m.content?.length ?? 0)) / 4) + 2)\n    return { total: perMessage.reduce((a, b) => a + b, 0), perMessage }\n  },\n}\n\nfunction countTools(tools: ToolDefinition[] | undefined, counter: TokenCounter): number | Promise<number> {\n  if (!tools || tools.length === 0) return 0\n  return counter.count(\n    tools.map(t => ({ role: 'system' as const, content: `${t.name}:${t.description ?? ''}:${JSON.stringify(t.schema ?? {})}` })),\n  )\n}\n\nasync function toNumber(v: number | Promise<number>): Promise<number> {\n  return typeof v === 'number' ? v : await v\n}\n\n/**\n * Take a declared `budget` and a set of messages/system/tools, then\n * return a trimmed request guaranteed to fit under `budget`. Three\n * strategies:\n *  - 'drop-oldest': remove oldest messages until it fits\n *  - 'sliding-window': keep only the most recent N messages\n *  - 'summarize': fold dropped messages into a single summary message\n */\nexport async function compileBudget(input: CompileBudgetInput): Promise<CompileBudgetResult> {\n  const counter = input.counter ?? approximateCounter\n  const strategy = input.strategy ?? 'drop-oldest'\n  const keepRecent = Math.max(1, input.keepRecent ?? 1)\n  const reserved = input.reserveForOutput ?? 0\n  const effectiveBudget = input.budget - reserved\n\n  if (effectiveBudget <= 0) {\n    throw new Error(`Budget must exceed reserveForOutput (${input.budget} ≤ ${reserved})`)\n  }\n\n  const systemMsg: Message | undefined = input.systemPrompt\n    ? {\n        id: '__system',\n        role: 'system',\n        content: input.systemPrompt,\n        status: 'complete',\n        createdAt: new Date(0),\n      }\n    : undefined\n\n  const systemTokens = systemMsg ? await toNumber(counter.count([systemMsg])) : 0\n  const toolTokens = await toNumber(countTools(input.tools, counter))\n  const floor = systemTokens + toolTokens\n\n  if (floor > effectiveBudget) {\n    throw new Error(\n      `System prompt + tools (${floor} tokens) exceed budget (${effectiveBudget}). Shrink system prompt or raise budget.`,\n    )\n  }\n\n  const working = [...input.messages]\n  const dropped: Message[] = []\n\n  const messageTokens = async (msgs: Message[]): Promise<number> => toNumber(counter.count(msgs))\n\n  let total = floor + (await messageTokens(working))\n\n  if (strategy === 'sliding-window') {\n    while (working.length > keepRecent && total > effectiveBudget) {\n      dropped.push(working.shift()!)\n      total = floor + (await messageTokens(working))\n    }\n  } else if (strategy === 'drop-oldest') {\n    while (working.length > keepRecent && total > effectiveBudget) {\n      dropped.push(working.shift()!)\n      total = floor + (await messageTokens(working))\n    }\n  } else if (strategy === 'summarize') {\n    if (!input.summarizer) {\n      throw new Error(`strategy='summarize' requires a summarizer function`)\n    }\n    while (working.length > keepRecent && total > effectiveBudget) {\n      dropped.push(working.shift()!)\n      total = floor + (await messageTokens(working))\n    }\n    if (dropped.length > 0) {\n      const summary = await input.summarizer(dropped)\n      working.unshift(summary)\n      total = floor + (await messageTokens(working))\n    }\n  }\n\n  return {\n    messages: working,\n    systemPrompt: input.systemPrompt,\n    tokens: {\n      system: systemTokens,\n      messages: total - floor,\n      tools: toolTokens,\n      total,\n      budget: effectiveBudget,\n    },\n    dropped,\n    fits: total <= effectiveBudget,\n    strategy,\n  }\n}\n","import type { ChatMemory } from './types/memory'\nimport type { Message } from './types/message'\nimport type { TokenCounter } from './types/token-counter'\nimport { approximateCounter } from './budget'\n\nexport interface AutoSummarizeOptions {\n  /** Hard cap on stored tokens. Once exceeded, the oldest messages are summarized. */\n  maxTokens: number\n  /**\n   * Called with the messages selected for compaction. Returns a\n   * single summary message that replaces them.\n   */\n  summarizer: (messages: Message[]) => Message | Promise<Message>\n  /** Messages to always keep verbatim at the tail. Default 4. */\n  keepRecent?: number\n  /** Token counter. Defaults to `approximateCounter`. */\n  counter?: TokenCounter\n  /** Fires after every compaction. */\n  onCompact?: (info: {\n    droppedCount: number\n    beforeTokens: number\n    afterTokens: number\n    summary: Message\n  }) => void\n}\n\ninterface CompactResult {\n  messages: Message[]\n  summary?: Message\n  droppedCount: number\n  beforeTokens: number\n  afterTokens: number\n}\n\nasync function compact(messages: Message[], options: AutoSummarizeOptions): Promise<CompactResult> {\n  const counter = options.counter ?? approximateCounter\n  const keepRecent = Math.max(1, options.keepRecent ?? 4)\n  const toNum = async (v: number | Promise<number>): Promise<number> => (typeof v === 'number' ? v : await v)\n\n  const total = await toNum(counter.count(messages))\n  if (total <= options.maxTokens || messages.length <= keepRecent) {\n    return { messages, droppedCount: 0, beforeTokens: total, afterTokens: total }\n  }\n\n  const tail = messages.slice(messages.length - keepRecent)\n  const head = messages.slice(0, messages.length - keepRecent)\n  const droppable: Message[] = []\n  const keptHead: Message[] = []\n\n  // Drop preserved summaries last; they already represent compacted history.\n  for (const m of head) {\n    if (m.metadata?.agentskitSummary === true) keptHead.push(m)\n    else droppable.push(m)\n  }\n\n  if (droppable.length === 0) {\n    return { messages, droppedCount: 0, beforeTokens: total, afterTokens: total }\n  }\n\n  const summary = await options.summarizer(droppable)\n  const withSummary: Message = {\n    ...summary,\n    metadata: { ...(summary.metadata ?? {}), agentskitSummary: true },\n  }\n\n  const next = [...keptHead, withSummary, ...tail]\n  const afterTokens = await toNum(counter.count(next))\n  return {\n    messages: next,\n    summary: withSummary,\n    droppedCount: droppable.length,\n    beforeTokens: total,\n    afterTokens,\n  }\n}\n\n/**\n * Wrap any `ChatMemory` so that on every `save`, messages over\n * `maxTokens` are folded into a single summary message via\n * `summarizer`. Summaries are idempotent: they're tagged with\n * `metadata.agentskitSummary = true` and are never re-summarized.\n *\n * Pair this with a runtime-level `maxTokens` equal to the model's\n * context window minus a reserve for the response, and your agent's\n * history auto-compacts forever.\n */\nexport function createAutoSummarizingMemory(\n  backing: ChatMemory,\n  options: AutoSummarizeOptions,\n): ChatMemory {\n  return {\n    async load() {\n      return backing.load()\n    },\n    async save(messages) {\n      const result = await compact(messages, options)\n      await backing.save(result.messages)\n      if (result.summary) {\n        options.onCompact?.({\n          droppedCount: result.droppedCount,\n          beforeTokens: result.beforeTokens,\n          afterTokens: result.afterTokens,\n          summary: result.summary,\n        })\n      }\n    },\n    async clear() {\n      await backing.clear?.()\n    },\n  }\n}\n"]}