ai-functions 2.1.1 → 2.3.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.
Files changed (286) hide show
  1. package/.turbo/turbo-build.log +1 -4
  2. package/CHANGELOG.md +68 -1
  3. package/README.md +397 -157
  4. package/dist/ai-promise.d.ts +50 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +410 -51
  7. package/dist/ai-promise.js.map +1 -1
  8. package/dist/ai-schemas.d.ts +56 -0
  9. package/dist/ai-schemas.d.ts.map +1 -0
  10. package/dist/ai-schemas.js +53 -0
  11. package/dist/ai-schemas.js.map +1 -0
  12. package/dist/ai.d.ts +16 -242
  13. package/dist/ai.d.ts.map +1 -1
  14. package/dist/ai.js +54 -837
  15. package/dist/ai.js.map +1 -1
  16. package/dist/batch/anthropic.d.ts +6 -4
  17. package/dist/batch/anthropic.d.ts.map +1 -1
  18. package/dist/batch/anthropic.js +83 -145
  19. package/dist/batch/anthropic.js.map +1 -1
  20. package/dist/batch/bedrock.d.ts +8 -30
  21. package/dist/batch/bedrock.d.ts.map +1 -1
  22. package/dist/batch/bedrock.js +155 -338
  23. package/dist/batch/bedrock.js.map +1 -1
  24. package/dist/batch/cloudflare.d.ts +8 -20
  25. package/dist/batch/cloudflare.d.ts.map +1 -1
  26. package/dist/batch/cloudflare.js +68 -189
  27. package/dist/batch/cloudflare.js.map +1 -1
  28. package/dist/batch/google.d.ts +6 -20
  29. package/dist/batch/google.d.ts.map +1 -1
  30. package/dist/batch/google.js +70 -238
  31. package/dist/batch/google.js.map +1 -1
  32. package/dist/batch/index.d.ts +4 -1
  33. package/dist/batch/index.d.ts.map +1 -1
  34. package/dist/batch/index.js +4 -1
  35. package/dist/batch/index.js.map +1 -1
  36. package/dist/batch/memory.d.ts +1 -1
  37. package/dist/batch/memory.d.ts.map +1 -1
  38. package/dist/batch/memory.js +14 -10
  39. package/dist/batch/memory.js.map +1 -1
  40. package/dist/batch/openai.d.ts +11 -14
  41. package/dist/batch/openai.d.ts.map +1 -1
  42. package/dist/batch/openai.js +52 -156
  43. package/dist/batch/openai.js.map +1 -1
  44. package/dist/batch/provider.d.ts +111 -0
  45. package/dist/batch/provider.d.ts.map +1 -0
  46. package/dist/batch/provider.js +233 -0
  47. package/dist/batch/provider.js.map +1 -0
  48. package/dist/batch-map.d.ts.map +1 -1
  49. package/dist/batch-map.js +23 -17
  50. package/dist/batch-map.js.map +1 -1
  51. package/dist/batch-queue.d.ts +65 -0
  52. package/dist/batch-queue.d.ts.map +1 -1
  53. package/dist/batch-queue.js +169 -14
  54. package/dist/batch-queue.js.map +1 -1
  55. package/dist/budget.d.ts +272 -0
  56. package/dist/budget.d.ts.map +1 -0
  57. package/dist/budget.js +513 -0
  58. package/dist/budget.js.map +1 -0
  59. package/dist/cache.d.ts +295 -0
  60. package/dist/cache.d.ts.map +1 -0
  61. package/dist/cache.js +433 -0
  62. package/dist/cache.js.map +1 -0
  63. package/dist/context.d.ts +42 -8
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +64 -62
  66. package/dist/context.js.map +1 -1
  67. package/dist/digital-objects-registry.d.ts +229 -0
  68. package/dist/digital-objects-registry.d.ts.map +1 -0
  69. package/dist/digital-objects-registry.js +617 -0
  70. package/dist/digital-objects-registry.js.map +1 -0
  71. package/dist/embeddings.d.ts +2 -2
  72. package/dist/embeddings.d.ts.map +1 -1
  73. package/dist/errors.d.ts +22 -0
  74. package/dist/errors.d.ts.map +1 -0
  75. package/dist/errors.js +35 -0
  76. package/dist/errors.js.map +1 -0
  77. package/dist/eval/runner.d.ts +10 -1
  78. package/dist/eval/runner.d.ts.map +1 -1
  79. package/dist/eval/runner.js +41 -35
  80. package/dist/eval/runner.js.map +1 -1
  81. package/dist/eval-log/in-memory.d.ts +34 -0
  82. package/dist/eval-log/in-memory.d.ts.map +1 -0
  83. package/dist/eval-log/in-memory.js +84 -0
  84. package/dist/eval-log/in-memory.js.map +1 -0
  85. package/dist/eval-log/index.d.ts +29 -0
  86. package/dist/eval-log/index.d.ts.map +1 -0
  87. package/dist/eval-log/index.js +39 -0
  88. package/dist/eval-log/index.js.map +1 -0
  89. package/dist/eval-log/types.d.ts +101 -0
  90. package/dist/eval-log/types.d.ts.map +1 -0
  91. package/dist/eval-log/types.js +16 -0
  92. package/dist/eval-log/types.js.map +1 -0
  93. package/dist/function-registry.d.ts +116 -0
  94. package/dist/function-registry.d.ts.map +1 -0
  95. package/dist/function-registry.js +546 -0
  96. package/dist/function-registry.js.map +1 -0
  97. package/dist/generate.d.ts +9 -3
  98. package/dist/generate.d.ts.map +1 -1
  99. package/dist/generate.js +18 -22
  100. package/dist/generate.js.map +1 -1
  101. package/dist/index.d.ts +35 -20
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +89 -42
  104. package/dist/index.js.map +1 -1
  105. package/dist/logger.d.ts +118 -0
  106. package/dist/logger.d.ts.map +1 -0
  107. package/dist/logger.js +187 -0
  108. package/dist/logger.js.map +1 -0
  109. package/dist/middleware/budget.d.ts +84 -0
  110. package/dist/middleware/budget.d.ts.map +1 -0
  111. package/dist/middleware/budget.js +110 -0
  112. package/dist/middleware/budget.js.map +1 -0
  113. package/dist/middleware/cache.d.ts +103 -0
  114. package/dist/middleware/cache.d.ts.map +1 -0
  115. package/dist/middleware/cache.js +228 -0
  116. package/dist/middleware/cache.js.map +1 -0
  117. package/dist/middleware/embed-cache.d.ts +99 -0
  118. package/dist/middleware/embed-cache.d.ts.map +1 -0
  119. package/dist/middleware/embed-cache.js +128 -0
  120. package/dist/middleware/embed-cache.js.map +1 -0
  121. package/dist/middleware/index.d.ts +11 -0
  122. package/dist/middleware/index.d.ts.map +1 -0
  123. package/dist/middleware/index.js +11 -0
  124. package/dist/middleware/index.js.map +1 -0
  125. package/dist/middleware/trace.d.ts +103 -0
  126. package/dist/middleware/trace.d.ts.map +1 -0
  127. package/dist/middleware/trace.js +176 -0
  128. package/dist/middleware/trace.js.map +1 -0
  129. package/dist/primitives.d.ts +120 -1
  130. package/dist/primitives.d.ts.map +1 -1
  131. package/dist/primitives.js +398 -26
  132. package/dist/primitives.js.map +1 -1
  133. package/dist/retry.d.ts +368 -0
  134. package/dist/retry.d.ts.map +1 -0
  135. package/dist/retry.js +646 -0
  136. package/dist/retry.js.map +1 -0
  137. package/dist/schema.d.ts.map +1 -1
  138. package/dist/schema.js +2 -10
  139. package/dist/schema.js.map +1 -1
  140. package/dist/telemetry.d.ts +128 -0
  141. package/dist/telemetry.d.ts.map +1 -0
  142. package/dist/telemetry.js +285 -0
  143. package/dist/telemetry.js.map +1 -0
  144. package/dist/template.d.ts.map +1 -1
  145. package/dist/template.js +6 -1
  146. package/dist/template.js.map +1 -1
  147. package/dist/tool-orchestration.d.ts +453 -0
  148. package/dist/tool-orchestration.d.ts.map +1 -0
  149. package/dist/tool-orchestration.js +763 -0
  150. package/dist/tool-orchestration.js.map +1 -0
  151. package/dist/type-guards.d.ts +28 -0
  152. package/dist/type-guards.d.ts.map +1 -0
  153. package/dist/type-guards.js +29 -0
  154. package/dist/type-guards.js.map +1 -0
  155. package/dist/types.d.ts +135 -17
  156. package/dist/types.d.ts.map +1 -1
  157. package/dist/types.js +36 -1
  158. package/dist/types.js.map +1 -1
  159. package/dist/wrap-for-v3.d.ts +80 -0
  160. package/dist/wrap-for-v3.d.ts.map +1 -0
  161. package/dist/wrap-for-v3.js +89 -0
  162. package/dist/wrap-for-v3.js.map +1 -0
  163. package/examples/00-quickstart.ts +232 -0
  164. package/examples/01-rag-chatbot.ts +212 -0
  165. package/examples/02-multi-agent-research.ts +290 -0
  166. package/examples/03-email-classification.ts +379 -0
  167. package/examples/04-content-moderation.ts +400 -0
  168. package/examples/05-document-extraction.ts +455 -0
  169. package/examples/06-streaming-chat-nextjs.ts +437 -0
  170. package/examples/07-cloudflare-worker.ts +483 -0
  171. package/examples/08-batch-processing.ts +491 -0
  172. package/examples/09-budget-constrained.ts +527 -0
  173. package/examples/10-tool-orchestration.ts +565 -0
  174. package/examples/11-retry-resilience.ts +403 -0
  175. package/examples/12-caching-strategies.ts +422 -0
  176. package/examples/README.md +145 -0
  177. package/package.json +10 -6
  178. package/src/ai-promise.ts +528 -99
  179. package/src/ai-schemas.ts +122 -0
  180. package/src/ai.ts +69 -1153
  181. package/src/batch/anthropic.ts +96 -161
  182. package/src/batch/bedrock.ts +203 -454
  183. package/src/batch/cloudflare.ts +99 -282
  184. package/src/batch/google.ts +91 -297
  185. package/src/batch/index.ts +4 -1
  186. package/src/batch/memory.ts +15 -10
  187. package/src/batch/openai.ts +65 -193
  188. package/src/batch/provider.ts +336 -0
  189. package/src/batch-map.ts +29 -24
  190. package/src/batch-queue.ts +200 -11
  191. package/src/budget.ts +740 -0
  192. package/src/cache.ts +681 -0
  193. package/src/context.ts +122 -76
  194. package/src/digital-objects-registry.ts +750 -0
  195. package/src/errors.ts +37 -0
  196. package/src/eval/runner.ts +63 -38
  197. package/src/eval-log/in-memory.ts +90 -0
  198. package/src/eval-log/index.ts +46 -0
  199. package/src/eval-log/types.ts +110 -0
  200. package/src/function-registry.ts +671 -0
  201. package/src/generate.ts +33 -33
  202. package/src/index.ts +325 -49
  203. package/src/logger.ts +232 -0
  204. package/src/middleware/budget.ts +171 -0
  205. package/src/middleware/cache.ts +299 -0
  206. package/src/middleware/embed-cache.ts +195 -0
  207. package/src/middleware/index.ts +23 -0
  208. package/src/middleware/trace.ts +248 -0
  209. package/src/primitives.ts +589 -62
  210. package/src/retry.ts +902 -0
  211. package/src/schema.ts +8 -17
  212. package/src/telemetry.ts +403 -0
  213. package/src/template.ts +8 -4
  214. package/src/tool-orchestration.ts +1173 -0
  215. package/src/type-guards.ts +31 -0
  216. package/src/types.ts +164 -25
  217. package/src/wrap-for-v3.ts +105 -0
  218. package/test/ai-promise.test.ts +1080 -0
  219. package/test/ai-proxy.test.ts +1 -1
  220. package/test/backward-compat.test.ts +147 -0
  221. package/test/batch-autosubmit-errors.test.ts +610 -0
  222. package/test/batch-blog-posts.test.ts +87 -129
  223. package/test/budget-tracking.test.ts +800 -0
  224. package/test/cache.test.ts +712 -0
  225. package/test/context-isolation.test.ts +687 -0
  226. package/test/core-functions.test.ts +183 -579
  227. package/test/decide.test.ts +154 -322
  228. package/test/define.test.ts +211 -8
  229. package/test/digital-objects-registry.test.ts +760 -0
  230. package/test/embedding-cache-middleware.test.ts +140 -0
  231. package/test/evals/deterministic.eval.test.ts +376 -0
  232. package/test/generate-core.test.ts +140 -229
  233. package/test/implicit-batch.test.ts +22 -65
  234. package/test/json-parse-error-handling.test.ts +463 -0
  235. package/test/retry-policy-integration.test.ts +117 -0
  236. package/test/retry.test.ts +1016 -0
  237. package/test/schema.test.ts +55 -19
  238. package/test/streaming.test.ts +316 -0
  239. package/test/template.test.ts +1164 -0
  240. package/test/tool-orchestration.test.ts +1040 -0
  241. package/test/wrap-for-v3.test.ts +612 -0
  242. package/vitest.config.js +6 -0
  243. package/vitest.config.ts +20 -0
  244. package/dist/rpc/auth.d.ts +0 -69
  245. package/dist/rpc/auth.d.ts.map +0 -1
  246. package/dist/rpc/auth.js +0 -136
  247. package/dist/rpc/auth.js.map +0 -1
  248. package/dist/rpc/client.d.ts +0 -62
  249. package/dist/rpc/client.d.ts.map +0 -1
  250. package/dist/rpc/client.js +0 -103
  251. package/dist/rpc/client.js.map +0 -1
  252. package/dist/rpc/deferred.d.ts +0 -60
  253. package/dist/rpc/deferred.d.ts.map +0 -1
  254. package/dist/rpc/deferred.js +0 -96
  255. package/dist/rpc/deferred.js.map +0 -1
  256. package/dist/rpc/index.d.ts +0 -22
  257. package/dist/rpc/index.d.ts.map +0 -1
  258. package/dist/rpc/index.js +0 -38
  259. package/dist/rpc/index.js.map +0 -1
  260. package/dist/rpc/local.d.ts +0 -42
  261. package/dist/rpc/local.d.ts.map +0 -1
  262. package/dist/rpc/local.js +0 -50
  263. package/dist/rpc/local.js.map +0 -1
  264. package/dist/rpc/server.d.ts +0 -165
  265. package/dist/rpc/server.d.ts.map +0 -1
  266. package/dist/rpc/server.js +0 -405
  267. package/dist/rpc/server.js.map +0 -1
  268. package/dist/rpc/session.d.ts +0 -32
  269. package/dist/rpc/session.d.ts.map +0 -1
  270. package/dist/rpc/session.js +0 -43
  271. package/dist/rpc/session.js.map +0 -1
  272. package/dist/rpc/transport.d.ts +0 -306
  273. package/dist/rpc/transport.d.ts.map +0 -1
  274. package/dist/rpc/transport.js +0 -731
  275. package/dist/rpc/transport.js.map +0 -1
  276. package/src/batch/anthropic.js +0 -256
  277. package/src/batch/bedrock.js +0 -584
  278. package/src/batch/cloudflare.js +0 -287
  279. package/src/batch/google.js +0 -359
  280. package/src/batch/index.js +0 -30
  281. package/src/batch/memory.js +0 -187
  282. package/src/batch/openai.js +0 -402
  283. package/src/eval/index.js +0 -7
  284. package/src/eval/models.js +0 -119
  285. package/src/eval/runner.js +0 -147
  286. package/test/schema.test.js +0 -96
