SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
4.0 KB · 112 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: src/anthropic/caching.ts4 * Description: Prompt-cache breakpoint planning — deliberate cache_control placement per byte-stability rules (ARCHITECTURE.md §6.5).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import type {11  AnthropicMessage,12  CacheControl,13  ContentBlockParam,14  SystemTier,15  TextBlockParam,16} from "./types.js";1718/** The API allows at most four cache_control breakpoints per request. */19export const MAX_CACHE_BREAKPOINTS = 4;2021const EPHEMERAL: CacheControl = { type: "ephemeral" };2223/** Deterministic breakpoint plan for one request. */24export interface CachePlan {25  /** Indices into `system` tiers whose block gets a breakpoint (tier ends, ADR-7 rule 1). */26  systemTierIndices: number[];27  /** Indices into `messages` whose last cacheable block gets a breakpoint. */28  messageIndices: number[];29}3031function isCacheable(block: ContentBlockParam): boolean {32  // thinking/redacted_thinking blocks do not accept cache_control.33  return block.type === "text" || block.type === "tool_result" || block.type === "tool_use";34}3536function lastCacheableBlockIndex(message: AnthropicMessage): number {37  for (let i = message.content.length - 1; i >= 0; i--) {38    const block = message.content[i];39    if (block !== undefined && isCacheable(block)) return i;40  }41  return -1;42}4344/**45 * Plan breakpoints (budget: 4):46 *47 * - System tiers are byte-stable for the whole session, so tier-end48 *   breakpoints give a durable prefix cache. Up to two are used: the end49 *   of the first tier (survives project-instruction edits at session50 *   boundaries) and the end of the last tier (the full system prompt).51 * - The remaining budget marks the last cacheable block of the final two52 *   user messages — the standard sliding pattern: history before the53 *   previous breakpoint is a byte-identical prefix (rule 2), so each turn54 *   re-reads the long prefix and writes only the new tail.55 *56 * Deterministic: same request shape ⇒ same plan (replay-safe).57 */58export function planCacheBreakpoints(input: {59  system: SystemTier[];60  messages: AnthropicMessage[];61}): CachePlan {62  const systemTierIndices: number[] = [];63  if (input.system.length > 0) {64    systemTierIndices.push(input.system.length - 1); // full system prompt65    if (input.system.length > 1) systemTierIndices.unshift(0); // first tier end66  }6768  const remaining = MAX_CACHE_BREAKPOINTS - systemTierIndices.length;69  const messageIndices: number[] = [];70  for (let i = input.messages.length - 1; i >= 0 && messageIndices.length < remaining; i--) {71    const message = input.messages[i];72    if (message === undefined || message.role !== "user") continue;73    if (lastCacheableBlockIndex(message) === -1) continue;74    messageIndices.unshift(i);75    if (messageIndices.length >= 2) break; // last two user messages only76  }7778  return { systemTierIndices, messageIndices };79}8081/**82 * Render system tiers as API text blocks, applying the plan's breakpoints.83 * Tier text is passed through verbatim (byte-stability, ADR-7 rule 1).84 */85export function buildSystemBlocks(system: SystemTier[], plan: CachePlan): TextBlockParam[] {86  return system.map((tier, index) => {87    const block: TextBlockParam = { type: "text", text: tier.text };88    if (plan.systemTierIndices.includes(index)) block.cache_control = EPHEMERAL;89    return block;90  });91}9293/**94 * Apply message breakpoints without mutating the byte-stable history:95 * marked messages are shallow-copied and their last cacheable block gets96 * cache_control. Everything else is passed through by reference.97 */98export function buildMessagesWithCacheControl(99  messages: AnthropicMessage[],100  plan: CachePlan,101): AnthropicMessage[] {102  return messages.map((message, index) => {103    if (!plan.messageIndices.includes(index)) return message;104    const blockIndex = lastCacheableBlockIndex(message);105    if (blockIndex === -1) return message;106    const content = message.content.map((block, i) =>107      i === blockIndex ? ({ ...block, cache_control: EPHEMERAL } as ContentBlockParam) : block,108    );109    return { ...message, content };110  });111}112