@pi-unipi/memory 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/README.md +94 -0
- package/commands.ts +178 -0
- package/embedding.ts +46 -0
- package/index.ts +150 -0
- package/package.json +55 -0
- package/search.ts +134 -0
- package/skills/memory/SKILL.md +151 -0
- package/storage.ts +716 -0
- package/tools.ts +354 -0
package/tools.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/memory — Tool registration
|
|
3
|
+
*
|
|
4
|
+
* Tools for the LLM to store, search, and delete memories.
|
|
5
|
+
* All storage is project-scoped. "Global" tools search across all projects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import {
|
|
11
|
+
MemoryStorage,
|
|
12
|
+
searchAllProjects,
|
|
13
|
+
listAllProjects,
|
|
14
|
+
getProjectName,
|
|
15
|
+
type MemoryRecord,
|
|
16
|
+
} from "./storage.js";
|
|
17
|
+
import { generateEmbedding } from "./embedding.js";
|
|
18
|
+
import { hybridSearch } from "./search.js";
|
|
19
|
+
|
|
20
|
+
/** Tool names */
|
|
21
|
+
export const MEMORY_TOOLS = {
|
|
22
|
+
STORE: "memory_store",
|
|
23
|
+
SEARCH: "memory_search",
|
|
24
|
+
DELETE: "memory_delete",
|
|
25
|
+
LIST: "memory_list",
|
|
26
|
+
GLOBAL_SEARCH: "global_memory_search",
|
|
27
|
+
GLOBAL_LIST: "global_memory_list",
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register memory tools.
|
|
32
|
+
*/
|
|
33
|
+
export function registerMemoryTools(
|
|
34
|
+
pi: ExtensionAPI,
|
|
35
|
+
getStorage: () => MemoryStorage
|
|
36
|
+
): void {
|
|
37
|
+
// --- memory_store tool ---
|
|
38
|
+
pi.registerTool({
|
|
39
|
+
name: MEMORY_TOOLS.STORE,
|
|
40
|
+
label: "Store Memory",
|
|
41
|
+
description:
|
|
42
|
+
"Store or update a memory for cross-session recall. Use for user preferences, project decisions, code patterns, and conversation summaries.",
|
|
43
|
+
promptSnippet: "Store a memory for cross-session recall.",
|
|
44
|
+
promptGuidelines: [
|
|
45
|
+
"Use memory_store to remember important user preferences, decisions, patterns, or summaries.",
|
|
46
|
+
"Memory is scoped to the current project.",
|
|
47
|
+
"Update existing memories instead of creating duplicates.",
|
|
48
|
+
],
|
|
49
|
+
parameters: Type.Object({
|
|
50
|
+
title: Type.String({
|
|
51
|
+
description:
|
|
52
|
+
"Memory title in <most_important>_<less_important>_<lesser> format (e.g., 'auth_jwt_prefer_refresh_tokens')",
|
|
53
|
+
}),
|
|
54
|
+
content: Type.String({ description: "Full memory content (markdown supported)" }),
|
|
55
|
+
tags: Type.Optional(
|
|
56
|
+
Type.Array(Type.String(), { description: "Tags for categorization" })
|
|
57
|
+
),
|
|
58
|
+
type: Type.Optional(
|
|
59
|
+
Type.String({
|
|
60
|
+
description: "Memory type: preference, decision, pattern, or summary",
|
|
61
|
+
enum: ["preference", "decision", "pattern", "summary"],
|
|
62
|
+
})
|
|
63
|
+
),
|
|
64
|
+
}),
|
|
65
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
66
|
+
const storage = getStorage();
|
|
67
|
+
|
|
68
|
+
// Check if similar memory exists
|
|
69
|
+
const existing = storage.getByTitle(params.title);
|
|
70
|
+
if (existing) {
|
|
71
|
+
// Update existing
|
|
72
|
+
const updated: MemoryRecord = {
|
|
73
|
+
...existing,
|
|
74
|
+
content: params.content,
|
|
75
|
+
tags: params.tags || existing.tags,
|
|
76
|
+
type: (params.type as MemoryRecord["type"]) || existing.type,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const embedding = await generateEmbedding(
|
|
80
|
+
params.title + " " + params.content,
|
|
81
|
+
pi
|
|
82
|
+
);
|
|
83
|
+
updated.embedding = embedding;
|
|
84
|
+
|
|
85
|
+
storage.store(updated);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: `Updated memory: ${params.title}`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
details: { action: "updated", id: updated.id },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Create new memory
|
|
99
|
+
const record: MemoryRecord = {
|
|
100
|
+
id: "",
|
|
101
|
+
title: params.title,
|
|
102
|
+
content: params.content,
|
|
103
|
+
tags: params.tags || [],
|
|
104
|
+
project: getProjectName(ctx.cwd),
|
|
105
|
+
type: (params.type as MemoryRecord["type"]) || "summary",
|
|
106
|
+
created: "",
|
|
107
|
+
updated: "",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const embedding = await generateEmbedding(
|
|
111
|
+
params.title + " " + params.content,
|
|
112
|
+
pi
|
|
113
|
+
);
|
|
114
|
+
record.embedding = embedding;
|
|
115
|
+
|
|
116
|
+
storage.store(record);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "text",
|
|
122
|
+
text: `Stored memory: ${params.title}`,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
details: { action: "created", id: record.id },
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// --- memory_search tool ---
|
|
131
|
+
pi.registerTool({
|
|
132
|
+
name: MEMORY_TOOLS.SEARCH,
|
|
133
|
+
label: "Search Memory",
|
|
134
|
+
description:
|
|
135
|
+
"Search current project memories by keyword. Returns ranked results with snippets.",
|
|
136
|
+
promptSnippet: "Search project memories for relevant context.",
|
|
137
|
+
promptGuidelines: [
|
|
138
|
+
"Use memory_search before making decisions when you suspect past work exists.",
|
|
139
|
+
"Search for user preferences when setting up new features.",
|
|
140
|
+
"Search for patterns when implementing similar functionality.",
|
|
141
|
+
"Use global_memory_search to search across ALL projects.",
|
|
142
|
+
],
|
|
143
|
+
parameters: Type.Object({
|
|
144
|
+
query: Type.String({ description: "Search query" }),
|
|
145
|
+
limit: Type.Optional(
|
|
146
|
+
Type.Number({ description: "Max results (default 10)", default: 10 })
|
|
147
|
+
),
|
|
148
|
+
}),
|
|
149
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
150
|
+
const storage = getStorage();
|
|
151
|
+
|
|
152
|
+
const embedding = await generateEmbedding(params.query, pi);
|
|
153
|
+
|
|
154
|
+
const results = hybridSearch(
|
|
155
|
+
storage,
|
|
156
|
+
params.query,
|
|
157
|
+
params.limit || 10,
|
|
158
|
+
embedding
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (results.length === 0) {
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: `No memories found for: "${params.query}"`,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
details: { results: [] },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const output = results
|
|
174
|
+
.map(
|
|
175
|
+
(r, i) =>
|
|
176
|
+
`${i + 1}. **${r.record.title}** (${r.record.type})\n ${r.snippet}`
|
|
177
|
+
)
|
|
178
|
+
.join("\n\n");
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: `Found ${results.length} memories:\n\n${output}`,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
details: { results: results.map((r) => r.record.id) },
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// --- memory_delete tool ---
|
|
193
|
+
pi.registerTool({
|
|
194
|
+
name: MEMORY_TOOLS.DELETE,
|
|
195
|
+
label: "Delete Memory",
|
|
196
|
+
description: "Delete a memory by title or ID from the current project.",
|
|
197
|
+
promptSnippet: "Delete a memory.",
|
|
198
|
+
parameters: Type.Object({
|
|
199
|
+
title: Type.Optional(Type.String({ description: "Memory title to delete" })),
|
|
200
|
+
id: Type.Optional(Type.String({ description: "Memory ID to delete" })),
|
|
201
|
+
}),
|
|
202
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
203
|
+
const storage = getStorage();
|
|
204
|
+
|
|
205
|
+
let deleted = false;
|
|
206
|
+
if (params.id) {
|
|
207
|
+
deleted = storage.delete(params.id);
|
|
208
|
+
} else if (params.title) {
|
|
209
|
+
deleted = storage.deleteByTitle(params.title);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: "text",
|
|
216
|
+
text: deleted
|
|
217
|
+
? `Deleted memory: ${params.title || params.id}`
|
|
218
|
+
: `Memory not found: ${params.title || params.id}`,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
details: { deleted },
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// --- memory_list tool ---
|
|
227
|
+
pi.registerTool({
|
|
228
|
+
name: MEMORY_TOOLS.LIST,
|
|
229
|
+
label: "List Project Memories",
|
|
230
|
+
description: "List all memories for the current project.",
|
|
231
|
+
promptSnippet: "List all project memories.",
|
|
232
|
+
parameters: Type.Object({}),
|
|
233
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
234
|
+
const storage = getStorage();
|
|
235
|
+
const memories = storage.listAll();
|
|
236
|
+
|
|
237
|
+
if (memories.length === 0) {
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text: "No memories stored for this project." }],
|
|
240
|
+
details: { memories: [] },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const output = memories
|
|
245
|
+
.map((m) => `- ${m.title} (${m.type})`)
|
|
246
|
+
.join("\n");
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
content: [
|
|
250
|
+
{
|
|
251
|
+
type: "text",
|
|
252
|
+
text: `Project memories (${memories.length}):\n\n${output}`,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
details: { memories },
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// --- global_memory_search tool ---
|
|
261
|
+
pi.registerTool({
|
|
262
|
+
name: MEMORY_TOOLS.GLOBAL_SEARCH,
|
|
263
|
+
label: "Search All Projects",
|
|
264
|
+
description: "Search memories across ALL projects. Returns results with project names.",
|
|
265
|
+
promptSnippet: "Search memories across all projects.",
|
|
266
|
+
promptGuidelines: [
|
|
267
|
+
"Use global_memory_search when looking for memories from other projects.",
|
|
268
|
+
"Returns results with [project_name] prefix to identify source.",
|
|
269
|
+
],
|
|
270
|
+
parameters: Type.Object({
|
|
271
|
+
query: Type.String({ description: "Search query" }),
|
|
272
|
+
limit: Type.Optional(
|
|
273
|
+
Type.Number({ description: "Max results (default 10)", default: 10 })
|
|
274
|
+
),
|
|
275
|
+
}),
|
|
276
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
277
|
+
const results = searchAllProjects(params.query, params.limit || 10);
|
|
278
|
+
|
|
279
|
+
if (results.length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
content: [
|
|
282
|
+
{
|
|
283
|
+
type: "text",
|
|
284
|
+
text: `No memories found across projects for: "${params.query}"`,
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
details: { results: [] },
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const output = results
|
|
292
|
+
.map(
|
|
293
|
+
(r, i) =>
|
|
294
|
+
`${i + 1}. [${r.record.project}] **${r.record.title}** (${r.record.type})\n ${r.snippet}`
|
|
295
|
+
)
|
|
296
|
+
.join("\n\n");
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
content: [
|
|
300
|
+
{
|
|
301
|
+
type: "text",
|
|
302
|
+
text: `Found ${results.length} memories across projects:\n\n${output}`,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
details: { results: results.map((r) => r.record.id) },
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// --- global_memory_list tool ---
|
|
311
|
+
pi.registerTool({
|
|
312
|
+
name: MEMORY_TOOLS.GLOBAL_LIST,
|
|
313
|
+
label: "List All Project Memories",
|
|
314
|
+
description: "List all memories across all projects with project names.",
|
|
315
|
+
promptSnippet: "List all memories across projects.",
|
|
316
|
+
parameters: Type.Object({}),
|
|
317
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
318
|
+
const memories = listAllProjects();
|
|
319
|
+
|
|
320
|
+
if (memories.length === 0) {
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: "text", text: "No memories stored in any project." }],
|
|
323
|
+
details: { memories: [] },
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Group by project
|
|
328
|
+
const grouped = new Map<string, typeof memories>();
|
|
329
|
+
for (const m of memories) {
|
|
330
|
+
const list = grouped.get(m.project) || [];
|
|
331
|
+
list.push(m);
|
|
332
|
+
grouped.set(m.project, list);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let output = "";
|
|
336
|
+
for (const [project, projectMemories] of grouped) {
|
|
337
|
+
output += `\n**${project}** (${projectMemories.length}):\n`;
|
|
338
|
+
for (const m of projectMemories) {
|
|
339
|
+
output += ` - ${m.title} (${m.type})\n`;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
content: [
|
|
345
|
+
{
|
|
346
|
+
type: "text",
|
|
347
|
+
text: `All memories across ${grouped.size} projects (${memories.length} total):${output}`,
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
details: { memories },
|
|
351
|
+
};
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
}
|