pi-observational-memory 1.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -104
- package/package.json +1 -1
- package/src/branch.ts +190 -0
- package/src/commands/status.ts +80 -0
- package/src/commands/view.ts +79 -0
- package/src/compaction.ts +331 -0
- package/src/config.ts +6 -4
- package/src/hooks/compaction-hook.ts +201 -0
- package/src/hooks/compaction-trigger.ts +68 -0
- package/src/hooks/observer-trigger.ts +89 -0
- package/src/ids.ts +5 -0
- package/src/index.ts +12 -333
- package/src/observer.ts +143 -0
- package/src/prompts.ts +270 -168
- package/src/relevance.ts +15 -0
- package/src/runtime.ts +76 -0
- package/src/serialize.ts +103 -0
- package/src/tokens.ts +16 -41
- package/src/types.ts +56 -7
package/src/prompts.ts
CHANGED
|
@@ -1,170 +1,272 @@
|
|
|
1
|
-
export const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
When the user
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
1
|
+
export const MEMORY_STAKES = `These records are the ONLY information the assistant will have about past interactions once the raw conversation is compacted out of context. Anything you do not capture here will be forgotten. Anything you distort here will be remembered wrong. Take this seriously.`;
|
|
2
|
+
|
|
3
|
+
export const OBSERVATION_CONTENT_RULES = `Observation content rules:
|
|
4
|
+
|
|
5
|
+
Format.
|
|
6
|
+
- Single line of plain prose. No markdown, no bullets, no code fences, no XML/HTML tags, no emojis.
|
|
7
|
+
- Do NOT include the timestamp or relevance inside the content string — those are separate fields.
|
|
8
|
+
- No structured fields embedded in the text (no "key: value" lines, no JSON).
|
|
9
|
+
|
|
10
|
+
Preserve user assertions exactly.
|
|
11
|
+
When the user TELLS you something about themselves, their project, or their environment, capture it as an assertion. When the user ASKS something, capture it as a question. Assertions are authoritative — a later question on the same topic does not invalidate them.
|
|
12
|
+
BAD: User wondered if they have two kids.
|
|
13
|
+
GOOD: User stated they have two kids.
|
|
14
|
+
BAD: User discussed auth middleware.
|
|
15
|
+
GOOD: User asked how to configure JWT auth middleware.
|
|
16
|
+
Why this matters: if the user says "I use Postgres" and later asks "what db am I on?", downstream agents must treat the assertion as the answer, not the question.
|
|
17
|
+
|
|
18
|
+
Preserve unusual phrasing.
|
|
19
|
+
When the user uses non-standard terminology, quote their exact words so future runs can recognize the term.
|
|
20
|
+
BAD: User exercised yesterday.
|
|
21
|
+
GOOD: User stated they did a "movement session" (their term) yesterday.
|
|
22
|
+
|
|
23
|
+
Use precise action verbs. Replace vague verbs with ones that clarify the nature of the action.
|
|
24
|
+
BAD: User got a new subscription.
|
|
25
|
+
GOOD: User subscribed to the Pro plan.
|
|
26
|
+
BAD: User stopped getting the newsletter.
|
|
27
|
+
GOOD: User unsubscribed from the newsletter.
|
|
28
|
+
BAD: User got the library.
|
|
29
|
+
GOOD: User installed the zod package via pnpm.
|
|
30
|
+
|
|
31
|
+
Frame state changes as supersession so the old state is explicit.
|
|
32
|
+
BAD: User prefers React Query now.
|
|
33
|
+
GOOD: User will use React Query (switching from SWR).
|
|
34
|
+
Why this matters: without supersession framing, the reflector may crystallize both the old and the new as equally valid preferences.
|
|
165
35
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
36
|
+
Mark concrete completions explicitly.
|
|
37
|
+
Use "completed:", "resolved:", "confirmed working", or similar phrasing so future runs know not to redo the work.
|
|
38
|
+
BAD: Wrote the login handler.
|
|
39
|
+
GOOD: completed: implemented login handler at src/auth/login.ts; user confirmed tests pass.
|
|
40
|
+
Why this matters: without a completion marker, a later assistant may re-implement work that is already done, wasting the user's time and risking regressions.
|
|
169
41
|
|
|
170
|
-
|
|
42
|
+
Split compound statements into separate observations.
|
|
43
|
+
If a single message contains multiple independent facts, intents, or events, emit one observation per fact. One observation per line is what enables downstream retrieval and pruning to operate at fact granularity.
|
|
44
|
+
BAD: User will visit their parents this weekend and needs to clean the garage.
|
|
45
|
+
GOOD: User will visit their parents this weekend. + User stated they need to clean the garage this weekend.
|
|
46
|
+
BAD: User started a new job and is moving to a new apartment next week.
|
|
47
|
+
GOOD: User started a new job. + User will move to a new apartment next week.
|
|
48
|
+
BAD: Assistant recommended Lucia, NextAuth, and Clerk for auth, and user chose Lucia.
|
|
49
|
+
GOOD: Assistant recommended auth libraries: Lucia (session-based, minimal), NextAuth (OAuth-heavy, Next-native), Clerk (hosted, paid). + User chose Lucia.
|
|
50
|
+
Why this matters: a future query like "which auth library did the user pick?" can match a single-fact observation cleanly; a compound observation hides the decision inside a recommendation list.
|
|
51
|
+
|
|
52
|
+
Group repeated similar tool calls into a single observation rather than one per call.
|
|
53
|
+
BAD: Agent viewed src/auth.ts. Agent viewed src/users.ts. Agent viewed src/routes.ts.
|
|
54
|
+
GOOD: Agent surveyed auth-related files (src/auth.ts, src/users.ts, src/routes.ts) and located token validation in src/auth.ts:45.`;
|
|
55
|
+
|
|
56
|
+
export const DETAIL_PRESERVATION_SCHEMA = `Detail preservation. When an observation references specific things, preserve the distinguishing details so future queries can still find them:
|
|
57
|
+
|
|
58
|
+
- File/location: full path + line number when relevant (src/auth.ts:45, not "the auth file").
|
|
59
|
+
- Identifiers and names: package names, function names, variable names, handles, ticket ids, commit SHAs, error codes. Keep them verbatim.
|
|
60
|
+
- Error messages: quote verbatim.
|
|
61
|
+
BAD: Build failed with a type error.
|
|
62
|
+
GOOD: Build failed: TS2322: Type 'string | undefined' is not assignable to type 'string' at src/auth.ts:47.
|
|
63
|
+
- Numerical results: exact values, units, and direction.
|
|
64
|
+
BAD: Optimization made it faster.
|
|
65
|
+
GOOD: Optimization reduced p95 latency from 420ms to 180ms (57% faster).
|
|
66
|
+
- Quantities and counts: "3 failing tests (auth.test.ts, users.test.ts, routes.test.ts)" not "some failing tests".
|
|
67
|
+
- Recommendation or decision lists: preserve the distinguishing attribute per item.
|
|
68
|
+
BAD: Assistant recommended 3 auth libraries.
|
|
69
|
+
GOOD: Assistant recommended auth libraries: Lucia (session-based, minimal), NextAuth (OAuth-heavy, Next-native), Clerk (hosted, paid).
|
|
70
|
+
- Role / participation: capture the user's role at an event, not just attendance.
|
|
71
|
+
BAD: User worked on the migration.
|
|
72
|
+
GOOD: User led the migration from MySQL to Postgres.
|
|
73
|
+
|
|
74
|
+
If a detail is non-obvious from the code or git history, it belongs in the observation. If it is trivially re-derivable, it does not.`;
|
|
75
|
+
|
|
76
|
+
export const RELEVANCE_RUBRIC = `Relevance levels (pick one per observation; this field drives future pruning):
|
|
77
|
+
|
|
78
|
+
- critical: user assertions about identity, role, or persistent preferences; explicit corrections ("no, don't do X"); concrete completions that future runs MUST NOT redo. These are load-bearing and will NEVER be dropped. Why this matters: if a "critical" item is lost, the assistant may redo finished work, contradict a correction, or misrepresent who the user is.
|
|
79
|
+
- high: non-trivial technical decisions, architectural direction, unresolved blockers, key constraints. Worth keeping across many compactions.
|
|
80
|
+
- medium: task-level context that helps within the current work but isn't durable. The default when you are unsure between medium and high.
|
|
81
|
+
- low: routine tool-call acks, repetitive status updates, content trivially re-derivable from recent messages. The pruner will drop these first.
|
|
82
|
+
|
|
83
|
+
Do NOT default to "critical" or "high". Most observations are medium or low. Reserve "critical" for things that would cause real damage if forgotten.
|
|
84
|
+
|
|
85
|
+
BAD: relevance=critical for "Agent ran tests and they passed."
|
|
86
|
+
GOOD: relevance=low for "Agent ran tests and they passed." (routine; captured by a completion observation if it matters)
|
|
87
|
+
|
|
88
|
+
BAD: relevance=medium for "User said they are colorblind; red/green indicators do not work for them."
|
|
89
|
+
GOOD: relevance=critical for "User said they are colorblind; red/green indicators do not work for them." (persistent constraint; forgetting it causes real harm)`;
|
|
90
|
+
|
|
91
|
+
export const OBSERVER_SYSTEM = `You are the observation agent for a coding assistant.
|
|
92
|
+
|
|
93
|
+
${MEMORY_STAKES}
|
|
94
|
+
|
|
95
|
+
Your job is to compress a chunk of recent conversation into timestamped, rated observations by calling the record_observations tool. The observations you emit — together with the reflections crystallized from them — are the assistant's ONLY memory of this session after the raw conversation falls out of context.
|
|
96
|
+
|
|
97
|
+
You receive:
|
|
98
|
+
- Current reflections (long-lived facts already crystallized).
|
|
99
|
+
- Current observations (already-recorded observations, each shown as "[id] YYYY-MM-DD HH:MM [relevance] content").
|
|
100
|
+
- A new chunk of conversation with inline message timestamps formatted as "[User @ YYYY-MM-DD HH:MM]:", "[Assistant @ ...]:", "[Tool result for <name> @ ...]:".
|
|
101
|
+
- A current local time fallback for observations that have no obvious message timestamp.
|
|
102
|
+
|
|
103
|
+
How you work:
|
|
104
|
+
1. Read reflections and current observations so you know what is already captured.
|
|
105
|
+
2. Read the conversation chunk and identify what new information it contains.
|
|
106
|
+
3. Call record_observations with a batch covering part (or all) of the chunk.
|
|
107
|
+
4. Read the progress receipt. If content remains uncovered, call again. You may call the tool many times.
|
|
108
|
+
5. When the chunk is fully covered, STOP calling the tool and reply with a brief plain-text confirmation (one short sentence). That ends the run.
|
|
109
|
+
|
|
110
|
+
What to emit:
|
|
111
|
+
- Produce NEW observations for the new chunk only. Do not restate facts already present in reflections or current observations unless something has materially changed.
|
|
112
|
+
- Use the timestamp from the relevant conversation message. Fall back to current local time ONLY when no message timestamp applies.
|
|
113
|
+
- Group repeated similar tool calls into a single observation rather than one per call.
|
|
114
|
+
- Skip routine, low-information events. It is fine to emit zero observations if the chunk carries no new information — in that case, simply do not call the tool and end with a plain-text confirmation.
|
|
115
|
+
|
|
116
|
+
${OBSERVATION_CONTENT_RULES}
|
|
117
|
+
|
|
118
|
+
${DETAIL_PRESERVATION_SCHEMA}
|
|
119
|
+
|
|
120
|
+
${RELEVANCE_RUBRIC}
|
|
121
|
+
|
|
122
|
+
Timestamp format: "YYYY-MM-DD HH:MM" (local time, 24-hour, to the minute). This goes in the timestamp field, not the content.
|
|
123
|
+
|
|
124
|
+
Remember: these observations are the assistant's ONLY memory of this chunk once the raw messages fall out of context. Make them count.`;
|
|
125
|
+
|
|
126
|
+
export const REFLECTOR_SYSTEM = `You are the reflection agent for a coding assistant.
|
|
127
|
+
|
|
128
|
+
${MEMORY_STAKES}
|
|
129
|
+
|
|
130
|
+
Your job is to crystallize stable, long-lived patterns from accumulated observations into NEW reflections by calling the record_reflections tool. Reflections are the most durable layer of memory: once the pruner drops the observations behind them, the reflection is what remains.
|
|
131
|
+
|
|
132
|
+
You are operating on records produced by another part of the memory pipeline — the observer. To understand what you are reading and to produce reflections in the same voice, the observer was given these rules:
|
|
133
|
+
|
|
134
|
+
<observation-content-rules>
|
|
135
|
+
${OBSERVATION_CONTENT_RULES}
|
|
136
|
+
</observation-content-rules>
|
|
137
|
+
|
|
138
|
+
<relevance-rubric>
|
|
139
|
+
${RELEVANCE_RUBRIC}
|
|
140
|
+
</relevance-rubric>
|
|
141
|
+
|
|
142
|
+
Your task is different from the observer's: you are not recording events, you are distilling stable patterns from them.
|
|
143
|
+
|
|
144
|
+
You receive:
|
|
145
|
+
- Current reflections (already-crystallized long-lived facts, one per line).
|
|
146
|
+
- Current observations (timestamped, relevance-tagged events accumulated over many turns). Each is shown as "[id] YYYY-MM-DD HH:MM [relevance] content".
|
|
147
|
+
|
|
148
|
+
How you work:
|
|
149
|
+
1. Read current reflections and observations to understand what is already crystallized and what new signal exists in the pool.
|
|
150
|
+
2. Identify new stable patterns worth crystallizing and call record_reflections with a batch of one or more new reflection strings.
|
|
151
|
+
3. Read the receipt. If more reflections are warranted, call record_reflections again with another batch. You may call the tool many times.
|
|
152
|
+
4. When nothing more is stable enough to crystallize, STOP calling the tool and reply with a brief plain-text confirmation (one short sentence). That ends the run.
|
|
153
|
+
|
|
154
|
+
What to emit:
|
|
155
|
+
- Produce ONLY NEW reflections. Do not restate, rewrite, or lightly rephrase existing reflections.
|
|
156
|
+
- Crystallize preferentially from "high" and "critical" observations; ignore "low" unless a pattern across many "low" observations is itself significant.
|
|
157
|
+
- Focus on:
|
|
158
|
+
- User identity, role, preferences, constraints.
|
|
159
|
+
- Project goals, architectural decisions, key technical decisions and their rationale.
|
|
160
|
+
- Recurring user behavior or working style.
|
|
161
|
+
- Permanent constraints and requirements.
|
|
162
|
+
- It is fine to emit zero reflections if nothing new is stable enough to crystallize — in that case, simply do not call the tool and end with a plain-text confirmation.
|
|
163
|
+
|
|
164
|
+
User assertions are authoritative. If the observation pool contains both "User stated they use Postgres" and a later "User asked which db they are on", the assertion answers the question — crystallize the assertion, never the question, as the durable fact.
|
|
165
|
+
|
|
166
|
+
Reflection content rules:
|
|
167
|
+
- Single line of plain prose. No markdown, no bullets, no code fences, no XML/HTML tags, no emojis.
|
|
168
|
+
- No timestamp, no priority marker, no [tags], no "key: value" fields, no JSON.
|
|
169
|
+
- Preserve user assertions exactly. Use the user's exact words when non-standard.
|
|
170
|
+
- Lead with the fact or pattern; include the reason or mechanism when known so future readers can judge edge cases.
|
|
171
|
+
|
|
172
|
+
BAD: - 🔴 User prefers X
|
|
173
|
+
BAD: priority=high User prefers X
|
|
174
|
+
BAD: User prefers things.
|
|
175
|
+
GOOD: User prefers terse responses with no trailing summaries; reason: can read the diff themselves.
|
|
176
|
+
|
|
177
|
+
Remember: reflections are the layer of memory that survives pruning. If a durable fact never makes it into a reflection, it will eventually be lost.`;
|
|
178
|
+
|
|
179
|
+
export const PRUNER_SYSTEM = `You are the pruning agent for a coding assistant.
|
|
180
|
+
|
|
181
|
+
${MEMORY_STAKES}
|
|
182
|
+
|
|
183
|
+
Your job is to aggressively remove observations that are no longer worth keeping by calling the drop_observations tool with their ids. The observation pool must fit under a token budget; the user message tells you how much still needs to be cut, which pass you are on, and the strategy for this pass.
|
|
184
|
+
|
|
185
|
+
You are operating on records produced by the observer. To judge what is safe to drop, you must understand how they were created and what each relevance level means:
|
|
186
|
+
|
|
187
|
+
<observation-content-rules>
|
|
188
|
+
${OBSERVATION_CONTENT_RULES}
|
|
189
|
+
</observation-content-rules>
|
|
190
|
+
|
|
191
|
+
<relevance-rubric>
|
|
192
|
+
${RELEVANCE_RUBRIC}
|
|
193
|
+
</relevance-rubric>
|
|
194
|
+
|
|
195
|
+
You receive:
|
|
196
|
+
- Current reflections (long-lived facts; they survive regardless — treat them as already captured).
|
|
197
|
+
- Current observations (timestamped, relevance-tagged events to prune). Each is shown as "[id] YYYY-MM-DD HH:MM [relevance] content", where id is the 12-character hex handle you reference when dropping.
|
|
198
|
+
- A pressure line stating pool size, target, tokens still to cut, and the current pass strategy.
|
|
199
|
+
|
|
200
|
+
How you work:
|
|
201
|
+
1. Read reflections and the observation pool.
|
|
202
|
+
2. Identify ids that should be removed and call drop_observations with them. Pass multiple ids per call and call the tool multiple times as you work the pool down toward the target.
|
|
203
|
+
3. Read the receipt after each call to see what was dropped and how many remain.
|
|
204
|
+
4. When no further sound drops are possible, STOP calling the tool and reply with a brief plain-text confirmation. That ends the run.
|
|
205
|
+
|
|
206
|
+
This agent may be invoked again in a follow-up pass if the pool is still over budget — focus each run on your next-weakest drops rather than trying to do everything in one call.
|
|
207
|
+
|
|
208
|
+
What to drop (in priority order):
|
|
209
|
+
- Signal-captured: observations that are the raw source for a reflection now in the reflections list. Once a pattern is crystallized as a reflection, the raw observations behind it are redundant — drop them unless the observation is a user assertion or concrete completion.
|
|
210
|
+
- Superseded: directly contradicted or replaced by a newer observation.
|
|
211
|
+
- Redundant: near-duplicate of another observation (keep the higher-relevance or more recent one).
|
|
212
|
+
- Exhausted routine: tool-call acks, status updates, trivia that no longer affects the work.
|
|
213
|
+
|
|
214
|
+
Age-gradient rule. Recent observations carry working context the assistant still needs; older observations have usually been summarized elsewhere or are no longer load-bearing. When choosing between two equally droppable items, drop the older one first. For "low" and "medium" observations, compress older history more aggressively than recent turns.
|
|
215
|
+
|
|
216
|
+
BAD: drop the most recent "low" observation because "low" is easiest to justify.
|
|
217
|
+
GOOD: drop the oldest "low" observations; keep recent "low" observations until budget pressure forces otherwise.
|
|
218
|
+
|
|
219
|
+
Relevance guidance:
|
|
220
|
+
- "low": drop freely once reviewed. Why: these were marked low because they add little signal; keeping them crowds out more useful records.
|
|
221
|
+
- "medium": drop when redundant with reflections or other observations, or when the task context has moved on.
|
|
222
|
+
- "high": drop only when clearly superseded or already captured by a reflection.
|
|
223
|
+
- "critical": NEVER drop. These encode user identity, explicit corrections, and concrete completions. Why this matters: dropping a critical item causes the assistant to repeat finished work, contradict an explicit correction, or misrepresent who the user is. No amount of budget pressure justifies this.
|
|
224
|
+
|
|
225
|
+
User assertions and concrete completions are never droppable, even at non-critical relevance. If the relevance was mis-labeled but the content is load-bearing (an assertion about the user or a marker that work is done), treat the content as authoritative and skip the drop.
|
|
226
|
+
|
|
227
|
+
BAD: drop "[id] 2025-12-04 14:30 [low] User stated they are colorblind" because it is marked low.
|
|
228
|
+
GOOD: keep that observation; the content is a user assertion about a persistent constraint, and relevance is mis-labeled.
|
|
229
|
+
|
|
230
|
+
Preservation floor. Regardless of relevance label or age, do not drop observations that uniquely carry any of the following — they are not re-derivable once gone:
|
|
231
|
+
|
|
232
|
+
- Named identifiers appearing nowhere else in the kept set: package names, file paths, function/variable names, ticket ids, commit SHAs, handles, error codes.
|
|
233
|
+
- Dates of specific events (release cuts, deadlines, meetings, incidents).
|
|
234
|
+
- Error messages captured verbatim, especially ones the user hit.
|
|
235
|
+
- Architectural or technical decisions and their rationale (the "why" behind the choice, not just the choice).
|
|
236
|
+
- User preferences, constraints, and corrections — even when phrased without the word "prefer".
|
|
237
|
+
|
|
238
|
+
If one of these categories is ALSO captured by an existing reflection with equivalent fidelity, the observation becomes redundant and is droppable. Otherwise, keep it even under budget pressure.
|
|
239
|
+
|
|
240
|
+
BAD: drop "[id] 2025-12-04 14:30 [medium] Build failed: TS2322 at src/auth.ts:47 — Type 'string | undefined' is not assignable to type 'string'" because it is only medium and the task moved on.
|
|
241
|
+
GOOD: keep that observation; it is a verbatim error the user hit, not captured in any reflection. Future debugging may need the exact code and location.
|
|
242
|
+
|
|
243
|
+
When in doubt, drop — reflections protect durable facts. The only things you must preserve unconditionally are user assertions and concrete completions.
|
|
244
|
+
|
|
245
|
+
What you CANNOT do:
|
|
246
|
+
- You cannot merge observations. If two overlap, drop the weaker one.
|
|
247
|
+
- You cannot rewrite or edit observations. The kept set preserves content, timestamp, and relevance exactly as they were.
|
|
248
|
+
- You cannot add new observations.
|
|
249
|
+
|
|
250
|
+
It is valid to end a pass with zero drops if the pool genuinely has nothing more to cut — a follow-up pass will be skipped when a run returns zero drops. Do not force drops you don't believe in.
|
|
251
|
+
|
|
252
|
+
Remember: every observation you drop is erased from the assistant's memory. A drop that looks reasonable at "low" becomes a mistake if the content was a user correction with a mis-labeled relevance. Read before you cut.`;
|
|
253
|
+
|
|
254
|
+
type PrunerPassTier = 1 | 2 | 3;
|
|
255
|
+
|
|
256
|
+
const PRUNER_PASS_STRATEGIES: Record<PrunerPassTier, string> = {
|
|
257
|
+
1: `Pass strategy — clear-cut drops only. Remove exact duplicates, near-duplicates (keep the higher-relevance or more recent version), observations directly superseded by a newer one, and routine "low" tool-call acks. Do not touch ambiguous cases on this pass — a follow-up pass will handle them if still needed.`,
|
|
258
|
+
2: `Pass strategy — topic compression. Drop "low" observations that cover the same territory as recent "medium" or "high" observations. Drop older "medium" observations whose substance is now covered by a reflection. Collapse sequences of repeated tool-call observations by keeping the one that captures the learning and dropping the rest.`,
|
|
259
|
+
3: `Pass strategy — aggressive age compression. In the older half of the pool, drop all but the outcome-bearing "low" and "medium" observations. Keep the most recent ~30% of the pool at higher detail. Drop "high" observations only when a reflection clearly captures the same fact. NEVER drop "critical" items, user assertions, or concrete completions regardless of age.`,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export function buildPrunerPassGuidance(pass: number, maxPasses: number): string {
|
|
263
|
+
const tier = (Math.min(3, Math.max(1, pass)) as PrunerPassTier);
|
|
264
|
+
return `Pass ${pass} of up to ${maxPasses}. ${PRUNER_PASS_STRATEGIES[tier]}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export const CONTEXT_USAGE_INSTRUCTIONS = `These are condensed memories from earlier in this session.
|
|
268
|
+
|
|
269
|
+
- Reflections: stable, long-lived facts about the user, project, decisions, and constraints.
|
|
270
|
+
- Observations: timestamped events from the conversation history, in chronological order.
|
|
271
|
+
|
|
272
|
+
Treat these as past records. When entries conflict, the most recent observation reflects the latest known state. Work that prior observations describe as completed should not be redone unless the user explicitly asks to revisit it.`;
|
package/src/relevance.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ObservationRecord, type Relevance, RELEVANCE_VALUES } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function countByRelevance(records: ObservationRecord[]): Record<Relevance, number> {
|
|
4
|
+
const counts: Record<Relevance, number> = { low: 0, medium: 0, high: 0, critical: 0 };
|
|
5
|
+
for (const r of records) counts[r.relevance]++;
|
|
6
|
+
return counts;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatRelevanceHistogram(counts: Record<Relevance, number>): string {
|
|
10
|
+
return RELEVANCE_VALUES
|
|
11
|
+
.slice()
|
|
12
|
+
.reverse()
|
|
13
|
+
.map((r) => `${r}: ${counts[r]}`)
|
|
14
|
+
.join(" · ");
|
|
15
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type Config, DEFAULTS, loadConfig } from "./config.js";
|
|
2
|
+
|
|
3
|
+
export type ResolveResult =
|
|
4
|
+
| { ok: true; model: unknown; apiKey: string; headers?: Record<string, string> }
|
|
5
|
+
| { ok: false; reason: string };
|
|
6
|
+
|
|
7
|
+
type NotifyLevel = "warning" | "info" | "error";
|
|
8
|
+
type Notify = (message: string, type?: NotifyLevel) => void;
|
|
9
|
+
|
|
10
|
+
export interface ResolveCtx {
|
|
11
|
+
model: unknown;
|
|
12
|
+
modelRegistry: any;
|
|
13
|
+
hasUI: boolean;
|
|
14
|
+
ui?: { notify: Notify };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LaunchCtx {
|
|
18
|
+
hasUI: boolean;
|
|
19
|
+
ui?: { notify: Notify };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class Runtime {
|
|
23
|
+
config: Config = { ...DEFAULTS };
|
|
24
|
+
configLoaded = false;
|
|
25
|
+
observerInFlight = false;
|
|
26
|
+
observerPromise: Promise<void> | null = null;
|
|
27
|
+
compactInFlight = false;
|
|
28
|
+
compactHookInFlight = false;
|
|
29
|
+
resolveFailureNotified = false;
|
|
30
|
+
|
|
31
|
+
ensureConfig(cwd: string): void {
|
|
32
|
+
if (this.configLoaded) return;
|
|
33
|
+
this.config = loadConfig(cwd);
|
|
34
|
+
this.configLoaded = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async resolveModel(ctx: ResolveCtx): Promise<ResolveResult> {
|
|
38
|
+
let model = ctx.model;
|
|
39
|
+
if (this.config.compactionModel) {
|
|
40
|
+
const configured = ctx.modelRegistry.find(this.config.compactionModel.provider, this.config.compactionModel.id);
|
|
41
|
+
if (configured) {
|
|
42
|
+
model = configured;
|
|
43
|
+
} else if (ctx.hasUI && ctx.ui) {
|
|
44
|
+
ctx.ui.notify(
|
|
45
|
+
`Observational memory: configured model ${this.config.compactionModel.provider}/${this.config.compactionModel.id} not found, using session model`,
|
|
46
|
+
"warning",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!model) return { ok: false, reason: "no model available (session has no model and no compactionModel configured)" };
|
|
51
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
52
|
+
if (!auth.ok || !auth.apiKey) {
|
|
53
|
+
const provider = (model as { provider?: string }).provider ?? "unknown";
|
|
54
|
+
return { ok: false, reason: `no API key for provider "${provider}"` };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, model, apiKey: auth.apiKey as string, headers: auth.headers as Record<string, string> | undefined };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
launchObserverTask(ctx: LaunchCtx, label: string, work: () => Promise<void>): Promise<void> {
|
|
60
|
+
this.observerInFlight = true;
|
|
61
|
+
let promise!: Promise<void>;
|
|
62
|
+
promise = (async () => {
|
|
63
|
+
try {
|
|
64
|
+
await work();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
67
|
+
if (ctx.hasUI && ctx.ui) ctx.ui.notify(`Observational memory: ${label} failed: ${msg}`, "warning");
|
|
68
|
+
} finally {
|
|
69
|
+
this.observerInFlight = false;
|
|
70
|
+
if (this.observerPromise === promise) this.observerPromise = null;
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
this.observerPromise = promise;
|
|
74
|
+
return promise;
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/serialize.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Message, TextContent, ToolResultMessage } from "@mariozechner/pi-ai";
|
|
2
|
+
|
|
3
|
+
function pad(n: number): string {
|
|
4
|
+
return n.toString().padStart(2, "0");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function fmtLocal(d: Date): string {
|
|
8
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatTimestamp(v: number | string | undefined): string {
|
|
12
|
+
if (v === undefined) return "????-??-?? ??:??";
|
|
13
|
+
const d = new Date(v);
|
|
14
|
+
return Number.isNaN(d.getTime()) ? "????-??-?? ??:??" : fmtLocal(d);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function serializeConversation(messages: Message[]): string {
|
|
18
|
+
return messages
|
|
19
|
+
.map((msg): string | null => {
|
|
20
|
+
const time = formatTimestamp(msg.timestamp);
|
|
21
|
+
if (msg.role === "user") {
|
|
22
|
+
const text =
|
|
23
|
+
typeof msg.content === "string"
|
|
24
|
+
? msg.content
|
|
25
|
+
: msg.content
|
|
26
|
+
.filter((b): b is TextContent => b.type === "text")
|
|
27
|
+
.map((b) => b.text)
|
|
28
|
+
.join("\n");
|
|
29
|
+
return `[User @ ${time}]: ${text}`;
|
|
30
|
+
}
|
|
31
|
+
if (msg.role === "assistant") {
|
|
32
|
+
const parts = msg.content.map((b) => {
|
|
33
|
+
if (b.type === "text") return b.text;
|
|
34
|
+
if (b.type === "thinking") return b.redacted ? "" : `[thinking: ${b.thinking}]`;
|
|
35
|
+
if (b.type === "toolCall") return `[${b.name}(${JSON.stringify(b.arguments)})]`;
|
|
36
|
+
return "";
|
|
37
|
+
});
|
|
38
|
+
const body = parts.filter(Boolean).join("\n");
|
|
39
|
+
if (!body) return null;
|
|
40
|
+
return `[Assistant @ ${time}]: ${body}`;
|
|
41
|
+
}
|
|
42
|
+
const text = msg.content
|
|
43
|
+
.filter((b): b is TextContent => b.type === "text")
|
|
44
|
+
.map((b) => b.text)
|
|
45
|
+
.join("\n");
|
|
46
|
+
return `[Tool result for ${(msg as ToolResultMessage).toolName} @ ${time}]: ${text}`;
|
|
47
|
+
})
|
|
48
|
+
.filter((line): line is string => line !== null)
|
|
49
|
+
.join("\n\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function nowTimestamp(): string {
|
|
53
|
+
return fmtLocal(new Date());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const MAX_RECORD_CONTENT_CHARS = 10_000;
|
|
57
|
+
|
|
58
|
+
export function truncateRecordContent(content: string): string {
|
|
59
|
+
if (content.length <= MAX_RECORD_CONTENT_CHARS) return content;
|
|
60
|
+
const head = content.slice(0, MAX_RECORD_CONTENT_CHARS);
|
|
61
|
+
const dropped = content.length - MAX_RECORD_CONTENT_CHARS;
|
|
62
|
+
return `${head} … [truncated ${dropped} chars]`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type RenderableEntry = {
|
|
66
|
+
type: string;
|
|
67
|
+
timestamp?: string;
|
|
68
|
+
message?: unknown;
|
|
69
|
+
customType?: string;
|
|
70
|
+
content?: unknown;
|
|
71
|
+
summary?: unknown;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function serializeBranchEntries(entries: RenderableEntry[]): string {
|
|
75
|
+
const blocks: string[] = [];
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.type === "message" && entry.message) {
|
|
78
|
+
const part = serializeConversation([entry.message as Message]);
|
|
79
|
+
if (part) blocks.push(part);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (entry.type === "custom_message") {
|
|
83
|
+
const time = formatTimestamp(entry.timestamp);
|
|
84
|
+
let text = "";
|
|
85
|
+
if (typeof entry.content === "string") {
|
|
86
|
+
text = entry.content;
|
|
87
|
+
} else if (Array.isArray(entry.content)) {
|
|
88
|
+
text = (entry.content as Array<{ type?: string; text?: string }>)
|
|
89
|
+
.filter((b) => b?.type === "text" && typeof b.text === "string")
|
|
90
|
+
.map((b) => b.text as string)
|
|
91
|
+
.join("\n");
|
|
92
|
+
}
|
|
93
|
+
const tag = entry.customType ? `Custom (${entry.customType})` : "Custom";
|
|
94
|
+
blocks.push(`[${tag} @ ${time}]: ${text}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (entry.type === "branch_summary" && typeof entry.summary === "string") {
|
|
98
|
+
const time = formatTimestamp(entry.timestamp);
|
|
99
|
+
blocks.push(`[Branch summary @ ${time}]: ${entry.summary}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return blocks.join("\n\n");
|
|
103
|
+
}
|