@agent-custody/state 0.5.9 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -146,17 +146,22 @@ The first eight lines come from the receipt alone and work without a ledger; the
146
146
 
147
147
  ## Write-through to the stores you already use
148
148
 
149
- 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. For teams whose recall is a pgvector table, `pgvectorStore` writes the fact's text, its embedding from the function you already use, and its custody metadata as one row keyed by the fact id, and verifies a removal with the same nearest-neighbour query a retrieval call runs.
149
+ 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. For teams whose recall is a pgvector table, `pgvectorStore` writes the fact's text, its embedding from the function you already use, and its custody metadata as one row keyed by the fact id, and verifies a removal with the same nearest-neighbour query a retrieval call runs. Three more, each with its own limitation stated: `lettaStore` writes a passage to an agent's archival memory with the custody as tags, since Letta passages carry no free metadata, and verifies removal through archival search; `langgraphStore` writes one item per fact into a LangGraph store, which is where LangMem keeps its memories, keyed by the fact id under the namespace you give, and verifies removal with `get` and a filtered `search`; `cogneeStore` speaks Cognee's REST API directly, because Cognee has no JavaScript client, adding the text with the custody as external metadata and finding the data id by listing the dataset, since Cognee's add returns a pipeline run rather than an id; it was built against Cognee's routers and tested against a stand-in of them, not a running Cognee.
150
150
 
151
151
  ```ts
152
152
  import { MemoryClient } from "mem0ai";
153
153
  import { ZepClient } from "@getzep/zep-cloud";
154
- import { Ledger, createMemoryServer, mem0Store, zepStore } from "@agent-custody/state";
154
+ import { Letta } from "@letta-ai/letta-client";
155
+ import { InMemoryStore } from "@langchain/langgraph";
156
+ import { Ledger, cogneeStore, createMemoryServer, langgraphStore, lettaStore, mem0Store, pgvectorStore, zepStore } from "@agent-custody/state";
155
157
 
156
158
  const stores = [
157
159
  mem0Store(new MemoryClient({ apiKey: process.env.MEM0_API_KEY! }), { userId: "user_42" }), // infer is off: the memory is the fact, verbatim
158
160
  zepStore(new ZepClient({ apiKey: process.env.ZEP_API_KEY! }), { userId: "user_42" }), // or { graphId } for a shared graph
159
161
  pgvectorStore(pool, { embed, dimensions: 1536, table: "agent_memories" }), // your Postgres, your embedding function
162
+ lettaStore(new Letta({ apiKey: process.env.LETTA_API_KEY! }), { agentId: "agent-…" }), // archival memory of one agent, custody as tags
163
+ langgraphStore(new InMemoryStore(), { namespace: ["memories", "user_42"] }), // or any BaseStore: LangMem's memories live here
164
+ cogneeStore({ url: "http://localhost:8000", datasetId: "…", apiKeyEnv: "COGNEE_API_KEY" }), // Cognee's REST API, no client package exists
160
165
  ];
161
166
  createMemoryServer(new Ledger("./ledger.jsonl"), { stores });
162
167
  ```
@@ -220,7 +225,7 @@ src/review.ts the review page: the explain output as pages, served on loopback
220
225
  src/pack.ts the custody pack: build, sign, verify, format
221
226
  src/cli.ts agent-custody-memory serve (stdio or --http, with --retention and --forget-key-env), sweep (ledger-only or --via a gateway), eval, explain, review, pack, export, blast
222
227
  src/blast.ts blast radius: from receipts' consumed facts and the ledger's source receipts, forward
223
- src/stores.ts write-through adapters: Mem0, Zep, and pgvector, and the Store interface for others
228
+ src/stores.ts write-through adapters: Mem0, Zep, pgvector, Letta, a LangGraph store (LangMem), and Cognee, and the Store interface for others
224
229
  src/evals.ts the memory-mutation harness: scenarios, scoring, report
225
230
  src/evals-ledger.ts the ledger and a naive overwrite store behind the harness interface
226
231
  src/evals-file.ts scenario files, validated; signed eval reports and their verification
@@ -250,6 +255,7 @@ tsconfig.build.json emits dist/ for consumers; the repo itself runs the .ts dir
250
255
  - 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.
251
256
  - 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.
252
257
  - Write-through to pgvector: the fact's text, embedding, and custody metadata as one row in the Postgres a team already runs, keyed by the fact id; removal verified by the same nearest-neighbour query recall runs; tested against Postgres with the extension in Docker.
258
+ - Write-through adapters for Letta (archival passages, custody as tags), a LangGraph store where LangMem keeps its memories (one item per fact under a namespace), and Cognee (its REST API, the data id found by listing the dataset); each verifies its own removals, each states its limitation.
253
259
  - 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.
