@warlock.js/ai 4.15.0 → 5.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/CHANGELOG.md +183 -158
- package/cjs/index.cjs +637 -104
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -2
- package/esm/contracts/memory/index.d.mts +1 -1
- package/esm/contracts/memory/memory-config.type.d.mts +29 -3
- package/esm/contracts/memory/memory-config.type.d.mts.map +1 -1
- package/esm/contracts/memory/memory-item.type.d.mts +15 -1
- package/esm/contracts/memory/memory-item.type.d.mts.map +1 -1
- package/esm/contracts/memory/memory.contract.d.mts +15 -2
- package/esm/contracts/memory/memory.contract.d.mts.map +1 -1
- package/esm/contracts/memory/recall-options.type.d.mts +12 -0
- package/esm/contracts/memory/recall-options.type.d.mts.map +1 -1
- package/esm/contracts/orchestrator/index.d.mts +1 -1
- package/esm/contracts/orchestrator/orchestrator-config.type.d.mts +38 -1
- package/esm/contracts/orchestrator/orchestrator-config.type.d.mts.map +1 -1
- package/esm/contracts/orchestrator/orchestrator.contract.d.mts +67 -3
- package/esm/contracts/orchestrator/orchestrator.contract.d.mts.map +1 -1
- package/esm/contracts/supervisor/supervisor-config.type.d.mts +23 -0
- package/esm/contracts/supervisor/supervisor-config.type.d.mts.map +1 -1
- package/esm/index.d.mts +6 -5
- package/esm/index.mjs +3 -2
- package/esm/memory/episodic-memory.mjs +14 -6
- package/esm/memory/episodic-memory.mjs.map +1 -1
- package/esm/memory/index.d.mts +1 -1
- package/esm/memory/memory.d.mts +13 -1
- package/esm/memory/memory.d.mts.map +1 -1
- package/esm/memory/memory.mjs +41 -7
- package/esm/memory/memory.mjs.map +1 -1
- package/esm/memory/procedural-memory.mjs +20 -7
- package/esm/memory/procedural-memory.mjs.map +1 -1
- package/esm/memory/semantic-memory.mjs +27 -10
- package/esm/memory/semantic-memory.mjs.map +1 -1
- package/esm/memory/working-memory.mjs +70 -13
- package/esm/memory/working-memory.mjs.map +1 -1
- package/esm/middleware/builtins/semantic-cache.d.mts +46 -1
- package/esm/middleware/builtins/semantic-cache.d.mts.map +1 -1
- package/esm/middleware/builtins/semantic-cache.mjs +60 -15
- package/esm/middleware/builtins/semantic-cache.mjs.map +1 -1
- package/esm/middleware/index.d.mts +1 -1
- package/esm/orchestrator/as-tool.d.mts +35 -9
- package/esm/orchestrator/as-tool.d.mts.map +1 -1
- package/esm/orchestrator/as-tool.mjs +67 -19
- package/esm/orchestrator/as-tool.mjs.map +1 -1
- package/esm/orchestrator/execution.d.mts.map +1 -1
- package/esm/orchestrator/execution.mjs +2 -2
- package/esm/orchestrator/execution.mjs.map +1 -1
- package/esm/orchestrator/index.d.mts +1 -1
- package/esm/orchestrator/index.mjs +1 -1
- package/esm/orchestrator/memory.d.mts +41 -5
- package/esm/orchestrator/memory.d.mts.map +1 -1
- package/esm/orchestrator/memory.mjs +53 -5
- package/esm/orchestrator/memory.mjs.map +1 -1
- package/esm/planner/plan-schema.d.mts +3 -3
- package/esm/planner/plan-schema.d.mts.map +1 -1
- package/esm/planner/plan-schema.mjs +30 -0
- package/esm/planner/plan-schema.mjs.map +1 -1
- package/esm/security/index.mjs +1 -0
- package/esm/security/outbound-policy.d.mts +9 -0
- package/esm/security/outbound-policy.d.mts.map +1 -1
- package/esm/security/outbound-policy.mjs +79 -5
- package/esm/security/outbound-policy.mjs.map +1 -1
- package/esm/security/outbound-policy.type.d.mts +8 -0
- package/esm/security/outbound-policy.type.d.mts.map +1 -1
- package/esm/security/safe-merge.d.mts +52 -0
- package/esm/security/safe-merge.d.mts.map +1 -0
- package/esm/security/safe-merge.mjs +68 -0
- package/esm/security/safe-merge.mjs.map +1 -0
- package/esm/supervisor/decide.mjs +52 -5
- package/esm/supervisor/decide.mjs.map +1 -1
- package/esm/supervisor/execution.d.mts +22 -0
- package/esm/supervisor/execution.d.mts.map +1 -1
- package/esm/supervisor/execution.mjs +46 -9
- package/esm/supervisor/execution.mjs.map +1 -1
- package/esm/supervisor/supervisor.mjs +4 -0
- package/esm/supervisor/supervisor.mjs.map +1 -1
- package/llms-full.txt +174 -10
- package/llms.txt +4 -3
- package/package.json +4 -4
- package/skills/README.md +5 -1
- package/skills/attach-ai-middleware/SKILL.md +17 -1
- package/skills/rag-loaders-and-stores/SKILL.md +3 -0
- package/skills/run-ai-agent/SKILL.md +3 -0
- package/skills/run-orchestrator/SKILL.md +6 -1
- package/skills/run-planner/SKILL.md +7 -3
- package/skills/run-supervisor/SKILL.md +11 -1
- package/skills/secure-outbound-requests/SKILL.md +85 -0
- package/skills/use-ai-memory/SKILL.md +36 -3
- package/skills/use-runtime-skills/SKILL.md +2 -1
|
@@ -49,9 +49,10 @@ var EpisodicMemory = class {
|
|
|
49
49
|
id,
|
|
50
50
|
text: item.text,
|
|
51
51
|
ts: this.now(),
|
|
52
|
+
scope: item.scope,
|
|
52
53
|
metadata: item.metadata
|
|
53
54
|
};
|
|
54
|
-
await this.store.set(this.keyFor(id), value, { vector });
|
|
55
|
+
await this.store.set(this.keyFor(id, item.scope), value, { vector });
|
|
55
56
|
}
|
|
56
57
|
/**
|
|
57
58
|
* Embed `query`, pull the nearest episodes clearing the similarity
|
|
@@ -59,8 +60,13 @@ var EpisodicMemory = class {
|
|
|
59
60
|
* returning the top `k`. The similarity floor still gates relevance —
|
|
60
61
|
* recency only reorders episodes that already cleared it, it never
|
|
61
62
|
* surfaces an irrelevant-but-recent one.
|
|
63
|
+
*
|
|
64
|
+
* Episodes written under a different `scope` (another tenant /
|
|
65
|
+
* session) are dropped here, before scoring and slicing, so they can
|
|
66
|
+
* neither leak nor consume a slot. An unscoped recall reads only
|
|
67
|
+
* unscoped episodes.
|
|
62
68
|
*/
|
|
63
|
-
async recall(query, k, threshold) {
|
|
69
|
+
async recall(query, k, threshold, scope) {
|
|
64
70
|
const { vector } = await this.embedder.embed(query);
|
|
65
71
|
const hits = await this.store.similar(vector, {
|
|
66
72
|
topK: Math.max(k * RECALL_OVERSCAN, k),
|
|
@@ -68,7 +74,7 @@ var EpisodicMemory = class {
|
|
|
68
74
|
});
|
|
69
75
|
const prefix = `${this.namespace}.`;
|
|
70
76
|
const now = this.now();
|
|
71
|
-
return hits.filter((hit) => hit.key.startsWith(prefix)).map((hit) => ({
|
|
77
|
+
return hits.filter((hit) => hit.key.startsWith(prefix) && hit.value?.scope === scope).map((hit) => ({
|
|
72
78
|
id: hit.value.id,
|
|
73
79
|
text: hit.value.text,
|
|
74
80
|
tier: "episodic",
|
|
@@ -94,10 +100,12 @@ var EpisodicMemory = class {
|
|
|
94
100
|
/**
|
|
95
101
|
* Namespaced key for an entry. Mirrors the semantic tier's dot
|
|
96
102
|
* separator so the prefix used here matches the `hit.key` the driver
|
|
97
|
-
* returns from `similar()
|
|
103
|
+
* returns from `similar()`, and its hashed scope segment so two
|
|
104
|
+
* scopes never overwrite one another's identical text. Unscoped keys
|
|
105
|
+
* keep their pre-4.15.0 shape.
|
|
98
106
|
*/
|
|
99
|
-
keyFor(id) {
|
|
100
|
-
return `${this.namespace}.${id}`;
|
|
107
|
+
keyFor(id, scope) {
|
|
108
|
+
return scope === void 0 ? `${this.namespace}.${id}` : `${this.namespace}.${deriveMemoryId(scope)}.${id}`;
|
|
101
109
|
}
|
|
102
110
|
};
|
|
103
111
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"episodic-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/episodic-memory.ts"],"sourcesContent":["import type { CacheDriver, CacheSimilarHit } from \"@warlock.js/cache\";\nimport type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * Shape persisted per episode. `ts` is the wall-clock time the episode\n * was remembered — the basis for the recency half of the blended recall\n * score. The vector lives in the driver's index (via `set({ vector })`),\n * so it is not duplicated here.\n */\ntype StoredEpisode = {\n id: string;\n text: string;\n ts: number;\n metadata?: Record<string, unknown>;\n};\n\n/**\n * How many extra candidates to pull from `similar()` before re-ranking by\n * the recency-blended score and slicing to `k`. Recency can promote a\n * slightly-less-similar-but-recent episode past a stale exact match, so\n * the raw top-`k` by similarity alone would miss it — overscan, then\n * re-rank.\n */\nconst RECALL_OVERSCAN = 5;\n\n/**\n * Episodic recall tier (memory core M2).\n *\n * Holds a durable, timestamped log of *what happened* — events/episodes —\n * and retrieves the ones most relevant to a query, **blended with\n * recency** so recent episodes outrank stale ones at equal similarity.\n * That recency weighting is the whole difference from the {@link\n * import(\"./semantic-memory\").SemanticMemory} tier (pure similarity over\n * timeless facts): episodic memory is time-anchored.\n *\n * Like the semantic tier it delegates the similarity search to the\n * `@warlock.js/cache` driver's `similar()` and never implements ANN\n * itself; it adds a stored `ts` per entry and a decay curve at recall.\n * The blended `score` stays in `[0, 1]` so a consumer can merge episodic\n * hits with the other tiers and sort on one field.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class EpisodicMemory {\n public constructor(\n private readonly embedder: EmbedderContract,\n private readonly store: CacheDriver<any, any>,\n private readonly namespace: string,\n private readonly recencyWeight: number,\n private readonly halfLifeMs: number,\n private readonly now: () => number,\n ) {}\n\n /**\n * Embed the episode text and index it under a namespaced, id-derived\n * key, stamping the current time. Re-remembering the same id overwrites\n * the prior entry (and refreshes its timestamp).\n */\n public async remember(item: MemoryItem): Promise<void> {\n const id = item.id ?? deriveMemoryId(item.text);\n const { vector } = await this.embedder.embed(item.text);\n\n const value: StoredEpisode = {\n id,\n text: item.text,\n ts: this.now(),\n metadata: item.metadata,\n };\n\n await this.store.set(this.keyFor(id), value, { vector });\n }\n\n /**\n * Embed `query`, pull the nearest episodes clearing the similarity\n * `threshold`, then re-rank each by a recency-blended score before\n * returning the top `k`. The similarity floor still gates relevance —\n * recency only reorders episodes that already cleared it, it never\n * surfaces an irrelevant-but-recent one.\n */\n public async recall(\n query: string,\n k: number,\n threshold: number,\n ): Promise<RecalledMemory[]> {\n const { vector } = await this.embedder.embed(query);\n\n const hits = await this.store.similar<StoredEpisode>(vector, {\n topK: Math.max(k * RECALL_OVERSCAN, k),\n threshold,\n });\n\n const prefix = `${this.namespace}.`;\n const now = this.now();\n\n return hits\n .filter((hit: CacheSimilarHit<StoredEpisode>) =>\n
|
|
1
|
+
{"version":3,"file":"episodic-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/episodic-memory.ts"],"sourcesContent":["import type { CacheDriver, CacheSimilarHit } from \"@warlock.js/cache\";\nimport type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * Shape persisted per episode. `ts` is the wall-clock time the episode\n * was remembered — the basis for the recency half of the blended recall\n * score. The vector lives in the driver's index (via `set({ vector })`),\n * so it is not duplicated here.\n */\ntype StoredEpisode = {\n id: string;\n text: string;\n ts: number;\n /** Isolation key the episode was written under; absent = the shared pool. */\n scope?: string;\n metadata?: Record<string, unknown>;\n};\n\n/**\n * How many extra candidates to pull from `similar()` before re-ranking by\n * the recency-blended score and slicing to `k`. Recency can promote a\n * slightly-less-similar-but-recent episode past a stale exact match, so\n * the raw top-`k` by similarity alone would miss it — overscan, then\n * re-rank.\n */\nconst RECALL_OVERSCAN = 5;\n\n/**\n * Episodic recall tier (memory core M2).\n *\n * Holds a durable, timestamped log of *what happened* — events/episodes —\n * and retrieves the ones most relevant to a query, **blended with\n * recency** so recent episodes outrank stale ones at equal similarity.\n * That recency weighting is the whole difference from the {@link\n * import(\"./semantic-memory\").SemanticMemory} tier (pure similarity over\n * timeless facts): episodic memory is time-anchored.\n *\n * Like the semantic tier it delegates the similarity search to the\n * `@warlock.js/cache` driver's `similar()` and never implements ANN\n * itself; it adds a stored `ts` per entry and a decay curve at recall.\n * The blended `score` stays in `[0, 1]` so a consumer can merge episodic\n * hits with the other tiers and sort on one field.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class EpisodicMemory {\n public constructor(\n private readonly embedder: EmbedderContract,\n private readonly store: CacheDriver<any, any>,\n private readonly namespace: string,\n private readonly recencyWeight: number,\n private readonly halfLifeMs: number,\n private readonly now: () => number,\n ) {}\n\n /**\n * Embed the episode text and index it under a namespaced, id-derived\n * key, stamping the current time. Re-remembering the same id overwrites\n * the prior entry (and refreshes its timestamp).\n */\n public async remember(item: MemoryItem): Promise<void> {\n const id = item.id ?? deriveMemoryId(item.text);\n const { vector } = await this.embedder.embed(item.text);\n\n const value: StoredEpisode = {\n id,\n text: item.text,\n ts: this.now(),\n scope: item.scope,\n metadata: item.metadata,\n };\n\n await this.store.set(this.keyFor(id, item.scope), value, { vector });\n }\n\n /**\n * Embed `query`, pull the nearest episodes clearing the similarity\n * `threshold`, then re-rank each by a recency-blended score before\n * returning the top `k`. The similarity floor still gates relevance —\n * recency only reorders episodes that already cleared it, it never\n * surfaces an irrelevant-but-recent one.\n *\n * Episodes written under a different `scope` (another tenant /\n * session) are dropped here, before scoring and slicing, so they can\n * neither leak nor consume a slot. An unscoped recall reads only\n * unscoped episodes.\n */\n public async recall(\n query: string,\n k: number,\n threshold: number,\n scope?: string,\n ): Promise<RecalledMemory[]> {\n const { vector } = await this.embedder.embed(query);\n\n const hits = await this.store.similar<StoredEpisode>(vector, {\n topK: Math.max(k * RECALL_OVERSCAN, k),\n threshold,\n });\n\n const prefix = `${this.namespace}.`;\n const now = this.now();\n\n return hits\n .filter(\n (hit: CacheSimilarHit<StoredEpisode>) =>\n hit.key.startsWith(prefix) && hit.value?.scope === scope,\n )\n .map((hit: CacheSimilarHit<StoredEpisode>) => ({\n id: hit.value.id,\n text: hit.value.text,\n tier: \"episodic\" as const,\n score: this.blend(hit.score, hit.value.ts, now),\n metadata: hit.value.metadata,\n }))\n .sort((first, second) => second.score - first.score)\n .slice(0, k);\n }\n\n /** Drop every episode written under this instance's namespace. */\n public async clear(): Promise<void> {\n await this.store.removeNamespace(this.namespace);\n }\n\n /**\n * Combine raw similarity with an exponential recency decay:\n * `(1 - w)·similarity + w·0.5^(age / halfLife)`. A just-remembered\n * episode contributes a recency of `1`; one `halfLife` old, `0.5`;\n * older trends toward `0`. With `recencyWeight` 0 the score is pure\n * similarity (an opt-out back to semantic-style ranking).\n */\n private blend(similarity: number, ts: number, now: number): number {\n const ageMs = Math.max(0, now - ts);\n const recency = 0.5 ** (ageMs / this.halfLifeMs);\n\n return (1 - this.recencyWeight) * similarity + this.recencyWeight * recency;\n }\n\n /**\n * Namespaced key for an entry. Mirrors the semantic tier's dot\n * separator so the prefix used here matches the `hit.key` the driver\n * returns from `similar()`, and its hashed scope segment so two\n * scopes never overwrite one another's identical text. Unscoped keys\n * keep their pre-4.15.0 shape.\n */\n private keyFor(id: string, scope?: string): string {\n return scope === undefined\n ? `${this.namespace}.${id}`\n : `${this.namespace}.${deriveMemoryId(scope)}.${id}`;\n }\n}\n"],"mappings":";;;;;;;;;;AA8BA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;AAqBxB,IAAa,iBAAb,MAA4B;CAC1B,AAAO,YACL,AAAiB,UACjB,AAAiB,OACjB,AAAiB,WACjB,AAAiB,eACjB,AAAiB,YACjB,AAAiB,KACjB;EANiB;EACA;EACA;EACA;EACA;EACA;CAChB;;;;;;CAOH,MAAa,SAAS,MAAiC;EACrD,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAC9C,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,IAAI;EAEtD,MAAM,QAAuB;GAC3B;GACA,MAAM,KAAK;GACX,IAAI,KAAK,IAAI;GACb,OAAO,KAAK;GACZ,UAAU,KAAK;EACjB;EAEA,MAAM,KAAK,MAAM,IAAI,KAAK,OAAO,IAAI,KAAK,KAAK,GAAG,OAAO,EAAE,OAAO,CAAC;CACrE;;;;;;;;;;;;;CAcA,MAAa,OACX,OACA,GACA,WACA,OAC2B;EAC3B,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK;EAElD,MAAM,OAAO,MAAM,KAAK,MAAM,QAAuB,QAAQ;GAC3D,MAAM,KAAK,IAAI,IAAI,iBAAiB,CAAC;GACrC;EACF,CAAC;EAED,MAAM,SAAS,GAAG,KAAK,UAAU;EACjC,MAAM,MAAM,KAAK,IAAI;EAErB,OAAO,KACJ,QACE,QACC,IAAI,IAAI,WAAW,MAAM,KAAK,IAAI,OAAO,UAAU,KACvD,CAAC,CACA,KAAK,SAAyC;GAC7C,IAAI,IAAI,MAAM;GACd,MAAM,IAAI,MAAM;GAChB,MAAM;GACN,OAAO,KAAK,MAAM,IAAI,OAAO,IAAI,MAAM,IAAI,GAAG;GAC9C,UAAU,IAAI,MAAM;EACtB,EAAE,CAAC,CACF,MAAM,OAAO,WAAW,OAAO,QAAQ,MAAM,KAAK,CAAC,CACnD,MAAM,GAAG,CAAC;CACf;;CAGA,MAAa,QAAuB;EAClC,MAAM,KAAK,MAAM,gBAAgB,KAAK,SAAS;CACjD;;;;;;;;CASA,AAAQ,MAAM,YAAoB,IAAY,KAAqB;EAEjE,MAAM,UAAU,OADF,KAAK,IAAI,GAAG,MAAM,EACJ,IAAI,KAAK;EAErC,QAAQ,IAAI,KAAK,iBAAiB,aAAa,KAAK,gBAAgB;CACtE;;;;;;;;CASA,AAAQ,OAAO,IAAY,OAAwB;EACjD,OAAO,UAAU,SACb,GAAG,KAAK,UAAU,GAAG,OACrB,GAAG,KAAK,UAAU,GAAG,eAAe,KAAK,EAAE,GAAG;CACpD;AACF"}
|
package/esm/memory/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MemoryItem, MemoryTier, RecalledMemory } from "../contracts/memory/memory-item.type.mjs";
|
|
2
2
|
import { RecallOptions } from "../contracts/memory/recall-options.type.mjs";
|
|
3
3
|
import { MemoryContract } from "../contracts/memory/memory.contract.mjs";
|
|
4
|
-
import { EpisodicMemoryConfig, MemoryConfig, ProceduralMemoryConfig, SemanticMemoryConfig } from "../contracts/memory/memory-config.type.mjs";
|
|
4
|
+
import { EpisodicMemoryConfig, MemoryConfig, ProceduralMemoryConfig, SemanticMemoryConfig, WorkingMemoryConfig } from "../contracts/memory/memory-config.type.mjs";
|
|
5
5
|
import { memory } from "./memory.mjs";
|
package/esm/memory/memory.d.mts
CHANGED
|
@@ -19,7 +19,19 @@ import { MemoryConfig } from "../contracts/memory/memory-config.type.mjs";
|
|
|
19
19
|
* no `ai.config({ defaultStore })` throws now; enabling no tier at all
|
|
20
20
|
* throws now.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* TTL-based decay / forgetting remains deferred. The working tier is
|
|
23
|
+
* size-bounded (`working: { maxItems }`, default `1000`, oldest-written
|
|
24
|
+
* evicted first) because it is the one tier that holds everything it is
|
|
25
|
+
* told in process memory for the life of the instance; the durable tiers
|
|
26
|
+
* delegate retention to their `CacheDriver`.
|
|
27
|
+
*
|
|
28
|
+
* **Isolation (4.15.0).** `remember({ scope })` / `recall(query, { scope })`
|
|
29
|
+
* carry an opaque tenant / session key that every tier enforces as an
|
|
30
|
+
* exact-equality filter before scoring — one scope's memories never
|
|
31
|
+
* surface in another's recall, and identical text under two scopes stays
|
|
32
|
+
* two entries. Unscoped writes form a shared pool that only an unscoped
|
|
33
|
+
* recall can read; there is no "all scopes" query. `ai.orchestrator()`
|
|
34
|
+
* derives this from the turn's `sessionId` automatically.
|
|
23
35
|
*
|
|
24
36
|
* @example
|
|
25
37
|
* import { ai } from "@warlock.js/ai";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.mts","names":[],"sources":["../../../../../../../ai/src/memory/memory.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"memory.d.mts","names":[],"sources":["../../../../../../../ai/src/memory/memory.ts"],"mappings":";;;;;;AAkFA;;;;;;;;AAAiE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAjD,MAAA,CAAO,MAAA,GAAQ,YAAA,GAAoB,cAAc"}
|
package/esm/memory/memory.mjs
CHANGED
|
@@ -15,6 +15,14 @@ const DEFAULT_RECENCY_WEIGHT = .3;
|
|
|
15
15
|
const DEFAULT_HALF_LIFE_MS = 10080 * 60 * 1e3;
|
|
16
16
|
const DEFAULT_REINFORCEMENT_WEIGHT = .3;
|
|
17
17
|
/**
|
|
18
|
+
* Entries the in-process working buffer holds before it starts evicting
|
|
19
|
+
* its oldest (4.15.0 — security fix for unbounded growth). Sized to hold
|
|
20
|
+
* a deep multi-session scratch history while capping the tier's worst
|
|
21
|
+
* case at a few MB of resident text rather than "everything this process
|
|
22
|
+
* has ever been told."
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_WORKING_MAX_ITEMS = 1e3;
|
|
25
|
+
/**
|
|
18
26
|
* Create an agent memory store (memory core M2).
|
|
19
27
|
*
|
|
20
28
|
* Wires up to four tiers behind the {@link MemoryContract}: **working**
|
|
@@ -31,7 +39,19 @@ const DEFAULT_REINFORCEMENT_WEIGHT = .3;
|
|
|
31
39
|
* no `ai.config({ defaultStore })` throws now; enabling no tier at all
|
|
32
40
|
* throws now.
|
|
33
41
|
*
|
|
34
|
-
*
|
|
42
|
+
* TTL-based decay / forgetting remains deferred. The working tier is
|
|
43
|
+
* size-bounded (`working: { maxItems }`, default `1000`, oldest-written
|
|
44
|
+
* evicted first) because it is the one tier that holds everything it is
|
|
45
|
+
* told in process memory for the life of the instance; the durable tiers
|
|
46
|
+
* delegate retention to their `CacheDriver`.
|
|
47
|
+
*
|
|
48
|
+
* **Isolation (4.15.0).** `remember({ scope })` / `recall(query, { scope })`
|
|
49
|
+
* carry an opaque tenant / session key that every tier enforces as an
|
|
50
|
+
* exact-equality filter before scoring — one scope's memories never
|
|
51
|
+
* surface in another's recall, and identical text under two scopes stays
|
|
52
|
+
* two entries. Unscoped writes form a shared pool that only an unscoped
|
|
53
|
+
* recall can read; there is no "all scopes" query. `ai.orchestrator()`
|
|
54
|
+
* derives this from the turn's `sessionId` automatically.
|
|
35
55
|
*
|
|
36
56
|
* @example
|
|
37
57
|
* import { ai } from "@warlock.js/ai";
|
|
@@ -50,10 +70,10 @@ const DEFAULT_REINFORCEMENT_WEIGHT = .3;
|
|
|
50
70
|
*/
|
|
51
71
|
function memory(config = {}) {
|
|
52
72
|
const name = config.name ?? DEFAULT_NAME;
|
|
53
|
-
const
|
|
73
|
+
const workingConfig = config.working ?? true;
|
|
54
74
|
const defaultK = config.k ?? DEFAULT_K;
|
|
55
75
|
const defaultThreshold = config.threshold ?? DEFAULT_THRESHOLD;
|
|
56
|
-
const working =
|
|
76
|
+
const working = workingConfig === false ? void 0 : new WorkingMemory(resolveWorkingMaxItems(workingConfig, name));
|
|
57
77
|
const semantic = config.semantic ? buildSemanticTier(config.semantic, name) : void 0;
|
|
58
78
|
const episodic = config.episodic ? buildEpisodicTier(config.episodic, name) : void 0;
|
|
59
79
|
const procedural = config.procedural ? buildProceduralTier(config.procedural, name) : void 0;
|
|
@@ -95,11 +115,12 @@ function memory(config = {}) {
|
|
|
95
115
|
const threshold = options.threshold ?? defaultThreshold;
|
|
96
116
|
if (options.tier) assertTierEnabled(options.tier, tiers, name);
|
|
97
117
|
const wants = (tier) => !options.tier || options.tier === tier;
|
|
118
|
+
const scope = options.scope;
|
|
98
119
|
const [workingHits, semanticHits, episodicHits, proceduralHits] = await Promise.all([
|
|
99
|
-
working && wants("working") ? Promise.resolve(working.recall(k)) : Promise.resolve([]),
|
|
100
|
-
semantic && wants("semantic") ? semantic.recall(query, k, threshold) : Promise.resolve([]),
|
|
101
|
-
episodic && wants("episodic") ? episodic.recall(query, k, threshold) : Promise.resolve([]),
|
|
102
|
-
procedural && wants("procedural") ? procedural.recall(query, k, threshold) : Promise.resolve([])
|
|
120
|
+
working && wants("working") ? Promise.resolve(working.recall(k, scope)) : Promise.resolve([]),
|
|
121
|
+
semantic && wants("semantic") ? semantic.recall(query, k, threshold, scope) : Promise.resolve([]),
|
|
122
|
+
episodic && wants("episodic") ? episodic.recall(query, k, threshold, scope) : Promise.resolve([]),
|
|
123
|
+
procedural && wants("procedural") ? procedural.recall(query, k, threshold, scope) : Promise.resolve([])
|
|
103
124
|
]);
|
|
104
125
|
return [
|
|
105
126
|
...workingHits,
|
|
@@ -119,6 +140,19 @@ function memory(config = {}) {
|
|
|
119
140
|
};
|
|
120
141
|
}
|
|
121
142
|
/**
|
|
143
|
+
* Resolve the working tier's size bound from the `working` config
|
|
144
|
+
* (`true` / a `{ maxItems }` object), validating it at construction the
|
|
145
|
+
* same way every other tier's wiring fails loud-and-now rather than on
|
|
146
|
+
* first use. There is deliberately no unbounded setting — the buffer is
|
|
147
|
+
* process-resident for the life of the memory instance, so "no cap" is
|
|
148
|
+
* a memory-exhaustion vector, not a configuration choice.
|
|
149
|
+
*/
|
|
150
|
+
function resolveWorkingMaxItems(workingConfig, name) {
|
|
151
|
+
const maxItems = workingConfig === true ? DEFAULT_WORKING_MAX_ITEMS : workingConfig.maxItems ?? DEFAULT_WORKING_MAX_ITEMS;
|
|
152
|
+
if (!Number.isInteger(maxItems) || maxItems < 1) throw new Error(`memory("${name}"): working tier \`maxItems\` must be an integer >= 1 — received ${String(maxItems)}`);
|
|
153
|
+
return maxItems;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
122
156
|
* Resolve the semantic tier's store (explicit `store` wins, else the
|
|
123
157
|
* global `ai.config({ defaultStore })`) and build the tier. Throws at
|
|
124
158
|
* construction when neither is available — the same loud-now contract
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/memory.ts"],"sourcesContent":["import { resolveDefaultStore } from \"../config\";\nimport type { MemoryConfig } from \"../contracts/memory/memory-config.type\";\nimport type {\n MemoryItem,\n MemoryTier,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport type { MemoryContract } from \"../contracts/memory/memory.contract\";\nimport type { RecallOptions } from \"../contracts/memory/recall-options.type\";\nimport { EpisodicMemory } from \"./episodic-memory\";\nimport { ProceduralMemory } from \"./procedural-memory\";\nimport { SemanticMemory } from \"./semantic-memory\";\nimport { WorkingMemory } from \"./working-memory\";\n\nconst DEFAULT_NAME = \"memory\";\nconst DEFAULT_SEMANTIC_NAMESPACE = \"ai.memory.semantic\";\nconst DEFAULT_EPISODIC_NAMESPACE = \"ai.memory.episodic\";\nconst DEFAULT_PROCEDURAL_NAMESPACE = \"ai.memory.procedural\";\nconst DEFAULT_K = 5;\nconst DEFAULT_THRESHOLD = 0.7;\nconst DEFAULT_RECENCY_WEIGHT = 0.3;\nconst DEFAULT_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000;\nconst DEFAULT_REINFORCEMENT_WEIGHT = 0.3;\n\n/**\n * Create an agent memory store (memory core M2).\n *\n * Wires up to four tiers behind the {@link MemoryContract}: **working**\n * (in-run scratch, recency), **semantic** (durable facts by cosine\n * similarity), **episodic** (durable events, similarity blended with\n * recency), and **procedural** (durable how-tos, similarity blended with\n * reinforcement). The working tier is on by default; the other three each\n * activate only when their config is supplied. The three vector tiers\n * mirror how `semanticCache` delegates similarity to the cache driver's\n * `.similar()`.\n *\n * Resolution happens once here, at construction (loud), rather than per\n * call (silent until first use): a vector-tier config with no `store` and\n * no `ai.config({ defaultStore })` throws now; enabling no tier at all\n * throws now.\n *\n * Decay / forgetting (TTL-based falloff, eviction) remains deferred.\n *\n * @example\n * import { ai } from \"@warlock.js/ai\";\n * import { MemoryCacheDriver } from \"@warlock.js/cache\";\n *\n * const store = new MemoryCacheDriver();\n * store.setOptions({});\n *\n * const mem = ai.memory({\n * semantic: { embedder, store },\n * defaultTier: \"semantic\",\n * });\n *\n * await mem.remember({ text: \"User prefers concise answers.\" });\n * const hits = await mem.recall(\"how should I respond?\", { k: 3 });\n */\nexport function memory(config: MemoryConfig = {}): MemoryContract {\n const name = config.name ?? DEFAULT_NAME;\n const workingEnabled = config.working ?? true;\n const defaultK = config.k ?? DEFAULT_K;\n const defaultThreshold = config.threshold ?? DEFAULT_THRESHOLD;\n\n const working = workingEnabled ? new WorkingMemory() : undefined;\n\n const semantic = config.semantic\n ? buildSemanticTier(config.semantic, name)\n : undefined;\n\n const episodic = config.episodic\n ? buildEpisodicTier(config.episodic, name)\n : undefined;\n\n const procedural = config.procedural\n ? buildProceduralTier(config.procedural, name)\n : undefined;\n\n const tiers: Tiers = { working, semantic, episodic, procedural };\n\n if (!working && !semantic && !episodic && !procedural) {\n throw new Error(\n `memory(\"${name}\"): no tier enabled — enable \\`working\\` (default) or pass a \\`semantic\\` / \\`episodic\\` / \\`procedural\\` config; a memory with no tiers can neither store nor recall`,\n );\n }\n\n const defaultTier: MemoryTier = config.defaultTier ?? \"working\";\n\n assertTierEnabled(defaultTier, tiers, name);\n\n return {\n name,\n async remember(items: MemoryItem | MemoryItem[]): Promise<void> {\n const list = Array.isArray(items) ? items : [items];\n\n const writes: Promise<void>[] = [];\n\n for (const item of list) {\n const tier = item.tier ?? defaultTier;\n\n assertTierEnabled(tier, tiers, name);\n\n if (tier === \"working\") {\n working!.remember(item);\n\n continue;\n }\n\n if (tier === \"semantic\") {\n writes.push(semantic!.remember(item));\n\n continue;\n }\n\n if (tier === \"episodic\") {\n writes.push(episodic!.remember(item));\n\n continue;\n }\n\n writes.push(procedural!.remember(item));\n }\n\n await Promise.all(writes);\n },\n async recall(\n query: string,\n options: RecallOptions = {},\n ): Promise<RecalledMemory[]> {\n const k = options.k ?? defaultK;\n const threshold = options.threshold ?? defaultThreshold;\n\n if (options.tier) {\n assertTierEnabled(options.tier, tiers, name);\n }\n\n const wants = (tier: MemoryTier): boolean =>\n !options.tier || options.tier === tier;\n\n const [workingHits, semanticHits, episodicHits, proceduralHits] =\n await Promise.all([\n working && wants(\"working\")\n ? Promise.resolve(working.recall(k))\n : Promise.resolve([] as RecalledMemory[]),\n semantic && wants(\"semantic\")\n ? semantic.recall(query, k, threshold)\n : Promise.resolve([] as RecalledMemory[]),\n episodic && wants(\"episodic\")\n ? episodic.recall(query, k, threshold)\n : Promise.resolve([] as RecalledMemory[]),\n procedural && wants(\"procedural\")\n ? procedural.recall(query, k, threshold)\n : Promise.resolve([] as RecalledMemory[]),\n ]);\n\n return [\n ...workingHits,\n ...semanticHits,\n ...episodicHits,\n ...proceduralHits,\n ]\n .sort((first, second) => second.score - first.score)\n .slice(0, k);\n },\n async clear(tier?: MemoryTier): Promise<void> {\n const clears: Promise<void>[] = [];\n\n if (working && (!tier || tier === \"working\")) {\n working.clear();\n }\n\n if (semantic && (!tier || tier === \"semantic\")) {\n clears.push(semantic.clear());\n }\n\n if (episodic && (!tier || tier === \"episodic\")) {\n clears.push(episodic.clear());\n }\n\n if (procedural && (!tier || tier === \"procedural\")) {\n clears.push(procedural.clear());\n }\n\n await Promise.all(clears);\n },\n };\n}\n\n/** The four tier instances a `memory()` composes; `undefined` when off. */\ntype Tiers = {\n working: WorkingMemory | undefined;\n semantic: SemanticMemory | undefined;\n episodic: EpisodicMemory | undefined;\n procedural: ProceduralMemory | undefined;\n};\n\n/**\n * Resolve the semantic tier's store (explicit `store` wins, else the\n * global `ai.config({ defaultStore })`) and build the tier. Throws at\n * construction when neither is available — the same loud-now contract\n * `semanticCache` follows.\n */\nfunction buildSemanticTier(\n semanticConfig: NonNullable<MemoryConfig[\"semantic\"]>,\n name: string,\n): SemanticMemory {\n const store = semanticConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): semantic tier has no store — pass \\`semantic.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new SemanticMemory(\n semanticConfig.embedder,\n store,\n semanticConfig.namespace ?? DEFAULT_SEMANTIC_NAMESPACE,\n );\n}\n\n/**\n * Resolve the episodic tier's store (explicit `store` wins, else the\n * global default) and build the tier with its recency knobs. Throws at\n * construction when neither store is available — the same loud-now\n * contract the semantic tier follows.\n */\nfunction buildEpisodicTier(\n episodicConfig: NonNullable<MemoryConfig[\"episodic\"]>,\n name: string,\n): EpisodicMemory {\n const store = episodicConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): episodic tier has no store — pass \\`episodic.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new EpisodicMemory(\n episodicConfig.embedder,\n store,\n episodicConfig.namespace ?? DEFAULT_EPISODIC_NAMESPACE,\n episodicConfig.recencyWeight ?? DEFAULT_RECENCY_WEIGHT,\n episodicConfig.halfLifeMs ?? DEFAULT_HALF_LIFE_MS,\n episodicConfig.now ?? (() => Date.now()),\n );\n}\n\n/**\n * Resolve the procedural tier's store and build the tier with its\n * reinforcement knob. Throws at construction when no store is available.\n */\nfunction buildProceduralTier(\n proceduralConfig: NonNullable<MemoryConfig[\"procedural\"]>,\n name: string,\n): ProceduralMemory {\n const store = proceduralConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): procedural tier has no store — pass \\`procedural.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new ProceduralMemory(\n proceduralConfig.embedder,\n store,\n proceduralConfig.namespace ?? DEFAULT_PROCEDURAL_NAMESPACE,\n proceduralConfig.reinforcementWeight ?? DEFAULT_REINFORCEMENT_WEIGHT,\n );\n}\n\n/**\n * Guard that a tier referenced by config / a call is actually enabled,\n * failing fast with an actionable message instead of a downstream\n * `undefined` dereference.\n */\nfunction assertTierEnabled(tier: MemoryTier, tiers: Tiers, name: string): void {\n if (tier === \"working\" && !tiers.working) {\n throw new Error(\n `memory(\"${name}\"): working tier is disabled — set \\`working: true\\` (the default) to use it`,\n );\n }\n\n if (tier === \"semantic\" && !tiers.semantic) {\n throw new Error(\n `memory(\"${name}\"): semantic tier is not configured — pass \\`semantic\\` config to use it`,\n );\n }\n\n if (tier === \"episodic\" && !tiers.episodic) {\n throw new Error(\n `memory(\"${name}\"): episodic tier is not configured — pass \\`episodic\\` config to use it`,\n );\n }\n\n if (tier === \"procedural\" && !tiers.procedural) {\n throw new Error(\n `memory(\"${name}\"): procedural tier is not configured — pass \\`procedural\\` config to use it`,\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/memory.ts"],"sourcesContent":["import { resolveDefaultStore } from \"../config\";\nimport type {\n MemoryConfig,\n WorkingMemoryConfig,\n} from \"../contracts/memory/memory-config.type\";\nimport type {\n MemoryItem,\n MemoryTier,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport type { MemoryContract } from \"../contracts/memory/memory.contract\";\nimport type { RecallOptions } from \"../contracts/memory/recall-options.type\";\nimport { EpisodicMemory } from \"./episodic-memory\";\nimport { ProceduralMemory } from \"./procedural-memory\";\nimport { SemanticMemory } from \"./semantic-memory\";\nimport { WorkingMemory } from \"./working-memory\";\n\nconst DEFAULT_NAME = \"memory\";\nconst DEFAULT_SEMANTIC_NAMESPACE = \"ai.memory.semantic\";\nconst DEFAULT_EPISODIC_NAMESPACE = \"ai.memory.episodic\";\nconst DEFAULT_PROCEDURAL_NAMESPACE = \"ai.memory.procedural\";\nconst DEFAULT_K = 5;\nconst DEFAULT_THRESHOLD = 0.7;\nconst DEFAULT_RECENCY_WEIGHT = 0.3;\nconst DEFAULT_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000;\nconst DEFAULT_REINFORCEMENT_WEIGHT = 0.3;\n\n/**\n * Entries the in-process working buffer holds before it starts evicting\n * its oldest (4.15.0 — security fix for unbounded growth). Sized to hold\n * a deep multi-session scratch history while capping the tier's worst\n * case at a few MB of resident text rather than \"everything this process\n * has ever been told.\"\n */\nconst DEFAULT_WORKING_MAX_ITEMS = 1000;\n\n/**\n * Create an agent memory store (memory core M2).\n *\n * Wires up to four tiers behind the {@link MemoryContract}: **working**\n * (in-run scratch, recency), **semantic** (durable facts by cosine\n * similarity), **episodic** (durable events, similarity blended with\n * recency), and **procedural** (durable how-tos, similarity blended with\n * reinforcement). The working tier is on by default; the other three each\n * activate only when their config is supplied. The three vector tiers\n * mirror how `semanticCache` delegates similarity to the cache driver's\n * `.similar()`.\n *\n * Resolution happens once here, at construction (loud), rather than per\n * call (silent until first use): a vector-tier config with no `store` and\n * no `ai.config({ defaultStore })` throws now; enabling no tier at all\n * throws now.\n *\n * TTL-based decay / forgetting remains deferred. The working tier is\n * size-bounded (`working: { maxItems }`, default `1000`, oldest-written\n * evicted first) because it is the one tier that holds everything it is\n * told in process memory for the life of the instance; the durable tiers\n * delegate retention to their `CacheDriver`.\n *\n * **Isolation (4.15.0).** `remember({ scope })` / `recall(query, { scope })`\n * carry an opaque tenant / session key that every tier enforces as an\n * exact-equality filter before scoring — one scope's memories never\n * surface in another's recall, and identical text under two scopes stays\n * two entries. Unscoped writes form a shared pool that only an unscoped\n * recall can read; there is no \"all scopes\" query. `ai.orchestrator()`\n * derives this from the turn's `sessionId` automatically.\n *\n * @example\n * import { ai } from \"@warlock.js/ai\";\n * import { MemoryCacheDriver } from \"@warlock.js/cache\";\n *\n * const store = new MemoryCacheDriver();\n * store.setOptions({});\n *\n * const mem = ai.memory({\n * semantic: { embedder, store },\n * defaultTier: \"semantic\",\n * });\n *\n * await mem.remember({ text: \"User prefers concise answers.\" });\n * const hits = await mem.recall(\"how should I respond?\", { k: 3 });\n */\nexport function memory(config: MemoryConfig = {}): MemoryContract {\n const name = config.name ?? DEFAULT_NAME;\n const workingConfig = config.working ?? true;\n const defaultK = config.k ?? DEFAULT_K;\n const defaultThreshold = config.threshold ?? DEFAULT_THRESHOLD;\n\n const working =\n workingConfig === false\n ? undefined\n : new WorkingMemory(resolveWorkingMaxItems(workingConfig, name));\n\n const semantic = config.semantic\n ? buildSemanticTier(config.semantic, name)\n : undefined;\n\n const episodic = config.episodic\n ? buildEpisodicTier(config.episodic, name)\n : undefined;\n\n const procedural = config.procedural\n ? buildProceduralTier(config.procedural, name)\n : undefined;\n\n const tiers: Tiers = { working, semantic, episodic, procedural };\n\n if (!working && !semantic && !episodic && !procedural) {\n throw new Error(\n `memory(\"${name}\"): no tier enabled — enable \\`working\\` (default) or pass a \\`semantic\\` / \\`episodic\\` / \\`procedural\\` config; a memory with no tiers can neither store nor recall`,\n );\n }\n\n const defaultTier: MemoryTier = config.defaultTier ?? \"working\";\n\n assertTierEnabled(defaultTier, tiers, name);\n\n return {\n name,\n async remember(items: MemoryItem | MemoryItem[]): Promise<void> {\n const list = Array.isArray(items) ? items : [items];\n\n const writes: Promise<void>[] = [];\n\n for (const item of list) {\n const tier = item.tier ?? defaultTier;\n\n assertTierEnabled(tier, tiers, name);\n\n if (tier === \"working\") {\n working!.remember(item);\n\n continue;\n }\n\n if (tier === \"semantic\") {\n writes.push(semantic!.remember(item));\n\n continue;\n }\n\n if (tier === \"episodic\") {\n writes.push(episodic!.remember(item));\n\n continue;\n }\n\n writes.push(procedural!.remember(item));\n }\n\n await Promise.all(writes);\n },\n async recall(\n query: string,\n options: RecallOptions = {},\n ): Promise<RecalledMemory[]> {\n const k = options.k ?? defaultK;\n const threshold = options.threshold ?? defaultThreshold;\n\n if (options.tier) {\n assertTierEnabled(options.tier, tiers, name);\n }\n\n const wants = (tier: MemoryTier): boolean =>\n !options.tier || options.tier === tier;\n\n // `options.scope` is the isolation key — each tier applies it as an\n // exact-equality filter internally, BEFORE its own scoring and\n // slicing, so nothing outside the scope reaches this merge.\n const scope = options.scope;\n\n const [workingHits, semanticHits, episodicHits, proceduralHits] =\n await Promise.all([\n working && wants(\"working\")\n ? Promise.resolve(working.recall(k, scope))\n : Promise.resolve([] as RecalledMemory[]),\n semantic && wants(\"semantic\")\n ? semantic.recall(query, k, threshold, scope)\n : Promise.resolve([] as RecalledMemory[]),\n episodic && wants(\"episodic\")\n ? episodic.recall(query, k, threshold, scope)\n : Promise.resolve([] as RecalledMemory[]),\n procedural && wants(\"procedural\")\n ? procedural.recall(query, k, threshold, scope)\n : Promise.resolve([] as RecalledMemory[]),\n ]);\n\n return [\n ...workingHits,\n ...semanticHits,\n ...episodicHits,\n ...proceduralHits,\n ]\n .sort((first, second) => second.score - first.score)\n .slice(0, k);\n },\n async clear(tier?: MemoryTier): Promise<void> {\n const clears: Promise<void>[] = [];\n\n if (working && (!tier || tier === \"working\")) {\n working.clear();\n }\n\n if (semantic && (!tier || tier === \"semantic\")) {\n clears.push(semantic.clear());\n }\n\n if (episodic && (!tier || tier === \"episodic\")) {\n clears.push(episodic.clear());\n }\n\n if (procedural && (!tier || tier === \"procedural\")) {\n clears.push(procedural.clear());\n }\n\n await Promise.all(clears);\n },\n };\n}\n\n/** The four tier instances a `memory()` composes; `undefined` when off. */\ntype Tiers = {\n working: WorkingMemory | undefined;\n semantic: SemanticMemory | undefined;\n episodic: EpisodicMemory | undefined;\n procedural: ProceduralMemory | undefined;\n};\n\n/**\n * Resolve the working tier's size bound from the `working` config\n * (`true` / a `{ maxItems }` object), validating it at construction the\n * same way every other tier's wiring fails loud-and-now rather than on\n * first use. There is deliberately no unbounded setting — the buffer is\n * process-resident for the life of the memory instance, so \"no cap\" is\n * a memory-exhaustion vector, not a configuration choice.\n */\nfunction resolveWorkingMaxItems(\n workingConfig: true | WorkingMemoryConfig,\n name: string,\n): number {\n const maxItems =\n workingConfig === true\n ? DEFAULT_WORKING_MAX_ITEMS\n : (workingConfig.maxItems ?? DEFAULT_WORKING_MAX_ITEMS);\n\n if (!Number.isInteger(maxItems) || maxItems < 1) {\n throw new Error(\n `memory(\"${name}\"): working tier \\`maxItems\\` must be an integer >= 1 — received ${String(maxItems)}`,\n );\n }\n\n return maxItems;\n}\n\n/**\n * Resolve the semantic tier's store (explicit `store` wins, else the\n * global `ai.config({ defaultStore })`) and build the tier. Throws at\n * construction when neither is available — the same loud-now contract\n * `semanticCache` follows.\n */\nfunction buildSemanticTier(\n semanticConfig: NonNullable<MemoryConfig[\"semantic\"]>,\n name: string,\n): SemanticMemory {\n const store = semanticConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): semantic tier has no store — pass \\`semantic.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new SemanticMemory(\n semanticConfig.embedder,\n store,\n semanticConfig.namespace ?? DEFAULT_SEMANTIC_NAMESPACE,\n );\n}\n\n/**\n * Resolve the episodic tier's store (explicit `store` wins, else the\n * global default) and build the tier with its recency knobs. Throws at\n * construction when neither store is available — the same loud-now\n * contract the semantic tier follows.\n */\nfunction buildEpisodicTier(\n episodicConfig: NonNullable<MemoryConfig[\"episodic\"]>,\n name: string,\n): EpisodicMemory {\n const store = episodicConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): episodic tier has no store — pass \\`episodic.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new EpisodicMemory(\n episodicConfig.embedder,\n store,\n episodicConfig.namespace ?? DEFAULT_EPISODIC_NAMESPACE,\n episodicConfig.recencyWeight ?? DEFAULT_RECENCY_WEIGHT,\n episodicConfig.halfLifeMs ?? DEFAULT_HALF_LIFE_MS,\n episodicConfig.now ?? (() => Date.now()),\n );\n}\n\n/**\n * Resolve the procedural tier's store and build the tier with its\n * reinforcement knob. Throws at construction when no store is available.\n */\nfunction buildProceduralTier(\n proceduralConfig: NonNullable<MemoryConfig[\"procedural\"]>,\n name: string,\n): ProceduralMemory {\n const store = proceduralConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): procedural tier has no store — pass \\`procedural.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new ProceduralMemory(\n proceduralConfig.embedder,\n store,\n proceduralConfig.namespace ?? DEFAULT_PROCEDURAL_NAMESPACE,\n proceduralConfig.reinforcementWeight ?? DEFAULT_REINFORCEMENT_WEIGHT,\n );\n}\n\n/**\n * Guard that a tier referenced by config / a call is actually enabled,\n * failing fast with an actionable message instead of a downstream\n * `undefined` dereference.\n */\nfunction assertTierEnabled(tier: MemoryTier, tiers: Tiers, name: string): void {\n if (tier === \"working\" && !tiers.working) {\n throw new Error(\n `memory(\"${name}\"): working tier is disabled — set \\`working: true\\` (the default) to use it`,\n );\n }\n\n if (tier === \"semantic\" && !tiers.semantic) {\n throw new Error(\n `memory(\"${name}\"): semantic tier is not configured — pass \\`semantic\\` config to use it`,\n );\n }\n\n if (tier === \"episodic\" && !tiers.episodic) {\n throw new Error(\n `memory(\"${name}\"): episodic tier is not configured — pass \\`episodic\\` config to use it`,\n );\n }\n\n if (tier === \"procedural\" && !tiers.procedural) {\n throw new Error(\n `memory(\"${name}\"): procedural tier is not configured — pass \\`procedural\\` config to use it`,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -46,30 +46,37 @@ var ProceduralMemory = class {
|
|
|
46
46
|
async remember(item) {
|
|
47
47
|
const id = item.id ?? deriveMemoryId(item.text);
|
|
48
48
|
const { vector } = await this.embedder.embed(item.text);
|
|
49
|
-
const
|
|
49
|
+
const key = this.keyFor(id, item.scope);
|
|
50
|
+
const existing = await this.store.get(key);
|
|
50
51
|
const uses = (existing?.uses ?? 0) + 1;
|
|
51
52
|
const value = {
|
|
52
53
|
id,
|
|
53
54
|
text: item.text,
|
|
54
55
|
uses,
|
|
56
|
+
scope: item.scope,
|
|
55
57
|
metadata: item.metadata ?? existing?.metadata
|
|
56
58
|
};
|
|
57
|
-
await this.store.set(
|
|
59
|
+
await this.store.set(key, value, { vector });
|
|
58
60
|
}
|
|
59
61
|
/**
|
|
60
62
|
* Embed `query`, pull the nearest procedures clearing the similarity
|
|
61
63
|
* `threshold`, then re-rank each by a reinforcement-blended score and
|
|
62
64
|
* return the top `k`. The similarity floor still gates relevance;
|
|
63
65
|
* reinforcement only reorders procedures that already cleared it.
|
|
66
|
+
*
|
|
67
|
+
* Procedures written under a different `scope` (another tenant /
|
|
68
|
+
* session) are dropped here, before scoring and slicing, so they can
|
|
69
|
+
* neither leak nor consume a slot. An unscoped recall reads only
|
|
70
|
+
* unscoped procedures.
|
|
64
71
|
*/
|
|
65
|
-
async recall(query, k, threshold) {
|
|
72
|
+
async recall(query, k, threshold, scope) {
|
|
66
73
|
const { vector } = await this.embedder.embed(query);
|
|
67
74
|
const hits = await this.store.similar(vector, {
|
|
68
75
|
topK: Math.max(k * RECALL_OVERSCAN, k),
|
|
69
76
|
threshold
|
|
70
77
|
});
|
|
71
78
|
const prefix = `${this.namespace}.`;
|
|
72
|
-
return hits.filter((hit) => hit.key.startsWith(prefix)).map((hit) => ({
|
|
79
|
+
return hits.filter((hit) => hit.key.startsWith(prefix) && hit.value?.scope === scope).map((hit) => ({
|
|
73
80
|
id: hit.value.id,
|
|
74
81
|
text: hit.value.text,
|
|
75
82
|
tier: "procedural",
|
|
@@ -92,9 +99,15 @@ var ProceduralMemory = class {
|
|
|
92
99
|
const reinforcement = uses / (uses + 1);
|
|
93
100
|
return (1 - this.reinforcementWeight) * similarity + this.reinforcementWeight * reinforcement;
|
|
94
101
|
}
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Namespaced key for an entry — dot separator, matching `similar()`
|
|
104
|
+
* keys, plus a hashed scope segment so reinforcement counters never
|
|
105
|
+
* cross a scope boundary (one tenant re-affirming a procedure must not
|
|
106
|
+
* strengthen — or overwrite — another tenant's identical text).
|
|
107
|
+
* Unscoped keys keep their pre-4.15.0 shape.
|
|
108
|
+
*/
|
|
109
|
+
keyFor(id, scope) {
|
|
110
|
+
return scope === void 0 ? `${this.namespace}.${id}` : `${this.namespace}.${deriveMemoryId(scope)}.${id}`;
|
|
98
111
|
}
|
|
99
112
|
};
|
|
100
113
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"procedural-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/procedural-memory.ts"],"sourcesContent":["import type { CacheDriver, CacheSimilarHit } from \"@warlock.js/cache\";\nimport type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * Shape persisted per procedure. `uses` is the reinforcement counter —\n * how many times the procedure has been remembered/re-affirmed — and\n * feeds the reinforcement half of the blended recall score. The vector\n * lives in the driver's index, so it is not duplicated here.\n */\ntype StoredProcedure = {\n id: string;\n text: string;\n uses: number;\n metadata?: Record<string, unknown>;\n};\n\n/**\n * Extra candidates pulled from `similar()` before re-ranking by the\n * reinforcement-blended score and slicing to `k` — reinforcement can\n * promote a well-worn procedure past a slightly-closer one-off, which the\n * raw top-`k` by similarity would miss.\n */\nconst RECALL_OVERSCAN = 5;\n\n/**\n * Procedural recall tier (memory core M2).\n *\n * Holds durable *how-to* knowledge — learned procedures, policies, and\n * playbooks — and retrieves the ones relevant to a query, **blended with\n * reinforcement** so procedures that have proven themselves (remembered /\n * re-affirmed more often) outrank one-offs at equal similarity. That\n * reinforcement weighting is the difference from the semantic tier (which\n * treats every fact equally): procedural memory gets *stronger with use*.\n *\n * Reinforcement is explicit and side-effect-free on read: re-remembering\n * a procedure (same id, or same text → same derived id) increments its\n * `uses`, so a caller strengthens a procedure by remembering it again\n * after a successful application. Recall never mutates.\n *\n * Like the other vector tiers it delegates similarity to the\n * `@warlock.js/cache` driver's `similar()`. The blended `score` stays in\n * `[0, 1]` so procedural hits merge and sort alongside the other tiers.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class ProceduralMemory {\n public constructor(\n private readonly embedder: EmbedderContract,\n private readonly store: CacheDriver<any, any>,\n private readonly namespace: string,\n private readonly reinforcementWeight: number,\n ) {}\n\n /**\n * Embed the procedure text and index it, incrementing its `uses` when\n * it already exists (reinforcement) or seeding it at `1` when new.\n * Metadata on a reinforcing write wins; an omitted metadata keeps the\n * prior value rather than wiping it.\n */\n public async remember(item: MemoryItem): Promise<void> {\n const id = item.id ?? deriveMemoryId(item.text);\n const { vector } = await this.embedder.embed(item.text);\n\n const existing = await this.store.get<StoredProcedure>(
|
|
1
|
+
{"version":3,"file":"procedural-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/procedural-memory.ts"],"sourcesContent":["import type { CacheDriver, CacheSimilarHit } from \"@warlock.js/cache\";\nimport type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * Shape persisted per procedure. `uses` is the reinforcement counter —\n * how many times the procedure has been remembered/re-affirmed — and\n * feeds the reinforcement half of the blended recall score. The vector\n * lives in the driver's index, so it is not duplicated here.\n */\ntype StoredProcedure = {\n id: string;\n text: string;\n uses: number;\n /** Isolation key the procedure was written under; absent = the shared pool. */\n scope?: string;\n metadata?: Record<string, unknown>;\n};\n\n/**\n * Extra candidates pulled from `similar()` before re-ranking by the\n * reinforcement-blended score and slicing to `k` — reinforcement can\n * promote a well-worn procedure past a slightly-closer one-off, which the\n * raw top-`k` by similarity would miss.\n */\nconst RECALL_OVERSCAN = 5;\n\n/**\n * Procedural recall tier (memory core M2).\n *\n * Holds durable *how-to* knowledge — learned procedures, policies, and\n * playbooks — and retrieves the ones relevant to a query, **blended with\n * reinforcement** so procedures that have proven themselves (remembered /\n * re-affirmed more often) outrank one-offs at equal similarity. That\n * reinforcement weighting is the difference from the semantic tier (which\n * treats every fact equally): procedural memory gets *stronger with use*.\n *\n * Reinforcement is explicit and side-effect-free on read: re-remembering\n * a procedure (same id, or same text → same derived id) increments its\n * `uses`, so a caller strengthens a procedure by remembering it again\n * after a successful application. Recall never mutates.\n *\n * Like the other vector tiers it delegates similarity to the\n * `@warlock.js/cache` driver's `similar()`. The blended `score` stays in\n * `[0, 1]` so procedural hits merge and sort alongside the other tiers.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class ProceduralMemory {\n public constructor(\n private readonly embedder: EmbedderContract,\n private readonly store: CacheDriver<any, any>,\n private readonly namespace: string,\n private readonly reinforcementWeight: number,\n ) {}\n\n /**\n * Embed the procedure text and index it, incrementing its `uses` when\n * it already exists (reinforcement) or seeding it at `1` when new.\n * Metadata on a reinforcing write wins; an omitted metadata keeps the\n * prior value rather than wiping it.\n */\n public async remember(item: MemoryItem): Promise<void> {\n const id = item.id ?? deriveMemoryId(item.text);\n const { vector } = await this.embedder.embed(item.text);\n\n const key = this.keyFor(id, item.scope);\n const existing = await this.store.get<StoredProcedure>(key);\n const uses = (existing?.uses ?? 0) + 1;\n\n const value: StoredProcedure = {\n id,\n text: item.text,\n uses,\n scope: item.scope,\n metadata: item.metadata ?? existing?.metadata,\n };\n\n await this.store.set(key, value, { vector });\n }\n\n /**\n * Embed `query`, pull the nearest procedures clearing the similarity\n * `threshold`, then re-rank each by a reinforcement-blended score and\n * return the top `k`. The similarity floor still gates relevance;\n * reinforcement only reorders procedures that already cleared it.\n *\n * Procedures written under a different `scope` (another tenant /\n * session) are dropped here, before scoring and slicing, so they can\n * neither leak nor consume a slot. An unscoped recall reads only\n * unscoped procedures.\n */\n public async recall(\n query: string,\n k: number,\n threshold: number,\n scope?: string,\n ): Promise<RecalledMemory[]> {\n const { vector } = await this.embedder.embed(query);\n\n const hits = await this.store.similar<StoredProcedure>(vector, {\n topK: Math.max(k * RECALL_OVERSCAN, k),\n threshold,\n });\n\n const prefix = `${this.namespace}.`;\n\n return hits\n .filter(\n (hit: CacheSimilarHit<StoredProcedure>) =>\n hit.key.startsWith(prefix) && hit.value?.scope === scope,\n )\n .map((hit: CacheSimilarHit<StoredProcedure>) => ({\n id: hit.value.id,\n text: hit.value.text,\n tier: \"procedural\" as const,\n score: this.blend(hit.score, hit.value.uses),\n metadata: hit.value.metadata,\n }))\n .sort((first, second) => second.score - first.score)\n .slice(0, k);\n }\n\n /** Drop every procedure written under this instance's namespace. */\n public async clear(): Promise<void> {\n await this.store.removeNamespace(this.namespace);\n }\n\n /**\n * Combine raw similarity with a saturating reinforcement proxy:\n * `(1 - w)·similarity + w·(uses / (uses + 1))`. A first-time procedure\n * contributes `0.5`; each reinforcement nudges it toward `1` with\n * diminishing returns. With `reinforcementWeight` 0 the score is pure\n * similarity.\n */\n private blend(similarity: number, uses: number): number {\n const reinforcement = uses / (uses + 1);\n\n return (\n (1 - this.reinforcementWeight) * similarity +\n this.reinforcementWeight * reinforcement\n );\n }\n\n /**\n * Namespaced key for an entry — dot separator, matching `similar()`\n * keys, plus a hashed scope segment so reinforcement counters never\n * cross a scope boundary (one tenant re-affirming a procedure must not\n * strengthen — or overwrite — another tenant's identical text).\n * Unscoped keys keep their pre-4.15.0 shape.\n */\n private keyFor(id: string, scope?: string): string {\n return scope === undefined\n ? `${this.namespace}.${id}`\n : `${this.namespace}.${deriveMemoryId(scope)}.${id}`;\n }\n}\n"],"mappings":";;;;;;;;;AA6BA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;AAwBxB,IAAa,mBAAb,MAA8B;CAC5B,AAAO,YACL,AAAiB,UACjB,AAAiB,OACjB,AAAiB,WACjB,AAAiB,qBACjB;EAJiB;EACA;EACA;EACA;CAChB;;;;;;;CAQH,MAAa,SAAS,MAAiC;EACrD,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAC9C,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,IAAI;EAEtD,MAAM,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK;EACtC,MAAM,WAAW,MAAM,KAAK,MAAM,IAAqB,GAAG;EAC1D,MAAM,QAAQ,UAAU,QAAQ,KAAK;EAErC,MAAM,QAAyB;GAC7B;GACA,MAAM,KAAK;GACX;GACA,OAAO,KAAK;GACZ,UAAU,KAAK,YAAY,UAAU;EACvC;EAEA,MAAM,KAAK,MAAM,IAAI,KAAK,OAAO,EAAE,OAAO,CAAC;CAC7C;;;;;;;;;;;;CAaA,MAAa,OACX,OACA,GACA,WACA,OAC2B;EAC3B,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK;EAElD,MAAM,OAAO,MAAM,KAAK,MAAM,QAAyB,QAAQ;GAC7D,MAAM,KAAK,IAAI,IAAI,iBAAiB,CAAC;GACrC;EACF,CAAC;EAED,MAAM,SAAS,GAAG,KAAK,UAAU;EAEjC,OAAO,KACJ,QACE,QACC,IAAI,IAAI,WAAW,MAAM,KAAK,IAAI,OAAO,UAAU,KACvD,CAAC,CACA,KAAK,SAA2C;GAC/C,IAAI,IAAI,MAAM;GACd,MAAM,IAAI,MAAM;GAChB,MAAM;GACN,OAAO,KAAK,MAAM,IAAI,OAAO,IAAI,MAAM,IAAI;GAC3C,UAAU,IAAI,MAAM;EACtB,EAAE,CAAC,CACF,MAAM,OAAO,WAAW,OAAO,QAAQ,MAAM,KAAK,CAAC,CACnD,MAAM,GAAG,CAAC;CACf;;CAGA,MAAa,QAAuB;EAClC,MAAM,KAAK,MAAM,gBAAgB,KAAK,SAAS;CACjD;;;;;;;;CASA,AAAQ,MAAM,YAAoB,MAAsB;EACtD,MAAM,gBAAgB,QAAQ,OAAO;EAErC,QACG,IAAI,KAAK,uBAAuB,aACjC,KAAK,sBAAsB;CAE/B;;;;;;;;CASA,AAAQ,OAAO,IAAY,OAAwB;EACjD,OAAO,UAAU,SACb,GAAG,KAAK,UAAU,GAAG,OACrB,GAAG,KAAK,UAAU,GAAG,eAAe,KAAK,EAAE,GAAG;CACpD;AACF"}
|
|
@@ -2,6 +2,13 @@ import { deriveMemoryId } from "./derive-id.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region ../ai/src/memory/semantic-memory.ts
|
|
4
4
|
/**
|
|
5
|
+
* Extra candidates pulled from `similar()` on a SCOPED recall before the
|
|
6
|
+
* scope filter runs — the driver's ranking spans every scope in the
|
|
7
|
+
* index, so a bare top-`k` can come back entirely foreign. Mirrors the
|
|
8
|
+
* episodic / procedural tiers' overscan constant.
|
|
9
|
+
*/
|
|
10
|
+
const RECALL_OVERSCAN = 5;
|
|
11
|
+
/**
|
|
5
12
|
* Semantic recall tier (memory core M1).
|
|
6
13
|
*
|
|
7
14
|
* Owns: embedding remembered text, writing it to a `@warlock.js/cache`
|
|
@@ -36,30 +43,33 @@ var SemanticMemory = class {
|
|
|
36
43
|
const value = {
|
|
37
44
|
id,
|
|
38
45
|
text: item.text,
|
|
46
|
+
scope: item.scope,
|
|
39
47
|
metadata: item.metadata
|
|
40
48
|
};
|
|
41
|
-
await this.store.set(this.keyFor(id), value, { vector });
|
|
49
|
+
await this.store.set(this.keyFor(id, item.scope), value, { vector });
|
|
42
50
|
}
|
|
43
51
|
/**
|
|
44
52
|
* Embed `query`, ask the driver for the `k` nearest entries clearing
|
|
45
|
-
* `threshold`, and return those within this instance's namespace
|
|
46
|
-
* scored {@link RecalledMemory}. Hits indexed
|
|
47
|
-
* namespace (a shared driver)
|
|
53
|
+
* `threshold`, and return those within this instance's namespace AND
|
|
54
|
+
* this call's `scope` as scored {@link RecalledMemory}. Hits indexed
|
|
55
|
+
* under a different namespace (a shared driver) or a different scope
|
|
56
|
+
* (another tenant / session) are filtered out here, before the caller
|
|
57
|
+
* ever sees them — an unscoped recall reads only unscoped entries.
|
|
48
58
|
*/
|
|
49
|
-
async recall(query, k, threshold) {
|
|
59
|
+
async recall(query, k, threshold, scope) {
|
|
50
60
|
const { vector } = await this.embedder.embed(query);
|
|
51
61
|
const hits = await this.store.similar(vector, {
|
|
52
|
-
topK: k,
|
|
62
|
+
topK: scope === void 0 ? k : Math.max(k * RECALL_OVERSCAN, k),
|
|
53
63
|
threshold
|
|
54
64
|
});
|
|
55
65
|
const prefix = `${this.namespace}.`;
|
|
56
|
-
return hits.filter((hit) => hit.key.startsWith(prefix)).map((hit) => ({
|
|
66
|
+
return hits.filter((hit) => hit.key.startsWith(prefix) && hit.value?.scope === scope).map((hit) => ({
|
|
57
67
|
id: hit.value.id,
|
|
58
68
|
text: hit.value.text,
|
|
59
69
|
tier: "semantic",
|
|
60
70
|
score: hit.score,
|
|
61
71
|
metadata: hit.value.metadata
|
|
62
|
-
}));
|
|
72
|
+
})).slice(0, Math.max(0, k));
|
|
63
73
|
}
|
|
64
74
|
/** Drop every semantic entry written under this instance's namespace. */
|
|
65
75
|
async clear() {
|
|
@@ -69,9 +79,16 @@ var SemanticMemory = class {
|
|
|
69
79
|
* Namespaced key for an entry. The cache's `parseKey` normalizes `:`
|
|
70
80
|
* to `.`, so a dot separator keeps the prefix used here aligned with
|
|
71
81
|
* the `hit.key` the driver returns from `similar()`.
|
|
82
|
+
*
|
|
83
|
+
* A scoped entry gets an extra hashed segment so two scopes writing
|
|
84
|
+
* identical text (same derived id) don't overwrite each other; the
|
|
85
|
+
* unscoped key shape is unchanged, so entries written before 4.15.0
|
|
86
|
+
* still resolve. The hash is a write-separation device only — recall
|
|
87
|
+
* authorization is the exact `value.scope` equality check, so even a
|
|
88
|
+
* hash collision cannot widen what a scope can read.
|
|
72
89
|
*/
|
|
73
|
-
keyFor(id) {
|
|
74
|
-
return `${this.namespace}.${id}`;
|
|
90
|
+
keyFor(id, scope) {
|
|
91
|
+
return scope === void 0 ? `${this.namespace}.${id}` : `${this.namespace}.${deriveMemoryId(scope)}.${id}`;
|
|
75
92
|
}
|
|
76
93
|
};
|
|
77
94
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/semantic-memory.ts"],"sourcesContent":["import type { CacheDriver, CacheSimilarHit } from \"@warlock.js/cache\";\nimport type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * Shape persisted per semantic memory in the cache driver. The vector\n * itself is stored by the driver's own index (passed via\n * `set({ vector })`), so it is not duplicated in the value.\n */\ntype StoredMemory = {\n id: string;\n text: string;\n metadata?: Record<string, unknown>;\n};\n\n/**\n * Semantic recall tier (memory core M1).\n *\n * Owns: embedding remembered text, writing it to a `@warlock.js/cache`\n * driver with `set({ vector })`, and retrieving by cosine similarity via\n * the driver's `similar()`. Does NOT own: the similarity algorithm or\n * the ANN index — those belong to the cache driver. This mirrors the\n * delegation model of `middleware/builtins/semantic-cache.ts`: memory is\n * embedding-agnostic and store-agnostic, gluing an {@link EmbedderContract}\n * to a {@link CacheDriver}.\n *\n * The driver may be shared across memory instances, so every key carries\n * the configured `namespace` and recall filters hits to that prefix —\n * foreign entries indexed by another instance never leak into a query.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class SemanticMemory {\n public constructor(\n private readonly embedder: EmbedderContract,\n private readonly store: CacheDriver<any, any>,\n private readonly namespace: string,\n ) {}\n\n /**\n * Embed the item's text and index it under a namespaced, id-derived\n * key. Re-remembering the same id overwrites the prior vector +\n * value (the driver upserts by key).\n */\n public async remember(item: MemoryItem): Promise<void> {\n const id = item.id ?? deriveMemoryId(item.text);\n const { vector } = await this.embedder.embed(item.text);\n\n const value: StoredMemory = {\n id,\n text: item.text,\n metadata: item.metadata,\n };\n\n await this.store.set(this.keyFor(id), value, { vector });\n }\n\n /**\n * Embed `query`, ask the driver for the `k` nearest entries clearing\n * `threshold`, and return those within this instance's namespace
|
|
1
|
+
{"version":3,"file":"semantic-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/semantic-memory.ts"],"sourcesContent":["import type { CacheDriver, CacheSimilarHit } from \"@warlock.js/cache\";\nimport type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * Extra candidates pulled from `similar()` on a SCOPED recall before the\n * scope filter runs — the driver's ranking spans every scope in the\n * index, so a bare top-`k` can come back entirely foreign. Mirrors the\n * episodic / procedural tiers' overscan constant.\n */\nconst RECALL_OVERSCAN = 5;\n\n/**\n * Shape persisted per semantic memory in the cache driver. The vector\n * itself is stored by the driver's own index (passed via\n * `set({ vector })`), so it is not duplicated in the value.\n */\ntype StoredMemory = {\n id: string;\n text: string;\n /** Isolation key the entry was written under; absent = the shared pool. */\n scope?: string;\n metadata?: Record<string, unknown>;\n};\n\n/**\n * Semantic recall tier (memory core M1).\n *\n * Owns: embedding remembered text, writing it to a `@warlock.js/cache`\n * driver with `set({ vector })`, and retrieving by cosine similarity via\n * the driver's `similar()`. Does NOT own: the similarity algorithm or\n * the ANN index — those belong to the cache driver. This mirrors the\n * delegation model of `middleware/builtins/semantic-cache.ts`: memory is\n * embedding-agnostic and store-agnostic, gluing an {@link EmbedderContract}\n * to a {@link CacheDriver}.\n *\n * The driver may be shared across memory instances, so every key carries\n * the configured `namespace` and recall filters hits to that prefix —\n * foreign entries indexed by another instance never leak into a query.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class SemanticMemory {\n public constructor(\n private readonly embedder: EmbedderContract,\n private readonly store: CacheDriver<any, any>,\n private readonly namespace: string,\n ) {}\n\n /**\n * Embed the item's text and index it under a namespaced, id-derived\n * key. Re-remembering the same id overwrites the prior vector +\n * value (the driver upserts by key).\n */\n public async remember(item: MemoryItem): Promise<void> {\n const id = item.id ?? deriveMemoryId(item.text);\n const { vector } = await this.embedder.embed(item.text);\n\n const value: StoredMemory = {\n id,\n text: item.text,\n scope: item.scope,\n metadata: item.metadata,\n };\n\n await this.store.set(this.keyFor(id, item.scope), value, { vector });\n }\n\n /**\n * Embed `query`, ask the driver for the `k` nearest entries clearing\n * `threshold`, and return those within this instance's namespace AND\n * this call's `scope` as scored {@link RecalledMemory}. Hits indexed\n * under a different namespace (a shared driver) or a different scope\n * (another tenant / session) are filtered out here, before the caller\n * ever sees them — an unscoped recall reads only unscoped entries.\n */\n public async recall(\n query: string,\n k: number,\n threshold: number,\n scope?: string,\n ): Promise<RecalledMemory[]> {\n const { vector } = await this.embedder.embed(query);\n\n // A scoped recall overscans: the driver ranks across every scope in\n // the index, so a plain top-`k` could be filled entirely by foreign\n // scopes and starve this one. Pull extra candidates, filter, then cap.\n const hits = await this.store.similar<StoredMemory>(vector, {\n topK: scope === undefined ? k : Math.max(k * RECALL_OVERSCAN, k),\n threshold,\n });\n\n const prefix = `${this.namespace}.`;\n\n return hits\n .filter(\n (hit: CacheSimilarHit<StoredMemory>) =>\n hit.key.startsWith(prefix) && hit.value?.scope === scope,\n )\n .map((hit: CacheSimilarHit<StoredMemory>) => ({\n id: hit.value.id,\n text: hit.value.text,\n tier: \"semantic\" as const,\n score: hit.score,\n metadata: hit.value.metadata,\n }))\n .slice(0, Math.max(0, k));\n }\n\n /** Drop every semantic entry written under this instance's namespace. */\n public async clear(): Promise<void> {\n await this.store.removeNamespace(this.namespace);\n }\n\n /**\n * Namespaced key for an entry. The cache's `parseKey` normalizes `:`\n * to `.`, so a dot separator keeps the prefix used here aligned with\n * the `hit.key` the driver returns from `similar()`.\n *\n * A scoped entry gets an extra hashed segment so two scopes writing\n * identical text (same derived id) don't overwrite each other; the\n * unscoped key shape is unchanged, so entries written before 4.15.0\n * still resolve. The hash is a write-separation device only — recall\n * authorization is the exact `value.scope` equality check, so even a\n * hash collision cannot widen what a scope can read.\n */\n private keyFor(id: string, scope?: string): string {\n return scope === undefined\n ? `${this.namespace}.${id}`\n : `${this.namespace}.${deriveMemoryId(scope)}.${id}`;\n }\n}\n"],"mappings":";;;;;;;;;AAcA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;AAiCxB,IAAa,iBAAb,MAA4B;CAC1B,AAAO,YACL,AAAiB,UACjB,AAAiB,OACjB,AAAiB,WACjB;EAHiB;EACA;EACA;CAChB;;;;;;CAOH,MAAa,SAAS,MAAiC;EACrD,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAC9C,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,IAAI;EAEtD,MAAM,QAAsB;GAC1B;GACA,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,UAAU,KAAK;EACjB;EAEA,MAAM,KAAK,MAAM,IAAI,KAAK,OAAO,IAAI,KAAK,KAAK,GAAG,OAAO,EAAE,OAAO,CAAC;CACrE;;;;;;;;;CAUA,MAAa,OACX,OACA,GACA,WACA,OAC2B;EAC3B,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK;EAKlD,MAAM,OAAO,MAAM,KAAK,MAAM,QAAsB,QAAQ;GAC1D,MAAM,UAAU,SAAY,IAAI,KAAK,IAAI,IAAI,iBAAiB,CAAC;GAC/D;EACF,CAAC;EAED,MAAM,SAAS,GAAG,KAAK,UAAU;EAEjC,OAAO,KACJ,QACE,QACC,IAAI,IAAI,WAAW,MAAM,KAAK,IAAI,OAAO,UAAU,KACvD,CAAC,CACA,KAAK,SAAwC;GAC5C,IAAI,IAAI,MAAM;GACd,MAAM,IAAI,MAAM;GAChB,MAAM;GACN,OAAO,IAAI;GACX,UAAU,IAAI,MAAM;EACtB,EAAE,CAAC,CACF,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;CAC5B;;CAGA,MAAa,QAAuB;EAClC,MAAM,KAAK,MAAM,gBAAgB,KAAK,SAAS;CACjD;;;;;;;;;;;;;CAcA,AAAQ,OAAO,IAAY,OAAwB;EACjD,OAAO,UAAU,SACb,GAAG,KAAK,UAAU,GAAG,OACrB,GAAG,KAAK,UAAU,GAAG,eAAe,KAAK,EAAE,GAAG;CACpD;AACF"}
|
|
@@ -15,47 +15,104 @@ import { deriveMemoryId } from "./derive-id.mjs";
|
|
|
15
15
|
* first, each scored on a `[0, 1]` recency proxy so a caller can merge
|
|
16
16
|
* working hits with semantic hits and sort on one `score` field.
|
|
17
17
|
*
|
|
18
|
+
* **Bounded (4.15.0).** The buffer holds at most `maxItems` entries
|
|
19
|
+
* across every scope; the oldest-written entry is evicted on overflow
|
|
20
|
+
* (FIFO). The tier lives in process memory for the lifetime of the
|
|
21
|
+
* `memory()` instance — which the orchestrator resolves once and reuses
|
|
22
|
+
* for every session — so an unbounded buffer was a memory-exhaustion
|
|
23
|
+
* vector for any long-lived, internet-reachable deployment.
|
|
24
|
+
*
|
|
18
25
|
* Internal to the `memory()` factory — never exported on the package
|
|
19
26
|
* surface.
|
|
20
27
|
*/
|
|
21
28
|
var WorkingMemory = class {
|
|
22
|
-
constructor() {
|
|
29
|
+
constructor(maxItems) {
|
|
23
30
|
this.entries = /* @__PURE__ */ new Map();
|
|
31
|
+
this.maxItems = maxItems;
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* Append an item to the buffer (or overwrite the entry sharing its
|
|
27
|
-
* id). Re-inserting an existing
|
|
28
|
-
* + set would move it to the end and lie
|
|
29
|
-
* is updated in place.
|
|
35
|
+
* id *within the same scope*). Re-inserting an existing key keeps its
|
|
36
|
+
* original position; delete + set would move it to the end and lie
|
|
37
|
+
* about recency, so the value is updated in place.
|
|
38
|
+
*
|
|
39
|
+
* Overflowing `maxItems` evicts from the front — see
|
|
40
|
+
* {@link evictOverflow}.
|
|
30
41
|
*/
|
|
31
42
|
remember(item) {
|
|
32
43
|
const id = item.id ?? deriveMemoryId(item.text);
|
|
33
|
-
this.entries.set(id, {
|
|
44
|
+
this.entries.set(scopedKey(item.scope, id), {
|
|
45
|
+
id,
|
|
34
46
|
text: item.text,
|
|
47
|
+
scope: item.scope,
|
|
35
48
|
metadata: item.metadata
|
|
36
49
|
});
|
|
50
|
+
this.evictOverflow();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Enforce the size bound by dropping oldest-written entries first
|
|
54
|
+
* (FIFO over the `Map`'s insertion order).
|
|
55
|
+
*
|
|
56
|
+
* **Why FIFO, not LRU.** Recall here is a pure recency proxy — it
|
|
57
|
+
* reverses insertion order and slices the newest `k` — and never
|
|
58
|
+
* reorders anything, so the front of the buffer is by construction the
|
|
59
|
+
* region recall reaches last. FIFO therefore evicts exactly the
|
|
60
|
+
* entries a bounded recall would never have returned. True LRU would
|
|
61
|
+
* need read-time reordering, which would also rewrite the `score`
|
|
62
|
+
* every recall reports (a re-read entry would masquerade as freshly
|
|
63
|
+
* remembered), trading a real correctness property for no gain.
|
|
64
|
+
*
|
|
65
|
+
* **Known limitation (documented, not a regression).** The bound is
|
|
66
|
+
* global, not per-scope: a session writing heavily can push another
|
|
67
|
+
* session's older entries out of the buffer. That is a recall-quality
|
|
68
|
+
* degradation on a volatile scratch tier, never a disclosure — the
|
|
69
|
+
* scope filter in {@link recall} still applies — and a per-scope quota
|
|
70
|
+
* would not help anyway, since an attacker holding many sessions
|
|
71
|
+
* evicts through the global bound regardless. Durable recall belongs
|
|
72
|
+
* in the semantic / episodic tiers.
|
|
73
|
+
*/
|
|
74
|
+
evictOverflow() {
|
|
75
|
+
while (this.entries.size > this.maxItems) {
|
|
76
|
+
const oldest = this.entries.keys().next();
|
|
77
|
+
if (oldest.done) return;
|
|
78
|
+
this.entries.delete(oldest.value);
|
|
79
|
+
}
|
|
37
80
|
}
|
|
38
81
|
/**
|
|
39
|
-
* Return up to `k` most-recently-remembered items
|
|
40
|
-
*
|
|
41
|
-
*
|
|
82
|
+
* Return up to `k` most-recently-remembered items *within `scope`*,
|
|
83
|
+
* newest first. The scope match is exact equality (an unscoped recall
|
|
84
|
+
* sees only unscoped entries) and is applied BEFORE the slice, so a
|
|
85
|
+
* foreign scope's entries can never consume a slot or leak out.
|
|
86
|
+
*
|
|
87
|
+
* The `score` is a linear recency proxy: the newest item scores `1`,
|
|
88
|
+
* the oldest of the returned slice trends toward `0`. Working memory
|
|
42
89
|
* ignores any similarity threshold — it has no vector to compare.
|
|
43
90
|
*/
|
|
44
|
-
recall(k) {
|
|
45
|
-
const slice = [...this.entries.
|
|
46
|
-
return slice.map((
|
|
47
|
-
id,
|
|
91
|
+
recall(k, scope) {
|
|
92
|
+
const slice = [...this.entries.values()].reverse().filter((entry) => entry.scope === scope).slice(0, Math.max(0, k));
|
|
93
|
+
return slice.map((entry, index) => ({
|
|
94
|
+
id: entry.id,
|
|
48
95
|
text: entry.text,
|
|
49
96
|
tier: "working",
|
|
50
97
|
score: slice.length <= 1 ? 1 : 1 - index / slice.length,
|
|
51
98
|
metadata: entry.metadata
|
|
52
99
|
}));
|
|
53
100
|
}
|
|
54
|
-
/** Drop every working-tier entry. */
|
|
101
|
+
/** Drop every working-tier entry, across every scope. */
|
|
55
102
|
clear() {
|
|
56
103
|
this.entries.clear();
|
|
57
104
|
}
|
|
58
105
|
};
|
|
106
|
+
/**
|
|
107
|
+
* Map key for a buffer entry: the isolation `scope` (empty for the
|
|
108
|
+
* unscoped pool) length-prefixed and joined to the logical id. The
|
|
109
|
+
* length prefix makes the encoding injective — no crafted scope/id pair
|
|
110
|
+
* can collide with a different scope's entry the way a plain `:` join
|
|
111
|
+
* would allow.
|
|
112
|
+
*/
|
|
113
|
+
function scopedKey(scope, id) {
|
|
114
|
+
return `${scope?.length ?? 0}:${scope ?? ""}:${id}`;
|
|
115
|
+
}
|
|
59
116
|
|
|
60
117
|
//#endregion
|
|
61
118
|
export { WorkingMemory };
|