Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/cache-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export function normalizeContentForHash(content: string): string {
return content.replaceAll("\r\n", "\n").trim();
}

export function extractTaggedBlock(prompt: string, tag: "instructions" | "content"): string | null {
export function extractTaggedBlock(
prompt: string,
tag: "instructions" | "content" | "context",
): string | null {
const open = `<${tag}>`;
const close = `</${tag}>`;
const start = prompt.indexOf(open);
Expand All @@ -25,8 +28,18 @@ export function extractTaggedBlock(prompt: string, tag: "instructions" | "conten
}

export function buildPromptHash(prompt: string): string {
const instructions = extractTaggedBlock(prompt, "instructions") ?? prompt;
return hashString(instructions.trim());
const instructionsContent = extractTaggedBlock(prompt, "instructions");
const contextContent = extractTaggedBlock(prompt, "context");

// If at least one of the tags is present (even if empty), hash their contents.
if (instructionsContent !== null || contextContent !== null) {
const instructions = instructionsContent ?? "";
const context = contextContent ?? "";
return hashString(`${instructions}\n${context}`.trim());
}

// Fallback for prompts without any tags.
return hashString(prompt.trim());
}

export function buildPromptContentHash({
Expand Down
2 changes: 1 addition & 1 deletion src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export type CacheConfig = {
path?: string;
};

export const CACHE_FORMAT_VERSION = 1;
export const CACHE_FORMAT_VERSION = 2;
export const DEFAULT_CACHE_MAX_MB = 512;
export const DEFAULT_CACHE_TTL_DAYS = 30;

Expand Down
46 changes: 46 additions & 0 deletions tests/cache.keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,64 @@ import { describe, expect, it } from "vitest";
import {
buildExtractCacheKey,
buildPromptContentHash,
buildPromptHash,
buildSummaryCacheKey,
extractTaggedBlock,
hashString,
} from "../src/cache.js";

describe("cache keys and tags", () => {
it("extracts tagged blocks", () => {
const prompt = "<instructions>Do the thing.</instructions>\n<content>Body</content>";
expect(extractTaggedBlock(prompt, "instructions")).toBe("Do the thing.");
expect(extractTaggedBlock(prompt, "content")).toBe("Body");
expect(extractTaggedBlock(prompt, "context")).toBeNull();
expect(extractTaggedBlock("<context>Site</context>", "context")).toBe("Site");
expect(extractTaggedBlock("no tags here", "instructions")).toBeNull();
});

it("changes prompt hashes when context changes", () => {
const instructions = "Summarize it.";
const contextA = "URL: https://a.com";
const contextB = "URL: https://b.com";
const prompt1 = `<instructions>${instructions}</instructions>\n<context>${contextA}</context>\n<content></content>`;
const prompt2 = `<instructions>${instructions}</instructions>\n<context>${contextB}</context>\n<content></content>`;

const hash1 = buildPromptHash(prompt1);
const hash2 = buildPromptHash(prompt2);

expect(hash1).not.toBe(hash2);
});

it("hashes instructions-only prompt consistently", () => {
const promptWithEmptyContext = "<instructions>Summarize.</instructions>\n<context></context>\n<content>Body</content>";
const promptWithNoContextTag = "<instructions>Summarize.</instructions>\n<content>Body</content>";

const hash1 = buildPromptHash(promptWithEmptyContext);
const hash2 = buildPromptHash(promptWithNoContextTag);

// Both should hash just the instructions since context is empty/missing
expect(hash1).toBe(hash2);
});

it("treats multiple empty tags consistently", () => {
const p1 = "<instructions></instructions>";
const p2 = "<context></context>";
const p3 = "<instructions></instructions><context></context>";
const p4 = "<instructions> </instructions>";

const h1 = buildPromptHash(p1);
const h2 = buildPromptHash(p2);
const h3 = buildPromptHash(p3);
const h4 = buildPromptHash(p4);

expect(h1).toBe(h2);
expect(h2).toBe(h3);
expect(h3).toBe(h4);
// They should all hash to an empty string's hash (after trim)
expect(h1).toBe(hashString(""));
});

it("changes summary keys when inputs change", () => {
const base = buildSummaryCacheKey({
contentHash: "content",
Expand Down