@agent-custody/state 0.1.3 → 0.1.5

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
@@ -22,7 +22,7 @@ ledger.retract({ factId: a.fact.factId, actor: "user:admin", reason: "poisoned b
22
22
  ledger.asOf({ validAt: "2026-09-01T00:00:00Z", txAt: "2026-09-01T00:00:00Z" });
23
23
  ```
24
24
 
25
- Three runnable examples, all executed by the test suite. [03-memory-behind-the-gateway.ts](examples/03-memory-behind-the-gateway.ts) runs the memory server as the gateway's upstream. [01-ledger.ts](examples/01-ledger.ts) walks through a wrong write and its undo. [02-receipt-to-belief.ts](examples/02-receipt-to-belief.ts) runs the whole loop with the receipts package: a tool call gets a signed receipt, the receipt is verified, the belief taken from it is recorded citing the receipt, and later retracted. Run them with `node examples/<file>` from this directory, after `bun run build` at the repository root.
25
+ Five runnable examples, all executed by the test suite. [05-blast-radius.ts](examples/05-blast-radius.ts) walks from a retracted belief to everything that relied on it. [04-evals.ts](examples/04-evals.ts) scores the ledger and a naive store on the same memory incidents. [03-memory-behind-the-gateway.ts](examples/03-memory-behind-the-gateway.ts) runs the memory server as the gateway's upstream. [01-ledger.ts](examples/01-ledger.ts) walks through a wrong write and its undo. [02-receipt-to-belief.ts](examples/02-receipt-to-belief.ts) runs the whole loop with the receipts package: a tool call gets a signed receipt, the receipt is verified, the belief taken from it is recorded citing the receipt, and later retracted. Run them with `node examples/<file>` from this directory, after `bun run build` at the repository root.
26
26
 
27
27
  ## The memory server
28
28
 
@@ -42,13 +42,94 @@ In the gateway's config, the memory server is the upstream, and the grant names
42
42
  | --- | --- | --- |
43
43
  | `memory.write` | records a belief in a space, optionally superseding an earlier fact | `context.args.space`, `subject`, `predicate`, `value` |
44
44
  | `memory.read` | the facts believed at a moment, by space, subject, predicate, valid time, transaction time | the query |
45
+ | `memory.confirm` | lifts a quarantined fact to attested; accepted only through the gateway | `factId` |
45
46
  | `memory.retract` | undoes a belief, keeping it visible to questions about the past | `factId`, `reason` |
47
+ | `memory.forget` | erases the value from the ledger and every store, keeping the digest; the receipt is the certificate | `factId`, `reason` |
48
+ | `memory.get` | one fact by id in any state, for the gateway's policy lookups | `factId` |
46
49
  | `memory.history` | every event that touched a fact | `factId` |
47
50
 
48
- Trust tiers are Cedar policies over the space: `permit(principal, action == Action::"memory.write", resource) when { context.args.space == "team:support" };` lets this agent write team memory and nothing else. A read's receipt carries, as `observed`, the exact facts returned, so the ids the agent relied on are already on the record.
51
+ **Quarantine.** Every fact carries a provenance. A write that came through the gateway is `attested`: its actor is the agent named in a human-signed grant and its receipt exists. A write that arrived any other way is `claimed`, and claimed facts are quarantined: `memory.read` leaves them out unless the caller asks for `includeClaimed`, and the policy can refuse that. `memory.confirm`, accepted only through the gateway, lifts a claimed fact to attested with its own receipt and transaction time, so "was this fact still in quarantine on Tuesday" is answerable. A tool result an SDK-only agent wrote down cannot become something the rest of the fleet believes until an attested party says so. Over stdio the server has one client; shared over HTTP, below, gateways and direct writers feed one ledger and quarantine does its job.
52
+
53
+ **Policy over the fact being changed.** The gateway can look up the fact a write supersedes or a retraction targets before deciding, through its fact-lookup mechanism and the `memory.get` tool, and the policy then sees that fact's space, actor, and provenance as observed facts. This is the second half of trust tiers: a self-reported note in the org space can be superseded by anyone the grant allows, while an attested org fact cannot be displaced or retracted except by whoever the policy names. In the gateway config:
54
+
55
+ ```json
56
+ "facts": [
57
+ { "name": "target", "tool": "memory.get", "args": { "factId": "$args.supersedes" }, "forTools": ["memory.write"], "optional": true },
58
+ { "name": "target", "tool": "memory.get", "args": { "factId": "$args.factId" }, "forTools": ["memory.retract"] }
59
+ ]
60
+ ```
61
+
62
+ ```cedar
63
+ forbid(principal, action in [Action::"memory.write", Action::"memory.retract"], resource)
64
+ when { context.facts has target && context.facts.target.space == "org" && context.facts.target.provenance == "attested" };
65
+ ```
66
+
67
+ The lookup for writes is optional, so a write that supersedes nothing needs no lookup; the one for retractions is required. The denial receipt records the fact the policy saw, observed by the gateway. The test suite runs exactly this configuration.
68
+
69
+ **Attested executions.** Started with `--key memory.key`, the server signs every result for the receipt the gateway is issuing. A verifier given `memory.pub` then reports the execution of each memory call as attested by the memory server, not only observed by the gateway; the test suite verifies a write this way.
70
+
71
+ **Shared over HTTP.** `agent-custody-memory serve --ledger ./ledger.jsonl --http --port 8790 --token-env MEMORY_TOKEN` serves the same tools over Streamable HTTP, so several gateways, one per agent host, and, with `--allow-direct`, SDK-only agents share one ledger. That is the deployment quarantine was built for: writes arriving through a gateway are attested, writes arriving directly are claimed and hidden until a gateway confirms them, and the ledger tells them apart. A gateway reaches it with `"upstream": { "url": "http://127.0.0.1:8790/mcp", "tokenEnv": "MEMORY_TOKEN" }` in its config. Bind wider than loopback only behind the token. The test suite runs exactly this: one gateway writer, one direct writer, one ledger.
72
+
73
+ Trust tiers are Cedar policies over the space and, through `includeClaimed`, over quarantine: `permit(principal, action == Action::"memory.write", resource) when { context.args.space == "team:support" };` lets this agent write team memory and nothing else. A read's receipt carries, as `observed`, the exact facts returned, so the ids the agent relied on are already on the record.
49
74
 
50
75
  `--allow-direct` lets the server take calls without a gateway; then `source.receiptId` is null and `actor` is whatever the caller said, recorded as such. [examples/03-memory-behind-the-gateway.ts](examples/03-memory-behind-the-gateway.ts) runs the whole loop, including a denied write and a retraction that cites its own receipt.
51
76
 
77
+ ## Certified forget
78
+
79
+ A deletion demand is different from a correction. Retract keeps the record; forget erases the value. `memory.forget` removes the fact's value from the ledger file itself, replacing it with the value's digest so the ledger can still prove what it held without holding it, stops believing the fact, and removes it from every store behind the server. The result says exactly what happened: erased from the ledger, removed from which stores, still held by which, with the digest, the actor, and the reason.
80
+
81
+ The certificate is the receipt. Through the gateway, `memory.forget` is a receipted call whose result the gateway observed, so the signed, logged receipt records that the erasure happened, who asked for it, and what the stores answered. Hand that receipt to whoever demanded the deletion; anyone with the gateway's public key can verify it.
82
+
83
+ What forget does not reach, and the docs will not pretend otherwise: receipts. The receipt that recorded the original write carries the value in its request arguments, and the receipts of reads carry it in their results; they are signed and in a Merkle log, so they cannot be edited. Erasure from the receipt log is a retention policy on the log, and selective redaction of receipts is on the receipts roadmap.
84
+
85
+ ## Blast radius
86
+
87
+ When a belief turns out wrong, the next question is what relied on it. Two records answer it together. Every gateway receipt carries `consumed`: the fact ids the agent had been shown, through the gateway, before that call, which the memory server declares on each read. Every fact in the ledger carries the receipt that wrote it. Walking forward from a fact through those two links gives every later call and every belief written in those calls, transitively, and whether the root was retracted and which derived beliefs are still believed.
88
+
89
+ ```bash
90
+ agent-custody-memory blast --ledger ./ledger.jsonl --receipts ./receipts --fact <factId>
91
+ ```
92
+
93
+ ```
94
+ fact 3f2a…: acct:42 plan = "enterprise" (space team:support, by support-agent, attested)
95
+ retracted at 2026-09-07T10:12:04.118Z by support-agent: CRM sync bug: account is on the free plan
96
+ 3 call(s) made after the agent was shown it:
97
+ 2026-09-07T10:12:03.902Z memory.write executed receipt 7c1e…
98
+ ...
99
+ 2 belief(s) written in those calls, 2 still believed:
100
+ acct:42 discount = "20%" fact 9b04… STILL BELIEVED
101
+ acct:42 support_tier = "priority" fact e77d… STILL BELIEVED
102
+ ```
103
+
104
+ It is an upper bound by design: a call made after the agent had seen the fact is in the radius whether or not the agent used it, because no receipt can prove what a model attended to. What it never misses is the thing that matters, a downstream action or belief that did depend on the fact. [examples/05-blast-radius.ts](examples/05-blast-radius.ts) runs the whole loop.
105
+
106
+ ## Write-through to the stores you already use
107
+
108
+ The ledger is not a retrieval store, and it does not try to be. `src/stores.ts` puts it under the ones teams already run: a fact written through the memory server also lands in every configured store, with its custody metadata (fact id, space, actor, provenance, receipt id), the store's own id is recorded on the fact, and a retraction reaches the store by that id. Certified forget will be built on this: a deletion is only real once it has reached the stores that serve recall.
109
+
110
+ ```ts
111
+ import { MemoryClient } from "mem0ai";
112
+ import { ZepClient } from "@getzep/zep-cloud";
113
+ import { Ledger, createMemoryServer, mem0Store, zepStore } from "@agent-custody/state";
114
+
115
+ const stores = [
116
+ mem0Store(new MemoryClient({ apiKey: process.env.MEM0_API_KEY! }), { userId: "user_42" }), // infer is off: the memory is the fact, verbatim
117
+ zepStore(new ZepClient({ apiKey: process.env.ZEP_API_KEY! }), { userId: "user_42" }), // or { graphId } for a shared graph
118
+ ];
119
+ createMemoryServer(new Ledger("./ledger.jsonl"), { stores });
120
+ ```
121
+
122
+ Order matters and is fixed: the ledger's checks run first, so a write it would refuse never reaches a store; the stores are written next, so their ids can be recorded; the ledger appends last. A store that refuses the write fails the write and nothing is recorded anywhere. On retraction the ledger goes first, since custody must not depend on a store being up, and a store that fails to remove is named in the error so the caller knows recall may still serve the value. The adapters are typed structurally and carry no runtime dependency on either vendor; the tests drive the real `mem0ai` and `@getzep/zep-cloud` clients against fake endpoints, offline.
123
+
124
+ ## Scoring memory mutations
125
+
126
+ `src/evals.ts` is a harness that scores a memory system on what goes wrong after writes, not on recall. A scenario is a script of writes, reads, supersessions, and retractions with the value a correct system returns at each read. The score counts stale reads (a value served after a correction was known), contradictions (two values for one subject and predicate at once), blast radius (reads that served a write the scenario marks as bad), and correct reads. Any system behind the small `MemoryUnderTest` interface can be scored; this ledger and a naive overwrite store ship as the two reference points, and [examples/04-evals.ts](examples/04-evals.ts) prints both reports side by side.
127
+
128
+ ```ts
129
+ import { Ledger, ledgerUnderTest, runAll, SCENARIOS, formatReport } from "@agent-custody/state";
130
+ console.log(formatReport(await runAll(ledgerUnderTest(new Ledger("./ledger.jsonl")), SCENARIOS)));
131
+ ```
132
+
52
133
  ## The ledger
53
134
 
54
135
  `src/ledger.ts` is an append-only JSONL log of two kinds of event.
@@ -72,7 +153,12 @@ The ledger refuses to supersede a fact that is unknown, already superseded, or r
72
153
  ```
73
154
  src/ledger.ts the fact record, the two event kinds, as-of queries, supersession, retraction, JSONL persistence
74
155
  src/server.ts the ledger as MCP tools; source and actor taken from the gateway's _meta
75
- src/cli.ts agent-custody-memory serve
156
+ src/http.ts the memory server over Streamable HTTP with bearer auth, for a shared ledger
157
+ src/cli.ts agent-custody-memory serve (stdio or --http), blast
158
+ src/blast.ts blast radius: from receipts' consumed facts and the ledger's source receipts, forward
159
+ src/stores.ts write-through adapters: Mem0 and Zep, and the Store interface for others
160
+ src/evals.ts the memory-mutation harness: scenarios, scoring, report
161
+ src/evals-ledger.ts the ledger and a naive overwrite store behind the harness interface
76
162
  src/index.ts public surface
77
163
  examples/ runnable walkthroughs, each ends with OK and is run by the test suite
78
164
  test/ one test per question a platform owner asks after a memory incident
@@ -85,11 +171,19 @@ tsconfig.build.json emits dist/ for consumers; the repo itself runs the .ts dir
85
171
 
86
172
  - Bitemporal fact ledger with supersession, retraction, as-of and history queries, persisted as JSONL.
87
173
  - The memory server: the ledger as MCP tools behind the receipts gateway, with the source receipt id and the attested actor supplied by the gateway, policy over spaces, and a denial receipt for every refused write.
174
+ - Attested executions: with a key, the memory server signs its results for the gateway's receipt, so a verifier holding its public key sees memory calls as attested.
175
+ - The memory server over HTTP: one ledger shared by several gateways and direct writers, bearer-token auth, quarantine live.
176
+ - Certified forget: `memory.forget` erases a value from the ledger file keeping its digest, stops believing it, removes it from every store, and reports exactly what happened; the gateway's receipt of that call is the certificate.
177
+ - Policy over provenance: the gateway looks up the fact a write supersedes or a retraction targets, so policy decides on its space, actor, and provenance; a claimed fact can be displaced, an attested org fact cannot.
178
+ - Consumed facts and blast radius: the memory server declares the facts it serves, the gateway records them on every later receipt, and `blast` walks from a fact to every downstream call and derived belief, transitively, with its retraction status.
179
+ - Write-through adapters for Mem0 and Zep: every write lands in the store with custody metadata, the store id is recorded on the fact, retractions reach the store, and failures are ordered so nothing is half-recorded.
180
+ - The memory-mutation eval harness: stale reads, contradictions, blast radius, and correct reads over scripted incidents, scored the same way for the ledger and for anything behind the same interface.
181
+ - Quarantine: facts carry `attested` or `claimed` provenance; claimed facts are hidden from reads by default and a gateway-only `memory.confirm` lifts them, as a recorded event.
88
182
 
89
183
  **Next, in the order it pays off**
90
184
 
91
- 1. A consumed-facts field on receipts: the gateway records which fact ids a read returned, so later receipts in the session show what the agent relied on.
92
- 2. Blast radius: given a fact id, every downstream receipt and derived fact that cited it.
93
- 3. Trust tiers as Cedar policies over spaces and receipt provenance: a self-reported write cannot overwrite an org-space fact that was attested through the gateway.
94
- 4. Write-through adapters for existing memory stores, tested against the real packages.
95
- 5. Signed forget statements: a retention or deletion request produces a verifiable record of which facts were removed.
185
+ 1. Value-level quarantine: a value that came from untrusted tool output stays quarantined even when the actor is attested, until a second source or a human agrees.
186
+ 2. Retention windows and legal hold on the ledger: forget on a schedule, and a hold that refuses forget for named facts until lifted.
187
+ 3. A gateway with several upstreams under one grant, so memory and the tools an agent acts with share a session and blast radius reaches the emails sent, not only the beliefs written.
188
+ 4. The memory tools from Python, through the sidecar, so SDK-only Python agents can write claimed facts to a shared ledger.
189
+
@@ -0,0 +1,25 @@
1
+ import type { Fact, Ledger, RetractEvent } from "./ledger.ts";
2
+ /** The parts of a receipt this query needs. Decoded from a bundle's statement; nothing here is verified, so verify first. */
3
+ export interface ReceiptSummary {
4
+ receiptId: string;
5
+ timestamp: string;
6
+ tool: string;
7
+ status: string;
8
+ /** fact ids the agent had been shown before this call, from the receipt's consumed field */
9
+ consumed: string[];
10
+ }
11
+ /** Reads every bundle in a receipts directory. Order is by timestamp. */
12
+ export declare function loadReceipts(dir: string): ReceiptSummary[];
13
+ export interface BlastRadius {
14
+ fact: Fact | null;
15
+ /** every receipt for a call made after the agent had been shown this fact or one derived from it */
16
+ receipts: ReceiptSummary[];
17
+ /** every fact written in one of those calls, transitively */
18
+ derivedFacts: Fact[];
19
+ /** the retraction of the root fact, when it has been undone */
20
+ retraction: RetractEvent | null;
21
+ /** derived facts that are still believed; the ones a cleanup has to decide about */
22
+ stillBelieved: Fact[];
23
+ }
24
+ export declare function blastRadius(ledger: Ledger, receipts: ReceiptSummary[], factId: string): BlastRadius;
25
+ export declare function formatBlastRadius(b: BlastRadius, factId: string): string;
package/dist/blast.js ADDED
@@ -0,0 +1,62 @@
1
+ // Blast radius: given a fact, everything that relied on it. The receipts say which facts the agent had been shown
2
+ // before each call; the ledger says which facts were written in which call. Walking both, forward, gives every
3
+ // downstream action and every derived belief, transitively, and the retraction that undoes the belief if there is one.
4
+ import { readdirSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ /** Reads every bundle in a receipts directory. Order is by timestamp. */
7
+ export function loadReceipts(dir) {
8
+ const out = [];
9
+ for (const f of readdirSync(dir)) {
10
+ if (!f.endsWith(".json"))
11
+ continue;
12
+ const bundle = JSON.parse(readFileSync(join(dir, f), "utf8"));
13
+ if (!bundle.envelope?.payload)
14
+ continue;
15
+ const st = JSON.parse(Buffer.from(bundle.envelope.payload, "base64").toString());
16
+ const p = st.predicate;
17
+ if (!p?.receiptId)
18
+ continue;
19
+ out.push({ receiptId: p.receiptId, timestamp: p.timestamp, tool: p.tool?.name ?? "?", status: p.execution?.status ?? "?", consumed: Array.isArray(p.consumed?.factIds) ? p.consumed.factIds : [] });
20
+ }
21
+ return out.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
22
+ }
23
+ export function blastRadius(ledger, receipts, factId) {
24
+ const all = ledger.facts();
25
+ const byId = new Map(all.map((f) => [f.factId, f]));
26
+ const believed = new Set(ledger.asOf().map((f) => f.factId));
27
+ const seen = new Set([factId]);
28
+ const hit = new Map();
29
+ const derived = new Map();
30
+ let grew = true;
31
+ while (grew) {
32
+ grew = false;
33
+ for (const r of receipts) {
34
+ if (hit.has(r.receiptId) || !r.consumed.some((id) => seen.has(id)))
35
+ continue;
36
+ hit.set(r.receiptId, r);
37
+ grew = true;
38
+ }
39
+ for (const f of all) {
40
+ if (f.factId === factId || derived.has(f.factId) || !f.source.receiptId || !hit.has(f.source.receiptId))
41
+ continue;
42
+ derived.set(f.factId, f);
43
+ seen.add(f.factId);
44
+ grew = true;
45
+ }
46
+ }
47
+ const retraction = ledger.history(factId).find((e) => e.kind === "retract") ?? null;
48
+ const derivedFacts = [...derived.values()];
49
+ return { fact: byId.get(factId) ?? null, receipts: [...hit.values()].sort((a, b) => a.timestamp.localeCompare(b.timestamp)), derivedFacts, retraction, stillBelieved: derivedFacts.filter((f) => believed.has(f.factId)) };
50
+ }
51
+ export function formatBlastRadius(b, factId) {
52
+ const lines = [];
53
+ lines.push(b.fact ? `fact ${factId}: ${b.fact.subject} ${b.fact.predicate} = ${JSON.stringify(b.fact.value)} (space ${b.fact.space}, by ${b.fact.actor}, ${b.fact.provenance})` : `fact ${factId}: not in this ledger`);
54
+ lines.push(b.retraction ? `retracted at ${b.retraction.txTime} by ${b.retraction.actor}: ${b.retraction.reason}` : "not retracted");
55
+ lines.push(`${b.receipts.length} call(s) made after the agent was shown it:`);
56
+ for (const r of b.receipts)
57
+ lines.push(` ${r.timestamp} ${r.tool.padEnd(18)} ${r.status.padEnd(9)} receipt ${r.receiptId.slice(0, 8)}`);
58
+ lines.push(`${b.derivedFacts.length} belief(s) written in those calls, ${b.stillBelieved.length} still believed:`);
59
+ for (const f of b.derivedFacts)
60
+ lines.push(` ${f.subject} ${f.predicate} = ${JSON.stringify(f.value)} fact ${f.factId.slice(0, 8)} ${b.stillBelieved.includes(f) ? "STILL BELIEVED" : "no longer believed"}`);
61
+ return lines.join("\n");
62
+ }
package/dist/cli.js CHANGED
@@ -2,23 +2,51 @@
2
2
  import { parseArgs } from "node:util";
3
3
  import { Ledger } from "./ledger.js";
4
4
  import { createMemoryServer, serveStdio } from "./server.js";
5
+ import { blastRadius, formatBlastRadius, loadReceipts } from "./blast.js";
6
+ import { serveMemoryHttp } from "./http.js";
7
+ import { loadPrivateKey } from "@agent-custody/receipts";
5
8
  const USAGE = `agent-custody-memory <command>
6
9
 
7
- serve --ledger <ledger.jsonl> [--allow-direct] the memory server over stdio; run it as the receipts gateway's upstream.
10
+ serve --ledger <ledger.jsonl> [--allow-direct] [--key <memory.key>]
11
+ the memory server over stdio; run it as the receipts gateway's upstream.
12
+ --key signs every result for its receipt, so executions verify as attested by this server.
8
13
  By default it refuses calls that did not come through the gateway.
14
+ serve --ledger <ledger.jsonl> --http [--port 8790] [--host 127.0.0.1] [--token-env NAME] [--allow-direct]
15
+ the same server shared over HTTP: several gateways, one ledger
16
+ blast --ledger <ledger.jsonl> --receipts <dir> --fact <factId> [--json]
17
+ everything that relied on a fact: later calls, derived beliefs, and whether it was retracted
9
18
  `;
10
19
  async function main(argv) {
11
20
  const [cmd, ...rest] = argv;
12
21
  switch (cmd) {
13
22
  case "serve": {
14
- const { values } = parseArgs({ args: rest, options: { ledger: { type: "string" }, "allow-direct": { type: "boolean", default: false } } });
23
+ const { values } = parseArgs({ args: rest, options: { ledger: { type: "string" }, "allow-direct": { type: "boolean", default: false }, http: { type: "boolean", default: false }, port: { type: "string", default: "8790" }, host: { type: "string", default: "127.0.0.1" }, "token-env": { type: "string" }, key: { type: "string" } } });
15
24
  if (!values.ledger)
16
25
  throw new Error("serve needs --ledger");
17
26
  const ledger = new Ledger(values.ledger);
27
+ const identity = values.key ? loadPrivateKey(values.key) : undefined;
28
+ if (values.http) {
29
+ const token = values["token-env"] ? process.env[values["token-env"]] : undefined;
30
+ if (values["token-env"] && !token)
31
+ throw new Error(`serve: environment variable ${values["token-env"]} is not set`);
32
+ const running = await serveMemoryHttp(ledger, { port: Number(values.port), host: values.host, requireGateway: !values["allow-direct"], ...(token ? { tokens: [token] } : {}), ...(identity ? { identity } : {}) });
33
+ console.error(`agent-custody-memory: ${running.url} ledger=${values.ledger} events=${ledger.size} ${token ? "bearer token required" : "open"} ${values["allow-direct"] ? "direct calls allowed" : "gateway calls only"}`);
34
+ await new Promise((resolve) => process.once("SIGINT", resolve));
35
+ await running.close();
36
+ return 0;
37
+ }
18
38
  console.error(`agent-custody-memory: ledger=${values.ledger} events=${ledger.size} ${values["allow-direct"] ? "direct calls allowed" : "gateway calls only"}`);
19
- await serveStdio(createMemoryServer(ledger, { requireGateway: !values["allow-direct"] }));
39
+ await serveStdio(createMemoryServer(ledger, { requireGateway: !values["allow-direct"], ...(identity ? { identity } : {}) }));
20
40
  return 0;
21
41
  }
42
+ case "blast": {
43
+ const { values } = parseArgs({ args: rest, options: { ledger: { type: "string" }, receipts: { type: "string" }, fact: { type: "string" }, json: { type: "boolean", default: false } } });
44
+ if (!values.ledger || !values.receipts || !values.fact)
45
+ throw new Error("blast needs --ledger, --receipts, and --fact");
46
+ const b = blastRadius(new Ledger(values.ledger), loadReceipts(values.receipts), values.fact);
47
+ console.log(values.json ? JSON.stringify(b, null, 2) : formatBlastRadius(b, values.fact));
48
+ return b.fact ? 0 : 1;
49
+ }
22
50
  default:
23
51
  console.error(USAGE);
24
52
  return cmd === undefined || cmd === "--help" || cmd === "-h" ? 0 : 2;
@@ -0,0 +1,8 @@
1
+ import { Ledger } from "./ledger.ts";
2
+ import type { MemoryUnderTest } from "./evals.ts";
3
+ export declare function ledgerUnderTest(ledger: Ledger): MemoryUnderTest;
4
+ /**
5
+ * A key-value memory that overwrites on write and deletes on retract: the shape most memory layers have. It has no
6
+ * idea what a retracted value displaced, so a retraction leaves a hole where the earlier belief should return.
7
+ */
8
+ export declare function overwriteStoreUnderTest(): MemoryUnderTest;
@@ -0,0 +1,30 @@
1
+ export function ledgerUnderTest(ledger) {
2
+ return {
3
+ write: (i) => ({ id: ledger.assert({ ...i, provenance: "attested" }).fact.factId }),
4
+ read: (q) => ledger.asOf({ ...q, include: "attested" }).map(({ subject, predicate, value }) => ({ subject, predicate, value })),
5
+ retract: (i) => {
6
+ ledger.retract({ factId: i.id, actor: i.actor, reason: i.reason });
7
+ },
8
+ };
9
+ }
10
+ /**
11
+ * A key-value memory that overwrites on write and deletes on retract: the shape most memory layers have. It has no
12
+ * idea what a retracted value displaced, so a retraction leaves a hole where the earlier belief should return.
13
+ */
14
+ export function overwriteStoreUnderTest() {
15
+ const rows = new Map();
16
+ let n = 0;
17
+ return {
18
+ write: (i) => {
19
+ const id = `m${++n}`;
20
+ rows.set(`${i.space}|${i.subject}|${i.predicate}`, { id, subject: i.subject, predicate: i.predicate, value: i.value });
21
+ return { id };
22
+ },
23
+ read: (q) => [...rows.values()].filter((r) => r.subject === q.subject && (!q.predicate || r.predicate === q.predicate)).map(({ subject, predicate, value }) => ({ subject, predicate, value })),
24
+ retract: (i) => {
25
+ for (const [k, r] of rows)
26
+ if (r.id === i.id)
27
+ rows.delete(k);
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,74 @@
1
+ import type { Fact } from "./ledger.ts";
2
+ /** What the harness needs from a memory system. Reads return whatever the system would give an agent right now. */
3
+ export interface MemoryUnderTest {
4
+ write(input: {
5
+ subject: string;
6
+ predicate: string;
7
+ value: unknown;
8
+ space: string;
9
+ actor: string;
10
+ supersedes?: string;
11
+ }): Promise<{
12
+ id: string;
13
+ }> | {
14
+ id: string;
15
+ };
16
+ read(query: {
17
+ subject: string;
18
+ predicate?: string;
19
+ space?: string;
20
+ }): Promise<Pick<Fact, "subject" | "predicate" | "value">[]> | Pick<Fact, "subject" | "predicate" | "value">[];
21
+ retract(input: {
22
+ id: string;
23
+ actor: string;
24
+ reason: string;
25
+ }): Promise<void> | void;
26
+ }
27
+ export type Op = {
28
+ op: "write";
29
+ key: string;
30
+ subject: string;
31
+ predicate: string;
32
+ value: unknown;
33
+ space?: string;
34
+ actor?: string;
35
+ supersedes?: string; /** the harness marks this write as wrong; later reads that return its value count against the system */
36
+ bad?: boolean;
37
+ } | {
38
+ op: "read";
39
+ subject: string;
40
+ predicate?: string;
41
+ space?: string; /** the single value a correct system returns, or null for nothing */
42
+ expect: unknown | null;
43
+ } | {
44
+ op: "retract";
45
+ key: string;
46
+ actor?: string;
47
+ reason?: string;
48
+ };
49
+ export interface Scenario {
50
+ name: string;
51
+ ops: Op[];
52
+ }
53
+ export interface ScenarioScore {
54
+ name: string;
55
+ reads: number;
56
+ /** reads that returned a value the scenario says should no longer be believed */
57
+ staleReads: number;
58
+ /** reads that returned more than one value for one subject and predicate */
59
+ contradictions: number;
60
+ /** reads that returned the value of a write marked bad, before or after its retraction */
61
+ blastRadius: number;
62
+ /** reads whose result matched exactly what the scenario expected */
63
+ correctReads: number;
64
+ failures: string[];
65
+ }
66
+ export interface Report {
67
+ scenarios: ScenarioScore[];
68
+ totals: Omit<ScenarioScore, "name" | "failures">;
69
+ }
70
+ export declare function runScenario(system: MemoryUnderTest, scenario: Scenario): Promise<ScenarioScore>;
71
+ export declare function runAll(system: MemoryUnderTest, scenarios: Scenario[]): Promise<Report>;
72
+ /** The built-in scenarios: each is a memory incident a platform owner has actually had. */
73
+ export declare const SCENARIOS: Scenario[];
74
+ export declare function formatReport(r: Report): string;
package/dist/evals.js ADDED
@@ -0,0 +1,104 @@
1
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
2
+ export async function runScenario(system, scenario) {
3
+ const ids = new Map();
4
+ const badValues = [];
5
+ const score = { name: scenario.name, reads: 0, staleReads: 0, contradictions: 0, blastRadius: 0, correctReads: 0, failures: [] };
6
+ for (const [i, op] of scenario.ops.entries()) {
7
+ try {
8
+ if (op.op === "write") {
9
+ const supersedes = op.supersedes ? ids.get(op.supersedes) : undefined;
10
+ if (op.supersedes && !supersedes)
11
+ throw new Error(`write ${op.key} supersedes unknown key ${op.supersedes}`);
12
+ const { id } = await system.write({ subject: op.subject, predicate: op.predicate, value: op.value, space: op.space ?? "org", actor: op.actor ?? "agent", ...(supersedes ? { supersedes } : {}) });
13
+ ids.set(op.key, id);
14
+ if (op.bad)
15
+ badValues.push(op.value);
16
+ }
17
+ else if (op.op === "retract") {
18
+ const id = ids.get(op.key);
19
+ if (!id)
20
+ throw new Error(`retract of unknown key ${op.key}`);
21
+ await system.retract({ id, actor: op.actor ?? "admin", reason: op.reason ?? "wrong" });
22
+ }
23
+ else {
24
+ score.reads++;
25
+ const facts = await system.read({ subject: op.subject, ...(op.predicate ? { predicate: op.predicate } : {}), ...(op.space ? { space: op.space } : {}) });
26
+ const values = facts.map((f) => f.value);
27
+ const byPredicate = new Map();
28
+ for (const f of facts)
29
+ byPredicate.set(f.predicate, [...(byPredicate.get(f.predicate) ?? []), f.value]);
30
+ if ([...byPredicate.values()].some((vs) => vs.length > 1))
31
+ score.contradictions++;
32
+ if (values.some((v) => badValues.some((b) => same(b, v))))
33
+ score.blastRadius++;
34
+ const correct = op.expect === null ? values.length === 0 : values.length === 1 && same(values[0], op.expect);
35
+ if (correct)
36
+ score.correctReads++;
37
+ else if (values.length > 0 && !values.some((v) => same(v, op.expect)))
38
+ score.staleReads++;
39
+ else if (values.length > 1)
40
+ score.staleReads++;
41
+ }
42
+ }
43
+ catch (e) {
44
+ score.failures.push(`op ${i} (${op.op}): ${e instanceof Error ? e.message : String(e)}`);
45
+ }
46
+ }
47
+ return score;
48
+ }
49
+ export async function runAll(system, scenarios) {
50
+ const results = [];
51
+ for (const s of scenarios)
52
+ results.push(await runScenario(system, s));
53
+ const totals = results.reduce((t, s) => ({ reads: t.reads + s.reads, staleReads: t.staleReads + s.staleReads, contradictions: t.contradictions + s.contradictions, blastRadius: t.blastRadius + s.blastRadius, correctReads: t.correctReads + s.correctReads }), { reads: 0, staleReads: 0, contradictions: 0, blastRadius: 0, correctReads: 0 });
54
+ return { scenarios: results, totals };
55
+ }
56
+ /** The built-in scenarios: each is a memory incident a platform owner has actually had. */
57
+ export const SCENARIOS = [
58
+ {
59
+ name: "correction: a superseded value must stop being served",
60
+ ops: [
61
+ { op: "write", key: "a", subject: "acct:42", predicate: "plan", value: "pro" },
62
+ { op: "read", subject: "acct:42", predicate: "plan", expect: "pro" },
63
+ { op: "write", key: "b", subject: "acct:42", predicate: "plan", value: "enterprise", supersedes: "a" },
64
+ { op: "read", subject: "acct:42", predicate: "plan", expect: "enterprise" },
65
+ ],
66
+ },
67
+ {
68
+ name: "contradiction: two writers, one belief",
69
+ ops: [
70
+ { op: "write", key: "a", subject: "acct:42", predicate: "owner", value: "dana", actor: "support" },
71
+ { op: "write", key: "b", subject: "acct:42", predicate: "owner", value: "mallory", actor: "intern", supersedes: "a" },
72
+ { op: "read", subject: "acct:42", predicate: "owner", expect: "mallory" },
73
+ { op: "retract", key: "b", reason: "poisoned tool result" },
74
+ { op: "read", subject: "acct:42", predicate: "owner", expect: "dana" },
75
+ ],
76
+ },
77
+ {
78
+ name: "blast radius: a bad write is retracted and must not be served afterwards",
79
+ ops: [
80
+ { op: "write", key: "good", subject: "acct:7", predicate: "credit_limit", value: 1000 },
81
+ { op: "write", key: "bad", subject: "acct:7", predicate: "credit_limit", value: 1000000, supersedes: "good", bad: true },
82
+ { op: "read", subject: "acct:7", predicate: "credit_limit", expect: 1000000 },
83
+ { op: "read", subject: "acct:7", predicate: "credit_limit", expect: 1000000 },
84
+ { op: "retract", key: "bad" },
85
+ { op: "read", subject: "acct:7", predicate: "credit_limit", expect: 1000 },
86
+ { op: "read", subject: "acct:7", predicate: "credit_limit", expect: 1000 },
87
+ ],
88
+ },
89
+ {
90
+ name: "retraction with nothing underneath: the belief must disappear, not linger",
91
+ ops: [
92
+ { op: "write", key: "a", subject: "deal:9", predicate: "status", value: "signed", bad: true },
93
+ { op: "read", subject: "deal:9", predicate: "status", expect: "signed" },
94
+ { op: "retract", key: "a" },
95
+ { op: "read", subject: "deal:9", predicate: "status", expect: null },
96
+ ],
97
+ },
98
+ ];
99
+ export function formatReport(r) {
100
+ const row = (s) => `${s.name.padEnd(76)} reads ${String(s.reads).padStart(2)} correct ${String(s.correctReads).padStart(2)} stale ${String(s.staleReads).padStart(2)} contradictions ${String(s.contradictions).padStart(2)} blast ${String(s.blastRadius).padStart(2)}`;
101
+ const lines = r.scenarios.map((s) => row(s) + (s.failures.length ? `\n${s.failures.map((f) => ` ! ${f}`).join("\n")}` : ""));
102
+ lines.push(row({ name: "TOTAL", ...r.totals }));
103
+ return lines.join("\n");
104
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type IncomingMessage, type ServerResponse } from "node:http";
2
+ import type { Ledger } from "./ledger.ts";
3
+ import { type MemoryServerOptions } from "./server.ts";
4
+ export interface MemoryHttpOptions extends MemoryServerOptions {
5
+ port: number;
6
+ host?: string;
7
+ /** bearer tokens accepted; when empty, anyone who can reach the port may call */
8
+ tokens?: string[];
9
+ }
10
+ export interface RunningMemoryServer {
11
+ url: string;
12
+ close(): Promise<void>;
13
+ }
14
+ export declare function memoryHttpHandler(ledger: Ledger, opts: MemoryHttpOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
15
+ /** Starts the memory server over HTTP. Port 0 picks a free port. Host defaults to loopback; bind wider only behind auth. */
16
+ export declare function serveMemoryHttp(ledger: Ledger, opts: MemoryHttpOptions): Promise<RunningMemoryServer>;
package/dist/http.js ADDED
@@ -0,0 +1,46 @@
1
+ // The memory server over HTTP: one ledger shared by several gateways and, if allowed, direct writers. This is what
2
+ // makes quarantine a live deployment: attested writes arrive through gateways, self-reported ones directly, and the
3
+ // ledger tells them apart. Stateless Streamable HTTP, one MCP server instance per request over the shared ledger.
4
+ import { timingSafeEqual } from "node:crypto";
5
+ import { createServer } from "node:http";
6
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
7
+ import { createMemoryServer } from "./server.js";
8
+ export function memoryHttpHandler(ledger, opts) {
9
+ const tokens = opts.tokens ?? [];
10
+ const authorized = (req) => {
11
+ if (tokens.length === 0)
12
+ return true;
13
+ const h = req.headers.authorization ?? "";
14
+ const given = Buffer.from(h.startsWith("Bearer ") ? h.slice(7) : "");
15
+ return tokens.some((t) => Buffer.from(t).length === given.length && timingSafeEqual(Buffer.from(t), given));
16
+ };
17
+ return async (req, res) => {
18
+ if (!authorized(req)) {
19
+ res.writeHead(401, { "content-type": "application/json" });
20
+ res.end(JSON.stringify({ error: "unauthorized" }));
21
+ return;
22
+ }
23
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
24
+ const server = createMemoryServer(ledger, opts);
25
+ res.on("close", () => {
26
+ void transport.close();
27
+ void server.close();
28
+ });
29
+ await server.connect(transport);
30
+ await transport.handleRequest(req, res);
31
+ };
32
+ }
33
+ /** Starts the memory server over HTTP. Port 0 picks a free port. Host defaults to loopback; bind wider only behind auth. */
34
+ export function serveMemoryHttp(ledger, opts) {
35
+ const host = opts.host ?? "127.0.0.1";
36
+ const handler = memoryHttpHandler(ledger, opts);
37
+ const server = createServer((req, res) => {
38
+ void handler(req, res);
39
+ });
40
+ return new Promise((resolve) => {
41
+ server.listen(opts.port, host, () => {
42
+ const { port } = server.address();
43
+ resolve({ url: `http://${host}:${port}/mcp`, close: () => new Promise((r) => server.close(() => r())) });
44
+ });
45
+ });
46
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,13 @@
1
1
  export { Ledger } from "./ledger.ts";
2
- export type { AsOf, AssertEvent, AssertInput, Fact, LedgerEvent, RetractEvent, RetractInput, Source } from "./ledger.ts";
3
- export { AGENT_META_KEY, RECEIPT_META_KEY, SERVER_VERSION, TOOLS, createMemoryServer, serveStdio } from "./server.ts";
2
+ export type { AsOf, AssertEvent, AssertInput, ConfirmEvent, ConfirmInput, Fact, FactProvenance, ForgetEvent, ForgetInput, LedgerEvent, RetractEvent, RetractInput, Source } from "./ledger.ts";
3
+ export { AGENT_META_KEY, FACTS_META_KEY, RECEIPT_META_KEY, SERVER_VERSION, TOOLS, createMemoryServer, serveStdio } from "./server.ts";
4
4
  export type { MemoryServerOptions } from "./server.ts";
5
+ export { SCENARIOS, formatReport, runAll, runScenario } from "./evals.ts";
6
+ export type { MemoryUnderTest, Op, Report, Scenario, ScenarioScore } from "./evals.ts";
7
+ export { ledgerUnderTest, overwriteStoreUnderTest } from "./evals-ledger.ts";
8
+ export { factMetadata, factText, mem0Store, zepStore } from "./stores.ts";
9
+ export { blastRadius, formatBlastRadius, loadReceipts } from "./blast.ts";
10
+ export { memoryHttpHandler, serveMemoryHttp } from "./http.ts";
11
+ export type { MemoryHttpOptions, RunningMemoryServer } from "./http.ts";
12
+ export type { BlastRadius, ReceiptSummary } from "./blast.ts";
13
+ export type { Mem0Like, Mem0Options, Store, ZepLike, ZepOptions } from "./stores.ts";
package/dist/index.js CHANGED
@@ -1,3 +1,8 @@
1
1
  // Public surface of @agent-custody/state.
2
2
  export { Ledger } from "./ledger.js";
3
- export { AGENT_META_KEY, RECEIPT_META_KEY, SERVER_VERSION, TOOLS, createMemoryServer, serveStdio } from "./server.js";
3
+ export { AGENT_META_KEY, FACTS_META_KEY, RECEIPT_META_KEY, SERVER_VERSION, TOOLS, createMemoryServer, serveStdio } from "./server.js";
4
+ export { SCENARIOS, formatReport, runAll, runScenario } from "./evals.js";
5
+ export { ledgerUnderTest, overwriteStoreUnderTest } from "./evals-ledger.js";
6
+ export { factMetadata, factText, mem0Store, zepStore } from "./stores.js";
7
+ export { blastRadius, formatBlastRadius, loadReceipts } from "./blast.js";
8
+ export { memoryHttpHandler, serveMemoryHttp } from "./http.js";
package/dist/ledger.d.ts CHANGED
@@ -2,6 +2,12 @@
2
2
  export interface Source {
3
3
  receiptId: string | null;
4
4
  }
5
+ /**
6
+ * How far the write can be trusted. attested: it came through the receipts gateway, so the actor is the agent named in
7
+ * a human-signed grant and the receipt exists. claimed: it came from somewhere that only says who it is. A claimed
8
+ * fact is quarantined: the memory server does not return it by default until an attested party confirms it.
9
+ */
10
+ export type FactProvenance = "attested" | "claimed";
5
11
  export interface Fact {
6
12
  factId: string;
7
13
  subject: string;
@@ -12,6 +18,14 @@ export interface Fact {
12
18
  /** Who wrote it: a user id, an agent id, a tool name. */
13
19
  actor: string;
14
20
  source: Source;
21
+ provenance: FactProvenance;
22
+ /** present once the value has been erased: the value field is null and this is the digest of what it was */
23
+ forgotten?: {
24
+ valueDigest: string;
25
+ at: string;
26
+ };
27
+ /** ids of this fact in the retrieval stores it was written through to, by store name; absent when there are none */
28
+ external?: Record<string, string>;
15
29
  /** ISO timestamps. validTo is null while the fact is believed to still hold. */
16
30
  validFrom: string;
17
31
  validTo: string | null;
@@ -35,7 +49,31 @@ export interface RetractEvent {
35
49
  reason: string;
36
50
  source: Source;
37
51
  }
38
- export type LedgerEvent = AssertEvent | RetractEvent;
52
+ /** A confirm lifts a claimed fact to attested. Only an attested party can confirm; the event records who and which receipt. */
53
+ export interface ConfirmEvent {
54
+ eventId: string;
55
+ kind: "confirm";
56
+ txTime: string;
57
+ factId: string;
58
+ actor: string;
59
+ source: Source;
60
+ }
61
+ /**
62
+ * A forget is erasure, not correction. The fact's value is removed from the ledger file itself and replaced by its
63
+ * digest, so the ledger can still prove which value it held without holding it. The fact stops being believed.
64
+ */
65
+ export interface ForgetEvent {
66
+ eventId: string;
67
+ kind: "forget";
68
+ txTime: string;
69
+ factId: string;
70
+ actor: string;
71
+ reason: string;
72
+ source: Source;
73
+ /** sha256 of the canonical JSON of the erased value */
74
+ valueDigest: string;
75
+ }
76
+ export type LedgerEvent = AssertEvent | RetractEvent | ConfirmEvent | ForgetEvent;
39
77
  export interface AssertInput {
40
78
  subject: string;
41
79
  predicate: string;
@@ -43,10 +81,24 @@ export interface AssertInput {
43
81
  space: string;
44
82
  actor: string;
45
83
  source?: Source;
84
+ /** default claimed; the memory server sets attested for writes that came through the gateway */
85
+ provenance?: FactProvenance;
86
+ external?: Record<string, string>;
46
87
  validFrom?: string;
47
88
  confidence?: number;
48
89
  supersedes?: string;
49
90
  }
91
+ export interface ConfirmInput {
92
+ factId: string;
93
+ actor: string;
94
+ source?: Source;
95
+ }
96
+ export interface ForgetInput {
97
+ factId: string;
98
+ actor: string;
99
+ reason: string;
100
+ source?: Source;
101
+ }
50
102
  export interface RetractInput {
51
103
  factId: string;
52
104
  actor: string;
@@ -61,6 +113,8 @@ export interface AsOf {
61
113
  space?: string;
62
114
  subject?: string;
63
115
  predicate?: string;
116
+ /** "attested" returns only facts that were attested at txAt; default "all" */
117
+ include?: "attested" | "all";
64
118
  }
65
119
  export declare class Ledger {
66
120
  private readonly events;
@@ -70,12 +124,24 @@ export declare class Ledger {
70
124
  now?: () => Date;
71
125
  });
72
126
  get size(): number;
127
+ /** The checks assert makes, without appending. For callers that must do something irreversible before the append. */
128
+ validateAssert(input: AssertInput): void;
73
129
  assert(input: AssertInput): AssertEvent;
74
130
  retract(input: RetractInput): RetractEvent;
131
+ confirm(input: ConfirmInput): ConfirmEvent;
132
+ /**
133
+ * Erases a fact's value from the ledger file, keeping its digest, and stops believing it. The file is rewritten in
134
+ * place, which is the one thing an append-only ledger must do for a deletion demand. Everything else about the fact
135
+ * stays: who wrote it, when, from which receipt, and now who erased it and why.
136
+ */
137
+ forget(input: ForgetInput): ForgetEvent;
75
138
  /** The facts believed at a moment. Valid time answers "was it true then"; transaction time answers "did the ledger know it then". */
76
139
  asOf(q?: AsOf): Fact[];
77
- /** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its retraction. */
140
+ /** Every fact ever asserted, with supersession applied and retracted ones included, for audits that must see everything. */
141
+ facts(): Fact[];
142
+ /** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its confirmation, its retraction. */
78
143
  history(factId: string): LedgerEvent[];
144
+ private confirmedAt;
79
145
  private factById;
80
146
  private retractedAt;
81
147
  private append;
package/dist/ledger.js CHANGED
@@ -2,7 +2,8 @@
2
2
  // Bitemporal. Valid time is when a fact was true in the world; transaction time is when the ledger learned of it.
3
3
  // Nothing is ever edited in place. Correcting a belief is a new event, so "what did the agent believe at T" is always answerable.
4
4
  import { randomUUID } from "node:crypto";
5
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
5
+ import { createHash } from "node:crypto";
6
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
7
  import { dirname } from "node:path";
7
8
  export class Ledger {
8
9
  events = [];
@@ -24,9 +25,9 @@ export class Ledger {
24
25
  get size() {
25
26
  return this.events.length;
26
27
  }
27
- assert(input) {
28
- const txTime = this.now().toISOString();
29
- const validFrom = input.validFrom ?? txTime;
28
+ /** The checks assert makes, without appending. For callers that must do something irreversible before the append. */
29
+ validateAssert(input) {
30
+ const validFrom = input.validFrom ?? this.now().toISOString();
30
31
  if (input.supersedes !== undefined) {
31
32
  const prior = this.factById(input.supersedes);
32
33
  if (!prior)
@@ -38,6 +39,11 @@ export class Ledger {
38
39
  if (validFrom < prior.fact.validFrom)
39
40
  throw new Error(`replacement cannot start before the fact it supersedes`);
40
41
  }
42
+ }
43
+ assert(input) {
44
+ this.validateAssert(input);
45
+ const txTime = this.now().toISOString();
46
+ const validFrom = input.validFrom ?? txTime;
41
47
  const event = {
42
48
  eventId: randomUUID(),
43
49
  kind: "assert",
@@ -50,6 +56,8 @@ export class Ledger {
50
56
  space: input.space,
51
57
  actor: input.actor,
52
58
  source: input.source ?? { receiptId: null },
59
+ provenance: input.provenance ?? "claimed",
60
+ ...(input.external && Object.keys(input.external).length > 0 ? { external: input.external } : {}),
53
61
  validFrom,
54
62
  validTo: null,
55
63
  confidence: input.confidence ?? null,
@@ -76,17 +84,56 @@ export class Ledger {
76
84
  this.append(event);
77
85
  return event;
78
86
  }
87
+ confirm(input) {
88
+ const prior = this.factById(input.factId);
89
+ if (!prior)
90
+ throw new Error(`cannot confirm unknown fact ${input.factId}`);
91
+ if (this.retractedAt(input.factId))
92
+ throw new Error(`fact ${input.factId} is retracted`);
93
+ if (prior.fact.provenance === "attested" || this.confirmedAt(input.factId))
94
+ throw new Error(`fact ${input.factId} is already attested`);
95
+ const event = { eventId: randomUUID(), kind: "confirm", txTime: this.now().toISOString(), factId: input.factId, actor: input.actor, source: input.source ?? { receiptId: null } };
96
+ this.append(event);
97
+ return event;
98
+ }
99
+ /**
100
+ * Erases a fact's value from the ledger file, keeping its digest, and stops believing it. The file is rewritten in
101
+ * place, which is the one thing an append-only ledger must do for a deletion demand. Everything else about the fact
102
+ * stays: who wrote it, when, from which receipt, and now who erased it and why.
103
+ */
104
+ forget(input) {
105
+ const prior = this.factById(input.factId);
106
+ if (!prior)
107
+ throw new Error(`cannot forget unknown fact ${input.factId}`);
108
+ if (prior.fact.forgotten)
109
+ throw new Error(`fact ${input.factId} is already forgotten`);
110
+ const txTime = this.now().toISOString();
111
+ const valueDigest = createHash("sha256").update(canonical(prior.fact.value)).digest("hex");
112
+ for (const e of this.events) {
113
+ if (e.kind === "assert" && e.fact.factId === input.factId) {
114
+ e.fact.value = null;
115
+ e.fact.forgotten = { valueDigest, at: txTime };
116
+ }
117
+ }
118
+ const event = { eventId: randomUUID(), kind: "forget", txTime, factId: input.factId, actor: input.actor, reason: input.reason, source: input.source ?? { receiptId: null }, valueDigest };
119
+ this.events.push(event);
120
+ const tmp = `${this.file}.tmp`;
121
+ writeFileSync(tmp, this.events.map((e) => JSON.stringify(e)).join("\n") + "\n");
122
+ renameSync(tmp, this.file);
123
+ return event;
124
+ }
79
125
  /** The facts believed at a moment. Valid time answers "was it true then"; transaction time answers "did the ledger know it then". */
80
126
  asOf(q = {}) {
81
127
  const validAt = q.validAt ?? this.now().toISOString();
82
128
  const txAt = q.txAt ?? this.now().toISOString();
83
129
  const known = this.events.filter((e) => e.txTime <= txAt);
84
- const retracted = new Set(known.filter((e) => e.kind === "retract").map((e) => e.factId));
130
+ const retracted = new Set(known.filter((e) => e.kind === "retract" || e.kind === "forget").map((e) => e.factId));
131
+ const confirmed = new Set(known.filter((e) => e.kind === "confirm").map((e) => e.factId));
85
132
  const facts = new Map();
86
133
  for (const e of known) {
87
134
  if (e.kind !== "assert")
88
135
  continue;
89
- facts.set(e.fact.factId, { ...e.fact });
136
+ facts.set(e.fact.factId, { ...e.fact, provenance: confirmed.has(e.fact.factId) ? "attested" : e.fact.provenance });
90
137
  if (e.supersedes && facts.has(e.supersedes) && !retracted.has(e.fact.factId)) {
91
138
  facts.get(e.supersedes).validTo = e.fact.validFrom;
92
139
  }
@@ -96,12 +143,28 @@ export class Ledger {
96
143
  (f.validTo === null || validAt < f.validTo) &&
97
144
  (q.space === undefined || f.space === q.space) &&
98
145
  (q.subject === undefined || f.subject === q.subject) &&
99
- (q.predicate === undefined || f.predicate === q.predicate));
146
+ (q.predicate === undefined || f.predicate === q.predicate) &&
147
+ (q.include !== "attested" || f.provenance === "attested"));
100
148
  }
101
- /** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its retraction. */
149
+ /** Every fact ever asserted, with supersession applied and retracted ones included, for audits that must see everything. */
150
+ facts() {
151
+ const out = new Map();
152
+ for (const e of this.events) {
153
+ if (e.kind !== "assert")
154
+ continue;
155
+ out.set(e.fact.factId, { ...e.fact });
156
+ if (e.supersedes && out.has(e.supersedes) && !this.retractedAt(e.fact.factId))
157
+ out.get(e.supersedes).validTo = e.fact.validFrom;
158
+ }
159
+ return [...out.values()];
160
+ }
161
+ /** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its confirmation, its retraction. */
102
162
  history(factId) {
103
163
  return this.events.filter((e) => (e.kind === "assert" ? e.fact.factId === factId || e.supersedes === factId : e.factId === factId));
104
164
  }
165
+ confirmedAt(factId) {
166
+ return this.events.some((e) => e.kind === "confirm" && e.factId === factId);
167
+ }
105
168
  factById(factId) {
106
169
  let found;
107
170
  for (const e of this.events) {
@@ -113,10 +176,28 @@ export class Ledger {
113
176
  return found;
114
177
  }
115
178
  retractedAt(factId) {
116
- return this.events.some((e) => e.kind === "retract" && e.factId === factId);
179
+ return this.events.some((e) => (e.kind === "retract" || e.kind === "forget") && e.factId === factId);
117
180
  }
118
181
  append(event) {
119
182
  appendFileSync(this.file, JSON.stringify(event) + "\n");
120
183
  this.events.push(event);
121
184
  }
122
185
  }
186
+ /** Canonical JSON: sorted keys, no whitespace, undefined dropped. The same encoding the receipts package uses. */
187
+ function canonical(value) {
188
+ const sort = (v) => {
189
+ if (Array.isArray(v))
190
+ return v.map(sort);
191
+ if (v && typeof v === "object") {
192
+ const o = {};
193
+ for (const k of Object.keys(v).sort()) {
194
+ const x = v[k];
195
+ if (x !== undefined)
196
+ o[k] = sort(x);
197
+ }
198
+ return o;
199
+ }
200
+ return v;
201
+ };
202
+ return JSON.stringify(sort(value));
203
+ }
package/dist/server.d.ts CHANGED
@@ -1,14 +1,22 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { type Tool } from "@modelcontextprotocol/sdk/types.js";
3
+ import { type KeyPair } from "@agent-custody/receipts";
3
4
  import type { Ledger } from "./ledger.ts";
5
+ import type { Store } from "./stores.ts";
4
6
  export declare const SERVER_VERSION = "0.1.0";
5
7
  /** The same keys the receipts gateway sets on the upstream call. Duplicated here so this package needs no runtime import from receipts. */
6
8
  export declare const RECEIPT_META_KEY = "agent-custody/receipt";
7
9
  export declare const AGENT_META_KEY = "agent-custody/agent";
10
+ /** Set on read results: the ids of the facts served, so the gateway can record what the agent was shown. */
11
+ export declare const FACTS_META_KEY = "agent-custody/facts";
8
12
  export declare const TOOLS: Tool[];
9
13
  export interface MemoryServerOptions {
10
14
  /** Refuse calls that did not come through the gateway, i.e. carry no receipt id. On by default when served from the CLI. */
11
15
  requireGateway?: boolean;
16
+ /** Retrieval stores every write goes through to and every retraction reaches. A store that refuses a write fails the write; nothing is recorded. */
17
+ stores?: Store[];
18
+ /** With a key, every result to a gateway call is signed for that receipt, so a verifier holding the public key sees the execution as attested by this server. */
19
+ identity?: KeyPair;
12
20
  }
13
21
  export declare function createMemoryServer(ledger: Ledger, opts?: MemoryServerOptions): Server;
14
22
  /** Serves over stdio, the way the gateway spawns it. Diagnostics must go to stderr. */
package/dist/server.js CHANGED
@@ -7,10 +7,13 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { z } from "zod";
10
+ import { signResult } from "@agent-custody/receipts";
10
11
  export const SERVER_VERSION = "0.1.0";
11
12
  /** The same keys the receipts gateway sets on the upstream call. Duplicated here so this package needs no runtime import from receipts. */
12
13
  export const RECEIPT_META_KEY = "agent-custody/receipt";
13
14
  export const AGENT_META_KEY = "agent-custody/agent";
15
+ /** Set on read results: the ids of the facts served, so the gateway can record what the agent was shown. */
16
+ export const FACTS_META_KEY = "agent-custody/facts";
14
17
  const iso = z.string().datetime({ offset: true });
15
18
  const Write = z.object({
16
19
  subject: z.string().min(1),
@@ -23,9 +26,12 @@ const Write = z.object({
23
26
  confidence: z.number().min(0).max(1).optional(),
24
27
  supersedes: z.string().min(1).optional(),
25
28
  });
26
- const Read = z.object({ subject: z.string().min(1).optional(), predicate: z.string().min(1).optional(), space: z.string().min(1).optional(), validAt: iso.optional(), txAt: iso.optional() });
29
+ const Read = z.object({ subject: z.string().min(1).optional(), predicate: z.string().min(1).optional(), space: z.string().min(1).optional(), validAt: iso.optional(), txAt: iso.optional(), includeClaimed: z.boolean().optional() });
30
+ const Confirm = z.object({ factId: z.string().min(1) });
27
31
  const Retract = z.object({ factId: z.string().min(1), reason: z.string().min(1), actor: z.string().min(1).optional() });
28
32
  const History = z.object({ factId: z.string().min(1) });
33
+ const Get = z.object({ factId: z.string().min(1) });
34
+ const Forget = z.object({ factId: z.string().min(1), reason: z.string().min(1) });
29
35
  const str = { type: "string" };
30
36
  export const TOOLS = [
31
37
  {
@@ -35,14 +41,29 @@ export const TOOLS = [
35
41
  },
36
42
  {
37
43
  name: "memory.read",
38
- description: "The facts believed at a moment. validAt asks whether a fact was true then; txAt asks whether the ledger knew it then. Both default to now. Filter by space, subject, predicate.",
39
- inputSchema: { type: "object", properties: { subject: str, predicate: str, space: str, validAt: str, txAt: str } },
44
+ description: "The facts believed at a moment. validAt asks whether a fact was true then; txAt asks whether the ledger knew it then. Both default to now. Filter by space, subject, predicate. Quarantined (claimed, unconfirmed) facts are left out unless includeClaimed is true.",
45
+ inputSchema: { type: "object", properties: { subject: str, predicate: str, space: str, validAt: str, txAt: str, includeClaimed: { type: "boolean" } } },
46
+ },
47
+ {
48
+ name: "memory.confirm",
49
+ description: "Lift a quarantined (claimed) fact to attested. Only accepted through the gateway, so the confirming actor is the one named in the signed grant.",
50
+ inputSchema: { type: "object", properties: { factId: str }, required: ["factId"] },
40
51
  },
41
52
  {
42
53
  name: "memory.retract",
43
54
  description: "Undo a belief: the fact leaves the present, stays visible to questions about the past, and whatever it superseded is believed again.",
44
55
  inputSchema: { type: "object", properties: { factId: str, reason: str, actor: str }, required: ["factId", "reason"] },
45
56
  },
57
+ {
58
+ name: "memory.forget",
59
+ description: "Erase a fact's value: from the ledger file, keeping only its digest, and from every store behind the server. The fact stops being believed. The receipt for this call, with the result the gateway observed, is the certificate that the erasure happened.",
60
+ inputSchema: { type: "object", properties: { factId: str, reason: str }, required: ["factId", "reason"] },
61
+ },
62
+ {
63
+ name: "memory.get",
64
+ description: "One fact by id, whatever its state: its space, actor, provenance, source receipt, validity. Meant for the gateway's fact lookups, so policy can decide on the fact a write supersedes or a retraction targets.",
65
+ inputSchema: { type: "object", properties: { factId: str }, required: ["factId"] },
66
+ },
46
67
  {
47
68
  name: "memory.history",
48
69
  description: "Every event that touched a fact, oldest first.",
@@ -57,37 +78,109 @@ export function createMemoryServer(ledger, opts = {}) {
57
78
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
58
79
  const meta = (req.params._meta ?? {});
59
80
  const receiptId = typeof meta[RECEIPT_META_KEY] === "string" ? meta[RECEIPT_META_KEY] : null;
81
+ const result = await handle(req.params.name, req.params.arguments ?? {}, meta, receiptId);
82
+ return opts.identity && receiptId ? signResult(result, opts.identity, receiptId, req.params.name) : result;
83
+ });
84
+ async function handle(name, args, meta, receiptId) {
60
85
  const gatewayAgent = typeof meta[AGENT_META_KEY] === "string" ? meta[AGENT_META_KEY] : null;
61
86
  if (opts.requireGateway && !receiptId)
62
87
  return fail("memory server accepts calls only through the receipts gateway; no receipt id on this call");
63
- const args = req.params.arguments ?? {};
64
88
  const actorFor = (claimed) => gatewayAgent ?? claimed ?? "anonymous";
89
+ // A write is attested when it came through the gateway: the actor is from a signed grant and the receipt exists.
90
+ const provenance = receiptId && gatewayAgent ? "attested" : "claimed";
65
91
  try {
66
- switch (req.params.name) {
92
+ switch (name) {
67
93
  case "memory.write": {
68
94
  const a = Write.parse(args);
69
- const ev = ledger.assert({ subject: a.subject, predicate: a.predicate, value: a.value ?? null, space: a.space, actor: actorFor(a.actor), source: { receiptId }, ...(a.validFrom ? { validFrom: a.validFrom } : {}), ...(a.confidence !== undefined ? { confidence: a.confidence } : {}), ...(a.supersedes ? { supersedes: a.supersedes } : {}) });
95
+ const input = { subject: a.subject, predicate: a.predicate, value: a.value ?? null, space: a.space, actor: actorFor(a.actor), source: { receiptId }, provenance, ...(a.validFrom ? { validFrom: a.validFrom } : {}), ...(a.confidence !== undefined ? { confidence: a.confidence } : {}), ...(a.supersedes ? { supersedes: a.supersedes } : {}) };
96
+ // The stores are written first, so their ids can be recorded on the fact; the ledger's checks run beforehand
97
+ // so a write the ledger would refuse never reaches a store.
98
+ ledger.validateAssert(input);
99
+ const external = {};
100
+ const preview = { ...input, factId: "pending", validFrom: input.validFrom ?? new Date().toISOString(), validTo: null, confidence: input.confidence ?? null };
101
+ for (const store of opts.stores ?? [])
102
+ external[store.name] = await store.put(preview);
103
+ const ev = ledger.assert({ ...input, external });
70
104
  return json({ fact: ev.fact, eventId: ev.eventId, txTime: ev.txTime, supersedes: ev.supersedes });
71
105
  }
72
106
  case "memory.read": {
73
- const q = Read.parse(args);
74
- return json({ facts: ledger.asOf(q) });
107
+ const { includeClaimed, ...q } = Read.parse(args);
108
+ const facts = ledger.asOf({ ...q, include: includeClaimed ? "all" : "attested" });
109
+ return { ...json({ facts }), _meta: { [FACTS_META_KEY]: facts.map((f) => f.factId) } };
75
110
  }
76
111
  case "memory.retract": {
77
112
  const a = Retract.parse(args);
113
+ const fact = ledger.history(a.factId).find((e) => e.kind === "assert" && e.fact.factId === a.factId)?.fact;
78
114
  const ev = ledger.retract({ factId: a.factId, actor: actorFor(a.actor), reason: a.reason, source: { receiptId } });
79
- return json({ eventId: ev.eventId, factId: ev.factId, txTime: ev.txTime, actor: ev.actor, reason: ev.reason, source: ev.source });
115
+ // The ledger is retracted first: custody must not depend on a store being up. A store that fails to remove
116
+ // is reported, so the caller knows recall may still serve the value.
117
+ const stillHeld = [];
118
+ for (const store of opts.stores ?? []) {
119
+ const id = fact?.external?.[store.name];
120
+ if (!id)
121
+ continue;
122
+ try {
123
+ await store.remove(id, fact);
124
+ }
125
+ catch (e) {
126
+ stillHeld.push(`${store.name}: ${e instanceof Error ? e.message : String(e)}`);
127
+ }
128
+ }
129
+ const out = { eventId: ev.eventId, factId: ev.factId, txTime: ev.txTime, actor: ev.actor, reason: ev.reason, source: ev.source, removedFrom: (opts.stores ?? []).map((s) => s.name).filter((n) => fact?.external?.[n] && !stillHeld.some((h) => h.startsWith(n))) };
130
+ if (stillHeld.length > 0)
131
+ return { isError: true, content: [{ type: "text", text: `retracted in the ledger, but still held by ${stillHeld.join("; ")}` }, { type: "text", text: JSON.stringify(out) }] };
132
+ return json(out);
133
+ }
134
+ case "memory.confirm": {
135
+ if (provenance !== "attested")
136
+ return fail("confirmation must come through the receipts gateway; a self-reported caller cannot lift a fact out of quarantine");
137
+ const a = Confirm.parse(args);
138
+ const ev = ledger.confirm({ factId: a.factId, actor: actorFor(undefined), source: { receiptId } });
139
+ return json({ eventId: ev.eventId, factId: ev.factId, txTime: ev.txTime, actor: ev.actor, source: ev.source });
140
+ }
141
+ case "memory.forget": {
142
+ const a = Forget.parse(args);
143
+ const fact = ledger.facts().find((f) => f.factId === a.factId);
144
+ const ev = ledger.forget({ factId: a.factId, actor: actorFor(undefined), reason: a.reason, source: { receiptId } });
145
+ const removedFrom = [];
146
+ const stillHeld = [];
147
+ for (const store of opts.stores ?? []) {
148
+ const id = fact?.external?.[store.name];
149
+ if (!id)
150
+ continue;
151
+ try {
152
+ await store.remove(id, fact);
153
+ removedFrom.push(store.name);
154
+ }
155
+ catch (e) {
156
+ stillHeld.push(`${store.name}: ${e instanceof Error ? e.message : String(e)}`);
157
+ }
158
+ }
159
+ const out = { factId: ev.factId, valueDigest: ev.valueDigest, txTime: ev.txTime, actor: ev.actor, reason: ev.reason, source: ev.source, erasedFromLedger: true, removedFrom, stillHeld };
160
+ if (stillHeld.length > 0)
161
+ return { isError: true, content: [{ type: "text", text: `erased from the ledger, but still held by ${stillHeld.join("; ")}` }, { type: "text", text: JSON.stringify(out) }] };
162
+ return json(out);
163
+ }
164
+ case "memory.get": {
165
+ const a = Get.parse(args);
166
+ const f = ledger.facts().find((x) => x.factId === a.factId);
167
+ if (!f)
168
+ return fail(`unknown fact ${a.factId}`);
169
+ const retracted = ledger.history(a.factId).some((e) => e.kind === "retract");
170
+ // Cedar has no null: absent fields stay absent, so a policy tests them with `has`.
171
+ const clean = Object.fromEntries(Object.entries({ ...f, source: f.source.receiptId ?? undefined, retracted }).filter(([, v]) => v !== null && v !== undefined));
172
+ return json(clean);
80
173
  }
81
174
  case "memory.history":
82
175
  return json({ events: ledger.history(History.parse(args).factId) });
83
176
  default:
84
- return fail(`unknown tool ${req.params.name}`);
177
+ return fail(`unknown tool ${name}`);
85
178
  }
86
179
  }
87
180
  catch (e) {
88
181
  return fail(e instanceof z.ZodError ? `invalid arguments: ${e.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}` : String(e instanceof Error ? e.message : e));
89
182
  }
90
- });
183
+ }
91
184
  return server;
92
185
  }
93
186
  /** Serves over stdio, the way the gateway spawns it. Diagnostics must go to stderr. */
@@ -0,0 +1,55 @@
1
+ import type { Fact } from "./ledger.ts";
2
+ export interface Store {
3
+ /** the key under which the store's id is recorded on the fact */
4
+ readonly name: string;
5
+ /** writes the fact and returns the store's own id for it */
6
+ put(fact: Fact): Promise<string>;
7
+ /** removes the fact from the store; called on retraction */
8
+ remove(externalId: string, fact: Fact): Promise<void>;
9
+ }
10
+ /** One line a retrieval store can index: what the fact says, in words. */
11
+ export declare function factText(f: Fact): string;
12
+ /** Metadata every store receives alongside the text, so a memory can always be traced back to its custody. */
13
+ export declare function factMetadata(f: Fact): Record<string, unknown>;
14
+ /** The subset of mem0ai's MemoryClient this adapter uses. */
15
+ export interface Mem0Like {
16
+ add(messages: {
17
+ role: "user" | "assistant";
18
+ content: string;
19
+ }[], options?: Record<string, unknown>): Promise<{
20
+ id?: string;
21
+ }[]>;
22
+ delete(memoryId: string): Promise<unknown>;
23
+ }
24
+ export interface Mem0Options {
25
+ userId: string;
26
+ /** Mem0 extracts memories with an LLM when infer is true. Off by default, so the memory is the fact, verbatim. */
27
+ infer?: boolean;
28
+ }
29
+ export declare function mem0Store(client: Mem0Like, opts: Mem0Options): Store;
30
+ /** The subset of @getzep/zep-cloud's ZepClient this adapter uses. */
31
+ export interface ZepLike {
32
+ graph: {
33
+ add(request: {
34
+ userId?: string;
35
+ graphId?: string;
36
+ type: "json" | "text";
37
+ data: string;
38
+ sourceDescription?: string;
39
+ metadata?: Record<string, unknown>;
40
+ }): Promise<{
41
+ uuid: string;
42
+ }>;
43
+ episode: {
44
+ delete(uuid: string): Promise<unknown>;
45
+ };
46
+ };
47
+ }
48
+ export type ZepOptions = {
49
+ userId: string;
50
+ graphId?: undefined;
51
+ } | {
52
+ graphId: string;
53
+ userId?: undefined;
54
+ };
55
+ export declare function zepStore(client: ZepLike, opts: ZepOptions): Store;
package/dist/stores.js ADDED
@@ -0,0 +1,38 @@
1
+ /** One line a retrieval store can index: what the fact says, in words. */
2
+ export function factText(f) {
3
+ return `${f.subject} ${f.predicate}: ${typeof f.value === "string" ? f.value : JSON.stringify(f.value)}`;
4
+ }
5
+ /** Metadata every store receives alongside the text, so a memory can always be traced back to its custody. */
6
+ export function factMetadata(f) {
7
+ return { factId: f.factId, space: f.space, actor: f.actor, provenance: f.provenance, receiptId: f.source.receiptId, validFrom: f.validFrom, source: "agent-custody" };
8
+ }
9
+ export function mem0Store(client, opts) {
10
+ return {
11
+ name: "mem0",
12
+ async put(fact) {
13
+ const results = await client.add([{ role: "user", content: factText(fact) }], { user_id: opts.userId, infer: opts.infer ?? false, metadata: factMetadata(fact) });
14
+ const id = results.find((r) => typeof r.id === "string")?.id;
15
+ if (!id)
16
+ throw new Error("mem0 returned no memory id");
17
+ return id;
18
+ },
19
+ async remove(externalId) {
20
+ await client.delete(externalId);
21
+ },
22
+ };
23
+ }
24
+ export function zepStore(client, opts) {
25
+ return {
26
+ name: "zep",
27
+ async put(fact) {
28
+ const target = opts.graphId ? { graphId: opts.graphId } : { userId: opts.userId };
29
+ const episode = await client.graph.add({ ...target, type: "json", data: JSON.stringify({ subject: fact.subject, predicate: fact.predicate, value: fact.value, ...factMetadata(fact) }), sourceDescription: "agent-custody", metadata: factMetadata(fact) });
30
+ if (!episode?.uuid)
31
+ throw new Error("zep returned no episode uuid");
32
+ return episode.uuid;
33
+ },
34
+ async remove(externalId) {
35
+ await client.graph.episode.delete(externalId);
36
+ },
37
+ };
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-custody/state",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Governed memory for AI agents: a fact ledger with provenance, valid time, rollback, and lineage, built on @agent-custody/receipts",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -33,13 +33,15 @@
33
33
  "node": ">=22"
34
34
  },
35
35
  "dependencies": {
36
- "@agent-custody/receipts": "0.1.3",
36
+ "@agent-custody/receipts": "0.1.5",
37
37
  "@modelcontextprotocol/sdk": "^1.30.0",
38
38
  "zod": "^4.5.4"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^26.4.1",
42
42
  "typescript": "^7.0.2",
43
- "vitest": "^5.0.0"
43
+ "vitest": "^5.0.0",
44
+ "mem0ai": "^3.1.8",
45
+ "@getzep/zep-cloud": "^3.28.0"
44
46
  }
45
47
  }