254
260
  - The eval CLI: built-in or custom scenario files, the naive baseline beside the ledger, a signed report a reviewer verifies, and a non-zero exit on regression, for cron.
255
261
  - 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.
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export type { MemoryUnderTest, Op, Report, Scenario, ScenarioScore } from "./eva
9
9
  export { ledgerUnderTest, overwriteStoreUnderTest } from "./evals-ledger.ts";
10
10
  export { EVAL_REPORT_TYPE, ScenarioFileSchema, loadScenarios, signReport, verifyReport } from "./evals-file.ts";
11
11
  export type { EvalReportPredicate, ReportCheck, ScenarioFile } from "./evals-file.ts";
12
- export { factMetadata, factText, mem0Store, pgvectorStore, zepStore } from "./stores.ts";
12
+ export { cogneeStore, factMetadata, factText, langgraphStore, lettaStore, mem0Store, pgvectorStore, zepStore } from "./stores.ts";
13
13
  export { blastRadius, formatBlastRadius, loadReceipts } from "./blast.ts";
14
14
  export { memoryHttpHandler, serveMemoryHttp } from "./http.ts";
15
15
  export { PACK_TYPE, buildPack, formatPack, signPack, verifyPack } from "./pack.ts";
@@ -20,4 +20,4 @@ export type { ActionPack, ActionPackCheck, ActionPackVerification, ReceiptKeys }
20
20
  export type { CustodyPack, PackCheck, PackVerification } from "./pack.ts";
21
21
  export type { MemoryHttpOptions, RunningMemoryServer } from "./http.ts";
22
22
  export type { BlastRadius, ReceiptSummary } from "./blast.ts";
23
- export type { Mem0Like, Mem0Options, PgLike, PgvectorOptions, RemovalOutcome, Store, ZepLike, ZepOptions } from "./stores.ts";
23
+ export type { CogneeOptions, LangGraphStoreLike, LangGraphStoreOptions, LettaLike, LettaOptions, Mem0Like, Mem0Options, PgLike, PgvectorOptions, RemovalOutcome, Store, ZepLike, ZepOptions } from "./stores.ts";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { AGENT_META_KEY, FACTS_META_KEY, OBSERVED_META_KEY, RECEIPT_META_KEY, SE
5
5
  export { SCENARIOS, formatReport, runAll, runScenario } from "./evals.js";
6
6
  export { ledgerUnderTest, overwriteStoreUnderTest } from "./evals-ledger.js";
7
7
  export { EVAL_REPORT_TYPE, ScenarioFileSchema, loadScenarios, signReport, verifyReport } from "./evals-file.js";
8
- export { factMetadata, factText, mem0Store, pgvectorStore, zepStore } from "./stores.js";
8
+ export { cogneeStore, factMetadata, factText, langgraphStore, lettaStore, mem0Store, pgvectorStore, zepStore } from "./stores.js";
9
9
  export { blastRadius, formatBlastRadius, loadReceipts } from "./blast.js";
10
10
  export { memoryHttpHandler, serveMemoryHttp } from "./http.js";
11
11
  export { PACK_TYPE, buildPack, formatPack, signPack, verifyPack } from "./pack.js";
package/dist/stores.d.ts CHANGED
@@ -104,3 +104,86 @@ export interface PgvectorOptions {
104
104
  * and checks the removed id is not among the nearest rows, which is what a retrieval query would return.
105
105
  */
106
106
  export declare function pgvectorStore(client: PgLike, opts: PgvectorOptions): Store;
107
+ /** The subset of @letta-ai/letta-client's Letta this adapter uses: an agent's archival memory. */
108
+ export interface LettaLike {
109
+ agents: {
110
+ passages: {
111
+ create(agentId: string, body: {
112
+ text: string;
113
+ tags?: string[] | null;
114
+ }): Promise<{
115
+ id?: string | undefined;
116
+ }[]>;
117
+ delete(memoryId: string, params: {
118
+ agent_id: string;
119
+ }): Promise<unknown>;
120
+ search?(agentId: string, query: {
121
+ query: string;
122
+ tags?: string[] | null;
123
+ tag_match_mode?: "any" | "all";
124
+ }): Promise<{
125
+ results: {
126
+ id: string;
127
+ content: string;
128
+ }[];
129
+ }>;
130
+ };
131
+ };
132
+ }
133
+ export interface LettaOptions {
134
+ /** the agent whose archival memory receives the fact */
135
+ agentId: string;
136
+ /** tags attached to every passage, beside the custody tags; default none */
137
+ tags?: string[];
138
+ }
139
+ /**
140
+ * Letta keeps archival memory as passages with text and tags, no free metadata. Custody travels as tags
141
+ * (`agent-custody`, `fact:<id>`, `space:<name>`, `receipt:<id>`) so a passage always leads back to its fact.
142
+ */
143
+ export declare function lettaStore(client: LettaLike, opts: LettaOptions): Store;
144
+ /**
145
+ * The subset of LangGraph's BaseStore this adapter uses. LangMem's memories live in this store, so custody over a
146
+ * LangMem deployment is custody over its store: one item per fact, keyed by the fact id, in the namespace given.
147
+ */
148
+ export interface LangGraphStoreLike {
149
+ put(namespace: string[], key: string, value: Record<string, unknown>): Promise<void>;
150
+ get(namespace: string[], key: string): Promise<{
151
+ key: string;
152
+ value: Record<string, unknown>;
153
+ } | null>;
154
+ delete(namespace: string[], key: string): Promise<void>;
155
+ search?(namespacePrefix: string[], options?: {
156
+ filter?: Record<string, unknown>;
157
+ limit?: number;
158
+ query?: string;
159
+ }): Promise<{
160
+ key: string;
161
+ namespace: string[];
162
+ value: Record<string, unknown>;
163
+ }[]>;
164
+ }
165
+ export interface LangGraphStoreOptions {
166
+ /** the namespace the memories live under, e.g. ["memories", userId] */
167
+ namespace: string[];
168
+ }
169
+ export declare function langgraphStore(store: LangGraphStoreLike, opts: LangGraphStoreOptions): Store;
170
+ export interface CogneeOptions {
171
+ /** the Cognee server, e.g. http://localhost:8000 */
172
+ url: string;
173
+ /** the dataset the facts go into, by id */
174
+ datasetId: string;
175
+ /** environment variable holding an API key (sent as X-Api-Key) */
176
+ apiKeyEnv?: string;
177
+ /** environment variable holding a bearer token (sent as Authorization: Bearer) */
178
+ tokenEnv?: string;
179
+ fetch?: typeof fetch;
180
+ env?: Record<string, string | undefined>;
181
+ }
182
+ /**
183
+ * Cognee has no JavaScript client; this adapter speaks its REST API directly, built against the add and datasets
184
+ * routers of cognee 0.3: `POST /api/v1/add` (multipart, `raw_data` and `external_metadata`), `GET
185
+ * /api/v1/datasets/{id}/data`, `DELETE /api/v1/datasets/{id}/data/{dataId}`. The add call returns a pipeline run,
186
+ * not the data id, so the id is found by listing the dataset and matching the fact id written into the item's
187
+ * external metadata. Tested against a stand-in of those three routes, not against a running Cognee.
188
+ */
189
+ export declare function cogneeStore(opts: CogneeOptions): Store;
package/dist/stores.js CHANGED
@@ -96,3 +96,117 @@ export function pgvectorStore(client, opts) {
96
96
  },
97
97
  };
98
98
  }
