@persql/context 0.1.0 → 1.0.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/README.md +83 -36
- package/dist/{chunk-RMG66FDI.js → chunk-HRHBXOPL.js} +91 -113
- package/dist/chunk-HRHBXOPL.js.map +1 -0
- package/dist/core.cjs +93 -116
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +54 -48
- package/dist/core.d.ts +54 -48
- package/dist/core.js +7 -9
- package/dist/index.cjs +211 -178
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -40
- package/dist/index.d.ts +34 -40
- package/dist/index.js +124 -71
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-RMG66FDI.js.map +0 -1
package/dist/core.cjs
CHANGED
|
@@ -25,62 +25,59 @@ __export(core_exports, {
|
|
|
25
25
|
SCHEMA_SQL: () => SCHEMA_SQL,
|
|
26
26
|
SCHEMA_VERSION: () => SCHEMA_VERSION,
|
|
27
27
|
TABLE_PREFIX: () => TABLE_PREFIX,
|
|
28
|
-
|
|
28
|
+
buildDetectLegacySchema: () => buildDetectLegacySchema,
|
|
29
|
+
buildDropLegacy: () => buildDropLegacy,
|
|
29
30
|
buildEdgesAmong: () => buildEdgesAmong,
|
|
30
31
|
buildExtractionMessages: () => buildExtractionMessages,
|
|
31
32
|
buildForget: () => buildForget,
|
|
32
33
|
buildGet: () => buildGet,
|
|
34
|
+
buildIndex: () => buildIndex,
|
|
33
35
|
buildInit: () => buildInit,
|
|
34
36
|
buildNeighborEntities: () => buildNeighborEntities,
|
|
35
37
|
buildRecall: () => buildRecall,
|
|
36
|
-
buildRecent: () => buildRecent,
|
|
37
38
|
buildRemember: () => buildRemember,
|
|
38
39
|
buildStats: () => buildStats,
|
|
39
|
-
buildSupersede: () => buildSupersede,
|
|
40
40
|
buildUpsertEdge: () => buildUpsertEdge,
|
|
41
41
|
buildUpsertEntity: () => buildUpsertEntity,
|
|
42
42
|
entityId: () => entityId,
|
|
43
|
-
normalizeTags: () => normalizeTags,
|
|
44
43
|
toFtsQuery: () => toFtsQuery
|
|
45
44
|
});
|
|
46
45
|
module.exports = __toCommonJS(core_exports);
|
|
47
|
-
var SCHEMA_VERSION =
|
|
46
|
+
var SCHEMA_VERSION = 2;
|
|
48
47
|
var TABLE_PREFIX = "ctx_";
|
|
49
48
|
var T = TABLE_PREFIX;
|
|
50
49
|
var SCHEMA_SQL = [
|
|
51
50
|
`CREATE TABLE IF NOT EXISTS ${T}memory (
|
|
52
51
|
id TEXT PRIMARY KEY,
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
name TEXT NOT NULL,
|
|
53
|
+
description TEXT NOT NULL,
|
|
54
|
+
type TEXT NOT NULL DEFAULT 'project',
|
|
55
55
|
body TEXT NOT NULL,
|
|
56
|
-
tags TEXT,
|
|
57
56
|
source TEXT,
|
|
58
57
|
created_at INTEGER NOT NULL,
|
|
59
|
-
|
|
58
|
+
updated_at INTEGER NOT NULL
|
|
60
59
|
)`,
|
|
61
|
-
`CREATE INDEX IF NOT EXISTS ${T}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// External-content FTS index: body text is not duplicated, only indexed.
|
|
65
|
-
// Porter stemmer so "invoice" recalls "invoicing" without embeddings.
|
|
60
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS ${T}memory_name ON ${T}memory(name)`,
|
|
61
|
+
// External-content FTS on name+description+body. Porter stemmer so
|
|
62
|
+
// "invoice" recalls "invoicing" without embeddings.
|
|
66
63
|
`CREATE VIRTUAL TABLE IF NOT EXISTS ${T}memory_fts USING fts5(
|
|
67
|
-
|
|
64
|
+
name, description, body,
|
|
68
65
|
content='${T}memory', content_rowid='rowid',
|
|
69
66
|
tokenize='porter unicode61'
|
|
70
67
|
)`,
|
|
71
68
|
`CREATE TRIGGER IF NOT EXISTS ${T}memory_ai AFTER INSERT ON ${T}memory BEGIN
|
|
72
|
-
INSERT INTO ${T}memory_fts(rowid,
|
|
73
|
-
VALUES (new.rowid, new.
|
|
69
|
+
INSERT INTO ${T}memory_fts(rowid, name, description, body)
|
|
70
|
+
VALUES (new.rowid, new.name, new.description, new.body);
|
|
74
71
|
END`,
|
|
75
72
|
`CREATE TRIGGER IF NOT EXISTS ${T}memory_ad AFTER DELETE ON ${T}memory BEGIN
|
|
76
|
-
INSERT INTO ${T}memory_fts(${T}memory_fts, rowid,
|
|
77
|
-
VALUES ('delete', old.rowid, old.
|
|
73
|
+
INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, name, description, body)
|
|
74
|
+
VALUES ('delete', old.rowid, old.name, old.description, old.body);
|
|
78
75
|
END`,
|
|
79
76
|
`CREATE TRIGGER IF NOT EXISTS ${T}memory_au AFTER UPDATE ON ${T}memory BEGIN
|
|
80
|
-
INSERT INTO ${T}memory_fts(${T}memory_fts, rowid,
|
|
81
|
-
VALUES ('delete', old.rowid, old.
|
|
82
|
-
INSERT INTO ${T}memory_fts(rowid,
|
|
83
|
-
VALUES (new.rowid, new.
|
|
77
|
+
INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, name, description, body)
|
|
78
|
+
VALUES ('delete', old.rowid, old.name, old.description, old.body);
|
|
79
|
+
INSERT INTO ${T}memory_fts(rowid, name, description, body)
|
|
80
|
+
VALUES (new.rowid, new.name, new.description, new.body);
|
|
84
81
|
END`,
|
|
85
82
|
`CREATE TABLE IF NOT EXISTS ${T}entity (
|
|
86
83
|
id TEXT PRIMARY KEY,
|
|
@@ -103,15 +100,21 @@ var SCHEMA_SQL = [
|
|
|
103
100
|
function buildInit() {
|
|
104
101
|
return SCHEMA_SQL.map((sql) => ({ sql, params: [] }));
|
|
105
102
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
new Set(arr.map((t) => t.trim().toLowerCase()).filter(Boolean))
|
|
112
|
-
);
|
|
113
|
-
return norm.length ? norm.join(" ") : null;
|
|
103
|
+
function buildDetectLegacySchema() {
|
|
104
|
+
return {
|
|
105
|
+
sql: `SELECT count(*) AS n FROM pragma_table_info('${T}memory') WHERE name='topic'`,
|
|
106
|
+
params: []
|
|
107
|
+
};
|
|
114
108
|
}
|
|
109
|
+
function buildDropLegacy() {
|
|
110
|
+
return [
|
|
111
|
+
{ sql: `DROP TABLE IF EXISTS ${T}memory_fts`, params: [] },
|
|
112
|
+
{ sql: `DROP TABLE IF EXISTS ${T}memory`, params: [] },
|
|
113
|
+
{ sql: `DROP TABLE IF EXISTS ${T}entity`, params: [] },
|
|
114
|
+
{ sql: `DROP TABLE IF EXISTS ${T}edge`, params: [] }
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
var FTS_OPERATORS = /* @__PURE__ */ new Set(["and", "or", "not", "near"]);
|
|
115
118
|
function toFtsQuery(input, opts = {}) {
|
|
116
119
|
if (opts.mode === "raw") return input.trim() || null;
|
|
117
120
|
const tokens = (input.match(/[\p{L}\p{N}_]+/gu) ?? []).filter(
|
|
@@ -134,103 +137,71 @@ function resolveEntityRef(ref) {
|
|
|
134
137
|
function edgeId(srcId, rel, dstId) {
|
|
135
138
|
return slugId("x_", `${srcId}|${rel}|${dstId}`);
|
|
136
139
|
}
|
|
137
|
-
var MEMORY_COLS = "id,
|
|
140
|
+
var MEMORY_COLS = "id, name, description, type, body, source, created_at AS createdAt, updated_at AS updatedAt";
|
|
138
141
|
function buildRemember(input, now, id) {
|
|
139
142
|
return {
|
|
140
|
-
sql: `INSERT INTO ${T}memory (id,
|
|
141
|
-
VALUES (?, ?, ?, ?, ?, ?, ?,
|
|
143
|
+
sql: `INSERT INTO ${T}memory (id, name, description, type, body, source, created_at, updated_at)
|
|
144
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
145
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
146
|
+
description = excluded.description,
|
|
147
|
+
body = excluded.body,
|
|
148
|
+
type = excluded.type,
|
|
149
|
+
source = excluded.source,
|
|
150
|
+
updated_at = excluded.updated_at`,
|
|
142
151
|
params: [
|
|
143
152
|
id,
|
|
144
|
-
input.
|
|
145
|
-
input.
|
|
153
|
+
input.name,
|
|
154
|
+
input.description,
|
|
155
|
+
input.type ?? "project",
|
|
146
156
|
input.body,
|
|
147
|
-
normalizeTags(input.tags),
|
|
148
157
|
input.source ?? null,
|
|
158
|
+
now,
|
|
149
159
|
now
|
|
150
160
|
]
|
|
151
161
|
};
|
|
152
162
|
}
|
|
153
|
-
function
|
|
154
|
-
return {
|
|
155
|
-
sql: `UPDATE ${T}memory SET superseded_by = ? WHERE id = ? AND superseded_by IS NULL`,
|
|
156
|
-
params: [newId, oldId]
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
function buildForget(id) {
|
|
160
|
-
return { sql: `DELETE FROM ${T}memory WHERE id = ?`, params: [id] };
|
|
163
|
+
function buildForget(name) {
|
|
164
|
+
return { sql: `DELETE FROM ${T}memory WHERE name = ?`, params: [name] };
|
|
161
165
|
}
|
|
162
|
-
function buildGet(
|
|
163
|
-
return {
|
|
164
|
-
sql: `SELECT ${MEMORY_COLS} FROM ${T}memory WHERE id = ?`,
|
|
165
|
-
params: [id]
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
function buildRecall(query, opts = {}) {
|
|
169
|
-
const match = toFtsQuery(query, { operator: opts.operator, mode: opts.mode });
|
|
170
|
-
if (!match) return null;
|
|
171
|
-
const where = [`${T}memory_fts MATCH ?`];
|
|
172
|
-
const params = [match];
|
|
173
|
-
if (!opts.includeSuperseded) where.push("m.superseded_by IS NULL");
|
|
174
|
-
if (opts.kind) {
|
|
175
|
-
where.push("m.kind = ?");
|
|
176
|
-
params.push(opts.kind);
|
|
177
|
-
}
|
|
178
|
-
if (opts.sinceMs != null) {
|
|
179
|
-
where.push("m.created_at >= ?");
|
|
180
|
-
params.push(opts.sinceMs);
|
|
181
|
-
}
|
|
182
|
-
params.push(opts.limit ?? 8);
|
|
166
|
+
function buildGet(name) {
|
|
183
167
|
return {
|
|
184
|
-
sql: `SELECT
|
|
185
|
-
|
|
186
|
-
FROM ${T}memory_fts
|
|
187
|
-
JOIN ${T}memory m ON m.rowid = ${T}memory_fts.rowid
|
|
188
|
-
WHERE ${where.join(" AND ")}
|
|
189
|
-
ORDER BY ${T}memory_fts.rank
|
|
190
|
-
LIMIT ?`,
|
|
191
|
-
params
|
|
168
|
+
sql: `SELECT ${MEMORY_COLS} FROM ${T}memory WHERE name = ?`,
|
|
169
|
+
params: [name]
|
|
192
170
|
};
|
|
193
171
|
}
|
|
194
|
-
function
|
|
172
|
+
function buildIndex(opts = {}) {
|
|
195
173
|
const where = [];
|
|
196
174
|
const params = [];
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
params.push(opts.kind);
|
|
175
|
+
if (opts.type) {
|
|
176
|
+
where.push("type = ?");
|
|
177
|
+
params.push(opts.type);
|
|
201
178
|
}
|
|
202
|
-
params.push(opts.limit ??
|
|
179
|
+
params.push(opts.limit ?? 50);
|
|
203
180
|
return {
|
|
204
|
-
sql: `SELECT
|
|
181
|
+
sql: `SELECT id, name, description, type, updated_at AS updatedAt FROM ${T}memory
|
|
205
182
|
${where.length ? "WHERE " + where.join(" AND ") : ""}
|
|
206
|
-
ORDER BY
|
|
207
|
-
LIMIT ?`,
|
|
183
|
+
ORDER BY updated_at DESC LIMIT ?`,
|
|
208
184
|
params
|
|
209
185
|
};
|
|
210
186
|
}
|
|
211
|
-
function
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
const params = [`% ${t} %`];
|
|
215
|
-
if (!opts.includeSuperseded) where.push("superseded_by IS NULL");
|
|
216
|
-
if (opts.kind) {
|
|
217
|
-
where.push("kind = ?");
|
|
218
|
-
params.push(opts.kind);
|
|
219
|
-
}
|
|
220
|
-
params.push(opts.limit ?? 20);
|
|
187
|
+
function buildRecall(query, opts = {}) {
|
|
188
|
+
const match = toFtsQuery(query, { operator: opts.operator, mode: opts.mode });
|
|
189
|
+
if (!match) return null;
|
|
221
190
|
return {
|
|
222
|
-
sql: `SELECT
|
|
223
|
-
|
|
224
|
-
|
|
191
|
+
sql: `SELECT m.id, m.name, m.description, m.type, m.body, m.source,
|
|
192
|
+
m.created_at AS createdAt, m.updated_at AS updatedAt
|
|
193
|
+
FROM ${T}memory_fts
|
|
194
|
+
JOIN ${T}memory m ON m.rowid = ${T}memory_fts.rowid
|
|
195
|
+
WHERE ${T}memory_fts MATCH ?
|
|
196
|
+
ORDER BY ${T}memory_fts.rank
|
|
225
197
|
LIMIT ?`,
|
|
226
|
-
params
|
|
198
|
+
params: [match, opts.limit ?? 8]
|
|
227
199
|
};
|
|
228
200
|
}
|
|
229
201
|
function buildStats() {
|
|
230
202
|
return {
|
|
231
203
|
sql: `SELECT
|
|
232
|
-
(SELECT count(*) FROM ${T}memory
|
|
233
|
-
(SELECT count(*) FROM ${T}memory) AS facts_total,
|
|
204
|
+
(SELECT count(*) FROM ${T}memory) AS memories,
|
|
234
205
|
(SELECT count(*) FROM ${T}entity) AS entities,
|
|
235
206
|
(SELECT count(*) FROM ${T}edge) AS edges`,
|
|
236
207
|
params: []
|
|
@@ -279,8 +250,7 @@ function buildNeighborEntities(seedId, depth, limit) {
|
|
|
279
250
|
GROUP BY en.id
|
|
280
251
|
ORDER BY depth, en.name
|
|
281
252
|
LIMIT ?`,
|
|
282
|
-
// The seed
|
|
283
|
-
// exclude it explicitly so it isn't listed as its own neighbor.
|
|
253
|
+
// The seed appears at depth >0 on undirected round-trips — exclude it.
|
|
284
254
|
params: [seedId, depth, seedId, limit]
|
|
285
255
|
};
|
|
286
256
|
}
|
|
@@ -296,14 +266,21 @@ function buildEdgesAmong(ids) {
|
|
|
296
266
|
params: [...ids, ...ids]
|
|
297
267
|
};
|
|
298
268
|
}
|
|
299
|
-
var EXTRACTION_SYSTEM_PROMPT = `You extract durable,
|
|
269
|
+
var EXTRACTION_SYSTEM_PROMPT = `You extract durable, structured memories from a conversation or document for an AI agent that will recall them across sessions.
|
|
300
270
|
|
|
301
|
-
Return JSON: { "
|
|
302
|
-
-
|
|
303
|
-
|
|
304
|
-
|
|
271
|
+
Return JSON: { "memories": [...], "entities": [...], "edges": [...] }.
|
|
272
|
+
- memories: facts worth remembering across sessions \u2014 decisions, conventions, constraints, identifiers, preferences, schema details. Each:
|
|
273
|
+
{
|
|
274
|
+
"name": string (kebab-case slug, unique key \u2014 e.g. "auth-provider-choice", "todos-table-schema", "user-prefers-metrics"),
|
|
275
|
+
"description": string (one-liner shown in the always-loaded index),
|
|
276
|
+
"type": "user"|"feedback"|"project"|"reference",
|
|
277
|
+
"body": string (full markdown content with all relevant detail)
|
|
278
|
+
}
|
|
279
|
+
Types: user=who they are/preferences; feedback=guidance for agents; project=ongoing work/decisions; reference=where to find things.
|
|
280
|
+
- entities: named things \u2014 services, people, tables, repos. Each: { "name": string, "kind"?: string, "body"?: string }
|
|
281
|
+
- edges: relationships between entities. Each: { "src": name, "rel": string, "dst": name }
|
|
305
282
|
|
|
306
|
-
Be conservative
|
|
283
|
+
Be conservative. Use unique, meaningful names. If nothing durable is found, return empty arrays.`;
|
|
307
284
|
function buildExtractionMessages(raw, opts = {}) {
|
|
308
285
|
return [
|
|
309
286
|
{ role: "system", content: EXTRACTION_SYSTEM_PROMPT },
|
|
@@ -318,16 +295,17 @@ function buildExtractionMessages(raw, opts = {}) {
|
|
|
318
295
|
var EXTRACTION_JSON_SCHEMA = {
|
|
319
296
|
type: "object",
|
|
320
297
|
properties: {
|
|
321
|
-
|
|
298
|
+
memories: {
|
|
322
299
|
type: "array",
|
|
323
300
|
items: {
|
|
324
301
|
type: "object",
|
|
325
302
|
properties: {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
303
|
+
name: { type: "string" },
|
|
304
|
+
description: { type: "string" },
|
|
305
|
+
type: { type: "string", enum: ["user", "feedback", "project", "reference"] },
|
|
306
|
+
body: { type: "string" }
|
|
329
307
|
},
|
|
330
|
-
required: ["body"]
|
|
308
|
+
required: ["name", "description", "body"]
|
|
331
309
|
}
|
|
332
310
|
},
|
|
333
311
|
entities: {
|
|
@@ -363,22 +341,21 @@ var EXTRACTION_JSON_SCHEMA = {
|
|
|
363
341
|
SCHEMA_SQL,
|
|
364
342
|
SCHEMA_VERSION,
|
|
365
343
|
TABLE_PREFIX,
|
|
366
|
-
|
|
344
|
+
buildDetectLegacySchema,
|
|
345
|
+
buildDropLegacy,
|
|
367
346
|
buildEdgesAmong,
|
|
368
347
|
buildExtractionMessages,
|
|
369
348
|
buildForget,
|
|
370
349
|
buildGet,
|
|
350
|
+
buildIndex,
|
|
371
351
|
buildInit,
|
|
372
352
|
buildNeighborEntities,
|
|
373
353
|
buildRecall,
|
|
374
|
-
buildRecent,
|
|
375
354
|
buildRemember,
|
|
376
355
|
buildStats,
|
|
377
|
-
buildSupersede,
|
|
378
356
|
buildUpsertEdge,
|
|
379
357
|
buildUpsertEntity,
|
|
380
358
|
entityId,
|
|
381
|
-
normalizeTags,
|
|
382
359
|
toFtsQuery
|
|
383
360
|
});
|
|
384
361
|
//# sourceMappingURL=core.cjs.map
|
package/dist/core.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * @persql/context/core — zero-runtime-dependency engine.\n *\n * Schema DDL, SQL builders, and the extraction/compaction contracts shared by\n * the `@persql/context` SDK (client-side, over `@persql/sdk`) and the hosted\n * PerSQL Context worker (server-side, over `@persql/ai`). Keeping both sides on\n * the same builders means client-recall and server-recall can never drift.\n *\n * Retrieval is lexical: FTS5 with the porter stemmer, BM25-ranked. No vectors.\n */\n\nexport const SCHEMA_VERSION = 1;\nexport const TABLE_PREFIX = \"ctx_\";\nconst T = TABLE_PREFIX;\n\nexport type MemoryKind = \"fact\" | \"episode\" | \"artifact\";\n\nexport interface MemoryRow {\n id: string;\n kind: MemoryKind;\n topic: string | null;\n body: string;\n tags: string | null;\n source: string | null;\n createdAt: number;\n supersededBy: string | null;\n}\n\nexport interface Entity {\n id: string;\n name: string;\n kind: string | null;\n body: string | null;\n createdAt: number;\n}\n\nexport interface Edge {\n id: string;\n src: string;\n rel: string;\n dst: string;\n source: string | null;\n createdAt: number;\n}\n\nexport interface RememberInput {\n body: string;\n topic?: string;\n kind?: MemoryKind;\n tags?: string[] | string;\n source?: string;\n supersedes?: string | string[];\n id?: string;\n}\n\nexport interface EntityInput {\n name: string;\n kind?: string;\n body?: string;\n id?: string;\n}\n\nexport interface EdgeInput {\n src: string;\n rel: string;\n dst: string;\n source?: string;\n}\n\nexport interface RecallOptions {\n limit?: number;\n kind?: MemoryKind;\n operator?: \"or\" | \"and\";\n mode?: \"terms\" | \"raw\";\n sinceMs?: number;\n withinDays?: number;\n includeSuperseded?: boolean;\n}\n\nexport interface ListOptions {\n limit?: number;\n kind?: MemoryKind;\n includeSuperseded?: boolean;\n}\n\n/** The shape an extractor (LLM or the hosted worker) returns from raw text. */\nexport interface ExtractedContext {\n facts?: Array<{ body: string; topic?: string; tags?: string[]; kind?: MemoryKind }>;\n entities?: EntityInput[];\n edges?: EdgeInput[];\n}\n\nexport interface SqlStatement {\n sql: string;\n params: unknown[];\n}\n\n// ---------------------------------------------------------------------------\n// Schema\n// ---------------------------------------------------------------------------\n\nexport const SCHEMA_SQL: string[] = [\n `CREATE TABLE IF NOT EXISTS ${T}memory (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL DEFAULT 'fact',\n topic TEXT,\n body TEXT NOT NULL,\n tags TEXT,\n source TEXT,\n created_at INTEGER NOT NULL,\n superseded_by TEXT\n )`,\n `CREATE INDEX IF NOT EXISTS ${T}memory_kind ON ${T}memory(kind)`,\n `CREATE INDEX IF NOT EXISTS ${T}memory_created ON ${T}memory(created_at)`,\n `CREATE INDEX IF NOT EXISTS ${T}memory_superseded ON ${T}memory(superseded_by)`,\n // External-content FTS index: body text is not duplicated, only indexed.\n // Porter stemmer so \"invoice\" recalls \"invoicing\" without embeddings.\n `CREATE VIRTUAL TABLE IF NOT EXISTS ${T}memory_fts USING fts5(\n topic, body, tags,\n content='${T}memory', content_rowid='rowid',\n tokenize='porter unicode61'\n )`,\n `CREATE TRIGGER IF NOT EXISTS ${T}memory_ai AFTER INSERT ON ${T}memory BEGIN\n INSERT INTO ${T}memory_fts(rowid, topic, body, tags)\n VALUES (new.rowid, new.topic, new.body, new.tags);\n END`,\n `CREATE TRIGGER IF NOT EXISTS ${T}memory_ad AFTER DELETE ON ${T}memory BEGIN\n INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, topic, body, tags)\n VALUES ('delete', old.rowid, old.topic, old.body, old.tags);\n END`,\n `CREATE TRIGGER IF NOT EXISTS ${T}memory_au AFTER UPDATE ON ${T}memory BEGIN\n INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, topic, body, tags)\n VALUES ('delete', old.rowid, old.topic, old.body, old.tags);\n INSERT INTO ${T}memory_fts(rowid, topic, body, tags)\n VALUES (new.rowid, new.topic, new.body, new.tags);\n END`,\n `CREATE TABLE IF NOT EXISTS ${T}entity (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n kind TEXT,\n body TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS ${T}edge (\n id TEXT PRIMARY KEY,\n src TEXT NOT NULL,\n rel TEXT NOT NULL,\n dst TEXT NOT NULL,\n source TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE INDEX IF NOT EXISTS ${T}edge_src ON ${T}edge(src)`,\n `CREATE INDEX IF NOT EXISTS ${T}edge_dst ON ${T}edge(dst)`,\n];\n\nexport function buildInit(): SqlStatement[] {\n return SCHEMA_SQL.map((sql) => ({ sql, params: [] }));\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst FTS_OPERATORS = new Set([\"and\", \"or\", \"not\", \"near\"]);\n\nexport function normalizeTags(tags?: string[] | string | null): string | null {\n if (!tags) return null;\n const arr = Array.isArray(tags) ? tags : String(tags).split(/[,\\s]+/);\n const norm = Array.from(\n new Set(arr.map((t) => t.trim().toLowerCase()).filter(Boolean))\n );\n return norm.length ? norm.join(\" \") : null;\n}\n\n/**\n * Turn arbitrary user text into a safe FTS5 MATCH expression. `terms` mode\n * (default) extracts word tokens, drops bare boolean operators, and ORs the\n * rest as quoted terms — so a caller can paste \"invoice OR billing\" or just\n * \"invoice billing\" and neither crashes the parser nor injects FTS syntax.\n * `raw` mode passes a hand-written FTS expression through untouched.\n */\nexport function toFtsQuery(\n input: string,\n opts: { operator?: \"or\" | \"and\"; mode?: \"terms\" | \"raw\" } = {}\n): string | null {\n if (opts.mode === \"raw\") return input.trim() || null;\n const tokens = (input.match(/[\\p{L}\\p{N}_]+/gu) ?? []).filter(\n (t) => !FTS_OPERATORS.has(t.toLowerCase())\n );\n if (!tokens.length) return null;\n const joiner = opts.operator === \"and\" ? \" AND \" : \" OR \";\n return tokens.map((t) => `\"${t}\"`).join(joiner);\n}\n\nfunction slugId(prefix: string, s: string): string {\n const base = s\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 96);\n return prefix + (base || \"x\");\n}\n\n/** Deterministic id for an entity, keyed on its name (case-insensitive). */\nexport function entityId(name: string): string {\n return slugId(\"e_\", name);\n}\n\nfunction resolveEntityRef(ref: string): string {\n return ref.startsWith(\"e_\") ? ref : entityId(ref);\n}\n\nfunction edgeId(srcId: string, rel: string, dstId: string): string {\n return slugId(\"x_\", `${srcId}|${rel}|${dstId}`);\n}\n\nconst MEMORY_COLS =\n \"id, kind, topic, body, tags, source, created_at AS createdAt, superseded_by AS supersededBy\";\n\n// ---------------------------------------------------------------------------\n// Memory builders\n// ---------------------------------------------------------------------------\n\nexport function buildRemember(\n input: RememberInput,\n now: number,\n id: string\n): SqlStatement {\n return {\n sql: `INSERT INTO ${T}memory (id, kind, topic, body, tags, source, created_at, superseded_by)\n VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`,\n params: [\n id,\n input.kind ?? \"fact\",\n input.topic ?? null,\n input.body,\n normalizeTags(input.tags),\n input.source ?? null,\n now,\n ],\n };\n}\n\nexport function buildSupersede(oldId: string, newId: string): SqlStatement {\n return {\n sql: `UPDATE ${T}memory SET superseded_by = ? WHERE id = ? AND superseded_by IS NULL`,\n params: [newId, oldId],\n };\n}\n\nexport function buildForget(id: string): SqlStatement {\n return { sql: `DELETE FROM ${T}memory WHERE id = ?`, params: [id] };\n}\n\nexport function buildGet(id: string): SqlStatement {\n return {\n sql: `SELECT ${MEMORY_COLS} FROM ${T}memory WHERE id = ?`,\n params: [id],\n };\n}\n\nexport function buildRecall(\n query: string,\n opts: RecallOptions = {}\n): SqlStatement | null {\n const match = toFtsQuery(query, { operator: opts.operator, mode: opts.mode });\n if (!match) return null;\n const where: string[] = [`${T}memory_fts MATCH ?`];\n const params: unknown[] = [match];\n if (!opts.includeSuperseded) where.push(\"m.superseded_by IS NULL\");\n if (opts.kind) {\n where.push(\"m.kind = ?\");\n params.push(opts.kind);\n }\n if (opts.sinceMs != null) {\n where.push(\"m.created_at >= ?\");\n params.push(opts.sinceMs);\n }\n params.push(opts.limit ?? 8);\n return {\n sql: `SELECT m.id, m.kind, m.topic, m.body, m.tags, m.source,\n m.created_at AS createdAt, m.superseded_by AS supersededBy\n FROM ${T}memory_fts\n JOIN ${T}memory m ON m.rowid = ${T}memory_fts.rowid\n WHERE ${where.join(\" AND \")}\n ORDER BY ${T}memory_fts.rank\n LIMIT ?`,\n params,\n };\n}\n\nexport function buildRecent(opts: ListOptions = {}): SqlStatement {\n const where: string[] = [];\n const params: unknown[] = [];\n if (!opts.includeSuperseded) where.push(\"superseded_by IS NULL\");\n if (opts.kind) {\n where.push(\"kind = ?\");\n params.push(opts.kind);\n }\n params.push(opts.limit ?? 20);\n return {\n sql: `SELECT ${MEMORY_COLS} FROM ${T}memory\n ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"}\n ORDER BY created_at DESC\n LIMIT ?`,\n params,\n };\n}\n\nexport function buildByTag(tag: string, opts: ListOptions = {}): SqlStatement {\n const t = tag.trim().toLowerCase();\n const where: string[] = [\"(' ' || COALESCE(tags, '') || ' ') LIKE ?\"];\n const params: unknown[] = [`% ${t} %`];\n if (!opts.includeSuperseded) where.push(\"superseded_by IS NULL\");\n if (opts.kind) {\n where.push(\"kind = ?\");\n params.push(opts.kind);\n }\n params.push(opts.limit ?? 20);\n return {\n sql: `SELECT ${MEMORY_COLS} FROM ${T}memory\n WHERE ${where.join(\" AND \")}\n ORDER BY created_at DESC\n LIMIT ?`,\n params,\n };\n}\n\nexport function buildStats(): SqlStatement {\n return {\n sql: `SELECT\n (SELECT count(*) FROM ${T}memory WHERE superseded_by IS NULL) AS facts,\n (SELECT count(*) FROM ${T}memory) AS facts_total,\n (SELECT count(*) FROM ${T}entity) AS entities,\n (SELECT count(*) FROM ${T}edge) AS edges`,\n params: [],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Graph builders\n// ---------------------------------------------------------------------------\n\nexport function buildUpsertEntity(e: EntityInput, now: number): SqlStatement {\n const id = e.id ?? entityId(e.name);\n return {\n sql: `INSERT INTO ${T}entity (id, name, kind, body, created_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n name = excluded.name,\n kind = COALESCE(excluded.kind, ${T}entity.kind),\n body = COALESCE(excluded.body, ${T}entity.body)`,\n params: [id, e.name, e.kind ?? null, e.body ?? null, now],\n };\n}\n\nexport function buildUpsertEdge(e: EdgeInput, now: number): SqlStatement {\n const srcId = resolveEntityRef(e.src);\n const dstId = resolveEntityRef(e.dst);\n const id = edgeId(srcId, e.rel, dstId);\n return {\n sql: `INSERT INTO ${T}edge (id, src, rel, dst, source, created_at)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n source = COALESCE(excluded.source, ${T}edge.source)`,\n params: [id, srcId, e.rel, dstId, e.source ?? null, now],\n };\n}\n\n/** Entities reachable from a seed within `depth` hops (seed excluded). */\nexport function buildNeighborEntities(\n seedId: string,\n depth: number,\n limit: number\n): SqlStatement {\n return {\n sql: `WITH RECURSIVE reach(id, depth) AS (\n SELECT ?, 0\n UNION\n SELECT CASE WHEN e.src = reach.id THEN e.dst ELSE e.src END,\n reach.depth + 1\n FROM reach\n JOIN ${T}edge e ON (e.src = reach.id OR e.dst = reach.id)\n WHERE reach.depth < ?\n )\n SELECT en.id, en.name, en.kind, en.body,\n en.created_at AS createdAt, MIN(reach.depth) AS depth\n FROM reach\n JOIN ${T}entity en ON en.id = reach.id\n WHERE reach.depth > 0 AND en.id <> ?\n GROUP BY en.id\n ORDER BY depth, en.name\n LIMIT ?`,\n // The seed reappears at depth >0 via a round-trip on undirected edges;\n // exclude it explicitly so it isn't listed as its own neighbor.\n params: [seedId, depth, seedId, limit],\n };\n}\n\nexport function buildEdgesAmong(ids: string[]): SqlStatement {\n if (!ids.length) {\n return { sql: `SELECT id, src, rel, dst, source, created_at AS createdAt FROM ${T}edge WHERE 0`, params: [] };\n }\n const ph = ids.map(() => \"?\").join(\", \");\n return {\n sql: `SELECT id, src, rel, dst, source, created_at AS createdAt\n FROM ${T}edge\n WHERE src IN (${ph}) AND dst IN (${ph})`,\n params: [...ids, ...ids],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Extraction / compaction contracts (used by the hosted worker + documented\n// for bring-your-own-LLM callers; kept here so the prompt is versioned with\n// the schema it has to populate).\n// ---------------------------------------------------------------------------\n\nexport const EXTRACTION_SYSTEM_PROMPT = `You extract durable, shareable context from raw text for a team of AI agents that will read it later by keyword.\n\nReturn JSON: { \"facts\": [...], \"entities\": [...], \"edges\": [...] }.\n- facts: atomic, self-contained statements worth remembering across sessions — decisions, conventions, constraints, identifiers, preferences. Each: { \"body\": string, \"topic\"?: string, \"tags\"?: string[] }. Prefer specific and stable over transient chatter.\n- entities: named things — services, people, tables, repos, concepts. Each: { \"name\": string, \"kind\"?: string, \"body\"?: short description }.\n- edges: relationships between entities, referencing entity names. Each: { \"src\": name, \"rel\": string, \"dst\": name }.\n\nBe conservative: omit anything ephemeral or low-value. Use lowercase tags. If nothing is worth keeping, return empty arrays.`;\n\nexport function buildExtractionMessages(\n raw: string,\n opts: { hint?: string } = {}\n): Array<{ role: \"system\" | \"user\"; content: string }> {\n return [\n { role: \"system\", content: EXTRACTION_SYSTEM_PROMPT },\n {\n role: \"user\",\n content: (opts.hint ? `Context: ${opts.hint}\\n\\n` : \"\") + raw,\n },\n ];\n}\n\nexport const EXTRACTION_JSON_SCHEMA = {\n type: \"object\",\n properties: {\n facts: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n body: { type: \"string\" },\n topic: { type: \"string\" },\n tags: { type: \"array\", items: { type: \"string\" } },\n },\n required: [\"body\"],\n },\n },\n entities: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n name: { type: \"string\" },\n kind: { type: \"string\" },\n body: { type: \"string\" },\n },\n required: [\"name\"],\n },\n },\n edges: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n src: { type: \"string\" },\n rel: { type: \"string\" },\n dst: { type: \"string\" },\n },\n required: [\"src\", \"rel\", \"dst\"],\n },\n },\n },\n} as const;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * @persql/context/core — zero-runtime-dependency engine.\n *\n * Schema DDL, SQL builders, and the extraction contract shared by the\n * `@persql/context` SDK (client-side, over `@persql/sdk`) and the hosted\n * PerSQL Context worker (server-side, over `@persql/ai`). Structured memories\n * with a name-keyed UPSERT + always-loaded index replace the old episode model.\n *\n * Retrieval is lexical: FTS5 with the porter stemmer, BM25-ranked. No vectors.\n */\n\nexport const SCHEMA_VERSION = 2;\nexport const TABLE_PREFIX = \"ctx_\";\nconst T = TABLE_PREFIX;\n\nexport type MemoryType = \"user\" | \"feedback\" | \"project\" | \"reference\";\n\nexport interface MemoryRow {\n id: string;\n name: string;\n description: string;\n type: MemoryType;\n body: string;\n source: string | null;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface IndexRow {\n id: string;\n name: string;\n description: string;\n type: MemoryType;\n updatedAt: number;\n}\n\nexport interface Entity {\n id: string;\n name: string;\n kind: string | null;\n body: string | null;\n createdAt: number;\n}\n\nexport interface Edge {\n id: string;\n src: string;\n rel: string;\n dst: string;\n source: string | null;\n createdAt: number;\n}\n\nexport interface RememberInput {\n name: string;\n description: string;\n type?: MemoryType;\n body: string;\n source?: string;\n}\n\nexport interface EntityInput {\n name: string;\n kind?: string;\n body?: string;\n id?: string;\n}\n\nexport interface EdgeInput {\n src: string;\n rel: string;\n dst: string;\n source?: string;\n}\n\nexport interface RecallOptions {\n limit?: number;\n operator?: \"or\" | \"and\";\n mode?: \"terms\" | \"raw\";\n}\n\nexport interface ListOptions {\n limit?: number;\n type?: MemoryType;\n}\n\n/** Structured memories extracted by an LLM from raw text. */\nexport interface ExtractedContext {\n memories?: Array<{\n name: string;\n description: string;\n type?: MemoryType;\n body: string;\n }>;\n entities?: EntityInput[];\n edges?: EdgeInput[];\n}\n\nexport interface SqlStatement {\n sql: string;\n params: unknown[];\n}\n\n// ---------------------------------------------------------------------------\n// Schema\n// ---------------------------------------------------------------------------\n\nexport const SCHEMA_SQL: string[] = [\n `CREATE TABLE IF NOT EXISTS ${T}memory (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n description TEXT NOT NULL,\n type TEXT NOT NULL DEFAULT 'project',\n body TEXT NOT NULL,\n source TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n )`,\n `CREATE UNIQUE INDEX IF NOT EXISTS ${T}memory_name ON ${T}memory(name)`,\n // External-content FTS on name+description+body. Porter stemmer so\n // \"invoice\" recalls \"invoicing\" without embeddings.\n `CREATE VIRTUAL TABLE IF NOT EXISTS ${T}memory_fts USING fts5(\n name, description, body,\n content='${T}memory', content_rowid='rowid',\n tokenize='porter unicode61'\n )`,\n `CREATE TRIGGER IF NOT EXISTS ${T}memory_ai AFTER INSERT ON ${T}memory BEGIN\n INSERT INTO ${T}memory_fts(rowid, name, description, body)\n VALUES (new.rowid, new.name, new.description, new.body);\n END`,\n `CREATE TRIGGER IF NOT EXISTS ${T}memory_ad AFTER DELETE ON ${T}memory BEGIN\n INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, name, description, body)\n VALUES ('delete', old.rowid, old.name, old.description, old.body);\n END`,\n `CREATE TRIGGER IF NOT EXISTS ${T}memory_au AFTER UPDATE ON ${T}memory BEGIN\n INSERT INTO ${T}memory_fts(${T}memory_fts, rowid, name, description, body)\n VALUES ('delete', old.rowid, old.name, old.description, old.body);\n INSERT INTO ${T}memory_fts(rowid, name, description, body)\n VALUES (new.rowid, new.name, new.description, new.body);\n END`,\n `CREATE TABLE IF NOT EXISTS ${T}entity (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n kind TEXT,\n body TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS ${T}edge (\n id TEXT PRIMARY KEY,\n src TEXT NOT NULL,\n rel TEXT NOT NULL,\n dst TEXT NOT NULL,\n source TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE INDEX IF NOT EXISTS ${T}edge_src ON ${T}edge(src)`,\n `CREATE INDEX IF NOT EXISTS ${T}edge_dst ON ${T}edge(dst)`,\n];\n\nexport function buildInit(): SqlStatement[] {\n return SCHEMA_SQL.map((sql) => ({ sql, params: [] }));\n}\n\n// ---------------------------------------------------------------------------\n// Legacy migration helpers\n// ---------------------------------------------------------------------------\n\n/** Returns a non-zero count when the old (topic-based) schema is present. */\nexport function buildDetectLegacySchema(): SqlStatement {\n return {\n sql: `SELECT count(*) AS n FROM pragma_table_info('${T}memory') WHERE name='topic'`,\n params: [],\n };\n}\n\n/**\n * Drop all ctx_ tables + triggers so buildInit() can recreate them cleanly.\n * Run in order — FTS must go first (it references the content table).\n */\nexport function buildDropLegacy(): SqlStatement[] {\n return [\n { sql: `DROP TABLE IF EXISTS ${T}memory_fts`, params: [] },\n { sql: `DROP TABLE IF EXISTS ${T}memory`, params: [] },\n { sql: `DROP TABLE IF EXISTS ${T}entity`, params: [] },\n { sql: `DROP TABLE IF EXISTS ${T}edge`, params: [] },\n ];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst FTS_OPERATORS = new Set([\"and\", \"or\", \"not\", \"near\"]);\n\n/**\n * Turn arbitrary user text into a safe FTS5 MATCH expression. `terms` mode\n * (default) extracts word tokens, drops bare boolean operators, and ORs the\n * rest as quoted terms. `raw` mode passes a hand-written FTS expression through.\n */\nexport function toFtsQuery(\n input: string,\n opts: { operator?: \"or\" | \"and\"; mode?: \"terms\" | \"raw\" } = {}\n): string | null {\n if (opts.mode === \"raw\") return input.trim() || null;\n const tokens = (input.match(/[\\p{L}\\p{N}_]+/gu) ?? []).filter(\n (t) => !FTS_OPERATORS.has(t.toLowerCase())\n );\n if (!tokens.length) return null;\n const joiner = opts.operator === \"and\" ? \" AND \" : \" OR \";\n return tokens.map((t) => `\"${t}\"`).join(joiner);\n}\n\nfunction slugId(prefix: string, s: string): string {\n const base = s\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 96);\n return prefix + (base || \"x\");\n}\n\nexport function entityId(name: string): string {\n return slugId(\"e_\", name);\n}\n\nfunction resolveEntityRef(ref: string): string {\n return ref.startsWith(\"e_\") ? ref : entityId(ref);\n}\n\nfunction edgeId(srcId: string, rel: string, dstId: string): string {\n return slugId(\"x_\", `${srcId}|${rel}|${dstId}`);\n}\n\nconst MEMORY_COLS =\n \"id, name, description, type, body, source, created_at AS createdAt, updated_at AS updatedAt\";\n\n// ---------------------------------------------------------------------------\n// Memory builders\n// ---------------------------------------------------------------------------\n\n/** UPSERT by name — same name overwrites description/body/type/source. */\nexport function buildRemember(\n input: RememberInput,\n now: number,\n id: string\n): SqlStatement {\n return {\n sql: `INSERT INTO ${T}memory (id, name, description, type, body, source, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n description = excluded.description,\n body = excluded.body,\n type = excluded.type,\n source = excluded.source,\n updated_at = excluded.updated_at`,\n params: [\n id,\n input.name,\n input.description,\n input.type ?? \"project\",\n input.body,\n input.source ?? null,\n now,\n now,\n ],\n };\n}\n\n/** Delete a memory by its name slug. */\nexport function buildForget(name: string): SqlStatement {\n return { sql: `DELETE FROM ${T}memory WHERE name = ?`, params: [name] };\n}\n\n/** Fetch one memory row by name, or null if absent. */\nexport function buildGet(name: string): SqlStatement {\n return {\n sql: `SELECT ${MEMORY_COLS} FROM ${T}memory WHERE name = ?`,\n params: [name],\n };\n}\n\n/** Always-loaded index: name+description+type, newest first. */\nexport function buildIndex(opts: ListOptions = {}): SqlStatement {\n const where: string[] = [];\n const params: unknown[] = [];\n if (opts.type) {\n where.push(\"type = ?\");\n params.push(opts.type);\n }\n params.push(opts.limit ?? 50);\n return {\n sql: `SELECT id, name, description, type, updated_at AS updatedAt FROM ${T}memory\n ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"}\n ORDER BY updated_at DESC LIMIT ?`,\n params,\n };\n}\n\n/** BM25 recall across name+description+body. */\nexport function buildRecall(\n query: string,\n opts: RecallOptions = {}\n): SqlStatement | null {\n const match = toFtsQuery(query, { operator: opts.operator, mode: opts.mode });\n if (!match) return null;\n return {\n sql: `SELECT m.id, m.name, m.description, m.type, m.body, m.source,\n m.created_at AS createdAt, m.updated_at AS updatedAt\n FROM ${T}memory_fts\n JOIN ${T}memory m ON m.rowid = ${T}memory_fts.rowid\n WHERE ${T}memory_fts MATCH ?\n ORDER BY ${T}memory_fts.rank\n LIMIT ?`,\n params: [match, opts.limit ?? 8],\n };\n}\n\nexport function buildStats(): SqlStatement {\n return {\n sql: `SELECT\n (SELECT count(*) FROM ${T}memory) AS memories,\n (SELECT count(*) FROM ${T}entity) AS entities,\n (SELECT count(*) FROM ${T}edge) AS edges`,\n params: [],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Graph builders (unchanged from v1)\n// ---------------------------------------------------------------------------\n\nexport function buildUpsertEntity(e: EntityInput, now: number): SqlStatement {\n const id = e.id ?? entityId(e.name);\n return {\n sql: `INSERT INTO ${T}entity (id, name, kind, body, created_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n name = excluded.name,\n kind = COALESCE(excluded.kind, ${T}entity.kind),\n body = COALESCE(excluded.body, ${T}entity.body)`,\n params: [id, e.name, e.kind ?? null, e.body ?? null, now],\n };\n}\n\nexport function buildUpsertEdge(e: EdgeInput, now: number): SqlStatement {\n const srcId = resolveEntityRef(e.src);\n const dstId = resolveEntityRef(e.dst);\n const id = edgeId(srcId, e.rel, dstId);\n return {\n sql: `INSERT INTO ${T}edge (id, src, rel, dst, source, created_at)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n source = COALESCE(excluded.source, ${T}edge.source)`,\n params: [id, srcId, e.rel, dstId, e.source ?? null, now],\n };\n}\n\nexport function buildNeighborEntities(\n seedId: string,\n depth: number,\n limit: number\n): SqlStatement {\n return {\n sql: `WITH RECURSIVE reach(id, depth) AS (\n SELECT ?, 0\n UNION\n SELECT CASE WHEN e.src = reach.id THEN e.dst ELSE e.src END,\n reach.depth + 1\n FROM reach\n JOIN ${T}edge e ON (e.src = reach.id OR e.dst = reach.id)\n WHERE reach.depth < ?\n )\n SELECT en.id, en.name, en.kind, en.body,\n en.created_at AS createdAt, MIN(reach.depth) AS depth\n FROM reach\n JOIN ${T}entity en ON en.id = reach.id\n WHERE reach.depth > 0 AND en.id <> ?\n GROUP BY en.id\n ORDER BY depth, en.name\n LIMIT ?`,\n // The seed appears at depth >0 on undirected round-trips — exclude it.\n params: [seedId, depth, seedId, limit],\n };\n}\n\nexport function buildEdgesAmong(ids: string[]): SqlStatement {\n if (!ids.length) {\n return { sql: `SELECT id, src, rel, dst, source, created_at AS createdAt FROM ${T}edge WHERE 0`, params: [] };\n }\n const ph = ids.map(() => \"?\").join(\", \");\n return {\n sql: `SELECT id, src, rel, dst, source, created_at AS createdAt\n FROM ${T}edge\n WHERE src IN (${ph}) AND dst IN (${ph})`,\n params: [...ids, ...ids],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Extraction contract (versioned here so client and server stay in sync)\n// ---------------------------------------------------------------------------\n\nexport const EXTRACTION_SYSTEM_PROMPT = `You extract durable, structured memories from a conversation or document for an AI agent that will recall them across sessions.\n\nReturn JSON: { \"memories\": [...], \"entities\": [...], \"edges\": [...] }.\n- memories: facts worth remembering across sessions — decisions, conventions, constraints, identifiers, preferences, schema details. Each:\n {\n \"name\": string (kebab-case slug, unique key — e.g. \"auth-provider-choice\", \"todos-table-schema\", \"user-prefers-metrics\"),\n \"description\": string (one-liner shown in the always-loaded index),\n \"type\": \"user\"|\"feedback\"|\"project\"|\"reference\",\n \"body\": string (full markdown content with all relevant detail)\n }\n Types: user=who they are/preferences; feedback=guidance for agents; project=ongoing work/decisions; reference=where to find things.\n- entities: named things — services, people, tables, repos. Each: { \"name\": string, \"kind\"?: string, \"body\"?: string }\n- edges: relationships between entities. Each: { \"src\": name, \"rel\": string, \"dst\": name }\n\nBe conservative. Use unique, meaningful names. If nothing durable is found, return empty arrays.`;\n\nexport function buildExtractionMessages(\n raw: string,\n opts: { hint?: string } = {}\n): Array<{ role: \"system\" | \"user\"; content: string }> {\n return [\n { role: \"system\", content: EXTRACTION_SYSTEM_PROMPT },\n {\n role: \"user\",\n content: (opts.hint ? `Context: ${opts.hint}\\n\\n` : \"\") + raw,\n },\n ];\n}\n\nexport const EXTRACTION_JSON_SCHEMA = {\n type: \"object\",\n properties: {\n memories: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n name: { type: \"string\" },\n description: { type: \"string\" },\n type: { type: \"string\", enum: [\"user\", \"feedback\", \"project\", \"reference\"] },\n body: { type: \"string\" },\n },\n required: [\"name\", \"description\", \"body\"],\n },\n },\n entities: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n name: { type: \"string\" },\n kind: { type: \"string\" },\n body: { type: \"string\" },\n },\n required: [\"name\"],\n },\n },\n edges: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n src: { type: \"string\" },\n rel: { type: \"string\" },\n dst: { type: \"string\" },\n },\n required: [\"src\", \"rel\", \"dst\"],\n },\n },\n },\n} as const;\n"],"mappings":"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","names":[]}
|
package/dist/core.d.cts
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @persql/context/core — zero-runtime-dependency engine.
|
|
3
3
|
*
|
|
4
|
-
* Schema DDL, SQL builders, and the extraction
|
|
5
|
-
*
|
|
6
|
-
* PerSQL Context worker (server-side, over `@persql/ai`).
|
|
7
|
-
*
|
|
4
|
+
* Schema DDL, SQL builders, and the extraction contract shared by the
|
|
5
|
+
* `@persql/context` SDK (client-side, over `@persql/sdk`) and the hosted
|
|
6
|
+
* PerSQL Context worker (server-side, over `@persql/ai`). Structured memories
|
|
7
|
+
* with a name-keyed UPSERT + always-loaded index replace the old episode model.
|
|
8
8
|
*
|
|
9
9
|
* Retrieval is lexical: FTS5 with the porter stemmer, BM25-ranked. No vectors.
|
|
10
10
|
*/
|
|
11
|
-
declare const SCHEMA_VERSION =
|
|
11
|
+
declare const SCHEMA_VERSION = 2;
|
|
12
12
|
declare const TABLE_PREFIX = "ctx_";
|
|
13
|
-
type
|
|
13
|
+
type MemoryType = "user" | "feedback" | "project" | "reference";
|
|
14
14
|
interface MemoryRow {
|
|
15
15
|
id: string;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
type: MemoryType;
|
|
18
19
|
body: string;
|
|
19
|
-
tags: string | null;
|
|
20
20
|
source: string | null;
|
|
21
21
|
createdAt: number;
|
|
22
|
-
|
|
22
|
+
updatedAt: number;
|
|
23
|
+
}
|
|
24
|
+
interface IndexRow {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
type: MemoryType;
|
|
29
|
+
updatedAt: number;
|
|
23
30
|
}
|
|
24
31
|
interface Entity {
|
|
25
32
|
id: string;
|
|
@@ -37,13 +44,11 @@ interface Edge {
|
|
|
37
44
|
createdAt: number;
|
|
38
45
|
}
|
|
39
46
|
interface RememberInput {
|
|
47
|
+
name: string;
|
|
48
|
+
description: string;
|
|
49
|
+
type?: MemoryType;
|
|
40
50
|
body: string;
|
|
41
|
-
topic?: string;
|
|
42
|
-
kind?: MemoryKind;
|
|
43
|
-
tags?: string[] | string;
|
|
44
51
|
source?: string;
|
|
45
|
-
supersedes?: string | string[];
|
|
46
|
-
id?: string;
|
|
47
52
|
}
|
|
48
53
|
interface EntityInput {
|
|
49
54
|
name: string;
|
|
@@ -59,25 +64,20 @@ interface EdgeInput {
|
|
|
59
64
|
}
|
|
60
65
|
interface RecallOptions {
|
|
61
66
|
limit?: number;
|
|
62
|
-
kind?: MemoryKind;
|
|
63
67
|
operator?: "or" | "and";
|
|
64
68
|
mode?: "terms" | "raw";
|
|
65
|
-
sinceMs?: number;
|
|
66
|
-
withinDays?: number;
|
|
67
|
-
includeSuperseded?: boolean;
|
|
68
69
|
}
|
|
69
70
|
interface ListOptions {
|
|
70
71
|
limit?: number;
|
|
71
|
-
|
|
72
|
-
includeSuperseded?: boolean;
|
|
72
|
+
type?: MemoryType;
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
74
|
+
/** Structured memories extracted by an LLM from raw text. */
|
|
75
75
|
interface ExtractedContext {
|
|
76
|
-
|
|
76
|
+
memories?: Array<{
|
|
77
|
+
name: string;
|
|
78
|
+
description: string;
|
|
79
|
+
type?: MemoryType;
|
|
77
80
|
body: string;
|
|
78
|
-
topic?: string;
|
|
79
|
-
tags?: string[];
|
|
80
|
-
kind?: MemoryKind;
|
|
81
81
|
}>;
|
|
82
82
|
entities?: EntityInput[];
|
|
83
83
|
edges?: EdgeInput[];
|
|
@@ -88,34 +88,39 @@ interface SqlStatement {
|
|
|
88
88
|
}
|
|
89
89
|
declare const SCHEMA_SQL: string[];
|
|
90
90
|
declare function buildInit(): SqlStatement[];
|
|
91
|
-
|
|
91
|
+
/** Returns a non-zero count when the old (topic-based) schema is present. */
|
|
92
|
+
declare function buildDetectLegacySchema(): SqlStatement;
|
|
93
|
+
/**
|
|
94
|
+
* Drop all ctx_ tables + triggers so buildInit() can recreate them cleanly.
|
|
95
|
+
* Run in order — FTS must go first (it references the content table).
|
|
96
|
+
*/
|
|
97
|
+
declare function buildDropLegacy(): SqlStatement[];
|
|
92
98
|
/**
|
|
93
99
|
* Turn arbitrary user text into a safe FTS5 MATCH expression. `terms` mode
|
|
94
100
|
* (default) extracts word tokens, drops bare boolean operators, and ORs the
|
|
95
|
-
* rest as quoted terms
|
|
96
|
-
* "invoice billing" and neither crashes the parser nor injects FTS syntax.
|
|
97
|
-
* `raw` mode passes a hand-written FTS expression through untouched.
|
|
101
|
+
* rest as quoted terms. `raw` mode passes a hand-written FTS expression through.
|
|
98
102
|
*/
|
|
99
103
|
declare function toFtsQuery(input: string, opts?: {
|
|
100
104
|
operator?: "or" | "and";
|
|
101
105
|
mode?: "terms" | "raw";
|
|
102
106
|
}): string | null;
|
|
103
|
-
/** Deterministic id for an entity, keyed on its name (case-insensitive). */
|
|
104
107
|
declare function entityId(name: string): string;
|
|
108
|
+
/** UPSERT by name — same name overwrites description/body/type/source. */
|
|
105
109
|
declare function buildRemember(input: RememberInput, now: number, id: string): SqlStatement;
|
|
106
|
-
|
|
107
|
-
declare function buildForget(
|
|
108
|
-
|
|
110
|
+
/** Delete a memory by its name slug. */
|
|
111
|
+
declare function buildForget(name: string): SqlStatement;
|
|
112
|
+
/** Fetch one memory row by name, or null if absent. */
|
|
113
|
+
declare function buildGet(name: string): SqlStatement;
|
|
114
|
+
/** Always-loaded index: name+description+type, newest first. */
|
|
115
|
+
declare function buildIndex(opts?: ListOptions): SqlStatement;
|
|
116
|
+
/** BM25 recall across name+description+body. */
|
|
109
117
|
declare function buildRecall(query: string, opts?: RecallOptions): SqlStatement | null;
|
|
110
|
-
declare function buildRecent(opts?: ListOptions): SqlStatement;
|
|
111
|
-
declare function buildByTag(tag: string, opts?: ListOptions): SqlStatement;
|
|
112
118
|
declare function buildStats(): SqlStatement;
|
|
113
119
|
declare function buildUpsertEntity(e: EntityInput, now: number): SqlStatement;
|
|
114
120
|
declare function buildUpsertEdge(e: EdgeInput, now: number): SqlStatement;
|
|
115
|
-
/** Entities reachable from a seed within `depth` hops (seed excluded). */
|
|
116
121
|
declare function buildNeighborEntities(seedId: string, depth: number, limit: number): SqlStatement;
|
|
117
122
|
declare function buildEdgesAmong(ids: string[]): SqlStatement;
|
|
118
|
-
declare const EXTRACTION_SYSTEM_PROMPT = "You extract durable,
|
|
123
|
+
declare const EXTRACTION_SYSTEM_PROMPT = "You extract durable, structured memories from a conversation or document for an AI agent that will recall them across sessions.\n\nReturn JSON: { \"memories\": [...], \"entities\": [...], \"edges\": [...] }.\n- memories: facts worth remembering across sessions \u2014 decisions, conventions, constraints, identifiers, preferences, schema details. Each:\n {\n \"name\": string (kebab-case slug, unique key \u2014 e.g. \"auth-provider-choice\", \"todos-table-schema\", \"user-prefers-metrics\"),\n \"description\": string (one-liner shown in the always-loaded index),\n \"type\": \"user\"|\"feedback\"|\"project\"|\"reference\",\n \"body\": string (full markdown content with all relevant detail)\n }\n Types: user=who they are/preferences; feedback=guidance for agents; project=ongoing work/decisions; reference=where to find things.\n- entities: named things \u2014 services, people, tables, repos. Each: { \"name\": string, \"kind\"?: string, \"body\"?: string }\n- edges: relationships between entities. Each: { \"src\": name, \"rel\": string, \"dst\": name }\n\nBe conservative. Use unique, meaningful names. If nothing durable is found, return empty arrays.";
|
|
119
124
|
declare function buildExtractionMessages(raw: string, opts?: {
|
|
120
125
|
hint?: string;
|
|
121
126
|
}): Array<{
|
|
@@ -125,25 +130,26 @@ declare function buildExtractionMessages(raw: string, opts?: {
|
|
|
125
130
|
declare const EXTRACTION_JSON_SCHEMA: {
|
|
126
131
|
readonly type: "object";
|
|
127
132
|
readonly properties: {
|
|
128
|
-
readonly
|
|
133
|
+
readonly memories: {
|
|
129
134
|
readonly type: "array";
|
|
130
135
|
readonly items: {
|
|
131
136
|
readonly type: "object";
|
|
132
137
|
readonly properties: {
|
|
133
|
-
readonly
|
|
138
|
+
readonly name: {
|
|
134
139
|
readonly type: "string";
|
|
135
140
|
};
|
|
136
|
-
readonly
|
|
141
|
+
readonly description: {
|
|
137
142
|
readonly type: "string";
|
|
138
143
|
};
|
|
139
|
-
readonly
|
|
140
|
-
readonly type: "
|
|
141
|
-
readonly
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
readonly type: {
|
|
145
|
+
readonly type: "string";
|
|
146
|
+
readonly enum: readonly ["user", "feedback", "project", "reference"];
|
|
147
|
+
};
|
|
148
|
+
readonly body: {
|
|
149
|
+
readonly type: "string";
|
|
144
150
|
};
|
|
145
151
|
};
|
|
146
|
-
readonly required: readonly ["body"];
|
|
152
|
+
readonly required: readonly ["name", "description", "body"];
|
|
147
153
|
};
|
|
148
154
|
};
|
|
149
155
|
readonly entities: {
|
|
@@ -185,4 +191,4 @@ declare const EXTRACTION_JSON_SCHEMA: {
|
|
|
185
191
|
};
|
|
186
192
|
};
|
|
187
193
|
|
|
188
|
-
export { EXTRACTION_JSON_SCHEMA, EXTRACTION_SYSTEM_PROMPT, type Edge, type EdgeInput, type Entity, type EntityInput, type ExtractedContext, type
|
|
194
|
+
export { EXTRACTION_JSON_SCHEMA, EXTRACTION_SYSTEM_PROMPT, type Edge, type EdgeInput, type Entity, type EntityInput, type ExtractedContext, type IndexRow, type ListOptions, type MemoryRow, type MemoryType, type RecallOptions, type RememberInput, SCHEMA_SQL, SCHEMA_VERSION, type SqlStatement, TABLE_PREFIX, buildDetectLegacySchema, buildDropLegacy, buildEdgesAmong, buildExtractionMessages, buildForget, buildGet, buildIndex, buildInit, buildNeighborEntities, buildRecall, buildRemember, buildStats, buildUpsertEdge, buildUpsertEntity, entityId, toFtsQuery };
|