@ultimat3/ai 11.2.0 → 11.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.
- package/CLAUDE.md +11 -0
- package/package.json +10 -10
- package/src/llm-cache.ts +19 -1
package/CLAUDE.md
CHANGED
|
@@ -80,6 +80,17 @@ until 2026-08, naming a tool no catalog contained (`llm.test.ts`, `agent.test.ts
|
|
|
80
80
|
- Semantic scopes are separate cache INSTANCES, never a filter over a shared one — cosine
|
|
81
81
|
similarity has no notion of a tenant. The instance key carries the prompt hash too, which is
|
|
82
82
|
what makes a version bump invalidate the cache.
|
|
83
|
+
- **The instance key carries `ctx.locale` too, `As of 2026-08-24`, and it sits with the prompt hash
|
|
84
|
+
rather than with the scope.** A prompt taking `locale` as a var — the reference app's `summarize`
|
|
85
|
+
does, and the model obeys it — differs by ONE token between languages while carrying a whole
|
|
86
|
+
document, so the two rendered prompts are neighbours: measured with this package's own
|
|
87
|
+
`HashEmbedder` over that template, **0.9986**, against the declaration's `threshold: 0.97`. The
|
|
88
|
+
Spanish summary was a hit for an English reader, and the model was never wrong — the cache was.
|
|
89
|
+
No threshold repairs it, because the same number has to keep an honest repeat above it. It is in
|
|
90
|
+
the UNCONDITIONAL half of the key because a `scope` answers "who may share this answer" and a
|
|
91
|
+
locale is part of what the answer IS: an app writing `scope: () => 'global'` is saying its
|
|
92
|
+
callers may read one another's summaries, never that a Spanish one will do. Same rule
|
|
93
|
+
`@ultimat3/render` applies to ISR.
|
|
83
94
|
- **The default scope is the calling ACTOR, and `scope` receives `{ input, ctx }`** (`As of
|
|
84
95
|
2026-08`). It defaulted to the literal string `'global'` and took the bare `input`, so the rule
|
|
85
96
|
in the bullet above was contradicted by the very default that shipped: `cache: { semantic: { ttl:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/ai",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.3.0",
|
|
4
4
|
"description": "LLM gateway, versioned prompts, evals as tests, embeddings, hybrid vector search, RAG",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"test": "bun test"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@ultimat3/action": "11.
|
|
36
|
-
"@ultimat3/cache": "11.
|
|
37
|
-
"@ultimat3/core": "11.
|
|
38
|
-
"@ultimat3/db": "11.
|
|
39
|
-
"@ultimat3/jobs": "11.
|
|
40
|
-
"@ultimat3/money": "11.
|
|
41
|
-
"@ultimat3/policy": "11.
|
|
42
|
-
"@ultimat3/schema": "11.
|
|
43
|
-
"@ultimat3/time": "11.
|
|
35
|
+
"@ultimat3/action": "11.3.0",
|
|
36
|
+
"@ultimat3/cache": "11.3.0",
|
|
37
|
+
"@ultimat3/core": "11.3.0",
|
|
38
|
+
"@ultimat3/db": "11.3.0",
|
|
39
|
+
"@ultimat3/jobs": "11.3.0",
|
|
40
|
+
"@ultimat3/money": "11.3.0",
|
|
41
|
+
"@ultimat3/policy": "11.3.0",
|
|
42
|
+
"@ultimat3/schema": "11.3.0",
|
|
43
|
+
"@ultimat3/time": "11.3.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/llm-cache.ts
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* carries the prompt VERSION as well — which is what makes "editing a prompt requires a version
|
|
7
7
|
* bump" invalidate the cache: a bumped version reaches a different store, so an old answer cannot
|
|
8
8
|
* survive a prompt edit no matter how similar the text.
|
|
9
|
+
*
|
|
10
|
+
* It carries the request LOCALE for the same structural reason, `As of 2026-08-24`. A prompt that
|
|
11
|
+
* takes `locale` as a var — the reference app's `summarize` does, and the model obeys it — differs
|
|
12
|
+
* by one token between languages while carrying a whole document, so the two renderings are
|
|
13
|
+
* neighbours: measured with this package's own `HashEmbedder` over that template, **0.9986**
|
|
14
|
+
* against a declared threshold of `0.97`. The Spanish answer was therefore a hit for an English
|
|
15
|
+
* reader. No threshold fixes it, because the same number has to keep an honest repeat above it.
|
|
9
16
|
*/
|
|
10
17
|
|
|
11
18
|
import type { Ctx } from '@ultimat3/core';
|
|
@@ -62,6 +69,10 @@ export interface PromptCache {
|
|
|
62
69
|
* same reason it is JSON rather than a joined string — an actor id is app data and may carry any
|
|
63
70
|
* separator, and a value that can spell a boundary can spell somebody else's.
|
|
64
71
|
*
|
|
72
|
+
* The locale is deliberately NOT here. This function is the DEFAULT a declaration replaces by
|
|
73
|
+
* writing its own `scope`, and the locale has to survive that — so it lives in the unconditional
|
|
74
|
+
* half of the store key, beside the prompt hash.
|
|
75
|
+
*
|
|
65
76
|
* `ctx.actor` is never absent (`createContext` defaults it to `anonymousActor()`), so every
|
|
66
77
|
* anonymous caller shares one partition — which is what the anonymous actor already means
|
|
67
78
|
* everywhere else in the framework.
|
|
@@ -92,7 +103,14 @@ export async function openCache<TParsed>(
|
|
|
92
103
|
const semantic = args.cache?.semantic;
|
|
93
104
|
if (semantic === undefined) return undefined;
|
|
94
105
|
const scope = semantic.scope?.({ input: args.input, ctx: args.ctx }) ?? actorScope(args.ctx);
|
|
95
|
-
|
|
106
|
+
// The locale sits with the prompt HASH and not with the scope, on purpose: a scope answers "who
|
|
107
|
+
// may share this answer" and the locale is part of what the answer IS — so a written-down
|
|
108
|
+
// `scope: () => 'global'` is still partitioned by it. An app declaring a shared store is saying
|
|
109
|
+
// its callers may read one another's summaries, never that a Spanish one will do for an English
|
|
110
|
+
// reader. `@ultimat3/render`'s ISR keys by locale for exactly this reason.
|
|
111
|
+
const store = semanticCacheFor(
|
|
112
|
+
`${args.prompt.ref}#${args.prompt.hash}@${args.ctx.locale}::${scope}`,
|
|
113
|
+
);
|
|
96
114
|
const embedding = Array.from(await embedOne(aiEmbedder(), args.rendered));
|
|
97
115
|
const ttlMs = semantic.ttl === undefined ? undefined : parseDuration(semantic.ttl);
|
|
98
116
|
return {
|