@remind_ai/remind 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +127 -0
  3. package/dist/src/brain/consolidation.d.ts +3 -0
  4. package/dist/src/brain/consolidation.js +153 -0
  5. package/dist/src/brain/constants.d.ts +18 -0
  6. package/dist/src/brain/constants.js +26 -0
  7. package/dist/src/brain/encoding.d.ts +10 -0
  8. package/dist/src/brain/encoding.js +45 -0
  9. package/dist/src/brain/forgetting.d.ts +3 -0
  10. package/dist/src/brain/forgetting.js +48 -0
  11. package/dist/src/brain/index.d.ts +6 -0
  12. package/dist/src/brain/index.js +13 -0
  13. package/dist/src/brain/llm-deps.d.ts +8 -0
  14. package/dist/src/brain/llm-deps.js +4 -0
  15. package/dist/src/brain/selftest.d.ts +1 -0
  16. package/dist/src/brain/selftest.js +229 -0
  17. package/dist/src/brain/similarity.d.ts +10 -0
  18. package/dist/src/brain/similarity.js +65 -0
  19. package/dist/src/brain/temporal.d.ts +9 -0
  20. package/dist/src/brain/temporal.js +65 -0
  21. package/dist/src/brain/tiering.d.ts +9 -0
  22. package/dist/src/brain/tiering.js +39 -0
  23. package/dist/src/config.d.ts +36 -0
  24. package/dist/src/config.js +61 -0
  25. package/dist/src/core/index.d.ts +12 -0
  26. package/dist/src/core/index.js +31 -0
  27. package/dist/src/core/ingest.d.ts +2 -0
  28. package/dist/src/core/ingest.js +117 -0
  29. package/dist/src/core/memory-types.d.ts +45 -0
  30. package/dist/src/core/memory-types.js +104 -0
  31. package/dist/src/core/retrieval.d.ts +2 -0
  32. package/dist/src/core/retrieval.js +55 -0
  33. package/dist/src/core/stores/doc.store.d.ts +2 -0
  34. package/dist/src/core/stores/doc.store.js +74 -0
  35. package/dist/src/core/stores/graph.store.d.ts +2 -0
  36. package/dist/src/core/stores/graph.store.js +61 -0
  37. package/dist/src/core/stores/vector.store.d.ts +2 -0
  38. package/dist/src/core/stores/vector.store.js +87 -0
  39. package/dist/src/engine.d.ts +33 -0
  40. package/dist/src/engine.js +63 -0
  41. package/dist/src/ids.d.ts +1 -0
  42. package/dist/src/ids.js +6 -0
  43. package/dist/src/index.d.ts +19 -0
  44. package/dist/src/index.js +25 -0
  45. package/dist/src/llm.d.ts +8 -0
  46. package/dist/src/llm.js +68 -0
  47. package/dist/src/memguard/auditor.d.ts +19 -0
  48. package/dist/src/memguard/auditor.js +48 -0
  49. package/dist/src/memguard/canary.d.ts +49 -0
  50. package/dist/src/memguard/canary.js +73 -0
  51. package/dist/src/memguard/detectors/exfiltration.d.ts +3 -0
  52. package/dist/src/memguard/detectors/exfiltration.js +18 -0
  53. package/dist/src/memguard/detectors/pii.d.ts +12 -0
  54. package/dist/src/memguard/detectors/pii.js +32 -0
  55. package/dist/src/memguard/detectors/secrets.d.ts +9 -0
  56. package/dist/src/memguard/detectors/secrets.js +25 -0
  57. package/dist/src/memguard/firewall.d.ts +15 -0
  58. package/dist/src/memguard/firewall.js +93 -0
  59. package/dist/src/memguard/index.d.ts +26 -0
  60. package/dist/src/memguard/index.js +24 -0
  61. package/dist/src/memguard/output-filter.d.ts +6 -0
  62. package/dist/src/memguard/output-filter.js +10 -0
  63. package/dist/src/memguard/provenance.d.ts +10 -0
  64. package/dist/src/memguard/provenance.js +36 -0
  65. package/dist/src/memguard/trust.d.ts +27 -0
  66. package/dist/src/memguard/trust.js +53 -0
  67. package/dist/src/queue/connection.d.ts +6 -0
  68. package/dist/src/queue/connection.js +23 -0
  69. package/dist/src/queue/queues.d.ts +29 -0
  70. package/dist/src/queue/queues.js +64 -0
  71. package/dist/src/types.d.ts +97 -0
  72. package/dist/src/types.js +3 -0
  73. package/dist/src/workers/consolidate.worker.d.ts +5 -0
  74. package/dist/src/workers/consolidate.worker.js +15 -0
  75. package/dist/src/workers/forget.worker.d.ts +4 -0
  76. package/dist/src/workers/forget.worker.js +8 -0
  77. package/dist/src/workers/ingest.worker.d.ts +6 -0
  78. package/dist/src/workers/ingest.worker.js +36 -0
  79. package/package.json +46 -0
