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 +21 -0
- package/README.md +53 -0
- package/dist/cjs/index.js +158 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/dts/index.d.ts +38 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/esm/index.js +150 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/package.json +35 -0
- package/src/index.ts +251 -0
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":";;;;;;AAEA,IAAAA,OAAA,GAAAC,OAAA;AACA,IAAAC,UAAA,GAAAD,OAAA;AAHA;;AAsCA,MAAME,QAAQ,gBAAGC,cAAM,CAACC,MAAM,CAAC;EAC7BC,EAAE,EAAEF,cAAM,CAACG,MAAM;EACjBC,WAAW,EAAEJ,cAAM,CAACG,MAAM;EAC1BE,SAAS,EAAEL,cAAM,CAACG,MAAM;EACxBG,OAAO,EAAEN,cAAM,CAACG,MAAM;EACtBI,UAAU,eAAEP,cAAM,CAACQ,KAAK,CAACR,cAAM,CAACS,YAAY,EAAET,cAAM,CAACU,IAAI;CAC1D,CAAC;AAEF,MAAMC,SAAS,gBAAGX,cAAM,CAACC,MAAM,CAAC;EAAE,GAAGF,QAAQ,CAACa,MAAM;EAAEC,KAAK,EAAEb,cAAM,CAACc;AAAM,CAAE,CAAC;AAE7E,MAAMC,gBAAgB,gBAAGf,cAAM,CAACC,MAAM,CAAC;EAAEK,OAAO,EAAEN,cAAM,CAACG;AAAM,CAAE,CAAC;AAElE,MAAMa,eAAe,gBAAGhB,cAAM,CAACiB,aAAa,cAACjB,cAAM,CAACkB,KAAK,CAACnB,QAAQ,CAAC,CAAC;AACpE,MAAMoB,gBAAgB,gBAAGnB,cAAM,CAACiB,aAAa,cAACjB,cAAM,CAACkB,KAAK,CAACP,SAAS,CAAC,CAAC;AACtE,MAAMS,uBAAuB,gBAAGpB,cAAM,CAACiB,aAAa,cAACjB,cAAM,CAACkB,KAAK,CAACH,gBAAgB,CAAC,CAAC;AAEpF,MAAMM,OAAO,GAAIC,GAAyB,KAAmB;EAC3DpB,EAAE,EAAEoB,GAAG,CAACpB,EAAE;EACVqB,UAAU,EAAED,GAAG,CAAClB,WAAW;EAC3BoB,QAAQ,EAAEF,GAAG,CAACjB,SAAS;EACvBC,OAAO,EAAEgB,GAAG,CAAChB,OAAO;EACpBmB,SAAS,EAAEH,GAAG,CAACf;CAChB,CAAC;AAEF;AACA,MAAMmB,eAAe,GAAIC,SAAgC,IAAKC,IAAI,CAACC,SAAS,CAACF,SAAS,CAAC;AAEvF;;;;AAIA,MAAMG,mBAAmB,GAAGA,CAACC,MAAc,EAAEC,UAAkB,KAA4B,CACzF,uCAAuC,EACvC,8BAA8BD,MAAM;;;;;uBAKfC,UAAU;;;IAG7B,EACF,8BAA8BD,MAAM;SAC7BA,MAAM,mCAAmC,EAChD,8BAA8BA,MAAM;;;;;;;IAOlC,CACH;AAED;AACA,MAAME,mBAAmB,GAAIC,GAAqB,IAAK,CACrDA,GAAG,CAACC,KAAK,EACTD,GAAG,CAACX,UAAU,EACdW,GAAG,CAACC,KAAK,KAAK,UAAU,GAAG,EAAE,GAAGD,GAAG,CAACV,QAAQ,CAC7C;AAED,MAAMY,aAAa,GAAIC,OAA+B,IACpD,QAAQ,IAAIA,OAAO,GACfC,cAAM,CAACC,OAAO,CAACF,OAAO,CAACG,MAAM,CAAC,GAC9BF,cAAM,CAACG,cAAc,CACrBH,cAAM,CAACI,OAAO,CAAC,YAAW;EACxB,MAAM;IAAEC,OAAO,EAAEC;EAAE,CAAE,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC;EAC1C,OAAO,IAAIA,EAAE,CAACC,IAAI,CAAC;IAAEC,gBAAgB,EAAET,OAAO,CAACS;EAAgB,CAAE,CAAC;AACpE,CAAC,CAAC,EACDC,IAAI,IAAKT,cAAM,CAACI,OAAO,CAAC,MAAMK,IAAI,CAACC,GAAG,EAAE,CAAC,CAC3C;AAEL;;;;;AAKO,MAAMC,eAAe,GAAIZ,OAA+B,IAC7Da,aAAK,CAACC,MAAM,CACVC,iBAAM,EACNd,cAAM,CAACe,GAAG,CAAC,aAAS;EAClB,MAAMtB,MAAM,GAAGM,OAAO,CAACiB,WAAW,IAAI,YAAY;EAClD,IAAI,CAAC,oBAAoB,CAACC,IAAI,CAACxB,MAAM,CAAC,EAAE;IACtC,OAAO,OAAOO,cAAM,CAACkB,UAAU,CAC7B,uCAAuCzB,MAAM,gDAAgD,CAC9F;EACH;EACA,MAAM0B,OAAO,GAAG,GAAG1B,MAAM,UAAU;EACnC,MAAM2B,OAAO,GAAG,GAAG3B,MAAM,UAAU;EAEnC,MAAMS,MAAM,GAAG,OAAOJ,aAAa,CAACC,OAAO,CAAC;EAE5C,MAAMsB,GAAG,GAAGA,CAACC,SAAoB,EAAEC,GAAW,EAAEC,MAAuB,KACrExB,cAAM,CAACyB,UAAU,CAAC;IAChBC,GAAG,EAAEA,CAAA,KAAMxB,MAAM,CAACyB,KAAK,CAACJ,GAAG,EAAEC,MAAM,CAAC;IACpCI,KAAK,EAAGC,KAAK,IAAK,IAAIC,6BAAkB,CAAC;MAAER,SAAS;MAAEO;IAAK,CAAE;GAC9D,CAAC;EAEJ,MAAME,OAAO,GAAGA,CACdT,SAAoB,EACpBU,OAAoE,KAErEC,IAAoB,IACnBD,OAAO,CAACC,IAAI,CAAC,CAACC,IAAI,CAChBlC,cAAM,CAACmC,QAAQ,CAAEN,KAAK,IAAK,IAAIC,6BAAkB,CAAC;IAAER,SAAS;IAAEO;EAAK,CAAE,CAAC,CAAC,CACzE;EAEH,OAAO7B,cAAM,CAACoC,OAAO,CACnB5C,mBAAmB,CAACC,MAAM,EAAEM,OAAO,CAACL,UAAU,CAAC,EAC9C2C,SAAS,IAAKrC,cAAM,CAACI,OAAO,CAAC,MAAMF,MAAM,CAACyB,KAAK,CAACU,SAAS,CAAC,CAAC,CAC7D,CAACH,IAAI,CAAClC,cAAM,CAACsC,KAAK,CAAC;EAEpB,MAAMC,WAAW,GAAGA,CAACC,KAAkB,EAAEC,KAAa,KACpDzC,cAAM,CAACe,GAAG,CAAC,aAAS;IAClB,IAAI0B,KAAK,IAAI,CAAC,EAAE,OAAO,EAAgC;IACvD,MAAMC,MAAM,GAAG,OAAOrB,GAAG,CACvB,QAAQ,EACR;;;sBAGUF,OAAO;;;;;;;;2BAQF,EACf,CAACqB,KAAK,CAACvD,UAAU,EAAEuD,KAAK,CAACtD,QAAQ,EAAEsD,KAAK,CAAC5E,EAAE,EAAE6E,KAAK,CAAC,CACpD;IACD,MAAMR,IAAI,GAAG,OAAOF,OAAO,CAAC,QAAQ,EAAErD,eAAe,CAAC,CAACgE,MAAM,CAACT,IAAI,CAAC;IACnE,OAAOA,IAAI,CAACU,GAAG,CAAC5D,OAAO,CAAC;EAC1B,CAAC,CAAC;EAEJ,OAAO;IACL6D,KAAK,EAAGC,KAAK,IACX7C,cAAM,CAACe,GAAG,CAAC,aAAS;MAClB,MAAM+B,GAAG,GAAG,OAAOC,aAAK,CAACC,iBAAiB;MAC1C,MAAMR,KAAK,GAAgB;QACzB5E,EAAE,EAAE,OAAOqF,MAAM,CAACC,UAAU,EAAE,EAAE;QAChCjE,UAAU,EAAE4D,KAAK,CAAC5D,UAAU;QAC5BC,QAAQ,EAAE2D,KAAK,CAAC3D,QAAQ;QACxBlB,OAAO,EAAE6E,KAAK,CAAC7E,OAAO;QACtBmB,SAAS,EAAE,IAAIf,IAAI,CAAC0E,GAAG;OACxB;MACD,OAAOzB,GAAG,CACR,OAAO,EACP,eAAeF,OAAO;uDACmB,EACzC,CAACqB,KAAK,CAAC5E,EAAE,EAAE4E,KAAK,CAACvD,UAAU,EAAEuD,KAAK,CAACtD,QAAQ,EAAEsD,KAAK,CAACxE,OAAO,EAAEoB,eAAe,CAACyD,KAAK,CAACxD,SAAS,CAAC,EAAEmD,KAAK,CAACrD,SAAS,CAAC,CAC/G;MACD,OAAOqD,KAAK;IACd,CAAC,CAAC;IAEJW,MAAM,EAAGN,KAAwB,IAC/B7C,cAAM,CAACe,GAAG,CAAC,aAAS;MAClB,MAAM2B,MAAM,GAAG,OAAOrB,GAAG,CACvB,QAAQ,EACR;;sBAEQF,OAAO;;;;;wBAKL,EACV,CACE/B,eAAe,CAACyD,KAAK,CAACxD,SAAS,CAAC,EAChCwD,KAAK,CAAC5D,UAAU,EAChB4D,KAAK,CAAChD,KAAK,KAAK,QAAQ,GAAGgD,KAAK,CAAC3D,QAAQ,GAAG,IAAI,EAChD2D,KAAK,CAACO,SAAS,EACfP,KAAK,CAACQ,IAAI,CACX,CACF;MACD,MAAMpB,IAAI,GAAG,OAAOF,OAAO,CAAC,QAAQ,EAAElD,gBAAgB,CAAC,CAAC6D,MAAM,CAACT,IAAI,CAAC;MACpE,MAAMqB,OAAO,GAA8B,EAAE;MAC7C,KAAK,MAAMtE,GAAG,IAAIiD,IAAI,EAAE;QACtB,MAAMO,KAAK,GAAGzD,OAAO,CAACC,GAAG,CAAC;QAC1BsE,OAAO,CAACC,IAAI,CAAC;UACXf,KAAK;UACLjE,KAAK,EAAES,GAAG,CAACT,KAAK;UAChBiF,SAAS,EAAE,OAAOjB,WAAW,CAACC,KAAK,EAAEK,KAAK,CAACY,YAAY;SACxD,CAAC;MACJ;MACA,OAAOH,OAAO;IAChB,CAAC,CAAC;IAEJI,MAAM,EAAG9F,EAAE,IAAKyD,GAAG,CAAC,QAAQ,EAAE,eAAeF,OAAO,gBAAgB,EAAE,CAACvD,EAAE,CAAC,CAAC,CAACsE,IAAI,CAAClC,cAAM,CAAC2D,MAAM,CAAC;IAE/FC,gBAAgB,EAAGhE,GAAG,IACpBI,cAAM,CAACe,GAAG,CAAC,aAAS;MAClB,MAAM2B,MAAM,GAAG,OAAOrB,GAAG,CACvB,kBAAkB,EAClB,uBAAuBD,OAAO,2DAA2D,EACzFzB,mBAAmB,CAACC,GAAG,CAAC,CACzB;MACD,MAAMqC,IAAI,GAAG,OAAOF,OAAO,CAAC,kBAAkB,EAAEjD,uBAAuB,CAAC,CAAC4D,MAAM,CAACT,IAAI,CAAC;MACrF,OAAO4B,cAAM,CAACC,YAAY,CAAC7B,IAAI,CAAC,CAAC,CAAC,EAAEjE,OAAO,CAAC;IAC9C,CAAC,CAAC;IAEJ+F,gBAAgB,EAAEA,CAACnE,GAAG,EAAE5B,OAAO,KAC7BqD,GAAG,CACD,kBAAkB,EAClB,eAAeD,OAAO;;;0EAGwC,EAC9D,CAAC,GAAGzB,mBAAmB,CAACC,GAAG,CAAC,EAAE5B,OAAO,CAAC,CACvC,CAACkE,IAAI,CAAClC,cAAM,CAAC2D,MAAM;GACvB;AACH,CAAC,CAAC,CACH;AAAAK,OAAA,CAAArD,eAAA,GAAAA,eAAA","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":[]}
|
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
|
+
)
|