99
+ /**
100
+ * Letta keeps archival memory as passages with text and tags, no free metadata. Custody travels as tags
101
+ * (`agent-custody`, `fact:<id>`, `space:<name>`, `receipt:<id>`) so a passage always leads back to its fact.
102
+ */
103
+ export function lettaStore(client, opts) {
104
+ const tagsFor = (f) => ["agent-custody", `fact:${f.factId}`, `space:${f.space}`, ...(f.source.receiptId ? [`receipt:${f.source.receiptId}`] : []), ...(opts.tags ?? [])];
105
+ return {
106
+ name: "letta",
107
+ async put(fact) {
108
+ const passages = await client.agents.passages.create(opts.agentId, { text: factText(fact), tags: tagsFor(fact) });
109
+ const id = passages.find((p) => typeof p.id === "string")?.id;
110
+ if (!id)
111
+ throw new Error("letta returned no passage id");
112
+ return id;
113
+ },
114
+ async remove(externalId) {
115
+ await client.agents.passages.delete(externalId, { agent_id: opts.agentId });
116
+ },
117
+ ...(client.agents.passages.search
118
+ ? {
119
+ async verifyRemoved(externalId, fact) {
120
+ const { results } = await client.agents.passages.search(opts.agentId, { query: factText(fact), tags: [`fact:${fact.factId}`], tag_match_mode: "any" });
121
+ return !results.some((r) => r.id === externalId || r.content === factText(fact));
122
+ },
123
+ }
124
+ : {}),
125
+ };
126
+ }
127
+ export function langgraphStore(store, opts) {
128
+ return {
129
+ name: "langgraph",
130
+ async put(fact) {
131
+ await store.put(opts.namespace, fact.factId, { content: factText(fact), subject: fact.subject, predicate: fact.predicate, value: fact.value, ...factMetadata(fact) });
132
+ return fact.factId;
133
+ },
134
+ async remove(externalId) {
135
+ await store.delete(opts.namespace, externalId);
136
+ },
137
+ async verifyRemoved(externalId, fact) {
138
+ if ((await store.get(opts.namespace, externalId)) !== null)
139
+ return false;
140
+ if (!store.search)
141
+ return true;
142
+ const hits = await store.search(opts.namespace, { filter: { factId: fact.factId }, limit: 10 });
143
+ return !hits.some((h) => h.key === externalId);
144
+ },
145
+ };
146
+ }
147
+ /**
148
+ * Cognee has no JavaScript client; this adapter speaks its REST API directly, built against the add and datasets
149
+ * routers of cognee 0.3: `POST /api/v1/add` (multipart, `raw_data` and `external_metadata`), `GET
150
+ * /api/v1/datasets/{id}/data`, `DELETE /api/v1/datasets/{id}/data/{dataId}`. The add call returns a pipeline run,
151
+ * not the data id, so the id is found by listing the dataset and matching the fact id written into the item's
152
+ * external metadata. Tested against a stand-in of those three routes, not against a running Cognee.
153
+ */
154
+ export function cogneeStore(opts) {
155
+ const f = opts.fetch ?? fetch;
156
+ const env = opts.env ?? process.env;
157
+ const headers = {};
158
+ if (opts.apiKeyEnv) {
159
+ const v = env[opts.apiKeyEnv];
160
+ if (!v)
161
+ throw new Error(`cognee: environment variable ${opts.apiKeyEnv} is not set`);
162
+ headers["x-api-key"] = v;
163
+ }
164
+ if (opts.tokenEnv) {
165
+ const v = env[opts.tokenEnv];
166
+ if (!v)
167
+ throw new Error(`cognee: environment variable ${opts.tokenEnv} is not set`);
168
+ headers.authorization = `Bearer ${v}`;
169
+ }
170
+ const base = opts.url.endsWith("/") ? opts.url : `${opts.url}/`;
171
+ const call = async (method, path, body) => {
172
+ const res = await f(new URL(path, base), { method, headers, ...(body ? { body } : {}), signal: AbortSignal.timeout(30_000) });
173
+ if (!res.ok)
174
+ throw new Error(`cognee ${method} ${path}: ${res.status} ${(await res.text()).slice(0, 200)}`);
175
+ const text = await res.text();
176
+ return text ? JSON.parse(text) : null;
177
+ };
178
+ const list = async () => (await call("GET", `api/v1/datasets/${opts.datasetId}/data`));
179
+ const metadataOf = (item) => {
180
+ const m = item.external_metadata;
181
+ if (typeof m === "string") {
182
+ try {
183
+ return JSON.parse(m);
184
+ }
185
+ catch {
186
+ return {};
187
+ }
188
+ }
189
+ return (m ?? {});
190
+ };
191
+ return {
192
+ name: "cognee",
193
+ async put(fact) {
194
+ const form = new FormData();
195
+ form.append("raw_data", factText(fact));
196
+ form.append("datasetId", opts.datasetId);
197
+ form.append("external_metadata", JSON.stringify([factMetadata(fact)]));
198
+ form.append("labels", JSON.stringify(["agent-custody", `fact:${fact.factId}`]));
199
+ await call("POST", "api/v1/add", form);
200
+ const item = (await list()).reverse().find((i) => metadataOf(i).factId === fact.factId);
201
+ if (!item)
202
+ throw new Error(`cognee accepted the add but the dataset lists no item carrying fact ${fact.factId}`);
203
+ return item.id;
204
+ },
205
+ async remove(externalId) {
206
+ await call("DELETE", `api/v1/datasets/${opts.datasetId}/data/${externalId}`);
207
+ },
208
+ async verifyRemoved(externalId, fact) {
209
+ return !(await list()).some((i) => i.id === externalId || metadataOf(i).factId === fact.factId);
210
+ },
211
+ };
212
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-custody/state",
3
- "version": "0.5.9",
3
+ "version": "0.6.1",
4
4
  "description": "Governed memory for AI agents: a fact ledger with provenance, valid time, rollback, lineage, certified forget, and a store that is a JSONL file, SQLite, or Postgres, built on @agent-custody/receipts",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "node": ">=22"
34
34
  },
35
35
  "dependencies": {
36
- "@agent-custody/receipts": "0.5.9",
36
+ "@agent-custody/receipts": "0.6.1",
37
37
  "@modelcontextprotocol/sdk": "^1.30.0",
38
38
  "zod": "^4.5.4"
39
39
  },