Prompt caching
The storage surface caching writes through — backend contract, entry shape, and the per-call failure-mode policy.
A cache hit is determined entirely by the fingerprint. The variable
surface the backend stores against it is the recorded response —
content, toolCalls, finishReason, token usage, modelId,
and (for streaming calls) the chunk sequence — plus
recordedAt/recordedBy provenance and a sizeBytes accounting
field the backend maintains on set.
The seam derives the fingerprint key from @pleach/core/fingerprint;
this module is only about storage and retrieval semantics.
@pleach/core/cacheSourcesrc/cache/Public exports
| Export | Kind | Purpose |
|---|---|---|
CacheBackend | interface | Storage contract every backend implements. |
CacheEntry | type | Recorded response payload + provenance. |
CacheGetMode | union | Per-call failure-mode policy passed to get(). |
CacheGetOptions | type | { mode: CacheGetMode } — get()'s second argument. |
CacheStats | type | O(1) counters returned by stats(). |
MemoryCacheBackendOptions | type | Memory backend constructor options. |
createMemoryCacheBackend | factory | Returns a CacheBackend backed by an in-memory LRU. |
CacheBackend
Memory and Supabase backends both implement this surface. The seam holds only this contract.
interface CacheBackend {
readonly id: string;
get(key: Fingerprint, options: CacheGetOptions): Promise<CacheEntry | null>;
set(key: Fingerprint, entry: CacheEntry): Promise<void>;
delete(key: Fingerprint): Promise<void>;
list(prefix: Partial<Fingerprint>): AsyncIterable<Fingerprint>;
stats(): Promise<CacheStats>;
}Four invariants every implementation maintains:
get()returns a deep-frozen entry — callers must not mutate.set()is idempotent on fingerprint (last-write-wins).stats()is O(1) — counters update on every mutation.list()is async-iterable even for memory implementations so callers handle pagination from day one.
CacheGetMode
The per-call failure-mode policy. Distinct from
CacheReadPolicy in @pleach/core/fingerprint, which governs
cross-RuntimeMode boundary reads.
| Mode | Behavior on backend error | When to use |
|---|---|---|
strict-fail | Error propagates to the caller. | headless-replay — a missed hit breaks determinism. |
best-effort | Error returns null and logs. | Interactive production traffic — a cache outage must not break user-facing calls. |
Same backend instance serves both — the mode is a per-call decision, not a backend configuration.
CacheEntry
The recorded payload. Opaque to the backend; the seam owns shape validation.
| Field | Type | Notes |
|---|---|---|
fingerprint | Fingerprint | Cache key, replicated onto the entry. |
metadata | FingerprintMetadata | Key/metadata split sibling — diagnostic only. |
response.content | string | Final aggregated response text. |
response.toolCalls | ReadonlyArray<unknown> | Provider-shaped tool calls. |
response.finishReason | string | Unmodified provider string. |
response.usage | { inputTokens; outputTokens } | Token accounting. |
response.modelId | string | The model that produced the entry. |
streamChunks | ReadonlyArray<unknown> (optional) | Set on streaming invocations so replay can re-emit deltas in arrival order. |
recordedAt | ISO-8601 | When the entry was written. |
recordedBy | { pleachVersion; nodeId; stageId } | Provenance for cross-version replays. |
sizeBytes | number | Maintained by the backend on set; drives LRU eviction. |
CacheStats
interface CacheStats {
readonly id: string;
readonly entryCount: number;
readonly sizeBytes: number;
readonly hits: number;
readonly misses: number;
readonly hitRatio: number;
readonly evictions: number;
}hitRatio is hits / (hits + misses), or 0 when both are zero.
Counters update on every mutation, so stats() is O(1).
Memory backend
import { createMemoryCacheBackend } from "@pleach/core/cache";
const cache = createMemoryCacheBackend({
maxEntries: 1000, // default 1000
maxBytes: 64 * 1024 * 1024, // default 64 MB
id: "memory", // default "memory"
});
const entry = await cache.get(fingerprint, { mode: "best-effort" });
if (!entry) {
const response = await seam.invoke(...);
await cache.set(fingerprint, buildEntry(response));
}The implementation rides on JavaScript's insertion-ordered Map:
get() re-inserts on hit so the iterator's first entry is the LRU
candidate. Eviction runs after every set() until both maxEntries
and maxBytes caps are satisfied.
Wiring into a runtime
SessionRuntimeConfig.cacheBackend is the field every seam reads
through. Pass the backend at construction and every provider call
shares the same cache instance.
import { SessionRuntime } from "@pleach/core";
import { createMemoryCacheBackend } from "@pleach/core/cache";
const cacheBackend = createMemoryCacheBackend({ maxEntries: 5_000 });
const runtime = new SessionRuntime({
storage,
userId: "user_123",
cacheBackend,
});
// Later, inspect counters:
const stats = await cacheBackend.stats();
console.log(stats.hits, stats.misses, stats.hitRatio);Picking a mode per call
Same backend instance; different mode per get(). Production
traffic reads best-effort so a backend outage degrades to a miss;
replay reads strict-fail so a missed hit fails fast instead of
re-invoking the provider.
// Interactive request — a cache outage must not break the user-facing call.
const cached = await cacheBackend.get(fingerprint, { mode: "best-effort" });
if (cached) return cached;
return await seam.invoke(...);
// Replay run — a missed hit means the cache is wrong, not the model.
const replayed = await cacheBackend.get(fingerprint, { mode: "strict-fail" });
if (!replayed) throw new Error(`replay miss: ${fingerprint.promptHash}`);
return replayed;Where to go next
Fingerprint
How the cache key is derived — what's in, what's deliberately excluded, and why.
Determinism
`strict-fail` mode is what keeps `headless-replay` deterministic.
Eval and replay
Replay reads cache entries by fingerprint and re-emits stream chunks in arrival order.
Subpath exports
The `./cache` subpath in the package exports map.