feat(engine): add SystemPromptLayers type and builder for cross-session caching

This commit is contained in:
Matthew Greenberg
2026-05-08 19:52:34 -04:00
parent 4492fed361
commit a53a9a0802
2 changed files with 206 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
import { describe, it, expect } from "vitest";
import {
type SystemPromptLayers,
buildPromptLayers,
collapsePromptLayers,
} from "../prompt-layers.js";
describe("buildPromptLayers", () => {
it("separates base prompt into stable layer", () => {
const layers = buildPromptLayers({
basePrompt: "You are a reviewer.",
});
expect(layers.stable).toBe("You are a reviewer.");
expect(layers.dynamic).toBe("");
});
it("puts agent instructions into dynamic layer", () => {
const layers = buildPromptLayers({
basePrompt: "You are a reviewer.",
agentInstructions: "Always check for SQL injection.",
});
expect(layers.stable).toBe("You are a reviewer.");
expect(layers.dynamic).toContain("Always check for SQL injection.");
});
it("puts memory section into dynamic layer", () => {
const layers = buildPromptLayers({
basePrompt: "You are a reviewer.",
memorySection: "## Agent Memory\n\nRemember to check tests.",
});
expect(layers.stable).toBe("You are a reviewer.");
expect(layers.dynamic).toContain("Agent Memory");
});
it("puts plugin contributions into dynamic layer", () => {
const layers = buildPromptLayers({
basePrompt: "You are a reviewer.",
pluginContributions: "## Plugin: security\n\nScan for CVEs.",
});
expect(layers.stable).toBe("You are a reviewer.");
expect(layers.dynamic).toContain("security");
});
it("puts performance feedback into dynamic layer", () => {
const layers = buildPromptLayers({
basePrompt: "You are a reviewer.",
performanceFeedback: "## Performance Feedback\n\n- Average score: 8.5",
});
expect(layers.stable).toBe("You are a reviewer.");
expect(layers.dynamic).toContain("Performance Feedback");
});
it("combines multiple dynamic sections with double newlines", () => {
const layers = buildPromptLayers({
basePrompt: "Base.",
agentInstructions: "Instructions.",
memorySection: "Memory.",
pluginContributions: "Plugins.",
});
expect(layers.dynamic).toBe(
"## Custom Instructions\n\nInstructions.\n\nMemory.\n\nPlugins."
);
});
it("omits empty dynamic sections", () => {
const layers = buildPromptLayers({
basePrompt: "Base.",
agentInstructions: "",
memorySection: "",
pluginContributions: "Plugins.",
});
expect(layers.dynamic).toBe("Plugins.");
expect(layers.dynamic).not.toContain("Custom Instructions");
});
it("produces deterministic output for identical inputs", () => {
const input = {
basePrompt: "Base.",
agentInstructions: "Inst.",
memorySection: "Mem.",
pluginContributions: "Plug.",
performanceFeedback: "Perf.",
};
const layers1 = buildPromptLayers(input);
const layers2 = buildPromptLayers(input);
expect(layers1.stable).toBe(layers2.stable);
expect(layers1.dynamic).toBe(layers2.dynamic);
});
});
describe("collapsePromptLayers", () => {
it("returns stable when dynamic is empty", () => {
const result = collapsePromptLayers({ stable: "Base.", dynamic: "" });
expect(result).toBe("Base.");
});
it("joins stable and dynamic with double newline", () => {
const result = collapsePromptLayers({
stable: "Base.",
dynamic: "Dynamic.",
});
expect(result).toBe("Base.\n\nDynamic.");
});
it("matches legacy buildSystemPromptWithInstructions output", () => {
const layers = buildPromptLayers({
basePrompt: "You are a reviewer.",
agentInstructions: "Check for bugs.",
});
const collapsed = collapsePromptLayers(layers);
expect(collapsed).toBe(
"You are a reviewer.\n\n## Custom Instructions\n\nCheck for bugs."
);
});
});

View File

@@ -0,0 +1,81 @@
/**
* Structured system prompt layers for cross-session caching.
*
* The `stable` layer contains content that is identical across sessions of
* the same role (base role prompt). The `dynamic` layer holds per-session
* content (agent instructions, memory, performance feedback, plugins).
*
* When the stable layer is byte-identical across consecutive API calls,
* Anthropic's prompt cache gives a 90% read discount. OpenAI caches
* matching prefixes automatically at 50% discount.
*/
export interface SystemPromptLayers {
/** Role-specific base prompt — identical across all sessions of this role. */
stable: string;
/** Per-session content: agent instructions, memory, feedback, plugins. */
dynamic: string;
}
export interface PromptLayerInput {
/** The base role system prompt (e.g. REVIEWER_SYSTEM_PROMPT). */
basePrompt: string;
/** Resolved agent instructions (instructionsText + instructionsPath + soul). */
agentInstructions?: string;
/** Formatted memory section (agent memory + workspace memory). */
memorySection?: string;
/** Formatted plugin prompt contributions. */
pluginContributions?: string;
/** Formatted performance feedback section. */
performanceFeedback?: string;
}
/**
* Build structured prompt layers from the components that currently get
* concatenated into a single system prompt string.
*
* The stable layer is ONLY the base role prompt. Everything else goes into
* the dynamic layer so that the stable prefix is byte-identical across
* sessions of the same role, enabling cross-session prompt caching.
*/
export function buildPromptLayers(input: PromptLayerInput): SystemPromptLayers {
const { basePrompt, agentInstructions, memorySection, pluginContributions, performanceFeedback } = input;
const dynamicParts: string[] = [];
const trimmedInstructions = agentInstructions?.trim() ?? "";
if (trimmedInstructions) {
dynamicParts.push(`## Custom Instructions\n\n${trimmedInstructions}`);
}
const trimmedMemory = memorySection?.trim() ?? "";
if (trimmedMemory) {
dynamicParts.push(trimmedMemory);
}
const trimmedPlugins = pluginContributions?.trim() ?? "";
if (trimmedPlugins) {
dynamicParts.push(trimmedPlugins);
}
const trimmedFeedback = performanceFeedback?.trim() ?? "";
if (trimmedFeedback) {
dynamicParts.push(trimmedFeedback);
}
return {
stable: basePrompt,
dynamic: dynamicParts.join("\n\n"),
};
}
/**
* Collapse layers back into a single string for backward compatibility.
* Runtimes that don't support structured caching use this to get the same
* concatenated prompt as before.
*/
export function collapsePromptLayers(layers: SystemPromptLayers): string {
if (!layers.dynamic) {
return layers.stable;
}
return `${layers.stable}\n\n${layers.dynamic}`;
}