package/src/cache.ts ADDED
@@ -0,0 +1,681 @@
1
+ /**
2
+ * Caching layer for embeddings and generations
3
+ *
4
+ * Provides content-addressable caching for embeddings and parameter-aware
5
+ * caching for text/object generations with TTL support and LRU eviction.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Cache entry with metadata for tracking and eviction
16
+ */
17
+ export interface CacheEntry<T> {
18
+ /** The cached value */
19
+ value: T
20
+ /** When the entry was created */
21
+ createdAt: number
22
+ /** When the entry was last accessed */
23
+ lastAccessedAt: number
24
+ /** Number of times this entry has been accessed */
25
+ accessCount: number
26
+ /** When the entry expires (if TTL is set) */
27
+ expiresAt?: number
28
+ }
29
+
30
+ /**
31
+ * Options for cache operations
32
+ */
33
+ export interface CacheOptions {
34
+ /** Time-to-live in milliseconds */
35
+ ttl?: number
36
+ /** Whether to bypass cache and force fresh result */
37
+ bypass?: boolean
38
+ }
39
+
40
+ /**
41
+ * Cache statistics for monitoring
42
+ */
43
+ export interface CacheStats {
44
+ /** Number of cache hits */
45
+ hits: number
46
+ /** Number of cache misses */
47
+ misses: number
48
+ /** Hit rate (0-1) */
49
+ hitRate: number
50
+ /** Current number of entries */
51
+ size: number
52
+ }
53
+
54
+ /**
55
+ * Configuration options for MemoryCache
56
+ */
57
+ export interface MemoryCacheOptions {
58
+ /** Default TTL for entries in milliseconds */
59
+ defaultTTL?: number
60
+ /** Maximum number of entries (enables LRU eviction) */
61
+ maxSize?: number
62
+ /** Whether to refresh TTL on access (sliding window) */
63
+ slidingExpiration?: boolean
64
+ /** Interval for cleanup of expired entries in milliseconds */
65
+ cleanupInterval?: number
66
+ }
67
+
68
+ // ============================================================================
69
+ // Cache Storage Interface
70
+ // ============================================================================
71
+
72
+ /**
73
+ * Abstract cache storage interface for pluggable backends
74
+ */
75
+ export interface CacheStorage<T> {
76
+ /** Get a value by key */
77
+ get(key: string): Promise<T | undefined>
78
+ /** Set a value by key */
79
+ set(key: string, value: T, options?: CacheOptions): Promise<void>
80
+ /** Check if a key exists */
81
+ has(key: string): Promise<boolean>
82
+ /** Delete a key */
83
+ delete(key: string): Promise<void>
84
+ /** Clear all entries */
85
+ clear(): Promise<void>
86
+ /** Get the number of entries */
87
+ size(): Promise<number>
88
+ /** Get all keys */
89
+ keys(): Promise<string[]>
90
+ }
91
+
92
+ // ============================================================================
93
+ // Memory Cache Implementation
94
+ // ============================================================================
95
+
96
+ /**
97
+ * In-memory cache implementation with TTL and LRU eviction support
98
+ *
99
+ * @deprecated Phase C Week 1 — `MemoryCache` has zero production callers in
100
+ * primitives.org.ai (audited 2026-05-06; see `bd show aip-ibid`). New code
101
+ * should use `cacheMiddleware` (for `wrapLanguageModel`) or
102
+ * `embeddingCacheMiddleware` (for `wrapEmbeddingModel`) instead — both
103
+ * compose with AI SDK 6's `wrapLanguageModel` / `wrapEmbeddingModel` and
104
+ * carry per-call telemetry (TraceEvent emission) and budget tracking when
105
+ * paired via `wrapForV3`. The `MemoryCache` class will be removed in the
106
+ * Phase C semver bump alongside `EmbeddingCache` and `GenerationCache`.
107
+ */
108
+ export class MemoryCache<T> implements CacheStorage<T> {
109
+ private cache: Map<string, CacheEntry<T>> = new Map()
110
+ private accessOrder: string[] = []
111
+ private options: MemoryCacheOptions
112
+ private cleanupTimer?: ReturnType<typeof setInterval>
113
+
114
+ constructor(options: MemoryCacheOptions = {}) {
115
+ this.options = options
116
+
117
+ // Start cleanup timer if interval is specified
118
+ if (options.cleanupInterval && options.cleanupInterval > 0) {
119
+ this.cleanupTimer = setInterval(() => {
120
+ this.cleanup()
121
+ }, options.cleanupInterval)
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Get a value by key
127
+ */
128
+ async get(key: string): Promise<T | undefined> {
129
+ const entry = this.cache.get(key)
130
+
131
+ if (!entry) {
132
+ return undefined
133
+ }
134
+
135
+ // Check if expired
136
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
137
+ this.cache.delete(key)
138
+ this.removeFromAccessOrder(key)
139
+ return undefined
140
+ }
141
+
142
+ // Update access tracking
143
+ entry.lastAccessedAt = Date.now()
144
+ entry.accessCount++
145
+
146
+ // Update access order for LRU
147
+ this.updateAccessOrder(key)
148
+
149
+ // Sliding expiration - refresh TTL on access
150
+ if (this.options.slidingExpiration && this.options.defaultTTL) {
151
+ entry.expiresAt = Date.now() + this.options.defaultTTL
152
+ }
153
+
154
+ return entry.value
155
+ }
156
+
157
+ /**
158
+ * Set a value by key
159
+ */
160
+ async set(key: string, value: T, options?: CacheOptions): Promise<void> {
161
+ const now = Date.now()
162
+ const ttl = options?.ttl ?? this.options.defaultTTL
163
+
164
+ const entry: CacheEntry<T> = {
165
+ value,
166
+ createdAt: now,
167
+ lastAccessedAt: now,
168
+ accessCount: 0,
169
+ ...(ttl ? { expiresAt: now + ttl } : {}),
170
+ }
171
+
172
+ // Check if we need to evict (LRU)
173
+ if (this.options.maxSize && this.cache.size >= this.options.maxSize && !this.cache.has(key)) {
174
+ this.evictLRU()
175
+ }
176
+
177
+ this.cache.set(key, entry)
178
+ this.updateAccessOrder(key)
179
+ }
180
+
181
+ /**
182
+ * Check if a key exists
183
+ */
184
+ async has(key: string): Promise<boolean> {
185
+ const entry = this.cache.get(key)
186
+
187
+ if (!entry) {
188
+ return false
189
+ }
190
+
191
+ // Check if expired
192
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
193
+ this.cache.delete(key)
194
+ this.removeFromAccessOrder(key)
195
+ return false
196
+ }
197
+
198
+ return true
199
+ }
200
+
201
+ /**
202
+ * Delete a key
203
+ */
204
+ async delete(key: string): Promise<void> {
205
+ this.cache.delete(key)
206
+ this.removeFromAccessOrder(key)
207
+ }
208
+
209
+ /**
210
+ * Clear all entries
211
+ */
212
+ async clear(): Promise<void> {
213
+ this.cache.clear()
214
+ this.accessOrder = []
215
+ }
216
+
217
+ /**
218
+ * Get the number of entries (excluding expired)
219
+ */
220
+ async size(): Promise<number> {
221
+ await this.cleanup()
222
+ return this.cache.size
223
+ }
224
+
225
+ /**
226
+ * Get all keys
227
+ */
228
+ async keys(): Promise<string[]> {
229
+ await this.cleanup()
230
+ return Array.from(this.cache.keys())
231
+ }
232
+
233
+ /**
234
+ * Get full entry with metadata
235
+ */
236
+ async getEntry(key: string): Promise<CacheEntry<T> | undefined> {
237
+ const entry = this.cache.get(key)
238
+
239
+ if (!entry) {
240
+ return undefined
241
+ }
242
+
243
+ // Check if expired
244
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
245
+ this.cache.delete(key)
246
+ this.removeFromAccessOrder(key)
247
+ return undefined
248
+ }
249
+
250
+ return entry
251
+ }
252
+
253
+ /**
254
+ * Dispose cleanup timer
255
+ */
256
+ dispose(): void {
257
+ if (this.cleanupTimer) {
258
+ clearInterval(this.cleanupTimer)
259
+ delete this.cleanupTimer
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Clean up expired entries
265
+ */
266
+ private async cleanup(): Promise<void> {
267
+ const now = Date.now()
268
+ const keysToDelete: string[] = []
269
+
270
+ for (const [key, entry] of this.cache) {
271
+ if (entry.expiresAt && now > entry.expiresAt) {
272
+ keysToDelete.push(key)
273
+ }
274
+ }
275
+
276
+ for (const key of keysToDelete) {
277
+ this.cache.delete(key)
278
+ this.removeFromAccessOrder(key)
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Evict the least recently used entry
284
+ */
285
+ private evictLRU(): void {
286
+ if (this.accessOrder.length === 0) return
287
+
288
+ const lruKey = this.accessOrder[0]
289
+ if (lruKey) {
290
+ this.cache.delete(lruKey)
291
+ this.accessOrder.shift()
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Update access order for LRU tracking
297
+ */
298
+ private updateAccessOrder(key: string): void {
299
+ this.removeFromAccessOrder(key)
300
+ this.accessOrder.push(key)
301
+ }
302
+
303
+ /**
304
+ * Remove a key from access order
305
+ */
306
+ private removeFromAccessOrder(key: string): void {
307
+ const index = this.accessOrder.indexOf(key)
308
+ if (index > -1) {
309
+ this.accessOrder.splice(index, 1)
310
+ }
311
+ }
312
+ }
313
+
314
+ // ============================================================================
315
+ // Hash / Key Generation
316
+ // ============================================================================
317
+
318
+ /**
319
+ * Generate a hash for cache keys
320
+ * Uses a fast, non-cryptographic hash suitable for cache keys
321
+ */
322
+ export function hashKey(input: unknown): string {
323
+ const str = typeof input === 'string' ? input : stableStringify(input)
324
+
325
+ // Simple djb2 hash
326
+ let hash = 5381
327
+ for (let i = 0; i < str.length; i++) {
328
+ hash = (hash << 5) + hash + str.charCodeAt(i)
329
+ hash = hash & hash // Convert to 32bit integer
330
+ }
331
+
332
+ return (hash >>> 0).toString(36)
333
+ }
334
+
335
+ /**
336
+ * Stringify an object with sorted keys for stable hashing
337
+ */
338
+ function stableStringify(obj: unknown): string {
339
+ if (obj === null || typeof obj !== 'object') {
340
+ return JSON.stringify(obj)
341
+ }
342
+
343
+ if (Array.isArray(obj)) {
344
+ return '[' + obj.map(stableStringify).join(',') + ']'
345
+ }
346
+
347
+ const keys = Object.keys(obj).sort()
348
+ const pairs = keys.map((key) => {
349
+ return JSON.stringify(key) + ':' + stableStringify((obj as Record<string, unknown>)[key])
350
+ })
351
+
352
+ return '{' + pairs.join(',') + '}'
353
+ }
354
+
355
+ /**
356
+ * Cache key type
357
+ */
358
+ export type CacheKeyType = 'embedding' | 'generation'
359
+
360
+ /**
361
+ * Create a cache key for a specific type and parameters
362
+ */
363
+ export function createCacheKey(type: CacheKeyType, params: Record<string, unknown>): string {
364
+ const hash = hashKey(params)
365
+ return `${type}:${hash}`
366
+ }
367
+
368
+ // ============================================================================
369
+ // Embedding Cache
370
+ // ============================================================================
371
+
372
+ /**
373
+ * Options for embedding cache operations
374
+ */
375
+ export interface EmbeddingCacheOptions {
376
+ /** The embedding model used */
377
+ model: string
378
+ }
379
+
380
+ /**
381
+ * Result from batch embedding cache lookup
382
+ */
383
+ export interface BatchEmbeddingResult {
384
+ /** Map of text to cached embedding */
385
+ hits: Record<string, number[]>
386
+ /** Texts that were not in cache */
387
+ misses: string[]
388
+ }
389
+
390
+ /**
391
+ * Specialized cache for embedding vectors
392
+ *
393
+ * @deprecated Phase C Week 1 — `EmbeddingCache` has zero production callers
394
+ * in primitives.org.ai (audited 2026-05-06; see `bd show aip-ibid`). New
395
+ * code should use `embeddingCacheMiddleware` (for `wrapEmbeddingModel`) —
396
+ * it composes with AI SDK 6 directly and carries trace + budget telemetry
397
+ * when paired via `wrapForV3`. Note: `embeddingCacheMiddleware` keys on the
398
+ * whole batch, not per-text — callers wanting per-text caching should pass
399
+ * stable per-text batches. Will be removed in the Phase C semver bump.
400
+ */
401
+ export class EmbeddingCache {
402
+ private storage: MemoryCache<number[]>
403
+ private stats = { hits: 0, misses: 0 }
404
+
405
+ constructor(options?: MemoryCacheOptions) {
406
+ this.storage = new MemoryCache<number[]>(options)
407
+ }
408
+
409
+ /**
410
+ * Get a cached embedding
411
+ */
412
+ async get(content: string, options: EmbeddingCacheOptions): Promise<number[] | undefined> {
413
+ const key = createCacheKey('embedding', { content, model: options.model })
414
+ const result = await this.storage.get(key)
415
+
416
+ if (result) {
417
+ this.stats.hits++
418
+ } else {
419
+ this.stats.misses++
420
+ }
421
+
422
+ return result
423
+ }
424
+
425
+ /**
426
+ * Set a cached embedding
427
+ */
428
+ async set(content: string, embedding: number[], options: EmbeddingCacheOptions): Promise<void> {
429
+ const key = createCacheKey('embedding', { content, model: options.model })
430
+ await this.storage.set(key, embedding)
431
+ }
432
+
433
+ /**
434
+ * Set multiple embeddings at once
435
+ */
436
+ async setMany(
437
+ texts: string[],
438
+ embeddings: number[][],
439
+ options: EmbeddingCacheOptions
440
+ ): Promise<void> {
441
+ for (let i = 0; i < texts.length; i++) {
442
+ await this.set(texts[i]!, embeddings[i]!, options)
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Get multiple embeddings, returning hits and misses
448
+ */
449
+ async getMany(texts: string[], options: EmbeddingCacheOptions): Promise<BatchEmbeddingResult> {
450
+ const hits: Record<string, number[]> = {}
451
+ const misses: string[] = []
452
+
453
+ for (const text of texts) {
454
+ const embedding = await this.get(text, options)
455
+ if (embedding) {
456
+ hits[text] = embedding
457
+ // Note: hits counter already incremented in get()
458
+ this.stats.hits-- // Avoid double counting
459
+ } else {
460
+ misses.push(text)
461
+ this.stats.misses-- // Avoid double counting
462
+ }
463
+ }
464
+
465
+ // Add back correct counts
466
+ this.stats.hits += Object.keys(hits).length
467
+ this.stats.misses += misses.length
468
+
469
+ return { hits, misses }
470
+ }
471
+
472
+ /**
473
+ * Get cache statistics
474
+ */
475
+ getStats(): CacheStats {
476
+ const total = this.stats.hits + this.stats.misses
477
+ return {
478
+ hits: this.stats.hits,
479
+ misses: this.stats.misses,
480
+ hitRate: total > 0 ? this.stats.hits / total : 0,
481
+ size: this.storage['cache'].size,
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Clear the cache
487
+ */
488
+ async clear(): Promise<void> {
489
+ await this.storage.clear()
490
+ this.stats = { hits: 0, misses: 0 }
491
+ }
492
+ }
493
+
494
+ // ============================================================================
495
+ // Generation Cache
496
+ // ============================================================================
497
+
498
+ /**
499
+ * Parameters for generation cache key
500
+ */
501
+ export interface GenerationParams {
502
+ /** The prompt text */
503
+ prompt: string
504
+ /** The model to use */
505
+ model: string
506
+ /** System prompt */
507
+ system?: string
508
+ /** Temperature setting */
509
+ temperature?: number
510
+ /** Schema version for structured outputs */
511
+ schemaVersion?: string
512
+ }
513
+
514
+ /**
515
+ * Options for generation cache retrieval
516
+ */
517
+ export interface GenerationCacheGetOptions {
518
+ /** Bypass cache and return undefined */
519
+ bypass?: boolean
520
+ }
521
+
522
+ /**
523
+ * Specialized cache for generation results
524
+ *
525
+ * @deprecated Phase C Week 1 — `GenerationCache` has zero production callers
526
+ * in primitives.org.ai (audited 2026-05-06; see `bd show aip-ibid`). New
527
+ * code should use `cacheMiddleware` (for `wrapLanguageModel`) — it composes
528
+ * with AI SDK 6 directly and carries trace + budget telemetry when paired
529
+ * via `wrapForV3`. Will be removed in the Phase C semver bump.
530
+ */
531
+ export class GenerationCache {
532
+ private storage: MemoryCache<unknown>
533
+ private stats = { hits: 0, misses: 0 }
534
+
535
+ constructor(options?: MemoryCacheOptions) {
536
+ this.storage = new MemoryCache<unknown>(options)
537
+ }
538
+
539
+ /**
540
+ * Get a cached generation result
541
+ */
542
+ async get<T = unknown>(
543
+ params: GenerationParams,
544
+ options?: GenerationCacheGetOptions
545
+ ): Promise<T | undefined> {
546
+ if (options?.bypass) {
547
+ return undefined
548
+ }
549
+
550
+ const key = this.createKey(params)
551
+ const result = await this.storage.get(key)
552
+
553
+ if (result) {
554
+ this.stats.hits++
555
+ } else {
556
+ this.stats.misses++
557
+ }
558
+
559
+ return result as T | undefined
560
+ }
561
+
562
+ /**
563
+ * Set a cached generation result
564
+ */
565
+ async set<T = unknown>(params: GenerationParams, result: T): Promise<void> {
566
+ const key = this.createKey(params)
567
+ await this.storage.set(key, result)
568
+ }
569
+
570
+ /**
571
+ * Get cache statistics
572
+ */
573
+ getStats(): CacheStats {
574
+ const total = this.stats.hits + this.stats.misses
575
+ return {
576
+ hits: this.stats.hits,
577
+ misses: this.stats.misses,
578
+ hitRate: total > 0 ? this.stats.hits / total : 0,
579
+ size: this.storage['cache'].size,
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Clear the cache
585
+ */
586
+ async clear(): Promise<void> {
587
+ await this.storage.clear()
588
+ this.stats = { hits: 0, misses: 0 }
589
+ }
590
+
591
+ /**
592
+ * Create a cache key from generation parameters
593
+ */
594
+ private createKey(params: GenerationParams): string {
595
+ const keyParams: Record<string, unknown> = {
596
+ prompt: params.prompt,
597
+ model: params.model,
598
+ }
599
+
600
+ if (params.system !== undefined) {
601
+ keyParams['system'] = params.system
602
+ }
603
+
604
+ if (params.temperature !== undefined) {
605
+ keyParams['temperature'] = params.temperature
606
+ }
607
+
608
+ if (params.schemaVersion !== undefined) {
609
+ keyParams['schemaVersion'] = params.schemaVersion
610
+ }
611
+
612
+ return createCacheKey('generation', keyParams)
613
+ }
614
+ }
615
+
616
+ // ============================================================================
617
+ // withCache Wrapper
618
+ // ============================================================================
619
+
620
+ /**
621
+ * Options for withCache wrapper
622
+ */
623
+ export interface WithCacheOptions<TArgs extends unknown[]> {
624
+ /** Function to generate cache key from arguments */
625
+ keyFn: (...args: TArgs) => string
626
+ /** TTL for cached entries */
627
+ ttl?: number
628
+ }
629
+
630
+ /**
631
+ * Cached function type with bypass support
632
+ */
633
+ export interface CachedFunction<TArgs extends unknown[], TResult> {
634
+ (...args: TArgs): Promise<TResult>
635
+ /** Call with cache bypass (force fresh result) */
636
+ bypass: (...args: TArgs) => Promise<TResult>
637
+ }
638
+
639
+ /**
640
+ * Wrap an async function with caching
641
+ */
642
+ export function withCache<TArgs extends unknown[], TResult>(
643
+ cache: CacheStorage<TResult>,
644
+ fn: (...args: TArgs) => Promise<TResult>,
645
+ options: WithCacheOptions<TArgs>
646
+ ): CachedFunction<TArgs, TResult> {
647
+ const { keyFn, ttl } = options
648
+
649
+ const cachedFn = async (...args: TArgs): Promise<TResult> => {
650
+ const key = keyFn(...args)
651
+
652
+ // Check cache
653
+ const cached = await cache.get(key)
654
+ if (cached !== undefined) {
655
+ return cached
656
+ }
657
+
658
+ // Execute function
659
+ const result = await fn(...args)
660
+
661
+ // Cache result (don't cache errors - they throw before reaching here)
662
+ await cache.set(key, result, ttl !== undefined ? { ttl } : undefined)
663
+
664
+ return result
665
+ }
666
+
667
+ // Add bypass method
668
+ cachedFn.bypass = async (...args: TArgs): Promise<TResult> => {
669
+ const key = keyFn(...args)
670
+
671
+ // Execute function without checking cache
672
+ const result = await fn(...args)
673
+
674
+ // Update cache with new result
675
+ await cache.set(key, result, ttl !== undefined ? { ttl } : undefined)
676
+
677
+ return result
678
+ }
679
+
680
+ return cachedFn as CachedFunction<TArgs, TResult>
681
+ }