package/LICENSE ADDED
@@ -0,0 +1,6 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 REMind authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy...
6
+ (SKELETON — fill in full MIT text before publishing.)
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # @remind_ai/remind
2
+
3
+ **Brain-inspired memory for AI agents** — long-term memory that *consolidates* like a brain, with a built-in security layer (**MemGuard**) that stops memory-poisoning attacks. Backed by Azure.
4
+
5
+ ## Why REMind?
6
+
7
+ Most agent "memory" is just a vector store. REMind adds the two things production agents actually need:
8
+
9
+ - 🧠 **A brain, not a bucket.** A *sleep cycle* consolidates raw episodic memories into durable beliefs — de-duplicating, merging, and superseding stale facts over time.
10
+ - 🛡️ **MemGuard security.** Every write is firewalled and every belief promotion is audited, so an attacker can't quietly poison what your agent "knows."
11
+
12
+ | Layer | Role |
13
+ |-------|------|
14
+ | **Foundation** | Stores memories across facts (document), vectors (semantic), and a knowledge graph |
15
+ | **Brain** | Consolidation + forgetting — turns episodes into beliefs |
16
+ | **MemGuard** | Ingestion firewall, belief-promotion auditor, and canary drift detection |
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install @remind_ai/remind
22
+ ```
23
+
24
+ Requires **Node 20+**.
25
+
26
+ ## Prerequisites
27
+
28
+ REMind is a server-side SDK backed by your own Azure resources:
29
+
30
+ - **Azure OpenAI** — one chat deployment + one embeddings deployment
31
+ - **Azure AI Search** — vector store
32
+ - **Azure Cosmos DB for Apache Gremlin** — knowledge graph
33
+ - **Azure Cosmos DB for MongoDB** — facts / records
34
+ - *(optional)* **Azure Cache for Redis** — async, crash-safe queue mode
35
+
36
+ ## Configuration
37
+
38
+ REMind reads configuration from environment variables. Create a `.env` in your project:
39
+
40
+ ```dotenv
41
+ AZURE_OPENAI_ENDPOINT=https://<resource>.openai.azure.com/
42
+ AZURE_OPENAI_API_KEY=<key>
43
+ AZURE_OPENAI_API_VERSION=2024-10-21
44
+ AZURE_OPENAI_CHAT_DEPLOYMENT=<your chat deployment name>
45
+ AZURE_OPENAI_EMBED_DEPLOYMENT=<your embeddings deployment name>
46
+ AZURE_OPENAI_EMBED_DIMENSIONS=3072
47
+
48
+ AZURE_SEARCH_ENDPOINT=https://<service>.search.windows.net
49
+ AZURE_SEARCH_API_KEY=<key>
50
+ AZURE_SEARCH_INDEX=remind-memory
51
+
52
+ COSMOS_GREMLIN_ENDPOINT=wss://<account>.gremlin.cosmos.azure.com:443/
53
+ COSMOS_GREMLIN_KEY=<key>
54
+ COSMOS_GREMLIN_DB=remind
55
+ COSMOS_GREMLIN_GRAPH=memory
56
+
57
+ COSMOS_MONGO_URI=mongodb+srv://<user>:<url-encoded-password>@<cluster>/
58
+ COSMOS_MONGO_DB=remind
59
+
60
+ # Leave REDIS_URL empty and SYNC=1 to run inline (no Redis).
61
+ REDIS_URL=
62
+ SYNC=1
63
+ ```
64
+
65
+ > **Tip:** `AZURE_OPENAI_CHAT_DEPLOYMENT` must be the **deployment name** from Azure AI Foundry → Deployments, not the model name. URL-encode any special characters in the Mongo password.
66
+
67
+ ## Quickstart
68
+
69
+ ```ts
70
+ import { createMemory } from '@remind_ai/remind';
71
+
72
+ const mem = createMemory(); // Foundation + Brain + MemGuard, wired and ready
73
+
74
+ // 1. Remember an exchange (firewalled, then stored)
75
+ await mem.addToMemory({
76
+ agentId: 'user-42',
77
+ messages: { user: 'Our recommended vendor is ACME.', assistant: 'Noted.' },
78
+ });
79
+
80
+ // 2. Consolidate — the "sleep cycle" turns episodes into durable beliefs
81
+ const report = await mem.sleep('user-42');
82
+ console.log(`${report.promoted.length} beliefs formed, ${report.merged} merged`);
83
+
84
+ // 3. Retrieve a fused context for your next prompt
85
+ const { context } = await mem.fetchMemory('user-42', 'Which vendor do we use?');
86
+ console.log(context);
87
+
88
+ await mem.close();
89
+ ```
90
+
91
+ ## API
92
+
93
+ `createMemory(options?)` returns a `MemoryAPI`:
94
+
95
+ | Method | Description |
96
+ |--------|-------------|
97
+ | `addToMemory({ agentId, messages, source?, sourceId? })` | Ingest a `{ user, assistant }` exchange — firewalled by MemGuard, then routed to the right store. |
98
+ | `fetchMemory(agentId, query, sourceId?)` → `{ context, records, facts }` | Retrieve a ranked, fused context for a prompt. |
99
+ | `sleep(agentId)` → `ConsolidationReport` | Run a consolidation cycle (`{ promoted, blocked, merged }`). |
100
+ | `forget(agentId)` | Run a decay pass over stale memories. |
101
+ | `close()` | Release queue / Redis resources. |
102
+
103
+ `createMemory()` also accepts optional `core`, `brain`, `guard`, and `stores` overrides for advanced use and testing.
104
+
105
+ ## Inline vs. async
106
+
107
+ - **Inline (default):** `SYNC=1` with no `REDIS_URL` — writes and consolidation run in-process. Best for getting started.
108
+ - **Async / crash-safe:** set `REDIS_URL` and `SYNC=0` — the same calls run on **BullMQ over Azure Cache for Redis**, so jobs survive restarts and retry on failure. Identical API.
109
+
110
+ ## Security: MemGuard
111
+
112
+ On by default. MemGuard:
113
+
114
+ - **Firewalls ingestion** — scans for secrets, PII, exfiltration, and injection before anything is stored.
115
+ - **Audits belief promotion** — during `sleep()`, every candidate belief must pass the auditor before it becomes durable.
116
+ - **Watches canaries** — known-good beliefs are monitored for drift, so silent memory-poisoning is quarantined, not promoted.
117
+
118
+ To disable it (e.g., for benchmarking), inject a passthrough guard:
119
+
120
+ ```ts
121
+ import { createMemory, passthroughGuard } from '@remind_ai/remind';
122
+ const mem = createMemory({ guard: passthroughGuard });
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { ConsolidationReport, PromotionGate, Stores } from '../types.js';
2
+ import { type LlmDeps } from './llm-deps.js';
3
+ export declare function consolidate(agentId: string, stores: Stores, gate: PromotionGate, llm?: LlmDeps): Promise<ConsolidationReport>;
@@ -0,0 +1,153 @@
1
+ // REMind · L1 sleep cycle (heart of L1) · Owner: Charan
2
+ // consolidate(agentId, stores, gate): read recent episodic -> distill episodic->semantic gist -> merge near-dups -> reflect into insights.
3
+ // For each candidate belief: await gate(candidate, sources); persist only if promote===true (deterministic belief id). Idempotent / resumable.
4
+ import { idFor } from '../ids.js';
5
+ import { dedupe, scoreSalience } from './encoding.js';
6
+ import { embedAll, cluster } from './similarity.js';
7
+ import { supersede, currentlyValid } from './temporal.js';
8
+ import { defaultLlm } from './llm-deps.js';
9
+ import { MERGE_THRESHOLD, BASE_CONFIDENCE, CONFIDENCE_PER_SOURCE, SALIENCE_CONFIDENCE_GAIN, } from './constants.js';
10
+ const nowIso = () => new Date().toISOString();
11
+ const norm = (s) => s.trim().toLowerCase().replace(/\s+/g, ' ');
12
+ const clamp01 = (n) => Math.max(0, Math.min(1, n));
13
+ export async function consolidate(agentId, stores, gate, llm = defaultLlm) {
14
+ const report = { promoted: [], blocked: [], merged: 0 };
15
+ const cache = new Map(); // embeddings reused across the whole cycle
16
+ // 1. Fresh episodic memories are the raw material of a sleep cycle.
17
+ const episodic = (await stores.doc.getRecords(agentId, { type: 'episodic' })).filter((r) => r.tier === 'hot' && !r.quarantined);
18
+ if (!episodic.length)
19
+ return report;
20
+ // 2. Drop exact duplicates, then cluster what remains into belief candidates.
21
+ const { canonical } = await dedupe(episodic, cache, llm.embed);
22
+ const clusters = cluster(canonical, cache, MERGE_THRESHOLD);
23
+ report.merged = episodic.length - clusters.length;
24
+ // 3. Beliefs currently on the books (for supersession + corroboration), embedded once.
25
+ const existingBeliefs = currentlyValid(await stores.doc.getRecords(agentId, { type: 'factual' }));
26
+ await embedAll(existingBeliefs, cache, llm.embed);
27
+ // 4. Distill each cluster into a semantic gist, gate it, persist if promoted.
28
+ for (const group of clusters) {
29
+ try {
30
+ const { content, triples } = await distill(group, llm);
31
+ if (!content)
32
+ continue;
33
+ const salience = await scoreSalience(group[0], existingBeliefs, llm, cache);
34
+ const candidate = buildBelief(agentId, content, group, 'belief', salience, triples);
35
+ await offer(candidate, group, gate, stores, existingBeliefs, report, llm, cache);
36
+ }
37
+ catch (err) {
38
+ report.blocked.push({ record: placeholder(agentId, group), reason: `error: ${msg(err)}` });
39
+ }
40
+ }
41
+ // 5. Reflect across the new beliefs into higher-order insights.
42
+ const beliefSources = [...report.promoted];
43
+ try {
44
+ for (const text of await reflect(beliefSources, llm)) {
45
+ const candidate = buildBelief(agentId, text, beliefSources, 'insight');
46
+ await offer(candidate, beliefSources, gate, stores, existingBeliefs, report, llm, cache);
47
+ }
48
+ }
49
+ catch (err) {
50
+ report.blocked.push({ record: placeholder(agentId, beliefSources), reason: `error: ${msg(err)}` });
51
+ }
52
+ // 6. Demote consumed episodics so the next cycle only sees fresh memories.
53
+ for (const e of episodic) {
54
+ if (e.tier === 'hot')
55
+ await stores.doc.saveRecord({ ...e, tier: 'warm' });
56
+ }
57
+ return report;
58
+ }
59
+ async function offer(candidate, sources, gate, stores, existingBeliefs, report, llm, cache) {
60
+ const verdict = await gate(candidate, sources);
61
+ if (!verdict.promote) {
62
+ report.blocked.push({ record: candidate, reason: verdict.reason ?? 'blocked by gate' });
63
+ return;
64
+ }
65
+ await supersede(candidate, existingBeliefs, stores, llm, cache);
66
+ await persist(candidate, stores);
67
+ existingBeliefs.push(candidate);
68
+ report.promoted.push(candidate);
69
+ }
70
+ async function persist(record, stores) {
71
+ await stores.doc.saveRecord(record);
72
+ await stores.doc.pushFact(record.agentId, record.content);
73
+ await stores.vector.upsert(record);
74
+ const triples = record.triples;
75
+ if (triples && triples.length)
76
+ await stores.graph.writeTriples(record.agentId, triples);
77
+ }
78
+ function buildBelief(agentId, content, sources, kind = 'belief', salience = 0.5, triples = []) {
79
+ const trust = sources.length ? Math.min(...sources.map((s) => s.trust ?? 1)) : 1;
80
+ const confidence = clamp01(BASE_CONFIDENCE +
81
+ CONFIDENCE_PER_SOURCE * Math.max(0, sources.length - 1) +
82
+ SALIENCE_CONFIDENCE_GAIN * (salience - 0.5));
83
+ const record = {
84
+ id: idFor(agentId, kind, norm(content)),
85
+ agentId,
86
+ type: 'factual',
87
+ content,
88
+ trust,
89
+ confidence,
90
+ provenance: { source: 'consolidation', derivedFrom: sources.map((s) => s.id) },
91
+ tier: 'hot',
92
+ validFrom: nowIso(),
93
+ createdAt: nowIso(),
94
+ };
95
+ if (triples.length)
96
+ record.triples = triples;
97
+ return record;
98
+ }
99
+ /** Distill a cluster into one belief sentence + its knowledge-graph triples (single LLM call). */
100
+ async function distill(group, llm) {
101
+ const bullets = group.map((r) => `- ${r.content}`).join('\n');
102
+ const raw = await llm.complete(`Distill these related episodic memories into ONE durable semantic belief about the user, ` +
103
+ `plus its knowledge-graph triples. Return ONLY JSON, no prose, no markdown:\n` +
104
+ `{"belief":"<one concise sentence>","triples":[{"subject":"User","relation":"...","object":"..."}]}\n` +
105
+ `Memories:\n${bullets}`, { temperature: 0 });
106
+ const parsed = parseJson(raw);
107
+ if (parsed && typeof parsed.belief === 'string' && parsed.belief.trim()) {
108
+ return { content: parsed.belief.trim(), triples: cleanTriples(parsed.triples) };
109
+ }
110
+ // Fallback: treat the whole response as the belief sentence.
111
+ return { content: raw.trim(), triples: [] };
112
+ }
113
+ async function reflect(beliefs, llm) {
114
+ if (beliefs.length < 2)
115
+ return [];
116
+ const bullets = beliefs.map((b) => `- ${b.content}`).join('\n');
117
+ const raw = await llm.complete(`From these beliefs, infer up to 3 higher-order insights about the user. ` +
118
+ `Return ONLY a JSON array of strings, no prose, no markdown.\n${bullets}`, { temperature: 0 });
119
+ const parsed = parseJson(raw);
120
+ if (Array.isArray(parsed)) {
121
+ return parsed.filter((s) => typeof s === 'string' && s.trim()).slice(0, 3);
122
+ }
123
+ return [];
124
+ }
125
+ function parseJson(raw) {
126
+ try {
127
+ return JSON.parse(raw.replace(/^```(?:json)?/i, '').replace(/```$/, '').trim());
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ function cleanTriples(value) {
134
+ if (!Array.isArray(value))
135
+ return [];
136
+ return value.filter((t) => !!t && typeof t.subject === 'string' && typeof t.relation === 'string' && typeof t.object === 'string');
137
+ }
138
+ function placeholder(agentId, sources) {
139
+ return {
140
+ id: idFor(agentId, 'belief-error', sources.map((s) => s.id).join('|') || nowIso()),
141
+ agentId,
142
+ type: 'factual',
143
+ content: '',
144
+ trust: 0,
145
+ confidence: 0,
146
+ provenance: { source: 'consolidation', derivedFrom: sources.map((s) => s.id) },
147
+ tier: 'cold',
148
+ createdAt: nowIso(),
149
+ };
150
+ }
151
+ function msg(err) {
152
+ return err instanceof Error ? err.message : String(err);
153
+ }
@@ -0,0 +1,18 @@
1
+ import 'dotenv/config';
2
+ export declare const DAY_MS: number;
3
+ export declare const MERGE_THRESHOLD = 0.85;
4
+ export declare const DEDUP_THRESHOLD = 0.9;
5
+ export declare const SALIENCE_WEIGHTS: {
6
+ readonly novelty: 0.7;
7
+ readonly surprise: 0.3;
8
+ };
9
+ export declare const USE_LLM_SURPRISE = true;
10
+ export declare const SUPERSEDE_SIM_THRESHOLD = 0.6;
11
+ export declare const BASE_CONFIDENCE = 0.5;
12
+ export declare const CONFIDENCE_PER_SOURCE = 0.1;
13
+ export declare const SALIENCE_CONFIDENCE_GAIN = 0.2;
14
+ export declare const RECENCY_TAU_MS: number;
15
+ export declare const STABILITY_BASE_MS: number;
16
+ export declare const FORGET_R = 0.3;
17
+ export declare const HOT_DAYS: number;
18
+ export declare const WARM_DAYS: number;
@@ -0,0 +1,26 @@
1
+ // REMind · L1 tunables · Owner: Charan
2
+ // Single home for every Brain threshold/weight so the demo can be tuned fast.
3
+ import 'dotenv/config';
4
+ export const DAY_MS = 24 * 60 * 60 * 1000;
5
+ const MIN_MS = 60 * 1000;
6
+ // BRAIN_FAST_DECAY=1 compresses the time-scales so decay + tiering are visible in a live demo.
7
+ const FAST = process.env.BRAIN_FAST_DECAY === '1';
8
+ // --- clustering / dedup (cosine similarity) ---
9
+ export const MERGE_THRESHOLD = 0.85; // episodic memories this close fold into one belief
10
+ export const DEDUP_THRESHOLD = 0.9; // near-identical content treated as a duplicate
11
+ // --- salience (encoding) ---
12
+ export const SALIENCE_WEIGHTS = { novelty: 0.7, surprise: 0.3 };
13
+ export const USE_LLM_SURPRISE = true; // toggle the (paid) surprise/contradiction probe
14
+ // --- supersession (temporal) ---
15
+ export const SUPERSEDE_SIM_THRESHOLD = 0.6; // only LLM-check contradiction above this cosine
16
+ // --- belief confidence (consolidation) ---
17
+ export const BASE_CONFIDENCE = 0.5; // a single-source belief
18
+ export const CONFIDENCE_PER_SOURCE = 0.1; // + per extra corroborating source
19
+ export const SALIENCE_CONFIDENCE_GAIN = 0.2; // how much salience nudges confidence
20
+ // --- forgetting (Ebbinghaus + utility) ---
21
+ export const RECENCY_TAU_MS = FAST ? 5 * MIN_MS : 7 * DAY_MS; // recency decay constant
22
+ export const STABILITY_BASE_MS = FAST ? 2 * MIN_MS : 3 * DAY_MS; // base memory stability S0
23
+ export const FORGET_R = 0.3; // retention/utility below this ⇒ archive to cold
24
+ // --- tiering (access recency) ---
25
+ export const HOT_DAYS = FAST ? 1 / 1440 : 1; // 1 min when FAST
26
+ export const WARM_DAYS = FAST ? 10 / 1440 : 14; // 10 min when FAST
@@ -0,0 +1,10 @@
1
+ import type { MemoryRecord } from '../types.js';
2
+ import { type Vec } from './similarity.js';
3
+ import { type LlmDeps } from './llm-deps.js';
4
+ /** 0..1 importance of `record` relative to what we already know. */
5
+ export declare function scoreSalience(record: MemoryRecord, context: MemoryRecord[], llm?: LlmDeps, cache?: Map<string, Vec>): Promise<number>;
6
+ /** Collapse near-identical records, keeping the earliest as canonical. */
7
+ export declare function dedupe(records: MemoryRecord[], cache?: Map<string, Vec>, embedFn?: (text: string) => Promise<Vec>): Promise<{
8
+ canonical: MemoryRecord[];
9
+ duplicates: MemoryRecord[];
10
+ }>;
@@ -0,0 +1,45 @@
1
+ // REMind · L1 encoding · Owner: Charan
2
+ // scoreSalience(record, context) via surprise / novelty / prediction-error; content deduplication at ingest.
3
+ import { SALIENCE_WEIGHTS, DEDUP_THRESHOLD, USE_LLM_SURPRISE } from './constants.js';
4
+ import { embedAll, cosine, cluster } from './similarity.js';
5
+ import { defaultLlm } from './llm-deps.js';
6
+ const keyOf = (r) => r.id || r.content;
7
+ const clamp01 = (n) => Math.max(0, Math.min(1, n));
8
+ const ts = (s) => (s ? Date.parse(s) : 0);
9
+ /** 0..1 importance of `record` relative to what we already know. */
10
+ export async function scoreSalience(record, context, llm = defaultLlm, cache = new Map()) {
11
+ const vecs = await embedAll([record, ...context], cache, llm.embed);
12
+ const rv = vecs.get(keyOf(record));
13
+ if (!rv)
14
+ return 0.5;
15
+ let maxSim = 0;
16
+ for (const c of context) {
17
+ const cv = vecs.get(keyOf(c));
18
+ if (cv)
19
+ maxSim = Math.max(maxSim, cosine(rv, cv));
20
+ }
21
+ const novelty = 1 - maxSim;
22
+ let surprise = novelty;
23
+ if (USE_LLM_SURPRISE && context.length) {
24
+ const priors = context.slice(0, 5).map((c) => `- ${c.content}`).join('\n');
25
+ const ans = await llm.classify(`Prior beliefs:\n${priors}\n\nNew statement: "${record.content}"\n` +
26
+ `Is the new statement surprising or contradictory versus the priors? Answer 'yes' or 'no'.`);
27
+ surprise = ans.startsWith('yes') ? 1 : 0;
28
+ }
29
+ return clamp01(SALIENCE_WEIGHTS.novelty * novelty + SALIENCE_WEIGHTS.surprise * surprise);
30
+ }
31
+ /** Collapse near-identical records, keeping the earliest as canonical. */
32
+ export async function dedupe(records, cache = new Map(), embedFn = defaultLlm.embed) {
33
+ if (records.length <= 1)
34
+ return { canonical: records.slice(), duplicates: [] };
35
+ const vecs = await embedAll(records, cache, embedFn);
36
+ const groups = cluster(records, vecs, DEDUP_THRESHOLD);
37
+ const canonical = [];
38
+ const duplicates = [];
39
+ for (const g of groups) {
40
+ const sorted = [...g].sort((a, b) => ts(a.createdAt) - ts(b.createdAt));
41
+ canonical.push(sorted[0]);
42
+ duplicates.push(...sorted.slice(1));
43
+ }
44
+ return { canonical, duplicates };
45
+ }
@@ -0,0 +1,3 @@
1
+ import type { Stores } from '../types.js';
2
+ import { type LlmDeps } from './llm-deps.js';
3
+ export declare function decay(agentId: string, stores: Stores, llm?: LlmDeps): Promise<void>;
@@ -0,0 +1,48 @@
1
+ // REMind · L1 forgetting · Owner: Charan
2
+ // decay(agentId, stores): Ebbinghaus strength decay + adaptive forgetting (utility = recency x frequency x salience x redundancy) -> evict/archive to cold tier.
3
+ import { embedAll, cosine } from './similarity.js';
4
+ import { retier } from './tiering.js';
5
+ import { defaultLlm } from './llm-deps.js';
6
+ import { RECENCY_TAU_MS, STABILITY_BASE_MS, FORGET_R, DEDUP_THRESHOLD } from './constants.js';
7
+ const keyOf = (r) => r.id || r.content;
8
+ export async function decay(agentId, stores, llm = defaultLlm) {
9
+ const records = await stores.doc.getRecords(agentId);
10
+ if (records.length) {
11
+ const nowMs = Date.now();
12
+ const vecs = await embedAll(records, new Map(), llm.embed);
13
+ // redundancy: how many near-duplicates each record has.
14
+ const redundancy = new Map();
15
+ for (const r of records) {
16
+ const rv = vecs.get(keyOf(r));
17
+ let dups = 0;
18
+ if (rv) {
19
+ for (const o of records) {
20
+ if (o === r)
21
+ continue;
22
+ const ov = vecs.get(keyOf(o));
23
+ if (ov && cosine(rv, ov) >= DEDUP_THRESHOLD)
24
+ dups++;
25
+ }
26
+ }
27
+ redundancy.set(keyOf(r), dups);
28
+ }
29
+ for (const r of records) {
30
+ if (r.tier === 'cold')
31
+ continue;
32
+ const last = Date.parse(r.lastUsedAt ?? r.createdAt);
33
+ const dt = nowMs - (Number.isNaN(last) ? nowMs : last);
34
+ // Ebbinghaus retention R = exp(-dt / S); stability grows with corroboration.
35
+ const stability = STABILITY_BASE_MS * (1 + (r.confidence ?? 0.5));
36
+ const retention = Math.exp(-dt / stability);
37
+ // utility = recency x frequency(≈confidence) x salience(≈confidence) x redundancy.
38
+ const recency = Math.exp(-dt / RECENCY_TAU_MS);
39
+ const salience = r.confidence ?? 0.5;
40
+ const dups = redundancy.get(keyOf(r)) ?? 0;
41
+ const utility = recency * salience * (1 / (1 + dups));
42
+ if (retention < FORGET_R || utility < FORGET_R * 0.5) {
43
+ await stores.doc.saveRecord({ ...r, tier: 'cold' });
44
+ }
45
+ }
46
+ }
47
+ await retier(agentId, stores);
48
+ }
@@ -0,0 +1,6 @@
1
+ import type { Brain, ConsolidationReport, PromotionGate, Stores } from '../types.js';
2
+ export declare class Cognition implements Brain {
3
+ consolidate(agentId: string, stores: Stores, gate: PromotionGate): Promise<ConsolidationReport>;
4
+ decay(agentId: string, stores: Stores): Promise<void>;
5
+ }
6
+ export declare const cognition: Cognition;
@@ -0,0 +1,13 @@
1
+ // REMind · L1 Brain export · Owner: Charan
2
+ // export class Cognition implements Brain. The single public seam of brain/.
3
+ import { consolidate as consolidateFn } from './consolidation.js';
4
+ import { decay as decayFn } from './forgetting.js';
5
+ export class Cognition {
6
+ consolidate(agentId, stores, gate) {
7
+ return consolidateFn(agentId, stores, gate);
8
+ }
9
+ decay(agentId, stores) {
10
+ return decayFn(agentId, stores);
11
+ }
12
+ }
13
+ export const cognition = new Cognition();
@@ -0,0 +1,8 @@
1
+ export interface LlmDeps {
2
+ complete: (prompt: string, opts?: {
3
+ temperature?: number;
4
+ }) => Promise<string>;
5
+ classify: (prompt: string) => Promise<string>;
6
+ embed: (text: string) => Promise<number[]>;
7
+ }
8
+ export declare const defaultLlm: LlmDeps;
@@ -0,0 +1,4 @@
1
+ // REMind · L1 LLM dependency seam · Owner: Charan
2
+ // Lets the sleep cycle run against the real Azure LLM in prod, or an injected fake in tests.
3
+ import { complete, classify, embed } from '../llm.js';
4
+ export const defaultLlm = { complete, classify, embed };
@@ -0,0 +1 @@
1
+ export {};