pi-hermes-memory 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -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 +9 -5
- package/src/config.ts +11 -0
- package/src/constants.ts +78 -3
- package/src/handlers/auto-consolidate.ts +94 -0
- package/src/handlers/background-review.ts +42 -3
- package/src/handlers/correction-detector.ts +156 -0
- package/src/handlers/insights.ts +20 -1
- package/src/handlers/session-flush.ts +1 -0
- package/src/handlers/skill-auto-trigger.ts +108 -0
- package/src/handlers/skills-command.ts +38 -0
- package/src/index.ts +66 -13
- package/src/store/memory-store.ts +75 -21
- package/src/store/skill-store.ts +292 -0
- package/src/tools/memory-tool.ts +25 -6
- package/src/tools/skill-tool.ts +142 -0
- package/src/types.ts +42 -0
package/src/constants.ts
CHANGED
|
@@ -12,8 +12,12 @@ export const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
|
|
|
12
12
|
export const DEFAULT_USER_CHAR_LIMIT = 1375;
|
|
13
13
|
|
|
14
14
|
// ─── Learning loop defaults ───
|
|
15
|
+
export const DEFAULT_PROJECT_CHAR_LIMIT = 2200;
|
|
16
|
+
|
|
15
17
|
export const DEFAULT_NUDGE_INTERVAL = 10;
|
|
16
18
|
export const DEFAULT_FLUSH_MIN_TURNS = 6;
|
|
19
|
+
export const DEFAULT_NUDGE_TOOL_CALLS = 15;
|
|
20
|
+
export const DEFAULT_SKILL_TRIGGER_TOOL_CALLS = 8;
|
|
17
21
|
|
|
18
22
|
// ─── File names ───
|
|
19
23
|
export const MEMORY_FILE = "MEMORY.md";
|
|
@@ -33,9 +37,10 @@ PRIORITY: User preferences and corrections > environment facts > procedural know
|
|
|
33
37
|
|
|
34
38
|
Do NOT save task progress, session outcomes, completed-work logs, or temporary TODO state.
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
THREE TARGETS:
|
|
37
41
|
- 'user': who the user is -- name, role, preferences, communication style, pet peeves
|
|
38
|
-
- 'memory': your notes -- environment facts,
|
|
42
|
+
- 'memory': your global notes -- environment facts, tool quirks, lessons learned (shared across all projects)
|
|
43
|
+
- 'project': project-specific notes -- architecture decisions, API quirks, team norms, codebase conventions (scoped to current project)
|
|
39
44
|
|
|
40
45
|
ACTIONS: add (new entry), replace (update existing -- old_text identifies it), remove (delete -- old_text identifies it).`;
|
|
41
46
|
|
|
@@ -44,9 +49,79 @@ export const COMBINED_REVIEW_PROMPT = `Review the conversation above and conside
|
|
|
44
49
|
|
|
45
50
|
**Memory**: Has the user revealed things about themselves — their persona, desires, preferences, or personal details? Has the user expressed expectations about how you should behave, their work style, or ways they want you to operate? If so, save using the memory tool.
|
|
46
51
|
|
|
47
|
-
**Skills**: Was a non-trivial approach used to complete a task that required trial and error, or changing course
|
|
52
|
+
**Skills**: Was a complex, non-trivial approach used to complete a task — one that required trial and error, multiple tool calls, or changing course? If so, save a reusable procedure using the skill tool with action 'create'. Include: when to use it, step-by-step procedure, pitfalls to avoid, and how to verify success. If a related skill already exists, use action 'patch' to update it instead of creating a duplicate.
|
|
48
53
|
|
|
49
54
|
Only act if there's something genuinely worth saving. If nothing stands out, just say 'Nothing to save.' and stop.`;
|
|
50
55
|
|
|
51
56
|
// ─── Flush prompt (ported from flush_memories() in run_agent.py ~L7379) ───
|
|
52
57
|
export const FLUSH_PROMPT = `[System: The session is being compressed. Save anything worth remembering — prioritize user preferences, corrections, and recurring patterns over task-specific details.]`;
|
|
58
|
+
|
|
59
|
+
// ─── Auto-consolidation prompt ───
|
|
60
|
+
export const CONSOLIDATION_PROMPT = `The memory is at capacity. Review the current entries and consolidate them:
|
|
61
|
+
- Merge related entries into a single, concise entry
|
|
62
|
+
- Remove outdated or superseded entries
|
|
63
|
+
- Keep the most important and frequently-referenced facts
|
|
64
|
+
- Preserve user preferences and corrections (highest priority)
|
|
65
|
+
|
|
66
|
+
Use the memory tool to make changes. Be aggressive about merging — less is more.`;
|
|
67
|
+
|
|
68
|
+
// ─── Correction detection patterns (two-pass filter) ───
|
|
69
|
+
|
|
70
|
+
/** Strong patterns — always trigger (high confidence these are corrections) */
|
|
71
|
+
export const CORRECTION_STRONG_PATTERNS: RegExp[] = [
|
|
72
|
+
/don'?t do that/i,
|
|
73
|
+
/not like that/i,
|
|
74
|
+
/^I said\b/i,
|
|
75
|
+
/^I told you\b/i,
|
|
76
|
+
/we already discussed/i,
|
|
77
|
+
/^please don'?t/i,
|
|
78
|
+
/^that'?s not what I/i,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/** Weak patterns — only trigger if followed by a directive (verb or "the/that/this") */
|
|
82
|
+
export const CORRECTION_WEAK_PATTERNS: RegExp[] = [
|
|
83
|
+
/^no[,\.\s!]/i,
|
|
84
|
+
/^wrong[,\.\s!]/i,
|
|
85
|
+
/^actually[,\.\s]/i,
|
|
86
|
+
/^stop[,\.\s!]/i,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/** Negative patterns — suppress trigger even if a positive pattern matches */
|
|
90
|
+
export const CORRECTION_NEGATIVE_PATTERNS: RegExp[] = [
|
|
91
|
+
/^no worries/i,
|
|
92
|
+
/^no problem/i,
|
|
93
|
+
/^no thanks/i,
|
|
94
|
+
/^no need/i,
|
|
95
|
+
/^actually.{0,10}(looks? great|perfect|good|correct|right)/i,
|
|
96
|
+
/^stop.{0,5}(there|here|for now)/i,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// ─── Correction save prompt ───
|
|
100
|
+
export const CORRECTION_SAVE_PROMPT = `The user just corrected you. Review what went wrong and save the correction to persistent memory.
|
|
101
|
+
|
|
102
|
+
Priority:
|
|
103
|
+
1. User preference ("don't do X", "always use Y instead")
|
|
104
|
+
2. Wrong assumption you made
|
|
105
|
+
3. Environment fact you got wrong
|
|
106
|
+
|
|
107
|
+
Use the memory tool to save. If this contradicts an existing entry, use 'replace' to update it.`;
|
|
108
|
+
|
|
109
|
+
// ─── Skill tool description ───
|
|
110
|
+
export const SKILL_TOOL_DESCRIPTION = `Save reusable procedures and patterns as skills that survive across sessions. Skills are procedural memory — they capture HOW to do something, not just what happened.
|
|
111
|
+
|
|
112
|
+
WHEN TO CREATE A SKILL:
|
|
113
|
+
- After completing a complex task that required trial and error or multiple tool calls
|
|
114
|
+
- When you discover a non-obvious approach that could be reused
|
|
115
|
+
- When the user teaches you a specific workflow or procedure
|
|
116
|
+
|
|
117
|
+
WHEN TO UPDATE A SKILL (use 'patch'):
|
|
118
|
+
- You discover a better approach for an existing skill
|
|
119
|
+
- A pitfall or edge case not covered by the skill
|
|
120
|
+
- A step in the procedure changed
|
|
121
|
+
|
|
122
|
+
SKILL FORMAT:
|
|
123
|
+
- name: short, descriptive (e.g., "debug-typescript-errors")
|
|
124
|
+
- description: one-line summary of when to use it
|
|
125
|
+
- body: structured with sections — ## When to Use, ## Procedure, ## Pitfalls, ## Verification
|
|
126
|
+
|
|
127
|
+
ACTIONS: create (new skill), view (read full content), patch (update a section), edit (replace description + body), delete (remove skill).`;
|
|
@@ -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
|
+
}
|
|
@@ -16,9 +16,11 @@ import { getMessageText } from "../types.js";
|
|
|
16
16
|
export function setupBackgroundReview(
|
|
17
17
|
pi: ExtensionAPI,
|
|
18
18
|
store: MemoryStore,
|
|
19
|
+
projectStore: MemoryStore | null,
|
|
19
20
|
config: MemoryConfig,
|
|
20
21
|
): void {
|
|
21
22
|
let turnsSinceReview = 0;
|
|
23
|
+
let toolCallsSinceReview = 0;
|
|
22
24
|
let userTurnCount = 0;
|
|
23
25
|
let reviewInProgress = false;
|
|
24
26
|
|
|
@@ -33,10 +35,35 @@ export function setupBackgroundReview(
|
|
|
33
35
|
|
|
34
36
|
if (!config.reviewEnabled) return;
|
|
35
37
|
if (reviewInProgress) return;
|
|
36
|
-
|
|
38
|
+
|
|
39
|
+
// Count tool calls from this turn's message only (not cumulative branch scan —
|
|
40
|
+
// otherwise the counter resets to 0 at review, then immediately re-counts all
|
|
41
|
+
// historical tool calls and re-triggers on every subsequent turn).
|
|
42
|
+
try {
|
|
43
|
+
const msg = event.message;
|
|
44
|
+
if (msg?.role === "assistant") {
|
|
45
|
+
const content = msg?.content;
|
|
46
|
+
if (Array.isArray(content)) {
|
|
47
|
+
for (const block of content) {
|
|
48
|
+
if (block && typeof block === "object" && block.type === "toolCall") {
|
|
49
|
+
toolCallsSinceReview++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// If we can't count tool calls, fall back to turn-based only
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Trigger on EITHER turn count OR tool call count
|
|
59
|
+
const turnThresholdMet = turnsSinceReview >= config.nudgeInterval;
|
|
60
|
+
const toolCallThresholdMet = toolCallsSinceReview >= config.nudgeToolCalls;
|
|
61
|
+
|
|
62
|
+
if (!turnThresholdMet && !toolCallThresholdMet) return;
|
|
37
63
|
if (userTurnCount < 3) return;
|
|
38
64
|
|
|
39
65
|
turnsSinceReview = 0;
|
|
66
|
+
toolCallsSinceReview = 0;
|
|
40
67
|
reviewInProgress = true;
|
|
41
68
|
|
|
42
69
|
try {
|
|
@@ -56,6 +83,7 @@ export function setupBackgroundReview(
|
|
|
56
83
|
|
|
57
84
|
const currentMemory = store.getMemoryEntries().join("\n§\n");
|
|
58
85
|
const currentUser = store.getUserEntries().join("\n§\n");
|
|
86
|
+
const currentProject = projectStore ? projectStore.getMemoryEntries().join("\n§\n") : null;
|
|
59
87
|
|
|
60
88
|
const reviewPrompt = [
|
|
61
89
|
COMBINED_REVIEW_PROMPT,
|
|
@@ -65,12 +93,23 @@ export function setupBackgroundReview(
|
|
|
65
93
|
"",
|
|
66
94
|
"--- Current User Profile ---",
|
|
67
95
|
currentUser || "(empty)",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
if (currentProject !== null) {
|
|
99
|
+
reviewPrompt.push(
|
|
100
|
+
"",
|
|
101
|
+
"--- Current Project Memory ---",
|
|
102
|
+
currentProject || "(empty)",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
reviewPrompt.push(
|
|
68
107
|
"",
|
|
69
108
|
"--- Conversation to Review ---",
|
|
70
109
|
parts.join("\n\n"),
|
|
71
|
-
|
|
110
|
+
);
|
|
72
111
|
|
|
73
|
-
const result = await pi.exec("pi", ["-p", "--no-session", reviewPrompt], {
|
|
112
|
+
const result = await pi.exec("pi", ["-p", "--no-session", reviewPrompt.join("\n")], {
|
|
74
113
|
signal: ctx.signal,
|
|
75
114
|
timeout: 60000,
|
|
76
115
|
});
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
projectStore: MemoryStore | null,
|
|
61
|
+
config: MemoryConfig,
|
|
62
|
+
): void {
|
|
63
|
+
if (!config.correctionDetection) return;
|
|
64
|
+
|
|
65
|
+
let pendingCorrection = false;
|
|
66
|
+
let turnsSinceLastCorrection = 3; // Start at threshold so first correction can fire immediately
|
|
67
|
+
let correctionInProgress = false;
|
|
68
|
+
|
|
69
|
+
// Flag on message_end (user role)
|
|
70
|
+
pi.on("message_end", async (event, _ctx) => {
|
|
71
|
+
if (event.message.role !== "user") return;
|
|
72
|
+
const text = getMessageText(event.message);
|
|
73
|
+
if (!text) return;
|
|
74
|
+
if (isCorrection(text)) {
|
|
75
|
+
pendingCorrection = true;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Trigger on turn_end (we need full context: user correction + what agent said)
|
|
80
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
81
|
+
if (!pendingCorrection) {
|
|
82
|
+
turnsSinceLastCorrection++;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
pendingCorrection = false;
|
|
86
|
+
|
|
87
|
+
// Rate limit: max 1 correction save per 3 turns
|
|
88
|
+
if (turnsSinceLastCorrection < 3) return;
|
|
89
|
+
if (correctionInProgress) return;
|
|
90
|
+
|
|
91
|
+
turnsSinceLastCorrection = 0;
|
|
92
|
+
correctionInProgress = true;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Build conversation snapshot
|
|
96
|
+
const entries = ctx.sessionManager.getBranch();
|
|
97
|
+
const parts: string[] = [];
|
|
98
|
+
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (entry.type !== "message") continue;
|
|
101
|
+
const msg = entry.message;
|
|
102
|
+
const text = getMessageText(msg);
|
|
103
|
+
if (!text) continue;
|
|
104
|
+
const prefix = msg.role === "user" ? "[USER]" : "[ASSISTANT]";
|
|
105
|
+
parts.push(`${prefix}: ${text}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Only include last few exchanges (correction context is recent)
|
|
109
|
+
const recentParts = parts.slice(-6);
|
|
110
|
+
|
|
111
|
+
const currentMemory = store.getMemoryEntries().join(ENTRY_DELIMITER);
|
|
112
|
+
const currentUser = store.getUserEntries().join(ENTRY_DELIMITER);
|
|
113
|
+
const currentProject = projectStore ? projectStore.getMemoryEntries().join(ENTRY_DELIMITER) : null;
|
|
114
|
+
|
|
115
|
+
const prompt = [
|
|
116
|
+
CORRECTION_SAVE_PROMPT,
|
|
117
|
+
"",
|
|
118
|
+
"--- Current Memory ---",
|
|
119
|
+
currentMemory || "(empty)",
|
|
120
|
+
"",
|
|
121
|
+
"--- Current User Profile ---",
|
|
122
|
+
currentUser || "(empty)",
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
if (currentProject !== null) {
|
|
126
|
+
prompt.push(
|
|
127
|
+
"",
|
|
128
|
+
"--- Current Project Memory ---",
|
|
129
|
+
currentProject || "(empty)",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
prompt.push(
|
|
134
|
+
"",
|
|
135
|
+
"--- Recent Conversation ---",
|
|
136
|
+
recentParts.join("\n\n"),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const result = await pi.exec("pi", ["-p", "--no-session", prompt.join("\n")], {
|
|
140
|
+
signal: ctx.signal,
|
|
141
|
+
timeout: 30000,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (result.code === 0 && result.stdout) {
|
|
145
|
+
const output = result.stdout.trim();
|
|
146
|
+
if (output && !output.toLowerCase().includes("nothing to save")) {
|
|
147
|
+
ctx.ui.notify("🔧 Correction detected — memory updated", "info");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
// Best-effort — don't block the session
|
|
152
|
+
} finally {
|
|
153
|
+
correctionInProgress = false;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
package/src/handlers/insights.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import { MemoryStore } from "../store/memory-store.js";
|
|
7
7
|
|
|
8
|
-
export function registerInsightsCommand(pi: ExtensionAPI, store: MemoryStore): void {
|
|
8
|
+
export function registerInsightsCommand(pi: ExtensionAPI, store: MemoryStore, projectStore: MemoryStore | null, projectName: string): void {
|
|
9
9
|
pi.registerCommand("memory-insights", {
|
|
10
10
|
description: "Show what's stored in persistent memory",
|
|
11
11
|
handler: async (_args, ctx) => {
|
|
12
12
|
const memoryEntries = store.getMemoryEntries();
|
|
13
13
|
const userEntries = store.getUserEntries();
|
|
14
|
+
const projectEntries = projectStore ? projectStore.getMemoryEntries() : null;
|
|
14
15
|
|
|
15
16
|
const lines: string[] = [];
|
|
16
17
|
lines.push("");
|
|
@@ -51,6 +52,24 @@ export function registerInsightsCommand(pi: ExtensionAPI, store: MemoryStore): v
|
|
|
51
52
|
}
|
|
52
53
|
lines.push("");
|
|
53
54
|
|
|
55
|
+
// Project section
|
|
56
|
+
if (projectEntries !== null) {
|
|
57
|
+
lines.push(` 📁 PROJECT MEMORY: ${projectName}`);
|
|
58
|
+
lines.push(" " + "─".repeat(44));
|
|
59
|
+
if (projectEntries.length === 0) {
|
|
60
|
+
lines.push(" (empty)");
|
|
61
|
+
} else {
|
|
62
|
+
for (let i = 0; i < projectEntries.length; i++) {
|
|
63
|
+
const preview =
|
|
64
|
+
projectEntries[i].length > 100
|
|
65
|
+
? projectEntries[i].slice(0, 100) + "..."
|
|
66
|
+
: projectEntries[i];
|
|
67
|
+
lines.push(` ${i + 1}. ${preview}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
lines.push("");
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
55
74
|
},
|
|
56
75
|
});
|
|
@@ -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
|
+
// Accumulate tool calls across turns (reset on trigger)
|
|
24
|
+
let toolCallCount = 0;
|
|
25
|
+
const toolTypes = new Set<string>();
|
|
26
|
+
|
|
27
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
28
|
+
if (triggeredThisSession) return;
|
|
29
|
+
|
|
30
|
+
// Count tool calls from this turn's message only (not cumulative branch scan —
|
|
31
|
+
// otherwise the counter accumulates historical tool calls and fires prematurely).
|
|
32
|
+
try {
|
|
33
|
+
const msg = event.message;
|
|
34
|
+
if (msg?.role === "assistant") {
|
|
35
|
+
const content = msg?.content;
|
|
36
|
+
if (Array.isArray(content)) {
|
|
37
|
+
for (const block of content) {
|
|
38
|
+
if (block && typeof block === "object" && block.type === "toolCall") {
|
|
39
|
+
toolCallCount++;
|
|
40
|
+
if ((block as { name?: string }).name) toolTypes.add((block as { name: string }).name);
|
|
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
|
+
}
|