pi-observational-memory 1.0.2 → 1.0.3
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 +89 -19
- package/src/prompts.ts +3 -3
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 localDate(epochMs: number): string {
|
|
12
|
+
const d = new Date(epochMs);
|
|
13
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function localTime(epochMs: number): string {
|
|
17
|
+
const d = new Date(epochMs);
|
|
18
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function serializeWithTimestamps(messages: Message[]): string {
|
|
22
|
+
return messages
|
|
23
|
+
.map((msg): string | null => {
|
|
24
|
+
const time = localTime(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}]: ${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}]: ${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}]: ${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,28 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
79
131
|
const allMessages = [...messagesToSummarize, ...turnPrefixMessages];
|
|
80
132
|
if (allMessages.length === 0) return;
|
|
81
133
|
|
|
82
|
-
|
|
134
|
+
// Bug 1: use local date, not UTC
|
|
83
135
|
const now = new Date();
|
|
84
|
-
const dateStr = now.
|
|
85
|
-
const timeStr = now.
|
|
136
|
+
const dateStr = localDate(now.getTime());
|
|
137
|
+
const timeStr = localTime(now.getTime());
|
|
138
|
+
|
|
139
|
+
// Bug 2: serialize with per-message timestamps
|
|
140
|
+
const llmMessages = convertToLlm(allMessages);
|
|
141
|
+
const conversationText = serializeWithTimestamps(llmMessages);
|
|
142
|
+
|
|
143
|
+
// Bug 3: inform the Observer of the message date range
|
|
144
|
+
let dateRangeNote = "";
|
|
145
|
+
if (llmMessages.length > 0) {
|
|
146
|
+
const timestamps = llmMessages.map((m) => m.timestamp);
|
|
147
|
+
const firstTs = timestamps.reduce((a, b) => Math.min(a, b));
|
|
148
|
+
const lastTs = timestamps.reduce((a, b) => Math.max(a, b));
|
|
149
|
+
const firstMsgDate = localDate(firstTs);
|
|
150
|
+
const lastMsgDate = localDate(lastTs);
|
|
151
|
+
dateRangeNote =
|
|
152
|
+
firstMsgDate === lastMsgDate
|
|
153
|
+
? ` Messages in this batch are from ${firstMsgDate}.`
|
|
154
|
+
: ` Messages in this batch span ${firstMsgDate} to ${lastMsgDate}.`;
|
|
155
|
+
}
|
|
86
156
|
|
|
87
157
|
ctx.ui.notify("Observational memory: running observer...", "info");
|
|
88
158
|
|
|
@@ -101,7 +171,7 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
101
171
|
content: [
|
|
102
172
|
{
|
|
103
173
|
type: "text" as const,
|
|
104
|
-
text: `Today is ${dateStr}, current time is ${timeStr}
|
|
174
|
+
text: `Today is ${dateStr}, current time is ${timeStr}.${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
175
|
},
|
|
106
176
|
],
|
|
107
177
|
timestamp: Date.now(),
|
|
@@ -209,8 +279,8 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
209
279
|
"",
|
|
210
280
|
"── Parameters ──",
|
|
211
281
|
`Observation threshold: ${config.observationThreshold.toLocaleString()}`,
|
|
212
|
-
`Reflection threshold: ${config.reflectionThreshold.toLocaleString()}`,
|
|
213
|
-
`Keep recent tokens: ${keepRecentTokens.toLocaleString()} (pi compaction)`,
|
|
282
|
+
`Reflection threshold: ${config.reflectionThreshold.toLocaleString()} (interpreted as observations token budget)`,
|
|
283
|
+
`Keep recent tokens: ${keepRecentTokens.toLocaleString()} (pi compaction, how many tokens in raw messages)`,
|
|
214
284
|
];
|
|
215
285
|
|
|
216
286
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
@@ -257,7 +327,7 @@ export default function observationalMemory(pi: ExtensionAPI) {
|
|
|
257
327
|
sections.push("");
|
|
258
328
|
sections.push("── Raw Messages ──");
|
|
259
329
|
if (rawMessages.length > 0) {
|
|
260
|
-
sections.push(
|
|
330
|
+
sections.push(serializeWithTimestamps(convertToLlm(rawMessages)));
|
|
261
331
|
} else {
|
|
262
332
|
sections.push("(none)");
|
|
263
333
|
}
|
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]\`, \`[Assistant @ HH:MM]\`, and \`[Tool result for <name> @ HH:MM]\` — use those inline timestamps when assigning times to your observations.
|
|
2
2
|
|
|
3
3
|
Format as a date-grouped log:
|
|
4
4
|
|
|
5
5
|
Date: YYYY-MM-DD
|
|
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:
|