pi-observational-memory 0.1.7 → 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 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
- npm install pi-observational-memory
21
+ pi install npm:pi-observational-memory
20
22
  ```
21
23
 
22
- Then symlink or copy the package into Pi's extensions directory:
24
+ Or install directly from GitHub with pi:
23
25
 
24
26
  ```bash
25
- ln -s $(npm root)/pi-observational-memory ~/.pi/extensions/pi-observational-memory
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.1.7",
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",
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
- convertToLlm,
7
- estimateTokens as estimateMessageTokens,
8
- getAgentDir,
9
- serializeConversation,
10
- } from "@mariozechner/pi-coding-agent";
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
+ }