pi-observational-memory 0.1.7 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -22
- package/package.json +1 -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
|
@@ -1,35 +1,100 @@
|
|
|
1
1
|
# pi-observational-memory
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Make Pi sessions feel endless.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Every session has a cliff. You're three hours in, the context window fills up, compaction runs, and suddenly the agent doesn't remember what you decided in hour one. You start repeating yourself. The session that was flowing now feels like a new conversation with an amnesiac.
|
|
6
|
+
|
|
7
|
+
pi-observational-memory pushes that cliff out far enough that you stop thinking about it. It replaces Pi's compaction summary with a two-tier memory system — **observations** (a timestamped, priority-tagged event log) and **reflections** (stable long-term facts) — so the agent carries forward *what* you decided, *when*, *why*, and what's already done. Not as prose that degrades with each compaction cycle, but as structured memory that stays sharp.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
<reflections>
|
|
11
|
+
- User works at Acme Corp, building "Acme Dashboard"
|
|
12
|
+
- Stack: Next.js 15, Supabase auth, server components with client-side hydration
|
|
13
|
+
- Hard constraint: ship by January 22nd 2026
|
|
14
|
+
</reflections>
|
|
15
|
+
|
|
16
|
+
<observations>
|
|
17
|
+
Date: 2026-01-15
|
|
18
|
+
- 🔴 14:30 User decided to switch from REST to GraphQL for the public API
|
|
19
|
+
- 🟡 14:32 Motivation: reduce over-fetching on mobile clients
|
|
20
|
+
- 🟢 14:35 Agent scaffolded GraphQL schema in src/schema.ts
|
|
21
|
+
- ✅ 14:50 GraphQL migration completed — user confirmed queries working
|
|
22
|
+
- 🔴 15:10 User wants rate limiting on all public endpoints
|
|
23
|
+
- 🟡 15:12 Prefers token bucket algorithm, 100 req/min per API key
|
|
24
|
+
</observations>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Hour six should feel like hour one. The agent knows who you are, what you've built together, and what's left to do.
|
|
28
|
+
|
|
29
|
+
Pi's built-in compaction handles most sessions well — it tracks file operations, manages split turns, and keeps recent messages intact. This extension is for the sessions where "most" isn't enough: long builds, multi-feature sprints, and the kind of deep work where breaking flow to start a new session costs you more than the tokens.
|
|
30
|
+
|
|
31
|
+
Inspired by [Mastra's Observational Memory](https://mastra.ai/blog/observational-memory) research (94.87% on LongMemEval). This is an independent implementation built for Pi's extension system and compaction model.
|
|
32
|
+
|
|
33
|
+
## Why this matters
|
|
34
|
+
|
|
35
|
+
Pi's default compaction summarizes old messages into prose and tracks which files were read and modified. This works well for short-to-medium sessions. But prose summaries are inherently lossy in ways that compound over time — the third compaction summarizes a summary of a summary, and specific decisions, timestamps, and completion states get flattened.
|
|
36
|
+
|
|
37
|
+
Observational memory uses a different format that's designed to survive repeated compaction cycles:
|
|
38
|
+
|
|
39
|
+
| What you get | Why it matters |
|
|
40
|
+
|---|---|
|
|
41
|
+
| 🔴 Priority tags | Agent knows what's important vs. what's noise |
|
|
42
|
+
| Timestamps | Temporal reasoning — agent knows *when* things happened |
|
|
43
|
+
| ✅ Completion markers | Agent won't redo finished work |
|
|
44
|
+
| State change tracking | "Switched from A to B" — no stale decisions |
|
|
45
|
+
| User quotes preserved | Your exact words survive compression |
|
|
46
|
+
| Reflections tier | Identity and constraints never get pruned |
|
|
6
47
|
|
|
7
48
|
## How it works
|
|
8
49
|
|
|
9
|
-
|
|
50
|
+
Two LLM passes intercept Pi's compaction to produce structured output instead of prose:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Raw messages accumulate
|
|
54
|
+
│
|
|
55
|
+
▼ exceeds observationThreshold (default: 50k tokens)
|
|
56
|
+
┌─────────┐
|
|
57
|
+
│ Observer │──▶ Compresses messages into timestamped,
|
|
58
|
+
└─────────┘ priority-tagged observations. Append-only.
|
|
59
|
+
│
|
|
60
|
+
▼ observations exceed reflectionThreshold (default: 30k tokens)
|
|
61
|
+
┌───────────┐
|
|
62
|
+
│ Reflector │──▶ Promotes stable facts to reflections.
|
|
63
|
+
└───────────┘ Prunes only what it's certain is dead.
|
|
64
|
+
│
|
|
65
|
+
▼
|
|
66
|
+
┌──────────────────────────────┐
|
|
67
|
+
│ Agent context: │
|
|
68
|
+
│ 1. System prompt │
|
|
69
|
+
│ 2. Reflections (stable) │
|
|
70
|
+
│ 3. Observations (event log) │
|
|
71
|
+
│ 4. Recent raw messages │
|
|
72
|
+
└──────────────────────────────┘
|
|
73
|
+
```
|
|
10
74
|
|
|
11
|
-
|
|
12
|
-
2. **Reflector** — when observations grow past a threshold, promotes stable facts to reflections and prunes dead observations.
|
|
75
|
+
The observer is aggressive — it compresses everything into dense observations. The reflector is conservative — it only prunes what's clearly dead. Between reflector runs, no information is lost.
|
|
13
76
|
|
|
14
|
-
|
|
77
|
+
For the full technical breakdown — compaction lifecycle, state persistence, configuration interactions — see **[docs/how-it-works.md](docs/how-it-works.md)**.
|
|
15
78
|
|
|
16
79
|
## Install
|
|
17
80
|
|
|
18
81
|
```bash
|
|
19
|
-
|
|
82
|
+
pi install npm:pi-observational-memory
|
|
20
83
|
```
|
|
21
84
|
|
|
22
|
-
|
|
85
|
+
Or from GitHub:
|
|
23
86
|
|
|
24
87
|
```bash
|
|
25
|
-
|
|
88
|
+
pi install git:github.com/elpapi42/pi-observational-memory
|
|
26
89
|
```
|
|
27
90
|
|
|
28
|
-
Pi
|
|
91
|
+
That's it. The extension hooks into Pi's compaction lifecycle automatically. No config file needed to start — defaults work well for most sessions.
|
|
29
92
|
|
|
30
93
|
## Configuration
|
|
31
94
|
|
|
32
|
-
|
|
95
|
+
### Extension settings
|
|
96
|
+
|
|
97
|
+
Create `~/.pi/agent/observational-memory.json` (or `.pi/observational-memory.json` per project):
|
|
33
98
|
|
|
34
99
|
```json
|
|
35
100
|
{
|
|
@@ -38,29 +103,60 @@ Create `~/.pi/agent/observational-memory.json`:
|
|
|
38
103
|
}
|
|
39
104
|
```
|
|
40
105
|
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
| `observationThreshold` | `
|
|
44
|
-
| `reflectionThreshold` | `
|
|
45
|
-
| `compactionModel` | session model | Optional
|
|
106
|
+
| Setting | Default | What it controls |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `observationThreshold` | `50,000` tokens | How much raw conversation accumulates before the observer runs |
|
|
109
|
+
| `reflectionThreshold` | `30,000` tokens | How large observations grow before the reflector promotes and prunes |
|
|
110
|
+
| `compactionModel` | session model | Optional — use a cheaper model for observer/reflector passes |
|
|
111
|
+
|
|
112
|
+
### Using a cheaper model for compaction
|
|
46
113
|
|
|
47
|
-
|
|
114
|
+
The observer and reflector don't need the same capabilities as your coding agent. Offload them to something fast and cheap:
|
|
48
115
|
|
|
49
116
|
```json
|
|
50
117
|
{
|
|
51
|
-
"observationThreshold": 6000,
|
|
52
|
-
"reflectionThreshold": 1000,
|
|
53
118
|
"compactionModel": { "provider": "openrouter", "id": "google/gemma-4-31b-it" }
|
|
54
119
|
}
|
|
55
120
|
```
|
|
56
121
|
|
|
122
|
+
### Pi compaction settings
|
|
123
|
+
|
|
124
|
+
The extension works with Pi's built-in compaction settings in `~/.pi/agent/settings.json`:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"compaction": {
|
|
129
|
+
"keepRecentTokens": 20000
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
| Setting | Default | What it controls |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `keepRecentTokens` | `20,000` | Tokens of recent conversation kept verbatim (not summarized) |
|
|
137
|
+
| `reserveTokens` | `16,384` | Headroom for LLM response; Pi auto-compacts when context exceeds `window - reserveTokens` |
|
|
138
|
+
|
|
139
|
+
**How they interact:** Pi decides *when* to compact and *how many recent messages to keep raw*. The extension decides *how* to compact (observations + reflections instead of a flat summary) and *when to proactively trigger* compaction before the window fills. Both paths end up at the same `session_before_compact` hook.
|
|
140
|
+
|
|
57
141
|
## Commands
|
|
58
142
|
|
|
59
143
|
| Command | Description |
|
|
60
|
-
|
|
61
|
-
| `/om-status` |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `/om-status` | Token counts, thresholds, and when the next observer/reflector pass will trigger |
|
|
62
146
|
| `/om-view` | Print current reflections and observations |
|
|
63
|
-
| `/om-view --full` | Same as above, plus raw kept messages |
|
|
147
|
+
| `/om-view --full` | Same as above, plus the raw kept messages |
|
|
148
|
+
|
|
149
|
+
## Design decisions
|
|
150
|
+
|
|
151
|
+
**Why observations are append-only.** Between reflector runs, nothing is lost. The observer only compresses new messages — it doesn't decide what to keep. This keeps the observer simple and predictable.
|
|
152
|
+
|
|
153
|
+
**Why the reflector is conservative.** It only prunes what it's certain is dead: completed tasks no longer referenced, superseded information, exact duplicates. Being old or low-priority is not a reason to prune. If in doubt, it keeps.
|
|
154
|
+
|
|
155
|
+
**Why user messages are captured near-verbatim.** When the context window shrinks, observations become the only record of what you said. Short messages are preserved exactly; long ones are summarized with key phrases quoted.
|
|
156
|
+
|
|
157
|
+
**Why state changes are explicit.** When you say "switching from A to B," the observation notes both the new state and what it replaces. This prevents the agent from acting on stale information after compaction.
|
|
158
|
+
|
|
159
|
+
**Why memory lives in the session.** State is stored in Pi's session entries as compaction `details` — no external database, no filesystem state, no separate sync. On session resume, the extension walks backward through entries to restore memory. If you use `/tree` to branch, each branch gets its own memory state.
|
|
64
160
|
|
|
65
161
|
## License
|
|
66
162
|
|
package/package.json
CHANGED
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
|
+
}
|