agent-cache-optimizer 0.4.0 → 0.5.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-cache-optimizer",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Content-agnostic KV cache optimizer for LLM CLI agents — boosts prompt cache hit rate by 40-88% through automatic stability tracking and block reordering",
5
5
  "keywords": [
6
6
  "kv-cache",
package/src/core.ts CHANGED
@@ -2,7 +2,12 @@ import { createHash } from "node:crypto"
2
2
  import type { StabilityDB } from "./types"
3
3
 
4
4
  /**
5
- * Core hash-tracking engine — fully CLI-agnostic.
5
+ * Core engine — content-addressed hash tracking (CLI-agnostic).
6
+ *
7
+ * v0.5: Added content-addressed tracking. Instead of tracking which hash
8
+ * appears at which POSITION (which breaks when block count changes across
9
+ * calls), we track by CONTENT identity. The same CLAUDE.md block hash
10
+ * gets counted regardless of whether it appears at index 1, 2, or 3.
6
11
  */
7
12
 
8
13
  // ── Hashing ──────────────────────────────────────────────────────────
@@ -14,7 +19,14 @@ export function hashContent(content: string): string {
14
19
  // ── DB operations ────────────────────────────────────────────────────
15
20
 
16
21
  export function emptyDB(): StabilityDB {
17
- return { positions: {}, scores: {}, observations: 0, updated: 0 }
22
+ return {
23
+ positions: {},
24
+ scores: {},
25
+ contentIndex: {},
26
+ contentScores: {},
27
+ observations: 0,
28
+ updated: 0,
29
+ }
18
30
  }
19
31
 
20
32
  export function lookupScore(db: StabilityDB, hash: string): number | null {
@@ -22,7 +34,52 @@ export function lookupScore(db: StabilityDB, hash: string): number | null {
22
34
  return val !== undefined ? val : null
23
35
  }
24
36
 
25
- // ── Stability scoring ────────────────────────────────────────────────
37
+ // ── Content-addressed scoring (primary) ──────────────────────────────
38
+
39
+ /**
40
+ * Look up content-addressed stability score for a block hash.
41
+ * This is position-independent — the same block gets the same score
42
+ * regardless of where it appears in the system prompt.
43
+ */
44
+ export function lookupContentScore(db: StabilityDB, hash: string): number | null {
45
+ const val = db.contentScores[hash]
46
+ return val !== undefined ? val : null
47
+ }
48
+
49
+ /**
50
+ * Update content-addressed tracking.
51
+ *
52
+ * For each block, records its hash in the content index regardless of
53
+ * position. Then recomputes content scores:
54
+ *
55
+ * score = count / observations
56
+ *
57
+ * A block that appears in every call → score → 1.0 (stable)
58
+ * A block that appears once → score → 1/observations (dynamic)
59
+ */
60
+ export function updateContentDB(db: StabilityDB, blocks: string[]): StabilityDB {
61
+ const now = Date.now()
62
+
63
+ for (const block of blocks) {
64
+ const h = hashContent(block)
65
+ const existing = db.contentIndex[h]
66
+ if (existing) {
67
+ existing.lastSeen = now
68
+ existing.count++
69
+ } else {
70
+ db.contentIndex[h] = { hash: h, firstSeen: now, lastSeen: now, count: 1 }
71
+ }
72
+ }
73
+
74
+ // Recompute content scores
75
+ for (const fp of Object.values(db.contentIndex)) {
76
+ db.contentScores[fp.hash] = Math.min(1.0, fp.count / Math.max(1, db.observations))
77
+ }
78
+
79
+ return db
80
+ }
81
+
82
+ // ── Position-based scoring (legacy fallback) ─────────────────────────
26
83
 
27
84
  export function updateDB(db: StabilityDB, blocks: string[]): StabilityDB {
28
85
  const now = Date.now()
@@ -68,27 +125,19 @@ export function isWarm(db: StabilityDB, threshold = 2): boolean {
68
125
 
69
126
  // ── Cache warming ────────────────────────────────────────────────────
70
127
 
71
- /**
72
- * Extract stable hashes from a DB for cache warming.
73
- * A hash is "warmable" if its score >= 0.8 and it has been observed
74
- * at least 3 times at the same position.
75
- */
76
128
  export function extractWarmHashes(db: StabilityDB): Set<string> {
77
129
  const warm = new Set<string>()
78
- for (const fps of Object.values(db.positions)) {
79
- for (const fp of fps) {
80
- const score = db.scores[fp.hash]
81
- if (score !== undefined && score >= 0.8 && fp.count >= 3) {
82
- warm.add(fp.hash)
83
- }
84
- }
130
+ // Primary: content-addressed stable hashes
131
+ for (const [hash, score] of Object.entries(db.contentScores)) {
132
+ if (score >= 0.8) warm.add(hash)
133
+ }
134
+ // Fallback: position-based stable hashes
135
+ for (const [hash, score] of Object.entries(db.scores)) {
136
+ if (score >= 0.8) warm.add(hash)
85
137
  }
86
138
  return warm
87
139
  }
88
140
 
89
- /**
90
- * Check if a block hash is known-stable from cache warming data.
91
- */
92
141
  export function isWarmHash(warmHashes: Set<string> | null, hash: string): boolean {
93
142
  return warmHashes !== null && warmHashes.has(hash)
94
143
  }
@@ -96,13 +145,7 @@ export function isWarmHash(warmHashes: Set<string> | null, hash: string): boolea
96
145
  // ── Cost estimation ──────────────────────────────────────────────────
97
146
 
98
147
  /**
99
- * Estimate cache cost savings based on classification.
100
- *
101
- * DeepSeek v4-pro pricing (per 1M tokens):
102
- * Cache miss (input): $0.435
103
- * Cache hit (input): $0.003625
104
- * Savings: ~$0.431 per 1M cached tokens
105
- *
148
+ * Estimate cache cost savings. DeepSeek v4-pro: $0.435/M miss → $0.003625/M hit.
106
149
  * Rough estimate: 1 token ≈ 4 chars for English text.
107
150
  */
108
151
  export function estimateSavings(
package/src/heuristics.ts CHANGED
@@ -1,64 +1,39 @@
1
1
  import type { StabilityDB, Classified } from "./types"
2
2
  import { splitAll } from "./splitting"
3
- import { hashContent, lookupScore, isWarm } from "./core"
3
+ import { hashContent, lookupScore, lookupContentScore, isWarm } from "./core"
4
4
 
5
5
  /**
6
6
  * Cold-start heuristics — universal position/size/structure signals.
7
7
  *
8
- * These work across ANY agent framework, skill set, or config without
9
- * any content-specific patterns. Principles:
10
- *
11
- * - Position 0 is almost always status/handoff → dynamic
12
- * - Positions 1-7 with substantial content are config → stable
13
- * - Very large blocks (>3KB) are config/definitions → stable
14
- * - Very small blocks (<100B) are status/date → dynamic
15
- * - High date density signals log/diary content → dynamic
16
- * - Structural delimiters ({, [, <, ```) signal config → stable
17
- * - Second-person role assignment → agent prompt → stable
18
- * - Short-line documents (avg < 30 chars) → log/diary → dynamic
19
- * - Tail blocks (last 2) are dynamic UNLESS they look structural
8
+ * v0.5: Content-addressed classification. When content scores are
9
+ * available, they take priority over position-based scores, fixing the
10
+ * "position shift" problem where block count changes bust tracking.
20
11
  */
21
12
 
22
13
  export function coldStartScore(block: string, index: number, total: number): number {
23
14
  let score = 0.5
24
15
 
25
- // ── Position signals ──────────────────────────────────────────
26
-
27
- // Block 0 is status/handoff in virtually every agent framework
28
16
  if (index === 0) score = 0.15
29
-
30
- // Blocks at positions 1-7 with non-trivial content are stable config
31
17
  if (index >= 1 && index <= 7 && block.length > 200) score = 0.8
32
18
 
33
- // Last 2 blocks are usually dynamic, but structured blocks ({, [, <)
34
- // at the tail are probably split artifacts, not real injections.
35
19
  const isStructured = /^[<\{\[]/.test(block.trim())
36
20
  if (index >= total - 2 && !isStructured) score = Math.min(score, 0.25)
37
21
 
38
- // ── Size signals ──────────────────────────────────────────────
39
-
40
22
  if (block.length > 3000) score = Math.max(score, 0.85)
41
23
  if (block.length < 100) score = Math.min(score, 0.2)
42
24
 
43
- // ── Structure signals ─────────────────────────────────────────
44
-
45
- // High density of date stamps → log/diary → dynamic
46
25
  const dateCount = (block.match(/\d{4}-\d{2}-\d{2}/g) || []).length
47
26
  if (dateCount >= 3) score = Math.min(score, 0.25)
48
27
 
49
- // Starts with structural delimiter → JSON, XML, or code fence → config.
50
- // Skip the boost for tail blocks (they're likely <memory> injections).
51
28
  const trimmed = block.trim()
52
29
  if (/^[<\{\[]|^```/.test(trimmed) && index < total - 2) {
53
30
  score = Math.max(score, 0.8)
54
31
  }
55
32
 
56
- // Second-person role assignment → agent system prompt → stable
57
33
  if (/^(You are|Your (job|role|task)|As an? )/m.test(block)) {
58
34
  score = Math.max(score, 0.8)
59
35
  }
60
36
 
61
- // Many very short lines (avg < 30 chars) suggests log/diary → dynamic
62
37
  const lines = block.split("\n")
63
38
  const avgLineLen = block.length / Math.max(1, lines.length)
64
39
  if (lines.length > 15 && avgLineLen < 30) score = Math.min(score, 0.3)
@@ -71,15 +46,17 @@ export function coldStartScore(block: string, index: number, total: number): num
71
46
  /**
72
47
  * Classify blocks into stable / unknown / dynamic.
73
48
  *
74
- * In warm mode (hash-based), uses historical stability scores.
75
- * In cold mode (first few calls per agent), uses position/size heuristics.
49
+ * Scoring priority:
50
+ * 1. Cache warm hash score 0.85 (instant stable)
51
+ * 2. Content-addressed score → score from contentScores (position-independent)
52
+ * 3. Position-based score → score from scores (legacy fallback)
53
+ * 4. Cold-start heuristic → position/size signals
76
54
  */
77
55
  export function classify(
78
56
  blocks: string[],
79
57
  db: StabilityDB,
80
58
  opts?: { warmThreshold?: number; splitThreshold?: number; warmHashes?: Set<string> },
81
59
  ): Classified {
82
- // Split large blocks first
83
60
  const items = splitAll(blocks, opts?.splitThreshold)
84
61
 
85
62
  const result: Classified = { stable: [], unknown: [], dynamic: [] }
@@ -92,14 +69,24 @@ export function classify(
92
69
  if (item === undefined) continue
93
70
 
94
71
  const hash = hashContent(item)
95
- const known = lookupScore(db, hash)
96
- // Cache warming: if hash is in the warm set, treat as stable immediately
97
- const cached = warmSet?.has(hash) ?? false
98
72
 
73
+ // Priority 1: cache-warmed hash
74
+ if (warmSet?.has(hash)) {
75
+ result.stable.push(item)
76
+ continue
77
+ }
78
+
79
+ // Priority 2: content-addressed score (primary)
80
+ const contentScore = lookupContentScore(db, hash)
81
+ if (contentScore !== null && db.observations >= 2) {
82
+ if (contentScore >= 0.7) { result.stable.push(item); continue }
83
+ if (contentScore <= 0.2) { result.dynamic.push(item); continue }
84
+ }
85
+
86
+ // Priority 3: position-based score (fallback)
87
+ const known = lookupScore(db, hash)
99
88
  let score: number
100
- if (cached) {
101
- score = 0.85 // warmed: treat as stable even on cold DB
102
- } else if (known !== null && warm) {
89
+ if (known !== null && warm) {
103
90
  score = known
104
91
  } else {
105
92
  score = coldStartScore(item, i, total)
package/src/index.ts CHANGED
@@ -14,7 +14,7 @@ import type { Plugin } from "@opencode-ai/plugin"
14
14
  import { join } from "node:path"
15
15
  import { homedir } from "node:os"
16
16
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"
17
- import { emptyDB, updateDB, extractWarmHashes, estimateSavings } from "./core"
17
+ import { emptyDB, updateDB, updateContentDB, extractWarmHashes, estimateSavings } from "./core"
18
18
  import { classify } from "./heuristics"
19
19
  import type { StabilityDB } from "./types"
20
20
 
@@ -150,13 +150,14 @@ export const CacheOptimizerPlugin: Plugin = async () => {
150
150
  // Reorder: stable → unknown → dynamic
151
151
  output.system = [...classified.stable, ...classified.unknown, ...classified.dynamic]
152
152
 
153
- // Persist
154
- const updated = updateDB(db, output.system)
155
- saveDB(agent, updated)
153
+ // Persist position-based + content-addressed
154
+ updateDB(db, output.system)
155
+ updateContentDB(db, output.system)
156
+ saveDB(agent, db)
156
157
 
157
158
  // Update warm cache every 10 observations
158
- if (updated.observations % 10 === 0) {
159
- saveWarmCache(updated)
159
+ if (db.observations % 10 === 0) {
160
+ saveWarmCache(db)
160
161
  }
161
162
 
162
163
  // Track savings
@@ -173,7 +174,7 @@ export const CacheOptimizerPlugin: Plugin = async () => {
173
174
  agent,
174
175
  `S:${classified.stable.length} U:${classified.unknown.length} ` +
175
176
  `D:${classified.dynamic.length} T:${output.system.length} ` +
176
- `obs:${updated.observations} ` +
177
+ `obs:${db.observations} ` +
177
178
  `stableKB:${(stableBytes / 1024).toFixed(1)} ` +
178
179
  `saved:$${estCallSaving.toFixed(6)} ` +
179
180
  `total:$${savings.estimatedSavingsUSD.toFixed(4)}`,
@@ -208,7 +209,7 @@ export const CacheOptimizerPlugin: Plugin = async () => {
208
209
  }
209
210
 
210
211
  // Re-exports
211
- export { emptyDB, updateDB, hashContent, lookupScore, isWarm, extractWarmHashes, isWarmHash, estimateSavings } from "./core"
212
+ export { emptyDB, updateDB, updateContentDB, hashContent, lookupScore, lookupContentScore, isWarm, extractWarmHashes, isWarmHash, estimateSavings } from "./core"
212
213
  export { coldStartScore, classify } from "./heuristics"
213
214
  export { splitBlock, splitAll } from "./splitting"
214
215
  export type { StabilityDB, Classified, BlockFingerprint, CacheOptimizerOptions } from "./types"
package/src/types.ts CHANGED
@@ -1,27 +1,36 @@
1
1
  /** A fingerprint record for one hash observed at one position */
2
2
  export interface BlockFingerprint {
3
3
  hash: string
4
- /** First time this exact hash was seen (epoch ms) */
5
4
  firstSeen: number
6
- /** Most recent time this hash was seen */
7
5
  lastSeen: number
8
- /** Total observations of this hash at this position */
9
6
  count: number
10
7
  }
11
8
 
12
- /** Stability databasepersisted per-agent to track block stability over time */
9
+ /** Content-addressed fingerprintposition-independent */
10
+ export interface ContentFingerprint {
11
+ hash: string
12
+ firstSeen: number
13
+ lastSeen: number
14
+ count: number
15
+ }
16
+
17
+ /** Stability database — persisted per-agent */
13
18
  export interface StabilityDB {
14
- /** Block position → fingerprints observed at that position */
19
+ /** Position-based fingerprints (legacy, fallback) */
15
20
  positions: Record<number, BlockFingerprint[]>
16
- /** Hash stability score (1.0 = never changes, 0.0 = changes every call) */
21
+ /** Position-based scores */
17
22
  scores: Record<string, number>
18
- /** Total calls observed */
23
+ /** Content-addressed fingerprints (primary) */
24
+ contentIndex: Record<string, ContentFingerprint>
25
+ /** Content-addressed scores */
26
+ contentScores: Record<string, number>
27
+ /** Total observations */
19
28
  observations: number
20
29
  /** Last write timestamp */
21
30
  updated: number
22
31
  }
23
32
 
24
- /** Classification result after scoring all blocks */
33
+ /** Classification result */
25
34
  export interface Classified {
26
35
  stable: string[]
27
36
  unknown: string[]
@@ -30,10 +39,7 @@ export interface Classified {
30
39
 
31
40
  /** Options for the cache optimizer plugin */
32
41
  export interface CacheOptimizerOptions {
33
- /** Minimum block size in bytes to attempt splitting (default: 4000) */
34
42
  splitThreshold: number
35
- /** Path to store stability databases and logs */
36
43
  stateDir: string
37
- /** Minimum observations before switching from heuristics to hash-based scoring */
38
44
  warmThreshold: number
39
45
  }