pi-observational-memory 2.3.0 → 2.4.1
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 +30 -22
- package/package.json +1 -1
- package/src/branch.ts +275 -6
- package/src/commands/status.ts +37 -11
- package/src/commands/view.ts +6 -5
- package/src/compaction.ts +330 -44
- package/src/config.ts +21 -2
- package/src/hooks/compaction-hook.ts +27 -22
- package/src/hooks/compaction-trigger.ts +31 -23
- package/src/hooks/observer-trigger.ts +13 -7
- package/src/prompts.ts +37 -13
- package/src/runtime.ts +6 -1
- package/src/serialize.ts +3 -1
- package/src/tools/recall-observation.ts +315 -111
- package/src/types.ts +91 -9
|
@@ -4,27 +4,30 @@ import { defineTool, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
4
4
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
5
5
|
import { Text } from "@mariozechner/pi-tui";
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
recallMemorySources,
|
|
8
8
|
type Entry,
|
|
9
|
-
type
|
|
10
|
-
type
|
|
9
|
+
type RecallMemoryObservation,
|
|
10
|
+
type RecallMemorySourcesResult,
|
|
11
11
|
} from "../branch.js";
|
|
12
12
|
import { renderRecallSourceEntries, renderRecallSourceEntry } from "../serialize.js";
|
|
13
13
|
import { estimateEntryTokens } from "../tokens.js";
|
|
14
|
-
import type { ObservationRecord } from "../types.js";
|
|
14
|
+
import type { ObservationRecord, ReflectionRecord } from "../types.js";
|
|
15
15
|
|
|
16
16
|
export const RECALL_OBSERVATION_TOOL_NAME = "recall";
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const MEMORY_ID_PATTERN = /^[a-f0-9]{12}$/;
|
|
19
19
|
|
|
20
20
|
type RecallObservationToolStatus =
|
|
21
21
|
| "ok"
|
|
22
|
+
| "partial"
|
|
22
23
|
| "invalid_id"
|
|
23
24
|
| "not_found"
|
|
24
25
|
| "no_source"
|
|
25
|
-
| "source_unavailable"
|
|
26
|
+
| "source_unavailable"
|
|
27
|
+
| "no_provenance";
|
|
26
28
|
|
|
27
29
|
type ObservationDetails = Pick<ObservationRecord, "id" | "content" | "timestamp" | "relevance">;
|
|
30
|
+
type ReflectionDetails = Pick<ReflectionRecord, "id" | "content" | "supportingObservationIds" | "legacy"> & { reflectionIndex: number };
|
|
28
31
|
|
|
29
32
|
export type RecallSourceEntryDetails = {
|
|
30
33
|
id: string;
|
|
@@ -36,8 +39,9 @@ export type RecallSourceEntryDetails = {
|
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
type RecallObservationMatchDetails = {
|
|
39
|
-
status:
|
|
42
|
+
status: RecallMemoryObservation["status"];
|
|
40
43
|
observationEntryId: string;
|
|
44
|
+
observationRecordIndex: number;
|
|
41
45
|
observation: ObservationDetails;
|
|
42
46
|
sourceEntryIds?: string[];
|
|
43
47
|
sourceEntries?: RecallSourceEntryDetails[];
|
|
@@ -46,11 +50,33 @@ type RecallObservationMatchDetails = {
|
|
|
46
50
|
sourceCharacterCount?: number;
|
|
47
51
|
};
|
|
48
52
|
|
|
53
|
+
type RecallUnavailableSupportingObservationDetails = {
|
|
54
|
+
reflectionId: string;
|
|
55
|
+
reflectionIndex: number;
|
|
56
|
+
observationId: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type RecallUnavailableReflectionProvenanceDetails = {
|
|
60
|
+
reflectionId: string;
|
|
61
|
+
reflectionIndex: number;
|
|
62
|
+
reason: "legacy";
|
|
63
|
+
};
|
|
64
|
+
|
|
49
65
|
export type RecallObservationToolDetails = {
|
|
50
66
|
status: RecallObservationToolStatus;
|
|
67
|
+
memoryId: string;
|
|
51
68
|
observationId: string;
|
|
52
69
|
collision: boolean;
|
|
70
|
+
partial: boolean;
|
|
71
|
+
reflections: ReflectionDetails[];
|
|
72
|
+
directObservationMatches: RecallObservationMatchDetails[];
|
|
73
|
+
observations: RecallObservationMatchDetails[];
|
|
53
74
|
matches: RecallObservationMatchDetails[];
|
|
75
|
+
sourceEntries: RecallSourceEntryDetails[];
|
|
76
|
+
unavailableSupportingObservations: RecallUnavailableSupportingObservationDetails[];
|
|
77
|
+
unavailableReflectionProvenance: RecallUnavailableReflectionProvenanceDetails[];
|
|
78
|
+
missingSourceEntryIds: string[];
|
|
79
|
+
nonSourceEntryIds: string[];
|
|
54
80
|
sourceCharacterCount?: number;
|
|
55
81
|
message?: string;
|
|
56
82
|
};
|
|
@@ -143,23 +169,41 @@ function observationDetails(observation: ObservationRecord): ObservationDetails
|
|
|
143
169
|
};
|
|
144
170
|
}
|
|
145
171
|
|
|
146
|
-
function
|
|
172
|
+
function reflectionDetails(reflection: ReflectionRecord, reflectionIndex: number): ReflectionDetails {
|
|
173
|
+
return {
|
|
174
|
+
id: reflection.id,
|
|
175
|
+
content: reflection.content,
|
|
176
|
+
supportingObservationIds: reflection.supportingObservationIds,
|
|
177
|
+
...(reflection.legacy === true ? { legacy: true } : {}),
|
|
178
|
+
reflectionIndex,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function observationMatchDetails(match: RecallMemoryObservation, includeSourceContent = true): RecallObservationMatchDetails {
|
|
147
183
|
if (match.status === "ok") {
|
|
148
184
|
return {
|
|
149
185
|
status: "ok",
|
|
150
186
|
observationEntryId: match.observationEntryId,
|
|
187
|
+
observationRecordIndex: match.observationRecordIndex,
|
|
151
188
|
observation: observationDetails(match.observation),
|
|
152
189
|
sourceEntryIds: match.sourceEntryIds,
|
|
153
190
|
sourceEntries: match.sourceEntries.map((entry) => sourceEntryDetails(entry, includeSourceContent)),
|
|
154
|
-
sourceCharacterCount:
|
|
191
|
+
sourceCharacterCount: renderRecallSourceEntries(match.sourceEntries).length,
|
|
155
192
|
};
|
|
156
193
|
}
|
|
157
194
|
if (match.status === "source_unavailable") {
|
|
158
195
|
return {
|
|
159
196
|
status: "source_unavailable",
|
|
160
197
|
observationEntryId: match.observationEntryId,
|
|
198
|
+
observationRecordIndex: match.observationRecordIndex,
|
|
161
199
|
observation: observationDetails(match.observation),
|
|
162
200
|
sourceEntryIds: match.sourceEntryIds,
|
|
201
|
+
...(includeSourceContent
|
|
202
|
+
? {
|
|
203
|
+
sourceEntries: match.sourceEntries.map((entry) => sourceEntryDetails(entry, true)),
|
|
204
|
+
sourceCharacterCount: renderRecallSourceEntries(match.sourceEntries).length,
|
|
205
|
+
}
|
|
206
|
+
: {}),
|
|
163
207
|
missingSourceEntryIds: match.missingSourceEntryIds,
|
|
164
208
|
nonSourceEntryIds: match.nonSourceEntryIds,
|
|
165
209
|
};
|
|
@@ -167,6 +211,7 @@ function matchDetails(match: RecallObservationMatch, sourceText?: string, includ
|
|
|
167
211
|
return {
|
|
168
212
|
status: "no_source",
|
|
169
213
|
observationEntryId: match.observationEntryId,
|
|
214
|
+
observationRecordIndex: match.observationRecordIndex,
|
|
170
215
|
observation: observationDetails(match.observation),
|
|
171
216
|
};
|
|
172
217
|
}
|
|
@@ -178,59 +223,187 @@ function textResult(text: string, details: RecallObservationToolDetails) {
|
|
|
178
223
|
};
|
|
179
224
|
}
|
|
180
225
|
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
226
|
+
function emptyDetails(status: RecallObservationToolStatus, memoryId: string, message: string): RecallObservationToolDetails {
|
|
227
|
+
return {
|
|
228
|
+
status,
|
|
229
|
+
memoryId,
|
|
230
|
+
observationId: memoryId,
|
|
231
|
+
collision: false,
|
|
232
|
+
partial: false,
|
|
233
|
+
reflections: [],
|
|
234
|
+
directObservationMatches: [],
|
|
235
|
+
observations: [],
|
|
236
|
+
matches: [],
|
|
237
|
+
sourceEntries: [],
|
|
238
|
+
unavailableSupportingObservations: [],
|
|
239
|
+
unavailableReflectionProvenance: [],
|
|
240
|
+
missingSourceEntryIds: [],
|
|
241
|
+
nonSourceEntryIds: [],
|
|
242
|
+
message,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function aggregateStatus(details: Omit<RecallObservationToolDetails, "status">): RecallObservationToolStatus {
|
|
247
|
+
const observationOnly = details.reflections.length === 0 && details.unavailableSupportingObservations.length === 0 && details.unavailableReflectionProvenance.length === 0;
|
|
248
|
+
if (observationOnly && details.observations.some((match) => match.status === "ok")) return "ok";
|
|
249
|
+
if (observationOnly && details.observations.some((match) => match.status === "source_unavailable")) return "source_unavailable";
|
|
250
|
+
if (observationOnly && details.observations.length > 0) return "no_source";
|
|
251
|
+
if (details.unavailableReflectionProvenance.length > 0 && details.observations.length === 0 && details.sourceEntries.length === 0) return "no_provenance";
|
|
252
|
+
if (details.partial) return "partial";
|
|
253
|
+
if (details.sourceEntries.length > 0) return "ok";
|
|
254
|
+
if (details.reflections.length > 0) return "ok";
|
|
255
|
+
if (details.observations.length > 0) return "no_source";
|
|
256
|
+
return "not_found";
|
|
185
257
|
}
|
|
186
258
|
|
|
187
|
-
function friendlyNoSourceMessage(
|
|
188
|
-
return `Observation ${
|
|
259
|
+
function friendlyNoSourceMessage(memoryId: string): string {
|
|
260
|
+
return `Observation ${memoryId} has no source entries associated with it. This can happen for legacy observations created before source recall was available.`;
|
|
189
261
|
}
|
|
190
262
|
|
|
191
|
-
function friendlySourceUnavailableMessage(match:
|
|
192
|
-
const missing = match.missingSourceEntryIds.length > 0 ? ` missing: ${match.missingSourceEntryIds.join(", ")}` : "";
|
|
193
|
-
const nonSource = match.nonSourceEntryIds.length > 0 ? ` non-source: ${match.nonSourceEntryIds.join(", ")}` : "";
|
|
263
|
+
function friendlySourceUnavailableMessage(match: RecallObservationMatchDetails): string {
|
|
264
|
+
const missing = match.missingSourceEntryIds && match.missingSourceEntryIds.length > 0 ? ` missing: ${match.missingSourceEntryIds.join(", ")}` : "";
|
|
265
|
+
const nonSource = match.nonSourceEntryIds && match.nonSourceEntryIds.length > 0 ? ` non-source: ${match.nonSourceEntryIds.join(", ")}` : "";
|
|
194
266
|
return `Observation ${match.observation.id} has source entries associated, but some are unavailable on the current branch or are not source-renderable.${missing}${nonSource}`;
|
|
195
267
|
}
|
|
196
268
|
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
269
|
+
function reflectionLineText(reflection: ReflectionDetails): string {
|
|
270
|
+
return `[${reflection.id}] ${reflection.content}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function observationLineText(observation: ObservationDetails): string {
|
|
274
|
+
return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
|
|
275
|
+
}
|
|
201
276
|
|
|
277
|
+
function renderObservationOnlyTextFromResult(result: Extract<RecallMemorySourcesResult, { status: "found" }>): string {
|
|
278
|
+
const sections: string[] = [];
|
|
202
279
|
if (result.collision) {
|
|
203
|
-
sections.push(`Multiple observations share id ${result.
|
|
280
|
+
sections.push(`Multiple observations share id ${result.memoryId}; returning all matching source results from the current branch.`);
|
|
204
281
|
}
|
|
205
|
-
|
|
206
|
-
for (const match of result.matches) {
|
|
282
|
+
for (const match of result.directObservationMatches) {
|
|
207
283
|
if (match.status === "ok") {
|
|
208
284
|
const sourceText = renderRecallSourceEntries(match.sourceEntries);
|
|
209
|
-
sourceCharacterCount += sourceText.length;
|
|
210
|
-
detailsMatches.push(matchDetails(match, sourceText));
|
|
211
285
|
if (sourceText.trim()) sections.push(sourceText);
|
|
212
286
|
else sections.push(`Observation ${match.observation.id} has source entries associated, but they rendered no text content.`);
|
|
213
287
|
continue;
|
|
214
288
|
}
|
|
215
|
-
|
|
216
289
|
if (match.status === "source_unavailable") {
|
|
217
|
-
|
|
218
|
-
sections.push(friendlySourceUnavailableMessage(match));
|
|
290
|
+
sections.push(friendlySourceUnavailableMessage(observationMatchDetails(match, false)));
|
|
219
291
|
continue;
|
|
220
292
|
}
|
|
221
|
-
|
|
222
|
-
detailsMatches.push(matchDetails(match));
|
|
223
293
|
sections.push(friendlyNoSourceMessage(match.observation.id));
|
|
224
294
|
}
|
|
295
|
+
return sections.join("\n\n");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function unavailableSupportingLineText(item: RecallUnavailableSupportingObservationDetails): string {
|
|
299
|
+
return `Supporting observation ${item.observationId} for reflection ${item.reflectionId} is unavailable on the current branch.`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function unavailableReflectionProvenanceLineText(item: RecallUnavailableReflectionProvenanceDetails): string {
|
|
303
|
+
return `Reflection ${item.reflectionId} was migrated from legacy memory created before reflection provenance was recorded, so no supporting observations or raw sources are available.`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function unavailableObservationSourceLineText(match: RecallMemoryObservation): string {
|
|
307
|
+
return `Observation ${match.observation.id} has no source entries associated. This can happen for legacy observations created before source recall was available.`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function renderMemoryText(result: Extract<RecallMemorySourcesResult, { status: "found" }>): string {
|
|
311
|
+
const sections: string[] = [];
|
|
312
|
+
if (result.collision) {
|
|
313
|
+
sections.push(`Memory id ${result.memoryId} matched multiple observations/reflections; returning all available evidence from the current branch.`);
|
|
314
|
+
}
|
|
315
|
+
if (result.reflectionMatches.length > 0) {
|
|
316
|
+
sections.push(`Reflections:\n${result.reflectionMatches.map((match) => reflectionLineText(reflectionDetails(match.reflection, match.reflectionIndex))).join("\n")}`);
|
|
317
|
+
}
|
|
318
|
+
if (result.observations.length > 0) {
|
|
319
|
+
sections.push(`Observations:\n${result.observations.map((match) => observationLineText(match.observation)).join("\n")}`);
|
|
320
|
+
}
|
|
321
|
+
if (result.unavailableSupportingObservations.length > 0) {
|
|
322
|
+
sections.push(
|
|
323
|
+
`Unavailable supporting observations:\n${result.unavailableSupportingObservations
|
|
324
|
+
.map((item) => unavailableSupportingLineText({
|
|
325
|
+
reflectionId: item.reflection.id,
|
|
326
|
+
reflectionIndex: item.reflectionIndex,
|
|
327
|
+
observationId: item.observationId,
|
|
328
|
+
}))
|
|
329
|
+
.join("\n")}`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (result.unavailableReflectionProvenance.length > 0) {
|
|
333
|
+
sections.push(
|
|
334
|
+
`Unavailable reflection provenance:\n${result.unavailableReflectionProvenance
|
|
335
|
+
.map((item) => unavailableReflectionProvenanceLineText({
|
|
336
|
+
reflectionId: item.reflection.id,
|
|
337
|
+
reflectionIndex: item.reflectionIndex,
|
|
338
|
+
reason: item.reason,
|
|
339
|
+
}))
|
|
340
|
+
.join("\n")}`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const noSourceObservations = result.observations.filter((match) => match.status === "no_source");
|
|
344
|
+
if (noSourceObservations.length > 0) {
|
|
345
|
+
sections.push(`Unavailable observation sources:\n${noSourceObservations.map(unavailableObservationSourceLineText).join("\n")}`);
|
|
346
|
+
}
|
|
347
|
+
if (result.missingSourceEntryIds.length > 0 || result.nonSourceEntryIds.length > 0) {
|
|
348
|
+
const parts: string[] = [];
|
|
349
|
+
if (result.missingSourceEntryIds.length > 0) parts.push(`missing: ${result.missingSourceEntryIds.join(", ")}`);
|
|
350
|
+
if (result.nonSourceEntryIds.length > 0) parts.push(`non-source: ${result.nonSourceEntryIds.join(", ")}`);
|
|
351
|
+
sections.push(`Unavailable source entries: ${parts.join("; ")}`);
|
|
352
|
+
}
|
|
353
|
+
const sourceText = renderRecallSourceEntries(result.sourceEntries);
|
|
354
|
+
if (sourceText.trim()) sections.push(`Sources:\n${sourceText}`);
|
|
355
|
+
if (sections.length === 0) sections.push(`Memory ${result.memoryId} was found, but no source evidence rendered.`);
|
|
356
|
+
return sections.join("\n\n");
|
|
357
|
+
}
|
|
225
358
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
359
|
+
function resultDetails(result: Extract<RecallMemorySourcesResult, { status: "found" }>, includeSourceContent = true): RecallObservationToolDetails {
|
|
360
|
+
const reflections = result.reflectionMatches.map((match) => reflectionDetails(match.reflection, match.reflectionIndex));
|
|
361
|
+
const memoryLayerRecall = result.reflectionMatches.length > 0 || result.unavailableSupportingObservations.length > 0;
|
|
362
|
+
const includeObservationSources = (_match: RecallMemoryObservation) => includeSourceContent;
|
|
363
|
+
const observations = result.observations.map((match) => observationMatchDetails(match, includeObservationSources(match)));
|
|
364
|
+
const directObservationMatches = result.directObservationMatches.map((match) => observationMatchDetails(match, includeObservationSources(match)));
|
|
365
|
+
const sourceEntries = memoryLayerRecall ? result.sourceEntries.map((entry) => sourceEntryDetails(entry, includeSourceContent)) : [];
|
|
366
|
+
const unavailableSupportingObservations = result.unavailableSupportingObservations.map((item) => ({
|
|
367
|
+
reflectionId: item.reflection.id,
|
|
368
|
+
reflectionIndex: item.reflectionIndex,
|
|
369
|
+
observationId: item.observationId,
|
|
370
|
+
}));
|
|
371
|
+
const unavailableReflectionProvenance = result.unavailableReflectionProvenance.map((item) => ({
|
|
372
|
+
reflectionId: item.reflection.id,
|
|
373
|
+
reflectionIndex: item.reflectionIndex,
|
|
374
|
+
reason: item.reason,
|
|
375
|
+
}));
|
|
376
|
+
const partial = result.partial;
|
|
377
|
+
const detailWithoutStatus = {
|
|
378
|
+
memoryId: result.memoryId,
|
|
379
|
+
observationId: result.memoryId,
|
|
230
380
|
collision: result.collision,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
381
|
+
partial,
|
|
382
|
+
reflections,
|
|
383
|
+
directObservationMatches,
|
|
384
|
+
observations,
|
|
385
|
+
matches: directObservationMatches,
|
|
386
|
+
sourceEntries,
|
|
387
|
+
unavailableSupportingObservations,
|
|
388
|
+
unavailableReflectionProvenance,
|
|
389
|
+
missingSourceEntryIds: result.missingSourceEntryIds,
|
|
390
|
+
nonSourceEntryIds: result.nonSourceEntryIds,
|
|
391
|
+
sourceCharacterCount: renderRecallSourceEntries(result.sourceEntries).length,
|
|
392
|
+
};
|
|
393
|
+
return {
|
|
394
|
+
status: aggregateStatus(detailWithoutStatus),
|
|
395
|
+
...detailWithoutStatus,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function isObservationOnly(details: RecallObservationToolDetails): boolean {
|
|
400
|
+
return details.reflections.length === 0 && details.unavailableSupportingObservations.length === 0 && details.unavailableReflectionProvenance.length === 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function renderFoundResult(result: Extract<RecallMemorySourcesResult, { status: "found" }>): ReturnType<typeof textResult> {
|
|
404
|
+
const details = resultDetails(result);
|
|
405
|
+
const text = isObservationOnly(details) ? renderObservationOnlyTextFromResult(result) : renderMemoryText(result);
|
|
406
|
+
return textResult(text, details);
|
|
234
407
|
}
|
|
235
408
|
|
|
236
409
|
function plural(n: number, singular: string, pluralForm = `${singular}s`): string {
|
|
@@ -238,6 +411,7 @@ function plural(n: number, singular: string, pluralForm = `${singular}s`): strin
|
|
|
238
411
|
}
|
|
239
412
|
|
|
240
413
|
function sourceEntriesFromDetails(details: RecallObservationToolDetails): RecallSourceEntryDetails[] {
|
|
414
|
+
if (!isObservationOnly(details)) return details.sourceEntries;
|
|
241
415
|
return details.matches.flatMap((match) => match.sourceEntries ?? []);
|
|
242
416
|
}
|
|
243
417
|
|
|
@@ -245,40 +419,59 @@ function tokenSummary(tokens: number): string {
|
|
|
245
419
|
return `~${tokens.toLocaleString()} ${tokens === 1 ? "token" : "tokens"}`;
|
|
246
420
|
}
|
|
247
421
|
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
return "×";
|
|
422
|
+
function isFailureStatus(status: RecallObservationToolStatus): boolean {
|
|
423
|
+
return status === "invalid_id" || status === "not_found";
|
|
251
424
|
}
|
|
252
425
|
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
if (details.status === "not_found") return "not found";
|
|
256
|
-
if (details.status === "source_unavailable") return "source unavailable";
|
|
257
|
-
if (details.status === "no_source") return "no source";
|
|
258
|
-
return details.collision ? "recalled · id collision" : "recalled";
|
|
426
|
+
function observationCountForHeader(details: RecallObservationToolDetails): number {
|
|
427
|
+
return isObservationOnly(details) ? details.matches.length : details.observations.length;
|
|
259
428
|
}
|
|
260
429
|
|
|
261
430
|
export function formatRecallHeaderForTui(details: RecallObservationToolDetails): string {
|
|
262
|
-
|
|
263
|
-
|
|
431
|
+
if (isFailureStatus(details.status)) return "× failure";
|
|
432
|
+
|
|
433
|
+
const parts = ["✓ success"];
|
|
434
|
+
if (details.reflections.length > 0) parts.push(plural(details.reflections.length, "reflection"));
|
|
435
|
+
const observations = observationCountForHeader(details);
|
|
436
|
+
if (observations > 0) parts.push(plural(observations, "observation"));
|
|
264
437
|
const sources = sourceEntriesFromDetails(details);
|
|
265
|
-
if (sources.length > 0) parts.push(plural(sources.length, "source
|
|
438
|
+
if (sources.length > 0) parts.push(plural(sources.length, "source"));
|
|
266
439
|
const tokens = sources.reduce((sum, source) => sum + source.tokens, 0);
|
|
267
440
|
if (tokens > 0) parts.push(tokenSummary(tokens));
|
|
268
441
|
return parts.join(" · ");
|
|
269
442
|
}
|
|
270
443
|
|
|
271
|
-
|
|
272
|
-
|
|
444
|
+
const TUI_TYPE_WIDTH = 15;
|
|
445
|
+
const TUI_META_WIDTH = 31;
|
|
446
|
+
|
|
447
|
+
function alignedRow(type: string, meta: string, text: string): string {
|
|
448
|
+
return `${type.padEnd(TUI_TYPE_WIDTH)} ${meta.padEnd(TUI_META_WIDTH)} ${text}`.trimEnd();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function sourceTag(source: RecallSourceEntryDetails): string {
|
|
452
|
+
const origin = source.origin.trim().toLowerCase();
|
|
453
|
+
if (origin === "user") return "user";
|
|
454
|
+
if (origin === "assistant") return "assistant";
|
|
455
|
+
if (origin.startsWith("tool result")) return "tool";
|
|
456
|
+
if (origin.startsWith("custom message")) return "custom";
|
|
457
|
+
if (origin.startsWith("branch summary")) return "summary";
|
|
458
|
+
return origin.split(/[^a-z0-9]+/).find(Boolean) ?? "entry";
|
|
273
459
|
}
|
|
274
460
|
|
|
275
461
|
function sourceMetadataLine(source: RecallSourceEntryDetails): string {
|
|
276
|
-
|
|
277
|
-
return `✓ ${sourceLabel(source)} · ${source.timestamp} · entry ${source.id} · ${tokenSummary(source.tokens)}${qualifiers}`;
|
|
462
|
+
return alignedRow("✓ source", `${source.timestamp} [${sourceTag(source)}]`, tokenSummary(source.tokens));
|
|
278
463
|
}
|
|
279
464
|
|
|
280
465
|
function observationLine(observation: ObservationDetails): string {
|
|
281
|
-
return
|
|
466
|
+
return alignedRow("✓ observation", `${observation.timestamp} [${observation.relevance}]`, observation.content);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function reflectionLine(reflection: ReflectionDetails): string {
|
|
470
|
+
return alignedRow("✓ reflection", "", reflection.content);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function noteLine(kind: string, text: string): string {
|
|
474
|
+
return alignedRow("• note", `[${kind}]`, text);
|
|
282
475
|
}
|
|
283
476
|
|
|
284
477
|
function indentContent(content: string): string {
|
|
@@ -288,31 +481,46 @@ function indentContent(content: string): string {
|
|
|
288
481
|
.join("\n");
|
|
289
482
|
}
|
|
290
483
|
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
parts.push(`missing: ${match.missingSourceEntryIds.join(", ")}`);
|
|
484
|
+
function unavailableEvidenceMessage(details: RecallObservationToolDetails): string {
|
|
485
|
+
if (details.unavailableReflectionProvenance.length > 0 && details.observations.length === 0) {
|
|
486
|
+
return "migrated legacy reflection has no supporting observations";
|
|
295
487
|
}
|
|
296
|
-
|
|
297
|
-
parts.push(`non-source: ${match.nonSourceEntryIds.join(", ")}`);
|
|
298
|
-
}
|
|
299
|
-
return `× source unavailable${parts.length > 0 ? ` · ${parts.join(" · ")}` : ""}`;
|
|
488
|
+
return "no source entries are available for this memory id";
|
|
300
489
|
}
|
|
301
490
|
|
|
302
|
-
function
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
lines.push(
|
|
307
|
-
|
|
308
|
-
lines.push(indentContent(source.content));
|
|
309
|
-
lines.push("");
|
|
310
|
-
}
|
|
491
|
+
function pushSourceLines(lines: string[], sources: RecallSourceEntryDetails[], expanded: boolean): void {
|
|
492
|
+
for (const source of sources) {
|
|
493
|
+
lines.push(sourceMetadataLine(source));
|
|
494
|
+
if (expanded && source.content) {
|
|
495
|
+
lines.push(indentContent(source.content));
|
|
496
|
+
lines.push("");
|
|
311
497
|
}
|
|
312
|
-
return lines;
|
|
313
498
|
}
|
|
314
|
-
|
|
315
|
-
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function memoryRows(details: RecallObservationToolDetails): string[] {
|
|
502
|
+
if (isObservationOnly(details)) return details.matches.map((match) => observationLine(match.observation));
|
|
503
|
+
return [
|
|
504
|
+
...details.reflections.map((reflection) => reflectionLine(reflection)),
|
|
505
|
+
...details.observations.map((observation) => observationLine(observation.observation)),
|
|
506
|
+
];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function noteRows(details: RecallObservationToolDetails, sources: RecallSourceEntryDetails[]): string[] {
|
|
510
|
+
const notes: string[] = [];
|
|
511
|
+
if (details.status === "invalid_id") {
|
|
512
|
+
notes.push(noteLine("invalid id", `memory ids must be 12 lowercase hex characters; received ${details.memoryId}`));
|
|
513
|
+
return notes;
|
|
514
|
+
}
|
|
515
|
+
if (details.status === "not_found") {
|
|
516
|
+
notes.push(noteLine("not found", `no observation or reflection with id ${details.memoryId} was found on the current branch`));
|
|
517
|
+
return notes;
|
|
518
|
+
}
|
|
519
|
+
if (details.collision) notes.push(noteLine("id collision", `multiple memory items share ${details.memoryId}`));
|
|
520
|
+
if (sources.length === 0 && (details.reflections.length > 0 || details.observations.length > 0 || details.matches.length > 0)) {
|
|
521
|
+
notes.push(noteLine("unavailable evidence", unavailableEvidenceMessage(details)));
|
|
522
|
+
}
|
|
523
|
+
return notes;
|
|
316
524
|
}
|
|
317
525
|
|
|
318
526
|
export function formatRecallResultForTui(result: AgentToolResult<RecallObservationToolDetails>, expanded: boolean): string {
|
|
@@ -325,16 +533,17 @@ export function formatRecallResultForTui(result: AgentToolResult<RecallObservati
|
|
|
325
533
|
return text || "recall";
|
|
326
534
|
}
|
|
327
535
|
|
|
536
|
+
const sources = sourceEntriesFromDetails(details);
|
|
328
537
|
const lines: string[] = [];
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (!expanded &&
|
|
538
|
+
const rows = memoryRows(details);
|
|
539
|
+
const notes = noteRows(details, sources);
|
|
540
|
+
lines.push(...rows);
|
|
541
|
+
if (rows.length > 0 && notes.length > 0) lines.push("");
|
|
542
|
+
lines.push(...notes);
|
|
543
|
+
if ((rows.length > 0 || notes.length > 0) && sources.length > 0) lines.push("");
|
|
544
|
+
pushSourceLines(lines, sources, expanded);
|
|
545
|
+
|
|
546
|
+
if (!expanded && sources.some((source) => source.content)) {
|
|
338
547
|
lines.push("", "(Ctrl+O to expand)");
|
|
339
548
|
}
|
|
340
549
|
return lines.join("\n").trimEnd();
|
|
@@ -354,18 +563,25 @@ export function formatRecallRenderedResultForTui(result: AgentToolResult<RecallO
|
|
|
354
563
|
|
|
355
564
|
export const recallObservationTool = defineTool({
|
|
356
565
|
name: RECALL_OBSERVATION_TOOL_NAME,
|
|
357
|
-
label: "Recall
|
|
358
|
-
description:
|
|
359
|
-
|
|
566
|
+
label: "Recall memory evidence",
|
|
567
|
+
description:
|
|
568
|
+
"Recover exact evidence and source context behind a compacted observational-memory observation or reflection id on the current branch. " +
|
|
569
|
+
"Use when compressed memory is important and original source context is needed before acting.",
|
|
570
|
+
promptSnippet: "Use recall(<id>) to recover exact source context behind compacted memory observations/reflections when precision matters.",
|
|
360
571
|
promptGuidelines: [
|
|
361
|
-
"Use recall
|
|
362
|
-
"
|
|
363
|
-
"
|
|
572
|
+
"Use recall before making an important decision that depends on a compacted observation or reflection whose details are unclear.",
|
|
573
|
+
"Use recall when you need exact wording, rationale, file paths, commands, errors, commits, user constraints, or provenance behind a remembered claim.",
|
|
574
|
+
"Use recall when a broad reflection is relevant but you need its supporting observations or raw sources to continue safely.",
|
|
575
|
+
"Use recall when the user asks why you believe something, what supports a memory, or what was decided earlier.",
|
|
576
|
+
"Do not use recall as semantic search or transcript browsing; you must already have a specific 12-character memory id.",
|
|
577
|
+
"Do not recall every id preemptively. Recall only when exact source context will materially improve the next action.",
|
|
364
578
|
],
|
|
365
579
|
parameters: Type.Object({
|
|
366
580
|
id: Type.String({
|
|
367
581
|
pattern: "^[a-f0-9]{12}$",
|
|
368
|
-
description:
|
|
582
|
+
description:
|
|
583
|
+
"12-character lowercase hex observation or reflection id shown in compacted memory, /om-view, or a previous recall result. " +
|
|
584
|
+
"Must be a specific id; this tool does not search by topic.",
|
|
369
585
|
}),
|
|
370
586
|
}),
|
|
371
587
|
renderCall(args) {
|
|
@@ -375,29 +591,17 @@ export const recallObservationTool = defineTool({
|
|
|
375
591
|
return new Text(formatRecallRenderedResultForTui(result as AgentToolResult<RecallObservationToolDetails>, options.expanded), 0, 0);
|
|
376
592
|
},
|
|
377
593
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
378
|
-
const
|
|
379
|
-
if (!
|
|
380
|
-
const message = `
|
|
381
|
-
return textResult(message,
|
|
382
|
-
status: "invalid_id",
|
|
383
|
-
observationId,
|
|
384
|
-
collision: false,
|
|
385
|
-
matches: [],
|
|
386
|
-
message,
|
|
387
|
-
});
|
|
594
|
+
const memoryId = params.id;
|
|
595
|
+
if (!MEMORY_ID_PATTERN.test(memoryId)) {
|
|
596
|
+
const message = `Memory id must be 12 lowercase hex characters. Received: ${memoryId}`;
|
|
597
|
+
return textResult(message, emptyDetails("invalid_id", memoryId, message));
|
|
388
598
|
}
|
|
389
599
|
|
|
390
600
|
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
391
|
-
const result =
|
|
601
|
+
const result = recallMemorySources(branchEntries, memoryId);
|
|
392
602
|
if (result.status === "not_found") {
|
|
393
|
-
const message = `No observation with id ${
|
|
394
|
-
return textResult(message,
|
|
395
|
-
status: "not_found",
|
|
396
|
-
observationId,
|
|
397
|
-
collision: false,
|
|
398
|
-
matches: [],
|
|
399
|
-
message,
|
|
400
|
-
});
|
|
603
|
+
const message = `No observation or reflection with id ${memoryId} was found on the current branch.`;
|
|
604
|
+
return textResult(message, emptyDetails("not_found", memoryId, message));
|
|
401
605
|
}
|
|
402
606
|
|
|
403
607
|
return renderFoundResult(result);
|