pi-hermes-memory 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +288 -0
- package/docs/0.1/TASKS.md +197 -0
- package/docs/PUBLISHING.md +149 -0
- package/docs/ROADMAP.md +272 -0
- package/package.json +46 -0
- package/src/config.ts +49 -0
- package/src/constants.ts +52 -0
- package/src/handlers/background-review.ts +95 -0
- package/src/handlers/insights.ts +57 -0
- package/src/handlers/session-flush.ts +75 -0
- package/src/index.ts +54 -0
- package/src/store/content-scanner.ts +46 -0
- package/src/store/memory-store.ts +257 -0
- package/src/tools/memory-tool.ts +124 -0
- package/src/types.ts +66 -0
package/docs/ROADMAP.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Pi Hermes Memory — Roadmap
|
|
2
|
+
|
|
3
|
+
> From markdown files to a pluggable memory substrate for any Pi agent harness.
|
|
4
|
+
|
|
5
|
+
## Where We Are (v0.1.0)
|
|
6
|
+
|
|
7
|
+
- Persistent memory via `MEMORY.md` + `USER.md` with `§` delimiter
|
|
8
|
+
- Real-time `memory` tool (add / replace / remove) for the LLM
|
|
9
|
+
- Content scanning: prompt injection, role hijacking, secret exfiltration, invisible unicode
|
|
10
|
+
- Background learning loop (every N turns via `pi.exec`)
|
|
11
|
+
- Session flush before compaction and shutdown
|
|
12
|
+
- `/memory-insights` command
|
|
13
|
+
- Frozen snapshot injection into system prompt
|
|
14
|
+
- 119 automated tests, 0 type errors
|
|
15
|
+
- Atomic writes (temp + rename)
|
|
16
|
+
|
|
17
|
+
## Architecture Evolution
|
|
18
|
+
|
|
19
|
+
```mermaid
|
|
20
|
+
graph TB
|
|
21
|
+
subgraph "v0.1.0 — Current"
|
|
22
|
+
T1["memory tool<br/>(add / replace / remove)"]
|
|
23
|
+
SC["Content Scanner<br/>(injection · exfiltration · unicode)"]
|
|
24
|
+
MD["Markdown Backend<br/>MEMORY.md · USER.md"]
|
|
25
|
+
FS["Frozen Snapshot<br/>(system prompt injection)"]
|
|
26
|
+
BL["Background Review<br/>(pi.exec child process)"]
|
|
27
|
+
SF["Session Flush<br/>(compact · shutdown)"]
|
|
28
|
+
IC["/memory-insights<br/>(command)"]
|
|
29
|
+
CF["Config File<br/>(hermes-memory-config.json)"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
T1 --> SC --> MD
|
|
33
|
+
BL --> MD
|
|
34
|
+
SF --> MD
|
|
35
|
+
MD --> FS
|
|
36
|
+
|
|
37
|
+
style T1 fill:#e94560,stroke:#fff,color:#fff
|
|
38
|
+
style SC fill:#ff6600,stroke:#fff,color:#fff
|
|
39
|
+
style MD fill:#0f3460,stroke:#fff,color:#fff
|
|
40
|
+
style FS fill:#16213e,stroke:#fff,color:#fff
|
|
41
|
+
style BL fill:#16213e,stroke:#fff,color:#fff
|
|
42
|
+
style SF fill:#16213e,stroke:#fff,color:#fff
|
|
43
|
+
style IC fill:#16213e,stroke:#fff,color:#fff
|
|
44
|
+
style CF fill:#16213e,stroke:#fff,color:#fff
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```mermaid
|
|
48
|
+
graph TB
|
|
49
|
+
subgraph "v0.2.0 — Structured Storage & Search"
|
|
50
|
+
T2["memory tool<br/>(add / replace / remove / search)"]
|
|
51
|
+
SC2["Content Scanner<br/>(v0.1.0 scanner unchanged)"]
|
|
52
|
+
SA["Search Abstraction<br/>(MemoryBackend interface)"]
|
|
53
|
+
SQL["SQLite Backend<br/>(FTS5 · key-value · confidence)"]
|
|
54
|
+
PI2["Context-Aware Injection<br/>(relevance-filtered)"]
|
|
55
|
+
PS["Project-Scoped Memory<br/>(keyed by cwd)"]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
T2 --> SC2 --> SA
|
|
59
|
+
SA --> SQL
|
|
60
|
+
SQL --> PI2
|
|
61
|
+
SQL --> PS
|
|
62
|
+
|
|
63
|
+
style T2 fill:#e94560,stroke:#fff,color:#fff
|
|
64
|
+
style SC2 fill:#ff6600,stroke:#fff,color:#fff
|
|
65
|
+
style SA fill:#1282a2,stroke:#fff,color:#fff
|
|
66
|
+
style SQL fill:#0f3460,stroke:#fff,color:#fff
|
|
67
|
+
style PI2 fill:#16213e,stroke:#fff,color:#fff
|
|
68
|
+
style PS fill:#16213e,stroke:#fff,color:#fff
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
graph TB
|
|
73
|
+
subgraph "v0.3.0 — Pluggable Backend & External Memory"
|
|
74
|
+
T3["memory tool<br/>(add / replace / remove / search)"]
|
|
75
|
+
SC3["Content Scanner<br/>(unchanged — guards all backends)"]
|
|
76
|
+
SA3["Search Abstraction<br/>(MemoryBackend interface)"]
|
|
77
|
+
LOC["Local SQLite<br/>(default · offline)"]
|
|
78
|
+
M0["Mem0 Backend<br/>(vector search · cloud)"]
|
|
79
|
+
HON["Honcho Backend<br/>(dialectic reasoning · Hermes-native)"]
|
|
80
|
+
SEL["Selective Injection<br/>(search-relevant · project-scoped)"]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
T3 --> SC3 --> SA3
|
|
84
|
+
SA3 --> LOC
|
|
85
|
+
SA3 --> M0
|
|
86
|
+
SA3 --> HON
|
|
87
|
+
LOC --> SEL
|
|
88
|
+
M0 --> SEL
|
|
89
|
+
HON --> SEL
|
|
90
|
+
|
|
91
|
+
style T3 fill:#e94560,stroke:#fff,color:#fff
|
|
92
|
+
style SC3 fill:#ff6600,stroke:#fff,color:#fff
|
|
93
|
+
style SA3 fill:#1282a2,stroke:#fff,color:#fff
|
|
94
|
+
style LOC fill:#0f3460,stroke:#fff,color:#fff
|
|
95
|
+
style M0 fill:#6b21a8,stroke:#fff,color:#fff
|
|
96
|
+
style HON fill:#6b21a8,stroke:#fff,color:#fff
|
|
97
|
+
style SEL fill:#16213e,stroke:#fff,color:#fff
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```mermaid
|
|
101
|
+
graph TB
|
|
102
|
+
subgraph "v1.0.0 — Production Memory Substrate"
|
|
103
|
+
T4["memory tool<br/>(add / replace / remove / search / consolidate)"]
|
|
104
|
+
SC4["Content Scanner<br/>(extensible rule system)"]
|
|
105
|
+
SA4["Pluggable Backend<br/>(local · Mem0 · Honcho · custom)"]
|
|
106
|
+
CON["Smart Consolidation<br/>(structured extraction · dedup)"]
|
|
107
|
+
MUL["Multi-Agent Memory<br/>(shared context · scoping)"]
|
|
108
|
+
OBS["Observability<br/>(memory stats · usage · audit log)"]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
T4 --> SC4 --> SA4
|
|
112
|
+
SA4 --> CON
|
|
113
|
+
SA4 --> MUL
|
|
114
|
+
CON --> OBS
|
|
115
|
+
|
|
116
|
+
style T4 fill:#e94560,stroke:#fff,color:#fff
|
|
117
|
+
style SC4 fill:#ff6600,stroke:#fff,color:#fff
|
|
118
|
+
style SA4 fill:#1282a2,stroke:#fff,color:#fff
|
|
119
|
+
style CON fill:#16213e,stroke:#fff,color:#fff
|
|
120
|
+
style MUL fill:#16213e,stroke:#fff,color:#fff
|
|
121
|
+
style OBS fill:#16213e,stroke:#fff,color:#fff
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## v0.2.0 — Structured Storage & Search
|
|
127
|
+
|
|
128
|
+
**Goal**: Replace flat markdown with SQLite. Add search. Keep the same tool interface.
|
|
129
|
+
|
|
130
|
+
### `MemoryBackend` Interface
|
|
131
|
+
|
|
132
|
+
The core abstraction that makes everything after this possible:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
interface MemoryBackend {
|
|
136
|
+
// Write
|
|
137
|
+
add(target: "memory" | "user", entry: MemoryEntry): Promise<MemoryResult>;
|
|
138
|
+
replace(target: "memory" | "user", query: string, entry: MemoryEntry): Promise<MemoryResult>;
|
|
139
|
+
remove(target: "memory" | "user", query: string): Promise<MemoryResult>;
|
|
140
|
+
|
|
141
|
+
// Read
|
|
142
|
+
getAll(target: "memory" | "user"): Promise<MemoryEntry[]>;
|
|
143
|
+
search(query: string, limit?: number): Promise<MemoryEntry[]>;
|
|
144
|
+
|
|
145
|
+
// Lifecycle
|
|
146
|
+
formatForSystemPrompt(cwd?: string, prompt?: string): Promise<string>;
|
|
147
|
+
close(): Promise<void>;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Current `MemoryStore` becomes `MarkdownBackend` — the default, zero-dependency implementation. New `SQLiteBackend` adds structure without breaking anything.
|
|
152
|
+
|
|
153
|
+
### Deliverables
|
|
154
|
+
|
|
155
|
+
- [ ] `MemoryBackend` interface in `src/types.ts`
|
|
156
|
+
- [ ] `MarkdownBackend` — wraps current `MemoryStore` (backwards compatible)
|
|
157
|
+
- [ ] `SQLiteBackend` — FTS5 search, key-value entries, confidence scores, dedup by key
|
|
158
|
+
- [ ] `memory search` tool action — LLM can query existing entries
|
|
159
|
+
- [ ] Project-scoped memory — entries tagged with `cwd`, injected when matching
|
|
160
|
+
- [ ] Context-aware injection — `formatForSystemPrompt(cwd, prompt)` filters by relevance
|
|
161
|
+
- [ ] Config: `"backend": "markdown" | "sqlite"` (defaults to `markdown` for zero-dep install)
|
|
162
|
+
- [ ] Migration tool: `markdown → sqlite` one-time import
|
|
163
|
+
|
|
164
|
+
### What Does NOT Change
|
|
165
|
+
|
|
166
|
+
- Content scanner (guards all backends)
|
|
167
|
+
- Tool interface (`memory` tool name and actions)
|
|
168
|
+
- System prompt injection (frozen snapshot pattern)
|
|
169
|
+
- Config file location and format (just adds new fields)
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## v0.3.0 — Pluggable External Memory
|
|
174
|
+
|
|
175
|
+
**Goal**: Let users swap the backend to Mem0 or Honcho without changing anything else. The content scanner guards all data before it leaves the machine.
|
|
176
|
+
|
|
177
|
+
### Why This Matters
|
|
178
|
+
|
|
179
|
+
External memory services provide better semantic search, cross-session continuity, and multi-agent awareness. But they introduce trust boundaries — your agent's memories leave your machine. The content scanner becomes the security gate between Pi and any external service.
|
|
180
|
+
|
|
181
|
+
### Deliverables
|
|
182
|
+
|
|
183
|
+
- [ ] `Mem0Backend` — wraps Mem0's Node.js SDK (`add`, `search`, `update`, `delete`)
|
|
184
|
+
- [ ] `HonchoBackend` — wraps Honcho's API (`honcho_context`, `honcho_search_conclusions`, `honcho_reasoning`)
|
|
185
|
+
- [ ] Backend auto-detection — check for `MEM0_API_KEY` or `HONCHO_API_KEY` env vars, offer to configure
|
|
186
|
+
- [ ] Config: `"backend": "sqlite" | "mem0" | "honcho"` with `"mem0": { "apiKey": "...", "orgId": "..." }` options
|
|
187
|
+
- [ ] Selective injection by default when using external backends (leverage their search APIs)
|
|
188
|
+
- [ ] Offline fallback — if external backend is unreachable, fall back to local SQLite cache
|
|
189
|
+
- [ ] Data export — `memory export` command to dump all entries as JSON
|
|
190
|
+
|
|
191
|
+
### Security Model
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
LLM tool call
|
|
195
|
+
↓
|
|
196
|
+
Content Scanner (local, always runs first)
|
|
197
|
+
↓ blocked? → return error to LLM
|
|
198
|
+
↓ passed
|
|
199
|
+
MemoryBackend.add()
|
|
200
|
+
↓
|
|
201
|
+
Mem0 / Honcho / SQLite / Markdown
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The scanner runs **before** any backend. No adversarial content reaches external services.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## v1.0.0 — Production Memory Substrate
|
|
209
|
+
|
|
210
|
+
**Goal**: The memory layer that any Pi agent harness can build on top of.
|
|
211
|
+
|
|
212
|
+
### Deliverables
|
|
213
|
+
|
|
214
|
+
- [ ] Smart consolidation — structured extraction with typed output (preferences, patterns, corrections, tool prefs)
|
|
215
|
+
- [ ] Confidence scoring — entries gain confidence over time as they're referenced, decay if never used
|
|
216
|
+
- [ ] Multi-agent memory — shared context between agents, scoping rules (per-user, per-project, global)
|
|
217
|
+
- [ ] Extensible scanner rules — users can add custom patterns to the content scanner
|
|
218
|
+
- [ ] `/memory-insights` upgrade — show backend type, entry count, storage stats, last sync time
|
|
219
|
+
- [ ] Audit log — track all memory operations with timestamps (already in SQLite schema for `SQLiteBackend`)
|
|
220
|
+
- [ ] Import/export — migrate between backends without data loss
|
|
221
|
+
- [ ] Benchmarks — context injection latency, search relevance, token budget utilization
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Design Principles (Unchanging)
|
|
226
|
+
|
|
227
|
+
These hold across all versions:
|
|
228
|
+
|
|
229
|
+
1. **Security first** — Content scanning before any write, regardless of backend. No exceptions.
|
|
230
|
+
2. **Real-time saves** — The LLM can save memories mid-conversation via tool calls, not just at session end.
|
|
231
|
+
3. **Frozen snapshot** — Memory is injected into the system prompt once at session start. Never mutated mid-session.
|
|
232
|
+
4. **Crash safety** — Atomic writes for markdown, WAL mode for SQLite, graceful degradation for external backends.
|
|
233
|
+
5. **Zero-config start** — Install and it works with sensible defaults. Configuration is for power users.
|
|
234
|
+
6. **Backwards compatible** — Every new version is a drop-in upgrade. No breaking changes to the tool interface or config format without a major version bump.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Version Timeline
|
|
239
|
+
|
|
240
|
+
```mermaid
|
|
241
|
+
gantt
|
|
242
|
+
title Pi Hermes Memory — Release Timeline
|
|
243
|
+
dateFormat YYYY-MM-DD
|
|
244
|
+
axisFormat %b %Y
|
|
245
|
+
|
|
246
|
+
section v0.1.0
|
|
247
|
+
Core memory + scanner + tool + review + flush :done, v01, 2025-04-20, 5d
|
|
248
|
+
|
|
249
|
+
section v0.2.0
|
|
250
|
+
MemoryBackend interface :v02a, after v01, 7d
|
|
251
|
+
SQLite backend + FTS5 search :v02b, after v02a, 7d
|
|
252
|
+
memory search tool + project scoping :v02c, after v02b, 5d
|
|
253
|
+
Context-aware injection :v02d, after v02c, 5d
|
|
254
|
+
|
|
255
|
+
section v0.3.0
|
|
256
|
+
Mem0 backend :v03a, after v02d, 7d
|
|
257
|
+
Honcho backend :v03b, after v03a, 7d
|
|
258
|
+
Offline fallback + data export :v03c, after v03b, 5d
|
|
259
|
+
|
|
260
|
+
section v1.0.0
|
|
261
|
+
Smart consolidation + confidence :v1a, after v03c, 10d
|
|
262
|
+
Multi-agent memory + audit log :v1b, after v1a, 10d
|
|
263
|
+
Extensible scanner + benchmarks :v1c, after v1b, 7d
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## How to Contribute
|
|
269
|
+
|
|
270
|
+
See [TASKS.md](0.1/TASKS.md) for current work. Pick an unchecked item, mark it `[~]`, implement, mark it `[x]` with the commit hash.
|
|
271
|
+
|
|
272
|
+
For roadmap items, open an issue with the version tag (e.g. `v0.2.0`) and describe what you want to work on.
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-hermes-memory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Persistent memory and self-directed learning loop for Pi — ported from the Hermes agent harness. Security-first content scanning, real-time saves, and frozen snapshot injection.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"docs"
|
|
12
|
+
],
|
|
13
|
+
"pi": {
|
|
14
|
+
"extensions": [
|
|
15
|
+
"./src/index.ts"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"check": "tsc --noEmit",
|
|
20
|
+
"test": "npx tsx --test 'tests/**/*.test.ts' --test-concurrency=1"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi-package",
|
|
24
|
+
"pi-extension",
|
|
25
|
+
"memory",
|
|
26
|
+
"learning-loop",
|
|
27
|
+
"agent",
|
|
28
|
+
"hermes",
|
|
29
|
+
"persistent-memory",
|
|
30
|
+
"content-scanner"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/chandra447/pi-hermes-memory"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@mariozechner/pi-ai": "^0.70.0",
|
|
42
|
+
"@mariozechner/pi-coding-agent": "^0.70.0",
|
|
43
|
+
"typebox": "^1.1.33",
|
|
44
|
+
"typescript": "^6.0.3"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import type { MemoryConfig } from "./types.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_MEMORY_CHAR_LIMIT,
|
|
7
|
+
DEFAULT_USER_CHAR_LIMIT,
|
|
8
|
+
DEFAULT_NUDGE_INTERVAL,
|
|
9
|
+
DEFAULT_FLUSH_MIN_TURNS,
|
|
10
|
+
} from "./constants.js";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG: MemoryConfig = {
|
|
13
|
+
memoryCharLimit: DEFAULT_MEMORY_CHAR_LIMIT,
|
|
14
|
+
userCharLimit: DEFAULT_USER_CHAR_LIMIT,
|
|
15
|
+
nudgeInterval: DEFAULT_NUDGE_INTERVAL,
|
|
16
|
+
reviewEnabled: true,
|
|
17
|
+
flushOnCompact: true,
|
|
18
|
+
flushOnShutdown: true,
|
|
19
|
+
flushMinTurns: DEFAULT_FLUSH_MIN_TURNS,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_CONFIG_PATH = path.join(
|
|
23
|
+
os.homedir(),
|
|
24
|
+
".pi",
|
|
25
|
+
"agent",
|
|
26
|
+
"hermes-memory-config.json",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export function loadConfig(): MemoryConfig {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(DEFAULT_CONFIG_PATH)) {
|
|
32
|
+
const raw = fs.readFileSync(DEFAULT_CONFIG_PATH, "utf-8");
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
// Merge: override defaults with user config
|
|
35
|
+
const config: MemoryConfig = { ...DEFAULT_CONFIG };
|
|
36
|
+
if (typeof parsed.memoryCharLimit === "number") config.memoryCharLimit = parsed.memoryCharLimit;
|
|
37
|
+
if (typeof parsed.userCharLimit === "number") config.userCharLimit = parsed.userCharLimit;
|
|
38
|
+
if (typeof parsed.nudgeInterval === "number") config.nudgeInterval = parsed.nudgeInterval;
|
|
39
|
+
if (typeof parsed.reviewEnabled === "boolean") config.reviewEnabled = parsed.reviewEnabled;
|
|
40
|
+
if (typeof parsed.flushOnCompact === "boolean") config.flushOnCompact = parsed.flushOnCompact;
|
|
41
|
+
if (typeof parsed.flushOnShutdown === "boolean") config.flushOnShutdown = parsed.flushOnShutdown;
|
|
42
|
+
if (typeof parsed.flushMinTurns === "number") config.flushMinTurns = parsed.flushMinTurns;
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Fall back to defaults on parse error or access issues
|
|
47
|
+
}
|
|
48
|
+
return { ...DEFAULT_CONFIG };
|
|
49
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants — prompts, defaults, and delimiter.
|
|
3
|
+
* Ported from hermes-agent/tools/memory_tool.py and hermes-agent/run_agent.py.
|
|
4
|
+
* See PLAN.md → "Hermes Source File Reference Map" for exact source lines.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ─── Entry delimiter (same as Hermes) ───
|
|
8
|
+
export const ENTRY_DELIMITER = "\n§\n";
|
|
9
|
+
|
|
10
|
+
// ─── Character limits (not tokens — model-independent) ───
|
|
11
|
+
export const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
|
|
12
|
+
export const DEFAULT_USER_CHAR_LIMIT = 1375;
|
|
13
|
+
|
|
14
|
+
// ─── Learning loop defaults ───
|
|
15
|
+
export const DEFAULT_NUDGE_INTERVAL = 10;
|
|
16
|
+
export const DEFAULT_FLUSH_MIN_TURNS = 6;
|
|
17
|
+
|
|
18
|
+
// ─── File names ───
|
|
19
|
+
export const MEMORY_FILE = "MEMORY.md";
|
|
20
|
+
export const USER_FILE = "USER.md";
|
|
21
|
+
|
|
22
|
+
// ─── Tool description (ported from MEMORY_SCHEMA in hermes-agent/tools/memory_tool.py) ───
|
|
23
|
+
export const MEMORY_TOOL_DESCRIPTION = `Save durable information to persistent memory that survives across sessions. Memory is injected into future turns, so keep it compact and focused on facts that will still matter later.
|
|
24
|
+
|
|
25
|
+
WHEN TO SAVE (do this proactively, don't wait to be asked):
|
|
26
|
+
- User corrects you or says 'remember this' / 'don't do that again'
|
|
27
|
+
- User shares a preference, habit, or personal detail (name, role, timezone, coding style)
|
|
28
|
+
- You discover something about the environment (OS, installed tools, project structure)
|
|
29
|
+
- You learn a convention, API quirk, or workflow specific to this user's setup
|
|
30
|
+
- You identify a stable fact that will be useful again in future sessions
|
|
31
|
+
|
|
32
|
+
PRIORITY: User preferences and corrections > environment facts > procedural knowledge.
|
|
33
|
+
|
|
34
|
+
Do NOT save task progress, session outcomes, completed-work logs, or temporary TODO state.
|
|
35
|
+
|
|
36
|
+
TWO TARGETS:
|
|
37
|
+
- 'user': who the user is -- name, role, preferences, communication style, pet peeves
|
|
38
|
+
- 'memory': your notes -- environment facts, project conventions, tool quirks, lessons learned
|
|
39
|
+
|
|
40
|
+
ACTIONS: add (new entry), replace (update existing -- old_text identifies it), remove (delete -- old_text identifies it).`;
|
|
41
|
+
|
|
42
|
+
// ─── Background review prompt (ported from _COMBINED_REVIEW_PROMPT in run_agent.py ~L2855) ───
|
|
43
|
+
export const COMBINED_REVIEW_PROMPT = `Review the conversation above and consider two things:
|
|
44
|
+
|
|
45
|
+
**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
|
+
|
|
47
|
+
**Skills**: Was a non-trivial approach used to complete a task that required trial and error, or changing course due to experiential findings along the way, or did the user expect or desire a different method or outcome?
|
|
48
|
+
|
|
49
|
+
Only act if there's something genuinely worth saving. If nothing stands out, just say 'Nothing to save.' and stop.`;
|
|
50
|
+
|
|
51
|
+
// ─── Flush prompt (ported from flush_memories() in run_agent.py ~L7379) ───
|
|
52
|
+
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.]`;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background review — learning loop that auto-saves memory every N turns.
|
|
3
|
+
* Ported from hermes-agent/run_agent.py (_spawn_background_review, _memory_nudge_interval).
|
|
4
|
+
* See PLAN.md → "Hermes Source File Reference Map" for source lines.
|
|
5
|
+
*
|
|
6
|
+
* Uses pi.exec("pi", ["-p", ...]) for isolated one-shot review,
|
|
7
|
+
* keeping us within Pi's intended extension API.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { MemoryStore } from "../store/memory-store.js";
|
|
12
|
+
import { COMBINED_REVIEW_PROMPT } from "../constants.js";
|
|
13
|
+
import type { MemoryConfig } from "../types.js";
|
|
14
|
+
import { getMessageText } from "../types.js";
|
|
15
|
+
|
|
16
|
+
export function setupBackgroundReview(
|
|
17
|
+
pi: ExtensionAPI,
|
|
18
|
+
store: MemoryStore,
|
|
19
|
+
config: MemoryConfig,
|
|
20
|
+
): void {
|
|
21
|
+
let turnsSinceReview = 0;
|
|
22
|
+
let userTurnCount = 0;
|
|
23
|
+
let reviewInProgress = false;
|
|
24
|
+
|
|
25
|
+
pi.on("message_end", async (event, _ctx) => {
|
|
26
|
+
if (event.message.role === "user") {
|
|
27
|
+
userTurnCount++;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
32
|
+
turnsSinceReview++;
|
|
33
|
+
|
|
34
|
+
if (!config.reviewEnabled) return;
|
|
35
|
+
if (reviewInProgress) return;
|
|
36
|
+
if (turnsSinceReview < config.nudgeInterval) return;
|
|
37
|
+
if (userTurnCount < 3) return;
|
|
38
|
+
|
|
39
|
+
turnsSinceReview = 0;
|
|
40
|
+
reviewInProgress = true;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Build conversation snapshot from session entries
|
|
44
|
+
const entries = ctx.sessionManager.getBranch();
|
|
45
|
+
const parts: string[] = [];
|
|
46
|
+
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (entry.type !== "message") continue;
|
|
49
|
+
const msg = entry.message;
|
|
50
|
+
const text = getMessageText(msg);
|
|
51
|
+
if (!text) continue;
|
|
52
|
+
const prefix = msg.role === "user" ? "[USER]" : "[ASSISTANT]";
|
|
53
|
+
parts.push(`${prefix}: ${text}`);
|
|
54
|
+
}
|
|
55
|
+
if (parts.length < 4) return; // Not enough conversation to review
|
|
56
|
+
|
|
57
|
+
const currentMemory = store.getMemoryEntries().join("\n§\n");
|
|
58
|
+
const currentUser = store.getUserEntries().join("\n§\n");
|
|
59
|
+
|
|
60
|
+
const reviewPrompt = [
|
|
61
|
+
COMBINED_REVIEW_PROMPT,
|
|
62
|
+
"",
|
|
63
|
+
"--- Current Memory ---",
|
|
64
|
+
currentMemory || "(empty)",
|
|
65
|
+
"",
|
|
66
|
+
"--- Current User Profile ---",
|
|
67
|
+
currentUser || "(empty)",
|
|
68
|
+
"",
|
|
69
|
+
"--- Conversation to Review ---",
|
|
70
|
+
parts.join("\n\n"),
|
|
71
|
+
].join("\n");
|
|
72
|
+
|
|
73
|
+
const result = await pi.exec("pi", ["-p", "--no-session", reviewPrompt], {
|
|
74
|
+
signal: ctx.signal,
|
|
75
|
+
timeout: 60000,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (result.code === 0 && result.stdout) {
|
|
79
|
+
const output = result.stdout.trim();
|
|
80
|
+
if (output && !output.toLowerCase().includes("nothing to save")) {
|
|
81
|
+
ctx.ui.notify("💾 Memory auto-reviewed and updated", "info");
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
ctx.ui.notify(
|
|
85
|
+
`[hermes] auto-review failed (exit=${result.code}): ${result.stderr?.slice(0, 200) || "unknown error"}`,
|
|
86
|
+
"error",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
ctx.ui.notify(`[hermes] auto-review error: ${String(err).slice(0, 200)}`, "error");
|
|
91
|
+
} finally {
|
|
92
|
+
reviewInProgress = false;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insights command — /memory-insights shows what's stored in persistent memory.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { MemoryStore } from "../store/memory-store.js";
|
|
7
|
+
|
|
8
|
+
export function registerInsightsCommand(pi: ExtensionAPI, store: MemoryStore): void {
|
|
9
|
+
pi.registerCommand("memory-insights", {
|
|
10
|
+
description: "Show what's stored in persistent memory",
|
|
11
|
+
handler: async (_args, ctx) => {
|
|
12
|
+
const memoryEntries = store.getMemoryEntries();
|
|
13
|
+
const userEntries = store.getUserEntries();
|
|
14
|
+
|
|
15
|
+
const lines: string[] = [];
|
|
16
|
+
lines.push("");
|
|
17
|
+
lines.push(" ╔══════════════════════════════════════════════╗");
|
|
18
|
+
lines.push(" ║ 🧠 Memory Insights ║");
|
|
19
|
+
lines.push(" ╚══════════════════════════════════════════════╝");
|
|
20
|
+
lines.push("");
|
|
21
|
+
|
|
22
|
+
// Memory section
|
|
23
|
+
lines.push(" 📋 MEMORY (your personal notes)");
|
|
24
|
+
lines.push(" " + "─".repeat(44));
|
|
25
|
+
if (memoryEntries.length === 0) {
|
|
26
|
+
lines.push(" (empty)");
|
|
27
|
+
} else {
|
|
28
|
+
for (let i = 0; i < memoryEntries.length; i++) {
|
|
29
|
+
const preview =
|
|
30
|
+
memoryEntries[i].length > 100
|
|
31
|
+
? memoryEntries[i].slice(0, 100) + "..."
|
|
32
|
+
: memoryEntries[i];
|
|
33
|
+
lines.push(` ${i + 1}. ${preview}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
lines.push("");
|
|
37
|
+
|
|
38
|
+
// User section
|
|
39
|
+
lines.push(" 👤 USER PROFILE");
|
|
40
|
+
lines.push(" " + "─".repeat(44));
|
|
41
|
+
if (userEntries.length === 0) {
|
|
42
|
+
lines.push(" (empty)");
|
|
43
|
+
} else {
|
|
44
|
+
for (let i = 0; i < userEntries.length; i++) {
|
|
45
|
+
const preview =
|
|
46
|
+
userEntries[i].length > 100
|
|
47
|
+
? userEntries[i].slice(0, 100) + "..."
|
|
48
|
+
: userEntries[i];
|
|
49
|
+
lines.push(` ${i + 1}. ${preview}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
lines.push("");
|
|
53
|
+
|
|
54
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session flush — gives the agent one turn to save memories before context is lost.
|
|
3
|
+
* Ported from hermes-agent/run_agent.py (flush_memories).
|
|
4
|
+
* See PLAN.md → "Hermes Source File Reference Map" for source lines.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import { MemoryStore } from "../store/memory-store.js";
|
|
9
|
+
import { FLUSH_PROMPT } from "../constants.js";
|
|
10
|
+
import type { MemoryConfig } from "../types.js";
|
|
11
|
+
import { getMessageText } from "../types.js";
|
|
12
|
+
|
|
13
|
+
export function setupSessionFlush(
|
|
14
|
+
pi: ExtensionAPI,
|
|
15
|
+
store: MemoryStore,
|
|
16
|
+
config: MemoryConfig,
|
|
17
|
+
): void {
|
|
18
|
+
let userTurnCount = 0;
|
|
19
|
+
|
|
20
|
+
pi.on("message_end", async (event, _ctx) => {
|
|
21
|
+
if (event.message.role === "user") userTurnCount++;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** Shared flush logic — builds conversation snapshot and spawns pi -p */
|
|
25
|
+
async function flush(ctx: any, signal?: AbortSignal, timeoutMs = 30000): Promise<void> {
|
|
26
|
+
if (userTurnCount < config.flushMinTurns) return;
|
|
27
|
+
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = ctx.sessionManager.getBranch();
|
|
31
|
+
} catch {
|
|
32
|
+
return; // Context already stale
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parts: string[] = [];
|
|
36
|
+
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (entry.type !== "message") continue;
|
|
39
|
+
const msg = entry.message;
|
|
40
|
+
const text = getMessageText(msg);
|
|
41
|
+
if (!text) continue;
|
|
42
|
+
const prefix = msg.role === "user" ? "[USER]" : "[ASSISTANT]";
|
|
43
|
+
parts.push(`${prefix}: ${text}`);
|
|
44
|
+
}
|
|
45
|
+
const flushMessage = [
|
|
46
|
+
FLUSH_PROMPT,
|
|
47
|
+
"",
|
|
48
|
+
"--- Conversation ---",
|
|
49
|
+
parts.join("\n\n"),
|
|
50
|
+
].join("\n");
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await pi.exec("pi", ["-p", "--no-session", flushMessage], {
|
|
54
|
+
signal,
|
|
55
|
+
timeout: timeoutMs,
|
|
56
|
+
});
|
|
57
|
+
} catch {
|
|
58
|
+
// Best-effort flush — never block shutdown
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Flush before compaction (can afford to wait)
|
|
63
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
64
|
+
if (!config.flushOnCompact) return;
|
|
65
|
+
await flush(ctx, event.signal, 30000);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Flush before session shutdown (must be fast, non-blocking)
|
|
69
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
70
|
+
if (!config.flushOnShutdown) return;
|
|
71
|
+
// Fire-and-forget with a short timeout so we don't block Pi's shutdown.
|
|
72
|
+
// We intentionally do NOT await — Pi should not wait for the child process.
|
|
73
|
+
flush(ctx, undefined, 10000).catch(() => {});
|
|
74
|
+
});
|
|
75
|
+
}
|