pi-memoria 0.2.0 → 0.3.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/AGENTS.md +53 -0
- package/README.md +126 -166
- package/index.ts +299 -204
- package/package.json +28 -69
- package/src/aliases.ts +104 -0
- package/src/config.ts +22 -184
- package/src/database.ts +301 -0
- package/src/errors.ts +55 -0
- package/src/files.ts +29 -0
- package/src/hot-memory.ts +231 -0
- package/src/results.ts +264 -0
- package/src/sessions.ts +246 -843
- package/src/text.ts +48 -0
- package/skills/memoria/SKILL.md +0 -77
- package/skills/memoria-sessions/SKILL.md +0 -69
- package/src/commands.ts +0 -398
- package/src/frontmatter.ts +0 -318
- package/src/index-engine.ts +0 -1071
- package/src/recall.ts +0 -211
- package/src/rerank.ts +0 -85
- package/src/runtime.ts +0 -1469
- package/src/search.ts +0 -499
- package/src/similarity.ts +0 -374
- package/src/store.ts +0 -1724
- package/src/synonyms.ts +0 -105
- package/src/timeexpr.ts +0 -145
- package/src/tokenize.ts +0 -295
- package/src/tools.ts +0 -813
- package/src/types.ts +0 -541
- package/src/usage.ts +0 -188
- package/src/util.ts +0 -141
package/src/results.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { Memory, SearchMode } from "./database.ts";
|
|
6
|
+
import { ExpectedError } from "./errors.ts";
|
|
7
|
+
import type { HotMemory, HotSnapshot } from "./hot-memory.ts";
|
|
8
|
+
import { RESULT_BYTES } from "./text.ts";
|
|
9
|
+
|
|
10
|
+
export interface HotWriteResult extends HotSnapshot {
|
|
11
|
+
cap: number;
|
|
12
|
+
store: "hot";
|
|
13
|
+
memory: HotMemory;
|
|
14
|
+
created?: boolean;
|
|
15
|
+
note?: string;
|
|
16
|
+
ignored_fields?: string[];
|
|
17
|
+
warnings?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HotDeleteResult extends HotSnapshot {
|
|
21
|
+
cap: number;
|
|
22
|
+
store: "hot";
|
|
23
|
+
deleted: HotMemory;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LongTermWriteResult {
|
|
27
|
+
store: "long_term";
|
|
28
|
+
memory: Memory;
|
|
29
|
+
created?: boolean;
|
|
30
|
+
note?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LongTermDeleteResult { store: "long_term"; deleted: Memory }
|
|
34
|
+
|
|
35
|
+
export interface SearchRow extends Memory {
|
|
36
|
+
content_characters: number;
|
|
37
|
+
content_clipped: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SearchDetails {
|
|
41
|
+
mode: SearchMode | "browse";
|
|
42
|
+
query: string;
|
|
43
|
+
tags: string[];
|
|
44
|
+
alias_expansion_enabled: boolean;
|
|
45
|
+
expansions: Array<{ term: string; aliases: string[] }>;
|
|
46
|
+
warnings: string[];
|
|
47
|
+
results: SearchRow[];
|
|
48
|
+
next_offset: number | null;
|
|
49
|
+
elapsed_ms: number;
|
|
50
|
+
hint: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface MemoryReadDetails extends Memory {
|
|
54
|
+
content: string;
|
|
55
|
+
next_text_offset: number | null;
|
|
56
|
+
total_characters: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SessionHit {
|
|
60
|
+
entry_type: string;
|
|
61
|
+
entry_id: string | null;
|
|
62
|
+
parent_id: string | null;
|
|
63
|
+
role: string | null;
|
|
64
|
+
timestamp: string | null;
|
|
65
|
+
text: string;
|
|
66
|
+
text_clipped?: boolean;
|
|
67
|
+
path: string;
|
|
68
|
+
line: number;
|
|
69
|
+
citation: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SessionSearchDetails {
|
|
73
|
+
results: SessionHit[];
|
|
74
|
+
next_offset: number | null;
|
|
75
|
+
exhausted: boolean;
|
|
76
|
+
roots: string[];
|
|
77
|
+
warnings: string[];
|
|
78
|
+
candidate_entries: number;
|
|
79
|
+
elapsed_ms: number;
|
|
80
|
+
hint: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SessionReadDetails {
|
|
84
|
+
entry_type: string;
|
|
85
|
+
entry_id: string | null;
|
|
86
|
+
parent_id: string | null;
|
|
87
|
+
role: string | null;
|
|
88
|
+
timestamp: string | null;
|
|
89
|
+
text: string;
|
|
90
|
+
next_text_offset: number | null;
|
|
91
|
+
total_characters: number;
|
|
92
|
+
path: string;
|
|
93
|
+
line: number;
|
|
94
|
+
citation: string;
|
|
95
|
+
next_line: number;
|
|
96
|
+
hint: string;
|
|
97
|
+
warnings?: string[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Drop a UTF-8 prefix without splitting a multibyte sequence or surrogate pair. */
|
|
101
|
+
export function truncateUtf8(text: string, maxBytes: number): string {
|
|
102
|
+
if (maxBytes <= 0) return "";
|
|
103
|
+
if (Buffer.byteLength(text) <= maxBytes) return text;
|
|
104
|
+
const encoded = Buffer.from(text, "utf8");
|
|
105
|
+
let end = Math.min(maxBytes, encoded.length);
|
|
106
|
+
while (end > 0 && (encoded[end]! & 0xc0) === 0x80) end--;
|
|
107
|
+
return encoded.subarray(0, end).toString("utf8");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Bound model-facing text to RESULT_BYTES, spilling the complete structured payload to a private file. */
|
|
111
|
+
async function capped(text: string, details: unknown, purpose: string): Promise<string> {
|
|
112
|
+
if (Buffer.byteLength(text) <= RESULT_BYTES) return text;
|
|
113
|
+
const serialized = JSON.stringify(details, null, 2);
|
|
114
|
+
let notice: string;
|
|
115
|
+
try {
|
|
116
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-memoria-result-"));
|
|
117
|
+
const path = join(directory, "result.json");
|
|
118
|
+
await withFileMutationQueue(path, () => writeFile(path, serialized, { mode: 0o600 }));
|
|
119
|
+
notice = `\n\n[Response clipped to the ${RESULT_BYTES}-byte model-facing budget. ${purpose} (${Buffer.byteLength(serialized)} bytes) is at ${path}. Follow next_offset/next_text_offset or archived IDs in that file to retrieve the rest.]`;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
notice = `\n\n[Response clipped and the complete details could not be saved: ${(error as Error).message}. Use narrower queries and pagination to retrieve the remainder.]`;
|
|
122
|
+
}
|
|
123
|
+
const budget = RESULT_BYTES - Buffer.byteLength(notice) - 1;
|
|
124
|
+
return `${truncateUtf8(text, Math.max(0, budget))}${notice}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function reply(details: unknown, text: string) {
|
|
128
|
+
return { content: [{ type: "text" as const, text: await capped(text, details, "Complete structured result") }], details };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Boundary renderer for expected errors. Unexpected errors and aborts are
|
|
133
|
+
* rethrown unchanged so Pi still reports their real identity.
|
|
134
|
+
*/
|
|
135
|
+
export async function toolError(error: unknown): Promise<never> {
|
|
136
|
+
if (!(error instanceof ExpectedError)) throw error;
|
|
137
|
+
const details = { code: error.code, message: error.message, ...error.details };
|
|
138
|
+
const heading = `memoria error [${error.code}]: ${error.message}`;
|
|
139
|
+
const body = Object.keys(error.details).length ? `${heading}\n${JSON.stringify(details, null, 2)}` : heading;
|
|
140
|
+
throw new Error(await capped(body, details, "Complete error details"), { cause: error });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function tagList(tags: string[]): string {
|
|
144
|
+
return tags.length ? tags.join(", ") : "(none)";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function renderHotWrite(operation: string, details: HotWriteResult): string {
|
|
148
|
+
const lines = [
|
|
149
|
+
`${operation}: hot${details.created === undefined ? "" : `; created=${details.created}`}`,
|
|
150
|
+
`id=${details.memory.id} priority=${details.memory.priority}`,
|
|
151
|
+
`content: ${details.memory.content}`,
|
|
152
|
+
`budget: ${details.characters}/${details.cap} characters used; ${details.remaining} remaining`,
|
|
153
|
+
];
|
|
154
|
+
if (details.archived.length) {
|
|
155
|
+
lines.push(`archived: ${details.archived.map((entry) => entry.id).join(", ")} — saved in SQLite with tag hot-archive; retrieve with memoria_search query "hot-archive" or by the archived source IDs.`);
|
|
156
|
+
}
|
|
157
|
+
if (details.backup) lines.push(`backup: ${details.backup}`);
|
|
158
|
+
if (details.ignored_fields?.length) lines.push(`ignored_fields: ${details.ignored_fields.join(", ")} (hot memory stores content and priority only)`);
|
|
159
|
+
for (const warning of details.warnings ?? []) lines.push(`warning: ${warning}`);
|
|
160
|
+
if (details.note) lines.push(`note: ${details.note}`);
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function renderHotDelete(details: HotDeleteResult): string {
|
|
165
|
+
return [
|
|
166
|
+
"memoria_delete: hot",
|
|
167
|
+
`id=${details.deleted.id} priority=${details.deleted.priority}`,
|
|
168
|
+
`content: ${details.deleted.content}`,
|
|
169
|
+
`budget: ${details.characters}/${details.cap} characters used; ${details.remaining} remaining`,
|
|
170
|
+
"note: Explicit deletion does not archive the entry and does not erase original sessions.",
|
|
171
|
+
].join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function renderLongTermWrite(operation: string, details: LongTermWriteResult): string {
|
|
175
|
+
const lines = [
|
|
176
|
+
`${operation}: long_term${details.created === undefined ? "" : `; created=${details.created}`}`,
|
|
177
|
+
`id=${details.memory.id} revision=${details.memory.revision}`,
|
|
178
|
+
`content: ${details.memory.content}`,
|
|
179
|
+
`tags: ${tagList(details.memory.tags)}`,
|
|
180
|
+
`source: ${details.memory.source || "(none)"}`,
|
|
181
|
+
`timestamps: created=${details.memory.created_at}; updated=${details.memory.updated_at}`,
|
|
182
|
+
];
|
|
183
|
+
if (details.note) lines.push(`note: ${details.note}`);
|
|
184
|
+
return lines.join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function renderLongTermDelete(details: LongTermDeleteResult): string {
|
|
188
|
+
const item = details.deleted;
|
|
189
|
+
return [
|
|
190
|
+
"memoria_delete: long_term",
|
|
191
|
+
`id=${item.id} revision=${item.revision}`,
|
|
192
|
+
`content: ${item.content}`,
|
|
193
|
+
`tags: ${tagList(item.tags)}`,
|
|
194
|
+
`source: ${item.source || "(none)"}`,
|
|
195
|
+
`timestamps: created=${item.created_at}; updated=${item.updated_at}`,
|
|
196
|
+
"note: Explicit deletion does not erase original sessions or earlier backups.",
|
|
197
|
+
].join("\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function renderSearch(details: SearchDetails): string {
|
|
201
|
+
const lines = [
|
|
202
|
+
`memoria_search:${details.query ? ` query=${JSON.stringify(details.query)}` : ""} ${details.results.length} result(s); mode=${details.mode}; next_offset=${details.next_offset ?? "null"}`,
|
|
203
|
+
];
|
|
204
|
+
if (details.tags.length) lines.push(`required tags (exact, case-sensitive): ${details.tags.join(", ")}`);
|
|
205
|
+
if (details.alias_expansion_enabled) {
|
|
206
|
+
lines.push(details.expansions.length
|
|
207
|
+
? `aliases used in query: ${details.expansions.map((value) => `${value.term} -> ${value.aliases.join(", ")}`).join("; ")}`
|
|
208
|
+
: "alias expansion enabled; no configured alternatives applied to this query");
|
|
209
|
+
}
|
|
210
|
+
for (const warning of details.warnings) lines.push(`warning: ${warning}`);
|
|
211
|
+
for (const row of details.results) {
|
|
212
|
+
lines.push(
|
|
213
|
+
`[${row.id}] revision=${row.revision}; tags=[${row.tags.join(", ")}]`,
|
|
214
|
+
`${row.content}${row.content_clipped ? " …" : ""}`,
|
|
215
|
+
`source: ${row.source || "(none)"}; created ${row.created_at}; updated ${row.updated_at}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (!details.results.length) lines.push("No results on this page. Follow the hint before concluding that nothing is stored.");
|
|
219
|
+
lines.push(`hint: ${details.hint}`);
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function renderMemoryRead(details: MemoryReadDetails): string {
|
|
224
|
+
return [
|
|
225
|
+
`memoria_search: id=${details.id}; revision=${details.revision}`,
|
|
226
|
+
`tags: ${tagList(details.tags)}; source: ${details.source || "(none)"}`,
|
|
227
|
+
`timestamps: created=${details.created_at}; updated=${details.updated_at}`,
|
|
228
|
+
`content (characters ${details.content.length} of ${details.total_characters}):`,
|
|
229
|
+
details.content,
|
|
230
|
+
`next_text_offset=${details.next_text_offset ?? "null"}`,
|
|
231
|
+
].join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function renderSessionSearch(details: SessionSearchDetails): string {
|
|
235
|
+
const lines = [
|
|
236
|
+
`memoria_sessions: ${details.results.length} result(s); next_offset=${details.next_offset ?? "null"}; exhausted=${details.exhausted}; candidate_entries=${details.candidate_entries}`,
|
|
237
|
+
`roots (${details.roots.length}): ${details.roots.join(", ") || "(none)"}`,
|
|
238
|
+
];
|
|
239
|
+
for (const warning of details.warnings) lines.push(`warning: ${warning}`);
|
|
240
|
+
for (const hit of details.results) {
|
|
241
|
+
const ids = [hit.entry_id ? `entry=${hit.entry_id}` : undefined, hit.parent_id ? `parent=${hit.parent_id}` : undefined].filter(Boolean).join("; ");
|
|
242
|
+
lines.push(
|
|
243
|
+
`[${hit.entry_type}${hit.role ? `/${hit.role}` : ""}] ${hit.citation}${ids ? `; ${ids}` : ""}${hit.timestamp ? `; time=${hit.timestamp}` : ""}${hit.text_clipped ? "; preview clipped" : ""}`,
|
|
244
|
+
hit.text,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (!details.results.length) lines.push("No results on this page. An incomplete search is not proof of absence; check warnings and follow the hint.");
|
|
248
|
+
lines.push(`hint: ${details.hint}`);
|
|
249
|
+
return lines.join("\n");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function renderSessionRead(details: SessionReadDetails): string {
|
|
253
|
+
const lines = [
|
|
254
|
+
`memoria_sessions: read ${details.citation}; type=${details.entry_type}; role=${details.role ?? "(none)"}; entry_id=${details.entry_id ?? "(none)"}; parent_id=${details.parent_id ?? "(none)"}; timestamp=${details.timestamp ?? "(none)"}`,
|
|
255
|
+
];
|
|
256
|
+
for (const warning of details.warnings ?? []) lines.push(`warning: ${warning}`);
|
|
257
|
+
lines.push(
|
|
258
|
+
`text (characters ${details.text.length} of ${details.total_characters}):`,
|
|
259
|
+
details.text,
|
|
260
|
+
`next_text_offset=${details.next_text_offset ?? "null"}; next_line=${details.next_line}`,
|
|
261
|
+
`hint: ${details.hint}`,
|
|
262
|
+
);
|
|
263
|
+
return lines.join("\n");
|
|
264
|
+
}
|