eve-memory-pg 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present <PLACEHOLDER>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # eve-memory-pg
2
+
3
+ Postgres/pgvector storage adapter for [`eve-memory`](https://www.npmjs.com/package/eve-memory) — durable cross-session memory for [Vercel eve](https://vercel.com/eve) agents.
4
+
5
+ ```bash
6
+ pnpm add eve-memory eve-memory-pg pg
7
+ ```
8
+
9
+ Your database needs the [pgvector](https://github.com/pgvector/pgvector) extension available (Neon, Supabase, RDS, and Vercel-integrated Postgres all ship it). The adapter runs an idempotent migration at startup.
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { defineMemory } from "eve-memory";
15
+ import { gatewayEmbedder } from "eve-memory/adapters";
16
+ import { pgMemoryAdapter } from "eve-memory-pg";
17
+
18
+ export default defineMemory({
19
+ adapter: pgMemoryAdapter({
20
+ connectionString: process.env.DATABASE_URL!,
21
+ dimensions: 1536, // must match your embedding model
22
+ }),
23
+ embedder: gatewayEmbedder({ model: "openai/text-embedding-3-small" }),
24
+ semanticRecall: { topK: 5, scope: "resource" },
25
+ workingMemory: { template: "- name:\n- preferences:" },
26
+ });
27
+ ```
28
+
29
+ ### Bring your own client
30
+
31
+ Anything with a `query(sql, params) => Promise<{ rows }>` works — a `pg.Pool`, Neon's serverless driver, or PGlite in tests:
32
+
33
+ ```ts
34
+ pgMemoryAdapter({ client: myPool, dimensions: 1536 });
35
+ ```
36
+
37
+ With `connectionString`, the adapter creates a `pg.Pool` and closes it on `memory.dispose()`. With `client`, you own the lifecycle.
38
+
39
+ ### Options
40
+
41
+ | Option | Required | Description |
42
+ |---|---|---|
43
+ | `dimensions` | ✅ | Embedding dimension of the `vector` column (e.g. 1536 for `openai/text-embedding-3-small`, 128 for `stubEmbedder`) |
44
+ | `client` or `connectionString` | ✅ one of | How to reach Postgres |
45
+ | `tablePrefix` | | Table name prefix, default `eve_memory` |
46
+
47
+ ## Tables
48
+
49
+ `<prefix>_entries` (id, resource_id, thread_id, content, `vector` embedding, seq, created_at) and `<prefix>_working` (scope, resource_id, thread_id, content, updated_at). Search uses pgvector cosine distance (`<=>`).
50
+
51
+ ## License
52
+
53
+ MIT
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.pgMemoryAdapter = void 0;
7
+ var _effect = require("effect");
8
+ var _eveMemory = require("eve-memory");
9
+ /** eve-memory-pg — Postgres/pgvector storage adapter for eve-memory */
10
+
11
+ const EntryRow = /*#__PURE__*/_effect.Schema.Struct({
12
+ id: _effect.Schema.String,
13
+ resource_id: _effect.Schema.String,
14
+ thread_id: _effect.Schema.String,
15
+ content: _effect.Schema.String,
16
+ created_at: /*#__PURE__*/_effect.Schema.Union(_effect.Schema.DateFromSelf, _effect.Schema.Date)
17
+ });
18
+ const SearchRow = /*#__PURE__*/_effect.Schema.Struct({
19
+ ...EntryRow.fields,
20
+ score: _effect.Schema.Number
21
+ });
22
+ const WorkingMemoryRow = /*#__PURE__*/_effect.Schema.Struct({
23
+ content: _effect.Schema.String
24
+ });
25
+ const decodeEntryRows = /*#__PURE__*/_effect.Schema.decodeUnknown(/*#__PURE__*/_effect.Schema.Array(EntryRow));
26
+ const decodeSearchRows = /*#__PURE__*/_effect.Schema.decodeUnknown(/*#__PURE__*/_effect.Schema.Array(SearchRow));
27
+ const decodeWorkingMemoryRows = /*#__PURE__*/_effect.Schema.decodeUnknown(/*#__PURE__*/_effect.Schema.Array(WorkingMemoryRow));
28
+ const toEntry = row => ({
29
+ id: row.id,
30
+ resourceId: row.resource_id,
31
+ threadId: row.thread_id,
32
+ content: row.content,
33
+ createdAt: row.created_at
34
+ });
35
+ /** pgvector accepts the JSON array literal syntax for vector values. */
36
+ const toVectorLiteral = embedding => JSON.stringify(embedding);
37
+ /**
38
+ * One statement per entry: PGlite (and other extended-protocol clients)
39
+ * reject multi-statement strings, and every statement is idempotent.
40
+ */
41
+ const migrationStatements = (prefix, dimensions) => [`CREATE EXTENSION IF NOT EXISTS vector`, `CREATE TABLE IF NOT EXISTS ${prefix}_entries (
42
+ id text PRIMARY KEY,
43
+ resource_id text NOT NULL,
44
+ thread_id text NOT NULL,
45
+ content text NOT NULL,
46
+ embedding vector(${dimensions}) NOT NULL,
47
+ seq bigint GENERATED ALWAYS AS IDENTITY,
48
+ created_at timestamptz NOT NULL
49
+ )`, `CREATE INDEX IF NOT EXISTS ${prefix}_entries_scope_idx
50
+ ON ${prefix}_entries (resource_id, thread_id)`, `CREATE TABLE IF NOT EXISTS ${prefix}_working (
51
+ scope text NOT NULL,
52
+ resource_id text NOT NULL,
53
+ thread_id text NOT NULL,
54
+ content text NOT NULL,
55
+ updated_at timestamptz NOT NULL,
56
+ PRIMARY KEY (scope, resource_id, thread_id)
57
+ )`];
58
+ /** Working-memory rows use thread_id = '' for resource scope, keeping one natural key. */
59
+ const workingMemoryParams = key => [key.scope, key.resourceId, key.scope === "resource" ? "" : key.threadId];
60
+ const acquireClient = options => "client" in options ? _effect.Effect.succeed(options.client) : _effect.Effect.acquireRelease(_effect.Effect.promise(async () => {
61
+ const {
62
+ default: pg
63
+ } = await import("pg");
64
+ return new pg.Pool({
65
+ connectionString: options.connectionString
66
+ });
67
+ }), pool => _effect.Effect.promise(() => pool.end()));
68
+ /**
69
+ * Postgres storage adapter backed by pgvector cosine distance. The layer
70
+ * runs an idempotent migration at construction; connection and migration
71
+ * failures are configuration errors and fail fast (die).
72
+ */
73
+ const pgMemoryAdapter = options => _effect.Layer.scoped(_eveMemory.Memory, _effect.Effect.gen(function* () {
74
+ const prefix = options.tablePrefix ?? "eve_memory";
75
+ if (!/^[a-z_][a-z0-9_]*$/.test(prefix)) {
76
+ return yield* _effect.Effect.dieMessage(`eve-memory-pg: invalid tablePrefix "${prefix}" — use lowercase letters, digits, underscores`);
77
+ }
78
+ const entries = `${prefix}_entries`;
79
+ const working = `${prefix}_working`;
80
+ const client = yield* acquireClient(options);
81
+ const run = (operation, sql, params) => _effect.Effect.tryPromise({
82
+ try: () => client.query(sql, params),
83
+ catch: cause => new _eveMemory.MemoryStorageError({
84
+ operation,
85
+ cause
86
+ })
87
+ });
88
+ const decoded = (operation, decoder) => rows => decoder(rows).pipe(_effect.Effect.mapError(cause => new _eveMemory.MemoryStorageError({
89
+ operation,
90
+ cause
91
+ })));
92
+ yield* _effect.Effect.forEach(migrationStatements(prefix, options.dimensions), statement => _effect.Effect.promise(() => client.query(statement))).pipe(_effect.Effect.orDie);
93
+ const neighborsOf = (entry, range) => _effect.Effect.gen(function* () {
94
+ if (range <= 0) return [];
95
+ const result = yield* run("search", `WITH thread AS (
96
+ SELECT id, resource_id, thread_id, content, created_at,
97
+ row_number() OVER (ORDER BY seq) AS rn
98
+ FROM ${entries}
99
+ WHERE resource_id = $1 AND thread_id = $2
100
+ ), anchor AS (
101
+ SELECT rn FROM thread WHERE id = $3
102
+ )
103
+ SELECT t.id, t.resource_id, t.thread_id, t.content, t.created_at
104
+ FROM thread t, anchor a
105
+ WHERE t.rn BETWEEN a.rn - $4 AND a.rn + $4 AND t.id <> $3
106
+ ORDER BY t.rn`, [entry.resourceId, entry.threadId, entry.id, range]);
107
+ const rows = yield* decoded("search", decodeEntryRows)(result.rows);
108
+ return rows.map(toEntry);
109
+ });
110
+ return {
111
+ store: input => _effect.Effect.gen(function* () {
112
+ const now = yield* _effect.Clock.currentTimeMillis;
113
+ const entry = {
114
+ id: `mem_${crypto.randomUUID()}`,
115
+ resourceId: input.resourceId,
116
+ threadId: input.threadId,
117
+ content: input.content,
118
+ createdAt: new Date(now)
119
+ };
120
+ yield* run("store", `INSERT INTO ${entries} (id, resource_id, thread_id, content, embedding, created_at)
121
+ VALUES ($1, $2, $3, $4, $5::vector, $6)`, [entry.id, entry.resourceId, entry.threadId, entry.content, toVectorLiteral(input.embedding), entry.createdAt]);
122
+ return entry;
123
+ }),
124
+ search: input => _effect.Effect.gen(function* () {
125
+ const result = yield* run("search", `SELECT id, resource_id, thread_id, content, created_at,
126
+ 1 - (embedding <=> $1::vector) AS score
127
+ FROM ${entries}
128
+ WHERE resource_id = $2
129
+ AND ($3::text IS NULL OR thread_id = $3)
130
+ AND 1 - (embedding <=> $1::vector) >= $4
131
+ ORDER BY embedding <=> $1::vector
132
+ LIMIT $5`, [toVectorLiteral(input.embedding), input.resourceId, input.scope === "thread" ? input.threadId : null, input.threshold, input.topK]);
133
+ const rows = yield* decoded("search", decodeSearchRows)(result.rows);
134
+ const matches = [];
135
+ for (const row of rows) {
136
+ const entry = toEntry(row);
137
+ matches.push({
138
+ entry,
139
+ score: row.score,
140
+ neighbors: yield* neighborsOf(entry, input.messageRange)
141
+ });
142
+ }
143
+ return matches;
144
+ }),
145
+ remove: id => run("remove", `DELETE FROM ${entries} WHERE id = $1`, [id]).pipe(_effect.Effect.asVoid),
146
+ getWorkingMemory: key => _effect.Effect.gen(function* () {
147
+ const result = yield* run("getWorkingMemory", `SELECT content FROM ${working} WHERE scope = $1 AND resource_id = $2 AND thread_id = $3`, workingMemoryParams(key));
148
+ const rows = yield* decoded("getWorkingMemory", decodeWorkingMemoryRows)(result.rows);
149
+ return _effect.Option.fromNullable(rows[0]?.content);
150
+ }),
151
+ setWorkingMemory: (key, content) => run("setWorkingMemory", `INSERT INTO ${working} (scope, resource_id, thread_id, content, updated_at)
152
+ VALUES ($1, $2, $3, $4, now())
153
+ ON CONFLICT (scope, resource_id, thread_id)
154
+ DO UPDATE SET content = EXCLUDED.content, updated_at = now()`, [...workingMemoryParams(key), content]).pipe(_effect.Effect.asVoid)
155
+ };
156
+ }));
157
+ exports.pgMemoryAdapter = pgMemoryAdapter;
158
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["_effect","require","_eveMemory","EntryRow","Schema","Struct","id","String","resource_id","thread_id","content","created_at","Union","DateFromSelf","Date","SearchRow","fields","score","Number","WorkingMemoryRow","decodeEntryRows","decodeUnknown","Array","decodeSearchRows","decodeWorkingMemoryRows","toEntry","row","resourceId","threadId","createdAt","toVectorLiteral","embedding","JSON","stringify","migrationStatements","prefix","dimensions","workingMemoryParams","key","scope","acquireClient","options","Effect","succeed","client","acquireRelease","promise","default","pg","Pool","connectionString","pool","end","pgMemoryAdapter","Layer","scoped","Memory","gen","tablePrefix","test","dieMessage","entries","working","run","operation","sql","params","tryPromise","try","query","catch","cause","MemoryStorageError","decoded","decoder","rows","pipe","mapError","forEach","statement","orDie","neighborsOf","entry","range","result","map","store","input","now","Clock","currentTimeMillis","crypto","randomUUID","search","threshold","topK","matches","push","neighbors","messageRange","remove","asVoid","getWorkingMemory","Option","fromNullable","setWorkingMemory","exports"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,38 @@
1
+ /** eve-memory-pg — Postgres/pgvector storage adapter for eve-memory */
2
+ import { Layer } from "effect";
3
+ import { Memory } from "eve-memory";
4
+ /**
5
+ * The minimal query surface the adapter needs. Structurally satisfied by
6
+ * `pg.Pool`, `pg.Client`, Neon's serverless driver, and PGlite — pass
7
+ * whichever client your deployment already has.
8
+ */
9
+ export interface PgQuerier {
10
+ readonly query: (sql: string, params?: Array<unknown>) => Promise<{
11
+ rows: Array<unknown>;
12
+ }>;
13
+ }
14
+ export type PgConnection =
15
+ /** Use an existing client/pool. The caller owns its lifecycle. */
16
+ {
17
+ readonly client: PgQuerier;
18
+ }
19
+ /** Create a `pg.Pool` from a connection string. Requires `pg` installed; closed on dispose. */
20
+ | {
21
+ readonly connectionString: string;
22
+ };
23
+ export type PgMemoryAdapterOptions = PgConnection & {
24
+ /**
25
+ * Embedding dimension of the `vector` column — must match your embedder
26
+ * (e.g. 1536 for openai/text-embedding-3-small).
27
+ */
28
+ readonly dimensions: number;
29
+ /** Table name prefix (default "eve_memory"). Lowercase letters, digits, underscores. */
30
+ readonly tablePrefix?: string;
31
+ };
32
+ /**
33
+ * Postgres storage adapter backed by pgvector cosine distance. The layer
34
+ * runs an idempotent migration at construction; connection and migration
35
+ * failures are configuration errors and fail fast (die).
36
+ */
37
+ export declare const pgMemoryAdapter: (options: PgMemoryAdapterOptions) => Layer.Layer<Memory>;
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EAAiB,KAAK,EAAoC,MAAM,QAAQ,CAAA;AAC/E,OAAO,EAAE,MAAM,EAAsB,MAAM,YAAY,CAAA;AAQvD;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC,CAAA;CAC5F;AAED,MAAM,MAAM,YAAY;AACtB,kEAAkE;AAChE;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;CAAE;AAChC,+FAA+F;GAC7F;IAAE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAAA;AAEzC,MAAM,MAAM,sBAAsB,GAAG,YAAY,GAAG;IAClD;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,CAAA;AA4ED;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,sBAAsB,KAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAuIjF,CAAA"}
@@ -0,0 +1,150 @@
1
+ /** eve-memory-pg — Postgres/pgvector storage adapter for eve-memory */
2
+ import { Clock, Effect, Layer, Option, Schema } from "effect";
3
+ import { Memory, MemoryStorageError } from "eve-memory";
4
+ const EntryRow = /*#__PURE__*/Schema.Struct({
5
+ id: Schema.String,
6
+ resource_id: Schema.String,
7
+ thread_id: Schema.String,
8
+ content: Schema.String,
9
+ created_at: /*#__PURE__*/Schema.Union(Schema.DateFromSelf, Schema.Date)
10
+ });
11
+ const SearchRow = /*#__PURE__*/Schema.Struct({
12
+ ...EntryRow.fields,
13
+ score: Schema.Number
14
+ });
15
+ const WorkingMemoryRow = /*#__PURE__*/Schema.Struct({
16
+ content: Schema.String
17
+ });
18
+ const decodeEntryRows = /*#__PURE__*/Schema.decodeUnknown(/*#__PURE__*/Schema.Array(EntryRow));
19
+ const decodeSearchRows = /*#__PURE__*/Schema.decodeUnknown(/*#__PURE__*/Schema.Array(SearchRow));
20
+ const decodeWorkingMemoryRows = /*#__PURE__*/Schema.decodeUnknown(/*#__PURE__*/Schema.Array(WorkingMemoryRow));
21
+ const toEntry = row => ({
22
+ id: row.id,
23
+ resourceId: row.resource_id,
24
+ threadId: row.thread_id,
25
+ content: row.content,
26
+ createdAt: row.created_at
27
+ });
28
+ /** pgvector accepts the JSON array literal syntax for vector values. */
29
+ const toVectorLiteral = embedding => JSON.stringify(embedding);
30
+ /**
31
+ * One statement per entry: PGlite (and other extended-protocol clients)
32
+ * reject multi-statement strings, and every statement is idempotent.
33
+ */
34
+ const migrationStatements = (prefix, dimensions) => [`CREATE EXTENSION IF NOT EXISTS vector`, `CREATE TABLE IF NOT EXISTS ${prefix}_entries (
35
+ id text PRIMARY KEY,
36
+ resource_id text NOT NULL,
37
+ thread_id text NOT NULL,
38
+ content text NOT NULL,
39
+ embedding vector(${dimensions}) NOT NULL,
40
+ seq bigint GENERATED ALWAYS AS IDENTITY,
41
+ created_at timestamptz NOT NULL
42
+ )`, `CREATE INDEX IF NOT EXISTS ${prefix}_entries_scope_idx
43
+ ON ${prefix}_entries (resource_id, thread_id)`, `CREATE TABLE IF NOT EXISTS ${prefix}_working (
44
+ scope text NOT NULL,
45
+ resource_id text NOT NULL,
46
+ thread_id text NOT NULL,
47
+ content text NOT NULL,
48
+ updated_at timestamptz NOT NULL,
49
+ PRIMARY KEY (scope, resource_id, thread_id)
50
+ )`];
51
+ /** Working-memory rows use thread_id = '' for resource scope, keeping one natural key. */
52
+ const workingMemoryParams = key => [key.scope, key.resourceId, key.scope === "resource" ? "" : key.threadId];
53
+ const acquireClient = options => "client" in options ? Effect.succeed(options.client) : Effect.acquireRelease(Effect.promise(async () => {
54
+ const {
55
+ default: pg
56
+ } = await import("pg");
57
+ return new pg.Pool({
58
+ connectionString: options.connectionString
59
+ });
60
+ }), pool => Effect.promise(() => pool.end()));
61
+ /**
62
+ * Postgres storage adapter backed by pgvector cosine distance. The layer
63
+ * runs an idempotent migration at construction; connection and migration
64
+ * failures are configuration errors and fail fast (die).
65
+ */
66
+ export const pgMemoryAdapter = options => Layer.scoped(Memory, Effect.gen(function* () {
67
+ const prefix = options.tablePrefix ?? "eve_memory";
68
+ if (!/^[a-z_][a-z0-9_]*$/.test(prefix)) {
69
+ return yield* Effect.dieMessage(`eve-memory-pg: invalid tablePrefix "${prefix}" — use lowercase letters, digits, underscores`);
70
+ }
71
+ const entries = `${prefix}_entries`;
72
+ const working = `${prefix}_working`;
73
+ const client = yield* acquireClient(options);
74
+ const run = (operation, sql, params) => Effect.tryPromise({
75
+ try: () => client.query(sql, params),
76
+ catch: cause => new MemoryStorageError({
77
+ operation,
78
+ cause
79
+ })
80
+ });
81
+ const decoded = (operation, decoder) => rows => decoder(rows).pipe(Effect.mapError(cause => new MemoryStorageError({
82
+ operation,
83
+ cause
84
+ })));
85
+ yield* Effect.forEach(migrationStatements(prefix, options.dimensions), statement => Effect.promise(() => client.query(statement))).pipe(Effect.orDie);
86
+ const neighborsOf = (entry, range) => Effect.gen(function* () {
87
+ if (range <= 0) return [];
88
+ const result = yield* run("search", `WITH thread AS (
89
+ SELECT id, resource_id, thread_id, content, created_at,
90
+ row_number() OVER (ORDER BY seq) AS rn
91
+ FROM ${entries}
92
+ WHERE resource_id = $1 AND thread_id = $2
93
+ ), anchor AS (
94
+ SELECT rn FROM thread WHERE id = $3
95
+ )
96
+ SELECT t.id, t.resource_id, t.thread_id, t.content, t.created_at
97
+ FROM thread t, anchor a
98
+ WHERE t.rn BETWEEN a.rn - $4 AND a.rn + $4 AND t.id <> $3
99
+ ORDER BY t.rn`, [entry.resourceId, entry.threadId, entry.id, range]);
100
+ const rows = yield* decoded("search", decodeEntryRows)(result.rows);
101
+ return rows.map(toEntry);
102
+ });
103
+ return {
104
+ store: input => Effect.gen(function* () {
105
+ const now = yield* Clock.currentTimeMillis;
106
+ const entry = {
107
+ id: `mem_${crypto.randomUUID()}`,
108
+ resourceId: input.resourceId,
109
+ threadId: input.threadId,
110
+ content: input.content,
111
+ createdAt: new Date(now)
112
+ };
113
+ yield* run("store", `INSERT INTO ${entries} (id, resource_id, thread_id, content, embedding, created_at)
114
+ VALUES ($1, $2, $3, $4, $5::vector, $6)`, [entry.id, entry.resourceId, entry.threadId, entry.content, toVectorLiteral(input.embedding), entry.createdAt]);
115
+ return entry;
116
+ }),
117
+ search: input => Effect.gen(function* () {
118
+ const result = yield* run("search", `SELECT id, resource_id, thread_id, content, created_at,
119
+ 1 - (embedding <=> $1::vector) AS score
120
+ FROM ${entries}
121
+ WHERE resource_id = $2
122
+ AND ($3::text IS NULL OR thread_id = $3)
123
+ AND 1 - (embedding <=> $1::vector) >= $4
124
+ ORDER BY embedding <=> $1::vector
125
+ LIMIT $5`, [toVectorLiteral(input.embedding), input.resourceId, input.scope === "thread" ? input.threadId : null, input.threshold, input.topK]);
126
+ const rows = yield* decoded("search", decodeSearchRows)(result.rows);
127
+ const matches = [];
128
+ for (const row of rows) {
129
+ const entry = toEntry(row);
130
+ matches.push({
131
+ entry,
132
+ score: row.score,
133
+ neighbors: yield* neighborsOf(entry, input.messageRange)
134
+ });
135
+ }
136
+ return matches;
137
+ }),
138
+ remove: id => run("remove", `DELETE FROM ${entries} WHERE id = $1`, [id]).pipe(Effect.asVoid),
139
+ getWorkingMemory: key => Effect.gen(function* () {
140
+ const result = yield* run("getWorkingMemory", `SELECT content FROM ${working} WHERE scope = $1 AND resource_id = $2 AND thread_id = $3`, workingMemoryParams(key));
141
+ const rows = yield* decoded("getWorkingMemory", decodeWorkingMemoryRows)(result.rows);
142
+ return Option.fromNullable(rows[0]?.content);
143
+ }),
144
+ setWorkingMemory: (key, content) => run("setWorkingMemory", `INSERT INTO ${working} (scope, resource_id, thread_id, content, updated_at)
145
+ VALUES ($1, $2, $3, $4, now())
146
+ ON CONFLICT (scope, resource_id, thread_id)
147
+ DO UPDATE SET content = EXCLUDED.content, updated_at = now()`, [...workingMemoryParams(key), content]).pipe(Effect.asVoid)
148
+ };
149
+ }));
150
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["Clock","Effect","Layer","Option","Schema","Memory","MemoryStorageError","EntryRow","Struct","id","String","resource_id","thread_id","content","created_at","Union","DateFromSelf","Date","SearchRow","fields","score","Number","WorkingMemoryRow","decodeEntryRows","decodeUnknown","Array","decodeSearchRows","decodeWorkingMemoryRows","toEntry","row","resourceId","threadId","createdAt","toVectorLiteral","embedding","JSON","stringify","migrationStatements","prefix","dimensions","workingMemoryParams","key","scope","acquireClient","options","succeed","client","acquireRelease","promise","default","pg","Pool","connectionString","pool","end","pgMemoryAdapter","scoped","gen","tablePrefix","test","dieMessage","entries","working","run","operation","sql","params","tryPromise","try","query","catch","cause","decoded","decoder","rows","pipe","mapError","forEach","statement","orDie","neighborsOf","entry","range","result","map","store","input","now","currentTimeMillis","crypto","randomUUID","search","threshold","topK","matches","push","neighbors","messageRange","remove","asVoid","getWorkingMemory","fromNullable","setWorkingMemory"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "module",
3
+ "sideEffects": []
4
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "eve-memory-pg",
3
+ "version": "0.1.0",
4
+ "description": "Postgres/pgvector storage adapter for eve-memory",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/katungi/eve-memory.git",
9
+ "directory": "packages/adapter-pg"
10
+ },
11
+ "sideEffects": [],
12
+ "dependencies": {
13
+ "effect": "^3.10.7"
14
+ },
15
+ "peerDependencies": {
16
+ "pg": "^8.11.0",
17
+ "eve-memory": "^0.1.0"
18
+ },
19
+ "peerDependenciesMeta": {
20
+ "pg": {
21
+ "optional": true
22
+ }
23
+ },
24
+ "main": "./dist/cjs/index.js",
25
+ "module": "./dist/esm/index.js",
26
+ "types": "./dist/dts/index.d.ts",
27
+ "exports": {
28
+ "./package.json": "./package.json",
29
+ ".": {
30
+ "types": "./dist/dts/index.d.ts",
31
+ "import": "./dist/esm/index.js",
32
+ "default": "./dist/cjs/index.js"
33
+ }
34
+ }
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,251 @@
1
+ /** eve-memory-pg — Postgres/pgvector storage adapter for eve-memory */
2
+
3
+ import { Clock, Effect, Layer, Option, type ParseResult, Schema } from "effect"
4
+ import { Memory, MemoryStorageError } from "eve-memory"
5
+ import type {
6
+ MemoryEntry,
7
+ MemorySearchResult,
8
+ SearchMemoryInput,
9
+ WorkingMemoryKey
10
+ } from "eve-memory"
11
+
12
+ /**
13
+ * The minimal query surface the adapter needs. Structurally satisfied by
14
+ * `pg.Pool`, `pg.Client`, Neon's serverless driver, and PGlite — pass
15
+ * whichever client your deployment already has.
16
+ */
17
+ export interface PgQuerier {
18
+ readonly query: (sql: string, params?: Array<unknown>) => Promise<{ rows: Array<unknown> }>
19
+ }
20
+
21
+ export type PgConnection =
22
+ /** Use an existing client/pool. The caller owns its lifecycle. */
23
+ | { readonly client: PgQuerier }
24
+ /** Create a `pg.Pool` from a connection string. Requires `pg` installed; closed on dispose. */
25
+ | { readonly connectionString: string }
26
+
27
+ export type PgMemoryAdapterOptions = PgConnection & {
28
+ /**
29
+ * Embedding dimension of the `vector` column — must match your embedder
30
+ * (e.g. 1536 for openai/text-embedding-3-small).
31
+ */
32
+ readonly dimensions: number
33
+ /** Table name prefix (default "eve_memory"). Lowercase letters, digits, underscores. */
34
+ readonly tablePrefix?: string
35
+ }
36
+
37
+ type Operation = ConstructorParameters<typeof MemoryStorageError>[0]["operation"]
38
+
39
+ const EntryRow = Schema.Struct({
40
+ id: Schema.String,
41
+ resource_id: Schema.String,
42
+ thread_id: Schema.String,
43
+ content: Schema.String,
44
+ created_at: Schema.Union(Schema.DateFromSelf, Schema.Date)
45
+ })
46
+
47
+ const SearchRow = Schema.Struct({ ...EntryRow.fields, score: Schema.Number })
48
+
49
+ const WorkingMemoryRow = Schema.Struct({ content: Schema.String })
50
+
51
+ const decodeEntryRows = Schema.decodeUnknown(Schema.Array(EntryRow))
52
+ const decodeSearchRows = Schema.decodeUnknown(Schema.Array(SearchRow))
53
+ const decodeWorkingMemoryRows = Schema.decodeUnknown(Schema.Array(WorkingMemoryRow))
54
+
55
+ const toEntry = (row: typeof EntryRow.Type): MemoryEntry => ({
56
+ id: row.id,
57
+ resourceId: row.resource_id,
58
+ threadId: row.thread_id,
59
+ content: row.content,
60
+ createdAt: row.created_at
61
+ })
62
+
63
+ /** pgvector accepts the JSON array literal syntax for vector values. */
64
+ const toVectorLiteral = (embedding: ReadonlyArray<number>) => JSON.stringify(embedding)
65
+
66
+ /**
67
+ * One statement per entry: PGlite (and other extended-protocol clients)
68
+ * reject multi-statement strings, and every statement is idempotent.
69
+ */
70
+ const migrationStatements = (prefix: string, dimensions: number): ReadonlyArray<string> => [
71
+ `CREATE EXTENSION IF NOT EXISTS vector`,
72
+ `CREATE TABLE IF NOT EXISTS ${prefix}_entries (
73
+ id text PRIMARY KEY,
74
+ resource_id text NOT NULL,
75
+ thread_id text NOT NULL,
76
+ content text NOT NULL,
77
+ embedding vector(${dimensions}) NOT NULL,
78
+ seq bigint GENERATED ALWAYS AS IDENTITY,
79
+ created_at timestamptz NOT NULL
80
+ )`,
81
+ `CREATE INDEX IF NOT EXISTS ${prefix}_entries_scope_idx
82
+ ON ${prefix}_entries (resource_id, thread_id)`,
83
+ `CREATE TABLE IF NOT EXISTS ${prefix}_working (
84
+ scope text NOT NULL,
85
+ resource_id text NOT NULL,
86
+ thread_id text NOT NULL,
87
+ content text NOT NULL,
88
+ updated_at timestamptz NOT NULL,
89
+ PRIMARY KEY (scope, resource_id, thread_id)
90
+ )`
91
+ ]
92
+
93
+ /** Working-memory rows use thread_id = '' for resource scope, keeping one natural key. */
94
+ const workingMemoryParams = (key: WorkingMemoryKey) => [
95
+ key.scope,
96
+ key.resourceId,
97
+ key.scope === "resource" ? "" : key.threadId
98
+ ]
99
+
100
+ const acquireClient = (options: PgMemoryAdapterOptions) =>
101
+ "client" in options
102
+ ? Effect.succeed(options.client)
103
+ : Effect.acquireRelease(
104
+ Effect.promise(async () => {
105
+ const { default: pg } = await import("pg")
106
+ return new pg.Pool({ connectionString: options.connectionString })
107
+ }),
108
+ (pool) => Effect.promise(() => pool.end())
109
+ )
110
+
111
+ /**
112
+ * Postgres storage adapter backed by pgvector cosine distance. The layer
113
+ * runs an idempotent migration at construction; connection and migration
114
+ * failures are configuration errors and fail fast (die).
115
+ */
116
+ export const pgMemoryAdapter = (options: PgMemoryAdapterOptions): Layer.Layer<Memory> =>
117
+ Layer.scoped(
118
+ Memory,
119
+ Effect.gen(function*() {
120
+ const prefix = options.tablePrefix ?? "eve_memory"
121
+ if (!/^[a-z_][a-z0-9_]*$/.test(prefix)) {
122
+ return yield* Effect.dieMessage(
123
+ `eve-memory-pg: invalid tablePrefix "${prefix}" — use lowercase letters, digits, underscores`
124
+ )
125
+ }
126
+ const entries = `${prefix}_entries`
127
+ const working = `${prefix}_working`
128
+
129
+ const client = yield* acquireClient(options)
130
+
131
+ const run = (operation: Operation, sql: string, params?: Array<unknown>) =>
132
+ Effect.tryPromise({
133
+ try: () => client.query(sql, params),
134
+ catch: (cause) => new MemoryStorageError({ operation, cause })
135
+ })
136
+
137
+ const decoded = <A>(
138
+ operation: Operation,
139
+ decoder: (rows: unknown) => Effect.Effect<A, ParseResult.ParseError>
140
+ ) =>
141
+ (rows: Array<unknown>) =>
142
+ decoder(rows).pipe(
143
+ Effect.mapError((cause) => new MemoryStorageError({ operation, cause }))
144
+ )
145
+
146
+ yield* Effect.forEach(
147
+ migrationStatements(prefix, options.dimensions),
148
+ (statement) => Effect.promise(() => client.query(statement))
149
+ ).pipe(Effect.orDie)
150
+
151
+ const neighborsOf = (entry: MemoryEntry, range: number) =>
152
+ Effect.gen(function*() {
153
+ if (range <= 0) return [] as ReadonlyArray<MemoryEntry>
154
+ const result = yield* run(
155
+ "search",
156
+ `WITH thread AS (
157
+ SELECT id, resource_id, thread_id, content, created_at,
158
+ row_number() OVER (ORDER BY seq) AS rn
159
+ FROM ${entries}
160
+ WHERE resource_id = $1 AND thread_id = $2
161
+ ), anchor AS (
162
+ SELECT rn FROM thread WHERE id = $3
163
+ )
164
+ SELECT t.id, t.resource_id, t.thread_id, t.content, t.created_at
165
+ FROM thread t, anchor a
166
+ WHERE t.rn BETWEEN a.rn - $4 AND a.rn + $4 AND t.id <> $3
167
+ ORDER BY t.rn`,
168
+ [entry.resourceId, entry.threadId, entry.id, range]
169
+ )
170
+ const rows = yield* decoded("search", decodeEntryRows)(result.rows)
171
+ return rows.map(toEntry)
172
+ })
173
+
174
+ return {
175
+ store: (input) =>
176
+ Effect.gen(function*() {
177
+ const now = yield* Clock.currentTimeMillis
178
+ const entry: MemoryEntry = {
179
+ id: `mem_${crypto.randomUUID()}`,
180
+ resourceId: input.resourceId,
181
+ threadId: input.threadId,
182
+ content: input.content,
183
+ createdAt: new Date(now)
184
+ }
185
+ yield* run(
186
+ "store",
187
+ `INSERT INTO ${entries} (id, resource_id, thread_id, content, embedding, created_at)
188
+ VALUES ($1, $2, $3, $4, $5::vector, $6)`,
189
+ [entry.id, entry.resourceId, entry.threadId, entry.content, toVectorLiteral(input.embedding), entry.createdAt]
190
+ )
191
+ return entry
192
+ }),
193
+
194
+ search: (input: SearchMemoryInput) =>
195
+ Effect.gen(function*() {
196
+ const result = yield* run(
197
+ "search",
198
+ `SELECT id, resource_id, thread_id, content, created_at,
199
+ 1 - (embedding <=> $1::vector) AS score
200
+ FROM ${entries}
201
+ WHERE resource_id = $2
202
+ AND ($3::text IS NULL OR thread_id = $3)
203
+ AND 1 - (embedding <=> $1::vector) >= $4
204
+ ORDER BY embedding <=> $1::vector
205
+ LIMIT $5`,
206
+ [
207
+ toVectorLiteral(input.embedding),
208
+ input.resourceId,
209
+ input.scope === "thread" ? input.threadId : null,
210
+ input.threshold,
211
+ input.topK
212
+ ]
213
+ )
214
+ const rows = yield* decoded("search", decodeSearchRows)(result.rows)
215
+ const matches: Array<MemorySearchResult> = []
216
+ for (const row of rows) {
217
+ const entry = toEntry(row)
218
+ matches.push({
219
+ entry,
220
+ score: row.score,
221
+ neighbors: yield* neighborsOf(entry, input.messageRange)
222
+ })
223
+ }
224
+ return matches
225
+ }),
226
+
227
+ remove: (id) => run("remove", `DELETE FROM ${entries} WHERE id = $1`, [id]).pipe(Effect.asVoid),
228
+
229
+ getWorkingMemory: (key) =>
230
+ Effect.gen(function*() {
231
+ const result = yield* run(
232
+ "getWorkingMemory",
233
+ `SELECT content FROM ${working} WHERE scope = $1 AND resource_id = $2 AND thread_id = $3`,
234
+ workingMemoryParams(key)
235
+ )
236
+ const rows = yield* decoded("getWorkingMemory", decodeWorkingMemoryRows)(result.rows)
237
+ return Option.fromNullable(rows[0]?.content)
238
+ }),
239
+
240
+ setWorkingMemory: (key, content) =>
241
+ run(
242
+ "setWorkingMemory",
243
+ `INSERT INTO ${working} (scope, resource_id, thread_id, content, updated_at)
244
+ VALUES ($1, $2, $3, $4, now())
245
+ ON CONFLICT (scope, resource_id, thread_id)
246
+ DO UPDATE SET content = EXCLUDED.content, updated_at = now()`,
247
+ [...workingMemoryParams(key), content]
248
+ ).pipe(Effect.asVoid)
249
+ }
250
+ })
251
+ )