pi-observational-memory 1.0.2 → 1.0.4
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/package.json +1 -1
- package/src/index.ts +87 -20
- package/src/prompts.ts +7 -5
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { completeSimple } from "@mariozechner/pi-ai";
|
|
1
|
+
import { completeSimple, type Message, type TextContent, type ToolCall, type ToolResultMessage } from "@mariozechner/pi-ai";
|
|
2
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import { convertToLlm,
|
|
3
|
+
import { convertToLlm, SettingsManager } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { DEFAULTS, loadConfig } from "./config.js";
|
|
5
5
|
import type { Config } from "./config.js";
|
|
6
6
|
import { CONTEXT_USAGE_INSTRUCTIONS, OBSERVER_SYSTEM, REFLECTOR_SYSTEM } from "./prompts.js";
|
|
@@ -8,6 +8,52 @@ import { estimateRawTailTokens, estimateTokens, extractText } from "./tokens.js"
|
|
|
8
8
|
import type { MemoryDetails, MemoryState } from "./types.js";
|
|
9
9
|
import { isMemoryDetails } from "./types.js";
|
|
10
10
|
|
|
11
|
+
function utcDate(epochMs: number): string {
|
|
12
|
+
if (!Number.isFinite(epochMs)) return "????-??-??";
|
|
13
|
+
return new Date(epochMs).toISOString().slice(0, 10);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function utcTime(epochMs: number): string {
|
|
17
|
+
if (!Number.isFinite(epochMs)) return "??:??";
|
|
18
|
+
return new Date(epochMs).toISOString().slice(11, 16);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function serializeWithTimestamps(messages: Message[]): string {
|
|
22
|
+
return messages
|
|
23
|
+
.map((msg): string | null => {
|
|
24
|
+
const time = utcTime(msg.timestamp);
|
|
25
|
+
if (msg.role === "user") {
|
|
26
|
+
const text =
|
|
27
|
+
typeof msg.content === "string"
|
|
28
|
+
? msg.content
|
|
29
|
+
: msg.content
|
|
30
|
+
.filter((b): b is TextContent => b.type === "text")
|
|
31
|
+
.map((b) => b.text)
|
|
32
|
+
.join("\n");
|
|
33
|
+
return `[User @ ${time} UTC]: ${text}`;
|
|
34
|
+
}
|
|
35
|
+
if (msg.role === "assistant") {
|
|
36
|
+
const parts = msg.content.map((b) => {
|
|
37
|
+
if (b.type === "text") return b.text;
|
|
38
|
+
if (b.type === "thinking") return b.redacted ? "" : `[thinking: ${b.thinking}]`;
|
|
39
|
+
if (b.type === "toolCall") return `[${b.name}(${JSON.stringify(b.arguments)})]`;
|
|
40
|
+
return "";
|
|
41
|
+
});
|
|
42
|
+
const body = parts.filter(Boolean).join("\n");
|
|
43
|
+
if (!body) return null;
|
|
44
|
+
return `[Assistant @ ${time} UTC]: ${body}`;
|
|
45
|
+
}
|
|
46
|
+
// toolResult
|
|
47
|
+
const text = msg.content
|
|
48
|
+
.filter((b): b is TextContent => b.type === "text")
|
|
49
|
+
.map((b) => b.text)
|
|
50
|
+
.join("\n");
|
|
51
|
+
return `[Tool result for ${(msg as ToolResultMessage).toolName} @ ${time} UTC]: ${text}`;
|
|
52
|
+
})
|
|
53
|
+
.filter((line): line is string => line !== null)
|
|
54
|
+
.join("\n\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
11
57
|
export default function observationalMemory(pi: ExtensionAPI) {
|
|
12
58
|
let config: Config = { ...DEFAULTS };
|
|
13
59
|
let state: MemoryState = { observations: "", reflections: "" };
|
|
@@ -42,16 +88,22 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
42
88
|
compactInFlight = false;
|
|
43
89
|
return;
|
|
44
90
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
91
|
+
try {
|
|
92
|
+
ctx.compact({
|
|
93
|
+
onComplete: () => {
|
|
94
|
+
compactInFlight = false;
|
|
95
|
+
if (ctx.hasUI) ctx.ui.notify("Observational memory: compaction complete", "info");
|
|
96
|
+
},
|
|
97
|
+
onError: (error) => {
|
|
98
|
+
compactInFlight = false;
|
|
99
|
+
if (ctx.hasUI) ctx.ui.notify(`Observational memory: ${error.message}`, "error");
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
compactInFlight = false;
|
|
104
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
105
|
+
if (ctx.hasUI) ctx.ui.notify(`Observational memory: compact threw: ${msg}`, "error");
|
|
106
|
+
}
|
|
55
107
|
}, 0);
|
|
56
108
|
});
|
|
57
109
|
|
|
@@ -79,10 +131,25 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
79
131
|
const allMessages = [...messagesToSummarize, ...turnPrefixMessages];
|
|
80
132
|
if (allMessages.length === 0) return;
|
|
81
133
|
|
|
82
|
-
const conversationText = serializeConversation(convertToLlm(allMessages));
|
|
83
134
|
const now = new Date();
|
|
84
|
-
const dateStr = now.
|
|
85
|
-
const timeStr = now.
|
|
135
|
+
const dateStr = utcDate(now.getTime());
|
|
136
|
+
const timeStr = utcTime(now.getTime());
|
|
137
|
+
|
|
138
|
+
const llmMessages = convertToLlm(allMessages);
|
|
139
|
+
const conversationText = serializeWithTimestamps(llmMessages);
|
|
140
|
+
|
|
141
|
+
let dateRangeNote = "";
|
|
142
|
+
if (llmMessages.length > 0) {
|
|
143
|
+
const timestamps = llmMessages.map((m) => m.timestamp);
|
|
144
|
+
const firstTs = timestamps.reduce((a, b) => Math.min(a, b));
|
|
145
|
+
const lastTs = timestamps.reduce((a, b) => Math.max(a, b));
|
|
146
|
+
const firstMsgDate = utcDate(firstTs);
|
|
147
|
+
const lastMsgDate = utcDate(lastTs);
|
|
148
|
+
dateRangeNote =
|
|
149
|
+
firstMsgDate === lastMsgDate
|
|
150
|
+
? ` Messages in this batch are from ${firstMsgDate} (UTC).`
|
|
151
|
+
: ` Messages in this batch span ${firstMsgDate} to ${lastMsgDate} (UTC).`;
|
|
152
|
+
}
|
|
86
153
|
|
|
87
154
|
ctx.ui.notify("Observational memory: running observer...", "info");
|
|
88
155
|
|
|
@@ -101,7 +168,7 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
101
168
|
content: [
|
|
102
169
|
{
|
|
103
170
|
type: "text" as const,
|
|
104
|
-
text: `Today is ${dateStr}, current time is ${timeStr}
|
|
171
|
+
text: `Today is ${dateStr}, current time is ${timeStr} UTC.${dateRangeNote}\n\n<current-reflections>\n${state.reflections || "(none yet)"}\n</current-reflections>\n\n<current-observations>\n${state.observations || "(none yet)"}\n</current-observations>\n\nCompress the following conversation into new observations:\n\n<conversation>\n${conversationText}\n</conversation>`,
|
|
105
172
|
},
|
|
106
173
|
],
|
|
107
174
|
timestamp: Date.now(),
|
|
@@ -141,7 +208,7 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
141
208
|
content: [
|
|
142
209
|
{
|
|
143
210
|
type: "text" as const,
|
|
144
|
-
text: `Today is ${dateStr}.\n\n<current-reflections>\n${state.reflections || "(none yet)"}\n</current-reflections>\n\n<current-observations>\n${state.observations}\n</current-observations>\n\nGarbage-collect these observations. Promote long-lived facts to reflections, prune what's no longer needed, keep what's still active.`,
|
|
211
|
+
text: `Today is ${dateStr} (UTC).\n\n<current-reflections>\n${state.reflections || "(none yet)"}\n</current-reflections>\n\n<current-observations>\n${state.observations}\n</current-observations>\n\nGarbage-collect these observations. Promote long-lived facts to reflections, prune what's no longer needed, keep what's still active.`,
|
|
145
212
|
},
|
|
146
213
|
],
|
|
147
214
|
timestamp: Date.now(),
|
|
@@ -209,8 +276,8 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
209
276
|
"",
|
|
210
277
|
"── Parameters ──",
|
|
211
278
|
`Observation threshold: ${config.observationThreshold.toLocaleString()}`,
|
|
212
|
-
`Reflection threshold: ${config.reflectionThreshold.toLocaleString()}`,
|
|
213
|
-
`Keep recent tokens: ${keepRecentTokens.toLocaleString()} (pi compaction)`,
|
|
279
|
+
`Reflection threshold: ${config.reflectionThreshold.toLocaleString()} (interpreted as observations token budget)`,
|
|
280
|
+
`Keep recent tokens: ${keepRecentTokens.toLocaleString()} (pi compaction, how many tokens in raw messages)`,
|
|
214
281
|
];
|
|
215
282
|
|
|
216
283
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
@@ -257,7 +324,7 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
257
324
|
sections.push("");
|
|
258
325
|
sections.push("── Raw Messages ──");
|
|
259
326
|
if (rawMessages.length > 0) {
|
|
260
|
-
sections.push(
|
|
327
|
+
sections.push(serializeWithTimestamps(convertToLlm(rawMessages)));
|
|
261
328
|
} else {
|
|
262
329
|
sections.push("(none)");
|
|
263
330
|
}
|
package/src/prompts.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export const OBSERVER_SYSTEM = `You are an observation agent for a coding assistant. Compress conversation messages into concise, timestamped observations.
|
|
1
|
+
export const OBSERVER_SYSTEM = `You are an observation agent for a coding assistant. Compress conversation messages into concise, timestamped observations. Messages arrive pre-timestamped as \`[User @ HH:MM UTC]\`, \`[Assistant @ HH:MM UTC]\`, and \`[Tool result for <name> @ HH:MM UTC]\` — use those inline timestamps when assigning times to your observations. All timestamps are UTC.
|
|
2
2
|
|
|
3
3
|
Format as a date-grouped log:
|
|
4
4
|
|
|
5
|
-
Date: YYYY-MM-DD
|
|
5
|
+
Date: YYYY-MM-DD (UTC)
|
|
6
6
|
- 🔴 HH:MM Observation text
|
|
7
|
-
-
|
|
8
|
-
-
|
|
7
|
+
- Sub-observation (no timestamp if same moment)
|
|
8
|
+
- Sub-observation
|
|
9
9
|
- 🟢 HH:MM Another observation
|
|
10
10
|
|
|
11
11
|
Priority levels:
|
|
@@ -159,7 +159,9 @@ Output EXACTLY two sections with these tags:
|
|
|
159
159
|
[Surviving observations in the same date-grouped log format — most should be preserved]
|
|
160
160
|
</observations>
|
|
161
161
|
|
|
162
|
-
Do NOT wrap output in code blocks or markdown fences
|
|
162
|
+
Do NOT wrap output in code blocks or markdown fences.
|
|
163
|
+
|
|
164
|
+
All timestamps in observations are UTC.`;
|
|
163
165
|
|
|
164
166
|
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
167
|
|