@agent-custody/state 0.1.2 → 0.1.4

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,62 @@ 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
- Two runnable examples, both executed by the test suite. [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
+ Four runnable examples, all executed by the test suite. [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
+
27
+ ## The memory server
28
+
29
+ The ledger as MCP tools, meant to run as the upstream of the receipts gateway. Behind the gateway, every write and read is checked by the Cedar policy and gets a signed receipt, and two things reach this server in the call's `_meta` that no caller can supply: the receipt id, which becomes the fact's `source`, and the agent from the signed delegation grant, which becomes the fact's `actor`. A caller's own claims about either are ignored.
30
+
31
+ ```bash
32
+ agent-custody-memory serve --ledger ./ledger.jsonl # over stdio; refuses calls that did not come through the gateway
33
+ ```
34
+
35
+ In the gateway's config, the memory server is the upstream, and the grant names the memory tools as scopes:
36
+
37
+ ```json
38
+ { "upstream": { "command": "agent-custody-memory", "args": ["serve", "--ledger", "/abs/path/ledger.jsonl"] }, ... }
39
+ ```
40
+
41
+ | tool | does | policy sees |
42
+ | --- | --- | --- |
43
+ | `memory.write` | records a belief in a space, optionally superseding an earlier fact | `context.args.space`, `subject`, `predicate`, `value` |
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` |
46
+ | `memory.retract` | undoes a belief, keeping it visible to questions about the past | `factId`, `reason` |
47
+ | `memory.history` | every event that touched a fact | `factId` |
48
+
49
+ **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. Today the server has one client, over stdio, so claimed facts reach a gateway-fronted ledger by being there before it was put under the gateway, or by a direct writer sharing the file; a shared memory server over HTTP, on the plan, is what makes mixed attested and self-reported writers a live deployment.
50
+
51
+ 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.
52
+
53
+ `--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.
54
+
55
+ ## Write-through to the stores you already use
56
+
57
+ 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.
58
+
59
+ ```ts
60
+ import { MemoryClient } from "mem0ai";
61
+ import { ZepClient } from "@getzep/zep-cloud";
62
+ import { Ledger, createMemoryServer, mem0Store, zepStore } from "@agent-custody/state";
63
+
64
+ const stores = [
65
+ mem0Store(new MemoryClient({ apiKey: process.env.MEM0_API_KEY! }), { userId: "user_42" }), // infer is off: the memory is the fact, verbatim
66
+ zepStore(new ZepClient({ apiKey: process.env.ZEP_API_KEY! }), { userId: "user_42" }), // or { graphId } for a shared graph
67
+ ];
68
+ createMemoryServer(new Ledger("./ledger.jsonl"), { stores });
69
+ ```
70
+
71
+ 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.
72
+
73
+ ## Scoring memory mutations
74
+
75
+ `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.
76
+
77
+ ```ts
78
+ import { Ledger, ledgerUnderTest, runAll, SCENARIOS, formatReport } from "@agent-custody/state";
79
+ console.log(formatReport(await runAll(ledgerUnderTest(new Ledger("./ledger.jsonl")), SCENARIOS)));
80
+ ```
26
81
 
27
82
  ## The ledger
28
83
 
@@ -46,6 +101,11 @@ The ledger refuses to supersede a fact that is unknown, already superseded, or r
46
101
 
47
102
  ```
48
103
  src/ledger.ts the fact record, the two event kinds, as-of queries, supersession, retraction, JSONL persistence
104
+ src/server.ts the ledger as MCP tools; source and actor taken from the gateway's _meta
105
+ src/cli.ts agent-custody-memory serve
106
+ src/stores.ts write-through adapters: Mem0 and Zep, and the Store interface for others
107
+ src/evals.ts the memory-mutation harness: scenarios, scoring, report
108
+ src/evals-ledger.ts the ledger and a naive overwrite store behind the harness interface
49
109
  src/index.ts public surface
50
110
  examples/ runnable walkthroughs, each ends with OK and is run by the test suite
51
111
  test/ one test per question a platform owner asks after a memory incident
@@ -57,12 +117,14 @@ tsconfig.build.json emits dist/ for consumers; the repo itself runs the .ts dir
57
117
  **Done**
58
118
 
59
119
  - Bitemporal fact ledger with supersession, retraction, as-of and history queries, persisted as JSONL.
120
+ - 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.
121
+ - 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.
122
+ - 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.
123
+ - 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.
60
124
 
61
125
  **Next, in the order it pays off**
62
126
 
63
- 1. A memory MCP server exposing write, read, supersede, and forget as tools, run behind the receipts gateway so every call is receipted and policy-checked and the source receipt id is filled in by the gateway rather than the caller.
64
- 2. 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.
65
- 3. Blast radius: given a fact id, every downstream receipt and derived fact that cited it.
66
- 4. 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.
67
- 5. Write-through adapters for existing memory stores, tested against the real packages.
68
- 6. Signed forget statements: a retention or deletion request produces a verifiable record of which facts were removed.
127
+ 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.
128
+ 2. Blast radius: given a fact id, every downstream receipt and derived fact that cited it, and the retraction that undoes the belief.
129
+ 3. Trust tiers, the rest: Cedar policy over provenance so a claimed write cannot supersede an attested org-space fact, and quarantine of values that came from untrusted tool output even when the actor is attested.
130
+ 4. Signed forget statements: a retention or deletion request produces a verifiable record of which facts were removed from the ledger and from every store behind it.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { Ledger } from "./ledger.js";
4
+ import { createMemoryServer, serveStdio } from "./server.js";
5
+ const USAGE = `agent-custody-memory <command>
6
+
7
+ serve --ledger <ledger.jsonl> [--allow-direct] the memory server over stdio; run it as the receipts gateway's upstream.
8
+ By default it refuses calls that did not come through the gateway.
9
+ `;
10
+ async function main(argv) {
11
+ const [cmd, ...rest] = argv;
12
+ switch (cmd) {
13
+ case "serve": {
14
+ const { values } = parseArgs({ args: rest, options: { ledger: { type: "string" }, "allow-direct": { type: "boolean", default: false } } });
15
+ if (!values.ledger)
16
+ throw new Error("serve needs --ledger");
17
+ const ledger = new Ledger(values.ledger);
18
+ 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"] }));
20
+ return 0;
21
+ }
22
+ default:
23
+ console.error(USAGE);
24
+ return cmd === undefined || cmd === "--help" || cmd === "-h" ? 0 : 2;
25
+ }
26
+ }
27
+ main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
28
+ console.error(`error: ${e instanceof Error ? e.message : e}`);
29
+ process.exit(1);
30
+ });
@@ -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/index.d.ts CHANGED
@@ -1,2 +1,9 @@
1
1
  export { Ledger } from "./ledger.ts";
