@launchapp-dev/ao-memory-mcp 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db.d.ts +25 -0
- package/dist/db.js +90 -0
- package/dist/embeddings.d.ts +14 -0
- package/dist/embeddings.js +72 -0
- package/dist/schema.sql +194 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +56 -0
- package/dist/tools/context.d.ts +29 -0
- package/dist/tools/context.js +88 -0
- package/dist/tools/documents.d.ts +142 -0
- package/dist/tools/documents.js +201 -0
- package/dist/tools/episodes.d.ts +112 -0
- package/dist/tools/episodes.js +98 -0
- package/dist/tools/knowledge.d.ts +177 -0
- package/dist/tools/knowledge.js +235 -0
- package/dist/tools/recall.d.ts +153 -0
- package/dist/tools/recall.js +180 -0
- package/dist/tools/stats.d.ts +24 -0
- package/dist/tools/stats.js +50 -0
- package/dist/tools/store.d.ts +180 -0
- package/dist/tools/store.js +176 -0
- package/dist/tools/summarize.d.ts +74 -0
- package/dist/tools/summarize.js +92 -0
- package/package.json +9 -5
- package/migrate.ts +0 -250
- package/src/db.ts +0 -106
- package/src/embeddings.ts +0 -97
- package/src/server.ts +0 -70
- package/src/tools/context.ts +0 -106
- package/src/tools/documents.ts +0 -215
- package/src/tools/episodes.ts +0 -112
- package/src/tools/knowledge.ts +0 -248
- package/src/tools/recall.ts +0 -167
- package/src/tools/stats.ts +0 -51
- package/src/tools/store.ts +0 -168
- package/src/tools/summarize.ts +0 -114
- package/tsconfig.json +0 -12
package/src/tools/context.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import type Database from "better-sqlite3";
|
|
2
|
-
import { jsonResult } from "../db.ts";
|
|
3
|
-
|
|
4
|
-
export const contextTools = [
|
|
5
|
-
{
|
|
6
|
-
name: "memory.context",
|
|
7
|
-
description:
|
|
8
|
-
"Agent boot tool — call at the start of each run to load all relevant memory. Returns recent memories, active decisions, related entities, episode summaries, and document count. Scoped by namespace and agent role.",
|
|
9
|
-
inputSchema: {
|
|
10
|
-
type: "object" as const,
|
|
11
|
-
properties: {
|
|
12
|
-
namespace: { type: "string", description: "Project/scope to load context for" },
|
|
13
|
-
agent_role: { type: "string", description: "Agent role requesting context" },
|
|
14
|
-
limit: { type: "number", description: "Max entries per section (default 10)" },
|
|
15
|
-
},
|
|
16
|
-
required: ["namespace"],
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
export function handleContext(db: Database.Database, name: string, args: any) {
|
|
22
|
-
if (name === "memory.context") return memoryContext(db, args);
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function memoryContext(db: Database.Database, args: any) {
|
|
27
|
-
const { namespace, agent_role } = args;
|
|
28
|
-
const limit = args.limit || 10;
|
|
29
|
-
|
|
30
|
-
// Recent memories for this agent+namespace
|
|
31
|
-
const recentMemories = db.prepare(`
|
|
32
|
-
SELECT * FROM memories
|
|
33
|
-
WHERE namespace = ? ${agent_role ? "AND agent_role = ?" : ""}
|
|
34
|
-
AND status = 'active'
|
|
35
|
-
ORDER BY occurred_at DESC LIMIT ?
|
|
36
|
-
`).all(...(agent_role ? [namespace, agent_role, limit] : [namespace, limit]));
|
|
37
|
-
|
|
38
|
-
// Active semantic memories (facts/knowledge) for this namespace
|
|
39
|
-
const knowledge = db.prepare(`
|
|
40
|
-
SELECT * FROM memories
|
|
41
|
-
WHERE namespace = ? AND memory_type = 'semantic' AND status = 'active'
|
|
42
|
-
ORDER BY confidence DESC, access_count DESC LIMIT ?
|
|
43
|
-
`).all(namespace, limit);
|
|
44
|
-
|
|
45
|
-
// Active procedural memories (how-to) for this namespace
|
|
46
|
-
const procedures = db.prepare(`
|
|
47
|
-
SELECT * FROM memories
|
|
48
|
-
WHERE namespace = ? AND memory_type = 'procedural' AND status = 'active'
|
|
49
|
-
ORDER BY access_count DESC LIMIT ?
|
|
50
|
-
`).all(namespace, limit);
|
|
51
|
-
|
|
52
|
-
// Related entities
|
|
53
|
-
const entities = db.prepare(`
|
|
54
|
-
SELECT e.*, (SELECT COUNT(*) FROM relations r WHERE r.source_entity_id = e.id OR r.target_entity_id = e.id) as relation_count
|
|
55
|
-
FROM entities e
|
|
56
|
-
WHERE e.namespace = ?
|
|
57
|
-
ORDER BY relation_count DESC LIMIT ?
|
|
58
|
-
`).all(namespace, limit);
|
|
59
|
-
|
|
60
|
-
// Recent episode summaries
|
|
61
|
-
const episodeSummaries = db.prepare(`
|
|
62
|
-
SELECT DISTINCT session_id, summary, MAX(created_at) as last_at
|
|
63
|
-
FROM episodes
|
|
64
|
-
WHERE namespace = ? AND summary IS NOT NULL
|
|
65
|
-
GROUP BY session_id
|
|
66
|
-
ORDER BY last_at DESC LIMIT 5
|
|
67
|
-
`).all(namespace);
|
|
68
|
-
|
|
69
|
-
// Document count
|
|
70
|
-
const docCount = (db.prepare(
|
|
71
|
-
"SELECT COUNT(*) as count FROM documents WHERE namespace = ?"
|
|
72
|
-
).get(namespace) as any).count;
|
|
73
|
-
|
|
74
|
-
// Global memories (cross-project)
|
|
75
|
-
const globalMemories = db.prepare(`
|
|
76
|
-
SELECT * FROM memories
|
|
77
|
-
WHERE scope = 'global' AND status = 'active'
|
|
78
|
-
ORDER BY confidence DESC, occurred_at DESC LIMIT 5
|
|
79
|
-
`).all();
|
|
80
|
-
|
|
81
|
-
// Check if summarization needed
|
|
82
|
-
const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString();
|
|
83
|
-
const staleCount = (db.prepare(`
|
|
84
|
-
SELECT COUNT(*) as count FROM memories
|
|
85
|
-
WHERE namespace = ? ${agent_role ? "AND agent_role = ?" : ""}
|
|
86
|
-
AND status = 'active' AND occurred_at < ?
|
|
87
|
-
`).get(...(agent_role ? [namespace, agent_role, threeDaysAgo] : [namespace, threeDaysAgo])) as any).count;
|
|
88
|
-
|
|
89
|
-
// Stats
|
|
90
|
-
const totalMemories = (db.prepare(
|
|
91
|
-
"SELECT COUNT(*) as count FROM memories WHERE namespace = ? AND status = 'active'"
|
|
92
|
-
).get(namespace) as any).count;
|
|
93
|
-
|
|
94
|
-
return jsonResult({
|
|
95
|
-
recent_memories: recentMemories,
|
|
96
|
-
knowledge,
|
|
97
|
-
procedures,
|
|
98
|
-
entities,
|
|
99
|
-
episode_summaries: episodeSummaries,
|
|
100
|
-
global_memories: globalMemories,
|
|
101
|
-
document_count: docCount,
|
|
102
|
-
total_active_memories: totalMemories,
|
|
103
|
-
summarization_needed: staleCount >= 20,
|
|
104
|
-
stale_entry_count: staleCount,
|
|
105
|
-
});
|
|
106
|
-
}
|
package/src/tools/documents.ts
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import type Database from "better-sqlite3";
|
|
2
|
-
import { now, jsonResult, errorResult, chunkText } from "../db.ts";
|
|
3
|
-
import { embed, storeVector, deleteVector, hybridSearch } from "../embeddings.ts";
|
|
4
|
-
|
|
5
|
-
export const documentTools = [
|
|
6
|
-
{
|
|
7
|
-
name: "memory.doc.ingest",
|
|
8
|
-
description:
|
|
9
|
-
"Ingest a document into memory. Automatically chunks and embeds for semantic retrieval. Great for architecture docs, specs, READMEs, code files, or any reference material.",
|
|
10
|
-
inputSchema: {
|
|
11
|
-
type: "object" as const,
|
|
12
|
-
properties: {
|
|
13
|
-
title: { type: "string", description: "Document title" },
|
|
14
|
-
content: { type: "string", description: "Full document content" },
|
|
15
|
-
source: { type: "string", description: "File path, URL, or identifier" },
|
|
16
|
-
namespace: { type: "string", description: "Project/scope for this document" },
|
|
17
|
-
mime_type: { type: "string", description: "Content type (default: text/plain)" },
|
|
18
|
-
chunk_size: { type: "number", description: "Max chars per chunk (default: 1000)" },
|
|
19
|
-
chunk_overlap: { type: "number", description: "Overlap chars between chunks (default: 100)" },
|
|
20
|
-
metadata: { type: "object", description: "Custom metadata" },
|
|
21
|
-
},
|
|
22
|
-
required: ["title", "content"],
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
name: "memory.doc.search",
|
|
27
|
-
description:
|
|
28
|
-
"Search across ingested documents using hybrid semantic + keyword search. Returns relevant chunks with document context.",
|
|
29
|
-
inputSchema: {
|
|
30
|
-
type: "object" as const,
|
|
31
|
-
properties: {
|
|
32
|
-
query: { type: "string", description: "Search query" },
|
|
33
|
-
namespace: { type: "string", description: "Restrict to namespace" },
|
|
34
|
-
limit: { type: "number", description: "Max chunks to return (default 5)" },
|
|
35
|
-
alpha: { type: "number", description: "Semantic vs keyword weight (default 0.6)" },
|
|
36
|
-
},
|
|
37
|
-
required: ["query"],
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: "memory.doc.list",
|
|
42
|
-
description: "List ingested documents.",
|
|
43
|
-
inputSchema: {
|
|
44
|
-
type: "object" as const,
|
|
45
|
-
properties: {
|
|
46
|
-
namespace: { type: "string", description: "Filter by namespace" },
|
|
47
|
-
limit: { type: "number", description: "Max results (default 50)" },
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: "memory.doc.get",
|
|
53
|
-
description: "Get a full document by ID, including all its chunks.",
|
|
54
|
-
inputSchema: {
|
|
55
|
-
type: "object" as const,
|
|
56
|
-
properties: {
|
|
57
|
-
id: { type: "number", description: "Document ID" },
|
|
58
|
-
},
|
|
59
|
-
required: ["id"],
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: "memory.doc.delete",
|
|
64
|
-
description: "Delete a document and all its chunks.",
|
|
65
|
-
inputSchema: {
|
|
66
|
-
type: "object" as const,
|
|
67
|
-
properties: {
|
|
68
|
-
id: { type: "number", description: "Document ID" },
|
|
69
|
-
},
|
|
70
|
-
required: ["id"],
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
export function handleDocuments(db: Database.Database, name: string, args: any) {
|
|
76
|
-
if (name === "memory.doc.ingest") return docIngest(db, args);
|
|
77
|
-
if (name === "memory.doc.search") return docSearch(db, args);
|
|
78
|
-
if (name === "memory.doc.list") return docList(db, args);
|
|
79
|
-
if (name === "memory.doc.get") return docGet(db, args);
|
|
80
|
-
if (name === "memory.doc.delete") return docDelete(db, args);
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function docIngest(db: Database.Database, args: any) {
|
|
85
|
-
const ts = now();
|
|
86
|
-
const chunkSize = args.chunk_size || 1000;
|
|
87
|
-
const chunkOverlap = args.chunk_overlap || 100;
|
|
88
|
-
|
|
89
|
-
// Check for existing doc with same source
|
|
90
|
-
if (args.source) {
|
|
91
|
-
const existing = db.prepare("SELECT id FROM documents WHERE source = ? AND namespace IS ?").get(args.source, args.namespace || null) as any;
|
|
92
|
-
if (existing) {
|
|
93
|
-
// Re-ingest: delete old chunks
|
|
94
|
-
const oldChunks = db.prepare("SELECT id FROM chunks WHERE document_id = ?").all(existing.id) as any[];
|
|
95
|
-
for (const c of oldChunks) deleteVector(db, "vec_chunks", c.id);
|
|
96
|
-
db.prepare("DELETE FROM chunks WHERE document_id = ?").run(existing.id);
|
|
97
|
-
db.prepare("DELETE FROM documents WHERE id = ?").run(existing.id);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const docResult = db.prepare(`
|
|
102
|
-
INSERT INTO documents (namespace, title, source, mime_type, content, metadata, created_at, updated_at)
|
|
103
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
104
|
-
`).run(
|
|
105
|
-
args.namespace || null, args.title, args.source || null,
|
|
106
|
-
args.mime_type || "text/plain", args.content,
|
|
107
|
-
JSON.stringify(args.metadata || {}), ts, ts
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const docId = Number(docResult.lastInsertRowid);
|
|
111
|
-
const textChunks = chunkText(args.content, chunkSize, chunkOverlap);
|
|
112
|
-
|
|
113
|
-
const insertChunk = db.prepare(`
|
|
114
|
-
INSERT INTO chunks (document_id, chunk_index, content, char_offset, char_length, metadata, created_at)
|
|
115
|
-
VALUES (?, ?, ?, ?, ?, '{}', ?)
|
|
116
|
-
`);
|
|
117
|
-
|
|
118
|
-
const chunkIds: number[] = [];
|
|
119
|
-
for (let i = 0; i < textChunks.length; i++) {
|
|
120
|
-
const c = textChunks[i];
|
|
121
|
-
const result = insertChunk.run(docId, i, c.content, c.offset, c.content.length, ts);
|
|
122
|
-
chunkIds.push(Number(result.lastInsertRowid));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Embed all chunks
|
|
126
|
-
let embedded = 0;
|
|
127
|
-
for (let i = 0; i < textChunks.length; i++) {
|
|
128
|
-
try {
|
|
129
|
-
const embedding = await embed(textChunks[i].content);
|
|
130
|
-
storeVector(db, "vec_chunks", chunkIds[i], embedding);
|
|
131
|
-
embedded++;
|
|
132
|
-
} catch (e) {
|
|
133
|
-
console.error(`[ao-memory] Chunk embed failed:`, e);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return jsonResult({ document_id: docId, chunks: textChunks.length, embedded });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function docSearch(db: Database.Database, args: any) {
|
|
141
|
-
const limit = args.limit || 5;
|
|
142
|
-
const alpha = args.alpha ?? 0.6;
|
|
143
|
-
|
|
144
|
-
let queryEmbedding: Float32Array;
|
|
145
|
-
try {
|
|
146
|
-
queryEmbedding = await embed(args.query, true);
|
|
147
|
-
} catch {
|
|
148
|
-
// FTS-only fallback
|
|
149
|
-
const rows = db.prepare(`
|
|
150
|
-
SELECT c.*, d.title as doc_title, d.source as doc_source
|
|
151
|
-
FROM chunks_fts f
|
|
152
|
-
JOIN chunks c ON c.id = f.rowid
|
|
153
|
-
JOIN documents d ON d.id = c.document_id
|
|
154
|
-
${args.namespace ? "WHERE d.namespace = ?" : ""}
|
|
155
|
-
ORDER BY rank LIMIT ?
|
|
156
|
-
`).all(...(args.namespace ? [args.namespace, limit] : [limit]));
|
|
157
|
-
return jsonResult({ chunks: rows, count: rows.length, mode: "keyword_only" });
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const results = hybridSearch(db, "chunks_fts", "vec_chunks", args.query, queryEmbedding, limit * 2, alpha);
|
|
161
|
-
if (results.length === 0) return jsonResult({ chunks: [], count: 0 });
|
|
162
|
-
|
|
163
|
-
const ids = results.map(r => r.rowid);
|
|
164
|
-
const scoreMap = new Map(results.map(r => [r.rowid, r.score]));
|
|
165
|
-
|
|
166
|
-
const conditions = [`c.id IN (${ids.map(() => "?").join(",")})`];
|
|
167
|
-
const vals: any[] = [...ids];
|
|
168
|
-
if (args.namespace) { conditions.push("d.namespace = ?"); vals.push(args.namespace); }
|
|
169
|
-
|
|
170
|
-
const rows = db.prepare(`
|
|
171
|
-
SELECT c.*, d.title as doc_title, d.source as doc_source, d.namespace as doc_namespace
|
|
172
|
-
FROM chunks c
|
|
173
|
-
JOIN documents d ON d.id = c.document_id
|
|
174
|
-
WHERE ${conditions.join(" AND ")}
|
|
175
|
-
`).all(...vals) as any[];
|
|
176
|
-
|
|
177
|
-
const scored = rows
|
|
178
|
-
.map(r => ({ ...r, _score: scoreMap.get(r.id) || 0 }))
|
|
179
|
-
.sort((a, b) => b._score - a._score)
|
|
180
|
-
.slice(0, limit);
|
|
181
|
-
|
|
182
|
-
return jsonResult({ chunks: scored, count: scored.length });
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function docList(db: Database.Database, args: any) {
|
|
186
|
-
const conditions: string[] = [];
|
|
187
|
-
const vals: any[] = [];
|
|
188
|
-
if (args.namespace) { conditions.push("namespace = ?"); vals.push(args.namespace); }
|
|
189
|
-
|
|
190
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
191
|
-
const limit = args.limit || 50;
|
|
192
|
-
|
|
193
|
-
const rows = db.prepare(`
|
|
194
|
-
SELECT d.*, (SELECT COUNT(*) FROM chunks c WHERE c.document_id = d.id) as chunk_count
|
|
195
|
-
FROM documents d ${where}
|
|
196
|
-
ORDER BY d.created_at DESC LIMIT ?
|
|
197
|
-
`).all(...vals, limit);
|
|
198
|
-
|
|
199
|
-
return jsonResult({ documents: rows, count: rows.length });
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function docGet(db: Database.Database, args: any) {
|
|
203
|
-
const doc = db.prepare("SELECT * FROM documents WHERE id = ?").get(args.id) as any;
|
|
204
|
-
if (!doc) return errorResult(`Document ${args.id} not found`);
|
|
205
|
-
const chunks = db.prepare("SELECT * FROM chunks WHERE document_id = ? ORDER BY chunk_index").all(args.id);
|
|
206
|
-
return jsonResult({ ...doc, chunks });
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function docDelete(db: Database.Database, args: any) {
|
|
210
|
-
const chunks = db.prepare("SELECT id FROM chunks WHERE document_id = ?").all(args.id) as any[];
|
|
211
|
-
for (const c of chunks) deleteVector(db, "vec_chunks", c.id);
|
|
212
|
-
db.prepare("DELETE FROM chunks WHERE document_id = ?").run(args.id);
|
|
213
|
-
db.prepare("DELETE FROM documents WHERE id = ?").run(args.id);
|
|
214
|
-
return jsonResult({ deleted: true, chunks_removed: chunks.length });
|
|
215
|
-
}
|
package/src/tools/episodes.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import type Database from "better-sqlite3";
|
|
2
|
-
import { now, jsonResult, errorResult } from "../db.ts";
|
|
3
|
-
|
|
4
|
-
export const episodeTools = [
|
|
5
|
-
{
|
|
6
|
-
name: "memory.episode.log",
|
|
7
|
-
description:
|
|
8
|
-
"Log a conversation turn or run event to episodic memory. Use for tracking what happened during agent runs.",
|
|
9
|
-
inputSchema: {
|
|
10
|
-
type: "object" as const,
|
|
11
|
-
properties: {
|
|
12
|
-
session_id: { type: "string", description: "Session/run identifier" },
|
|
13
|
-
namespace: { type: "string", description: "Project or scope" },
|
|
14
|
-
agent_role: { type: "string", description: "Agent role" },
|
|
15
|
-
role: { type: "string", enum: ["user", "assistant", "system"], description: "Message role" },
|
|
16
|
-
content: { type: "string", description: "Message or event content" },
|
|
17
|
-
summary: { type: "string", description: "Optional short summary" },
|
|
18
|
-
metadata: { type: "object", description: "Custom metadata (e.g. tool calls, tokens used)" },
|
|
19
|
-
},
|
|
20
|
-
required: ["session_id", "role", "content"],
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: "memory.episode.list",
|
|
25
|
-
description: "List episodes for a session or namespace.",
|
|
26
|
-
inputSchema: {
|
|
27
|
-
type: "object" as const,
|
|
28
|
-
properties: {
|
|
29
|
-
session_id: { type: "string", description: "Filter by session" },
|
|
30
|
-
namespace: { type: "string", description: "Filter by namespace" },
|
|
31
|
-
agent_role: { type: "string", description: "Filter by agent role" },
|
|
32
|
-
limit: { type: "number", description: "Max results (default 50)" },
|
|
33
|
-
order: { type: "string", enum: ["newest", "oldest"], description: "Default: oldest" },
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
name: "memory.episode.summarize",
|
|
39
|
-
description: "Store a summary for a session. The calling agent provides the summary text.",
|
|
40
|
-
inputSchema: {
|
|
41
|
-
type: "object" as const,
|
|
42
|
-
properties: {
|
|
43
|
-
session_id: { type: "string", description: "Session to summarize" },
|
|
44
|
-
namespace: { type: "string", description: "Scope" },
|
|
45
|
-
agent_role: { type: "string", description: "Agent role" },
|
|
46
|
-
summary: { type: "string", description: "Summary text" },
|
|
47
|
-
},
|
|
48
|
-
required: ["session_id", "summary"],
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
export function handleEpisodes(db: Database.Database, name: string, args: any) {
|
|
54
|
-
if (name === "memory.episode.log") return episodeLog(db, args);
|
|
55
|
-
if (name === "memory.episode.list") return episodeList(db, args);
|
|
56
|
-
if (name === "memory.episode.summarize") return episodeSummarize(db, args);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function episodeLog(db: Database.Database, args: any) {
|
|
61
|
-
const ts = now();
|
|
62
|
-
const result = db.prepare(`
|
|
63
|
-
INSERT INTO episodes (session_id, namespace, agent_role, role, content, summary, metadata, created_at)
|
|
64
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
65
|
-
`).run(
|
|
66
|
-
args.session_id, args.namespace || null, args.agent_role || null,
|
|
67
|
-
args.role, args.content, args.summary || null,
|
|
68
|
-
JSON.stringify(args.metadata || {}), ts
|
|
69
|
-
);
|
|
70
|
-
return jsonResult({ id: Number(result.lastInsertRowid), created: true });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function episodeList(db: Database.Database, args: any) {
|
|
74
|
-
const conditions: string[] = [];
|
|
75
|
-
const vals: any[] = [];
|
|
76
|
-
if (args.session_id) { conditions.push("session_id = ?"); vals.push(args.session_id); }
|
|
77
|
-
if (args.namespace) { conditions.push("namespace = ?"); vals.push(args.namespace); }
|
|
78
|
-
if (args.agent_role) { conditions.push("agent_role = ?"); vals.push(args.agent_role); }
|
|
79
|
-
|
|
80
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
81
|
-
const order = args.order === "newest" ? "DESC" : "ASC";
|
|
82
|
-
const limit = args.limit || 50;
|
|
83
|
-
|
|
84
|
-
const rows = db.prepare(
|
|
85
|
-
`SELECT * FROM episodes ${where} ORDER BY created_at ${order} LIMIT ?`
|
|
86
|
-
).all(...vals, limit);
|
|
87
|
-
|
|
88
|
-
return jsonResult({ episodes: rows, count: rows.length });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function episodeSummarize(db: Database.Database, args: any) {
|
|
92
|
-
// Update all episodes in the session with the summary
|
|
93
|
-
const result = db.prepare(
|
|
94
|
-
`UPDATE episodes SET summary = ? WHERE session_id = ? AND summary IS NULL`
|
|
95
|
-
).run(args.summary, args.session_id);
|
|
96
|
-
|
|
97
|
-
// Also store as a memory for cross-session recall
|
|
98
|
-
const ts = now();
|
|
99
|
-
db.prepare(`
|
|
100
|
-
INSERT INTO memories (memory_type, scope, namespace, agent_role, title, content, status, confidence, tags, metadata, created_at, occurred_at, updated_at, content_hash)
|
|
101
|
-
VALUES ('episodic', 'session', ?, ?, ?, ?, 'active', 1.0, '["session_summary"]', ?, ?, ?, ?, ?)
|
|
102
|
-
`).run(
|
|
103
|
-
args.namespace || null, args.agent_role || null,
|
|
104
|
-
`Session ${args.session_id} summary`,
|
|
105
|
-
args.summary,
|
|
106
|
-
JSON.stringify({ session_id: args.session_id }),
|
|
107
|
-
ts, ts, ts,
|
|
108
|
-
require("node:crypto").createHash("sha256").update(`episode\0${args.session_id}\0${args.summary}`).digest("hex")
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
return jsonResult({ episodes_updated: result.changes, session_id: args.session_id });
|
|
112
|
-
}
|
package/src/tools/knowledge.ts
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import type Database from "better-sqlite3";
|
|
2
|
-
import { now, jsonResult, errorResult } from "../db.ts";
|
|
3
|
-
|
|
4
|
-
export const knowledgeTools = [
|
|
5
|
-
{
|
|
6
|
-
name: "memory.entity.add",
|
|
7
|
-
description:
|
|
8
|
-
"Add an entity to the knowledge graph. Entities represent projects, people, technologies, concepts, files, or any named thing.",
|
|
9
|
-
inputSchema: {
|
|
10
|
-
type: "object" as const,
|
|
11
|
-
properties: {
|
|
12
|
-
name: { type: "string", description: "Entity name (e.g. 'ao-cli', 'React', 'Sami')" },
|
|
13
|
-
entity_type: { type: "string", description: "Type (e.g. project, person, technology, concept, file, service)" },
|
|
14
|
-
namespace: { type: "string", description: "Scope" },
|
|
15
|
-
description: { type: "string", description: "Brief description" },
|
|
16
|
-
metadata: { type: "object", description: "Custom metadata" },
|
|
17
|
-
},
|
|
18
|
-
required: ["name", "entity_type"],
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
name: "memory.entity.link",
|
|
23
|
-
description:
|
|
24
|
-
"Create a relationship between two entities. E.g. 'ao-cli uses Rust', 'invoicer depends_on Drizzle'.",
|
|
25
|
-
inputSchema: {
|
|
26
|
-
type: "object" as const,
|
|
27
|
-
properties: {
|
|
28
|
-
source: { type: "string", description: "Source entity name" },
|
|
29
|
-
source_type: { type: "string", description: "Source entity type (for disambiguation)" },
|
|
30
|
-
relation: { type: "string", description: "Relation type (e.g. uses, depends_on, created_by, part_of, related_to)" },
|
|
31
|
-
target: { type: "string", description: "Target entity name" },
|
|
32
|
-
target_type: { type: "string", description: "Target entity type" },
|
|
33
|
-
weight: { type: "number", description: "Relation strength 0.0-1.0 (default 1.0)" },
|
|
34
|
-
memory_id: { type: "number", description: "Link to a memory entry as evidence" },
|
|
35
|
-
namespace: { type: "string", description: "Scope for auto-creating entities" },
|
|
36
|
-
metadata: { type: "object", description: "Custom metadata" },
|
|
37
|
-
},
|
|
38
|
-
required: ["source", "source_type", "relation", "target", "target_type"],
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
name: "memory.entity.query",
|
|
43
|
-
description:
|
|
44
|
-
"Query the knowledge graph. Find entities and traverse relationships. Supports multi-hop traversal.",
|
|
45
|
-
inputSchema: {
|
|
46
|
-
type: "object" as const,
|
|
47
|
-
properties: {
|
|
48
|
-
name: { type: "string", description: "Entity name to start from" },
|
|
49
|
-
entity_type: { type: "string", description: "Filter by entity type" },
|
|
50
|
-
relation: { type: "string", description: "Filter by relation type" },
|
|
51
|
-
direction: { type: "string", enum: ["outgoing", "incoming", "both"], description: "Traversal direction (default: both)" },
|
|
52
|
-
depth: { type: "number", description: "Max traversal depth (default 1, max 3)" },
|
|
53
|
-
namespace: { type: "string", description: "Filter by namespace" },
|
|
54
|
-
limit: { type: "number", description: "Max results (default 50)" },
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: "memory.entity.list",
|
|
60
|
-
description: "List entities in the knowledge graph.",
|
|
61
|
-
inputSchema: {
|
|
62
|
-
type: "object" as const,
|
|
63
|
-
properties: {
|
|
64
|
-
entity_type: { type: "string", description: "Filter by type" },
|
|
65
|
-
namespace: { type: "string", description: "Filter by namespace" },
|
|
66
|
-
limit: { type: "number", description: "Max results (default 50)" },
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
export function handleKnowledge(db: Database.Database, name: string, args: any) {
|
|
73
|
-
if (name === "memory.entity.add") return entityAdd(db, args);
|
|
74
|
-
if (name === "memory.entity.link") return entityLink(db, args);
|
|
75
|
-
if (name === "memory.entity.query") return entityQuery(db, args);
|
|
76
|
-
if (name === "memory.entity.list") return entityList(db, args);
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function getOrCreateEntity(db: Database.Database, name: string, entityType: string, namespace?: string): number {
|
|
81
|
-
const existing = db.prepare(
|
|
82
|
-
"SELECT id FROM entities WHERE name = ? AND entity_type = ? AND namespace IS ?"
|
|
83
|
-
).get(name, entityType, namespace || null) as any;
|
|
84
|
-
|
|
85
|
-
if (existing) return existing.id;
|
|
86
|
-
|
|
87
|
-
const ts = now();
|
|
88
|
-
const result = db.prepare(`
|
|
89
|
-
INSERT INTO entities (name, entity_type, namespace, metadata, created_at, updated_at)
|
|
90
|
-
VALUES (?, ?, ?, '{}', ?, ?)
|
|
91
|
-
`).run(name, entityType, namespace || null, ts, ts);
|
|
92
|
-
|
|
93
|
-
return Number(result.lastInsertRowid);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function entityAdd(db: Database.Database, args: any) {
|
|
97
|
-
const ts = now();
|
|
98
|
-
const existing = db.prepare(
|
|
99
|
-
"SELECT id FROM entities WHERE name = ? AND entity_type = ? AND namespace IS ?"
|
|
100
|
-
).get(args.name, args.entity_type, args.namespace || null) as any;
|
|
101
|
-
|
|
102
|
-
if (existing) {
|
|
103
|
-
// Update existing
|
|
104
|
-
const sets: string[] = ["updated_at = ?"];
|
|
105
|
-
const vals: any[] = [ts];
|
|
106
|
-
if (args.description) { sets.push("description = ?"); vals.push(args.description); }
|
|
107
|
-
if (args.metadata) { sets.push("metadata = ?"); vals.push(JSON.stringify(args.metadata)); }
|
|
108
|
-
vals.push(existing.id);
|
|
109
|
-
db.prepare(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`).run(...vals);
|
|
110
|
-
return jsonResult({ id: existing.id, updated: true });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const result = db.prepare(`
|
|
114
|
-
INSERT INTO entities (name, entity_type, namespace, description, metadata, created_at, updated_at)
|
|
115
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
116
|
-
`).run(
|
|
117
|
-
args.name, args.entity_type, args.namespace || null,
|
|
118
|
-
args.description || null, JSON.stringify(args.metadata || {}), ts, ts
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
return jsonResult({ id: Number(result.lastInsertRowid), created: true });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function entityLink(db: Database.Database, args: any) {
|
|
125
|
-
const sourceId = getOrCreateEntity(db, args.source, args.source_type, args.namespace);
|
|
126
|
-
const targetId = getOrCreateEntity(db, args.target, args.target_type, args.namespace);
|
|
127
|
-
|
|
128
|
-
const existing = db.prepare(
|
|
129
|
-
"SELECT id FROM relations WHERE source_entity_id = ? AND relation_type = ? AND target_entity_id = ?"
|
|
130
|
-
).get(sourceId, args.relation, targetId) as any;
|
|
131
|
-
|
|
132
|
-
if (existing) {
|
|
133
|
-
// Update weight/metadata
|
|
134
|
-
const sets: string[] = [];
|
|
135
|
-
const vals: any[] = [];
|
|
136
|
-
if (args.weight !== undefined) { sets.push("weight = ?"); vals.push(args.weight); }
|
|
137
|
-
if (args.memory_id) { sets.push("memory_id = ?"); vals.push(args.memory_id); }
|
|
138
|
-
if (args.metadata) { sets.push("metadata = ?"); vals.push(JSON.stringify(args.metadata)); }
|
|
139
|
-
if (sets.length > 0) {
|
|
140
|
-
vals.push(existing.id);
|
|
141
|
-
db.prepare(`UPDATE relations SET ${sets.join(", ")} WHERE id = ?`).run(...vals);
|
|
142
|
-
}
|
|
143
|
-
return jsonResult({ id: existing.id, updated: true });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const ts = now();
|
|
147
|
-
const result = db.prepare(`
|
|
148
|
-
INSERT INTO relations (source_entity_id, relation_type, target_entity_id, weight, memory_id, metadata, created_at)
|
|
149
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
150
|
-
`).run(sourceId, args.relation, targetId, args.weight ?? 1.0, args.memory_id || null, JSON.stringify(args.metadata || {}), ts);
|
|
151
|
-
|
|
152
|
-
return jsonResult({ id: Number(result.lastInsertRowid), created: true, source_id: sourceId, target_id: targetId });
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function entityQuery(db: Database.Database, args: any) {
|
|
156
|
-
const depth = Math.min(args.depth || 1, 3);
|
|
157
|
-
const limit = args.limit || 50;
|
|
158
|
-
const direction = args.direction || "both";
|
|
159
|
-
|
|
160
|
-
// Find starting entities
|
|
161
|
-
const startConditions: string[] = [];
|
|
162
|
-
const startVals: any[] = [];
|
|
163
|
-
if (args.name) { startConditions.push("name = ?"); startVals.push(args.name); }
|
|
164
|
-
if (args.entity_type) { startConditions.push("entity_type = ?"); startVals.push(args.entity_type); }
|
|
165
|
-
if (args.namespace) { startConditions.push("namespace = ?"); startVals.push(args.namespace); }
|
|
166
|
-
|
|
167
|
-
if (startConditions.length === 0) {
|
|
168
|
-
return errorResult("At least one of: name, entity_type, or namespace required");
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const startEntities = db.prepare(
|
|
172
|
-
`SELECT * FROM entities WHERE ${startConditions.join(" AND ")} LIMIT ?`
|
|
173
|
-
).all(...startVals, limit) as any[];
|
|
174
|
-
|
|
175
|
-
if (startEntities.length === 0) return jsonResult({ entities: [], relations: [] });
|
|
176
|
-
|
|
177
|
-
// Traverse relations
|
|
178
|
-
const visited = new Set<number>();
|
|
179
|
-
const allEntities: any[] = [...startEntities];
|
|
180
|
-
const allRelations: any[] = [];
|
|
181
|
-
let currentIds = startEntities.map(e => e.id);
|
|
182
|
-
startEntities.forEach(e => visited.add(e.id));
|
|
183
|
-
|
|
184
|
-
for (let d = 0; d < depth; d++) {
|
|
185
|
-
if (currentIds.length === 0) break;
|
|
186
|
-
const placeholders = currentIds.map(() => "?").join(",");
|
|
187
|
-
|
|
188
|
-
const relConditions: string[] = [];
|
|
189
|
-
if (direction === "outgoing" || direction === "both") {
|
|
190
|
-
relConditions.push(`source_entity_id IN (${placeholders})`);
|
|
191
|
-
}
|
|
192
|
-
if (direction === "incoming" || direction === "both") {
|
|
193
|
-
relConditions.push(`target_entity_id IN (${placeholders})`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const relFilter = args.relation ? ` AND relation_type = ?` : "";
|
|
197
|
-
const relVals = args.relation
|
|
198
|
-
? [...currentIds, ...(direction === "both" ? currentIds : []), args.relation]
|
|
199
|
-
: [...currentIds, ...(direction === "both" ? currentIds : [])];
|
|
200
|
-
|
|
201
|
-
const rels = db.prepare(`
|
|
202
|
-
SELECT r.*,
|
|
203
|
-
se.name as source_name, se.entity_type as source_type,
|
|
204
|
-
te.name as target_name, te.entity_type as target_type
|
|
205
|
-
FROM relations r
|
|
206
|
-
JOIN entities se ON se.id = r.source_entity_id
|
|
207
|
-
JOIN entities te ON te.id = r.target_entity_id
|
|
208
|
-
WHERE (${relConditions.join(" OR ")})${relFilter}
|
|
209
|
-
LIMIT ?
|
|
210
|
-
`).all(...relVals, limit) as any[];
|
|
211
|
-
|
|
212
|
-
allRelations.push(...rels);
|
|
213
|
-
|
|
214
|
-
const nextIds: number[] = [];
|
|
215
|
-
for (const rel of rels) {
|
|
216
|
-
for (const id of [rel.source_entity_id, rel.target_entity_id]) {
|
|
217
|
-
if (!visited.has(id)) {
|
|
218
|
-
visited.add(id);
|
|
219
|
-
nextIds.push(id);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (nextIds.length > 0) {
|
|
225
|
-
const ents = db.prepare(
|
|
226
|
-
`SELECT * FROM entities WHERE id IN (${nextIds.map(() => "?").join(",")})`
|
|
227
|
-
).all(...nextIds) as any[];
|
|
228
|
-
allEntities.push(...ents);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
currentIds = nextIds;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return jsonResult({ entities: allEntities, relations: allRelations });
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function entityList(db: Database.Database, args: any) {
|
|
238
|
-
const conditions: string[] = [];
|
|
239
|
-
const vals: any[] = [];
|
|
240
|
-
if (args.entity_type) { conditions.push("entity_type = ?"); vals.push(args.entity_type); }
|
|
241
|
-
if (args.namespace) { conditions.push("namespace = ?"); vals.push(args.namespace); }
|
|
242
|
-
|
|
243
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
244
|
-
const limit = args.limit || 50;
|
|
245
|
-
|
|
246
|
-
const rows = db.prepare(`SELECT * FROM entities ${where} ORDER BY name LIMIT ?`).all(...vals, limit);
|
|
247
|
-
return jsonResult({ entities: rows, count: rows.length });
|
|
248
|
-
}
|