@phi-code-admin/phi-code 0.77.2 → 0.78.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 +76 -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 +153 -0
- 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/memory.ts +171 -81
- package/extensions/phi/models.ts +91 -0
- package/extensions/phi/orchestrator-helpers.ts +62 -0
- package/extensions/phi/orchestrator.ts +193 -59
- package/extensions/phi/providers/context-window.ts +30 -2
- 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,52 @@
|
|
|
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
|
+
|
|
28
67
|
export default function memoryExtension(pi: ExtensionAPI) {
|
|
29
68
|
// Initialize sigma-memory with embedded vector store
|
|
30
69
|
const sigmaMemory = new SigmaMemory();
|
|
@@ -41,7 +80,8 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
41
80
|
name: "memory_search",
|
|
42
81
|
label: "Memory Search",
|
|
43
82
|
description: "Search for content in memory using unified search (notes + ontology + vector search)",
|
|
44
|
-
promptSnippet:
|
|
83
|
+
promptSnippet:
|
|
84
|
+
"Search project memory (notes, ontology, vector search). ALWAYS call before answering questions about prior work, decisions, or project context.",
|
|
45
85
|
promptGuidelines: [
|
|
46
86
|
"MANDATORY: Before starting ANY task, call memory_search with relevant keywords. This is not optional.",
|
|
47
87
|
"When starting work on a topic, search memory for existing notes and learnings.",
|
|
@@ -57,64 +97,72 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
57
97
|
|
|
58
98
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
59
99
|
const { query } = params as { query: string };
|
|
60
|
-
|
|
100
|
+
|
|
61
101
|
try {
|
|
62
102
|
const results = await sigmaMemory.search(query);
|
|
63
|
-
|
|
103
|
+
|
|
64
104
|
if (results.length === 0) {
|
|
65
105
|
return {
|
|
66
|
-
content: [
|
|
67
|
-
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: "text",
|
|
109
|
+
text: `No results found for "${query}". Use memory_write to create some memory files!`,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
details: { found: false, query, resultCount: 0 },
|
|
68
113
|
};
|
|
69
114
|
}
|
|
70
115
|
|
|
71
116
|
// Format results by source
|
|
72
117
|
let resultText = `Found ${results.length} results for "${query}":\n\n`;
|
|
73
|
-
|
|
74
|
-
const groupedResults = results.reduce(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
118
|
+
|
|
119
|
+
const groupedResults = results.reduce(
|
|
120
|
+
(groups, result) => {
|
|
121
|
+
if (!groups[result.source]) groups[result.source] = [];
|
|
122
|
+
groups[result.source].push(result);
|
|
123
|
+
return groups;
|
|
124
|
+
},
|
|
125
|
+
{} as Record<string, typeof results>,
|
|
126
|
+
);
|
|
79
127
|
|
|
80
128
|
for (const [source, sourceResults] of Object.entries(groupedResults)) {
|
|
81
129
|
resultText += `## ${source.toUpperCase()} (${sourceResults.length} results)\n\n`;
|
|
82
|
-
|
|
83
|
-
for (const result of sourceResults.slice(0, 5)) {
|
|
130
|
+
|
|
131
|
+
for (const result of sourceResults.slice(0, 5)) {
|
|
132
|
+
// Limit to 5 results per source
|
|
84
133
|
resultText += `**Score: ${result.score.toFixed(2)}** | Type: ${result.type}\n`;
|
|
85
|
-
|
|
86
|
-
if (result.source ===
|
|
134
|
+
|
|
135
|
+
if (result.source === "notes") {
|
|
87
136
|
const data = result.data;
|
|
88
137
|
resultText += `File: ${data.file} (line ${data.line})\n`;
|
|
89
138
|
resultText += `> ${data.content}\n\n`;
|
|
90
|
-
} else if (result.source ===
|
|
139
|
+
} else if (result.source === "ontology") {
|
|
91
140
|
const data = result.data;
|
|
92
|
-
if (result.type ===
|
|
141
|
+
if (result.type === "entity") {
|
|
93
142
|
resultText += `Entity: ${data.name} (${data.type})\n`;
|
|
94
143
|
resultText += `Properties: ${JSON.stringify(data.properties)}\n\n`;
|
|
95
|
-
} else if (result.type ===
|
|
144
|
+
} else if (result.type === "relation") {
|
|
96
145
|
resultText += `Relation: ${data.type} (${data.from} → ${data.to})\n`;
|
|
97
146
|
resultText += `Properties: ${JSON.stringify(data.properties)}\n\n`;
|
|
98
147
|
}
|
|
99
|
-
} else if (result.source ===
|
|
148
|
+
} else if (result.source === "vectors") {
|
|
100
149
|
const data = result.data;
|
|
101
150
|
resultText += `File: ${data.file} (line ${data.line})\n`;
|
|
102
151
|
resultText += `> ${data.content}\n\n`;
|
|
103
152
|
}
|
|
104
153
|
}
|
|
105
|
-
|
|
106
|
-
resultText +=
|
|
154
|
+
|
|
155
|
+
resultText += "---\n\n";
|
|
107
156
|
}
|
|
108
157
|
|
|
109
158
|
return {
|
|
110
159
|
content: [{ type: "text", text: resultText }],
|
|
111
|
-
details: { found: true, query, resultCount: results.length, sources: Object.keys(groupedResults) }
|
|
160
|
+
details: { found: true, query, resultCount: results.length, sources: Object.keys(groupedResults) },
|
|
112
161
|
};
|
|
113
|
-
|
|
114
162
|
} catch (error) {
|
|
115
163
|
return {
|
|
116
164
|
content: [{ type: "text", text: `Memory search failed: ${error}` }],
|
|
117
|
-
details: { error: String(error), found: false, query }
|
|
165
|
+
details: { error: String(error), found: false, query },
|
|
118
166
|
};
|
|
119
167
|
}
|
|
120
168
|
},
|
|
@@ -125,7 +173,7 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
125
173
|
*/
|
|
126
174
|
pi.registerTool({
|
|
127
175
|
name: "memory_write",
|
|
128
|
-
label: "Memory Write",
|
|
176
|
+
label: "Memory Write",
|
|
129
177
|
description: "Write content to a memory file. If no filename provided, uses today's date.",
|
|
130
178
|
parameters: Type.Object({
|
|
131
179
|
content: Type.String({ description: "Content to write to the memory file" }),
|
|
@@ -136,24 +184,29 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
136
184
|
const { content, file } = params as { content: string; file?: string };
|
|
137
185
|
|
|
138
186
|
try {
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
187
|
+
// Generate a unique per-fact filename when the caller did not pass
|
|
188
|
+
// one, so each memory_write becomes its own file instead of
|
|
189
|
+
// clobbering today's note. An explicit file name is still honored.
|
|
190
|
+
const targetName = file || buildFactFilename(content);
|
|
191
|
+
const finalContent = withFrontmatter(content, targetName);
|
|
192
|
+
|
|
193
|
+
// write() returns the name actually written (with a "-N" suffix if
|
|
194
|
+
// a same-name file already existed), so we never overwrite data.
|
|
195
|
+
const filename = sigmaMemory.notes.write(finalContent, targetName);
|
|
142
196
|
|
|
143
197
|
// Auto-index in vector store (non-blocking)
|
|
144
|
-
sigmaMemory.vectors.addDocument(filename,
|
|
198
|
+
sigmaMemory.vectors.addDocument(filename, finalContent).catch(() => {
|
|
145
199
|
// Vector indexing failed silently — notes still saved
|
|
146
200
|
});
|
|
147
|
-
|
|
201
|
+
|
|
148
202
|
return {
|
|
149
203
|
content: [{ type: "text", text: `Content written to ${filename} (indexed for vector search)` }],
|
|
150
|
-
details: { filename, contentLength: content.length, vectorIndexed: true }
|
|
204
|
+
details: { filename, contentLength: content.length, vectorIndexed: true },
|
|
151
205
|
};
|
|
152
|
-
|
|
153
206
|
} catch (error) {
|
|
154
207
|
return {
|
|
155
208
|
content: [{ type: "text", text: `Failed to write to memory: ${error}` }],
|
|
156
|
-
details: { error: String(error) }
|
|
209
|
+
details: { error: String(error) },
|
|
157
210
|
};
|
|
158
211
|
}
|
|
159
212
|
},
|
|
@@ -177,21 +230,23 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
177
230
|
if (!file) {
|
|
178
231
|
// List all available memory files
|
|
179
232
|
const files = sigmaMemory.notes.list();
|
|
180
|
-
|
|
233
|
+
|
|
181
234
|
if (files.length === 0) {
|
|
182
235
|
return {
|
|
183
236
|
content: [{ type: "text", text: "No memory files found." }],
|
|
184
|
-
details: { action: "list", fileCount: 0 }
|
|
237
|
+
details: { action: "list", fileCount: 0 },
|
|
185
238
|
};
|
|
186
239
|
}
|
|
187
240
|
|
|
188
241
|
const fileList = files
|
|
189
|
-
.map(
|
|
190
|
-
|
|
242
|
+
.map(
|
|
243
|
+
(f) => `- ${f.name} (${(f.size / 1024).toFixed(1)} KB, ${new Date(f.date).toLocaleDateString()})`,
|
|
244
|
+
)
|
|
245
|
+
.join("\n");
|
|
191
246
|
|
|
192
247
|
return {
|
|
193
248
|
content: [{ type: "text", text: `Available memory files (${files.length}):\n\n${fileList}` }],
|
|
194
|
-
details: { action: "list", fileCount: files.length }
|
|
249
|
+
details: { action: "list", fileCount: files.length },
|
|
195
250
|
};
|
|
196
251
|
}
|
|
197
252
|
|
|
@@ -200,13 +255,12 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
200
255
|
|
|
201
256
|
return {
|
|
202
257
|
content: [{ type: "text", text: `**${file}:**\n\n${content}` }],
|
|
203
|
-
details: { action: "read", found: true, filename: file, contentLength: content.length }
|
|
258
|
+
details: { action: "read", found: true, filename: file, contentLength: content.length },
|
|
204
259
|
};
|
|
205
|
-
|
|
206
260
|
} catch (error) {
|
|
207
261
|
return {
|
|
208
262
|
content: [{ type: "text", text: `Failed to read memory: ${error}` }],
|
|
209
|
-
details: { error: String(error), action: "read", filename: file }
|
|
263
|
+
details: { error: String(error), action: "read", filename: file },
|
|
210
264
|
};
|
|
211
265
|
}
|
|
212
266
|
},
|
|
@@ -218,21 +272,32 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
218
272
|
pi.registerTool({
|
|
219
273
|
name: "ontology_add",
|
|
220
274
|
label: "Ontology Add",
|
|
221
|
-
description:
|
|
275
|
+
description:
|
|
276
|
+
"Add an entity or relation to the project knowledge graph. Entities represent things (projects, files, services, people). Relations connect them.",
|
|
222
277
|
promptGuidelines: [
|
|
223
278
|
"When discovering project architecture (services, databases, APIs), add entities and relations to the ontology.",
|
|
224
279
|
"When learning about how components connect, add relations (e.g. 'api-server' → 'uses' → 'postgres-db').",
|
|
225
280
|
],
|
|
226
281
|
parameters: Type.Object({
|
|
227
|
-
type: Type.Union([Type.Literal("entity"), Type.Literal("relation")], {
|
|
282
|
+
type: Type.Union([Type.Literal("entity"), Type.Literal("relation")], {
|
|
283
|
+
description: "What to add: 'entity' or 'relation'",
|
|
284
|
+
}),
|
|
228
285
|
// Entity fields
|
|
229
|
-
entityType: Type.Optional(
|
|
286
|
+
entityType: Type.Optional(
|
|
287
|
+
Type.String({ description: "Entity type (e.g. Project, Service, Database, File, Person, Tool)" }),
|
|
288
|
+
),
|
|
230
289
|
name: Type.Optional(Type.String({ description: "Entity name (e.g. 'my-api', 'postgres-db')" })),
|
|
231
|
-
properties: Type.Optional(
|
|
290
|
+
properties: Type.Optional(
|
|
291
|
+
Type.Record(Type.String(), Type.String(), {
|
|
292
|
+
description: "Key-value properties (e.g. {language: 'TypeScript', port: '3000'})",
|
|
293
|
+
}),
|
|
294
|
+
),
|
|
232
295
|
// Relation fields
|
|
233
296
|
from: Type.Optional(Type.String({ description: "Source entity ID" })),
|
|
234
297
|
to: Type.Optional(Type.String({ description: "Target entity ID" })),
|
|
235
|
-
relationType: Type.Optional(
|
|
298
|
+
relationType: Type.Optional(
|
|
299
|
+
Type.String({ description: "Relation type (e.g. 'uses', 'depends-on', 'deployed-on', 'created-by')" }),
|
|
300
|
+
),
|
|
236
301
|
}),
|
|
237
302
|
|
|
238
303
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
@@ -240,7 +305,10 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
240
305
|
try {
|
|
241
306
|
if (p.type === "entity") {
|
|
242
307
|
if (!p.entityType || !p.name) {
|
|
243
|
-
return {
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text", text: "Entity requires 'entityType' and 'name'" }],
|
|
310
|
+
isError: true,
|
|
311
|
+
};
|
|
244
312
|
}
|
|
245
313
|
const id = sigmaMemory.ontology.addEntity({
|
|
246
314
|
type: p.entityType,
|
|
@@ -253,31 +321,34 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
253
321
|
};
|
|
254
322
|
} else if (p.type === "relation") {
|
|
255
323
|
if (!p.from || !p.to || !p.relationType) {
|
|
256
|
-
return {
|
|
324
|
+
return {
|
|
325
|
+
content: [{ type: "text", text: "Relation requires 'from', 'to', and 'relationType'" }],
|
|
326
|
+
isError: true,
|
|
327
|
+
};
|
|
257
328
|
}
|
|
258
|
-
|
|
329
|
+
|
|
259
330
|
// Try finding source entity by ID first, then by name
|
|
260
331
|
let sourceEntity = sigmaMemory.ontology.findEntity({ id: p.from })[0];
|
|
261
332
|
if (!sourceEntity) {
|
|
262
333
|
// Try finding by name (case-insensitive)
|
|
263
334
|
const allEntities = sigmaMemory.ontology.findEntity({});
|
|
264
|
-
sourceEntity = allEntities.find(e => e.name.toLowerCase() === p.from.toLowerCase());
|
|
335
|
+
sourceEntity = allEntities.find((e) => e.name.toLowerCase() === p.from.toLowerCase());
|
|
265
336
|
if (!sourceEntity) {
|
|
266
337
|
return { content: [{ type: "text", text: `Source entity not found: ${p.from}` }], isError: true };
|
|
267
338
|
}
|
|
268
339
|
}
|
|
269
|
-
|
|
340
|
+
|
|
270
341
|
// Try finding target entity by ID first, then by name
|
|
271
342
|
let targetEntity = sigmaMemory.ontology.findEntity({ id: p.to })[0];
|
|
272
343
|
if (!targetEntity) {
|
|
273
344
|
// Try finding by name (case-insensitive)
|
|
274
345
|
const allEntities = sigmaMemory.ontology.findEntity({});
|
|
275
|
-
targetEntity = allEntities.find(e => e.name.toLowerCase() === p.to.toLowerCase());
|
|
346
|
+
targetEntity = allEntities.find((e) => e.name.toLowerCase() === p.to.toLowerCase());
|
|
276
347
|
if (!targetEntity) {
|
|
277
348
|
return { content: [{ type: "text", text: `Target entity not found: ${p.to}` }], isError: true };
|
|
278
349
|
}
|
|
279
350
|
}
|
|
280
|
-
|
|
351
|
+
|
|
281
352
|
const id = sigmaMemory.ontology.addRelation({
|
|
282
353
|
from: sourceEntity.id,
|
|
283
354
|
to: targetEntity.id,
|
|
@@ -285,7 +356,12 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
285
356
|
properties: p.properties || {},
|
|
286
357
|
});
|
|
287
358
|
return {
|
|
288
|
-
content: [
|
|
359
|
+
content: [
|
|
360
|
+
{
|
|
361
|
+
type: "text",
|
|
362
|
+
text: `Relation added: \`${sourceEntity.name}\` → **${p.relationType}** → \`${targetEntity.name}\` — ID: \`${id}\``,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
289
365
|
details: { id, from: sourceEntity.id, to: targetEntity.id, type: p.relationType },
|
|
290
366
|
};
|
|
291
367
|
}
|
|
@@ -302,15 +378,22 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
302
378
|
pi.registerTool({
|
|
303
379
|
name: "ontology_query",
|
|
304
380
|
label: "Ontology Query",
|
|
305
|
-
description:
|
|
381
|
+
description:
|
|
382
|
+
"Query the project knowledge graph. Find entities by type/name, get relations, find paths between entities, or get stats.",
|
|
306
383
|
parameters: Type.Object({
|
|
307
|
-
action: Type.Union(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
384
|
+
action: Type.Union(
|
|
385
|
+
[
|
|
386
|
+
Type.Literal("find"),
|
|
387
|
+
Type.Literal("relations"),
|
|
388
|
+
Type.Literal("path"),
|
|
389
|
+
Type.Literal("stats"),
|
|
390
|
+
Type.Literal("graph"),
|
|
391
|
+
],
|
|
392
|
+
{
|
|
393
|
+
description:
|
|
394
|
+
"Query action: find (entities), relations (of entity), path (between entities), stats, graph (full export)",
|
|
395
|
+
},
|
|
396
|
+
),
|
|
314
397
|
entityType: Type.Optional(Type.String({ description: "Filter by entity type (for 'find' action)" })),
|
|
315
398
|
name: Type.Optional(Type.String({ description: "Filter by name (partial match, for 'find' action)" })),
|
|
316
399
|
entityId: Type.Optional(Type.String({ description: "Entity ID (for 'relations' action)" })),
|
|
@@ -325,21 +408,26 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
325
408
|
case "find": {
|
|
326
409
|
const results = sigmaMemory.ontology.findEntity({ type: p.entityType, name: p.name });
|
|
327
410
|
if (results.length === 0) return { content: [{ type: "text", text: "No entities found." }] };
|
|
328
|
-
const text = results
|
|
411
|
+
const text = results
|
|
412
|
+
.map((e) => `- **${e.name}** (${e.type}) ID:\`${e.id}\` ${JSON.stringify(e.properties)}`)
|
|
413
|
+
.join("\n");
|
|
329
414
|
return { content: [{ type: "text", text: `Found ${results.length} entities:\n${text}` }] };
|
|
330
415
|
}
|
|
331
416
|
case "relations": {
|
|
332
417
|
if (!p.entityId) return { content: [{ type: "text", text: "'entityId' required" }], isError: true };
|
|
333
418
|
const rels = sigmaMemory.ontology.findRelations(p.entityId);
|
|
334
419
|
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");
|
|
420
|
+
const text = rels.map((r) => `- \`${r.from}\` → **${r.type}** → \`${r.to}\``).join("\n");
|
|
336
421
|
return { content: [{ type: "text", text: `Found ${rels.length} relations:\n${text}` }] };
|
|
337
422
|
}
|
|
338
423
|
case "path": {
|
|
339
|
-
if (!p.fromId || !p.toId)
|
|
424
|
+
if (!p.fromId || !p.toId)
|
|
425
|
+
return { content: [{ type: "text", text: "'fromId' and 'toId' required" }], isError: true };
|
|
340
426
|
const path = sigmaMemory.ontology.queryPath(p.fromId, p.toId);
|
|
341
427
|
if (!path) return { content: [{ type: "text", text: "No path found between these entities." }] };
|
|
342
|
-
const text = path
|
|
428
|
+
const text = path
|
|
429
|
+
.map((s) => `${s.entity.name}${s.relation ? ` → [${s.relation.type}]` : ""}`)
|
|
430
|
+
.join(" → ");
|
|
343
431
|
return { content: [{ type: "text", text: `Path: ${text}` }] };
|
|
344
432
|
}
|
|
345
433
|
case "stats": {
|
|
@@ -355,7 +443,10 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
355
443
|
return { content: [{ type: "text", text: JSON.stringify(graph, null, 2) }] };
|
|
356
444
|
}
|
|
357
445
|
default:
|
|
358
|
-
return {
|
|
446
|
+
return {
|
|
447
|
+
content: [{ type: "text", text: "Action must be: find, relations, path, stats, graph" }],
|
|
448
|
+
isError: true,
|
|
449
|
+
};
|
|
359
450
|
}
|
|
360
451
|
} catch (error) {
|
|
361
452
|
return { content: [{ type: "text", text: `Ontology query error: ${error}` }], isError: true };
|
|
@@ -375,37 +466,36 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
375
466
|
async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
|
|
376
467
|
try {
|
|
377
468
|
const status = await sigmaMemory.status();
|
|
378
|
-
|
|
469
|
+
|
|
379
470
|
let statusText = "# Memory Status\n\n";
|
|
380
|
-
|
|
471
|
+
|
|
381
472
|
// Notes status
|
|
382
473
|
statusText += `## Notes\n`;
|
|
383
474
|
statusText += `- Files: ${status.notes.count}\n`;
|
|
384
475
|
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
|
-
|
|
476
|
+
statusText += `- Last modified: ${status.notes.lastModified ? new Date(status.notes.lastModified).toLocaleString() : "Never"}\n\n`;
|
|
477
|
+
|
|
387
478
|
// Ontology status
|
|
388
479
|
statusText += `## Ontology\n`;
|
|
389
480
|
statusText += `- Entities: ${status.ontology.entities}\n`;
|
|
390
481
|
statusText += `- Relations: ${status.ontology.relations}\n`;
|
|
391
482
|
statusText += `- Entities by type: ${JSON.stringify(status.ontology.entitiesByType)}\n`;
|
|
392
483
|
statusText += `- Relations by type: ${JSON.stringify(status.ontology.relationsByType)}\n\n`;
|
|
393
|
-
|
|
484
|
+
|
|
394
485
|
// Vector store status
|
|
395
486
|
statusText += `## Vector Search (embedded)\n`;
|
|
396
487
|
statusText += `- Documents: ${status.vectors.documentCount}\n`;
|
|
397
488
|
statusText += `- Chunks: ${status.vectors.chunkCount}\n`;
|
|
398
|
-
statusText += `- Last update: ${status.vectors.lastUpdate ||
|
|
489
|
+
statusText += `- Last update: ${status.vectors.lastUpdate || "Never"}\n`;
|
|
399
490
|
|
|
400
491
|
return {
|
|
401
492
|
content: [{ type: "text", text: statusText }],
|
|
402
|
-
details: { status }
|
|
493
|
+
details: { status },
|
|
403
494
|
};
|
|
404
|
-
|
|
405
495
|
} catch (error) {
|
|
406
496
|
return {
|
|
407
497
|
content: [{ type: "text", text: `Failed to get memory status: ${error}` }],
|
|
408
|
-
details: { error: String(error) }
|
|
498
|
+
details: { error: String(error) },
|
|
409
499
|
};
|
|
410
500
|
}
|
|
411
501
|
},
|
|
@@ -498,4 +588,4 @@ These two tool calls are not optional. Skipping them violates project rules.
|
|
|
498
588
|
// Non-critical, don't spam errors
|
|
499
589
|
}
|
|
500
590
|
});
|
|
501
|
-
}
|
|
591
|
+
}
|
package/extensions/phi/models.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
buildOpenCodeGoProviderConfig,
|
|
22
22
|
getOpenCodeGoModels,
|
|
23
23
|
} from "./providers/opencode-go.js";
|
|
24
|
+
import { formatWindow, inferContextWindow, parseContextWindow } from "./providers/context-window.js";
|
|
24
25
|
import { fetchLiveModels, peekCache, resetLiveModelsCache, toPersistedModel } from "./providers/live-models.js";
|
|
25
26
|
|
|
26
27
|
const PROVIDER_DISPLAY: Record<string, string> = {
|
|
@@ -190,6 +191,96 @@ export default function modelsExtension(pi: ExtensionAPI) {
|
|
|
190
191
|
},
|
|
191
192
|
});
|
|
192
193
|
|
|
194
|
+
pi.registerCommand("context", {
|
|
195
|
+
description:
|
|
196
|
+
"Show or set the active model's context window (e.g. `/context 256k`, `/context 1M`, `/context auto`). Drives when the conversation auto-compacts.",
|
|
197
|
+
handler: async (args, ctx) => {
|
|
198
|
+
const model = ctx.model;
|
|
199
|
+
if (!model) {
|
|
200
|
+
ctx.ui.notify("No active model. Select one with `/model` first.", "warning");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const provider = model.provider;
|
|
204
|
+
const modelId = model.id;
|
|
205
|
+
const arg = args.trim();
|
|
206
|
+
|
|
207
|
+
const readOverrideWindow = (): number | undefined => {
|
|
208
|
+
const overrides = store.getProvider(provider)?.modelOverrides as
|
|
209
|
+
| Record<string, { contextWindow?: number }>
|
|
210
|
+
| undefined;
|
|
211
|
+
return overrides?.[modelId]?.contextWindow;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const writeOverrides = (overrides: Record<string, unknown>): void => {
|
|
215
|
+
const stored = store.getProvider(provider) ?? {};
|
|
216
|
+
watcher.muteForWrite("models_json_changed");
|
|
217
|
+
store.setKey(provider, stored.apiKey ?? "local", { modelOverrides: overrides });
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
if (arg === "") {
|
|
222
|
+
const source = readOverrideWindow() !== undefined ? "manual override" : "provider / inferred";
|
|
223
|
+
ctx.ui.notify(
|
|
224
|
+
`**${modelId}** (\`${provider}\`) context window: \`${formatWindow(model.contextWindow)}\` (${source}).\n` +
|
|
225
|
+
"Set the real value with `/context 256k`, `/context 1M`, or `/context 200000`. " +
|
|
226
|
+
"Reset to the detected value with `/context auto`.\n" +
|
|
227
|
+
"This is what determines when the conversation auto-compacts.",
|
|
228
|
+
"info",
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (arg.toLowerCase() === "auto" || arg.toLowerCase() === "reset") {
|
|
234
|
+
const stored = store.getProvider(provider) ?? {};
|
|
235
|
+
const overrides = { ...((stored.modelOverrides as Record<string, unknown>) ?? {}) };
|
|
236
|
+
const entry = overrides[modelId];
|
|
237
|
+
if (entry && typeof entry === "object") {
|
|
238
|
+
const next = { ...(entry as Record<string, unknown>) };
|
|
239
|
+
delete next.contextWindow;
|
|
240
|
+
if (Object.keys(next).length === 0) delete overrides[modelId];
|
|
241
|
+
else overrides[modelId] = next;
|
|
242
|
+
}
|
|
243
|
+
writeOverrides(overrides);
|
|
244
|
+
|
|
245
|
+
// Revert the active model to the persisted/inferred window.
|
|
246
|
+
const persistedModels = (store.getProvider(provider)?.models as
|
|
247
|
+
| Array<{ id?: string; contextWindow?: number }>
|
|
248
|
+
| undefined) ?? [];
|
|
249
|
+
const persisted = persistedModels.find((m) => m?.id === modelId)?.contextWindow;
|
|
250
|
+
const reverted = persisted && persisted > 0 ? persisted : inferContextWindow(modelId, undefined, provider);
|
|
251
|
+
await pi.setModel({ ...model, contextWindow: reverted });
|
|
252
|
+
ctx.ui.notify(`Cleared context override for **${modelId}**. Reverted to \`${formatWindow(reverted)}\`.`, "info");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const value = parseContextWindow(arg);
|
|
257
|
+
if (!value) {
|
|
258
|
+
ctx.ui.notify("Invalid value. Use e.g. `256k`, `1M`, or `200000`.", "warning");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Immediate effect: the footer and auto-compaction use the new window right away.
|
|
263
|
+
await pi.setModel({ ...model, contextWindow: value });
|
|
264
|
+
|
|
265
|
+
// Persist as a per-model override so it survives restarts and the background
|
|
266
|
+
// refresh (which rewrites `models` but leaves `modelOverrides` untouched).
|
|
267
|
+
const stored = store.getProvider(provider) ?? {};
|
|
268
|
+
const overrides = { ...((stored.modelOverrides as Record<string, unknown>) ?? {}) };
|
|
269
|
+
const existing = (overrides[modelId] as Record<string, unknown> | undefined) ?? {};
|
|
270
|
+
overrides[modelId] = { ...existing, contextWindow: value };
|
|
271
|
+
writeOverrides(overrides);
|
|
272
|
+
|
|
273
|
+
ctx.ui.notify(
|
|
274
|
+
`Context window for **${modelId}** set to \`${formatWindow(value)}\` (saved). ` +
|
|
275
|
+
`Auto-compaction now triggers near ${formatWindow(value)}.`,
|
|
276
|
+
"info",
|
|
277
|
+
);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
ctx.ui.notify(`/context error: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
193
284
|
async function listCommand(target: string | undefined, ctx: { ui: { notify: (m: string, t?: "info" | "warning" | "error") => void } }): Promise<void> {
|
|
194
285
|
const providers = target ? [target] : store.listProviders();
|
|
195
286
|
if (providers.length === 0) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the /plan orchestrator. Kept separate from orchestrator.ts so
|
|
3
|
+
* they can be unit-tested (orchestrator.ts itself wires the Pi extension runtime).
|
|
4
|
+
*
|
|
5
|
+
* Everything here is text/regex only: the orchestration pipeline is driven by
|
|
6
|
+
* canonical text contracts in the .phi/plans/*.md handoff files, never by a
|
|
7
|
+
* model's structured-output (the upstream proxy does not guarantee valid JSON).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type PhaseVerdict = "PASS" | "FAIL" | "BLOCKED" | "SKIP";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse the canonical "VERDICT: PASS|FAIL|BLOCKED|SKIP" line a phase writes at the
|
|
14
|
+
* top of its report. Tolerant of leading markdown hashes and surrounding markup.
|
|
15
|
+
* Returns the first verdict found, or null when none is present.
|
|
16
|
+
*/
|
|
17
|
+
export function parsePhaseVerdict(content: string): PhaseVerdict | null {
|
|
18
|
+
if (!content) return null;
|
|
19
|
+
const m = content.match(/^\s{0,3}#{0,4}\s*\**\s*VERDICT\s*\**\s*:?\s*\**\s*(PASS|FAIL|BLOCKED|SKIP)\b/im);
|
|
20
|
+
return m ? (m[1].toUpperCase() as PhaseVerdict) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract the body of a markdown section by heading name (e.g. "BLOCKING",
|
|
25
|
+
* "HANDOFF"), up to the next heading or end of file. Returns "" if absent.
|
|
26
|
+
*/
|
|
27
|
+
export function extractSection(content: string, heading: string): string {
|
|
28
|
+
if (!content) return "";
|
|
29
|
+
const re = new RegExp(`#{1,6}\\s*\\**\\s*${heading}\\b[^\\n]*\\n([\\s\\S]*?)(?:\\n#{1,6}\\s|$)`, "i");
|
|
30
|
+
const m = content.match(re);
|
|
31
|
+
return m ? m[1].trim() : "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function extractBlockingFindings(content: string): string {
|
|
35
|
+
return extractSection(content, "BLOCKING");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function extractHandoff(content: string): string {
|
|
39
|
+
return extractSection(content, "HANDOFF");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detect a TRANSIENT provider/proxy failure in a phase's messages (timeout, 5xx,
|
|
44
|
+
* 429, connection reset, broken JSON tool call) that warrants a one-shot retry on
|
|
45
|
+
* a fallback model. A genuine 401 auth failure is NOT transient (handled as fatal
|
|
46
|
+
* by the caller) and is explicitly excluded.
|
|
47
|
+
*/
|
|
48
|
+
export function isTransientError(messages: Array<{ content?: unknown }>): boolean {
|
|
49
|
+
for (const msg of messages || []) {
|
|
50
|
+
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
|
|
51
|
+
if (content.includes("401")) continue;
|
|
52
|
+
if (/\b(429|500|502|503|504)\b/.test(content)) return true;
|
|
53
|
+
if (
|
|
54
|
+
/timed?\s?out|timeout|connection reset|ECONNRESET|ETIMEDOUT|socket hang ?up|overloaded|rate.?limit(ed)?|too many requests|invalid json|failed to parse|stream (error|interrupted|closed)|service unavailable|bad gateway|gateway timeout/i.test(
|
|
55
|
+
content,
|
|
56
|
+
)
|
|
57
|
+
) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|