@theokit/sdk-cache 0.3.1 → 1.0.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/dist/index.d.cts CHANGED
@@ -6,44 +6,170 @@ import { Plugin } from '@theokit/sdk';
6
6
  *
7
7
  * @public
8
8
  */
9
+ /**
10
+ * How long entries live, and which prompts never become entries at all.
11
+ *
12
+ * @public
13
+ */
9
14
  interface CacheTTLConfig {
10
- /** Default TTL applied to all entries. Format: `"1h" | "30m" | 86400 (seconds)`. */
15
+ /**
16
+ * Lifetime applied to every entry written, evaluated at write time.
17
+ *
18
+ * A NUMBER is SECONDS (`3600` is an hour). A STRING needs a unit suffix — `s`, `m`, `h`, `d`, `w`
19
+ * (`"30m"`, `"1h"`, `"7d"`). A bare numeric string is not a duration: `"3600"` throws
20
+ * {@link CacheInvalidTtlError}, and so do a negative number and an unknown unit. `0` / `"0s"`
21
+ * parses fine and writes entries that are already expired, which disables the cache without
22
+ * disabling the embedding calls.
23
+ *
24
+ * The throw happens on the WRITE, not at `Cache.semantic(...)` — a bad value survives
25
+ * construction and surfaces on the first `remember()` or the first cached assistant reply.
26
+ */
11
27
  readonly default: string | number;
12
- /** Regex marking queries that must NEVER be cached (e.g. /weather|today|now/i). */
28
+ /**
29
+ * Prompts matching this regex are never cached — e.g. `/weather|today|now/i` for anything whose
30
+ * answer goes stale.
31
+ *
32
+ * Applies to BOTH directions: a matching prompt is not looked up (it counts as
33
+ * {@link CacheStats.excluded}, not a miss) and not stored. Test it against your real prompts —
34
+ * a regex broad enough to match every question disables the cache while every counter still
35
+ * looks healthy.
36
+ *
37
+ * Bring your own regex object; a `/g` flag is a hazard here because `RegExp.test` is stateful
38
+ * with it and would match every other call.
39
+ */
13
40
  readonly exclude?: RegExp;
14
41
  }
42
+ /**
43
+ * Where cached entries live between process restarts.
44
+ *
45
+ * `"memory"` (the default when {@link CacheSemanticOptions.persistence} is omitted) keeps everything
46
+ * in the process and loses it on exit — right for a request-scoped worker, wrong for a CLI that runs
47
+ * once per invocation and would never see a hit.
48
+ *
49
+ * `"json"` writes the whole entry set, VECTORS INCLUDED, to a file under `dir`. That file grows with
50
+ * `maxEntries` × the embedder's dimension, so a 1000-entry cache over a 1536-dimension embedder is
51
+ * on the order of megabytes, and it is plaintext: every prompt and response is readable. Do not point
52
+ * `dir` at a directory that gets committed.
53
+ */
15
54
  interface CachePersistenceOptions {
55
+ /** `"memory"` for process-local, `"json"` for a file under {@link CachePersistenceOptions.dir}. */
16
56
  readonly backend: "memory" | "json";
17
- /** Required when backend = "json". */
57
+ /**
58
+ * Directory holding `<namespace>.json`. REQUIRED when `backend` is `"json"` — omitting it makes
59
+ * `Cache.semantic(...)` throw `ZodError`, it is not silently downgraded to memory.
60
+ *
61
+ * Created recursively on the first write. Writes are atomic but DEBOUNCED by 200 ms, and loading
62
+ * is fire-and-forget, so the file is eventually-consistent with memory in both directions. A
63
+ * corrupt or wrong-schema file is logged and treated as an empty cache — it never blocks startup.
64
+ */
18
65
  readonly dir?: string;
19
66
  }
20
67
  /**
21
68
  * Embedder runtime shape — minimal subset of `EmbeddingRuntime` (D11) the
22
69
  * Cache actually uses. Lets tests inject fake embedders without pulling
23
70
  * the full memory subsystem.
71
+ *
72
+ * `@theokit/sdk-cache` ships one implementation, `createLexicalEmbedder()`; anything with these
73
+ * four members works, including a wrapper around a provider's embedding endpoint.
74
+ *
75
+ * @public
24
76
  */
