agenticow 0.1.0 โ†’ 0.1.1

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/README.md CHANGED
@@ -87,6 +87,133 @@ A worked script lives in [`examples/parallel-agents.mjs`](./examples/parallel-ag
87
87
 
88
88
  ---
89
89
 
90
+ ## Applications
91
+
92
+ Concrete ways to use COW agent memory โ€” each with a runnable script in
93
+ [`examples/`](./examples). Framing is honest: **practical** ships and is proven,
94
+ **strategic** is shipped-but-early, **exotic** is vision/roadmap (clearly marked).
95
+
96
+ ### ๐ŸŸข Personalization โ€” one base, a branch per user *(practical)*
97
+ Give every user/account/tenant their own memory branch off a shared base. Private
98
+ edits stay isolated; storage is delta-only (KB/user, not a full copy).
99
+ ```js
100
+ const base = open('kb.rvf', { dimension: 1536 });
101
+ const userMem = base.fork(`user-${userId}`);
102
+ userMem.ingest([{ id, vector }]); // private to this user
103
+ userMem.query(q, 10); // reads through to the shared base
104
+ ```
105
+ โ†’ [`examples/personalization.mjs`](./examples/personalization.mjs) ยท [`parallel-agents.mjs`](./examples/parallel-agents.mjs)
106
+
107
+ ### ๐ŸŸข Rollback / quarantine โ€” discard a poisoned branch *(practical)*
108
+ An agent ingests hallucinated or adversarial memories into a sandbox branch.
109
+ Detect it, drop the branch โ€” the base is instantly clean, no re-index.
110
+ ```js
111
+ const sandbox = base.fork('untrusted');
112
+ sandbox.ingest(unvettedVectors);
113
+ // ...detect bad content...
114
+ sandbox.close(); // discard โ†’ base never saw it
115
+ ```
116
+ โ†’ [`examples/rollback-quarantine.mjs`](./examples/rollback-quarantine.mjs)
117
+
118
+ ### ๐ŸŸข Checkpointing โ€” crash recovery without replay *(practical)*
119
+ Checkpoint memory before each risky step (162 B each). On failure, roll back to
120
+ the last good checkpoint in ~0.5 ms โ€” earlier steps are not replayed.
121
+ ```js
122
+ const ck = mem.checkpoint('step-30');
123
+ // ...step 31 crashes...
124
+ mem.rollback(ck.id); // resume from step 30
125
+ ```
126
+ โ†’ [`examples/checkpointing.mjs`](./examples/checkpointing.mjs)
127
+
128
+ ### ๐ŸŸข Git-style memory workflow โ€” branch โ†’ diff โ†’ promote *(practical)*
129
+ Treat memory like code: branch a feature, review the change with `diff()`, and
130
+ `promote()` the vetted delta into production.
131
+ ```js
132
+ const feature = prod.branch('feature');
133
+ feature.ingest(newFacts);
134
+ feature.diff(); // { added, overridden, deleted }
135
+ feature.promote(prod); // merge into production
136
+ ```
137
+ โ†’ [`examples/git-workflow.mjs`](./examples/git-workflow.mjs)
138
+
139
+ ### ๐ŸŸก A/B testing & evolution โ€” score variants, promote the winner *(strategic)*
140
+ Fork N variant branches off one base, score each, and promote only the winner.
141
+ The substrate for Darwin-style / population-based agent-memory search.
142
+ ```js
143
+ const variants = ids.map((i) => base.fork(`variant-${i}`));
144
+ // ...score each...
145
+ variants[best].promote(base); // keep the winner, drop the rest free
146
+ ```
147
+ โ†’ [`examples/ab-branches.mjs`](./examples/ab-branches.mjs)
148
+
149
+ ### ๐ŸŸก Compliance & lineage โ€” provenance-tracked memory *(strategic)*
150
+ Every branch records its parent id + hash and a cryptographic witness chain.
151
+ `lineage()` gives an auditable history of how a memory state was reached โ€”
152
+ useful for reproducibility and audit trails.
153
+
154
+ ### ๐ŸŸก Edge / local-first agents โ€” embedded, no server *(strategic)*
155
+ agenticow runs in-process over a single `.rvf` file โ€” no DB server, no network.
156
+ Thousands of cheap branches fit on-device for offline/edge multi-agent memory.
157
+
158
+ ### ๐Ÿ”ญ Agent marketplaces & shared base memories *(exotic โ€” vision, not shipped)*
159
+ A published base memory that many agents branch from, contributing deltas back โ€”
160
+ a "memory package registry". The branch/promote primitives exist today; the
161
+ distribution, trust, and merge-policy layer is **roadmap, not shipped**.
162
+
163
+ ---
164
+
165
+ ## MetaHarness usage
166
+
167
+ agenticow is the **memory plane** of the [`@metaharness/*`](https://www.npmjs.com/org/metaharness)
168
+ agent-harness ecosystem. It pairs with [`@metaharness/jujutsu`](https://www.npmjs.com/package/@metaharness/jujutsu)
169
+ (`v0.1.0`), which wraps [`agentic-jujutsu`](https://www.npmjs.com/package/agentic-jujutsu)
170
+ (a Rust+NAPI Jujutsu `jj` op-log with QuantumDAG coordination, ReasoningBank
171
+ trajectories, and ML-DSA signing) โ€” the **code/op plane**.
172
+
173
+ **The dual-state bridge (ADR-202).** A coding agent that explores must branch and
174
+ roll back *two* planes: the **code/ops** it did and the **memory** it learned.
175
+ Used separately they drift (revert code but keep poisoned memory; promote a
176
+ memory delta whose ops were never merged). `@metaharness/jujutsu`'s
177
+ `DualStateBridge` ties them 1:1 โ€” one agent โ‡’ one op branch + one memory branch โ€”
178
+ mapping four lifecycle verbs onto agenticow:
179
+
180
+ | Verb | code/op plane (agentic-jujutsu) | memory plane (agenticow) |
181
+ |---|---|---|
182
+ | **spawn** | `jj bookmark create` + start trajectory | `fork()` off the base + `checkpoint('spawn')` |
183
+ | **learn** | finalize trajectory + read op-sequence | embed ops โ†’ `ingest()` into the branch |
184
+ | **revert** | `jj undo` | `rollback()` to the spawn checkpoint |
185
+ | **merge** | `jj squash` ops into base | `promote()` the winning delta into the base |
186
+
187
+ Install alongside (both planes are **optional peer deps** โ€” the bridge runs
188
+ degraded/mock-backed if one is absent, per the ADR-150 *removable-augmentation*
189
+ principle):
190
+
191
+ ```bash
192
+ npm install @metaharness/jujutsu agenticow agentic-jujutsu
193
+ ```
194
+
195
+ ```js
196
+ import { open } from 'agenticow';
197
+ // @metaharness/jujutsu wires these two planes behind DualStateBridge:
198
+ const base = open('reasoning-bank.rvf', { dimension: 1536 });
199
+ const agentMem = base.fork('agent-007'); // memory branch (spawn)
200
+ agentMem.checkpoint('spawn');
201
+ agentMem.ingest(embeddedTrajectory); // learn
202
+ // if the trajectory scores poorly:
203
+ agentMem.rollback(/* spawn checkpoint id */); // revert โ€” code revert via `jj undo`
204
+ // if it wins:
205
+ agentMem.promote(base); // merge โ€” ops via `jj squash`
206
+ ```
207
+
208
+ **Honest status (ADR-202):** spawn / learn / revert / merge are **wired
209
+ end-to-end** with both real native planes. **Cross-branch ANN query is stubbed**
210
+ behind a port โ€” agenticow's exact read-through answers it correctly but
211
+ unaccelerated across the COW boundary; the native single-index-across-the-branch
212
+ lands with [ruvnet/RuVector PR #617](https://github.com/ruvnet/RuVector/pull/617),
213
+ at which point only the adapter swaps.
214
+
215
+ ---
216
+
90
217
  ## How copy-on-write for vectors works
91
218
 
92
219
  ![COW concept](./assets/concept.png)
@@ -144,6 +271,29 @@ The acceptance test builds a brute-force ground truth (`base โˆช branch-inserts
144
271
 
145
272
  \* **Honest concession.** On SIFT-1M, same machine, the underlying [ruvector](https://github.com/ruvnet/RuVector) HNSW does ~2,197 QPS @ recall 0.95 vs hnswlib-node ~9,344 QPS โ€” roughly **2.7ร— slower** for raw ANN. If you need maximum raw similarity-search speed on a static index, use a dedicated ANN library. agenticow's edge is **cheap branching, checkpointing and rollback of agent memory** โ€” which none of the above have.
146
273
 
274
+ ### Performance ยท storage ยท cost at scale
275
+
276
+ **Scenario: 1,000 branches over a 1M-vector base** (dim 128, ~496 MB base). agenticow figures are **measured** on an AMD Ryzen 9 9950X; competitor figures are **published / estimated** (sources below) and labeled as such โ€” not fabricated.
277
+
278
+ | Approach | Branch / snapshot create | Per-branch storage | Query latency (ANN) | Cost @ 1,000 branches | Native COW / rollback |
279
+ |---|---|---|---|---|---|
280
+ | **agenticow (COW)** | 0.47 ms / 162 B *(measured, flat to 1M)* | ~10.8 KB *(measured)* | ~2.7ร— behind hnswlib *(measured\*)* | ~507 MB local ยท **~$0** infra (embedded) *(measuredโ€ )* | โœ… instant (p50 0.57 ms) |
281
+ | Naive full-copy | 67 ms / 496 MB *(measured @1M)* | full base (~496 MB) | = source engine | ~484 GB local *(measured ร—N)* | โŒ |
282
+ | Pinecone (serverless) | no native branch โ€” full re-upsert | full copy (managed) | fast (core strength) | ~484 GB โ‰ˆ **$160/mo** storage + units *(est.ยน)* | โŒ |
283
+ | Milvus | snapshot = full copy / reindex | full copy | fast (core strength) | ~484 GB resident โ†’ large cluster, **$$$/mo** *(est.ยฒ)* | โŒ |
284
+ | Qdrant | snapshot = full copy | full copy | fast (core strength) | ~484 GB โ†’ managed/self-host, **$$$/mo** *(est.ยณ)* | โŒ |
285
+ | pgvector | SQL dump + reindex | full copy | moderate | ~484 GB in Postgres *(est.)* | โŒ |
286
+ | Chroma | full collection copy | full copy | moderate | ~484 GB local/managed *(est.)* | โŒ |
287
+ | lakeFS / DVC | fast metadata branch *(their strength)* | file-level delta (cheap) | n/a โ€” not a vector engine | cheap branching, but you still build/serve the ANN index yourself *(published)* | โœ… data/files ยท โŒ vector index |
288
+
289
+ **Takeaway:** agenticow wins on branch-create speed, per-branch storage, and multi-branch cost, and is the only option with native COW branching + instant rollback of a live vector memory. It **concedes raw ANN search speed** to the dedicated vector DBs โ€” use those when single-index query throughput is the priority, and agenticow when you need cheap branching, checkpointing, and rollback of agent memory.
290
+
291
+ <sub>\* SIFT-1M same-machine (above). โ€  base ~496 MB + 1,000 ร— ~10.8 KB โ‰ˆ 507 MB, in-process. ยน est. from [pinecone.io/pricing](https://www.pinecone.io/pricing/) (~$0.33/GB-mo storage, excl. read/write units). ยฒ est. from [zilliz.com/pricing](https://zilliz.com/pricing). ยณ est. from [qdrant.tech/pricing](https://qdrant.tech/pricing/). Competitor figures are published/estimated; only agenticow's are measured.</sub>
292
+
293
+ The [live site](https://ruvnet.github.io/agenticow/#bench) is mobile-friendly (responsive layout, horizontally-scrollable tables):
294
+
295
+ <img src="./assets/mobile-hero.png" width="280" alt="agenticow on mobile (375px width)" />
296
+
147
297
  ---
148
298
 
149
299
  ## Honest scope
@@ -0,0 +1,111 @@
1
+ # agenticow examples
2
+
3
+ Runnable, optimized examples โ€” one per core use case. They import the library
4
+ from `../src/index.js` so they run against this repo with zero setup; in your own
5
+ project, import from `agenticow` instead.
6
+
7
+ ```bash
8
+ node examples/personalization.mjs
9
+ node examples/rollback-quarantine.mjs
10
+ node examples/checkpointing.mjs
11
+ node examples/git-workflow.mjs
12
+ node examples/ab-branches.mjs
13
+ node examples/parallel-agents.mjs
14
+ # or all of them:
15
+ npm run examples
16
+ ```
17
+
18
+ Vectors are generated by a seeded RNG (`_shared.mjs`) so the **id/order/recall
19
+ outputs are deterministic**; only the timings vary per machine. Outputs below
20
+ were captured on an AMD Ryzen 9 9950X, Node v22.
21
+
22
+ ---
23
+
24
+ ### `personalization.mjs` โ€” one base, a cheap branch per user
25
+
26
+ A shared base + an isolated COW branch per user. Private edits stay private;
27
+ storage is delta-only.
28
+
29
+ ```
30
+ base: 10,000 vectors, 2.52 MB
31
+ branched 50 users in 58.55 ms (1.176 ms/user) โ€” total delta 42.2 KB (0.84 KB/user)
32
+ vs 50 full copies of the base: 125.91 MB โ†’ 3,056x less storage
33
+ isolation: user-0 sees own pref (id 900000)=true, user-1's pref leaks to user-0=false
34
+ read-through: user-7 top hit for base vector #42 = id 42 (from base)
35
+ ```
36
+
37
+ ### `rollback-quarantine.mjs` โ€” quarantine a poisoned ingest
38
+
39
+ An untrusted agent ingests 100 unvetted vectors into a sandbox branch; discard
40
+ the branch โ†’ the base is instantly clean. No re-index, no restore.
41
+
42
+ ```
43
+ base: 2000 trusted vectors
44
+ agent ingested 100 unvetted vectors into its sandbox branch
45
+ before: query near poison[0] -> id 666000 (poison present in sandbox = true)
46
+ discarded branch in 0.256 ms โ†’ poison present in base = false
47
+ base intact: 2000 vectors, base vector #1 still found = true
48
+ ```
49
+
50
+ ### `checkpointing.mjs` โ€” zero-cost checkpoints + crash recovery without replay
51
+
52
+ Checkpoint every 10 steps; crash at step 31; roll back to step 30. The 30 steps
53
+ are NOT replayed โ€” they live in the 162-byte checkpoint.
54
+
55
+ ```
56
+ checkpoint @ step 10: id 93b1eb356aโ€ฆ (162 B, depth 1)
57
+ checkpoint @ step 20: id 8169723119โ€ฆ (162 B, depth 2)
58
+ checkpoint @ step 30: id b39dc86be8โ€ฆ (162 B, depth 3)
59
+ step 31 ran (partial work present = true) ... ๐Ÿ’ฅ simulated crash
60
+ recovered to step-30 in 0.731 ms (re-ingests during recovery: 0)
61
+ after recovery: step 31 gone = true, step 15 present = true, step 30 present = true
62
+ total ingest() calls across the whole run: 31 (31 steps, no replay)
63
+ ```
64
+
65
+ ### `git-workflow.mjs` โ€” agent โ†’ test โ†’ prod memory pipeline
66
+
67
+ Branch a feature off production, ingest/override/delete in it, review with
68
+ `diff()`, then `promote()` the vetted delta into prod. Prod is untouched until
69
+ you promote.
70
+
71
+ ```
72
+ prod: 2 vectors
73
+ diff(feature): +added=[100,101] ~overridden=[2] -deleted=[1]
74
+ prod before promote: id 1 present = true, feature id 100 present = false
75
+ promote โ†’ 3 vectors merged, 1 tombstoned into prod
76
+ prod after promote: id 100 present = true, id 1 retracted = true
77
+ ```
78
+
79
+ ### `ab-branches.mjs` โ€” N variant branches, score each, promote the winner
80
+
81
+ A/B / Darwin-style: fork variant branches, score each candidate against a
82
+ target, promote only the winner. Losing branches are discarded for free.
83
+
84
+ ```
85
+ base: 1000 vectors; scoring 8 variant branches against a target
86
+ variant-7: candidate distance to target = 0.0000
87
+ variant-6: candidate distance to target = 0.0179
88
+ variant-5: candidate distance to target = 0.0568
89
+ variant-4: candidate distance to target = 0.3386
90
+ variant-3: candidate distance to target = 0.4558
91
+ variant-2: candidate distance to target = 0.5061
92
+ variant-0: candidate distance to target = 0.9004
93
+ variant-1: candidate distance to target = 1.1748
94
+ winner: variant-7 (dist 0.0000)
95
+ promoted variant-7 โ†’ 1 vector merged; winner candidate now in base = true
96
+ ```
97
+
98
+ ### `parallel-agents.mjs` โ€” fan out, query, roll one back
99
+
100
+ Fork N agent branches off a base, each with private memory + tombstones, query
101
+ each (read-through), and roll one back after a bad ingest.
102
+
103
+ ```
104
+ base: 2000 vectors, 516 KB
105
+ forked 8 agent branches off the shared base
106
+ agent-0: top-5 = 5 base + 0 private (e.g. id 1503 @ 0.546)
107
+ ...
108
+ agent-0 before rollback: hallucination present = true
109
+ agent-0 after rollback: hallucination present = false (base intact)
110
+ done.
111
+ ```
@@ -0,0 +1,34 @@
1
+ // Shared helpers for the examples โ€” a deterministic RNG so outputs are
2
+ // reproducible, plus a tiny timer. Examples import the library from the local
3
+ // source so they run against this repo with zero setup:
4
+ //
5
+ // in your own project: import { open } from 'agenticow';
6
+ // in this repo: import { open } from '../src/index.js';
7
+
8
+ import os from 'node:os';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ // mulberry32 โ€” small, fast, seedable PRNG (deterministic example output).
13
+ export function rng(seed = 0x9e3779b9) {
14
+ let a = seed >>> 0;
15
+ return () => {
16
+ a |= 0; a = (a + 0x6d2b79f5) | 0;
17
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
18
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
19
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
20
+ };
21
+ }
22
+
23
+ export function vecFactory(dim, seed) {
24
+ const r = rng(seed);
25
+ return () => Float32Array.from({ length: dim }, () => r() * 2 - 1);
26
+ }
27
+
28
+ export function tmpdir(tag) {
29
+ return fs.mkdtempSync(path.join(os.tmpdir(), `agenticow-${tag}-`));
30
+ }
31
+
32
+ export const ms = (t0) => `${(performance.now() - t0).toFixed(3)} ms`;
33
+ export const kb = (b) => `${(b / 1024).toFixed(1)} KB`;
34
+ export const mb = (b) => `${(b / 1024 / 1024).toFixed(2)} MB`;
@@ -0,0 +1,60 @@
1
+ // ab-branches.mjs โ€” N variant branches off one base, score each, promote winner.
2
+ //
3
+ // Demonstrates A/B testing / Darwin-style evolution of agent memory: fork many
4
+ // variant branches off a shared base, give each a different candidate memory,
5
+ // score each variant against a target query, and promote() only the winner back
6
+ // into the base. Losing branches are discarded for free.
7
+ //
8
+ // Run: node examples/ab-branches.mjs
9
+ //
10
+ // โ”€โ”€ verified output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
11
+ // (see examples/README.md)
12
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { open } from '../src/index.js'; // in your project: from 'agenticow'
17
+ import { vecFactory, tmpdir } from './_shared.mjs';
18
+
19
+ const DIM = 32;
20
+ const VARIANTS = 8;
21
+ const dir = tmpdir('ab');
22
+ const vec = vecFactory(DIM, 31);
23
+
24
+ // 1. Base memory + a "target" we want a variant to match well.
25
+ const base = open(path.join(dir, 'base.rvf'), { dimension: DIM });
26
+ const flat = new Float32Array(1000 * DIM);
27
+ for (let i = 0; i < 1000; i++) flat.set(vec(), i * DIM);
28
+ base.ingest(flat, Array.from({ length: 1000 }, (_, i) => i));
29
+ const target = vec();
30
+ console.log(`base: ${base.status().totalVectors} vectors; scoring ${VARIANTS} variant branches against a target`);
31
+
32
+ // 2. Fork one branch per variant; each ingests a candidate vector (id 5000).
33
+ // Variant v's candidate is a blend of the target and noise โ€” higher v = closer.
34
+ const variants = [];
35
+ for (let v = 0; v < VARIANTS; v++) {
36
+ const br = base.fork(`variant-${v}`);
37
+ const blend = Float32Array.from(target, (x) => x * (v / (VARIANTS - 1)) + (vec()[0] * (1 - v / (VARIANTS - 1))));
38
+ br.ingest([{ id: 5000, vector: blend }]);
39
+ variants.push(br);
40
+ }
41
+
42
+ // 3. Score each variant: distance of its candidate (id 5000) to the target.
43
+ // Use k > base size so the candidate is always included in the read-through.
44
+ const scores = variants.map((br, v) => {
45
+ const hit = br.query(target, 1001).find((h) => h.id === 5000);
46
+ return { v, dist: hit ? hit.distance : Infinity };
47
+ });
48
+ scores.sort((a, b) => a.dist - b.dist);
49
+ for (const s of scores) console.log(` variant-${s.v}: candidate distance to target = ${s.dist.toFixed(4)}`);
50
+ const winner = scores[0];
51
+ console.log(`winner: variant-${winner.v} (dist ${winner.dist.toFixed(4)})`);
52
+
53
+ // 4. Promote the winner into the base; discard the rest for free.
54
+ const r = variants[winner.v].promote(base);
55
+ const inBase = base.query(target, 5).some((h) => h.id === 5000);
56
+ console.log(`promoted variant-${winner.v} โ†’ ${r.ingested} vector merged; winner candidate now in base = ${inBase}`);
57
+
58
+ base.close();
59
+ variants.forEach((br) => br.close());
60
+ fs.rmSync(dir, { recursive: true, force: true });
@@ -0,0 +1,64 @@
1
+ // checkpointing.mjs โ€” zero-cost checkpoints + crash recovery without replay.
2
+ //
3
+ // Demonstrates: an agent loop that checkpoints memory every 10 steps. A failure
4
+ // hits at step 31; we roll back to the step-30 checkpoint and resume. The first
5
+ // 30 steps are NOT replayed โ€” they live in the 162-byte checkpoint and are
6
+ // already visible via read-through.
7
+ //
8
+ // Run: node examples/checkpointing.mjs
9
+ //
10
+ // โ”€โ”€ verified output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
11
+ // (see examples/README.md)
12
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { open } from '../src/index.js'; // in your project: from 'agenticow'
17
+ import { vecFactory, tmpdir, ms } from './_shared.mjs';
18
+
19
+ const DIM = 32;
20
+ const dir = tmpdir('checkpoint');
21
+ const vec = vecFactory(DIM, 11);
22
+
23
+ const mem = open(path.join(dir, 'agent.rvf'), { dimension: DIM });
24
+ const stored = new Map(); // step -> vector, to verify read-through later
25
+
26
+ let ingestCalls = 0;
27
+ function step(n) {
28
+ const v = vec();
29
+ mem.ingest([{ id: n, vector: v }]);
30
+ stored.set(n, v);
31
+ ingestCalls++;
32
+ }
33
+
34
+ // Run steps 1..30, checkpoint at 10, 20, 30.
35
+ const checkpoints = {};
36
+ for (let n = 1; n <= 30; n++) {
37
+ step(n);
38
+ if (n % 10 === 0) {
39
+ checkpoints[n] = mem.checkpoint(`step-${n}`);
40
+ console.log(`checkpoint @ step ${n}: id ${checkpoints[n].id.slice(0, 10)}โ€ฆ (162 B, depth ${checkpoints[n].depth})`);
41
+ }
42
+ }
43
+
44
+ // Step 31 does some work, then "crashes".
45
+ step(31);
46
+ const crashedHasGarbage = mem.query(stored.get(31), 1)[0].id === 31;
47
+ console.log(`step 31 ran (partial work present = ${crashedHasGarbage}) ... ๐Ÿ’ฅ simulated crash`);
48
+
49
+ // Recover: roll back to the step-30 checkpoint.
50
+ const callsBefore = ingestCalls;
51
+ const t0 = performance.now();
52
+ mem.rollback(checkpoints[30].id);
53
+ const recoverMs = ms(t0);
54
+
55
+ // The 30 steps are intact WITHOUT replay (ingestCalls unchanged), step 31 gone.
56
+ const step31Gone = mem.query(stored.get(31), 5).every((h) => h.id !== 31);
57
+ const step15Present = mem.query(stored.get(15), 1)[0].id === 15;
58
+ const step30Present = mem.query(stored.get(30), 1)[0].id === 30;
59
+ console.log(`recovered to step-30 in ${recoverMs} (re-ingests during recovery: ${ingestCalls - callsBefore})`);
60
+ console.log(`after recovery: step 31 gone = ${step31Gone}, step 15 present = ${step15Present}, step 30 present = ${step30Present}`);
61
+ console.log(`total ingest() calls across the whole run: ${ingestCalls} (31 steps, no replay)`);
62
+
63
+ mem.close();
64
+ fs.rmSync(dir, { recursive: true, force: true });
@@ -0,0 +1,52 @@
1
+ // git-workflow.mjs โ€” agent โ†’ test โ†’ prod memory pipeline (branch/diff/promote).
2
+ //
3
+ // Demonstrates the Git-style workflow for vector memory: branch a feature off
4
+ // production, ingest into it, review the change with diff(), then promote() the
5
+ // vetted delta back into production. Production is untouched until you promote.
6
+ //
7
+ // Run: node examples/git-workflow.mjs
8
+ //
9
+ // โ”€โ”€ verified output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
10
+ // (see examples/README.md)
11
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
12
+
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { open } from '../src/index.js'; // in your project: from 'agenticow'
16
+ import { vecFactory, tmpdir } from './_shared.mjs';
17
+
18
+ const DIM = 32;
19
+ const dir = tmpdir('git-workflow');
20
+ const vec = vecFactory(DIM, 23);
21
+
22
+ // 1. Production memory.
23
+ const prod = open(path.join(dir, 'prod.rvf'), { dimension: DIM });
24
+ const v1 = vec(); const v2 = vec();
25
+ prod.ingest([{ id: 1, vector: v1 }, { id: 2, vector: v2 }]);
26
+ console.log(`prod: ${prod.status().totalVectors} vectors`);
27
+
28
+ // 2. Branch a feature, ingest new memories, override one, delete one.
29
+ const feature = prod.branch('feature/new-facts');
30
+ const f100 = vec(); const f101 = vec(); const v2new = vec();
31
+ feature.ingest([{ id: 100, vector: f100 }, { id: 101, vector: f101 }]);
32
+ feature.ingest([{ id: 2, vector: v2new }]); // override an existing prod id
33
+ feature.delete([1]); // retract a prod fact in the branch
34
+
35
+ // 3. Review the change set before merging.
36
+ const d = feature.diff();
37
+ console.log(`diff(feature): +added=${JSON.stringify(d.added)} ~overridden=${JSON.stringify(d.overridden)} -deleted=${JSON.stringify(d.deleted)}`);
38
+
39
+ // prod is still untouched at this point (feature edits are isolated):
40
+ const id100InProdBefore = prod.query(f100, 1)[0].id === 100;
41
+ console.log(`prod before promote: id 1 present = ${prod.query(v1, 1)[0].id === 1}, feature id 100 present = ${id100InProdBefore}`);
42
+
43
+ // 4. Promote the vetted delta into production.
44
+ const r = feature.promote(prod);
45
+ console.log(`promote โ†’ ${r.ingested} vectors merged, ${r.deleted} tombstoned into prod`);
46
+ const id100InProd = prod.query(f100, 1)[0].id === 100;
47
+ const id1Retracted = prod.query(v1, 5).every((h) => h.id !== 1);
48
+ console.log(`prod after promote: id 100 present = ${id100InProd}, id 1 retracted = ${id1Retracted}`);
49
+
50
+ prod.close();
51
+ feature.close();
52
+ fs.rmSync(dir, { recursive: true, force: true });
@@ -10,7 +10,7 @@
10
10
  import fs from 'node:fs';
11
11
  import os from 'node:os';
12
12
  import path from 'node:path';
13
- import { open } from 'agenticow'; // or '../src/index.js' from inside this repo
13
+ import { open } from '../src/index.js'; // in your project: from 'agenticow'
14
14
 
15
15
  const DIM = 64;
16
16
  const N_AGENTS = 8;
@@ -0,0 +1,66 @@
1
+ // personalization.mjs โ€” one shared base memory, a cheap COW branch per user.
2
+ //
3
+ // Demonstrates: per-user/per-account personalization without copying the base.
4
+ // Each user gets an isolated branch (their edits stay private) while still
5
+ // reading through to the shared base. Storage is delta-only (KB/user), not a
6
+ // full copy of the base (MB/user).
7
+ //
8
+ // Run: node examples/personalization.mjs
9
+ //
10
+ // โ”€โ”€ verified output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
11
+ // (see examples/README.md โ€” outputs are deterministic via a seeded RNG)
12
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { open } from '../src/index.js'; // in your project: from 'agenticow'
17
+ import { vecFactory, tmpdir, ms, kb, mb } from './_shared.mjs';
18
+
19
+ const DIM = 64;
20
+ const USERS = 50;
21
+ const dir = tmpdir('personalization');
22
+ const vec = vecFactory(DIM, 1);
23
+
24
+ // 1. One shared base memory (e.g. the org-wide knowledge base).
25
+ const base = open(path.join(dir, 'base.rvf'), { dimension: DIM });
26
+ const N = 10_000;
27
+ const flat = new Float32Array(N * DIM);
28
+ const ids = [];
29
+ for (let i = 0; i < N; i++) { flat.set(vec(), i * DIM); ids.push(i); }
30
+ base.ingest(flat, ids);
31
+ const baseBytes = base.status().fileSize;
32
+ console.log(`base: ${N.toLocaleString()} vectors, ${mb(baseBytes)}`);
33
+
34
+ // 2. A cheap COW branch per user โ€” fork() off the read-only base.
35
+ // Each user adds one private preference vector (id 900000 + u).
36
+ const t0 = performance.now();
37
+ const users = [];
38
+ for (let u = 0; u < USERS; u++) {
39
+ const ub = base.fork(`user-${u}`);
40
+ ub.ingest([{ id: 900000 + u, vector: vec() }]);
41
+ users.push(ub);
42
+ }
43
+ const forkWall = ms(t0);
44
+ let deltaBytes = 0;
45
+ for (const u of users) deltaBytes += fs.statSync(u.lineage()[0].path).size;
46
+ const fullCopyBytes = baseBytes * USERS;
47
+ console.log(`branched ${USERS} users in ${forkWall} (${((performance.now() - t0) / USERS).toFixed(3)} ms/user) โ€” ` +
48
+ `total delta ${kb(deltaBytes)} (${(deltaBytes / USERS / 1024).toFixed(2)} KB/user)`);
49
+ console.log(`vs ${USERS} full copies of the base: ${mb(fullCopyBytes)} โ†’ ` +
50
+ `${Math.round(fullCopyBytes / deltaBytes).toLocaleString()}x less storage`);
51
+
52
+ // 3. Isolation: user-0 sees its own preference; user-1's stays private.
53
+ const u0SeesOwn = users[0].diff().added.includes(900000);
54
+ const u1LeaksToU0 = users[0]
55
+ .query(flat.slice(0, DIM), USERS)
56
+ .some((h) => h.id === 900001);
57
+ console.log(`isolation: user-0 sees own pref (id 900000)=${u0SeesOwn}, user-1's pref leaks to user-0=${u1LeaksToU0}`);
58
+
59
+ // 4. Read-through: every user still queries the shared base.
60
+ const probe = flat.slice(42 * DIM, 43 * DIM);
61
+ const hit = users[7].query(probe, 1)[0];
62
+ console.log(`read-through: user-7 top hit for base vector #42 = id ${hit.id} (from ${hit.branch})`);
63
+
64
+ base.close();
65
+ users.forEach((u) => u.close());
66
+ fs.rmSync(dir, { recursive: true, force: true });
@@ -0,0 +1,54 @@
1
+ // rollback-quarantine.mjs โ€” quarantine a poisoned ingest, then discard it.
2
+ //
3
+ // Demonstrates: an agent ingests "hallucinated" / adversarial vectors into a
4
+ // branch. You detect them, then throw the branch away โ†’ the shared base is
5
+ // instantly clean. No re-indexing, no restore-from-backup. Shows the query
6
+ // result before (poison visible in the branch) and after (gone, base intact).
7
+ //
8
+ // Run: node examples/rollback-quarantine.mjs
9
+ //
10
+ // โ”€โ”€ verified output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
11
+ // (see examples/README.md)
12
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { open } from '../src/index.js'; // in your project: from 'agenticow'
17
+ import { vecFactory, tmpdir, ms } from './_shared.mjs';
18
+
19
+ const DIM = 48;
20
+ const dir = tmpdir('quarantine');
21
+ const vec = vecFactory(DIM, 7);
22
+
23
+ // 1. Trusted base memory.
24
+ const base = open(path.join(dir, 'base.rvf'), { dimension: DIM });
25
+ const known = Array.from({ length: 2000 }, () => vec());
26
+ const flat = new Float32Array(2000 * DIM);
27
+ known.forEach((v, i) => flat.set(v, i * DIM));
28
+ base.ingest(flat, known.map((_, i) => i));
29
+ console.log(`base: ${base.status().totalVectors} trusted vectors`);
30
+
31
+ // 2. An untrusted agent works in a sandbox branch and ingests poison.
32
+ const sandbox = base.fork('untrusted-agent');
33
+ const sandboxPath = sandbox.lineage()[0].path; // capture before we close it
34
+ const poison = Array.from({ length: 100 }, () => vec());
35
+ const POISON_IDS = poison.map((_, i) => 666000 + i);
36
+ poison.forEach((v, i) => sandbox.ingest([{ id: POISON_IDS[i], vector: v }]));
37
+ console.log(`agent ingested ${POISON_IDS.length} unvetted vectors into its sandbox branch`);
38
+
39
+ // 3. Detection: query near a poisoned vector โ€” it should surface in the sandbox.
40
+ const detect = sandbox.query(poison[0], 1)[0];
41
+ const poisonPresent = POISON_IDS.includes(detect.id);
42
+ console.log(`before: query near poison[0] -> id ${detect.id} (poison present in sandbox = ${poisonPresent})`);
43
+
44
+ // 4. Quarantine = discard the branch. The base never saw the poison.
45
+ const t0 = performance.now();
46
+ sandbox.close();
47
+ fs.rmSync(sandboxPath, { force: true });
48
+ const discardMs = ms(t0);
49
+ const poisonInBase = POISON_IDS.includes(base.query(poison[0], 1)[0].id);
50
+ console.log(`discarded branch in ${discardMs} โ†’ poison present in base = ${poisonInBase}`);
51
+ console.log(`base intact: ${base.status().totalVectors} vectors, base vector #1 still found = ${base.query(known[1], 1)[0].id === 1}`);
52
+
53
+ base.close();
54
+ fs.rmSync(dir, { recursive: true, force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenticow",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Git for Agent Memory: Copy-On-Write vector branching for embedded multi-agent memory. Branch a base memory in ~0.5ms / 162 bytes regardless of base size โ€” 83x faster, 3000x smaller than full-copy snapshots. Exact read-through queries (parent โˆช edits, child wins).",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -27,6 +27,13 @@
27
27
  "bench": "node bench/bench.js",
28
28
  "acceptance": "node bench/acceptance.js",
29
29
  "demo": "node bin/agenticow.js demo",
30
+ "examples": "for f in personalization rollback-quarantine checkpointing git-workflow ab-branches parallel-agents; do echo \"\\nโ”€โ”€ $f โ”€โ”€\"; node examples/$f.mjs; done",
31
+ "example:personalization": "node examples/personalization.mjs",
32
+ "example:rollback": "node examples/rollback-quarantine.mjs",
33
+ "example:checkpointing": "node examples/checkpointing.mjs",
34
+ "example:git-workflow": "node examples/git-workflow.mjs",
35
+ "example:ab-branches": "node examples/ab-branches.mjs",
36
+ "example:parallel-agents": "node examples/parallel-agents.mjs",
30
37
  "test": "node --test test/*.test.js"
31
38
  },
32
39
  "keywords": [
package/src/index.js CHANGED
@@ -74,7 +74,7 @@ class Node {
74
74
 
75
75
  export class AgenticMemory {
76
76
  /** @private */
77
- constructor(workingNode, ancestors, dim, metric, track = true) {
77
+ constructor(workingNode, ancestors, dim, metric, track = true, owned = null) {
78
78
  /** @type {Node} */
79
79
  this._working = workingNode;
80
80
  /** @type {Node[]} ancestors newest -> oldest (base last) */
@@ -83,6 +83,10 @@ export class AgenticMemory {
83
83
  this._metric = metric;
84
84
  this._track = track;
85
85
  this._normalize = String(metric).toLowerCase() === 'cosine';
86
+ // Nodes this instance is allowed to close. Ancestors shared from a parent
87
+ // (via fork/branch) are NOT owned, so closing a fork never closes the base.
88
+ /** @type {Set<Node>} */
89
+ this._owned = owned || new Set([workingNode]);
86
90
  this._closed = false;
87
91
  }
88
92
 
@@ -244,10 +248,12 @@ export class AgenticMemory {
244
248
  const parentChildDb = frozen.db.derive(parentChildPath, this._deriveOpts());
245
249
  const childPath = filePath || tmpChildPath(frozen.path, label);
246
250
  const childDb = frozen.db.derive(childPath, this._deriveOpts());
247
- // Parent continues, transparently, in its own fresh child.
251
+ // Parent continues, transparently, in its own fresh child (which it owns).
248
252
  this._ancestors = [frozen, ...this._ancestors];
249
253
  this._working = new Node(parentChildDb, parentChildPath, 'working');
250
- // Branch shares the frozen snapshot + all older ancestors.
254
+ this._owned.add(this._working);
255
+ // Branch shares the frozen snapshot + all older ancestors; it owns only its
256
+ // own working child.
251
257
  const branchNode = new Node(childDb, childPath, label || 'branch');
252
258
  return new AgenticMemory(branchNode, [...this._ancestors], this._dim, this._metric, this._track);
253
259
  }
@@ -339,6 +345,7 @@ export class AgenticMemory {
339
345
  const childNode = new Node(childDb, childPath, 'working');
340
346
  this._ancestors = [frozen, ...this._ancestors];
341
347
  this._working = childNode;
348
+ this._owned.add(childNode);
342
349
  return {
343
350
  id: frozen.id,
344
351
  label: frozen.label,
@@ -364,18 +371,21 @@ export class AgenticMemory {
364
371
  idx = this._ancestors.findIndex((n) => n.id === checkpointId);
365
372
  if (idx === -1) throw new Error(`agenticow: checkpoint ${checkpointId} not found`);
366
373
  }
367
- // Discard the current poisoned working child and any checkpoints newer than target.
368
- try {
369
- this._working.db.close();
370
- } catch { /* ignore */ }
371
- try {
372
- fs.rmSync(this._working.path, { force: true });
373
- } catch { /* ignore */ }
374
+ // Discard the current poisoned working child and any checkpoints newer than
375
+ // target โ€” but only ones THIS instance owns (never a shared ancestor).
376
+ const discarded = [this._working, ...this._ancestors.slice(0, idx)];
377
+ for (const n of discarded) {
378
+ if (!this._owned.has(n)) continue;
379
+ try { n.db.close(); } catch { /* ignore */ }
380
+ try { fs.rmSync(n.path, { force: true }); } catch { /* ignore */ }
381
+ this._owned.delete(n);
382
+ }
374
383
  const target = this._ancestors[idx];
375
384
  const newAncestors = this._ancestors.slice(idx); // target + older
376
385
  const childPath = tmpChildPath(target.path, 'work');
377
386
  const childDb = target.db.derive(childPath, this._deriveOpts());
378
387
  this._working = new Node(childDb, childPath, 'working');
388
+ this._owned.add(this._working);
379
389
  this._ancestors = newAncestors;
380
390
  return { restoredTo: target.id, depth: this._ancestors.length };
381
391
  }
@@ -444,13 +454,14 @@ export class AgenticMemory {
444
454
  node.editVecs = new Map(Object.entries(nm.editVecs || {}).map(([id, v]) => [Number(id), Float32Array.from(v)]));
445
455
  return node;
446
456
  });
447
- return new AgenticMemory(nodes[0], nodes.slice(1), m.dim, m.metric, m.track !== false);
457
+ // A loaded instance opened every handle itself, so it owns the whole chain.
458
+ return new AgenticMemory(nodes[0], nodes.slice(1), m.dim, m.metric, m.track !== false, new Set(nodes));
448
459
  }
449
460
 
450
- /** Close all open handles in the chain. */
461
+ /** Close the handles this instance owns (never a shared parent/base handle). */
451
462
  close() {
452
463
  if (this._closed) return;
453
- for (const n of this._chain()) {
464
+ for (const n of this._owned) {
454
465
  try { n.db.close(); } catch { /* ignore */ }
455
466
  }
456
467
  this._closed = true;