@phi-code-admin/phi-code 0.77.3 → 0.79.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/CHANGELOG.md +92 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +65 -3
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/utils.d.ts +1 -1
- package/dist/core/compaction/utils.d.ts.map +1 -1
- package/dist/core/compaction/utils.js +4 -2
- package/dist/core/compaction/utils.js.map +1 -1
- package/dist/core/tools/bash.d.ts +16 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +154 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +4 -1
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +6 -2
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +6 -2
- package/dist/core/tools/write.js.map +1 -1
- package/examples/extensions/subagent/agents.ts +3 -1
- package/extensions/phi/commit.ts +253 -0
- package/extensions/phi/memory.ts +300 -83
- package/extensions/phi/orchestrator.ts +290 -65
- package/extensions/phi/providers/orchestrator-helpers.ts +62 -0
- package/extensions/phi/web-search.ts +15 -2
- package/package.json +1 -1
package/extensions/phi/memory.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Now powered by sigma-memory package which provides:
|
|
5
5
|
* - NotesManager: Markdown files management
|
|
6
|
-
* - OntologyManager: Knowledge graph with entities and relations
|
|
6
|
+
* - OntologyManager: Knowledge graph with entities and relations
|
|
7
7
|
* - VectorStore: Embedded vector search (sql.js + local embeddings)
|
|
8
8
|
*
|
|
9
9
|
* Features:
|
|
@@ -18,13 +18,159 @@
|
|
|
18
18
|
* 2. Memory files are stored in ~/.phi/memory/
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
22
|
-
import
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
23
|
import { access } from "node:fs/promises";
|
|
24
24
|
import { join } from "node:path";
|
|
25
|
-
import {
|
|
25
|
+
import { Type } from "@sinclair/typebox";
|
|
26
|
+
import type { ExtensionAPI, ExtensionContext } from "phi-code";
|
|
26
27
|
import { SigmaMemory } from "sigma-memory";
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Build a unique, human-readable filename for a single memory fact.
|
|
31
|
+
*
|
|
32
|
+
* Combines a kebab-case slug of the first words of the content with a short
|
|
33
|
+
* content hash. This prevents same-day clobber (each fact gets its own file)
|
|
34
|
+
* while staying deterministic: re-writing the exact same fact yields the same
|
|
35
|
+
* name instead of piling up duplicates.
|
|
36
|
+
*/
|
|
37
|
+
function buildFactFilename(content: string): string {
|
|
38
|
+
const slug = content
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
41
|
+
.trim()
|
|
42
|
+
.split(/\s+/)
|
|
43
|
+
.slice(0, 6)
|
|
44
|
+
.join("-")
|
|
45
|
+
.replace(/-+/g, "-")
|
|
46
|
+
.replace(/^-|-$/g, "")
|
|
47
|
+
.slice(0, 60);
|
|
48
|
+
const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
49
|
+
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
|
50
|
+
const stem = slug ? `${date}-${slug}-${hash}` : `${date}-${hash}`;
|
|
51
|
+
return `${stem}.md`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Prepend a minimal YAML frontmatter ({name, description}) if absent, so each
|
|
56
|
+
* fact file carries lightweight metadata for later listing and search.
|
|
57
|
+
*/
|
|
58
|
+
function withFrontmatter(content: string, name: string): string {
|
|
59
|
+
if (content.startsWith("---\n") || content.startsWith("---\r\n")) {
|
|
60
|
+
return content;
|
|
61
|
+
}
|
|
62
|
+
const firstLine = content.split("\n", 1)[0]?.trim() ?? "";
|
|
63
|
+
const description = (firstLine || name).replace(/"/g, "'").slice(0, 120);
|
|
64
|
+
return `---\nname: "${name}"\ndescription: "${description}"\n---\n\n${content}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Hard caps for the deterministic recall manifest (no LLM involved). */
|
|
68
|
+
const MANIFEST_MAX_ENTRIES = 40;
|
|
69
|
+
const MANIFEST_MAX_CHARS = 2000;
|
|
70
|
+
const MANIFEST_DESC_MAX = 100;
|
|
71
|
+
// Chars reserved for the trailing "… (N notes total …)" overflow line so the
|
|
72
|
+
// final manifest still respects MANIFEST_MAX_CHARS once it is appended.
|
|
73
|
+
const MANIFEST_OVERFLOW_RESERVE = 96;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract a one-line summary for a single note file, deterministically.
|
|
77
|
+
*
|
|
78
|
+
* Prefers the YAML frontmatter `description:` field (written by
|
|
79
|
+
* withFrontmatter). Falls back to the first non-empty, non-frontmatter line.
|
|
80
|
+
* Returns a trimmed, single-line string truncated to MANIFEST_DESC_MAX chars.
|
|
81
|
+
*/
|
|
82
|
+
function summarizeNote(content: string): string {
|
|
83
|
+
const text = content.replace(/\r\n/g, "\n");
|
|
84
|
+
let body = text;
|
|
85
|
+
|
|
86
|
+
// If a frontmatter block is present, scan it for a description first.
|
|
87
|
+
if (text.startsWith("---\n")) {
|
|
88
|
+
const end = text.indexOf("\n---", 4);
|
|
89
|
+
if (end !== -1) {
|
|
90
|
+
const fm = text.slice(4, end);
|
|
91
|
+
for (const rawLine of fm.split("\n")) {
|
|
92
|
+
const m = rawLine.match(/^\s*description\s*:\s*(.+?)\s*$/);
|
|
93
|
+
if (m) {
|
|
94
|
+
// Strip surrounding quotes if present.
|
|
95
|
+
const value = m[1].replace(/^["']|["']$/g, "").trim();
|
|
96
|
+
if (value) return collapseLine(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// No description in frontmatter: summarize the body that follows it.
|
|
100
|
+
body = text.slice(end + 4);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const rawLine of body.split("\n")) {
|
|
105
|
+
const line = rawLine.replace(/^#+\s*/, "").trim();
|
|
106
|
+
if (line && line !== "---") {
|
|
107
|
+
return collapseLine(line);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Collapse internal whitespace to single spaces and truncate cleanly. */
|
|
114
|
+
function collapseLine(value: string): string {
|
|
115
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
116
|
+
return flat.length > MANIFEST_DESC_MAX ? `${flat.slice(0, MANIFEST_DESC_MAX - 1).trimEnd()}…` : flat;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a deterministic one-line-per-file manifest of all memory notes.
|
|
121
|
+
*
|
|
122
|
+
* Purely local: lists note files and reads their frontmatter / first line.
|
|
123
|
+
* No LLM call, no vector search. Bounded to MANIFEST_MAX_ENTRIES entries and
|
|
124
|
+
* MANIFEST_MAX_CHARS characters so it stays cheap to inject every turn.
|
|
125
|
+
*
|
|
126
|
+
* Returns an empty string when there are no notes, so callers can skip
|
|
127
|
+
* injection entirely.
|
|
128
|
+
*/
|
|
129
|
+
function buildMemoryManifest(sigmaMemory: SigmaMemory): string {
|
|
130
|
+
let files: Array<{ name: string; size: number; date: string }>;
|
|
131
|
+
try {
|
|
132
|
+
files = sigmaMemory.notes.list();
|
|
133
|
+
} catch {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
if (files.length === 0) {
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
let total = 0;
|
|
142
|
+
let truncated = false;
|
|
143
|
+
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
if (lines.length >= MANIFEST_MAX_ENTRIES) {
|
|
146
|
+
truncated = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
let summary = "";
|
|
150
|
+
try {
|
|
151
|
+
summary = summarizeNote(sigmaMemory.notes.read(file.name));
|
|
152
|
+
} catch {
|
|
153
|
+
// Unreadable note: still list its name so the model knows it exists.
|
|
154
|
+
summary = "";
|
|
155
|
+
}
|
|
156
|
+
const entry = summary ? `- ${file.name}: ${summary}` : `- ${file.name}`;
|
|
157
|
+
if (total + entry.length + 1 > MANIFEST_MAX_CHARS - MANIFEST_OVERFLOW_RESERVE) {
|
|
158
|
+
truncated = true;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
lines.push(entry);
|
|
162
|
+
total += entry.length + 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (lines.length === 0) {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
if (truncated || lines.length < files.length) {
|
|
169
|
+
lines.push(`- … (${files.length} notes total; use memory_read/memory_search for full content)`);
|
|
170
|
+
}
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
28
174
|
export default function memoryExtension(pi: ExtensionAPI) {
|
|
29
175
|
// Initialize sigma-memory with embedded vector store
|
|
30
176
|
const sigmaMemory = new SigmaMemory();
|
|
@@ -41,7 +187,8 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
41
187
|
name: "memory_search",
|
|
42
188
|
label: "Memory Search",
|
|
43
189
|
description: "Search for content in memory using unified search (notes + ontology + vector search)",
|
|
44
|
-
promptSnippet:
|
|
190
|
+
promptSnippet:
|
|
191
|
+
"Search project memory (notes, ontology, vector search). ALWAYS call before answering questions about prior work, decisions, or project context.",
|
|
45
192
|
promptGuidelines: [
|
|
46
193
|
"MANDATORY: Before starting ANY task, call memory_search with relevant keywords. This is not optional.",
|
|
47
194
|
"When starting work on a topic, search memory for existing notes and learnings.",
|
|
@@ -57,64 +204,72 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
57
204
|
|
|
58
205
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
59
206
|
const { query } = params as { query: string };
|
|
60
|
-
|
|
207
|
+
|
|
61
208
|
try {
|
|
62
209
|
const results = await sigmaMemory.search(query);
|
|
63
|
-
|
|
210
|
+
|
|
64
211
|
if (results.length === 0) {
|
|
65
212
|
return {
|
|
66
|
-
content: [
|
|
67
|
-
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: "text",
|
|
216
|
+
text: `No results found for "${query}". Use memory_write to create some memory files!`,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
details: { found: false, query, resultCount: 0 },
|
|
68
220
|
};
|
|
69
221
|
}
|
|
70
222
|
|
|
71
223
|
// Format results by source
|
|
72
224
|
let resultText = `Found ${results.length} results for "${query}":\n\n`;
|
|
73
|
-
|
|
74
|
-
const groupedResults = results.reduce(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
225
|
+
|
|
226
|
+
const groupedResults = results.reduce(
|
|
227
|
+
(groups, result) => {
|
|
228
|
+
if (!groups[result.source]) groups[result.source] = [];
|
|
229
|
+
groups[result.source].push(result);
|
|
230
|
+
return groups;
|
|
231
|
+
},
|
|
232
|
+
{} as Record<string, typeof results>,
|
|
233
|
+
);
|
|
79
234
|
|
|
80
235
|
for (const [source, sourceResults] of Object.entries(groupedResults)) {
|
|
81
236
|
resultText += `## ${source.toUpperCase()} (${sourceResults.length} results)\n\n`;
|
|
82
|
-
|
|
83
|
-
for (const result of sourceResults.slice(0, 5)) {
|
|
237
|
+
|
|
238
|
+
for (const result of sourceResults.slice(0, 5)) {
|
|
239
|
+
// Limit to 5 results per source
|
|
84
240
|
resultText += `**Score: ${result.score.toFixed(2)}** | Type: ${result.type}\n`;
|
|
85
|
-
|
|
86
|
-
if (result.source ===
|
|
241
|
+
|
|
242
|
+
if (result.source === "notes") {
|
|
87
243
|
const data = result.data;
|
|
88
244
|
resultText += `File: ${data.file} (line ${data.line})\n`;
|
|
89
245
|
resultText += `> ${data.content}\n\n`;
|
|
90
|
-
} else if (result.source ===
|
|
246
|
+
} else if (result.source === "ontology") {
|
|
91
247
|
const data = result.data;
|
|
92
|
-
if (result.type ===
|
|
248
|
+
if (result.type === "entity") {
|
|
93
249
|
resultText += `Entity: ${data.name} (${data.type})\n`;
|
|
94
250
|
resultText += `Properties: ${JSON.stringify(data.properties)}\n\n`;
|
|
95
|
-
} else if (result.type ===
|
|
251
|
+
} else if (result.type === "relation") {
|
|
96
252
|
resultText += `Relation: ${data.type} (${data.from} → ${data.to})\n`;
|
|
97
253
|
resultText += `Properties: ${JSON.stringify(data.properties)}\n\n`;
|
|
98
254
|
}
|
|
99
|
-
} else if (result.source ===
|
|
255
|
+
} else if (result.source === "vectors") {
|
|
100
256
|
const data = result.data;
|
|
101
257
|
resultText += `File: ${data.file} (line ${data.line})\n`;
|
|
102
258
|
resultText += `> ${data.content}\n\n`;
|
|
103
259
|
}
|
|
104
260
|
}
|
|
105
|
-
|
|
106
|
-
resultText +=
|
|
261
|
+
|
|
262
|
+
resultText += "---\n\n";
|
|
107
263
|
}
|
|
108
264
|
|
|
109
265
|
return {
|
|
110
266
|
content: [{ type: "text", text: resultText }],
|
|
111
|
-
details: { found: true, query, resultCount: results.length, sources: Object.keys(groupedResults) }
|
|
267
|
+
details: { found: true, query, resultCount: results.length, sources: Object.keys(groupedResults) },
|
|
112
268
|
};
|
|
113
|
-
|
|
114
269
|
} catch (error) {
|
|
115
270
|
return {
|
|
116
271
|
content: [{ type: "text", text: `Memory search failed: ${error}` }],
|
|
117
|
-
details: { error: String(error), found: false, query }
|
|
272
|
+
details: { error: String(error), found: false, query },
|
|
118
273
|
};
|
|
119
274
|
}
|
|
120
275
|
},
|
|
@@ -125,7 +280,7 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
125
280
|
*/
|
|
126
281
|
pi.registerTool({
|
|
127
282
|
name: "memory_write",
|
|
128
|
-
label: "Memory Write",
|
|
283
|
+
label: "Memory Write",
|
|
129
284
|
description: "Write content to a memory file. If no filename provided, uses today's date.",
|
|
130
285
|
parameters: Type.Object({
|
|
131
286
|
content: Type.String({ description: "Content to write to the memory file" }),
|
|
@@ -136,24 +291,29 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
136
291
|
const { content, file } = params as { content: string; file?: string };
|
|
137
292
|
|
|
138
293
|
try {
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
294
|
+
// Generate a unique per-fact filename when the caller did not pass
|
|
295
|
+
// one, so each memory_write becomes its own file instead of
|
|
296
|
+
// clobbering today's note. An explicit file name is still honored.
|
|
297
|
+
const targetName = file || buildFactFilename(content);
|
|
298
|
+
const finalContent = withFrontmatter(content, targetName);
|
|
299
|
+
|
|
300
|
+
// write() returns the name actually written (with a "-N" suffix if
|
|
301
|
+
// a same-name file already existed), so we never overwrite data.
|
|
302
|
+
const filename = sigmaMemory.notes.write(finalContent, targetName);
|
|
142
303
|
|
|
143
304
|
// Auto-index in vector store (non-blocking)
|
|
144
|
-
sigmaMemory.vectors.addDocument(filename,
|
|
305
|
+
sigmaMemory.vectors.addDocument(filename, finalContent).catch(() => {
|
|
145
306
|
// Vector indexing failed silently — notes still saved
|
|
146
307
|
});
|
|
147
|
-
|
|
308
|
+
|
|
148
309
|
return {
|
|
149
310
|
content: [{ type: "text", text: `Content written to ${filename} (indexed for vector search)` }],
|
|
150
|
-
details: { filename, contentLength: content.length, vectorIndexed: true }
|
|
311
|
+
details: { filename, contentLength: content.length, vectorIndexed: true },
|
|
151
312
|
};
|
|
152
|
-
|
|
153
313
|
} catch (error) {
|
|
154
314
|
return {
|
|
155
315
|
content: [{ type: "text", text: `Failed to write to memory: ${error}` }],
|
|
156
|
-
details: { error: String(error) }
|
|
316
|
+
details: { error: String(error) },
|
|
157
317
|
};
|
|
158
318
|
}
|
|
159
319
|
},
|
|
@@ -177,21 +337,34 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
177
337
|
if (!file) {
|
|
178
338
|
// List all available memory files
|
|
179
339
|
const files = sigmaMemory.notes.list();
|
|
180
|
-
|
|
340
|
+
|
|
181
341
|
if (files.length === 0) {
|
|
182
342
|
return {
|
|
183
343
|
content: [{ type: "text", text: "No memory files found." }],
|
|
184
|
-
details: { action: "list", fileCount: 0 }
|
|
344
|
+
details: { action: "list", fileCount: 0 },
|
|
185
345
|
};
|
|
186
346
|
}
|
|
187
347
|
|
|
188
348
|
const fileList = files
|
|
189
|
-
.map(
|
|
190
|
-
|
|
349
|
+
.map(
|
|
350
|
+
(f) => `- ${f.name} (${(f.size / 1024).toFixed(1)} KB, ${new Date(f.date).toLocaleDateString()})`,
|
|
351
|
+
)
|
|
352
|
+
.join("\n");
|
|
353
|
+
|
|
354
|
+
// Lead with the deterministic recall manifest (file + summary)
|
|
355
|
+
// so a no-arg memory_read surfaces what each note contains, not
|
|
356
|
+
// just file names. Falls back gracefully when empty.
|
|
357
|
+
const manifest = buildMemoryManifest(sigmaMemory);
|
|
358
|
+
const manifestSection = manifest ? `## Memory index (summaries)\n\n${manifest}\n\n` : "";
|
|
191
359
|
|
|
192
360
|
return {
|
|
193
|
-
content: [
|
|
194
|
-
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
type: "text",
|
|
364
|
+
text: `${manifestSection}## Available memory files (${files.length})\n\n${fileList}`,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
details: { action: "list", fileCount: files.length },
|
|
195
368
|
};
|
|
196
369
|
}
|
|
197
370
|
|
|
@@ -200,13 +373,12 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
200
373
|
|
|
201
374
|
return {
|
|
202
375
|
content: [{ type: "text", text: `**${file}:**\n\n${content}` }],
|
|
203
|
-
details: { action: "read", found: true, filename: file, contentLength: content.length }
|
|
376
|
+
details: { action: "read", found: true, filename: file, contentLength: content.length },
|
|
204
377
|
};
|
|
205
|
-
|
|
206
378
|
} catch (error) {
|
|
207
379
|
return {
|
|
208
380
|
content: [{ type: "text", text: `Failed to read memory: ${error}` }],
|
|
209
|
-
details: { error: String(error), action: "read", filename: file }
|
|
381
|
+
details: { error: String(error), action: "read", filename: file },
|
|
210
382
|
};
|
|
211
383
|
}
|
|
212
384
|
},
|
|
@@ -218,21 +390,32 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
218
390
|
pi.registerTool({
|
|
219
391
|
name: "ontology_add",
|
|
220
392
|
label: "Ontology Add",
|
|
221
|
-
description:
|
|
393
|
+
description:
|
|
394
|
+
"Add an entity or relation to the project knowledge graph. Entities represent things (projects, files, services, people). Relations connect them.",
|
|
222
395
|
promptGuidelines: [
|
|
223
396
|
"When discovering project architecture (services, databases, APIs), add entities and relations to the ontology.",
|
|
224
397
|
"When learning about how components connect, add relations (e.g. 'api-server' → 'uses' → 'postgres-db').",
|
|
225
398
|
],
|
|
226
399
|
parameters: Type.Object({
|
|
227
|
-
type: Type.Union([Type.Literal("entity"), Type.Literal("relation")], {
|
|
400
|
+
type: Type.Union([Type.Literal("entity"), Type.Literal("relation")], {
|
|
401
|
+
description: "What to add: 'entity' or 'relation'",
|
|
402
|
+
}),
|
|
228
403
|
// Entity fields
|
|
229
|
-
entityType: Type.Optional(
|
|
404
|
+
entityType: Type.Optional(
|
|
405
|
+
Type.String({ description: "Entity type (e.g. Project, Service, Database, File, Person, Tool)" }),
|
|
406
|
+
),
|
|
230
407
|
name: Type.Optional(Type.String({ description: "Entity name (e.g. 'my-api', 'postgres-db')" })),
|
|
231
|
-
properties: Type.Optional(
|
|
408
|
+
properties: Type.Optional(
|
|
409
|
+
Type.Record(Type.String(), Type.String(), {
|
|
410
|
+
description: "Key-value properties (e.g. {language: 'TypeScript', port: '3000'})",
|
|
411
|
+
}),
|
|
412
|
+
),
|
|
232
413
|
// Relation fields
|
|
233
414
|
from: Type.Optional(Type.String({ description: "Source entity ID" })),
|
|
234
415
|
to: Type.Optional(Type.String({ description: "Target entity ID" })),
|
|
235
|
-
relationType: Type.Optional(
|
|
416
|
+
relationType: Type.Optional(
|
|
417
|
+
Type.String({ description: "Relation type (e.g. 'uses', 'depends-on', 'deployed-on', 'created-by')" }),
|
|
418
|
+
),
|
|
236
419
|
}),
|
|
237
420
|
|
|
238
421
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
@@ -240,7 +423,10 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
240
423
|
try {
|
|
241
424
|
if (p.type === "entity") {
|
|
242
425
|
if (!p.entityType || !p.name) {
|
|
243
|
-
return {
|
|
426
|
+
return {
|
|
427
|
+
content: [{ type: "text", text: "Entity requires 'entityType' and 'name'" }],
|
|
428
|
+
isError: true,
|
|
429
|
+
};
|
|
244
430
|
}
|
|
245
431
|
const id = sigmaMemory.ontology.addEntity({
|
|
246
432
|
type: p.entityType,
|
|
@@ -253,31 +439,34 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
253
439
|
};
|
|
254
440
|
} else if (p.type === "relation") {
|
|
255
441
|
if (!p.from || !p.to || !p.relationType) {
|
|
256
|
-
return {
|
|
442
|
+
return {
|
|
443
|
+
content: [{ type: "text", text: "Relation requires 'from', 'to', and 'relationType'" }],
|
|
444
|
+
isError: true,
|
|
445
|
+
};
|
|
257
446
|
}
|
|
258
|
-
|
|
447
|
+
|
|
259
448
|
// Try finding source entity by ID first, then by name
|
|
260
449
|
let sourceEntity = sigmaMemory.ontology.findEntity({ id: p.from })[0];
|
|
261
450
|
if (!sourceEntity) {
|
|
262
451
|
// Try finding by name (case-insensitive)
|
|
263
452
|
const allEntities = sigmaMemory.ontology.findEntity({});
|
|
264
|
-
sourceEntity = allEntities.find(e => e.name.toLowerCase() === p.from.toLowerCase());
|
|
453
|
+
sourceEntity = allEntities.find((e) => e.name.toLowerCase() === p.from.toLowerCase());
|
|
265
454
|
if (!sourceEntity) {
|
|
266
455
|
return { content: [{ type: "text", text: `Source entity not found: ${p.from}` }], isError: true };
|
|
267
456
|
}
|
|
268
457
|
}
|
|
269
|
-
|
|
458
|
+
|
|
270
459
|
// Try finding target entity by ID first, then by name
|
|
271
460
|
let targetEntity = sigmaMemory.ontology.findEntity({ id: p.to })[0];
|
|
272
461
|
if (!targetEntity) {
|
|
273
462
|
// Try finding by name (case-insensitive)
|
|
274
463
|
const allEntities = sigmaMemory.ontology.findEntity({});
|
|
275
|
-
targetEntity = allEntities.find(e => e.name.toLowerCase() === p.to.toLowerCase());
|
|
464
|
+
targetEntity = allEntities.find((e) => e.name.toLowerCase() === p.to.toLowerCase());
|
|
276
465
|
if (!targetEntity) {
|
|
277
466
|
return { content: [{ type: "text", text: `Target entity not found: ${p.to}` }], isError: true };
|
|
278
467
|
}
|
|
279
468
|
}
|
|
280
|
-
|
|
469
|
+
|
|
281
470
|
const id = sigmaMemory.ontology.addRelation({
|
|
282
471
|
from: sourceEntity.id,
|
|
283
472
|
to: targetEntity.id,
|
|
@@ -285,7 +474,12 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
285
474
|
properties: p.properties || {},
|
|
286
475
|
});
|
|
287
476
|
return {
|
|
288
|
-
content: [
|
|
477
|
+
content: [
|
|
478
|
+
{
|
|
479
|
+
type: "text",
|
|
480
|
+
text: `Relation added: \`${sourceEntity.name}\` → **${p.relationType}** → \`${targetEntity.name}\` — ID: \`${id}\``,
|
|
481
|
+
},
|
|
482
|
+
],
|
|
289
483
|
details: { id, from: sourceEntity.id, to: targetEntity.id, type: p.relationType },
|
|
290
484
|
};
|
|
291
485
|
}
|
|
@@ -302,15 +496,22 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
302
496
|
pi.registerTool({
|
|
303
497
|
name: "ontology_query",
|
|
304
498
|
label: "Ontology Query",
|
|
305
|
-
description:
|
|
499
|
+
description:
|
|
500
|
+
"Query the project knowledge graph. Find entities by type/name, get relations, find paths between entities, or get stats.",
|
|
306
501
|
parameters: Type.Object({
|
|
307
|
-
action: Type.Union(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
502
|
+
action: Type.Union(
|
|
503
|
+
[
|
|
504
|
+
Type.Literal("find"),
|
|
505
|
+
Type.Literal("relations"),
|
|
506
|
+
Type.Literal("path"),
|
|
507
|
+
Type.Literal("stats"),
|
|
508
|
+
Type.Literal("graph"),
|
|
509
|
+
],
|
|
510
|
+
{
|
|
511
|
+
description:
|
|
512
|
+
"Query action: find (entities), relations (of entity), path (between entities), stats, graph (full export)",
|
|
513
|
+
},
|
|
514
|
+
),
|
|
314
515
|
entityType: Type.Optional(Type.String({ description: "Filter by entity type (for 'find' action)" })),
|
|
315
516
|
name: Type.Optional(Type.String({ description: "Filter by name (partial match, for 'find' action)" })),
|
|
316
517
|
entityId: Type.Optional(Type.String({ description: "Entity ID (for 'relations' action)" })),
|
|
@@ -325,21 +526,26 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
325
526
|
case "find": {
|
|
326
527
|
const results = sigmaMemory.ontology.findEntity({ type: p.entityType, name: p.name });
|
|
327
528
|
if (results.length === 0) return { content: [{ type: "text", text: "No entities found." }] };
|
|
328
|
-
const text = results
|
|
529
|
+
const text = results
|
|
530
|
+
.map((e) => `- **${e.name}** (${e.type}) ID:\`${e.id}\` ${JSON.stringify(e.properties)}`)
|
|
531
|
+
.join("\n");
|
|
329
532
|
return { content: [{ type: "text", text: `Found ${results.length} entities:\n${text}` }] };
|
|
330
533
|
}
|
|
331
534
|
case "relations": {
|
|
332
535
|
if (!p.entityId) return { content: [{ type: "text", text: "'entityId' required" }], isError: true };
|
|
333
536
|
const rels = sigmaMemory.ontology.findRelations(p.entityId);
|
|
334
537
|
if (rels.length === 0) return { content: [{ type: "text", text: "No relations found." }] };
|
|
335
|
-
const text = rels.map(r => `- \`${r.from}\` → **${r.type}** → \`${r.to}\``).join("\n");
|
|
538
|
+
const text = rels.map((r) => `- \`${r.from}\` → **${r.type}** → \`${r.to}\``).join("\n");
|
|
336
539
|
return { content: [{ type: "text", text: `Found ${rels.length} relations:\n${text}` }] };
|
|
337
540
|
}
|
|
338
541
|
case "path": {
|
|
339
|
-
if (!p.fromId || !p.toId)
|
|
542
|
+
if (!p.fromId || !p.toId)
|
|
543
|
+
return { content: [{ type: "text", text: "'fromId' and 'toId' required" }], isError: true };
|
|
340
544
|
const path = sigmaMemory.ontology.queryPath(p.fromId, p.toId);
|
|
341
545
|
if (!path) return { content: [{ type: "text", text: "No path found between these entities." }] };
|
|
342
|
-
const text = path
|
|
546
|
+
const text = path
|
|
547
|
+
.map((s) => `${s.entity.name}${s.relation ? ` → [${s.relation.type}]` : ""}`)
|
|
548
|
+
.join(" → ");
|
|
343
549
|
return { content: [{ type: "text", text: `Path: ${text}` }] };
|
|
344
550
|
}
|
|
345
551
|
case "stats": {
|
|
@@ -355,7 +561,10 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
355
561
|
return { content: [{ type: "text", text: JSON.stringify(graph, null, 2) }] };
|
|
356
562
|
}
|
|
357
563
|
default:
|
|
358
|
-
return {
|
|
564
|
+
return {
|
|
565
|
+
content: [{ type: "text", text: "Action must be: find, relations, path, stats, graph" }],
|
|
566
|
+
isError: true,
|
|
567
|
+
};
|
|
359
568
|
}
|
|
360
569
|
} catch (error) {
|
|
361
570
|
return { content: [{ type: "text", text: `Ontology query error: ${error}` }], isError: true };
|
|
@@ -375,37 +584,36 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
375
584
|
async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
|
|
376
585
|
try {
|
|
377
586
|
const status = await sigmaMemory.status();
|
|
378
|
-
|
|
587
|
+
|
|
379
588
|
let statusText = "# Memory Status\n\n";
|
|
380
|
-
|
|
589
|
+
|
|
381
590
|
// Notes status
|
|
382
591
|
statusText += `## Notes\n`;
|
|
383
592
|
statusText += `- Files: ${status.notes.count}\n`;
|
|
384
593
|
statusText += `- Total size: ${(status.notes.totalSize / 1024).toFixed(1)} KB\n`;
|
|
385
|
-
statusText += `- Last modified: ${status.notes.lastModified ? new Date(status.notes.lastModified).toLocaleString() :
|
|
386
|
-
|
|
594
|
+
statusText += `- Last modified: ${status.notes.lastModified ? new Date(status.notes.lastModified).toLocaleString() : "Never"}\n\n`;
|
|
595
|
+
|
|
387
596
|
// Ontology status
|
|
388
597
|
statusText += `## Ontology\n`;
|
|
389
598
|
statusText += `- Entities: ${status.ontology.entities}\n`;
|
|
390
599
|
statusText += `- Relations: ${status.ontology.relations}\n`;
|
|
391
600
|
statusText += `- Entities by type: ${JSON.stringify(status.ontology.entitiesByType)}\n`;
|
|
392
601
|
statusText += `- Relations by type: ${JSON.stringify(status.ontology.relationsByType)}\n\n`;
|
|
393
|
-
|
|
602
|
+
|
|
394
603
|
// Vector store status
|
|
395
604
|
statusText += `## Vector Search (embedded)\n`;
|
|
396
605
|
statusText += `- Documents: ${status.vectors.documentCount}\n`;
|
|
397
606
|
statusText += `- Chunks: ${status.vectors.chunkCount}\n`;
|
|
398
|
-
statusText += `- Last update: ${status.vectors.lastUpdate ||
|
|
607
|
+
statusText += `- Last update: ${status.vectors.lastUpdate || "Never"}\n`;
|
|
399
608
|
|
|
400
609
|
return {
|
|
401
610
|
content: [{ type: "text", text: statusText }],
|
|
402
|
-
details: { status }
|
|
611
|
+
details: { status },
|
|
403
612
|
};
|
|
404
|
-
|
|
405
613
|
} catch (error) {
|
|
406
614
|
return {
|
|
407
615
|
content: [{ type: "text", text: `Failed to get memory status: ${error}` }],
|
|
408
|
-
details: { error: String(error) }
|
|
616
|
+
details: { error: String(error) },
|
|
409
617
|
};
|
|
410
618
|
}
|
|
411
619
|
},
|
|
@@ -439,10 +647,19 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
439
647
|
// Neutralize angle brackets so user content cannot close the
|
|
440
648
|
// <system-reminder> block early and inject trusted instructions.
|
|
441
649
|
const safe = truncated.replace(/[<>]/g, (c) => (c === "<" ? "<" : ">")).replace(/"/g, '\\"');
|
|
650
|
+
|
|
651
|
+
// Deterministic recall index: a one-line-per-file manifest of memory
|
|
652
|
+
// notes so the model ALWAYS sees which facts exist without having to
|
|
653
|
+
// guess and call memory_search blindly. Built locally (no LLM).
|
|
654
|
+
const manifest = buildMemoryManifest(sigmaMemory);
|
|
655
|
+
const manifestBlock = manifest
|
|
656
|
+
? `\nMEMORY INDEX (existing saved notes, read with \`memory_read <file>\`):\n${manifest}\n`
|
|
657
|
+
: "";
|
|
658
|
+
|
|
442
659
|
const reminder = `<system-reminder>
|
|
443
660
|
You are about to respond to a new user message:
|
|
444
661
|
"${safe}"
|
|
445
|
-
|
|
662
|
+
${manifestBlock}
|
|
446
663
|
REMINDER (project rule, applies every turn):
|
|
447
664
|
1. Call \`memory_search\` FIRST with keywords from the user's intent. Recent
|
|
448
665
|
project context, prior decisions, and saved learnings are accessible
|
|
@@ -498,4 +715,4 @@ These two tool calls are not optional. Skipping them violates project rules.
|
|
|
498
715
|
// Non-critical, don't spam errors
|
|
499
716
|
}
|
|
500
717
|
});
|
|
501
|
-
}
|
|
718
|
+
}
|