pi-observational-memory 2.1.3 → 2.4.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 +40 -22
- package/package.json +10 -6
- package/src/branch.ts +395 -8
- package/src/commands/status.ts +37 -11
- package/src/commands/view.ts +6 -5
- package/src/compaction.ts +331 -45
- package/src/config.ts +21 -2
- package/src/hooks/compaction-hook.ts +31 -25
- package/src/hooks/compaction-trigger.ts +31 -23
- package/src/hooks/observer-trigger.ts +17 -10
- package/src/index.ts +2 -0
- package/src/observer.ts +42 -4
- package/src/prompts.ts +44 -15
- package/src/runtime.ts +6 -1
- package/src/serialize.ts +144 -31
- package/src/tools/recall-observation.ts +613 -0
- package/src/types.ts +101 -10
package/src/serialize.ts
CHANGED
|
@@ -14,35 +14,78 @@ function formatTimestamp(v: number | string | undefined): string {
|
|
|
14
14
|
return Number.isNaN(d.getTime()) ? "????-??-?? ??:??" : fmtLocal(d);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function formatRecallTimestamp(...values: Array<number | string | undefined>): string {
|
|
18
|
+
for (const v of values) {
|
|
19
|
+
if (v === undefined) continue;
|
|
20
|
+
const d = new Date(v);
|
|
21
|
+
if (!Number.isNaN(d.getTime())) return fmtLocal(d);
|
|
22
|
+
}
|
|
23
|
+
return "Unknown time";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function textAndPlaceholders(
|
|
27
|
+
content: unknown,
|
|
28
|
+
options: { omitRedactedThinking?: boolean; includeThinking?: boolean } = {},
|
|
29
|
+
): string {
|
|
30
|
+
if (typeof content === "string") return content;
|
|
31
|
+
if (!Array.isArray(content)) return "[non-text content omitted]";
|
|
32
|
+
|
|
33
|
+
const parts: string[] = [];
|
|
34
|
+
for (const block of content as Array<Record<string, unknown>>) {
|
|
35
|
+
if (!block || typeof block !== "object") {
|
|
36
|
+
parts.push("[non-text content omitted]");
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
40
|
+
parts.push(block.text);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (block.type === "thinking") {
|
|
44
|
+
if (options.omitRedactedThinking && block.redacted === true) continue;
|
|
45
|
+
if (options.includeThinking && typeof block.thinking === "string") {
|
|
46
|
+
parts.push(`[thinking: ${block.thinking}]`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
parts.push("[non-text content omitted]");
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (block.type === "toolCall" && typeof block.name === "string") {
|
|
53
|
+
parts.push(`[${block.name}(${JSON.stringify(block.arguments ?? {})})]`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
parts.push("[non-text content omitted]");
|
|
57
|
+
}
|
|
58
|
+
return parts.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function textOnly(content: string | Array<{ type?: string; text?: string }>): string {
|
|
62
|
+
if (typeof content === "string") return content;
|
|
63
|
+
return content
|
|
64
|
+
.filter((b): b is TextContent => b?.type === "text" && typeof b.text === "string")
|
|
65
|
+
.map((b) => b.text)
|
|
66
|
+
.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
17
69
|
export function serializeConversation(messages: Message[]): string {
|
|
18
70
|
return messages
|
|
19
71
|
.map((msg): string | null => {
|
|
20
72
|
const time = formatTimestamp(msg.timestamp);
|
|
21
73
|
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");
|
|
74
|
+
const text = textOnly(msg.content);
|
|
29
75
|
return `[User @ ${time}]: ${text}`;
|
|
30
76
|
}
|
|
31
77
|
if (msg.role === "assistant") {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
78
|
+
const body = textAndPlaceholders(msg.content, {
|
|
79
|
+
includeThinking: true,
|
|
80
|
+
omitRedactedThinking: true,
|
|
81
|
+
})
|
|
82
|
+
.split("\n")
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.join("\n");
|
|
39
85
|
if (!body) return null;
|
|
40
86
|
return `[Assistant @ ${time}]: ${body}`;
|
|
41
87
|
}
|
|
42
|
-
const text = msg.content
|
|
43
|
-
.filter((b): b is TextContent => b.type === "text")
|
|
44
|
-
.map((b) => b.text)
|
|
45
|
-
.join("\n");
|
|
88
|
+
const text = textOnly(msg.content);
|
|
46
89
|
return `[Tool result for ${(msg as ToolResultMessage).toolName} @ ${time}]: ${text}`;
|
|
47
90
|
})
|
|
48
91
|
.filter((line): line is string => line !== null)
|
|
@@ -62,8 +105,9 @@ export function truncateRecordContent(content: string): string {
|
|
|
62
105
|
return `${head} … [truncated ${dropped} chars]`;
|
|
63
106
|
}
|
|
64
107
|
|
|
65
|
-
type RenderableEntry = {
|
|
108
|
+
export type RenderableEntry = {
|
|
66
109
|
type: string;
|
|
110
|
+
id?: string;
|
|
67
111
|
timestamp?: string;
|
|
68
112
|
message?: unknown;
|
|
69
113
|
customType?: string;
|
|
@@ -71,6 +115,26 @@ type RenderableEntry = {
|
|
|
71
115
|
summary?: unknown;
|
|
72
116
|
};
|
|
73
117
|
|
|
118
|
+
function renderCustomMessage(entry: RenderableEntry, options: { recallFormat: boolean }): string {
|
|
119
|
+
const time = options.recallFormat ? formatRecallTimestamp(entry.timestamp) : formatTimestamp(entry.timestamp);
|
|
120
|
+
const text = options.recallFormat
|
|
121
|
+
? textAndPlaceholders(entry.content)
|
|
122
|
+
: typeof entry.content === "string"
|
|
123
|
+
? entry.content
|
|
124
|
+
: Array.isArray(entry.content)
|
|
125
|
+
? (entry.content as Array<{ type?: string; text?: string }>)
|
|
126
|
+
.filter((b) => b?.type === "text" && typeof b.text === "string")
|
|
127
|
+
.map((b) => b.text as string)
|
|
128
|
+
.join("\n")
|
|
129
|
+
: "";
|
|
130
|
+
if (options.recallFormat) {
|
|
131
|
+
const origin = entry.customType ? `Custom message (${entry.customType})` : "Custom message";
|
|
132
|
+
return `[${origin} @ ${time}]: ${text}`;
|
|
133
|
+
}
|
|
134
|
+
const tag = entry.customType ? `Custom (${entry.customType})` : "Custom";
|
|
135
|
+
return `[${tag} @ ${time}]: ${text}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
74
138
|
export function serializeBranchEntries(entries: RenderableEntry[]): string {
|
|
75
139
|
const blocks: string[] = [];
|
|
76
140
|
for (const entry of entries) {
|
|
@@ -80,18 +144,7 @@ export function serializeBranchEntries(entries: RenderableEntry[]): string {
|
|
|
80
144
|
continue;
|
|
81
145
|
}
|
|
82
146
|
if (entry.type === "custom_message") {
|
|
83
|
-
|
|
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}`);
|
|
147
|
+
blocks.push(renderCustomMessage(entry, { recallFormat: false }));
|
|
95
148
|
continue;
|
|
96
149
|
}
|
|
97
150
|
if (entry.type === "branch_summary" && typeof entry.summary === "string") {
|
|
@@ -101,3 +154,63 @@ export function serializeBranchEntries(entries: RenderableEntry[]): string {
|
|
|
101
154
|
}
|
|
102
155
|
return blocks.join("\n\n");
|
|
103
156
|
}
|
|
157
|
+
|
|
158
|
+
export type SourceAddressedSerialization = {
|
|
159
|
+
text: string;
|
|
160
|
+
sourceEntryIds: string[];
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
function isSourceRenderableEntry(entry: RenderableEntry): boolean {
|
|
164
|
+
return entry.type === "message" || entry.type === "custom_message" || entry.type === "branch_summary";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function serializeSourceAddressedBranchEntries(entries: RenderableEntry[]): SourceAddressedSerialization {
|
|
168
|
+
const blocks: string[] = [];
|
|
169
|
+
const sourceEntryIds: string[] = [];
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
if (!entry.id || !isSourceRenderableEntry(entry)) continue;
|
|
172
|
+
const rendered = serializeBranchEntries([entry]);
|
|
173
|
+
if (!rendered.trim()) continue;
|
|
174
|
+
sourceEntryIds.push(entry.id);
|
|
175
|
+
blocks.push(`[Source entry id: ${entry.id}]\n${rendered}`);
|
|
176
|
+
}
|
|
177
|
+
return { text: blocks.join("\n\n"), sourceEntryIds };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function renderRecallMessage(entry: RenderableEntry): string | null {
|
|
181
|
+
if (!entry.message || typeof entry.message !== "object") return null;
|
|
182
|
+
const msg = entry.message as Message;
|
|
183
|
+
const time = formatRecallTimestamp(msg.timestamp, entry.timestamp);
|
|
184
|
+
if (msg.role === "user") {
|
|
185
|
+
return `[User @ ${time}]: ${textAndPlaceholders(msg.content)}`;
|
|
186
|
+
}
|
|
187
|
+
if (msg.role === "assistant") {
|
|
188
|
+
const body = textAndPlaceholders(msg.content, {
|
|
189
|
+
includeThinking: true,
|
|
190
|
+
omitRedactedThinking: true,
|
|
191
|
+
})
|
|
192
|
+
.split("\n")
|
|
193
|
+
.filter(Boolean)
|
|
194
|
+
.join("\n");
|
|
195
|
+
if (!body) return null;
|
|
196
|
+
return `[Assistant @ ${time}]: ${body}`;
|
|
197
|
+
}
|
|
198
|
+
return `[Tool result: ${(msg as ToolResultMessage).toolName} @ ${time}]: ${textAndPlaceholders(msg.content)}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function renderRecallSourceEntry(entry: RenderableEntry): string | null {
|
|
202
|
+
if (entry.type === "message") return renderRecallMessage(entry);
|
|
203
|
+
if (entry.type === "custom_message") return renderCustomMessage(entry, { recallFormat: true });
|
|
204
|
+
if (entry.type === "branch_summary" && typeof entry.summary === "string") {
|
|
205
|
+
const time = formatRecallTimestamp(entry.timestamp);
|
|
206
|
+
return `[Branch summary @ ${time}]: ${entry.summary}`;
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function renderRecallSourceEntries(entries: RenderableEntry[]): string {
|
|
212
|
+
return entries
|
|
213
|
+
.map(renderRecallSourceEntry)
|
|
214
|
+
.filter((block): block is string => block !== null && block.trim().length > 0)
|
|
215
|
+
.join("\n\n");
|
|
216
|
+
}
|