25
77
  interface CacheEmbedderRuntime {
78
+ /**
79
+ * Stable identity of this embedding SPACE, not of the object.
80
+ *
81
+ * It is part of the exact-match key and of the semantic eligibility filter, so changing it
82
+ * invalidates every existing entry — which is the point: vectors from two different embedders
83
+ * are not comparable, and a shared id would let one embedder's vectors be matched against
84
+ * another's. Version it whenever the model, its parameters or the dimension change.
85
+ */
26
86
  readonly id: string;
87
+ /** Human-facing model name. Recorded for diagnostics; the cache never keys on it. */
27
88
  readonly model: string;
89
+ /**
90
+ * Length of the vectors `embed` returns.
91
+ *
92
+ * Entries whose stored vector has a different length are skipped during the semantic scan rather
93
+ * than compared, so a dimension change silently costs you the whole warm cache instead of
94
+ * throwing.
95
+ */
28
96
  readonly dimension: number;
97
+ /**
98
+ * Embed a batch; the cache always passes exactly one text and reads `result[0]`.
99
+ *
100
+ * A rejection is NOT propagated to the caller: the cache logs it, counts it in
101
+ * {@link CacheStats.embedderFailures} and treats the operation as a miss / skipped write. Return
102
+ * a zero vector only if you want it treated as a non-match, since cosine distance against it is
103
+ * defined as 1.0.
104
+ */
29
105
  embed(texts: ReadonlyArray<string>): Promise<number[][]>;
30
106
  }
107
+ /**
108
+ * Configuration for `Cache.semantic(...)`.
109
+ *
110
+ * Only `embedder` is required, and deliberately so: autoselecting one would make an agent start
111
+ * calling an embedding API because a cache was enabled, which is a surprise bill rather than a
112
+ * default. Everything else has a working default.
113
+ *
114
+ * The lookup runs in two stages — an exact key match first, then a vector search — so an identical
115
+ * prompt never pays for an embedding call. Only the second stage consults `threshold`.
116
+ */
31
117
  interface CacheSemanticOptions {
32
118
  /** Embedder instance. REQUIRED in v1 — no autoselect (avoids surprise API calls). */
33
119
  readonly embedder: CacheEmbedderRuntime;
34
- /** Cosine distance threshold (0..2). Default 0.85; lower = stricter. */
120
+ /**
121
+ * Maximum cosine DISTANCE (`1 - cosine similarity`) at which a stored entry counts as a match.
122
+ * Default 0.85. Lower is stricter; 0 requires an identical direction, 1 accepts orthogonal
123
+ * vectors, and the accepted range is 0..2.
124
+ *
125
+ * It is a distance and not a similarity, so raising it LOOSENS matching — 0.85 is already
126
+ * permissive for normalized embeddings and will return semantically unrelated answers if your
127
+ * embedder spreads vectors narrowly. Only the vector stage consults it; an exact-key hit ignores
128
+ * it entirely. Watch {@link CacheStats.semanticHits} when you tune it.
129
+ */
35
130
  readonly threshold?: number;
36
131
  /** TTL config. Default `{ default: "1h" }`. */
37
132
  readonly ttl?: CacheTTLConfig;
38
- /** Multi-tenant namespace. Default `"global"`. */
133
+ /**
134
+ * Isolation bucket, 1..64 chars. Default `"global"`. Entries never match across namespaces.
135
+ *
136
+ * It is also the plugin name (`cache-semantic-<namespace>`) and, under `"json"` persistence, the
137
+ * FILE name. Two caches sharing a namespace and a `dir` therefore share ONE store rather than
138
+ * racing over one file (#359) — with the consequence that the second one's `maxEntries` is
139
+ * ignored, since the store already exists.
140
+ */
39
141
  readonly namespace?: string;
40
- /** Default modelId attached to entries when caller doesn't override. */
142
+ /**
143
+ * Model id stamped on every entry, and part of both the exact key and the semantic eligibility
144
+ * filter — a response cached for one model is never returned for another.
145
+ *
146
+ * Defaults to the literal string `"unknown"`, which is an ORDINARY value rather than a wildcard:
147
+ * entries written by a cache that defaulted it are visible only to another cache that also
148
+ * defaults it. Set it to the same id you pass to `Agent.create({ model })`.
149
+ */
41
150
  readonly modelId?: string;
42
- /** Max entries (LRU eviction). Default 1000. */
151
+ /**
152
+ * Ceiling on stored entries. Default 1000. Exceeding it evicts the least recently used entry and
153
+ * increments {@link CacheStats.evicted}.
154
+ *
155
+ * Sizes the JSON snapshot too: the file holds every entry's full embedding, so this multiplied by
156
+ * the embedder dimension is the file's order of magnitude.
157
+ */
43
158
  readonly maxEntries?: number;
44
159
  /** Persistence backend. Default in-memory. */
45
160
  readonly persistence?: CachePersistenceOptions;
46
161
  }