2
- export type { AsOf, AssertEvent, AssertInput, Fact, LedgerEvent, RetractEvent, RetractInput, Source } from "./ledger.ts";
2
+ export type { AsOf, AssertEvent, AssertInput, ConfirmEvent, ConfirmInput, Fact, FactProvenance, LedgerEvent, RetractEvent, RetractInput, Source } from "./ledger.ts";
3
+ export { AGENT_META_KEY, RECEIPT_META_KEY, SERVER_VERSION, TOOLS, createMemoryServer, serveStdio } from "./server.ts";
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 type { Mem0Like, Mem0Options, Store, ZepLike, ZepOptions } from "./stores.ts";
package/dist/index.js CHANGED
@@ -1,2 +1,6 @@
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";
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";
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,9 @@ 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
+ /** ids of this fact in the retrieval stores it was written through to, by store name; absent when there are none */
23
+ external?: Record<string, string>;
15
24
  /** ISO timestamps. validTo is null while the fact is believed to still hold. */
16
25
  validFrom: string;
17
26
  validTo: string | null;
@@ -35,7 +44,16 @@ export interface RetractEvent {
35
44
  reason: string;
36
45
  source: Source;
37
46
  }
38
- export type LedgerEvent = AssertEvent | RetractEvent;
47
+ /** A confirm lifts a claimed fact to attested. Only an attested party can confirm; the event records who and which receipt. */
48
+ export interface ConfirmEvent {
49
+ eventId: string;
50
+ kind: "confirm";
51
+ txTime: string;
52
+ factId: string;
53
+ actor: string;
54
+ source: Source;
55
+ }
56
+ export type LedgerEvent = AssertEvent | RetractEvent | ConfirmEvent;
39
57
  export interface AssertInput {
40
58
  subject: string;
41
59
  predicate: string;
@@ -43,10 +61,18 @@ export interface AssertInput {
43
61
  space: string;
44
62
  actor: string;
45
63
  source?: Source;
64
+ /** default claimed; the memory server sets attested for writes that came through the gateway */
65
+ provenance?: FactProvenance;
66
+ external?: Record<string, string>;
46
67
  validFrom?: string;
47
68
  confidence?: number;
48
69
  supersedes?: string;
49
70
  }
71
+ export interface ConfirmInput {
72
+ factId: string;
73
+ actor: string;
74
+ source?: Source;
75
+ }
50
76
  export interface RetractInput {
51
77
  factId: string;
52
78
  actor: string;
@@ -61,6 +87,8 @@ export interface AsOf {
61
87
  space?: string;
62
88
  subject?: string;
63
89
  predicate?: string;
90
+ /** "attested" returns only facts that were attested at txAt; default "all" */
91
+ include?: "attested" | "all";
64
92
  }
65
93
  export declare class Ledger {
66
94
  private readonly events;
@@ -70,12 +98,16 @@ export declare class Ledger {
70
98
  now?: () => Date;
71
99
  });
72
100
  get size(): number;
101
+ /** The checks assert makes, without appending. For callers that must do something irreversible before the append. */
102
+ validateAssert(input: AssertInput): void;
73
103
  assert(input: AssertInput): AssertEvent;
74
104
  retract(input: RetractInput): RetractEvent;
105
+ confirm(input: ConfirmInput): ConfirmEvent;
75
106
  /** The facts believed at a moment. Valid time answers "was it true then"; transaction time answers "did the ledger know it then". */
76
107
  asOf(q?: AsOf): Fact[];
77
- /** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its retraction. */
108
+ /** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its confirmation, its retraction. */
78
109
  history(factId: string): LedgerEvent[];
110
+ private confirmedAt;
79
111
  private factById;
80
112
  private retractedAt;
81
113
  private append;
package/dist/ledger.js CHANGED
@@ -24,9 +24,9 @@ export class Ledger {
24
24
  get size() {
25
25
  return this.events.length;
26
26
  }
27
- assert(input) {
28
- const txTime = this.now().toISOString();
29
- const validFrom = input.validFrom ?? txTime;
27
+ /** The checks assert makes, without appending. For callers that must do something irreversible before the append. */
28
+ validateAssert(input) {
29
+ const validFrom = input.validFrom ?? this.now().toISOString();
30
30
  if (input.supersedes !== undefined) {
31
31
  const prior = this.factById(input.supersedes);
32
32
  if (!prior)
@@ -38,6 +38,11 @@ export class Ledger {
38
38
  if (validFrom < prior.fact.validFrom)
39
39
  throw new Error(`replacement cannot start before the fact it supersedes`);
40
40
  }
41
+ }
42
+ assert(input) {
43
+ this.validateAssert(input);
44
+ const txTime = this.now().toISOString();
45
+ const validFrom = input.validFrom ?? txTime;
41
46
  const event = {
42
47
  eventId: randomUUID(),
43
48
  kind: "assert",
@@ -50,6 +55,8 @@ export class Ledger {
50
55
  space: input.space,
51
56
  actor: input.actor,
52
57
  source: input.source ?? { receiptId: null },
58
+ provenance: input.provenance ?? "claimed",
59
+ ...(input.external && Object.keys(input.external).length > 0 ? { external: input.external } : {}),
53
60
  validFrom,
54
61
  validTo: null,
55
62
  confidence: input.confidence ?? null,
@@ -76,17 +83,30 @@ export class Ledger {
76
83
  this.append(event);
77
84
  return event;
78
85
  }
86
+ confirm(input) {
87
+ const prior = this.factById(input.factId);
88
+ if (!prior)
89
+ throw new Error(`cannot confirm unknown fact ${input.factId}`);
90
+ if (this.retractedAt(input.factId))
91
+ throw new Error(`fact ${input.factId} is retracted`);
92
+ if (prior.fact.provenance === "attested" || this.confirmedAt(input.factId))
93
+ throw new Error(`fact ${input.factId} is already attested`);
94
+ const event = { eventId: randomUUID(), kind: "confirm", txTime: this.now().toISOString(), factId: input.factId, actor: input.actor, source: input.source ?? { receiptId: null } };
95
+ this.append(event);
96
+ return event;
97
+ }
79
98
  /** The facts believed at a moment. Valid time answers "was it true then"; transaction time answers "did the ledger know it then". */
80
99
  asOf(q = {}) {
81
100
  const validAt = q.validAt ?? this.now().toISOString();
82
101
  const txAt = q.txAt ?? this.now().toISOString();
83
102
  const known = this.events.filter((e) => e.txTime <= txAt);
84
103
  const retracted = new Set(known.filter((e) => e.kind === "retract").map((e) => e.factId));
104
+ const confirmed = new Set(known.filter((e) => e.kind === "confirm").map((e) => e.factId));
85
105
  const facts = new Map();
86
106
  for (const e of known) {
87
107
  if (e.kind !== "assert")
88
108
  continue;
89
- facts.set(e.fact.factId, { ...e.fact });
109
+ facts.set(e.fact.factId, { ...e.fact, provenance: confirmed.has(e.fact.factId) ? "attested" : e.fact.provenance });
90
110
  if (e.supersedes && facts.has(e.supersedes) && !retracted.has(e.fact.factId)) {
91
111
  facts.get(e.supersedes).validTo = e.fact.validFrom;
92
112
  }
@@ -96,12 +116,16 @@ export class Ledger {
96
116
  (f.validTo === null || validAt < f.validTo) &&
97
117
  (q.space === undefined || f.space === q.space) &&
98
118
  (q.subject === undefined || f.subject === q.subject) &&
99
- (q.predicate === undefined || f.predicate === q.predicate));
119
+ (q.predicate === undefined || f.predicate === q.predicate) &&
120
+ (q.include !== "attested" || f.provenance === "attested"));
100
121
  }
101
- /** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its retraction. */
122
+ /** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its confirmation, its retraction. */
102
123
  history(factId) {
103
124
  return this.events.filter((e) => (e.kind === "assert" ? e.fact.factId === factId || e.supersedes === factId : e.factId === factId));
104
125
  }
126
+ confirmedAt(factId) {
127
+ return this.events.some((e) => e.kind === "confirm" && e.factId === factId);
128
+ }
105
129
  factById(factId) {
106
130
  let found;
107
131
  for (const e of this.events) {
@@ -0,0 +1,18 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { type Tool } from "@modelcontextprotocol/sdk/types.js";
3
+ import type { Ledger } from "./ledger.ts";
4
+ import type { Store } from "./stores.ts";
5
+ export declare const SERVER_VERSION = "0.1.0";
6
+ /** The same keys the receipts gateway sets on the upstream call. Duplicated here so this package needs no runtime import from receipts. */
7
+ export declare const RECEIPT_META_KEY = "agent-custody/receipt";
8
+ export declare const AGENT_META_KEY = "agent-custody/agent";
9
+ export declare const TOOLS: Tool[];
10
+ export interface MemoryServerOptions {
11
+ /** Refuse calls that did not come through the gateway, i.e. carry no receipt id. On by default when served from the CLI. */
12
+ requireGateway?: boolean;
13
+ /** Retrieval stores every write goes through to and every retraction reaches. A store that refuses a write fails the write; nothing is recorded. */
14
+ stores?: Store[];
15
+ }
16
+ export declare function createMemoryServer(ledger: Ledger, opts?: MemoryServerOptions): Server;
17
+ /** Serves over stdio, the way the gateway spawns it. Diagnostics must go to stderr. */
18
+ export declare function serveStdio(server: Server): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,140 @@
1
+ // The memory server: the ledger as MCP tools, meant to run as an upstream of the receipts gateway.
2
+ // Behind the gateway every write and read is policy-checked and receipted, and the gateway tells this server, in the
3
+ // call's _meta, which receipt it is and who the attested grant says is calling. Those become the fact's source and
4
+ // actor; a caller cannot supply them. Run it directly and the source is null and the actor is whatever the caller
5
+ // claims, which is recorded as such.
6
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
+ import { z } from "zod";
10
+ export const SERVER_VERSION = "0.1.0";
11
+ /** The same keys the receipts gateway sets on the upstream call. Duplicated here so this package needs no runtime import from receipts. */
12
+ export const RECEIPT_META_KEY = "agent-custody/receipt";
13
+ export const AGENT_META_KEY = "agent-custody/agent";
14
+ const iso = z.string().datetime({ offset: true });
15
+ const Write = z.object({
16
+ subject: z.string().min(1),
17
+ predicate: z.string().min(1),
18
+ value: z.unknown(),
19
+ space: z.string().min(1),
20
+ /** ignored behind the gateway, which supplies the attested agent instead */
21
+ actor: z.string().min(1).optional(),
22
+ validFrom: iso.optional(),
23
+ confidence: z.number().min(0).max(1).optional(),
24
+ supersedes: z.string().min(1).optional(),
25
+ });
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(), includeClaimed: z.boolean().optional() });
27
+ const Confirm = z.object({ factId: z.string().min(1) });
28
+ const Retract = z.object({ factId: z.string().min(1), reason: z.string().min(1), actor: z.string().min(1).optional() });
29
+ const History = z.object({ factId: z.string().min(1) });
30
+ const str = { type: "string" };
31
+ export const TOOLS = [
32
+ {
33
+ name: "memory.write",
34
+ description: "Record a belief: subject, predicate, value, in a space. Optionally supersede an earlier fact. Returns the new fact with its id, transaction time, actor, and source receipt.",
35
+ inputSchema: { type: "object", properties: { subject: str, predicate: str, value: {}, space: str, actor: str, validFrom: str, confidence: { type: "number" }, supersedes: str }, required: ["subject", "predicate", "value", "space"] },
36
+ },
37
+ {
38
+ name: "memory.read",
39
+ 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.",
40
+ inputSchema: { type: "object", properties: { subject: str, predicate: str, space: str, validAt: str, txAt: str, includeClaimed: { type: "boolean" } } },
41
+ },
42
+ {
43
+ name: "memory.confirm",
44
+ 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.",
45
+ inputSchema: { type: "object", properties: { factId: str }, required: ["factId"] },
46
+ },
47
+ {
48
+ name: "memory.retract",
49
+ description: "Undo a belief: the fact leaves the present, stays visible to questions about the past, and whatever it superseded is believed again.",
50
+ inputSchema: { type: "object", properties: { factId: str, reason: str, actor: str }, required: ["factId", "reason"] },
51
+ },
52
+ {
53
+ name: "memory.history",
54
+ description: "Every event that touched a fact, oldest first.",
55
+ inputSchema: { type: "object", properties: { factId: str }, required: ["factId"] },
56
+ },
57
+ ];
58
+ const json = (v) => ({ content: [{ type: "text", text: JSON.stringify(v) }] });
59
+ const fail = (msg) => ({ isError: true, content: [{ type: "text", text: msg }] });
60
+ export function createMemoryServer(ledger, opts = {}) {
61
+ const server = new Server({ name: "agent-custody-memory", version: SERVER_VERSION }, { capabilities: { tools: {} } });
62
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
63
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
64
+ const meta = (req.params._meta ?? {});
65
+ const receiptId = typeof meta[RECEIPT_META_KEY] === "string" ? meta[RECEIPT_META_KEY] : null;
66
+ const gatewayAgent = typeof meta[AGENT_META_KEY] === "string" ? meta[AGENT_META_KEY] : null;
67
+ if (opts.requireGateway && !receiptId)
68
+ return fail("memory server accepts calls only through the receipts gateway; no receipt id on this call");
69
+ const args = req.params.arguments ?? {};
70
+ const actorFor = (claimed) => gatewayAgent ?? claimed ?? "anonymous";
71
+ // A write is attested when it came through the gateway: the actor is from a signed grant and the receipt exists.
72
+ const provenance = receiptId && gatewayAgent ? "attested" : "claimed";
73
+ try {
74
+ switch (req.params.name) {
75
+ case "memory.write": {
76
+ const a = Write.parse(args);
77
+ 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 } : {}) };
78
+ // The stores are written first, so their ids can be recorded on the fact; the ledger's checks run beforehand
79
+ // so a write the ledger would refuse never reaches a store.
80
+ ledger.validateAssert(input);
81
+ const external = {};
82
+ const preview = { ...input, factId: "pending", validFrom: input.validFrom ?? new Date().toISOString(), validTo: null, confidence: input.confidence ?? null };
83
+ for (const store of opts.stores ?? [])
84
+ external[store.name] = await store.put(preview);
85
+ const ev = ledger.assert({ ...input, external });
86
+ return json({ fact: ev.fact, eventId: ev.eventId, txTime: ev.txTime, supersedes: ev.supersedes });
87
+ }
88
+ case "memory.read": {
89
+ const { includeClaimed, ...q } = Read.parse(args);
90
+ return json({ facts: ledger.asOf({ ...q, include: includeClaimed ? "all" : "attested" }) });
91
+ }
92
+ case "memory.retract": {
93
+ const a = Retract.parse(args);
94
+ const fact = ledger.history(a.factId).find((e) => e.kind === "assert" && e.fact.factId === a.factId)?.fact;
95
+ const ev = ledger.retract({ factId: a.factId, actor: actorFor(a.actor), reason: a.reason, source: { receiptId } });
96
+ // The ledger is retracted first: custody must not depend on a store being up. A store that fails to remove
97
+ // is reported, so the caller knows recall may still serve the value.
98
+ const stillHeld = [];
99
+ for (const store of opts.stores ?? []) {
100
+ const id = fact?.external?.[store.name];
101
+ if (!id)
102
+ continue;
103
+ try {
104
+ await store.remove(id, fact);
105
+ }
106
+ catch (e) {
107
+ stillHeld.push(`${store.name}: ${e instanceof Error ? e.message : String(e)}`);
108
+ }
109
+ }
110
+ 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))) };
111
+ if (stillHeld.length > 0)
112
+ return { isError: true, content: [{ type: "text", text: `retracted in the ledger, but still held by ${stillHeld.join("; ")}` }, { type: "text", text: JSON.stringify(out) }] };
113
+ return json(out);
114
+ }
115
+ case "memory.confirm": {
116
+ if (provenance !== "attested")
117
+ return fail("confirmation must come through the receipts gateway; a self-reported caller cannot lift a fact out of quarantine");
118
+ const a = Confirm.parse(args);
119
+ const ev = ledger.confirm({ factId: a.factId, actor: actorFor(undefined), source: { receiptId } });
120
+ return json({ eventId: ev.eventId, factId: ev.factId, txTime: ev.txTime, actor: ev.actor, source: ev.source });
121
+ }
122
+ case "memory.history":
123
+ return json({ events: ledger.history(History.parse(args).factId) });
124
+ default:
125
+ return fail(`unknown tool ${req.params.name}`);
126
+ }
127
+ }
128
+ catch (e) {
129
+ 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));
130
+ }
131
+ });
132
+ return server;
133
+ }
134
+ /** Serves over stdio, the way the gateway spawns it. Diagnostics must go to stderr. */
135
+ export async function serveStdio(server) {
136
+ await server.connect(new StdioServerTransport());
137
+ await new Promise((resolve) => {
138
+ server.onclose = resolve;
139
+ });
140
+ }
@@ -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.2",
3
+ "version": "0.1.4",
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": {
@@ -12,6 +12,9 @@
12
12
  "access": "public"
