groundswell 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -0
- package/.claude/system_prompts/task-breakdown.md +100 -0
- package/PRPs/001-hierarchical-workflow-engine.md +2438 -0
- package/PRPs/PRDs/001-hierarchical-workflow-engine.md +543 -0
- package/PRPs/PRDs/002-agent-prompt.md +390 -0
- package/PRPs/PRDs/003-agent-prompt.md +943 -0
- package/PRPs/PRDs/004-agent-prompt.md +1136 -0
- package/PRPs/PRDs/tasks-001.json +492 -0
- package/PRPs/README.md +83 -0
- package/PRPs/templates/prp_base.md +222 -0
- package/README.md +218 -0
- package/docs/agent.md +422 -0
- package/docs/prompt.md +419 -0
- package/docs/workflow.md +600 -0
- package/examples/README.md +244 -0
- package/examples/examples/01-basic-workflow.ts +100 -0
- package/examples/examples/02-decorator-options.ts +217 -0
- package/examples/examples/03-parent-child.ts +241 -0
- package/examples/examples/04-observers-debugger.ts +340 -0
- package/examples/examples/05-error-handling.ts +387 -0
- package/examples/examples/06-concurrent-tasks.ts +352 -0
- package/examples/examples/07-agent-loops.ts +432 -0
- package/examples/examples/08-sdk-features.ts +667 -0
- package/examples/examples/09-reflection.ts +573 -0
- package/examples/examples/10-introspection.ts +550 -0
- package/examples/index.ts +143 -0
- package/examples/utils/helpers.ts +57 -0
- package/llms_full.txt +5890 -0
- package/package.json +63 -0
- package/plan/P1P2/PRP.md +527 -0
- package/plan/P1P2/research/LRU_CACHE_BEST_PRACTICES.md +1929 -0
- package/plan/P1P2/research/LRU_CACHE_CODE_PATTERNS.md +857 -0
- package/plan/P1P2/research/LRU_CACHE_INTEGRATION_GUIDE.md +738 -0
- package/plan/P1P2/research/LRU_CACHE_RESEARCH_INDEX.md +424 -0
- package/plan/P1P2/research/REFLECTION_INDEX.md +291 -0
- package/plan/P1P2/research/REFLECTION_RESEARCH_REPORT.md +1342 -0
- package/plan/P1P2/research/RESEARCH_SUMMARY.md +342 -0
- package/plan/P1P2/research/anthropic-sdk.md +174 -0
- package/plan/P1P2/research/async-local-storage.md +200 -0
- package/plan/P1P2/research/reflection-code-patterns.md +1205 -0
- package/plan/P1P2/research/reflection-decision-matrix.md +421 -0
- package/plan/P1P2/research/reflection-implementation-guide.md +1341 -0
- package/plan/P1P2/research/reflection-integration-guide.md +834 -0
- package/plan/P1P2/research/reflection-patterns.md +1468 -0
- package/plan/P1P2/research/reflection-quick-reference.md +558 -0
- package/plan/P1P2/research/zod-schema.md +152 -0
- package/plan/P3P4/PRP.md +1388 -0
- package/plan/P3P4/research/caching-lru.md +116 -0
- package/plan/P3P4/research/introspection-tools.md +177 -0
- package/plan/P3P4/research/reflection-patterns.md +117 -0
- package/plan/P4P5/PRP.md +1136 -0
- package/plan/P4P5/research/RESEARCH_SUMMARY.md +151 -0
- package/plan/architecture/external_deps.md +358 -0
- package/plan/architecture/system_context.md +242 -0
- package/plan/backlog.json +867 -0
- package/plan/research/INTROSPECTION_RESEARCH_SUMMARY.md +378 -0
- package/plan/research/README-INTROSPECTION.md +352 -0
- package/plan/research/agent-introspection-patterns.md +1085 -0
- package/plan/research/introspection-security-guide.md +928 -0
- package/plan/research/introspection-tool-examples.md +875 -0
- package/scripts/generate-llms-full.ts +206 -0
- package/src/__tests__/integration/agent-workflow.test.ts +256 -0
- package/src/__tests__/integration/tree-mirroring.test.ts +114 -0
- package/src/__tests__/unit/agent.test.ts +169 -0
- package/src/__tests__/unit/cache-key.test.ts +182 -0
- package/src/__tests__/unit/cache.test.ts +172 -0
- package/src/__tests__/unit/context.test.ts +138 -0
- package/src/__tests__/unit/decorators.test.ts +100 -0
- package/src/__tests__/unit/introspection-tools.test.ts +277 -0
- package/src/__tests__/unit/prompt.test.ts +135 -0
- package/src/__tests__/unit/reflection.test.ts +210 -0
- package/src/__tests__/unit/tree-debugger.test.ts +85 -0
- package/src/__tests__/unit/workflow.test.ts +81 -0
- package/src/cache/cache-key.ts +244 -0
- package/src/cache/cache.ts +236 -0
- package/src/cache/index.ts +8 -0
- package/src/core/agent.ts +573 -0
- package/src/core/context.ts +119 -0
- package/src/core/event-tree.ts +260 -0
- package/src/core/factory.ts +123 -0
- package/src/core/index.ts +17 -0
- package/src/core/logger.ts +87 -0
- package/src/core/mcp-handler.ts +184 -0
- package/src/core/prompt.ts +150 -0
- package/src/core/workflow-context.ts +349 -0
- package/src/core/workflow.ts +302 -0
- package/src/debugger/index.ts +1 -0
- package/src/debugger/tree-debugger.ts +210 -0
- package/src/decorators/index.ts +3 -0
- package/src/decorators/observed-state.ts +95 -0
- package/src/decorators/step.ts +139 -0
- package/src/decorators/task.ts +96 -0
- package/src/examples/index.ts +2 -0
- package/src/examples/tdd-orchestrator.ts +65 -0
- package/src/examples/test-cycle-workflow.ts +64 -0
- package/src/index.ts +140 -0
- package/src/reflection/index.ts +5 -0
- package/src/reflection/reflection.ts +407 -0
- package/src/tools/index.ts +36 -0
- package/src/tools/introspection.ts +464 -0
- package/src/types/agent.ts +90 -0
- package/src/types/decorators.ts +25 -0
- package/src/types/error-strategy.ts +13 -0
- package/src/types/error.ts +20 -0
- package/src/types/events.ts +74 -0
- package/src/types/index.ts +55 -0
- package/src/types/logging.ts +24 -0
- package/src/types/observer.ts +18 -0
- package/src/types/prompt.ts +40 -0
- package/src/types/reflection.ts +117 -0
- package/src/types/sdk-primitives.ts +128 -0
- package/src/types/snapshot.ts +14 -0
- package/src/types/workflow-context.ts +163 -0
- package/src/types/workflow.ts +37 -0
- package/src/utils/id.ts +11 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/observable.ts +77 -0
- package/tasks.json +0 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
# LRU Cache Code Patterns for LLM Caching
|
|
2
|
+
|
|
3
|
+
**Quick Reference Guide with Copy-Paste Ready Examples**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Installation](#installation)
|
|
10
|
+
2. [Quick Start Pattern](#quick-start-pattern)
|
|
11
|
+
3. [Cache Key Generation Patterns](#cache-key-generation-patterns)
|
|
12
|
+
4. [Configuration Patterns](#configuration-patterns)
|
|
13
|
+
5. [Usage Patterns](#usage-patterns)
|
|
14
|
+
6. [Testing Patterns](#testing-patterns)
|
|
15
|
+
7. [Monitoring Patterns](#monitoring-patterns)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Core dependencies
|
|
23
|
+
npm install lru-cache safe-stable-stringify zod
|
|
24
|
+
|
|
25
|
+
# Optional: For semantic caching
|
|
26
|
+
npm install @xenova/transformers # For embeddings
|
|
27
|
+
|
|
28
|
+
# TypeScript support
|
|
29
|
+
npm install --save-dev typescript @types/node
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Package.json:**
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"lru-cache": "^10.0.0",
|
|
38
|
+
"safe-stable-stringify": "^2.4.0",
|
|
39
|
+
"zod": "^3.22.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"typescript": "^5.2.0",
|
|
43
|
+
"@types/node": "^20.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Quick Start Pattern
|
|
51
|
+
|
|
52
|
+
### Minimal Working Example
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { LRUCache } from 'lru-cache';
|
|
56
|
+
import { createHash } from 'node:crypto';
|
|
57
|
+
import safeStringify from 'safe-stable-stringify';
|
|
58
|
+
|
|
59
|
+
// 1. Create cache
|
|
60
|
+
const cache = new LRUCache<string, string>({
|
|
61
|
+
max: 100,
|
|
62
|
+
ttl: 3600000 // 1 hour
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 2. Generate deterministic key
|
|
66
|
+
function cacheKey(input: any): string {
|
|
67
|
+
const normalized = safeStringify(input);
|
|
68
|
+
const hash = createHash('sha256');
|
|
69
|
+
hash.update(normalized);
|
|
70
|
+
return hash.digest('hex');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 3. Use cache
|
|
74
|
+
async function getCachedResponse(prompt: string): Promise<string> {
|
|
75
|
+
const key = cacheKey({ prompt });
|
|
76
|
+
|
|
77
|
+
return cache.fetch(
|
|
78
|
+
key,
|
|
79
|
+
async () => {
|
|
80
|
+
// This only runs on cache miss
|
|
81
|
+
const response = await expensiveOperation(prompt);
|
|
82
|
+
return response;
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Cache Key Generation Patterns
|
|
91
|
+
|
|
92
|
+
### Pattern 1: Simple String Hashing
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
function simpleKeyHash(text: string): string {
|
|
96
|
+
return createHash('sha256').update(text).digest('hex');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Usage
|
|
100
|
+
const key = simpleKeyHash('user prompt text');
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Pattern 2: Object Hashing (Most Common)
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
function objectKeyHash(obj: Record<string, any>): string {
|
|
107
|
+
const normalized = safeStringify(obj);
|
|
108
|
+
const hash = createHash('sha256');
|
|
109
|
+
hash.update(normalized);
|
|
110
|
+
return hash.digest('hex').substring(0, 16); // 16 chars for readability
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Usage
|
|
114
|
+
const key = objectKeyHash({
|
|
115
|
+
model: 'gpt-4',
|
|
116
|
+
temperature: 0.7,
|
|
117
|
+
prompt: 'What is AI?'
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Pattern 3: Composite Keys with Prefix
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
function compositeKey(
|
|
125
|
+
model: string,
|
|
126
|
+
version: string,
|
|
127
|
+
input: Record<string, any>
|
|
128
|
+
): string {
|
|
129
|
+
const hash = objectKeyHash(input);
|
|
130
|
+
return `${model}:${version}:${hash}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Usage
|
|
134
|
+
const key = compositeKey('gpt-4', 'v1', { prompt: '...' });
|
|
135
|
+
// Output: gpt-4:v1:a1b2c3d4e5f6g7h8
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Pattern 4: Semantic Cache Key (Embedding-based)
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
async function semanticCacheKey(
|
|
142
|
+
prompt: string,
|
|
143
|
+
embeddingModel: any
|
|
144
|
+
): Promise<string> {
|
|
145
|
+
// Get embedding vector
|
|
146
|
+
const embedding = await embeddingModel.embed(prompt);
|
|
147
|
+
|
|
148
|
+
// Round to 2 decimal places for compression
|
|
149
|
+
const compressed = embedding
|
|
150
|
+
.slice(0, 10) // Take first 10 dimensions
|
|
151
|
+
.map((v: number) => Math.round(v * 100) / 100);
|
|
152
|
+
|
|
153
|
+
return safeStringify({
|
|
154
|
+
type: 'semantic',
|
|
155
|
+
embedding: compressed
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Pattern 5: Versioned Keys (Auto-invalidate on schema change)
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
class VersionedKeyGenerator {
|
|
164
|
+
private version: number = 1;
|
|
165
|
+
|
|
166
|
+
constructor(private modelName: string) {}
|
|
167
|
+
|
|
168
|
+
generate(input: any): string {
|
|
169
|
+
const normalized = safeStringify({
|
|
170
|
+
version: this.version,
|
|
171
|
+
model: this.modelName,
|
|
172
|
+
input
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const hash = createHash('sha256');
|
|
176
|
+
hash.update(normalized);
|
|
177
|
+
return hash.digest('hex');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Bump version to invalidate all old cache entries
|
|
181
|
+
invalidateAll(): void {
|
|
182
|
+
this.version++;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Usage
|
|
187
|
+
const keyGen = new VersionedKeyGenerator('gpt-4');
|
|
188
|
+
const key1 = keyGen.generate({ prompt: 'hello' });
|
|
189
|
+
|
|
190
|
+
keyGen.invalidateAll(); // All old keys now invalid
|
|
191
|
+
|
|
192
|
+
const key2 = keyGen.generate({ prompt: 'hello' });
|
|
193
|
+
// key1 !== key2 (different version)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Configuration Patterns
|
|
199
|
+
|
|
200
|
+
### Pattern 1: Development Cache (Small, Short-lived)
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const devCache = new LRUCache<string, any>({
|
|
204
|
+
max: 100,
|
|
205
|
+
ttl: 600000, // 10 minutes
|
|
206
|
+
updateAgeOnGet: true
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Pattern 2: Production Cache (Large, Long-lived)
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const prodCache = new LRUCache<string, any>({
|
|
214
|
+
max: 5000,
|
|
215
|
+
maxSize: 500 * 1024 * 1024, // 500 MB
|
|
216
|
+
ttl: 24 * 3600 * 1000, // 24 hours
|
|
217
|
+
sizeCalculation: (val) => {
|
|
218
|
+
const json = JSON.stringify(val);
|
|
219
|
+
return Buffer.byteLength(json, 'utf8') + 100;
|
|
220
|
+
},
|
|
221
|
+
updateAgeOnGet: true
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Pattern 3: Memory-Constrained Cache
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const memoryConstrainedCache = new LRUCache<string, any>({
|
|
229
|
+
max: 500, // Limit by item count instead of size
|
|
230
|
+
ttl: 3600000, // 1 hour
|
|
231
|
+
updateAgeOnGet: false // Don't refresh on every access
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Pattern 4: High-Throughput Cache
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const highThroughputCache = new LRUCache<string, any>({
|
|
239
|
+
max: 10000,
|
|
240
|
+
ttl: 1800000, // 30 minutes
|
|
241
|
+
updateAgeOnGet: true, // Keep hot items fresh
|
|
242
|
+
|
|
243
|
+
// Optional: Track evictions
|
|
244
|
+
dispose: (value, key, reason) => {
|
|
245
|
+
if (reason === 'evict') {
|
|
246
|
+
console.log(`Evicted key: ${key.substring(0, 8)}...`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Pattern 5: Persistent + In-Memory Hybrid
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { promises as fs } from 'node:fs';
|
|
256
|
+
|
|
257
|
+
class HybridCache {
|
|
258
|
+
private memory: LRUCache<string, any>;
|
|
259
|
+
private persistDir = './cache';
|
|
260
|
+
|
|
261
|
+
constructor() {
|
|
262
|
+
this.memory = new LRUCache<string, any>({
|
|
263
|
+
max: 1000,
|
|
264
|
+
maxSize: 100 * 1024 * 1024
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async get(key: string): Promise<any | undefined> {
|
|
269
|
+
// L1: Memory
|
|
270
|
+
const memValue = this.memory.get(key);
|
|
271
|
+
if (memValue) return memValue;
|
|
272
|
+
|
|
273
|
+
// L2: Disk
|
|
274
|
+
try {
|
|
275
|
+
const filePath = `${this.persistDir}/${key}.json`;
|
|
276
|
+
const data = await fs.readFile(filePath, 'utf8');
|
|
277
|
+
const value = JSON.parse(data);
|
|
278
|
+
|
|
279
|
+
// Promote to memory
|
|
280
|
+
this.memory.set(key, value);
|
|
281
|
+
return value;
|
|
282
|
+
} catch {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async set(key: string, value: any): Promise<void> {
|
|
288
|
+
this.memory.set(key, value);
|
|
289
|
+
|
|
290
|
+
// Persist to disk
|
|
291
|
+
const filePath = `${this.persistDir}/${key}.json`;
|
|
292
|
+
await fs.mkdir(this.persistDir, { recursive: true });
|
|
293
|
+
await fs.writeFile(filePath, JSON.stringify(value));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Usage Patterns
|
|
301
|
+
|
|
302
|
+
### Pattern 1: Basic Fetch (Recommended)
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
async function fetchWithCache(
|
|
306
|
+
prompt: string,
|
|
307
|
+
cache: LRUCache<string, string>
|
|
308
|
+
): Promise<string> {
|
|
309
|
+
const key = createHash('sha256').update(prompt).digest('hex');
|
|
310
|
+
|
|
311
|
+
return cache.fetch(
|
|
312
|
+
key,
|
|
313
|
+
async () => {
|
|
314
|
+
// Only executes on cache miss
|
|
315
|
+
const response = await callLLM(prompt);
|
|
316
|
+
return response;
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
ttl: 24 * 3600 * 1000 // Per-item override
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function callLLM(prompt: string): Promise<string> {
|
|
325
|
+
// Your LLM API call
|
|
326
|
+
return 'LLM response...';
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Pattern 2: Conditional Fetch
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
async function fetchWithExpiry(
|
|
334
|
+
key: string,
|
|
335
|
+
fetchFn: () => Promise<any>,
|
|
336
|
+
cache: LRUCache<string, any>,
|
|
337
|
+
forceRefresh = false
|
|
338
|
+
): Promise<any> {
|
|
339
|
+
if (forceRefresh) {
|
|
340
|
+
// Force refresh even if cached
|
|
341
|
+
const value = await fetchFn();
|
|
342
|
+
cache.set(key, value);
|
|
343
|
+
return value;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return cache.fetch(key, fetchFn);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Usage
|
|
350
|
+
const result = await fetchWithExpiry(
|
|
351
|
+
key,
|
|
352
|
+
async () => expensiveOperation(),
|
|
353
|
+
cache,
|
|
354
|
+
false // Set true to force refresh
|
|
355
|
+
);
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Pattern 3: Batch Operations
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
async function cacheBatchLookups(
|
|
362
|
+
prompts: string[],
|
|
363
|
+
cache: LRUCache<string, string>
|
|
364
|
+
): Promise<Map<string, string>> {
|
|
365
|
+
const results = new Map<string, string>();
|
|
366
|
+
|
|
367
|
+
// Process in parallel with cache deduplication
|
|
368
|
+
await Promise.all(
|
|
369
|
+
prompts.map(async (prompt) => {
|
|
370
|
+
const key = createHash('sha256').update(prompt).digest('hex');
|
|
371
|
+
|
|
372
|
+
const response = await cache.fetch(
|
|
373
|
+
key,
|
|
374
|
+
async () => callLLM(prompt)
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
results.set(prompt, response);
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
return results;
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Pattern 4: Stale-While-Revalidate Pattern
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
async function staleWhileRevalidate(
|
|
389
|
+
key: string,
|
|
390
|
+
cache: LRUCache<string, any>,
|
|
391
|
+
fetchFn: () => Promise<any>
|
|
392
|
+
): Promise<any> {
|
|
393
|
+
// Return stale value immediately if available
|
|
394
|
+
const cached = cache.get(key);
|
|
395
|
+
if (cached) {
|
|
396
|
+
// Refresh in background
|
|
397
|
+
fetchFn().then((fresh) => {
|
|
398
|
+
cache.set(key, fresh);
|
|
399
|
+
}).catch(console.error);
|
|
400
|
+
|
|
401
|
+
return cached;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// No cached value, wait for fetch
|
|
405
|
+
return fetchFn().then((value) => {
|
|
406
|
+
cache.set(key, value);
|
|
407
|
+
return value;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Pattern 5: Cache Warming
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
async function warmCache(
|
|
416
|
+
cache: LRUCache<string, any>,
|
|
417
|
+
prompts: string[]
|
|
418
|
+
): Promise<void> {
|
|
419
|
+
console.log(`Warming cache with ${prompts.length} entries...`);
|
|
420
|
+
|
|
421
|
+
for (const prompt of prompts) {
|
|
422
|
+
const key = createHash('sha256').update(prompt).digest('hex');
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
await cache.fetch(
|
|
426
|
+
key,
|
|
427
|
+
async () => callLLM(prompt),
|
|
428
|
+
{ ttl: 7 * 24 * 3600 * 1000 } // 7 days
|
|
429
|
+
);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
console.error(`Failed to warm cache for prompt: ${prompt}`, error);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log('Cache warming complete');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Usage
|
|
439
|
+
const commonPrompts = [
|
|
440
|
+
'What is AI?',
|
|
441
|
+
'Explain machine learning',
|
|
442
|
+
'Define neural networks'
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
await warmCache(cache, commonPrompts);
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## Testing Patterns
|
|
451
|
+
|
|
452
|
+
### Pattern 1: Cache Hit/Miss Testing
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
import { describe, it, expect } from 'vitest';
|
|
456
|
+
|
|
457
|
+
describe('LLM Cache', () => {
|
|
458
|
+
let cache: LRUCache<string, string>;
|
|
459
|
+
|
|
460
|
+
beforeEach(() => {
|
|
461
|
+
cache = new LRUCache({ max: 100 });
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should have cache hit on repeated prompt', async () => {
|
|
465
|
+
const prompt = 'What is AI?';
|
|
466
|
+
let callCount = 0;
|
|
467
|
+
|
|
468
|
+
const fetchFn = async () => {
|
|
469
|
+
callCount++;
|
|
470
|
+
return 'AI is...';
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// First call - miss
|
|
474
|
+
const result1 = await cache.fetch(
|
|
475
|
+
createHash('sha256').update(prompt).digest('hex'),
|
|
476
|
+
fetchFn
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// Second call - hit
|
|
480
|
+
const result2 = await cache.fetch(
|
|
481
|
+
createHash('sha256').update(prompt).digest('hex'),
|
|
482
|
+
fetchFn
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
expect(result1).toBe(result2);
|
|
486
|
+
expect(callCount).toBe(1); // Only called once
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should evict LRU items', () => {
|
|
490
|
+
const cache = new LRUCache({ max: 2 });
|
|
491
|
+
|
|
492
|
+
cache.set('key1', 'value1');
|
|
493
|
+
cache.set('key2', 'value2');
|
|
494
|
+
cache.set('key3', 'value3');
|
|
495
|
+
|
|
496
|
+
expect(cache.get('key1')).toBeUndefined(); // Evicted
|
|
497
|
+
expect(cache.get('key2')).toBe('value2');
|
|
498
|
+
expect(cache.get('key3')).toBe('value3');
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Pattern 2: Key Generation Testing
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
describe('Cache Key Generation', () => {
|
|
507
|
+
it('should produce same key for equivalent inputs', () => {
|
|
508
|
+
const obj1 = { a: 1, b: 2, c: 3 };
|
|
509
|
+
const obj2 = { c: 3, b: 2, a: 1 };
|
|
510
|
+
|
|
511
|
+
const key1 = objectKeyHash(obj1);
|
|
512
|
+
const key2 = objectKeyHash(obj2);
|
|
513
|
+
|
|
514
|
+
expect(key1).toBe(key2);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('should handle circular references', () => {
|
|
518
|
+
const obj: any = { value: 42 };
|
|
519
|
+
obj.self = obj;
|
|
520
|
+
|
|
521
|
+
expect(() => {
|
|
522
|
+
objectKeyHash(obj);
|
|
523
|
+
}).not.toThrow();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should produce different keys for different inputs', () => {
|
|
527
|
+
const key1 = objectKeyHash({ prompt: 'hello' });
|
|
528
|
+
const key2 = objectKeyHash({ prompt: 'world' });
|
|
529
|
+
|
|
530
|
+
expect(key1).not.toBe(key2);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Pattern 3: Performance Testing
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
import { performance } from 'node:perf_hooks';
|
|
539
|
+
|
|
540
|
+
describe('Cache Performance', () => {
|
|
541
|
+
it('should handle 10k cache hits in < 100ms', () => {
|
|
542
|
+
const cache = new LRUCache<string, string>({ max: 1000 });
|
|
543
|
+
cache.set('key', 'value');
|
|
544
|
+
|
|
545
|
+
const start = performance.now();
|
|
546
|
+
|
|
547
|
+
for (let i = 0; i < 10000; i++) {
|
|
548
|
+
cache.get('key');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const elapsed = performance.now() - start;
|
|
552
|
+
expect(elapsed).toBeLessThan(100);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('should deterministically stringify in < 1ms', () => {
|
|
556
|
+
const obj = {
|
|
557
|
+
messages: Array(10).fill({ role: 'user', content: 'x'.repeat(100) }),
|
|
558
|
+
model: 'gpt-4',
|
|
559
|
+
temperature: 0.7
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const start = performance.now();
|
|
563
|
+
|
|
564
|
+
for (let i = 0; i < 1000; i++) {
|
|
565
|
+
safeStringify(obj);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const elapsed = performance.now() - start;
|
|
569
|
+
expect(elapsed).toBeLessThan(1000); // 1000 iterations < 1 second
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Monitoring Patterns
|
|
577
|
+
|
|
578
|
+
### Pattern 1: Basic Metrics Collection
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
class CacheMetrics {
|
|
582
|
+
hits = 0;
|
|
583
|
+
misses = 0;
|
|
584
|
+
latencies: number[] = [];
|
|
585
|
+
|
|
586
|
+
recordHit(latency: number): void {
|
|
587
|
+
this.hits++;
|
|
588
|
+
this.latencies.push(latency);
|
|
589
|
+
this.trimLatencies();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
recordMiss(latency: number): void {
|
|
593
|
+
this.misses++;
|
|
594
|
+
this.latencies.push(latency);
|
|
595
|
+
this.trimLatencies();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private trimLatencies(): void {
|
|
599
|
+
if (this.latencies.length > 10000) {
|
|
600
|
+
this.latencies = this.latencies.slice(-5000);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
getStats() {
|
|
605
|
+
const total = this.hits + this.misses;
|
|
606
|
+
const avgLatency = this.latencies.length > 0
|
|
607
|
+
? this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length
|
|
608
|
+
: 0;
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
hits: this.hits,
|
|
612
|
+
misses: this.misses,
|
|
613
|
+
hitRate: total > 0 ? ((this.hits / total) * 100).toFixed(2) + '%' : '0%',
|
|
614
|
+
avgLatency: avgLatency.toFixed(3) + ' ms',
|
|
615
|
+
p95Latency: this.percentile(95) + ' ms',
|
|
616
|
+
p99Latency: this.percentile(99) + ' ms'
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private percentile(p: number): string {
|
|
621
|
+
const sorted = [...this.latencies].sort((a, b) => a - b);
|
|
622
|
+
const idx = Math.ceil((p / 100) * sorted.length) - 1;
|
|
623
|
+
return sorted[idx]?.toFixed(3) || '0';
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Pattern 2: Periodic Logging
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
function setupCacheMonitoring(
|
|
632
|
+
cache: LRUCache<string, any>,
|
|
633
|
+
metrics: CacheMetrics,
|
|
634
|
+
intervalMs = 60000
|
|
635
|
+
): () => void {
|
|
636
|
+
const timer = setInterval(() => {
|
|
637
|
+
const stats = metrics.getStats();
|
|
638
|
+
console.log('[Cache Status]', {
|
|
639
|
+
timestamp: new Date().toISOString(),
|
|
640
|
+
size: cache.size,
|
|
641
|
+
...stats
|
|
642
|
+
});
|
|
643
|
+
}, intervalMs);
|
|
644
|
+
|
|
645
|
+
return () => clearInterval(timer);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Usage
|
|
649
|
+
const metrics = new CacheMetrics();
|
|
650
|
+
const stopMonitoring = setupCacheMonitoring(cache, metrics, 30000);
|
|
651
|
+
|
|
652
|
+
// Later
|
|
653
|
+
stopMonitoring();
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Pattern 3: Alert on Low Hit Rate
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
function monitorHitRate(
|
|
660
|
+
metrics: CacheMetrics,
|
|
661
|
+
minHitRate = 0.3, // 30% minimum
|
|
662
|
+
checkIntervalMs = 60000
|
|
663
|
+
): () => void {
|
|
664
|
+
const timer = setInterval(() => {
|
|
665
|
+
const stats = metrics.getStats();
|
|
666
|
+
const hitRate = parseFloat(stats.hitRate);
|
|
667
|
+
|
|
668
|
+
if (hitRate < minHitRate * 100) {
|
|
669
|
+
console.warn(
|
|
670
|
+
`⚠️ Low cache hit rate: ${stats.hitRate} (threshold: ${minHitRate * 100}%)`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}, checkIntervalMs);
|
|
674
|
+
|
|
675
|
+
return () => clearInterval(timer);
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Pattern 4: Export Metrics to JSON
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
async function exportMetrics(
|
|
683
|
+
metrics: CacheMetrics,
|
|
684
|
+
filePath: string
|
|
685
|
+
): Promise<void> {
|
|
686
|
+
const stats = metrics.getStats();
|
|
687
|
+
const data = JSON.stringify(stats, null, 2);
|
|
688
|
+
|
|
689
|
+
await fs.promises.writeFile(filePath, data, 'utf8');
|
|
690
|
+
console.log(`Metrics exported to ${filePath}`);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Usage
|
|
694
|
+
await exportMetrics(metrics, './cache-metrics.json');
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## Edge Cases and Solutions
|
|
700
|
+
|
|
701
|
+
### Handling Large Prompts
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
// For very large prompts, use streaming hash
|
|
705
|
+
async function hashLargePrompt(prompt: string): Promise<string> {
|
|
706
|
+
const hash = createHash('sha256');
|
|
707
|
+
const chunkSize = 65536; // 64 KB
|
|
708
|
+
|
|
709
|
+
for (let i = 0; i < prompt.length; i += chunkSize) {
|
|
710
|
+
hash.update(prompt.slice(i, i + chunkSize));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return hash.digest('hex');
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### Handling Special Characters
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
// Ensure UTF-8 encoding for consistent hashing
|
|
721
|
+
function hashWithEncoding(input: string): string {
|
|
722
|
+
const buffer = Buffer.from(input, 'utf8');
|
|
723
|
+
const hash = createHash('sha256');
|
|
724
|
+
hash.update(buffer);
|
|
725
|
+
return hash.digest('hex');
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
### Handling Null/Undefined Values
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
function safeKeyHash(value: any): string {
|
|
733
|
+
if (value === null || value === undefined) {
|
|
734
|
+
return createHash('sha256').update('null').digest('hex');
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const str = safeStringify(value);
|
|
738
|
+
const hash = createHash('sha256');
|
|
739
|
+
hash.update(str);
|
|
740
|
+
return hash.digest('hex');
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
---
|
|
745
|
+
|
|
746
|
+
## Complete Integration Example
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
// cache.service.ts
|
|
750
|
+
import { LRUCache } from 'lru-cache';
|
|
751
|
+
import { createHash } from 'node:crypto';
|
|
752
|
+
import safeStringify from 'safe-stable-stringify';
|
|
753
|
+
|
|
754
|
+
export interface CacheConfig {
|
|
755
|
+
maxItems?: number;
|
|
756
|
+
maxSizeMB?: number;
|
|
757
|
+
ttlHours?: number;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export class LLMCacheService {
|
|
761
|
+
private cache: LRUCache<string, any>;
|
|
762
|
+
private metrics = {
|
|
763
|
+
hits: 0,
|
|
764
|
+
misses: 0,
|
|
765
|
+
latencies: [] as number[]
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
constructor(config: CacheConfig = {}) {
|
|
769
|
+
const {
|
|
770
|
+
maxItems = 5000,
|
|
771
|
+
maxSizeMB = 500,
|
|
772
|
+
ttlHours = 24
|
|
773
|
+
} = config;
|
|
774
|
+
|
|
775
|
+
this.cache = new LRUCache({
|
|
776
|
+
max: maxItems,
|
|
777
|
+
maxSize: maxSizeMB * 1024 * 1024,
|
|
778
|
+
sizeCalculation: (val) => {
|
|
779
|
+
const json = JSON.stringify(val);
|
|
780
|
+
return Buffer.byteLength(json, 'utf8') + 100;
|
|
781
|
+
},
|
|
782
|
+
ttl: ttlHours * 3600 * 1000,
|
|
783
|
+
updateAgeOnGet: true
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async fetch<T>(
|
|
788
|
+
input: Record<string, any>,
|
|
789
|
+
fetcher: () => Promise<T>
|
|
790
|
+
): Promise<T> {
|
|
791
|
+
const start = performance.now();
|
|
792
|
+
const key = this.generateKey(input);
|
|
793
|
+
|
|
794
|
+
const result = await this.cache.fetch(key, fetcher);
|
|
795
|
+
|
|
796
|
+
const latency = performance.now() - start;
|
|
797
|
+
const cached = this.cache.get(key) !== undefined;
|
|
798
|
+
|
|
799
|
+
if (cached) {
|
|
800
|
+
this.metrics.hits++;
|
|
801
|
+
} else {
|
|
802
|
+
this.metrics.misses++;
|
|
803
|
+
}
|
|
804
|
+
this.metrics.latencies.push(latency);
|
|
805
|
+
|
|
806
|
+
return result;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private generateKey(input: Record<string, any>): string {
|
|
810
|
+
const normalized = safeStringify(input);
|
|
811
|
+
const hash = createHash('sha256');
|
|
812
|
+
hash.update(normalized);
|
|
813
|
+
return hash.digest('hex');
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
getMetrics() {
|
|
817
|
+
const total = this.metrics.hits + this.metrics.misses;
|
|
818
|
+
return {
|
|
819
|
+
hits: this.metrics.hits,
|
|
820
|
+
misses: this.metrics.misses,
|
|
821
|
+
hitRate: total > 0 ? (this.metrics.hits / total * 100).toFixed(2) : '0',
|
|
822
|
+
size: this.cache.size
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
clear(): void {
|
|
827
|
+
this.cache.clear();
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Usage in your application
|
|
832
|
+
const cacheService = new LLMCacheService({
|
|
833
|
+
maxItems: 5000,
|
|
834
|
+
maxSizeMB: 500,
|
|
835
|
+
ttlHours: 24
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// Use in your LLM service
|
|
839
|
+
const response = await cacheService.fetch(
|
|
840
|
+
{
|
|
841
|
+
model: 'gpt-4',
|
|
842
|
+
prompt: 'What is AI?',
|
|
843
|
+
temperature: 0.7
|
|
844
|
+
},
|
|
845
|
+
async () => {
|
|
846
|
+
return callOpenAIAPI(...);
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
// Monitor
|
|
851
|
+
console.log(cacheService.getMetrics());
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
---
|
|
855
|
+
|
|
856
|
+
**Document Version:** 1.0
|
|
857
|
+
**Last Updated:** 2025-12-08
|