162
+ /**
163
+ * One cached prompt/response pair, as `Cache` stores it.
164
+ *
165
+ * Read-only from the outside: entries are produced by the cache, and reach a caller only through a
166
+ * `"json"` persistence dump. `vector` is the embedding of `prompt`, which is what makes the file
167
+ * large; `accessedAt` / `accessCount` are what LRU eviction reads when `maxEntries` is reached.
168
+ *
169
+ * `key` is the exact-match key (a hash of namespace, embedder, model and prompt), so two entries with
170
+ * the same prompt under different models are different entries — a cached answer never crosses a
171
+ * model boundary.
172
+ */
47
173
  interface CacheEntry {
48
174
  readonly key: string;
49
175
  readonly namespace: string;
@@ -57,6 +183,24 @@ interface CacheEntry {
57
183
  readonly accessedAt: number;
58
184
  readonly accessCount: number;
59
185
  }
186
+ /**
187
+ * Counters returned by `Cache.stats()`. All monotonic within a process; a `"json"` backend does not
188
+ * restore them, so they count THIS process's traffic, not the file's history.
189
+ *
190
+ * The three miss-shaped counters are distinct on purpose, and the distinction is the point of
191
+ * reading stats at all:
192
+ *
193
+ * - `misses` — looked up, nothing matched. The cache is working and cold. An empty or
194
+ * whitespace-only prompt also lands here, having never been looked up at all.
195
+ * - `excluded` — {@link CacheTTLConfig.exclude} matched the prompt, so no lookup happened. High and
196
+ * unexpected means the regex is too broad.
197
+ * - `embedderFailures` — the embedder threw. The lookup DEGRADES to a miss rather than failing the
198
+ * call, so a broken embedder shows up here as a rising number and nowhere else. A cache that
199
+ * suddenly stops hitting, with this climbing, is an embedder outage — not a cold cache.
200
+ *
201
+ * `kvHits` counts exact-key matches (no embedding call); `semanticHits` counts vector matches.
202
+ * A `semanticHits` of zero with healthy `kvHits` means `threshold` is too strict.
203
+ */
60
204
  interface CacheStats {
61
205
  readonly entries: number;
62
206
  readonly kvHits: number;
@@ -66,11 +210,18 @@ interface CacheStats {
66
210
  readonly evicted: number;
67
211
  readonly embedderFailures: number;
68
212
  }
69
- declare class CacheEmbedderError extends Error {
70
- readonly name = "CacheEmbedderError";
71
- readonly cause?: Error;
72
- constructor(message: string, cause?: Error);
73
- }
213
+ /**
214
+ * A TTL value that could not be parsed, thrown at configuration time rather than on first use.
215
+ *
216
+ * Accepts a number of SECONDS, or a string with a unit suffix `s` / `m` / `h` / `d` / `w`
217
+ * (`"30m"`, `"1h"`, `"7d"`). A bare numeric string is not a duration — `"3600"` is rejected,
218
+ * `3600` is an hour. Also rejected: a negative or non-finite number, and any unit outside that
219
+ * set. `input` carries what was passed, so the message names the offending value rather than the
220
+ * field.
221
+ *
222
+ * "Configuration time" means the first WRITE that applies the TTL, not `Cache.semantic(...)` —
223
+ * `CacheSemanticOptions` is validated for shape, never for TTL parseability.
224
+ */
74
225
  declare class CacheInvalidTtlError extends Error {
75
226
  readonly input: string | number;
76
227
  readonly name = "CacheInvalidTtlError";
@@ -83,28 +234,52 @@ declare class CacheInvalidTtlError extends Error {
83
234
  *
84
235
  * Usage:
85
236
  *
86
- * import { Agent, Cache, definePlugin } from "@theokit/sdk";
237
+ * import { Agent } from "@theokit/sdk";
238
+ * import { Cache, createLexicalEmbedder } from "@theokit/sdk-cache";
87
239
  *
88
240
  * const cache = Cache.semantic({
89
- * embedder: myEmbedderRuntime, // EmbeddingRuntime (D11)
241
+ * embedder: createLexicalEmbedder(), // or any CacheEmbedderRuntime
90
242
  * threshold: 0.85,
91
243
  * ttl: { default: "1h", exclude: /weather|today|now/i },
92
244
  * namespace: "my-app",
93
245
  * modelId: "openai/gpt-4o-mini",
94
246
  * });
95
247
  *
248
+ * // (a) Plugin mode — the cached answer is INJECTED as context; the LLM is still called.
96
249
  * const agent = await Agent.create({
97
250
  * model: { id: "openai/gpt-4o-mini" },
98
251
  * plugins: [cache.asPlugin()],
99
- * // ...
100
252
  * });
101
253
  *
102
- * await agent.send("What is the capital of France?"); // miss LLM
103
- * await agent.send("Tell me the capital of France"); // semantic hit
254
+ * // (b) Explicit mode — this is the one that skips the LLM call.
255
+ * const hit = await cache.consult(prompt);
256
+ * const answer = hit.hit ? hit.response : await callTheModel(prompt);
257
+ * if (!hit.hit) await cache.remember(prompt, answer);
258
+ *
259
+ * The distinction between (a) and (b) is the single most important thing to know about this
260
+ * package — see `Cache.asPlugin`.
104
261
  *
105
262
  * @public
106
263
  */
107
264
 
265
+ /**
266
+ * A semantic response cache: an exact-key lookup, then a vector-similarity lookup, over
267
+ * prompt/response pairs the caller has stored.
268
+ *
269
+ * Build one with {@link Cache.semantic}; `new Cache()` is a compile error. One instance owns one
270
+ * store, so two `Cache.semantic(...)` calls never share entries even under the same `namespace`
271
+ * and the same `dir` — the JSON backend will have both instances writing the same file.
272
+ *
273
+ * Two ways to use it, and they do NOT save the same thing:
274
+ *
275
+ * | | {@link Cache.consult} + {@link Cache.remember} | {@link Cache.asPlugin} |
276
+ * |---|---|---|
277
+ * | LLM call on a hit | skipped | still made |
278
+ * | What you save | the whole call | nothing, today |
279
+ * | Who drives it | you | the agent loop |
280
+ *
281
+ * @public
282
+ */
108
283
  declare class Cache {
109
284
  private readonly embedder;
110
285
  private readonly threshold;
@@ -112,20 +287,92 @@ declare class Cache {
112
287
  private readonly namespace;
113
288
  private readonly modelId;
114
289
  private readonly store;
290
+ private readonly hydrated;
115
291
  private _plugin?;
116
292
  private constructor();
293
+ /**
294
+ * Resolves once the `"json"` backend has finished reading its snapshot; resolves immediately on
295
+ * the in-memory backend.
296
+ *
297
+ * You rarely need it: `consult` and `remember` await hydration themselves, so a cache is correct
298
+ * without it. It exists for a caller who wants the read charged to startup rather than to the
299
+ * first lookup — and because the code promised it long before it existed (#359).
300
+ *
301
+ * A corrupt or unreadable snapshot resolves normally with an empty cache and a warning on
302
+ * stderr; a cache must not take the process down.
303
+ */
304
+ ready(): Promise<void>;
305
+ /**
306
+ * Write the pending snapshot to disk now, keeping every entry. No-op on the in-memory backend.
307
+ *
308
+ * Writes are debounced 200ms, so a process that remembers something and exits inside that window
309
+ * persists nothing — precisely the once-per-invocation CLI the `"json"` backend exists for. Call
310
+ * this before exiting. Nothing flushes on teardown: an `exit` handler cannot await, and a library
311
+ * installing a process-level hook is a side effect the caller did not ask for.
312
+ *
313
+ * Until #359 the only public call that wrote the snapshot was `clear()`, which also destroyed
314
+ * everything you wanted to persist.
315
+ */
316
+ flush(): Promise<void>;
317
+ /**
318
+ * Build a cache. Validates `options` with Zod and THROWS `ZodError` on a bad shape — an
319
+ * `embedder` missing `{ id, dimension, embed }`, a `threshold` outside `0..2`, a `namespace`
320
+ * longer than 64 chars, or `persistence: { backend: "json" }` without a `dir`.
321
+ *
322
+ * Defaults: `threshold` 0.85, `ttl` `{ default: "1h" }`, `namespace` `"global"`, `maxEntries`
323
+ * 1000 (LRU), `persistence` in-memory. `modelId` defaults to the literal string `"unknown"`,
324
+ * which is a real namespace value and not a wildcard: entries stored while `modelId` was
325
+ * defaulted are only ever returned to lookups that also default it.
326
+ *
327
+ * With `persistence: { backend: "json", dir }` the snapshot is read in the background and this
328
+ * call does not await it — but `consult` and `remember` do, so a lookup issued immediately after
329
+ * construction still sees what is on disk. Await {@link Cache.ready} to charge the read to
330
+ * startup instead of to the first lookup. Two caches built with the same `dir` and `namespace`
331
+ * share one store, so they cannot overwrite each other's snapshot; the FIRST one's `maxEntries`
332
+ * is the one that applies.
333
+ */
117
334
  static semantic(options: CacheSemanticOptions): Cache;
118
335
  /**
119
- * EC-4 absorbed: memoized so repeated `asPlugin()` calls return the SAME
120
- * plugin descriptor no duplicate hook registration.
336
+ * A `Plugin` for `Agent.create({ plugins: [...] })` that reads the cache before each user turn
337
+ * and writes it after each assistant reply.
338
+ *
339
+ * READ THIS BEFORE BUDGETING FOR IT. A hit does NOT skip the model call. The hook returns the
340
+ * cached response as `PreUserSendResult.recalledContext`, which the agent loop injects as a
341
+ * `<memory-context>` block ahead of the prompt — the request still goes to the provider, still
342
+ * costs tokens, and still returns whatever the model makes of that context, which need not be
343
+ * the cached text. Use {@link Cache.consult} / {@link Cache.remember} when the point is to avoid
344
+ * the call.
345
+ *
346
+ * A turn that invoked tools is NOT cached: the store hook reads
347
+ * `PostAssistantReplyContext.usedTools`, which the runtime derives from the run's tool calls
348
+ * (#358). Replaying such an answer would hand a later caller the result of a write that never
349
+ * happened. Until that signal existed the hook passed a literal `false` and cached those turns,
350
+ * despite the package's stated intent.
351
+ *
352
+ * Memoized: repeated calls return the SAME plugin, so registering it twice does not double the
353
+ * hooks.
121
354
  */
122
355
  asPlugin(): Plugin;
123
356
  /**
124
- * Explicit cache lookup callers that want true LLM short-circuit
125
- * call this BEFORE `agent.send()`, then dispatch to the LLM only on miss.
357
+ * Look a prompt up. Call it BEFORE dispatching to the model and skip the call on a hit — this is
358
+ * the only path in this package that actually avoids an LLM request.
359
+ *
360
+ * ```ts
361
+ * const hit = await cache.consult(prompt);
362
+ * if (hit.hit) return hit.response;
363
+ * ```
364
+ *
365
+ * `source` says which stage matched: `"kv"` is an exact-key match and costs NO embedding call;
366
+ * `"semantic"` means the prompt was embedded and a stored vector came within `threshold`, and
367
+ * only then is `distance` present (cosine distance, so smaller is closer).
126
368
  *
127
- * v1 plugin mode provides recall + context-inject (LLM still called).
128
- * v1.x will add transparent short-circuit via an agent-loop refactor.
369
+ * NEVER THROWS on an embedder failure. It degrades to `{ hit: false }`, logs a warning on
370
+ * stderr, and increments {@link CacheStats.embedderFailures} a cache must not take the request
371
+ * down with it. A cache that has silently stopped hitting is that counter climbing, not a cold
372
+ * cache.
373
+ *
374
+ * An empty or whitespace-only prompt returns `{ hit: false }` and counts as a MISS. A prompt
375
+ * matching {@link CacheTTLConfig.exclude} returns `{ hit: false }` and counts as `excluded`.
129
376
  */
130
377
  consult(prompt: string): Promise<{
131
378
  hit: false;
@@ -136,17 +383,51 @@ declare class Cache {
136
383
  distance?: number;
137
384
  }>;
138
385
  /**
139
- * Explicit cache store pair with `consult()` to manually feed the
140
- * cache after dispatching the LLM call yourself.
386
+ * Store a prompt/response pair. Pair it with {@link Cache.consult} after you dispatched the model
387
+ * call yourself.
388
+ *
389
+ * Pass `{ usedTools: true }` when the answer came from a run that invoked tools and replaying it
390
+ * would lose the side effects — the write is then skipped entirely.
391
+ *
392
+ * Silently writes nothing when the prompt is empty/whitespace, the response is empty, the prompt
393
+ * matches {@link CacheTTLConfig.exclude}, or the embedder fails (that last case increments
394
+ * {@link CacheStats.embedderFailures}). It resolves in every one of those cases: a resolved
395
+ * promise is not evidence that an entry exists — read {@link CacheStats.entries} if you need
396
+ * that.
397
+ *
398
+ * Writing beyond `maxEntries` evicts the least-recently-used entry. On the `"json"` backend the
399
+ * disk write is DEBOUNCED by 200 ms, so a process that exits right after this resolves loses the
400
+ * entry unless it calls {@link Cache.flush} — which writes the snapshot and keeps every entry.
401
+ * Nothing flushes on teardown.
141
402
  */
142
403
  remember(prompt: string, response: string, opts?: {
143
404
  usedTools?: boolean;
144
405
  }): Promise<void>;
145
- /** Stats snapshot — primary observable for dogfood verification. */
406
+ /**
407
+ * Counter snapshot for this instance. See {@link CacheStats} for what each counter separates —
408
+ * in particular `misses` vs `excluded` vs `embedderFailures`, which is how you tell a cold cache
409
+ * from a too-broad exclude regex from a broken embedder.
410
+ *
411
+ * Process-local: the `"json"` backend persists entries, never counters, so a restart reports
412
+ * zeros against a warm file.
413
+ */
146
414
  stats(): CacheStats;
147
- /** Clear all entries (and flush to disk if JSON backend). */
415
+ /**
416
+ * Drop every entry. On the `"json"` backend this also forces the debounced snapshot to disk
417
+ * immediately, so it is the one public call that guarantees the file matches memory.
418
+ *
419
+ * Counters are NOT reset — `stats()` keeps reporting the hits and misses accumulated before the
420
+ * clear, so `entries: 0` alongside a non-zero `kvHits` is expected, not a bug.
421
+ */
148
422
  clear(): Promise<void>;
149
- /** Force-evict expired entries. Returns count removed. */
423
+ /**
424
+ * Remove every entry whose TTL has elapsed, returning how many were dropped, and add that to
425
+ * {@link CacheStats.evicted}.
426
+ *
427
+ * Optional housekeeping: expired entries are already skipped on lookup and dropped when touched,
428
+ * so this only reclaims memory for entries nobody asks for. `now` exists to make the sweep
429
+ * testable; leave it out in production.
430
+ */
150
431
  evictExpired(now?: number): number;
151
432
  }
152
433
 
@@ -167,6 +448,26 @@ declare class Cache {
167
448
  * @public
168
449
  */
169
450
 
451
+ /**
452
+ * Build the built-in lexical embedder — a zero-dependency, zero-cost `CacheEmbedderRuntime`.
453
+ *
454
+ * ```ts
455
+ * const cache = Cache.semantic({ embedder: createLexicalEmbedder() });
456
+ * ```
457
+ *
458
+ * Matches on SHARED WORDS, not on meaning. "What is the capital of France?" and "Tell me the
459
+ * capital of France" land close together; "capital of France" and "French capital" do not. If you
460
+ * need paraphrase-level hits, wrap a real embedding API instead — that is what
461
+ * `CacheEmbedderRuntime` is for.
462
+ *
463
+ * `dimension` (default 256) is the number of hash buckets. Raising it reduces collisions between
464
+ * unrelated words, which is the failure mode here: a collision makes two unrelated prompts look
465
+ * similar and can serve a wrong cached answer. It is also baked into the returned `id`
466
+ * (`theokit-lexical-v1-d<dimension>`), so CHANGING IT INVALIDATES every entry already stored — the
467
+ * cache keys on the embedder id and skips vectors of a different length.
468
+ *
469
+ * Never rejects, and never calls the network.
470
+ */
170
471
  declare function createLexicalEmbedder(dimension?: number): CacheEmbedderRuntime;
171
472
 
172
- export { Cache, CacheEmbedderError, type CacheEmbedderRuntime, type CacheEntry, CacheInvalidTtlError, type CachePersistenceOptions, type CacheSemanticOptions, type CacheStats, type CacheTTLConfig, createLexicalEmbedder };
473
+ export { Cache, type CacheEmbedderRuntime, type CacheEntry, CacheInvalidTtlError, type CachePersistenceOptions, type CacheSemanticOptions, type CacheStats, type CacheTTLConfig, createLexicalEmbedder };