pi-hermes-memory 0.1.0 → 0.2.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 +205 -78
- package/docs/0.2/PLAN.md +290 -0
- package/docs/0.2/TASKS.md +134 -0
- package/docs/0.2/TEST-PLAN.md +216 -0
- package/docs/ROADMAP.md +245 -135
- package/package.json +6 -3
- package/src/config.ts +7 -0
- package/src/constants.ts +73 -1
- package/src/handlers/auto-consolidate.ts +94 -0
- package/src/handlers/background-review.ts +27 -1
- package/src/handlers/correction-detector.ts +143 -0
- package/src/handlers/skill-auto-trigger.ts +108 -0
- package/src/handlers/skills-command.ts +38 -0
- package/src/index.ts +46 -8
- package/src/store/memory-store.ts +25 -2
- package/src/store/skill-store.ts +292 -0
- package/src/tools/memory-tool.ts +1 -1
- package/src/tools/skill-tool.ts +142 -0
- package/src/types.ts +40 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-consolidation — when memory hits capacity, trigger automatic
|
|
3
|
+
* consolidation instead of returning an error.
|
|
4
|
+
*
|
|
5
|
+
* Uses pi.exec() to spawn a one-shot consolidation process.
|
|
6
|
+
* The child process modifies files on disk, so the parent MUST reload
|
|
7
|
+
* from disk after consolidation completes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { MemoryStore } from "../store/memory-store.js";
|
|
12
|
+
import { CONSOLIDATION_PROMPT, ENTRY_DELIMITER } from "../constants.js";
|
|
13
|
+
import type { ConsolidationResult } from "../types.js";
|
|
14
|
+
|
|
15
|
+
export async function triggerConsolidation(
|
|
16
|
+
pi: ExtensionAPI,
|
|
17
|
+
store: MemoryStore,
|
|
18
|
+
target: "memory" | "user",
|
|
19
|
+
signal?: AbortSignal,
|
|
20
|
+
): Promise<ConsolidationResult> {
|
|
21
|
+
const entries =
|
|
22
|
+
target === "memory" ? store.getMemoryEntries() : store.getUserEntries();
|
|
23
|
+
const currentContent = entries.join(ENTRY_DELIMITER);
|
|
24
|
+
|
|
25
|
+
const prompt = [
|
|
26
|
+
CONSOLIDATION_PROMPT,
|
|
27
|
+
"",
|
|
28
|
+
`--- Current ${target === "user" ? "User Profile" : "Memory"} Entries ---`,
|
|
29
|
+
currentContent || "(empty)",
|
|
30
|
+
"",
|
|
31
|
+
`Use the memory tool to consolidate. Target: '${target}'`,
|
|
32
|
+
].join("\n");
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await pi.exec("pi", ["-p", "--no-session", prompt], {
|
|
36
|
+
signal,
|
|
37
|
+
timeout: 60000,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (result.code === 0) {
|
|
41
|
+
return { consolidated: true };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
consolidated: false,
|
|
45
|
+
error: `Consolidation process exited with code ${result.code}: ${result.stderr?.slice(0, 200) || "unknown error"}`,
|
|
46
|
+
};
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return {
|
|
49
|
+
consolidated: false,
|
|
50
|
+
error: `Consolidation failed: ${String(err).slice(0, 200)}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Register the /memory-consolidate command for manual consolidation.
|
|
57
|
+
*/
|
|
58
|
+
export function registerConsolidateCommand(
|
|
59
|
+
pi: ExtensionAPI,
|
|
60
|
+
store: MemoryStore,
|
|
61
|
+
): void {
|
|
62
|
+
pi.registerCommand("memory-consolidate", {
|
|
63
|
+
description: "Manually trigger memory consolidation to free up space",
|
|
64
|
+
handler: async (_args, ctx) => {
|
|
65
|
+
const results: string[] = [];
|
|
66
|
+
|
|
67
|
+
for (const target of ["memory", "user"] as const) {
|
|
68
|
+
const entries =
|
|
69
|
+
target === "memory"
|
|
70
|
+
? store.getMemoryEntries()
|
|
71
|
+
: store.getUserEntries();
|
|
72
|
+
|
|
73
|
+
if (entries.length === 0) {
|
|
74
|
+
results.push(`${target}: (empty, nothing to consolidate)`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = await triggerConsolidation(pi, store, target, ctx.signal);
|
|
79
|
+
|
|
80
|
+
if (result.consolidated) {
|
|
81
|
+
await store.loadFromDisk();
|
|
82
|
+
results.push(`${target}: ✅ consolidated`);
|
|
83
|
+
} else {
|
|
84
|
+
results.push(`${target}: ❌ ${result.error}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
ctx.ui.notify(
|
|
89
|
+
`\n 🔄 Memory Consolidation\n ${"─".repeat(30)}\n${results.map((r) => ` ${r}`).join("\n")}`,
|
|
90
|
+
"info",
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -19,6 +19,7 @@ export function setupBackgroundReview(
|
|
|
19
19
|
config: MemoryConfig,
|
|
20
20
|
): void {
|
|
21
21
|
let turnsSinceReview = 0;
|
|
22
|
+
let toolCallsSinceReview = 0;
|
|
22
23
|
let userTurnCount = 0;
|
|
23
24
|
let reviewInProgress = false;
|
|
24
25
|
|
|
@@ -33,10 +34,35 @@ export function setupBackgroundReview(
|
|
|
33
34
|
|
|
34
35
|
if (!config.reviewEnabled) return;
|
|
35
36
|
if (reviewInProgress) return;
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
// Count tool-use entries from the branch for tool-call-aware nudge
|
|
39
|
+
try {
|
|
40
|
+
const branch = ctx.sessionManager.getBranch();
|
|
41
|
+
for (const entry of branch) {
|
|
42
|
+
if (entry.type === "message" && entry.message?.role === "assistant") {
|
|
43
|
+
const content = entry.message?.content;
|
|
44
|
+
if (Array.isArray(content)) {
|
|
45
|
+
for (const block of content) {
|
|
46
|
+
if (block && typeof block === "object" && block.type === "toolCall") {
|
|
47
|
+
toolCallsSinceReview++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// If we can't count tool calls, fall back to turn-based only
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Trigger on EITHER turn count OR tool call count
|
|
58
|
+
const turnThresholdMet = turnsSinceReview >= config.nudgeInterval;
|
|
59
|
+
const toolCallThresholdMet = toolCallsSinceReview >= config.nudgeToolCalls;
|
|
60
|
+
|
|
61
|
+
if (!turnThresholdMet && !toolCallThresholdMet) return;
|
|
37
62
|
if (userTurnCount < 3) return;
|
|
38
63
|
|
|
39
64
|
turnsSinceReview = 0;
|
|
65
|
+
toolCallsSinceReview = 0;
|
|
40
66
|
reviewInProgress = true;
|
|
41
67
|
|
|
42
68
|
try {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correction detection — detects user corrections in real-time and triggers
|
|
3
|
+
* an immediate memory save instead of waiting for the next nudge interval.
|
|
4
|
+
*
|
|
5
|
+
* Uses a two-pass filter:
|
|
6
|
+
* - Strong patterns: always trigger (high confidence)
|
|
7
|
+
* - Weak patterns: only trigger if followed by a directive clause
|
|
8
|
+
* - Negative patterns: suppress even if a positive pattern matched
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { MemoryStore } from "../store/memory-store.js";
|
|
13
|
+
import {
|
|
14
|
+
CORRECTION_SAVE_PROMPT,
|
|
15
|
+
CORRECTION_STRONG_PATTERNS,
|
|
16
|
+
CORRECTION_WEAK_PATTERNS,
|
|
17
|
+
CORRECTION_NEGATIVE_PATTERNS,
|
|
18
|
+
ENTRY_DELIMITER,
|
|
19
|
+
} from "../constants.js";
|
|
20
|
+
import type { MemoryConfig } from "../types.js";
|
|
21
|
+
import { getMessageText } from "../types.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if a user message is a correction using the two-pass filter.
|
|
25
|
+
* Returns true if the message should trigger an immediate save.
|
|
26
|
+
*/
|
|
27
|
+
export function isCorrection(text: string): boolean {
|
|
28
|
+
// Check negative patterns first — suppress even if positive matches
|
|
29
|
+
for (const pattern of CORRECTION_NEGATIVE_PATTERNS) {
|
|
30
|
+
if (pattern.test(text)) return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check strong patterns — always trigger
|
|
34
|
+
for (const pattern of CORRECTION_STRONG_PATTERNS) {
|
|
35
|
+
if (pattern.test(text)) return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check weak patterns — only trigger if followed by a directive clause
|
|
39
|
+
for (const pattern of CORRECTION_WEAK_PATTERNS) {
|
|
40
|
+
if (pattern.test(text)) {
|
|
41
|
+
// Look for a directive after the weak pattern match
|
|
42
|
+
// Directive = a verb or "the/that/this" in the remainder of the text
|
|
43
|
+
const match = pattern.exec(text);
|
|
44
|
+
if (match && match.index === 0) {
|
|
45
|
+
const remainder = text.slice(match[0].length).trim();
|
|
46
|
+
// Simple heuristic: remainder contains something directive-ish
|
|
47
|
+
if (/\b(use|don'?t|do|try|make|run|install|add|remove|delete|change|fix|put|set|write|go|stop|start|the|that|this|it)\b/i.test(remainder)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function setupCorrectionDetector(
|
|
58
|
+
pi: ExtensionAPI,
|
|
59
|
+
store: MemoryStore,
|
|
60
|
+
config: MemoryConfig,
|
|
61
|
+
): void {
|
|
62
|
+
if (!config.correctionDetection) return;
|
|
63
|
+
|
|
64
|
+
let pendingCorrection = false;
|
|
65
|
+
let turnsSinceLastCorrection = 3; // Start at threshold so first correction can fire immediately
|
|
66
|
+
let correctionInProgress = false;
|
|
67
|
+
|
|
68
|
+
// Flag on message_end (user role)
|
|
69
|
+
pi.on("message_end", async (event, _ctx) => {
|
|
70
|
+
if (event.message.role !== "user") return;
|
|
71
|
+
const text = getMessageText(event.message);
|
|
72
|
+
if (!text) return;
|
|
73
|
+
if (isCorrection(text)) {
|
|
74
|
+
pendingCorrection = true;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Trigger on turn_end (we need full context: user correction + what agent said)
|
|
79
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
80
|
+
if (!pendingCorrection) {
|
|
81
|
+
turnsSinceLastCorrection++;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
pendingCorrection = false;
|
|
85
|
+
|
|
86
|
+
// Rate limit: max 1 correction save per 3 turns
|
|
87
|
+
if (turnsSinceLastCorrection < 3) return;
|
|
88
|
+
if (correctionInProgress) return;
|
|
89
|
+
|
|
90
|
+
turnsSinceLastCorrection = 0;
|
|
91
|
+
correctionInProgress = true;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Build conversation snapshot
|
|
95
|
+
const entries = ctx.sessionManager.getBranch();
|
|
96
|
+
const parts: string[] = [];
|
|
97
|
+
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (entry.type !== "message") continue;
|
|
100
|
+
const msg = entry.message;
|
|
101
|
+
const text = getMessageText(msg);
|
|
102
|
+
if (!text) continue;
|
|
103
|
+
const prefix = msg.role === "user" ? "[USER]" : "[ASSISTANT]";
|
|
104
|
+
parts.push(`${prefix}: ${text}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Only include last few exchanges (correction context is recent)
|
|
108
|
+
const recentParts = parts.slice(-6);
|
|
109
|
+
|
|
110
|
+
const currentMemory = store.getMemoryEntries().join(ENTRY_DELIMITER);
|
|
111
|
+
const currentUser = store.getUserEntries().join(ENTRY_DELIMITER);
|
|
112
|
+
|
|
113
|
+
const prompt = [
|
|
114
|
+
CORRECTION_SAVE_PROMPT,
|
|
115
|
+
"",
|
|
116
|
+
"--- Current Memory ---",
|
|
117
|
+
currentMemory || "(empty)",
|
|
118
|
+
"",
|
|
119
|
+
"--- Current User Profile ---",
|
|
120
|
+
currentUser || "(empty)",
|
|
121
|
+
"",
|
|
122
|
+
"--- Recent Conversation ---",
|
|
123
|
+
recentParts.join("\n\n"),
|
|
124
|
+
].join("\n");
|
|
125
|
+
|
|
126
|
+
const result = await pi.exec("pi", ["-p", "--no-session", prompt], {
|
|
127
|
+
signal: ctx.signal,
|
|
128
|
+
timeout: 30000,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (result.code === 0 && result.stdout) {
|
|
132
|
+
const output = result.stdout.trim();
|
|
133
|
+
if (output && !output.toLowerCase().includes("nothing to save")) {
|
|
134
|
+
ctx.ui.notify("🔧 Correction detected — memory updated", "info");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Best-effort — don't block the session
|
|
139
|
+
} finally {
|
|
140
|
+
correctionInProgress = false;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill auto-trigger — after complex tasks (8+ tool calls, 2+ distinct tool types),
|
|
3
|
+
* trigger automatic skill extraction via pi.exec().
|
|
4
|
+
*
|
|
5
|
+
* This implements Hermes' "self-evaluation checkpoint" pattern.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { MemoryStore } from "../store/memory-store.js";
|
|
10
|
+
import { SkillStore } from "../store/skill-store.js";
|
|
11
|
+
import { COMBINED_REVIEW_PROMPT, DEFAULT_SKILL_TRIGGER_TOOL_CALLS, ENTRY_DELIMITER } from "../constants.js";
|
|
12
|
+
import type { MemoryConfig } from "../types.js";
|
|
13
|
+
import { getMessageText } from "../types.js";
|
|
14
|
+
|
|
15
|
+
export function setupSkillAutoTrigger(
|
|
16
|
+
pi: ExtensionAPI,
|
|
17
|
+
store: MemoryStore,
|
|
18
|
+
skillStore: SkillStore,
|
|
19
|
+
config: MemoryConfig,
|
|
20
|
+
): void {
|
|
21
|
+
let triggeredThisSession = false;
|
|
22
|
+
|
|
23
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
24
|
+
if (triggeredThisSession) return;
|
|
25
|
+
|
|
26
|
+
// Count tool-use entries from this turn's branch
|
|
27
|
+
let toolCallCount = 0;
|
|
28
|
+
const toolTypes = new Set<string>();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const branch = ctx.sessionManager.getBranch();
|
|
32
|
+
for (const entry of branch) {
|
|
33
|
+
if (entry.type === "message" && entry.message?.role === "assistant") {
|
|
34
|
+
const content = entry.message?.content;
|
|
35
|
+
if (Array.isArray(content)) {
|
|
36
|
+
for (const block of content) {
|
|
37
|
+
if (block && typeof block === "object" && block.type === "toolCall") {
|
|
38
|
+
toolCallCount++;
|
|
39
|
+
if ((block as { name?: string }).name) toolTypes.add((block as { name: string }).name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Require 8+ tool calls AND 2+ distinct tool types
|
|
50
|
+
if (toolCallCount < DEFAULT_SKILL_TRIGGER_TOOL_CALLS) return;
|
|
51
|
+
if (toolTypes.size < 2) return;
|
|
52
|
+
|
|
53
|
+
triggeredThisSession = true;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Build conversation context
|
|
57
|
+
const branch = ctx.sessionManager.getBranch();
|
|
58
|
+
const parts: string[] = [];
|
|
59
|
+
|
|
60
|
+
for (const entry of branch) {
|
|
61
|
+
if (entry.type !== "message") continue;
|
|
62
|
+
const msg = entry.message;
|
|
63
|
+
const text = getMessageText(msg);
|
|
64
|
+
if (!text) continue;
|
|
65
|
+
const prefix = msg.role === "user" ? "[USER]" : "[ASSISTANT]";
|
|
66
|
+
parts.push(`${prefix}: ${text}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Only include recent context
|
|
70
|
+
const recentParts = parts.slice(-10);
|
|
71
|
+
|
|
72
|
+
const currentMemory = store.getMemoryEntries().join(ENTRY_DELIMITER);
|
|
73
|
+
const skillIndex = await skillStore.loadIndex();
|
|
74
|
+
const skillSummary = skillIndex.map((s) => `${s.fileName}: ${s.name} - ${s.description}`).join("\n");
|
|
75
|
+
|
|
76
|
+
const prompt = [
|
|
77
|
+
"This was a complex task that required multiple tool calls. Extract any reusable procedures as skills.",
|
|
78
|
+
"",
|
|
79
|
+
"--- Existing Skills ---",
|
|
80
|
+
skillSummary || "(none)",
|
|
81
|
+
"",
|
|
82
|
+
"--- Current Memory ---",
|
|
83
|
+
currentMemory || "(empty)",
|
|
84
|
+
"",
|
|
85
|
+
"--- Recent Conversation ---",
|
|
86
|
+
recentParts.join("\n\n"),
|
|
87
|
+
"",
|
|
88
|
+
"If a skill should be created, use the skill tool with action 'create'.",
|
|
89
|
+
"If a related skill already exists, use 'patch' to update it.",
|
|
90
|
+
"If nothing reusable happened, say 'Nothing to extract.' and stop.",
|
|
91
|
+
].join("\n");
|
|
92
|
+
|
|
93
|
+
const result = await pi.exec("pi", ["-p", "--no-session", prompt], {
|
|
94
|
+
signal: ctx.signal,
|
|
95
|
+
timeout: 60000,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (result.code === 0 && result.stdout) {
|
|
99
|
+
const output = result.stdout.trim();
|
|
100
|
+
if (output && !output.toLowerCase().includes("nothing to extract")) {
|
|
101
|
+
ctx.ui.notify("🧠 Complex task detected — skill extracted", "info");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Best-effort — don't block
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills command — /memory-skills lists all agent-created skills.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { SkillStore } from "../store/skill-store.js";
|
|
7
|
+
|
|
8
|
+
export function registerSkillsCommand(pi: ExtensionAPI, store: SkillStore): void {
|
|
9
|
+
pi.registerCommand("memory-skills", {
|
|
10
|
+
description: "List all agent-created skills (procedural memory)",
|
|
11
|
+
handler: async (_args, ctx) => {
|
|
12
|
+
const skills = await store.loadIndex();
|
|
13
|
+
|
|
14
|
+
const lines: string[] = [];
|
|
15
|
+
lines.push("");
|
|
16
|
+
lines.push(" ╔══════════════════════════════════════════════╗");
|
|
17
|
+
lines.push(" ║ 🧠 Procedural Skills ║");
|
|
18
|
+
lines.push(" ╚══════════════════════════════════════════════╝");
|
|
19
|
+
lines.push("");
|
|
20
|
+
|
|
21
|
+
if (skills.length === 0) {
|
|
22
|
+
lines.push(" (no skills created yet)");
|
|
23
|
+
lines.push("");
|
|
24
|
+
lines.push(" Skills are auto-created after complex tasks,");
|
|
25
|
+
lines.push(" or you can ask the agent to create one.");
|
|
26
|
+
} else {
|
|
27
|
+
for (const skill of skills) {
|
|
28
|
+
lines.push(` 📄 ${skill.name}`);
|
|
29
|
+
lines.push(` ${skill.description}`);
|
|
30
|
+
lines.push(` file: ${skill.fileName}`);
|
|
31
|
+
lines.push("");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,35 +7,57 @@
|
|
|
7
7
|
* 1. Persistent Memory — MEMORY.md + USER.md that survive across sessions
|
|
8
8
|
* 2. Background Learning Loop — auto-saves notable facts every N turns
|
|
9
9
|
* 3. Session-End Flush — saves memories before compaction/shutdown
|
|
10
|
-
* 4.
|
|
10
|
+
* 4. Auto-Consolidation — merges memory when full instead of erroring
|
|
11
|
+
* 5. Correction Detection — immediate save on user corrections
|
|
12
|
+
* 6. Procedural Skills — SKILL.md files for reusable procedures
|
|
13
|
+
* 7. Tool-Call-Aware Nudge — review triggers on tool call count too
|
|
14
|
+
* 8. /memory-insights — shows what's stored
|
|
15
|
+
* 9. /memory-skills — lists procedural skills
|
|
16
|
+
* 10. /memory-consolidate — manual consolidation trigger
|
|
11
17
|
*
|
|
12
|
-
* See
|
|
18
|
+
* See docs/ROADMAP.md for full roadmap and Hermes competitive analysis.
|
|
13
19
|
*/
|
|
14
20
|
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import * as os from "node:os";
|
|
15
23
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
16
24
|
import { MemoryStore } from "./store/memory-store.js";
|
|
25
|
+
import { SkillStore } from "./store/skill-store.js";
|
|
17
26
|
import { registerMemoryTool } from "./tools/memory-tool.js";
|
|
27
|
+
import { registerSkillTool } from "./tools/skill-tool.js";
|
|
18
28
|
import { setupBackgroundReview } from "./handlers/background-review.js";
|
|
19
29
|
import { setupSessionFlush } from "./handlers/session-flush.js";
|
|
20
30
|
import { registerInsightsCommand } from "./handlers/insights.js";
|
|
31
|
+
import { triggerConsolidation, registerConsolidateCommand } from "./handlers/auto-consolidate.js";
|
|
32
|
+
import { setupCorrectionDetector } from "./handlers/correction-detector.js";
|
|
33
|
+
import { setupSkillAutoTrigger } from "./handlers/skill-auto-trigger.js";
|
|
34
|
+
import { registerSkillsCommand } from "./handlers/skills-command.js";
|
|
21
35
|
import { loadConfig } from "./config.js";
|
|
22
36
|
|
|
23
37
|
export default function (pi: ExtensionAPI) {
|
|
24
38
|
const config = loadConfig();
|
|
25
39
|
|
|
40
|
+
const memoryDir = config.memoryDir ?? path.join(os.homedir(), ".pi", "agent", "memory");
|
|
26
41
|
const store = new MemoryStore(config);
|
|
42
|
+
const skillStore = new SkillStore(path.join(memoryDir, "skills"));
|
|
27
43
|
|
|
28
44
|
// ── 1. Load memory from disk on session start ──
|
|
29
45
|
pi.on("session_start", async (_event, _ctx) => {
|
|
30
46
|
await store.loadFromDisk();
|
|
31
47
|
});
|
|
32
48
|
|
|
33
|
-
// ── 2. Inject frozen snapshot into system prompt ──
|
|
49
|
+
// ── 2. Inject frozen snapshot + skill index into system prompt ──
|
|
34
50
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
35
51
|
const memoryBlock = store.formatForSystemPrompt();
|
|
36
|
-
|
|
52
|
+
const skillIndex = await skillStore.formatIndexForSystemPrompt();
|
|
53
|
+
|
|
54
|
+
const parts: string[] = [];
|
|
55
|
+
if (memoryBlock) parts.push(memoryBlock);
|
|
56
|
+
if (skillIndex) parts.push(skillIndex);
|
|
57
|
+
|
|
58
|
+
if (parts.length > 0) {
|
|
37
59
|
return {
|
|
38
|
-
systemPrompt: event.systemPrompt + "\n\n" +
|
|
60
|
+
systemPrompt: event.systemPrompt + "\n\n" + parts.join("\n\n"),
|
|
39
61
|
};
|
|
40
62
|
}
|
|
41
63
|
});
|
|
@@ -43,12 +65,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
43
65
|
// ── 3. Register the memory tool ──
|
|
44
66
|
registerMemoryTool(pi, store);
|
|
45
67
|
|
|
46
|
-
// ── 4.
|
|
68
|
+
// ── 4. Register the skill tool ──
|
|
69
|
+
registerSkillTool(pi, skillStore);
|
|
70
|
+
|
|
71
|
+
// ── 5. Setup background learning loop (with tool-call-aware nudge) ──
|
|
47
72
|
setupBackgroundReview(pi, store, config);
|
|
48
73
|
|
|
49
|
-
// ──
|
|
74
|
+
// ── 6. Setup session-end flush ──
|
|
50
75
|
setupSessionFlush(pi, store, config);
|
|
51
76
|
|
|
52
|
-
// ──
|
|
77
|
+
// ── 7. Setup auto-consolidation (inject consolidator into store) ──
|
|
78
|
+
store.setConsolidator(async (target, signal) => {
|
|
79
|
+
return triggerConsolidation(pi, store, target, signal);
|
|
80
|
+
});
|
|
81
|
+
registerConsolidateCommand(pi, store);
|
|
82
|
+
|
|
83
|
+
// ── 8. Setup correction detection ──
|
|
84
|
+
setupCorrectionDetector(pi, store, config);
|
|
85
|
+
|
|
86
|
+
// ── 9. Setup skill auto-trigger ──
|
|
87
|
+
setupSkillAutoTrigger(pi, store, skillStore, config);
|
|
88
|
+
|
|
89
|
+
// ── 10. Register commands ──
|
|
53
90
|
registerInsightsCommand(pi, store);
|
|
91
|
+
registerSkillsCommand(pi, skillStore);
|
|
54
92
|
}
|
|
@@ -22,15 +22,24 @@ import {
|
|
|
22
22
|
MEMORY_FILE,
|
|
23
23
|
USER_FILE,
|
|
24
24
|
} from "../constants.js";
|
|
25
|
-
import type { MemoryConfig, MemoryResult, MemorySnapshot } from "../types.js";
|
|
25
|
+
import type { MemoryConfig, MemoryResult, MemorySnapshot, ConsolidationResult } from "../types.js";
|
|
26
26
|
|
|
27
27
|
export class MemoryStore {
|
|
28
28
|
private memoryEntries: string[] = [];
|
|
29
29
|
private userEntries: string[] = [];
|
|
30
30
|
private snapshot: MemorySnapshot = { memory: "", user: "" };
|
|
31
|
+
private consolidator: ((target: "memory" | "user", signal?: AbortSignal) => Promise<ConsolidationResult>) | null = null;
|
|
31
32
|
|
|
32
33
|
constructor(private config: MemoryConfig) {}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Inject a consolidation function (avoids circular imports).
|
|
37
|
+
* Called from index.ts after both store and pi are available.
|
|
38
|
+
*/
|
|
39
|
+
setConsolidator(fn: (target: "memory" | "user", signal?: AbortSignal) => Promise<ConsolidationResult>): void {
|
|
40
|
+
this.consolidator = fn;
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
// ─── Path helpers ───
|
|
35
44
|
|
|
36
45
|
private get memoryDir(): string {
|
|
@@ -79,7 +88,7 @@ export class MemoryStore {
|
|
|
79
88
|
|
|
80
89
|
// ─── CRUD ───
|
|
81
90
|
|
|
82
|
-
add(target: "memory" | "user", content: string): MemoryResult {
|
|
91
|
+
async add(target: "memory" | "user", content: string, signal?: AbortSignal): Promise<MemoryResult> {
|
|
83
92
|
content = content.trim();
|
|
84
93
|
if (!content) return { success: false, error: "Content cannot be empty." };
|
|
85
94
|
|
|
@@ -95,6 +104,20 @@ export class MemoryStore {
|
|
|
95
104
|
|
|
96
105
|
const newTotal = [...entries, content].join(ENTRY_DELIMITER).length;
|
|
97
106
|
if (newTotal > limit) {
|
|
107
|
+
// Auto-consolidate if configured and consolidator available
|
|
108
|
+
if (this.config.autoConsolidate && this.consolidator) {
|
|
109
|
+
try {
|
|
110
|
+
const result = await this.consolidator(target, signal);
|
|
111
|
+
if (result.consolidated) {
|
|
112
|
+
// CRITICAL: reload from disk — child process modified files, our arrays are stale
|
|
113
|
+
await this.loadFromDisk();
|
|
114
|
+
// Retry the add with fresh data
|
|
115
|
+
return this.add(target, content, signal);
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Consolidation failed — fall through to error
|
|
119
|
+
}
|
|
120
|
+
}
|
|
98
121
|
const current = this.charCount(target);
|
|
99
122
|
return {
|
|
100
123
|
success: false,
|