13
13
  },
14
14
  "type": "module",
15
+ "bin": {
16
+ "agent-custody-memory": "./dist/cli.js"
17
+ },
15
18
  "exports": {
16
19
  ".": {
17
20
  "types": "./dist/index.d.ts",
@@ -23,18 +26,22 @@
23
26
  ],
24
27
  "scripts": {
25
28
  "build": "tsc -p tsconfig.build.json",
26
- "typecheck": "tsc --noEmit",
29
+ "typecheck": "tsc -p ../receipts/tsconfig.build.json && tsc --noEmit",
27
30
  "test": "tsc -p ../receipts/tsconfig.build.json && vitest run"
28
31
  },
29
32
  "engines": {
30
33
  "node": ">=22"
31
34
  },
32
35
  "dependencies": {
33
- "@agent-custody/receipts": "0.1.2"
36
+ "@agent-custody/receipts": "0.1.4",
37
+ "@modelcontextprotocol/sdk": "^1.30.0",
38
+ "zod": "^4.5.4"
34
39
  },
35
40
  "devDependencies": {
36
41
  "@types/node": "^26.4.1",
37
42
  "typescript": "^7.0.2",
38
- "vitest": "^5.0.0"
43
+ "vitest": "^5.0.0",
44
+ "mem0ai": "^3.1.8",
45
+ "@getzep/zep-cloud": "^3.28.0"
39
46
  }
40
47
  }