@matthewfl/pi-contemplator 0.0.10 → 0.1.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 +12 -12
- package/package.json +6 -6
- package/src/agents/contemplator/agent.ts +325 -91
- package/src/agents/contemplator/prompts.ts +6 -6
- package/src/agents/observer/agent.ts +14 -6
- package/src/agents/observer/prompts.ts +16 -7
- package/src/agents/reviewer/agent.ts +24 -4
- package/src/agents/reviewer/prompts.ts +1 -1
- package/src/agents/reviewer/tools.ts +24 -9
- package/src/agents/stream-errors.ts +1 -1
- package/src/agents/summarizer/agent.ts +597 -0
- package/src/agents/summarizer/prompts.ts +46 -0
- package/src/agents/summarizer/sampling.ts +80 -0
- package/src/commands/contemplator-view.ts +22 -1
- package/src/commands/settings.ts +73 -69
- package/src/commands/status.ts +60 -36
- package/src/commands/summarizer-view.ts +58 -0
- package/src/commands/view.ts +22 -10
- package/src/config.ts +25 -32
- package/src/hooks/compaction-hook.ts +32 -17
- package/src/hooks/compaction-resume.ts +4 -4
- package/src/hooks/compaction-trigger.ts +33 -11
- package/src/hooks/consolidation-trigger.ts +213 -196
- package/src/memory-citations.ts +37 -0
- package/src/required-tool-choice.ts +28 -0
- package/src/runtime.ts +115 -32
- package/src/session-ledger/fold.ts +82 -53
- package/src/session-ledger/index.ts +1 -0
- package/src/session-ledger/pools.ts +77 -0
- package/src/session-ledger/progress.ts +7 -18
- package/src/session-ledger/projection.ts +45 -177
- package/src/session-ledger/recall.ts +129 -127
- package/src/session-ledger/render-summary.ts +20 -19
- package/src/session-ledger/search.ts +99 -115
- package/src/session-ledger/types.ts +102 -75
- package/src/tools/compact-context.ts +1 -1
- package/src/tools/recall-observation.ts +99 -459
- package/src/tools/search-memories.ts +31 -72
- package/src/agents/dropper/agent.ts +0 -291
- package/src/agents/dropper/coverage.ts +0 -128
- package/src/agents/dropper/pool.ts +0 -67
- package/src/agents/dropper/prompts.ts +0 -48
- package/src/agents/reflector/agent.ts +0 -213
- package/src/agents/reflector/prompts.ts +0 -81
|
@@ -1,484 +1,120 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { Static } from "typebox";
|
|
4
|
-
import type { Message, ToolResultMessage } from "@earendil-works/pi-ai";
|
|
5
4
|
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
type RecallResult,
|
|
11
|
-
type RecalledObservation,
|
|
12
|
-
} from "../session-ledger/recall.js";
|
|
13
|
-
import type { Observation, Reflection, ReviewResult } from "../session-ledger/index.js";
|
|
14
|
-
import { renderRecallSourceEntries, renderRecallSourceEntry } from "../serialize.js";
|
|
15
|
-
import { estimateEntryTokens } from "../tokens.js";
|
|
6
|
+
import { recallMemorySources, type Entry, type RecallResult } from "../session-ledger/recall.js";
|
|
7
|
+
import type { MemoryVisibility, Observation, ReviewResult, Summary } from "../session-ledger/index.js";
|
|
8
|
+
import { renderRecallSourceEntries } from "../serialize.js";
|
|
16
9
|
|
|
17
10
|
export const RECALL_OBSERVATION_TOOL_NAME = "recall";
|
|
18
11
|
export const RECALL_DESCRIPTION =
|
|
19
|
-
"Recover exact
|
|
12
|
+
"Recover one exact observation, summary, or review memory and its immediate summary-graph links. Follow cited source or forward-pointer ids with additional recall calls.";
|
|
20
13
|
|
|
21
14
|
const MEMORY_ID_PATTERN = /^[a-f0-9]{12}$/;
|
|
22
15
|
|
|
23
|
-
type
|
|
24
|
-
| "ok"
|
|
25
|
-
| "partial"
|
|
26
|
-
| "invalid_id"
|
|
27
|
-
| "not_found"
|
|
28
|
-
| "no_source"
|
|
29
|
-
| "source_unavailable";
|
|
30
|
-
|
|
31
|
-
type ObservationDetails = Pick<Observation, "id" | "content" | "timestamp" | "relevance"> & { status?: "active" | "dropped" };
|
|
32
|
-
type ReflectionDetails = Pick<Reflection, "id" | "content" | "supportingObservationIds"> & { reflectionIndex: number };
|
|
33
|
-
|
|
34
|
-
export type RecallSourceEntryDetails = {
|
|
35
|
-
id: string;
|
|
36
|
-
origin: string;
|
|
37
|
-
timestamp: string;
|
|
38
|
-
tokens: number;
|
|
39
|
-
qualifiers: string[];
|
|
40
|
-
content?: string;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
type RecallObservationMatchDetails = {
|
|
44
|
-
status: "active" | "dropped" | "source_unavailable" | "no_source";
|
|
45
|
-
observationEntryId: string;
|
|
46
|
-
observationRecordIndex: number;
|
|
47
|
-
observation: ObservationDetails;
|
|
48
|
-
sourceEntryIds?: string[];
|
|
49
|
-
sourceEntries?: RecallSourceEntryDetails[];
|
|
50
|
-
missingSourceEntryIds?: string[];
|
|
51
|
-
nonSourceEntryIds?: string[];
|
|
52
|
-
sourceCharacterCount?: number;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
type RecallUnavailableSupportingObservationDetails = {
|
|
56
|
-
observationId: string;
|
|
57
|
-
};
|
|
16
|
+
type RecallToolStatus = "ok" | "partial" | "invalid_id" | "not_found";
|
|
58
17
|
|
|
59
18
|
export type RecallObservationToolDetails = {
|
|
60
|
-
status:
|
|
19
|
+
status: RecallToolStatus;
|
|
61
20
|
memoryId: string;
|
|
62
|
-
observationId: string;
|
|
63
21
|
collision: boolean;
|
|
64
22
|
partial: boolean;
|
|
65
|
-
|
|
23
|
+
observations: Array<{
|
|
24
|
+
observation: Observation;
|
|
25
|
+
visibility: MemoryVisibility;
|
|
26
|
+
consumedBySummaryId?: string;
|
|
27
|
+
citedBySummaryIds: string[];
|
|
28
|
+
sourceEntryIds: string[];
|
|
29
|
+
missingSourceEntryIds: string[];
|
|
30
|
+
nonSourceEntryIds: string[];
|
|
31
|
+
}>;
|
|
32
|
+
summaries: Array<{
|
|
33
|
+
summary: Summary;
|
|
34
|
+
visibility: MemoryVisibility;
|
|
35
|
+
consumedBySummaryId?: string;
|
|
36
|
+
citedBySummaryIds: string[];
|
|
37
|
+
missingSourceMemoryIds: string[];
|
|
38
|
+
}>;
|
|
66
39
|
reviews: ReviewResult[];
|
|
67
|
-
|
|
68
|
-
observations: RecallObservationMatchDetails[];
|
|
69
|
-
matches: RecallObservationMatchDetails[];
|
|
70
|
-
sourceEntries: RecallSourceEntryDetails[];
|
|
71
|
-
unavailableSupportingObservations: RecallUnavailableSupportingObservationDetails[];
|
|
40
|
+
sourceEntries: Entry[];
|
|
72
41
|
missingSourceEntryIds: string[];
|
|
73
42
|
nonSourceEntryIds: string[];
|
|
74
|
-
|
|
43
|
+
missingSourceMemoryIds: string[];
|
|
75
44
|
message?: string;
|
|
76
45
|
};
|
|
77
46
|
|
|
78
|
-
function
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function fmtLocal(d: Date): string {
|
|
83
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function formatDisplayTimestamp(...values: Array<number | string | undefined>): string {
|
|
87
|
-
for (const v of values) {
|
|
88
|
-
if (v === undefined) continue;
|
|
89
|
-
const d = new Date(v);
|
|
90
|
-
if (!Number.isNaN(d.getTime())) return fmtLocal(d);
|
|
91
|
-
}
|
|
92
|
-
return "Unknown time";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function textContentBlocks(content: unknown): Array<Record<string, unknown>> {
|
|
96
|
-
return Array.isArray(content) ? content.filter((block): block is Record<string, unknown> => !!block && typeof block === "object") : [];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function uniqueStrings(items: string[]): string[] {
|
|
100
|
-
return Array.from(new Set(items));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function sourceOriginAndQualifiers(entry: Entry): { origin: string; timestamp: string; qualifiers: string[] } {
|
|
104
|
-
if (entry.type === "message" && entry.message && typeof entry.message === "object") {
|
|
105
|
-
const msg = entry.message as Message;
|
|
106
|
-
const timestamp = formatDisplayTimestamp(msg.timestamp, entry.timestamp);
|
|
107
|
-
if (msg.role === "user") return { origin: "User", timestamp, qualifiers: [] };
|
|
108
|
-
if (msg.role === "assistant") {
|
|
109
|
-
const toolCalls = uniqueStrings(
|
|
110
|
-
textContentBlocks(msg.content)
|
|
111
|
-
.filter((block) => block.type === "toolCall" && typeof block.name === "string")
|
|
112
|
-
.map((block) => block.name as string),
|
|
113
|
-
);
|
|
114
|
-
return { origin: "Assistant", timestamp, qualifiers: toolCalls.length > 0 ? [`tool calls: ${toolCalls.join(", ")}`] : [] };
|
|
115
|
-
}
|
|
116
|
-
const toolName = (msg as ToolResultMessage).toolName;
|
|
117
|
-
return { origin: `Tool result: ${typeof toolName === "string" && toolName ? toolName : "unknown"}`, timestamp, qualifiers: [] };
|
|
118
|
-
}
|
|
119
|
-
if (entry.type === "custom_message") {
|
|
120
|
-
return {
|
|
121
|
-
origin: "Custom message",
|
|
122
|
-
timestamp: formatDisplayTimestamp(entry.timestamp),
|
|
123
|
-
qualifiers: typeof entry.customType === "string" && entry.customType ? [`custom: ${entry.customType}`] : [],
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
if (entry.type === "branch_summary") return { origin: "Branch summary", timestamp: formatDisplayTimestamp(entry.timestamp), qualifiers: [] };
|
|
127
|
-
return { origin: entry.type || "Entry", timestamp: formatDisplayTimestamp(entry.timestamp), qualifiers: [] };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function renderSourceEntryContentOnly(entry: Entry): string | undefined {
|
|
131
|
-
const rendered = renderRecallSourceEntry(entry);
|
|
132
|
-
return rendered?.replace(/^\[[^\]]+\]:\s?/, "") || undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function sourceEntryDetails(entry: Entry, includeContent: boolean): RecallSourceEntryDetails {
|
|
136
|
-
const { origin, timestamp, qualifiers } = sourceOriginAndQualifiers(entry);
|
|
137
|
-
const content = renderSourceEntryContentOnly(entry);
|
|
138
|
-
return {
|
|
139
|
-
id: entry.id,
|
|
140
|
-
origin,
|
|
141
|
-
timestamp,
|
|
142
|
-
tokens: estimateEntryTokens(entry),
|
|
143
|
-
qualifiers,
|
|
144
|
-
...(includeContent && content ? { content } : {}),
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function observationDetails(observation: Observation, status?: "active" | "dropped"): ObservationDetails {
|
|
149
|
-
return { id: observation.id, content: observation.content, timestamp: observation.timestamp, relevance: observation.relevance, ...(status ? { status } : {}) };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function reflectionDetails(reflection: Reflection, reflectionIndex: number): ReflectionDetails {
|
|
153
|
-
return { id: reflection.id, content: reflection.content, supportingObservationIds: reflection.supportingObservationIds, reflectionIndex };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function observationMatchDetails(match: RecalledObservation, includeSourceContent = true): RecallObservationMatchDetails {
|
|
157
|
-
const unavailable = match.missingSourceEntryIds.length > 0 || match.nonSourceEntryIds.length > 0;
|
|
158
|
-
const status = unavailable ? "source_unavailable" : match.sourceEntries.length === 0 ? "no_source" : match.status;
|
|
159
|
-
return {
|
|
160
|
-
status,
|
|
161
|
-
observationEntryId: match.observationEntryId,
|
|
162
|
-
observationRecordIndex: match.observationRecordIndex,
|
|
163
|
-
observation: observationDetails(match.observation, match.status),
|
|
164
|
-
sourceEntryIds: match.sourceEntryIds,
|
|
165
|
-
sourceEntries: match.sourceEntries.map((entry) => sourceEntryDetails(entry, includeSourceContent)),
|
|
166
|
-
missingSourceEntryIds: match.missingSourceEntryIds,
|
|
167
|
-
nonSourceEntryIds: match.nonSourceEntryIds,
|
|
168
|
-
sourceCharacterCount: renderRecallSourceEntries(match.sourceEntries).length,
|
|
169
|
-
};
|
|
47
|
+
function emptyDetails(status: RecallToolStatus, memoryId: string, message: string): RecallObservationToolDetails {
|
|
48
|
+
return { status, memoryId, collision: false, partial: false, observations: [], summaries: [], reviews: [], sourceEntries: [], missingSourceEntryIds: [], nonSourceEntryIds: [], missingSourceMemoryIds: [], message };
|
|
170
49
|
}
|
|
171
50
|
|
|
172
51
|
function textResult(text: string, details: RecallObservationToolDetails) {
|
|
173
52
|
return { content: [{ type: "text" as const, text }], details };
|
|
174
53
|
}
|
|
175
54
|
|
|
176
|
-
function
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
collision: false,
|
|
182
|
-
partial: false,
|
|
183
|
-
reflections: [],
|
|
184
|
-
reviews: [],
|
|
185
|
-
directObservationMatches: [],
|
|
186
|
-
observations: [],
|
|
187
|
-
matches: [],
|
|
188
|
-
sourceEntries: [],
|
|
189
|
-
unavailableSupportingObservations: [],
|
|
190
|
-
missingSourceEntryIds: [],
|
|
191
|
-
nonSourceEntryIds: [],
|
|
192
|
-
message,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function aggregateStatus(details: Omit<RecallObservationToolDetails, "status">): RecallObservationToolStatus {
|
|
197
|
-
const observationOnly = details.reflections.length === 0 && details.unavailableSupportingObservations.length === 0;
|
|
198
|
-
if (details.partial) return "partial";
|
|
199
|
-
if (observationOnly && details.observations.some((match) => match.status === "source_unavailable")) return "source_unavailable";
|
|
200
|
-
if (observationOnly && details.observations.length > 0 && details.sourceEntries.length === 0 && details.matches.every((match) => (match.sourceEntries ?? []).length === 0)) return "no_source";
|
|
201
|
-
return "ok";
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function friendlyNoSourceMessage(memoryId: string): string {
|
|
205
|
-
return `Observation ${memoryId} has no source entries associated with it.`;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function friendlySourceUnavailableMessage(match: RecallObservationMatchDetails): string {
|
|
209
|
-
const missing = match.missingSourceEntryIds && match.missingSourceEntryIds.length > 0 ? ` missing: ${match.missingSourceEntryIds.join(", ")}` : "";
|
|
210
|
-
const nonSource = match.nonSourceEntryIds && match.nonSourceEntryIds.length > 0 ? ` non-source: ${match.nonSourceEntryIds.join(", ")}` : "";
|
|
211
|
-
return `Observation ${match.observation.id} has source entries associated, but some are unavailable on the current branch or are not source-renderable.${missing}${nonSource}`;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function reflectionLineText(reflection: ReflectionDetails): string {
|
|
215
|
-
return `[${reflection.id}] ${reflection.content}`;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function observationLineText(observation: ObservationDetails): string {
|
|
219
|
-
const status = observation.status === "dropped" ? " [dropped]" : "";
|
|
220
|
-
return `[${observation.id}]${status} ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function directObservationMatches(result: Extract<RecallResult, { status: "found" }>): RecalledObservation[] {
|
|
224
|
-
return result.observations.filter((match) => match.observation.id === result.memoryId);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function renderObservationOnlyTextFromResult(result: Extract<RecallResult, { status: "found" }>): string {
|
|
228
|
-
const sections: string[] = [];
|
|
229
|
-
if (result.collision) sections.push(`Memory id ${result.memoryId} matched multiple observations; returning all matching source results from the current branch.`);
|
|
230
|
-
for (const match of directObservationMatches(result)) {
|
|
231
|
-
if (match.status === "dropped") sections.push(`Observation ${match.observation.id} is dropped from active memory but remains recallable.`);
|
|
232
|
-
if (match.missingSourceEntryIds.length > 0 || match.nonSourceEntryIds.length > 0) {
|
|
233
|
-
sections.push(friendlySourceUnavailableMessage(observationMatchDetails(match, false)));
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
if (match.sourceEntries.length === 0) {
|
|
237
|
-
sections.push(friendlyNoSourceMessage(match.observation.id));
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
const sourceText = renderRecallSourceEntries(match.sourceEntries);
|
|
241
|
-
sections.push(sourceText.trim() ? sourceText : `Observation ${match.observation.id} has source entries associated, but they rendered no text content.`);
|
|
242
|
-
}
|
|
243
|
-
return sections.join("\n\n");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function unavailableSupportingLineText(item: RecallUnavailableSupportingObservationDetails): string {
|
|
247
|
-
return `Supporting observation ${item.observationId} is unavailable on the current branch.`;
|
|
55
|
+
function graphLines(citedBy: string[], consumedBy?: string): string[] {
|
|
56
|
+
return [
|
|
57
|
+
...(consumedBy ? [`Consumed from automatic context by summary [${consumedBy}].`] : []),
|
|
58
|
+
`Cited by summaries: ${citedBy.length ? `[${citedBy.join(", ")}]` : "(none)"}.`,
|
|
59
|
+
];
|
|
248
60
|
}
|
|
249
61
|
|
|
250
|
-
function
|
|
251
|
-
const lines = [
|
|
252
|
-
`[${review.id}] ${review.scope.toUpperCase()} REVIEW ${review.outcome === "proposal" ? "PROPOSAL" : "CONCLUDED WITH NO PROPOSAL"}`,
|
|
253
|
-
"Author: background reviewer",
|
|
254
|
-
"Requested by: contemplator",
|
|
255
|
-
"Status: advisory; not evidence of implementation or validation",
|
|
256
|
-
`Review request: ${review.reviewRequestId}`,
|
|
257
|
-
];
|
|
62
|
+
function reviewText(review: ReviewResult): string {
|
|
258
63
|
if (review.outcome === "no_proposal") {
|
|
259
|
-
|
|
260
|
-
if (review.reconsiderIf) lines.push("", "Reconsider if:", review.reconsiderIf);
|
|
261
|
-
return lines.join("\n");
|
|
262
|
-
}
|
|
263
|
-
lines.push("", "Title:", review.title, "", "Summary:", review.summary, "", "Evidence:", review.evidence);
|
|
264
|
-
if (review.proposalKind === "workflow") {
|
|
265
|
-
lines.push("", "Inefficiency:", review.inefficiency, "", "Conceptual design:", review.conceptualDesign);
|
|
266
|
-
if (review.inputs) lines.push("", "Inputs:", review.inputs);
|
|
267
|
-
if (review.outputs) lines.push("", "Outputs:", review.outputs);
|
|
268
|
-
if (review.integration) lines.push("", "Integration:", review.integration);
|
|
269
|
-
} else {
|
|
270
|
-
lines.push("", "Structural issue:", review.structuralIssue, "", "Conceptual design:", review.conceptualDesign, "", "Preserved behavior:", review.preservedBehavior);
|
|
64
|
+
return [`REVIEW [${review.id}] ${review.scope} — no proposal`, `Reason: ${review.reason}`, `Evidence reviewed: ${review.evidenceReviewed}`, ...(review.reconsiderIf ? [`Reconsider if: ${review.reconsiderIf}`] : [])].join("\n");
|
|
271
65
|
}
|
|
272
|
-
|
|
273
|
-
|
|
66
|
+
const scopeDetails = review.proposalKind === "workflow"
|
|
67
|
+
? [`Inefficiency: ${review.inefficiency}`]
|
|
68
|
+
: [`Structural issue: ${review.structuralIssue}`, `Preserved behavior: ${review.preservedBehavior}`];
|
|
69
|
+
return [`REVIEW [${review.id}] ${review.scope} proposal — ${review.title}`, review.summary, `Evidence: ${review.evidence}`, ...scopeDetails, `Conceptual design: ${review.conceptualDesign}`, `Expected effect: ${review.expectedEffect}`, `Uncertainties: ${review.uncertainties}`].join("\n");
|
|
274
70
|
}
|
|
275
71
|
|
|
276
|
-
function
|
|
72
|
+
function renderFound(result: Extract<RecallResult, { status: "found" }>): ReturnType<typeof textResult> {
|
|
277
73
|
const sections: string[] = [];
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
sections.push(`Unavailable source entries: ${parts.join("; ")}`);
|
|
74
|
+
for (const match of result.observations) {
|
|
75
|
+
sections.push([
|
|
76
|
+
`OBSERVATION [${match.observation.id}] [${match.visibility}] ${match.observation.timestamp} [${match.observation.relevance}]`,
|
|
77
|
+
match.observation.content,
|
|
78
|
+
...graphLines(match.citedBySummaryIds, match.consumedBySummaryId),
|
|
79
|
+
`Source entry ids: [${match.sourceEntryIds.join(", ")}].`,
|
|
80
|
+
match.sourceEntries.length ? `Exact source context:\n${renderRecallSourceEntries(match.sourceEntries)}` : "Exact source context is unavailable.",
|
|
81
|
+
].join("\n"));
|
|
287
82
|
}
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
const detailWithoutStatus = {
|
|
83
|
+
for (const match of result.summaries) {
|
|
84
|
+
sections.push([
|
|
85
|
+
`SUMMARY [${match.summary.id}] [${match.visibility}]`,
|
|
86
|
+
match.summary.content,
|
|
87
|
+
`Source memories: [${match.sourceMemoryIds.join(", ")}].`,
|
|
88
|
+
`Consumed memories: [${match.consumedMemoryIds.join(", ")}].`,
|
|
89
|
+
...graphLines(match.citedBySummaryIds, match.consumedBySummaryId),
|
|
90
|
+
].join("\n"));
|
|
91
|
+
}
|
|
92
|
+
for (const match of result.reviews) sections.push(`${reviewText(match.review)}\n${graphLines(match.citedBySummaryIds).join("\n")}`);
|
|
93
|
+
const details: RecallObservationToolDetails = {
|
|
94
|
+
status: result.partial ? "partial" : "ok",
|
|
301
95
|
memoryId: result.memoryId,
|
|
302
|
-
observationId: result.memoryId,
|
|
303
96
|
collision: result.collision,
|
|
304
97
|
partial: result.partial,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
matches: directMatches,
|
|
310
|
-
sourceEntries,
|
|
311
|
-
unavailableSupportingObservations: result.missingSupportingObservationIds.map((observationId) => ({ observationId })),
|
|
98
|
+
observations: result.observations.map((match) => ({ observation: match.observation, visibility: match.visibility, ...(match.consumedBySummaryId ? { consumedBySummaryId: match.consumedBySummaryId } : {}), citedBySummaryIds: match.citedBySummaryIds, sourceEntryIds: match.sourceEntryIds, missingSourceEntryIds: match.missingSourceEntryIds, nonSourceEntryIds: match.nonSourceEntryIds })),
|
|
99
|
+
summaries: result.summaries.map((match) => ({ summary: match.summary, visibility: match.visibility, ...(match.consumedBySummaryId ? { consumedBySummaryId: match.consumedBySummaryId } : {}), citedBySummaryIds: match.citedBySummaryIds, missingSourceMemoryIds: match.missingSourceMemoryIds })),
|
|
100
|
+
reviews: result.reviews.map((match) => match.review),
|
|
101
|
+
sourceEntries: result.sourceEntries,
|
|
312
102
|
missingSourceEntryIds: result.missingSourceEntryIds,
|
|
313
103
|
nonSourceEntryIds: result.nonSourceEntryIds,
|
|
314
|
-
|
|
104
|
+
missingSourceMemoryIds: result.missingSourceMemoryIds,
|
|
315
105
|
};
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function isObservationOnly(details: RecallObservationToolDetails): boolean {
|
|
320
|
-
return details.reflections.length === 0 && details.unavailableSupportingObservations.length === 0;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function renderFoundResult(result: Extract<RecallResult, { status: "found" }>): ReturnType<typeof textResult> {
|
|
324
|
-
const details = resultDetails(result);
|
|
325
|
-
const text = result.kind === "review"
|
|
326
|
-
? result.reviews.map((match) => renderReviewText(match.review)).join("\n\n")
|
|
327
|
-
: result.kind === "observation" ? renderObservationOnlyTextFromResult(result) : renderMemoryText(result);
|
|
328
|
-
return textResult(text, details);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function plural(n: number, singular: string, pluralForm = `${singular}s`): string {
|
|
332
|
-
return `${n.toLocaleString()} ${n === 1 ? singular : pluralForm}`;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function sourceEntriesFromDetails(details: RecallObservationToolDetails): RecallSourceEntryDetails[] {
|
|
336
|
-
if (!isObservationOnly(details)) return details.sourceEntries;
|
|
337
|
-
return details.matches.flatMap((match) => match.sourceEntries ?? []);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function tokenSummary(tokens: number): string {
|
|
341
|
-
return `~${tokens.toLocaleString()} ${tokens === 1 ? "token" : "tokens"}`;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function isFailureStatus(status: RecallObservationToolStatus): boolean {
|
|
345
|
-
return status === "invalid_id" || status === "not_found";
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function observationCountForHeader(details: RecallObservationToolDetails): number {
|
|
349
|
-
return isObservationOnly(details) ? details.matches.length : details.observations.length;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export function formatRecallHeaderForTui(details: RecallObservationToolDetails): string {
|
|
353
|
-
if (isFailureStatus(details.status)) return "× failure";
|
|
354
|
-
const parts = ["✓ success"];
|
|
355
|
-
if (details.reflections.length > 0) parts.push(plural(details.reflections.length, "reflection"));
|
|
356
|
-
const observations = observationCountForHeader(details);
|
|
357
|
-
if (observations > 0) parts.push(plural(observations, "observation"));
|
|
358
|
-
const sources = sourceEntriesFromDetails(details);
|
|
359
|
-
if (sources.length > 0) parts.push(plural(sources.length, "source"));
|
|
360
|
-
const tokens = sources.reduce((sum, source) => sum + source.tokens, 0);
|
|
361
|
-
if (tokens > 0) parts.push(tokenSummary(tokens));
|
|
362
|
-
if (details.partial && details.status !== "ok") parts.push(details.status.replace(/_/g, " "));
|
|
363
|
-
return parts.join(" · ");
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const TUI_TYPE_WIDTH = 15;
|
|
367
|
-
const TUI_META_WIDTH = 31;
|
|
368
|
-
|
|
369
|
-
function alignedRow(type: string, meta: string, text: string): string {
|
|
370
|
-
return `${type.padEnd(TUI_TYPE_WIDTH)} ${meta.padEnd(TUI_META_WIDTH)} ${text}`.trimEnd();
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function sourceTag(source: RecallSourceEntryDetails): string {
|
|
374
|
-
const origin = source.origin.trim().toLowerCase();
|
|
375
|
-
if (origin === "user") return "user";
|
|
376
|
-
if (origin === "assistant") return "assistant";
|
|
377
|
-
if (origin.startsWith("tool result")) return "tool";
|
|
378
|
-
if (origin.startsWith("custom message")) return "custom";
|
|
379
|
-
if (origin.startsWith("branch summary")) return "summary";
|
|
380
|
-
return origin.split(/[^a-z0-9]+/).find(Boolean) ?? "entry";
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function sourceMetadataLine(source: RecallSourceEntryDetails): string {
|
|
384
|
-
return alignedRow("✓ source", `${source.timestamp} [${sourceTag(source)}]`, tokenSummary(source.tokens));
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function observationLine(observation: ObservationDetails): string {
|
|
388
|
-
const status = observation.status === "dropped" ? " dropped" : "";
|
|
389
|
-
return alignedRow("✓ observation", `${observation.timestamp} [${observation.relevance}]${status}`, observation.content);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function reflectionLine(reflection: ReflectionDetails): string {
|
|
393
|
-
return alignedRow("✓ reflection", "", reflection.content);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function noteLine(kind: string, text: string): string {
|
|
397
|
-
return alignedRow("• note", `[${kind}]`, text);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function indentContent(content: string): string {
|
|
401
|
-
return content.split("\n").map((line) => ` ${line}`).join("\n");
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function unavailableEvidenceMessage(_details: RecallObservationToolDetails): string {
|
|
405
|
-
return "no source entries are available for this memory id";
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function pushSourceLines(lines: string[], sources: RecallSourceEntryDetails[], expanded: boolean): void {
|
|
409
|
-
for (const source of sources) {
|
|
410
|
-
lines.push(sourceMetadataLine(source));
|
|
411
|
-
if (expanded && source.content) {
|
|
412
|
-
lines.push(indentContent(source.content));
|
|
413
|
-
lines.push("");
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function memoryRows(details: RecallObservationToolDetails): string[] {
|
|
419
|
-
if (isObservationOnly(details)) return details.matches.map((match) => observationLine(match.observation));
|
|
420
|
-
return [...details.reflections.map((reflection) => reflectionLine(reflection)), ...details.observations.map((observation) => observationLine(observation.observation))];
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function noteRows(details: RecallObservationToolDetails, sources: RecallSourceEntryDetails[]): string[] {
|
|
424
|
-
const notes: string[] = [];
|
|
425
|
-
if (details.status === "invalid_id") {
|
|
426
|
-
notes.push(noteLine("invalid id", `memory ids must be 12 lowercase hex characters; received ${details.memoryId}`));
|
|
427
|
-
return notes;
|
|
428
|
-
}
|
|
429
|
-
if (details.status === "not_found") {
|
|
430
|
-
notes.push(noteLine("not found", `no observation or reflection with id ${details.memoryId} was found on the current branch`));
|
|
431
|
-
return notes;
|
|
432
|
-
}
|
|
433
|
-
if (details.collision) notes.push(noteLine("id collision", `multiple memory items share ${details.memoryId}`));
|
|
434
|
-
if (details.observations.some((match) => match.observation.status === "dropped")) notes.push(noteLine("dropped", "one or more observations are dropped from active memory but remain recallable"));
|
|
435
|
-
if (details.unavailableSupportingObservations.length > 0) notes.push(noteLine("missing support", details.unavailableSupportingObservations.map((item) => item.observationId).join(", ")));
|
|
436
|
-
if (details.missingSourceEntryIds.length > 0) notes.push(noteLine("missing source", details.missingSourceEntryIds.join(", ")));
|
|
437
|
-
if (details.nonSourceEntryIds.length > 0) notes.push(noteLine("non-source", details.nonSourceEntryIds.join(", ")));
|
|
438
|
-
if (sources.length === 0 && (details.reflections.length > 0 || details.observations.length > 0 || details.matches.length > 0)) notes.push(noteLine("unavailable evidence", unavailableEvidenceMessage(details)));
|
|
439
|
-
return notes;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
export function formatRecallResultForTui(result: AgentToolResult<RecallObservationToolDetails>, expanded: boolean): string {
|
|
443
|
-
const details = result.details;
|
|
444
|
-
if (!details) {
|
|
445
|
-
const text = result.content.filter((part): part is { type: "text"; text: string } => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
|
|
446
|
-
return text || "recall";
|
|
447
|
-
}
|
|
448
|
-
const sources = sourceEntriesFromDetails(details);
|
|
449
|
-
const lines: string[] = [];
|
|
450
|
-
const rows = memoryRows(details);
|
|
451
|
-
const notes = noteRows(details, sources);
|
|
452
|
-
lines.push(...rows);
|
|
453
|
-
if (rows.length > 0 && notes.length > 0) lines.push("");
|
|
454
|
-
lines.push(...notes);
|
|
455
|
-
if ((rows.length > 0 || notes.length > 0) && sources.length > 0) lines.push("");
|
|
456
|
-
pushSourceLines(lines, sources, expanded);
|
|
457
|
-
if (!expanded && sources.some((source) => source.content)) lines.push("", "(Ctrl+O to expand)");
|
|
458
|
-
return lines.join("\n").trimEnd();
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export function formatRecallCallForTui(id: string | undefined): string {
|
|
462
|
-
return `recall ${id ?? "..."}`;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
export function formatRecallRenderedResultForTui(result: AgentToolResult<RecallObservationToolDetails>, expanded: boolean): string {
|
|
466
|
-
const body = formatRecallResultForTui(result, expanded);
|
|
467
|
-
const header = result.details ? formatRecallHeaderForTui(result.details) : undefined;
|
|
468
|
-
if (header && body) return `\n${header}\n\n${body}`;
|
|
469
|
-
if (header) return `\n${header}`;
|
|
470
|
-
return body ? `\n${body}` : "";
|
|
106
|
+
if (result.collision) sections.unshift("WARNING: this id matched more than one durable record.");
|
|
107
|
+
if (result.partial) sections.push(`WARNING: some linked evidence was unavailable (${[...result.missingSourceEntryIds, ...result.nonSourceEntryIds, ...result.missingSourceMemoryIds].join(", ")}).`);
|
|
108
|
+
return textResult(sections.join("\n\n"), details);
|
|
471
109
|
}
|
|
472
110
|
|
|
473
111
|
export const RECALL_PARAMETERS = Type.Object({
|
|
474
|
-
id: Type.String({
|
|
475
|
-
pattern: "^[a-f0-9]{12}$",
|
|
476
|
-
description: "12-character lowercase hex observation or reflection id shown in compacted memory, /om:view, or a previous recall result. Must be a specific id; this tool does not search by topic.",
|
|
477
|
-
}),
|
|
112
|
+
id: Type.String({ pattern: "^[a-f0-9]{12}$", description: "Exact 12-character lowercase hexadecimal memory id." }),
|
|
478
113
|
});
|
|
479
114
|
export type RecallArgs = Static<typeof RECALL_PARAMETERS>;
|
|
115
|
+
export type RecallAgentToolOptions = Record<string, never>;
|
|
480
116
|
|
|
481
|
-
export function executeRecall(params: RecallArgs, getBranch: () => Entry[]) {
|
|
117
|
+
export function executeRecall(params: RecallArgs, getBranch: () => Entry[], _options: RecallAgentToolOptions = {}) {
|
|
482
118
|
const memoryId = params.id;
|
|
483
119
|
if (!MEMORY_ID_PATTERN.test(memoryId)) {
|
|
484
120
|
const message = `Memory id must be 12 lowercase hex characters. Received: ${memoryId}`;
|
|
@@ -486,45 +122,49 @@ export function executeRecall(params: RecallArgs, getBranch: () => Entry[]) {
|
|
|
486
122
|
}
|
|
487
123
|
const result = recallMemorySources(getBranch(), memoryId);
|
|
488
124
|
if (result.status === "not_found") {
|
|
489
|
-
const message = `No observation or
|
|
125
|
+
const message = `No observation, summary, or review with id ${memoryId} was found on the current branch.`;
|
|
490
126
|
return textResult(message, emptyDetails("not_found", memoryId, message));
|
|
491
127
|
}
|
|
492
|
-
return
|
|
128
|
+
return renderFound(result);
|
|
493
129
|
}
|
|
494
130
|
|
|
495
|
-
export function createRecallAgentTool(getBranch: () => Entry[]): AgentTool<typeof RECALL_PARAMETERS> {
|
|
496
|
-
return {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
131
|
+
export function createRecallAgentTool(getBranch: () => Entry[], options: RecallAgentToolOptions = {}): AgentTool<typeof RECALL_PARAMETERS> {
|
|
132
|
+
return { name: RECALL_OBSERVATION_TOOL_NAME, label: "Recall memory", description: RECALL_DESCRIPTION, parameters: RECALL_PARAMETERS, execute: async (_id, params) => executeRecall(params, getBranch, options) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function formatRecallHeaderForTui(details: RecallObservationToolDetails): string {
|
|
136
|
+
if (details.status === "invalid_id" || details.status === "not_found") return `✗ recall ${details.memoryId}`;
|
|
137
|
+
const kind = details.summaries.length ? "summary" : details.reviews.length ? "review" : "observation";
|
|
138
|
+
return `${details.partial ? "⚠" : "✓"} ${kind} ${details.memoryId}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function formatRecallResultForTui(result: AgentToolResult<RecallObservationToolDetails>, _expanded: boolean): string {
|
|
142
|
+
const text = result.content.filter((part): part is { type: "text"; text: string } => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
|
|
143
|
+
return result.details ? `${formatRecallHeaderForTui(result.details)}\n${text}` : text;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function formatRecallCallForTui(id: string | undefined): string {
|
|
147
|
+
return `recall ${id ?? ""}`.trimEnd();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function formatRecallRenderedResultForTui(result: AgentToolResult<RecallObservationToolDetails>, expanded: boolean): string {
|
|
151
|
+
return formatRecallResultForTui(result, expanded);
|
|
503
152
|
}
|
|
504
153
|
|
|
505
154
|
export const recallObservationTool = defineTool({
|
|
506
155
|
name: RECALL_OBSERVATION_TOOL_NAME,
|
|
507
156
|
label: "Recall memory evidence",
|
|
508
157
|
description: RECALL_DESCRIPTION,
|
|
509
|
-
promptSnippet: "Use recall(<id>) to
|
|
158
|
+
promptSnippet: "Use recall(<id>) to inspect an exact observation, summary, or review and its immediate citation links.",
|
|
510
159
|
promptGuidelines: [
|
|
511
|
-
"Use recall
|
|
512
|
-
"
|
|
513
|
-
"
|
|
514
|
-
"Use recall when the user asks why you believe something, what supports a memory, or what was decided earlier.",
|
|
515
|
-
"Do not use recall as semantic search or transcript browsing; you must already have a specific 12-character memory id.",
|
|
516
|
-
"Do not recall every id preemptively. Recall only when exact source context will materially improve the next action.",
|
|
160
|
+
"Use recall when exact wording, rationale, paths, commands, errors, constraints, or provenance matter.",
|
|
161
|
+
"For summaries, follow source memory ids with additional recall calls rather than recursively expanding the whole graph.",
|
|
162
|
+
"Do not use recall as broad search; use search_memories first when you do not have an exact id.",
|
|
517
163
|
],
|
|
518
164
|
parameters: RECALL_PARAMETERS,
|
|
519
|
-
renderCall(args) {
|
|
520
|
-
|
|
521
|
-
},
|
|
522
|
-
renderResult(result, options) {
|
|
523
|
-
return new Text(formatRecallRenderedResultForTui(result as AgentToolResult<RecallObservationToolDetails>, options.expanded), 0, 0);
|
|
524
|
-
},
|
|
525
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
526
|
-
return executeRecall(params, () => ctx.sessionManager.getBranch() as Entry[]);
|
|
527
|
-
},
|
|
165
|
+
renderCall(args) { return new Text(formatRecallCallForTui(args.id), 0, 0); },
|
|
166
|
+
renderResult(result, options) { return new Text(formatRecallRenderedResultForTui(result as AgentToolResult<RecallObservationToolDetails>, options.expanded), 0, 0); },
|
|
167
|
+
async execute(_id, params, _signal, _onUpdate, ctx) { return executeRecall(params, () => ctx.sessionManager.getBranch() as Entry[]); },
|
|
528
168
|
});
|
|
529
169
|
|
|
530
170
|
export function registerRecallTool(pi: ExtensionAPI): void {
|