pi-observational-memory 0.1.6 → 1.0.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 +5 -5
- package/package.json +2 -1
- package/src/config.ts +36 -0
- package/src/index.ts +7 -315
- package/src/prompts.ts +170 -0
- package/src/tokens.ts +52 -0
- package/src/types.ts +15 -0
package/README.md
CHANGED
|
@@ -15,18 +15,18 @@ The resulting `<reflections>` + `<observations>` block becomes the compaction su
|
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
18
|
+
Install from npm with pi:
|
|
19
|
+
|
|
18
20
|
```bash
|
|
19
|
-
|
|
21
|
+
pi install npm:pi-observational-memory
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
Or install directly from GitHub with pi:
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
|
|
27
|
+
pi install https://github.com/elpapi42/pi-observational-memory
|
|
26
28
|
```
|
|
27
29
|
|
|
28
|
-
Pi will discover it on next session start.
|
|
29
|
-
|
|
30
30
|
## Configuration
|
|
31
31
|
|
|
32
32
|
Create `~/.pi/agent/observational-memory.json`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-observational-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Observational memory extension for pi — cache-friendly tiered compaction with observations and reflections.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"keywords": [
|
|
14
14
|
"pi",
|
|
15
15
|
"pi-agent",
|
|
16
|
+
"pi-package",
|
|
16
17
|
"extension",
|
|
17
18
|
"observational-memory",
|
|
18
19
|
"memory",
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export interface Config {
|
|
6
|
+
observationThreshold: number;
|
|
7
|
+
reflectionThreshold: number;
|
|
8
|
+
compactionModel?: { provider: string; id: string };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const DEFAULTS: Config = {
|
|
12
|
+
observationThreshold: 50_000,
|
|
13
|
+
reflectionThreshold: 30_000,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function loadConfig(cwd: string): Config {
|
|
17
|
+
const globalPath = join(getAgentDir(), "observational-memory.json");
|
|
18
|
+
const projectPath = join(cwd, ".pi", "observational-memory.json");
|
|
19
|
+
|
|
20
|
+
let globalConfig: Partial<Config> = {};
|
|
21
|
+
let projectConfig: Partial<Config> = {};
|
|
22
|
+
|
|
23
|
+
if (existsSync(globalPath)) {
|
|
24
|
+
try {
|
|
25
|
+
globalConfig = JSON.parse(readFileSync(globalPath, "utf-8"));
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (existsSync(projectPath)) {
|
|
30
|
+
try {
|
|
31
|
+
projectConfig = JSON.parse(readFileSync(projectPath, "utf-8"));
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { ...DEFAULTS, ...globalConfig, ...projectConfig };
|
|
36
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,312 +1,18 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
1
|
import { completeSimple } from "@mariozechner/pi-ai";
|
|
4
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "
|
|
11
|
-
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// Config
|
|
14
|
-
// ============================================================================
|
|
15
|
-
|
|
16
|
-
interface Config {
|
|
17
|
-
observationThreshold: number;
|
|
18
|
-
reflectionThreshold: number;
|
|
19
|
-
compactionModel?: { provider: string; id: string };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const DEFAULTS: Config = {
|
|
23
|
-
observationThreshold: 50_000,
|
|
24
|
-
reflectionThreshold: 30_000,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function loadConfig(cwd: string): Config {
|
|
28
|
-
const globalPath = join(getAgentDir(), "observational-memory.json");
|
|
29
|
-
const projectPath = join(cwd, ".pi", "observational-memory.json");
|
|
30
|
-
|
|
31
|
-
let globalConfig: Partial<Config> = {};
|
|
32
|
-
let projectConfig: Partial<Config> = {};
|
|
33
|
-
|
|
34
|
-
if (existsSync(globalPath)) {
|
|
35
|
-
try {
|
|
36
|
-
globalConfig = JSON.parse(readFileSync(globalPath, "utf-8"));
|
|
37
|
-
} catch {}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (existsSync(projectPath)) {
|
|
41
|
-
try {
|
|
42
|
-
projectConfig = JSON.parse(readFileSync(projectPath, "utf-8"));
|
|
43
|
-
} catch {}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return { ...DEFAULTS, ...globalConfig, ...projectConfig };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ============================================================================
|
|
50
|
-
// Types
|
|
51
|
-
// ============================================================================
|
|
52
|
-
|
|
53
|
-
interface MemoryState {
|
|
54
|
-
observations: string;
|
|
55
|
-
reflections: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface MemoryDetails {
|
|
59
|
-
type: "observational-memory";
|
|
60
|
-
version: 1;
|
|
61
|
-
observations: string;
|
|
62
|
-
reflections: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ============================================================================
|
|
66
|
-
// Helpers
|
|
67
|
-
// ============================================================================
|
|
68
|
-
|
|
69
|
-
function isMemoryDetails(d: unknown): d is MemoryDetails {
|
|
70
|
-
return !!d && typeof d === "object" && (d as Record<string, unknown>).type === "observational-memory";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function estimateTokens(text: string): number {
|
|
74
|
-
return Math.ceil(text.length / 4);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function estimateRawTailTokens(
|
|
78
|
-
entries: Array<{ type: string; message?: unknown; content?: unknown; firstKeptEntryId?: string; id?: string }>,
|
|
79
|
-
): number {
|
|
80
|
-
let startIndex = 0;
|
|
81
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
82
|
-
if (entries[i].type === "compaction") {
|
|
83
|
-
const keptId = entries[i].firstKeptEntryId;
|
|
84
|
-
if (keptId) {
|
|
85
|
-
for (let j = 0; j < entries.length; j++) {
|
|
86
|
-
if (entries[j].id === keptId) {
|
|
87
|
-
startIndex = j;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
startIndex = i + 1;
|
|
93
|
-
}
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let tokens = 0;
|
|
99
|
-
for (let i = startIndex; i < entries.length; i++) {
|
|
100
|
-
const entry = entries[i];
|
|
101
|
-
if (entry.type === "message" && entry.message) {
|
|
102
|
-
tokens += estimateMessageTokens(entry.message as Parameters<typeof estimateMessageTokens>[0]);
|
|
103
|
-
} else if (entry.type === "custom_message" && entry.content) {
|
|
104
|
-
const content = entry.content;
|
|
105
|
-
if (typeof content === "string") {
|
|
106
|
-
tokens += Math.ceil(content.length / 4);
|
|
107
|
-
} else if (Array.isArray(content)) {
|
|
108
|
-
for (const block of content) {
|
|
109
|
-
if (block.type === "text" && block.text) tokens += Math.ceil(block.text.length / 4);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return tokens;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function extractText(response: { content: Array<{ type: string; text?: string }> }): string {
|
|
118
|
-
return response.content
|
|
119
|
-
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
120
|
-
.map((c) => c.text)
|
|
121
|
-
.join("\n");
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ============================================================================
|
|
125
|
-
// Prompts
|
|
126
|
-
// ============================================================================
|
|
127
|
-
|
|
128
|
-
const OBSERVER_SYSTEM = `You are an observation agent for a coding assistant. Compress conversation messages into concise, timestamped observations.
|
|
129
|
-
|
|
130
|
-
Format as a date-grouped log:
|
|
131
|
-
|
|
132
|
-
Date: YYYY-MM-DD
|
|
133
|
-
- 🔴 HH:MM Observation text
|
|
134
|
-
- 🔴 HH:MM Sub-observation
|
|
135
|
-
- 🟡 HH:MM Sub-observation
|
|
136
|
-
- 🟢 HH:MM Another observation
|
|
137
|
-
|
|
138
|
-
Priority levels:
|
|
139
|
-
- 🔴 Important: user goals, constraints, decisions, names, deadlines, architectural choices, bugs, errors
|
|
140
|
-
- 🟡 Maybe important: questions asked, preferences, approaches considered, configuration details
|
|
141
|
-
- 🟢 Info only: routine operations, minor details
|
|
142
|
-
- ✅ Completed: a task, question, subtask, or issue is concretely resolved
|
|
143
|
-
|
|
144
|
-
CRITICAL — DISTINGUISH USER ASSERTIONS FROM QUESTIONS:
|
|
145
|
-
|
|
146
|
-
When the user TELLS you something about themselves, mark it as an assertion:
|
|
147
|
-
- "I have two kids" → 🔴 User stated has two kids
|
|
148
|
-
- "I work at Acme Corp" → 🔴 User stated works at Acme Corp
|
|
149
|
-
|
|
150
|
-
When the user ASKS about something, mark it as a question/request:
|
|
151
|
-
- "Can you help me with X?" → 🔴 User asked help with X
|
|
152
|
-
- "What's the best way to do Y?" → 🟡 User asked best way to do Y
|
|
153
|
-
|
|
154
|
-
Distinguish between QUESTIONS and STATEMENTS OF INTENT:
|
|
155
|
-
- "Can you recommend..." → Question (extract as "User asked...")
|
|
156
|
-
- "I'm looking forward to doing X" → Statement of intent (extract as "User stated they will do X (include date if mentioned)")
|
|
157
|
-
- "I need to do X" → Statement of intent (extract as "User stated they need to do X")
|
|
158
|
-
|
|
159
|
-
USER ASSERTIONS ARE AUTHORITATIVE. The user is the source of truth about their own life. If a user previously stated something and later asks a question about the same topic, the assertion is the answer — the question doesn't invalidate what they already told you.
|
|
160
|
-
|
|
161
|
-
Rules:
|
|
162
|
-
- Group observations by date, with timestamps inline.
|
|
163
|
-
- Use the three-date model when relevant: note the observation date, the referenced date (if the event refers to a different day), and a relative date (e.g. "2 days ago").
|
|
164
|
-
- Nest related sub-observations under a parent observation.
|
|
165
|
-
- Preserve exact file paths, function names, error messages, and technical details.
|
|
166
|
-
- Focus on WHAT happened and WHY, not routine tool calls.
|
|
167
|
-
- Each observation should be one concise line.
|
|
168
|
-
|
|
169
|
-
CONTENT PRESERVATION:
|
|
170
|
-
|
|
171
|
-
User message capture:
|
|
172
|
-
- Short and medium-length user messages: capture nearly verbatim.
|
|
173
|
-
- Very long user messages: summarize but quote key phrases that carry specific intent or meaning.
|
|
174
|
-
- This is critical — when the conversation window shrinks, observations are the only record of what the user said.
|
|
175
|
-
|
|
176
|
-
Preserve unusual phrasing — quote the user's exact words when non-standard:
|
|
177
|
-
- BAD: User exercised.
|
|
178
|
-
- GOOD: User stated they did a "movement session" (their term for exercise).
|
|
179
|
-
|
|
180
|
-
Use precise action verbs — replace vague verbs with specific ones:
|
|
181
|
-
- BAD: User is getting X.
|
|
182
|
-
- GOOD: User subscribed to X. (if context confirms recurring delivery)
|
|
183
|
-
- GOOD: User purchased X. (if context confirms one-time acquisition)
|
|
184
|
-
Common: "getting regularly" → "subscribed to"; "got" → "purchased"/"received"/"was given"; "stopped getting" → "canceled"/"unsubscribed from"
|
|
185
|
-
If the assistant confirms or clarifies the user's vague language, prefer the assistant's more precise terminology.
|
|
186
|
-
|
|
187
|
-
Preserve distinguishing details in assistant-generated content:
|
|
188
|
-
- BAD: Assistant recommended 5 hotels.
|
|
189
|
-
- GOOD: Assistant recommended hotels: Hotel A (near station), Hotel B (budget-friendly), Hotel C (rooftop pool).
|
|
190
|
-
- BAD: Assistant provided social media accounts.
|
|
191
|
-
- GOOD: Assistant provided accounts: @user_one (portraits), @user_two (landscapes).
|
|
192
|
-
|
|
193
|
-
Preserve specific technical/numerical values:
|
|
194
|
-
- BAD: Assistant explained the performance improvements.
|
|
195
|
-
- GOOD: Optimization achieved 43.7% faster load times, memory dropped from 2.8GB to 940MB.
|
|
196
|
-
|
|
197
|
-
Preserve role/participation when user mentions their involvement:
|
|
198
|
-
- BAD: User attended the company event.
|
|
199
|
-
- GOOD: User was a presenter at the company event.
|
|
200
|
-
|
|
201
|
-
Code context — always preserve: exact file paths with line numbers, error messages verbatim, function/variable names, architectural decisions and rationale.
|
|
202
|
-
|
|
203
|
-
STATE CHANGES AND UPDATES:
|
|
204
|
-
When a user indicates they are changing something, frame it as a state change that supersedes previous information:
|
|
205
|
-
- "I'm going to start doing X instead of Y" → "User will start doing X (changing from Y)"
|
|
206
|
-
- "I'm switching from A to B" → "User is switching from A to B"
|
|
207
|
-
- "I moved my stuff to the new place" → "User moved to the new place (no longer at previous location)"
|
|
208
|
-
|
|
209
|
-
If the new state contradicts or updates previous information, make that explicit:
|
|
210
|
-
- BAD: User plans to use the new method.
|
|
211
|
-
- GOOD: User will use the new method (replacing the old approach).
|
|
212
|
-
- Do NOT repeat information already captured in existing reflections or observations.
|
|
213
|
-
- Do NOT wrap output in code blocks or markdown fences.
|
|
214
|
-
|
|
215
|
-
AVOIDING REPETITIVE OBSERVATIONS:
|
|
216
|
-
- Do NOT repeat the same observation across multiple turns if there is no new information.
|
|
217
|
-
- When the agent performs repeated similar actions (e.g., browsing files, running the same tool type multiple times), group them into a single parent observation with sub-bullets for each new result.
|
|
218
|
-
|
|
219
|
-
BAD (repetitive):
|
|
220
|
-
- 🟡 14:30 Agent used view tool on src/auth.ts
|
|
221
|
-
- 🟡 14:31 Agent used view tool on src/users.ts
|
|
222
|
-
- 🟡 14:32 Agent used view tool on src/routes.ts
|
|
223
|
-
|
|
224
|
-
GOOD (grouped):
|
|
225
|
-
- 🟡 14:30 Agent investigated auth flow
|
|
226
|
-
- -> viewed src/auth.ts — found token validation logic
|
|
227
|
-
- -> viewed src/users.ts — found user lookup by email
|
|
228
|
-
- -> viewed src/routes.ts — found middleware chain
|
|
229
|
-
|
|
230
|
-
Only add a new observation for a repeated action if the NEW result changes the picture.
|
|
231
|
-
|
|
232
|
-
COMPLETION TRACKING:
|
|
233
|
-
✅ markers are explicit memory signals telling the assistant that work is finished and should not be repeated.
|
|
234
|
-
|
|
235
|
-
Use ✅ when:
|
|
236
|
-
- The user explicitly confirms something worked ("thanks, that fixed it", "got it", "perfect")
|
|
237
|
-
- The assistant provided a definitive answer and the user moved on
|
|
238
|
-
- A multi-step task reached its stated goal
|
|
239
|
-
- The user acknowledged receipt of requested information
|
|
240
|
-
- A concrete subtask or implementation step completed during ongoing work
|
|
241
|
-
|
|
242
|
-
Do NOT use ✅ when:
|
|
243
|
-
- The assistant merely responded — the user might follow up with corrections
|
|
244
|
-
- The topic is paused but not resolved ("I'll try that later")
|
|
245
|
-
- The user's reaction is ambiguous
|
|
246
|
-
|
|
247
|
-
Two formats:
|
|
248
|
-
As a sub-bullet under a parent observation:
|
|
249
|
-
- 🔴 HH:MM User asked how to configure auth middleware
|
|
250
|
-
- -> Agent explained JWT setup with code example
|
|
251
|
-
- ✅ User confirmed auth is working
|
|
252
|
-
|
|
253
|
-
Or standalone when closing a broader task:
|
|
254
|
-
- ✅ HH:MM Auth configuration completed — user confirmed middleware is working
|
|
255
|
-
|
|
256
|
-
Completion observations should be terse but specific about WHAT was completed. Prefer concrete resolved outcomes over abstract workflow status.`;
|
|
257
|
-
|
|
258
|
-
const REFLECTOR_SYSTEM = `You are a reflection agent for a coding assistant. Your job is to maintain long-term reflections while preserving as many observations as possible.
|
|
259
|
-
|
|
260
|
-
You will receive current reflections (long-term facts) and accumulated observations.
|
|
261
|
-
|
|
262
|
-
Your task:
|
|
263
|
-
1. PROMOTE observations to reflections ONLY when they are clearly stable, long-lived facts:
|
|
264
|
-
- User identity, role, preferences
|
|
265
|
-
- Project goals and architecture decisions
|
|
266
|
-
- Permanent constraints and requirements
|
|
267
|
-
- Key technical decisions and their rationale
|
|
268
|
-
After promoting, KEEP the original observation — do not remove it.
|
|
269
|
-
2. PRUNE observations ONLY when you are certain they are dead:
|
|
270
|
-
- Tasks explicitly completed AND no longer referenced
|
|
271
|
-
- Information directly contradicted or superseded by a newer observation
|
|
272
|
-
- Exact duplicates of other observations
|
|
273
|
-
When in doubt, KEEP the observation.
|
|
274
|
-
IMPORTANT: Preserve ✅ completion markers — they tell the assistant what is already resolved and prevent repeated work. Preserve the concrete resolved outcome captured by ✅ markers. When pruning detailed steps of a completed task, keep the ✅ outcome line.
|
|
275
|
-
USER ASSERTIONS vs QUESTIONS: "User stated: X" = authoritative assertion. "User asked: X" = question/request. When consolidating, USER ASSERTIONS TAKE PRECEDENCE. If you see both "User stated: has two kids" and later "User asked: how many kids?", keep the assertion — the question doesn't invalidate what they told you.
|
|
276
|
-
3. KEEP everything else. Most observations should survive. An observation being old or low-priority (🟢) is NOT a reason to remove it.
|
|
277
|
-
4. UPDATE reflections: merge new promoted facts into existing reflections. Remove reflections only if directly contradicted by observations.
|
|
278
|
-
|
|
279
|
-
Output EXACTLY two sections with these tags:
|
|
280
|
-
|
|
281
|
-
<reflections>
|
|
282
|
-
[Updated long-term reflections — stable facts, one per line]
|
|
283
|
-
</reflections>
|
|
284
|
-
|
|
285
|
-
<observations>
|
|
286
|
-
[Surviving observations in the same date-grouped log format — most should be preserved]
|
|
287
|
-
</observations>
|
|
288
|
-
|
|
289
|
-
Do NOT wrap output in code blocks or markdown fences.`;
|
|
290
|
-
|
|
291
|
-
const CONTEXT_USAGE_INSTRUCTIONS = `KNOWLEDGE UPDATES: When observations contain conflicting information, prefer the MOST RECENT observation (check dates). Look for state-change phrases like "will start", "is switching", "changed to", "replacing" as indicators that older information has been superseded.
|
|
292
|
-
|
|
293
|
-
PLANNED ACTIONS: If an observation says the user planned to do something and the date is now in the past, assume they completed the action unless there's evidence they didn't.
|
|
294
|
-
|
|
295
|
-
USER ASSERTIONS: When observations contain both "User stated: X" and "User asked: X" about the same topic, the assertion is authoritative — the user is the source of truth about their own life.
|
|
296
|
-
|
|
297
|
-
COMPLETION MARKERS: Observations marked with ✅ indicate completed work. Do not re-do or re-investigate tasks marked as complete unless the user explicitly asks to revisit them.`;
|
|
298
|
-
|
|
299
|
-
// ============================================================================
|
|
300
|
-
// Extension
|
|
301
|
-
// ============================================================================
|
|
3
|
+
import { convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import { DEFAULTS, loadConfig } from "./config.js";
|
|
5
|
+
import type { Config } from "./config.js";
|
|
6
|
+
import { CONTEXT_USAGE_INSTRUCTIONS, OBSERVER_SYSTEM, REFLECTOR_SYSTEM } from "./prompts.js";
|
|
7
|
+
import { estimateRawTailTokens, estimateTokens, extractText } from "./tokens.js";
|
|
8
|
+
import type { MemoryDetails, MemoryState } from "./types.js";
|
|
9
|
+
import { isMemoryDetails } from "./types.js";
|
|
302
10
|
|
|
303
11
|
export default function observationalMemory(pi: ExtensionAPI) {
|
|
304
12
|
let config: Config = { ...DEFAULTS };
|
|
305
13
|
let state: MemoryState = { observations: "", reflections: "" };
|
|
306
14
|
let compactInFlight = false;
|
|
307
15
|
|
|
308
|
-
// ---- Restore state from last compaction entry ----
|
|
309
|
-
|
|
310
16
|
pi.on("session_start", (_event, ctx) => {
|
|
311
17
|
config = loadConfig(ctx.cwd);
|
|
312
18
|
state = { observations: "", reflections: "" };
|
|
@@ -323,8 +29,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
323
29
|
}
|
|
324
30
|
});
|
|
325
31
|
|
|
326
|
-
// ---- Trigger compaction when raw tail exceeds threshold ----
|
|
327
|
-
|
|
328
32
|
pi.on("agent_end", (_event, ctx) => {
|
|
329
33
|
if (compactInFlight) return;
|
|
330
34
|
|
|
@@ -351,8 +55,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
351
55
|
}, 0);
|
|
352
56
|
});
|
|
353
57
|
|
|
354
|
-
// ---- Custom compaction: observer + reflector ----
|
|
355
|
-
|
|
356
58
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
357
59
|
const { preparation, signal } = event;
|
|
358
60
|
const { messagesToSummarize, turnPrefixMessages, firstKeptEntryId, tokensBefore } = preparation;
|
|
@@ -382,8 +84,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
382
84
|
const dateStr = now.toISOString().split("T")[0];
|
|
383
85
|
const timeStr = now.toTimeString().slice(0, 5);
|
|
384
86
|
|
|
385
|
-
// ---- Run observer ----
|
|
386
|
-
|
|
387
87
|
ctx.ui.notify("Observational memory: running observer...", "info");
|
|
388
88
|
|
|
389
89
|
try {
|
|
@@ -423,8 +123,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
423
123
|
return;
|
|
424
124
|
}
|
|
425
125
|
|
|
426
|
-
// ---- Run reflector if observations are too large ----
|
|
427
|
-
|
|
428
126
|
if (estimateTokens(state.observations) > config.reflectionThreshold) {
|
|
429
127
|
ctx.ui.notify("Observational memory: running reflector...", "info");
|
|
430
128
|
|
|
@@ -465,8 +163,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
465
163
|
}
|
|
466
164
|
}
|
|
467
165
|
|
|
468
|
-
// ---- Build summary ----
|
|
469
|
-
|
|
470
166
|
let summary = "";
|
|
471
167
|
if (state.reflections) {
|
|
472
168
|
summary += `<reflections>\n${state.reflections}\n</reflections>\n\n`;
|
|
@@ -496,8 +192,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
496
192
|
};
|
|
497
193
|
});
|
|
498
194
|
|
|
499
|
-
// ---- /om-status command ----
|
|
500
|
-
|
|
501
195
|
pi.registerCommand("om-status", {
|
|
502
196
|
description: "Show observational memory status",
|
|
503
197
|
handler: async (_args, ctx) => {
|
|
@@ -521,8 +215,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
521
215
|
},
|
|
522
216
|
});
|
|
523
217
|
|
|
524
|
-
// ---- /om-view command ----
|
|
525
|
-
|
|
526
218
|
pi.registerCommand("om-view", {
|
|
527
219
|
description: "Print full observational memory contents (--full to include raw messages)",
|
|
528
220
|
handler: async (args, ctx) => {
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export const OBSERVER_SYSTEM = `You are an observation agent for a coding assistant. Compress conversation messages into concise, timestamped observations.
|
|
2
|
+
|
|
3
|
+
Format as a date-grouped log:
|
|
4
|
+
|
|
5
|
+
Date: YYYY-MM-DD
|
|
6
|
+
- 🔴 HH:MM Observation text
|
|
7
|
+
- 🔴 HH:MM Sub-observation
|
|
8
|
+
- 🟡 HH:MM Sub-observation
|
|
9
|
+
- 🟢 HH:MM Another observation
|
|
10
|
+
|
|
11
|
+
Priority levels:
|
|
12
|
+
- 🔴 Important: user goals, constraints, decisions, names, deadlines, architectural choices, bugs, errors
|
|
13
|
+
- 🟡 Maybe important: questions asked, preferences, approaches considered, configuration details
|
|
14
|
+
- 🟢 Info only: routine operations, minor details
|
|
15
|
+
- ✅ Completed: a task, question, subtask, or issue is concretely resolved
|
|
16
|
+
|
|
17
|
+
CRITICAL — DISTINGUISH USER ASSERTIONS FROM QUESTIONS:
|
|
18
|
+
|
|
19
|
+
When the user TELLS you something about themselves, mark it as an assertion:
|
|
20
|
+
- "I have two kids" → 🔴 User stated has two kids
|
|
21
|
+
- "I work at Acme Corp" → 🔴 User stated works at Acme Corp
|
|
22
|
+
|
|
23
|
+
When the user ASKS about something, mark it as a question/request:
|
|
24
|
+
- "Can you help me with X?" → 🔴 User asked help with X
|
|
25
|
+
- "What's the best way to do Y?" → 🟡 User asked best way to do Y
|
|
26
|
+
|
|
27
|
+
Distinguish between QUESTIONS and STATEMENTS OF INTENT:
|
|
28
|
+
- "Can you recommend..." → Question (extract as "User asked...")
|
|
29
|
+
- "I'm looking forward to doing X" → Statement of intent (extract as "User stated they will do X (include date if mentioned)")
|
|
30
|
+
- "I need to do X" → Statement of intent (extract as "User stated they need to do X")
|
|
31
|
+
|
|
32
|
+
USER ASSERTIONS ARE AUTHORITATIVE. The user is the source of truth about their own life. If a user previously stated something and later asks a question about the same topic, the assertion is the answer — the question doesn't invalidate what they already told you.
|
|
33
|
+
|
|
34
|
+
Rules:
|
|
35
|
+
- Group observations by date, with timestamps inline.
|
|
36
|
+
- Use the three-date model when relevant: note the observation date, the referenced date (if the event refers to a different day), and a relative date (e.g. "2 days ago").
|
|
37
|
+
- Nest related sub-observations under a parent observation.
|
|
38
|
+
- Preserve exact file paths, function names, error messages, and technical details.
|
|
39
|
+
- Focus on WHAT happened and WHY, not routine tool calls.
|
|
40
|
+
- Each observation should be one concise line.
|
|
41
|
+
|
|
42
|
+
CONTENT PRESERVATION:
|
|
43
|
+
|
|
44
|
+
User message capture:
|
|
45
|
+
- Short and medium-length user messages: capture nearly verbatim.
|
|
46
|
+
- Very long user messages: summarize but quote key phrases that carry specific intent or meaning.
|
|
47
|
+
- This is critical — when the conversation window shrinks, observations are the only record of what the user said.
|
|
48
|
+
|
|
49
|
+
Preserve unusual phrasing — quote the user's exact words when non-standard:
|
|
50
|
+
- BAD: User exercised.
|
|
51
|
+
- GOOD: User stated they did a "movement session" (their term for exercise).
|
|
52
|
+
|
|
53
|
+
Use precise action verbs — replace vague verbs with specific ones:
|
|
54
|
+
- BAD: User is getting X.
|
|
55
|
+
- GOOD: User subscribed to X. (if context confirms recurring delivery)
|
|
56
|
+
- GOOD: User purchased X. (if context confirms one-time acquisition)
|
|
57
|
+
Common: "getting regularly" → "subscribed to"; "got" → "purchased"/"received"/"was given"; "stopped getting" → "canceled"/"unsubscribed from"
|
|
58
|
+
If the assistant confirms or clarifies the user's vague language, prefer the assistant's more precise terminology.
|
|
59
|
+
|
|
60
|
+
Preserve distinguishing details in assistant-generated content:
|
|
61
|
+
- BAD: Assistant recommended 5 hotels.
|
|
62
|
+
- GOOD: Assistant recommended hotels: Hotel A (near station), Hotel B (budget-friendly), Hotel C (rooftop pool).
|
|
63
|
+
- BAD: Assistant provided social media accounts.
|
|
64
|
+
- GOOD: Assistant provided accounts: @user_one (portraits), @user_two (landscapes).
|
|
65
|
+
|
|
66
|
+
Preserve specific technical/numerical values:
|
|
67
|
+
- BAD: Assistant explained the performance improvements.
|
|
68
|
+
- GOOD: Optimization achieved 43.7% faster load times, memory dropped from 2.8GB to 940MB.
|
|
69
|
+
|
|
70
|
+
Preserve role/participation when user mentions their involvement:
|
|
71
|
+
- BAD: User attended the company event.
|
|
72
|
+
- GOOD: User was a presenter at the company event.
|
|
73
|
+
|
|
74
|
+
Code context — always preserve: exact file paths with line numbers, error messages verbatim, function/variable names, architectural decisions and rationale.
|
|
75
|
+
|
|
76
|
+
STATE CHANGES AND UPDATES:
|
|
77
|
+
When a user indicates they are changing something, frame it as a state change that supersedes previous information:
|
|
78
|
+
- "I'm going to start doing X instead of Y" → "User will start doing X (changing from Y)"
|
|
79
|
+
- "I'm switching from A to B" → "User is switching from A to B"
|
|
80
|
+
- "I moved my stuff to the new place" → "User moved to the new place (no longer at previous location)"
|
|
81
|
+
|
|
82
|
+
If the new state contradicts or updates previous information, make that explicit:
|
|
83
|
+
- BAD: User plans to use the new method.
|
|
84
|
+
- GOOD: User will use the new method (replacing the old approach).
|
|
85
|
+
- Do NOT repeat information already captured in existing reflections or observations.
|
|
86
|
+
- Do NOT wrap output in code blocks or markdown fences.
|
|
87
|
+
|
|
88
|
+
AVOIDING REPETITIVE OBSERVATIONS:
|
|
89
|
+
- Do NOT repeat the same observation across multiple turns if there is no new information.
|
|
90
|
+
- When the agent performs repeated similar actions (e.g., browsing files, running the same tool type multiple times), group them into a single parent observation with sub-bullets for each new result.
|
|
91
|
+
|
|
92
|
+
BAD (repetitive):
|
|
93
|
+
- 🟡 14:30 Agent used view tool on src/auth.ts
|
|
94
|
+
- 🟡 14:31 Agent used view tool on src/users.ts
|
|
95
|
+
- 🟡 14:32 Agent used view tool on src/routes.ts
|
|
96
|
+
|
|
97
|
+
GOOD (grouped):
|
|
98
|
+
- 🟡 14:30 Agent investigated auth flow
|
|
99
|
+
- -> viewed src/auth.ts — found token validation logic
|
|
100
|
+
- -> viewed src/users.ts — found user lookup by email
|
|
101
|
+
- -> viewed src/routes.ts — found middleware chain
|
|
102
|
+
|
|
103
|
+
Only add a new observation for a repeated action if the NEW result changes the picture.
|
|
104
|
+
|
|
105
|
+
COMPLETION TRACKING:
|
|
106
|
+
✅ markers are explicit memory signals telling the assistant that work is finished and should not be repeated.
|
|
107
|
+
|
|
108
|
+
Use ✅ when:
|
|
109
|
+
- The user explicitly confirms something worked ("thanks, that fixed it", "got it", "perfect")
|
|
110
|
+
- The assistant provided a definitive answer and the user moved on
|
|
111
|
+
- A multi-step task reached its stated goal
|
|
112
|
+
- The user acknowledged receipt of requested information
|
|
113
|
+
- A concrete subtask or implementation step completed during ongoing work
|
|
114
|
+
|
|
115
|
+
Do NOT use ✅ when:
|
|
116
|
+
- The assistant merely responded — the user might follow up with corrections
|
|
117
|
+
- The topic is paused but not resolved ("I'll try that later")
|
|
118
|
+
- The user's reaction is ambiguous
|
|
119
|
+
|
|
120
|
+
Two formats:
|
|
121
|
+
As a sub-bullet under a parent observation:
|
|
122
|
+
- 🔴 HH:MM User asked how to configure auth middleware
|
|
123
|
+
- -> Agent explained JWT setup with code example
|
|
124
|
+
- ✅ User confirmed auth is working
|
|
125
|
+
|
|
126
|
+
Or standalone when closing a broader task:
|
|
127
|
+
- ✅ HH:MM Auth configuration completed — user confirmed middleware is working
|
|
128
|
+
|
|
129
|
+
Completion observations should be terse but specific about WHAT was completed. Prefer concrete resolved outcomes over abstract workflow status.`;
|
|
130
|
+
|
|
131
|
+
export const REFLECTOR_SYSTEM = `You are a reflection agent for a coding assistant. Your job is to maintain long-term reflections while preserving as many observations as possible.
|
|
132
|
+
|
|
133
|
+
You will receive current reflections (long-term facts) and accumulated observations.
|
|
134
|
+
|
|
135
|
+
Your task:
|
|
136
|
+
1. PROMOTE observations to reflections ONLY when they are clearly stable, long-lived facts:
|
|
137
|
+
- User identity, role, preferences
|
|
138
|
+
- Project goals and architecture decisions
|
|
139
|
+
- Permanent constraints and requirements
|
|
140
|
+
- Key technical decisions and their rationale
|
|
141
|
+
After promoting, KEEP the original observation — do not remove it.
|
|
142
|
+
2. PRUNE observations ONLY when you are certain they are dead:
|
|
143
|
+
- Tasks explicitly completed AND no longer referenced
|
|
144
|
+
- Information directly contradicted or superseded by a newer observation
|
|
145
|
+
- Exact duplicates of other observations
|
|
146
|
+
When in doubt, KEEP the observation.
|
|
147
|
+
IMPORTANT: Preserve ✅ completion markers — they tell the assistant what is already resolved and prevent repeated work. Preserve the concrete resolved outcome captured by ✅ markers. When pruning detailed steps of a completed task, keep the ✅ outcome line.
|
|
148
|
+
USER ASSERTIONS vs QUESTIONS: "User stated: X" = authoritative assertion. "User asked: X" = question/request. When consolidating, USER ASSERTIONS TAKE PRECEDENCE. If you see both "User stated: has two kids" and later "User asked: how many kids?", keep the assertion — the question doesn't invalidate what they told you.
|
|
149
|
+
3. KEEP everything else. Most observations should survive. An observation being old or low-priority (🟢) is NOT a reason to remove it.
|
|
150
|
+
4. UPDATE reflections: merge new promoted facts into existing reflections. Remove reflections only if directly contradicted by observations.
|
|
151
|
+
|
|
152
|
+
Output EXACTLY two sections with these tags:
|
|
153
|
+
|
|
154
|
+
<reflections>
|
|
155
|
+
[Updated long-term reflections — stable facts, one per line]
|
|
156
|
+
</reflections>
|
|
157
|
+
|
|
158
|
+
<observations>
|
|
159
|
+
[Surviving observations in the same date-grouped log format — most should be preserved]
|
|
160
|
+
</observations>
|
|
161
|
+
|
|
162
|
+
Do NOT wrap output in code blocks or markdown fences.`;
|
|
163
|
+
|
|
164
|
+
export const CONTEXT_USAGE_INSTRUCTIONS = `KNOWLEDGE UPDATES: When observations contain conflicting information, prefer the MOST RECENT observation (check dates). Look for state-change phrases like "will start", "is switching", "changed to", "replacing" as indicators that older information has been superseded.
|
|
165
|
+
|
|
166
|
+
PLANNED ACTIONS: If an observation says the user planned to do something and the date is now in the past, assume they completed the action unless there's evidence they didn't.
|
|
167
|
+
|
|
168
|
+
USER ASSERTIONS: When observations contain both "User stated: X" and "User asked: X" about the same topic, the assertion is authoritative — the user is the source of truth about their own life.
|
|
169
|
+
|
|
170
|
+
COMPLETION MARKERS: Observations marked with ✅ indicate completed work. Do not re-do or re-investigate tasks marked as complete unless the user explicitly asks to revisit them.`;
|
package/src/tokens.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { estimateTokens as estimateMessageTokens } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export function estimateTokens(text: string): number {
|
|
4
|
+
return Math.ceil(text.length / 4);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function estimateRawTailTokens(
|
|
8
|
+
entries: Array<{ type: string; message?: unknown; content?: unknown; firstKeptEntryId?: string; id?: string }>,
|
|
9
|
+
): number {
|
|
10
|
+
let startIndex = 0;
|
|
11
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
12
|
+
if (entries[i].type === "compaction") {
|
|
13
|
+
const keptId = entries[i].firstKeptEntryId;
|
|
14
|
+
if (keptId) {
|
|
15
|
+
for (let j = 0; j < entries.length; j++) {
|
|
16
|
+
if (entries[j].id === keptId) {
|
|
17
|
+
startIndex = j;
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
startIndex = i + 1;
|
|
23
|
+
}
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let tokens = 0;
|
|
29
|
+
for (let i = startIndex; i < entries.length; i++) {
|
|
30
|
+
const entry = entries[i];
|
|
31
|
+
if (entry.type === "message" && entry.message) {
|
|
32
|
+
tokens += estimateMessageTokens(entry.message as Parameters<typeof estimateMessageTokens>[0]);
|
|
33
|
+
} else if (entry.type === "custom_message" && entry.content) {
|
|
34
|
+
const content = entry.content;
|
|
35
|
+
if (typeof content === "string") {
|
|
36
|
+
tokens += Math.ceil(content.length / 4);
|
|
37
|
+
} else if (Array.isArray(content)) {
|
|
38
|
+
for (const block of content) {
|
|
39
|
+
if (block.type === "text" && block.text) tokens += Math.ceil(block.text.length / 4);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return tokens;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function extractText(response: { content: Array<{ type: string; text?: string }> }): string {
|
|
48
|
+
return response.content
|
|
49
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
50
|
+
.map((c) => c.text)
|
|
51
|
+
.join("\n");
|
|
52
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface MemoryState {
|
|
2
|
+
observations: string;
|
|
3
|
+
reflections: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface MemoryDetails {
|
|
7
|
+
type: "observational-memory";
|
|
8
|
+
version: 1;
|
|
9
|
+
observations: string;
|
|
10
|
+
reflections: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isMemoryDetails(d: unknown): d is MemoryDetails {
|
|
14
|
+
return !!d && typeof d === "object" && (d as Record<string, unknown>).type === "observational-memory";
|
|
15
|
+
}
|