@matthewfl/pi-contemplator 0.0.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/package.json +60 -0
  4. package/src/agents/contemplator/agent.ts +718 -0
  5. package/src/agents/contemplator/prompts.ts +212 -0
  6. package/src/agents/dropper/agent.ts +291 -0
  7. package/src/agents/dropper/coverage.ts +128 -0
  8. package/src/agents/dropper/pool.ts +67 -0
  9. package/src/agents/dropper/prompts.ts +48 -0
  10. package/src/agents/observer/agent.ts +207 -0
  11. package/src/agents/observer/prompts.ts +119 -0
  12. package/src/agents/reflector/agent.ts +213 -0
  13. package/src/agents/reflector/prompts.ts +81 -0
  14. package/src/agents/reviewer/agent.ts +187 -0
  15. package/src/agents/reviewer/history-tools.ts +337 -0
  16. package/src/agents/reviewer/prompts.ts +135 -0
  17. package/src/agents/reviewer/tools.ts +84 -0
  18. package/src/agents/stream-errors.ts +22 -0
  19. package/src/clipboard.ts +63 -0
  20. package/src/commands/contemplator-view.ts +128 -0
  21. package/src/commands/reviewer-view.ts +89 -0
  22. package/src/commands/settings.ts +257 -0
  23. package/src/commands/status.ts +176 -0
  24. package/src/commands/view.ts +171 -0
  25. package/src/config.ts +284 -0
  26. package/src/debug-log.ts +72 -0
  27. package/src/hooks/compaction-hook.ts +99 -0
  28. package/src/hooks/compaction-resume.ts +124 -0
  29. package/src/hooks/compaction-trigger.ts +122 -0
  30. package/src/hooks/consolidation-trigger.ts +488 -0
  31. package/src/ids.ts +5 -0
  32. package/src/index.ts +32 -0
  33. package/src/model-budget.ts +16 -0
  34. package/src/runtime.ts +316 -0
  35. package/src/serialize.ts +274 -0
  36. package/src/session-ledger/fold.ts +115 -0
  37. package/src/session-ledger/index.ts +7 -0
  38. package/src/session-ledger/progress.ts +156 -0
  39. package/src/session-ledger/projection.ts +243 -0
  40. package/src/session-ledger/recall.ts +258 -0
  41. package/src/session-ledger/render-summary.ts +31 -0
  42. package/src/session-ledger/search.ts +184 -0
  43. package/src/session-ledger/types.ts +329 -0
  44. package/src/tokens.ts +27 -0
  45. package/src/tools/compact-context.ts +54 -0
  46. package/src/tools/recall-observation.ts +532 -0
  47. package/src/tools/search-memories.ts +131 -0
@@ -0,0 +1,258 @@
1
+ import {
2
+ isObservationsDroppedEntry,
3
+ isObservationsRecordedEntry,
4
+ isReflectionsRecordedEntry,
5
+ isReviewResultEntry,
6
+ type Entry,
7
+ type Observation,
8
+ type Reflection,
9
+ type ReviewResult,
10
+ } from "./types.js";
11
+
12
+ const SOURCE_TYPES = new Set(["message", "custom_message", "branch_summary"]);
13
+
14
+ export type { Entry, Observation, Reflection };
15
+
16
+ type ObservationLedgerLocation = {
17
+ entryId: string;
18
+ entryIndex: number;
19
+ recordIndex: number;
20
+ };
21
+
22
+ type ReflectionLedgerLocation = {
23
+ entryId: string;
24
+ entryIndex: number;
25
+ recordIndex: number;
26
+ };
27
+
28
+ export type RecalledObservation = {
29
+ observation: Observation;
30
+ observationEntryId: string;
31
+ observationRecordIndex: number;
32
+ status: "active" | "dropped";
33
+ sourceEntryIds: string[];
34
+ sourceEntries: Entry[];
35
+ missingSourceEntryIds: string[];
36
+ nonSourceEntryIds: string[];
37
+ };
38
+
39
+ export type RecalledReflection = {
40
+ reflection: Reflection;
41
+ reflectionEntryId: string;
42
+ reflectionRecordIndex: number;
43
+ };
44
+
45
+ export type RecalledReviewResult = {
46
+ review: ReviewResult;
47
+ reviewEntryId: string;
48
+ };
49
+
50
+ export type RecallResult =
51
+ | {
52
+ status: "not_found";
53
+ memoryId: string;
54
+ kind: undefined;
55
+ reflections: [];
56
+ reviews: [];
57
+ observations: [];
58
+ sourceEntries: [];
59
+ missingSourceEntryIds: [];
60
+ nonSourceEntryIds: [];
61
+ missingSupportingObservationIds: [];
62
+ collision: false;
63
+ partial: false;
64
+ }
65
+ | {
66
+ status: "found";
67
+ memoryId: string;
68
+ kind: "observation" | "reflection" | "review" | "mixed";
69
+ reflections: RecalledReflection[];
70
+ reviews: RecalledReviewResult[];
71
+ observations: RecalledObservation[];
72
+ sourceEntries: Entry[];
73
+ missingSourceEntryIds: string[];
74
+ nonSourceEntryIds: string[];
75
+ missingSupportingObservationIds: string[];
76
+ collision: boolean;
77
+ partial: boolean;
78
+ };
79
+
80
+ type IndexedObservation = ObservationLedgerLocation & { observation: Observation };
81
+ type IndexedReflection = ReflectionLedgerLocation & { reflection: Reflection };
82
+ type IndexedReviewResult = { review: ReviewResult; entryId: string };
83
+
84
+ function isSourceEntry(entry: Entry): boolean {
85
+ return SOURCE_TYPES.has(entry.type);
86
+ }
87
+
88
+ function uniqueById(entries: Entry[]): Entry[] {
89
+ const seen = new Set<string>();
90
+ const result: Entry[] = [];
91
+ for (const entry of entries) {
92
+ if (seen.has(entry.id)) continue;
93
+ seen.add(entry.id);
94
+ result.push(entry);
95
+ }
96
+ return result;
97
+ }
98
+
99
+ function uniqueStrings(values: string[]): string[] {
100
+ return Array.from(new Set(values));
101
+ }
102
+
103
+ function indexLedger(entries: Entry[]): {
104
+ observations: IndexedObservation[];
105
+ reflections: IndexedReflection[];
106
+ reviews: IndexedReviewResult[];
107
+ droppedIds: Set<string>;
108
+ } {
109
+ const observations: IndexedObservation[] = [];
110
+ const reflections: IndexedReflection[] = [];
111
+ const reviews: IndexedReviewResult[] = [];
112
+ const droppedIds = new Set<string>();
113
+
114
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
115
+ const entry = entries[entryIndex];
116
+ if (isObservationsRecordedEntry(entry)) {
117
+ entry.data.observations.forEach((observation, recordIndex) => {
118
+ observations.push({ observation, entryId: entry.id, entryIndex, recordIndex });
119
+ });
120
+ continue;
121
+ }
122
+ if (isReflectionsRecordedEntry(entry)) {
123
+ entry.data.reflections.forEach((reflection, recordIndex) => {
124
+ reflections.push({ reflection, entryId: entry.id, entryIndex, recordIndex });
125
+ });
126
+ continue;
127
+ }
128
+ if (isObservationsDroppedEntry(entry)) {
129
+ entry.data.observationIds.forEach((id) => droppedIds.add(id));
130
+ continue;
131
+ }
132
+ if (isReviewResultEntry(entry)) reviews.push({ review: entry.data.result, entryId: entry.id });
133
+ }
134
+
135
+ return { observations, reflections, reviews, droppedIds };
136
+ }
137
+
138
+ function resolveObservationSources(entries: Entry[], observation: Observation, location: ObservationLedgerLocation): RecalledObservation {
139
+ const sourceEntryIds = uniqueStrings(observation.sourceEntryIds);
140
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
141
+ const sourceEntries: Entry[] = [];
142
+ const missingSourceEntryIds: string[] = [];
143
+ const nonSourceEntryIds: string[] = [];
144
+
145
+ for (const sourceEntryId of sourceEntryIds) {
146
+ const sourceEntry = byId.get(sourceEntryId);
147
+ if (!sourceEntry) {
148
+ missingSourceEntryIds.push(sourceEntryId);
149
+ continue;
150
+ }
151
+ if (!isSourceEntry(sourceEntry)) {
152
+ nonSourceEntryIds.push(sourceEntryId);
153
+ continue;
154
+ }
155
+ sourceEntries.push(sourceEntry);
156
+ }
157
+
158
+ return {
159
+ observation,
160
+ observationEntryId: location.entryId,
161
+ observationRecordIndex: location.recordIndex,
162
+ status: "active",
163
+ sourceEntryIds,
164
+ sourceEntries,
165
+ missingSourceEntryIds,
166
+ nonSourceEntryIds,
167
+ };
168
+ }
169
+
170
+ function notFound(memoryId: string): RecallResult {
171
+ return {
172
+ status: "not_found",
173
+ memoryId,
174
+ kind: undefined,
175
+ reflections: [],
176
+ reviews: [],
177
+ observations: [],
178
+ sourceEntries: [],
179
+ missingSourceEntryIds: [],
180
+ nonSourceEntryIds: [],
181
+ missingSupportingObservationIds: [],
182
+ collision: false,
183
+ partial: false,
184
+ };
185
+ }
186
+
187
+ export function recallMemorySources(entries: Entry[], memoryId: string): RecallResult {
188
+ const { observations: indexedObservations, reflections: indexedReflections, reviews: indexedReviews, droppedIds } = indexLedger(entries);
189
+ const directObservationMatches = indexedObservations.filter(({ observation }) => observation.id === memoryId);
190
+ const reflectionMatches = indexedReflections.filter(({ reflection }) => reflection.id === memoryId);
191
+ const reviewMatches = indexedReviews.filter(({ review }) => review.id === memoryId);
192
+
193
+ if (directObservationMatches.length === 0 && reflectionMatches.length === 0 && reviewMatches.length === 0) return notFound(memoryId);
194
+
195
+ const observationsById = new Map<string, IndexedObservation>();
196
+ for (const indexed of indexedObservations) {
197
+ if (!observationsById.has(indexed.observation.id)) observationsById.set(indexed.observation.id, indexed);
198
+ }
199
+
200
+ const recalledByKey = new Map<string, RecalledObservation>();
201
+ const missingSupportingObservationIds: string[] = [];
202
+
203
+ function addObservation(indexed: IndexedObservation): void {
204
+ const key = `${indexed.entryId}:${indexed.recordIndex}`;
205
+ if (recalledByKey.has(key)) return;
206
+ const recalled = resolveObservationSources(entries, indexed.observation, indexed);
207
+ recalled.status = droppedIds.has(indexed.observation.id) ? "dropped" : "active";
208
+ recalledByKey.set(key, recalled);
209
+ }
210
+
211
+ for (const match of directObservationMatches) addObservation(match);
212
+
213
+ for (const { reflection } of reflectionMatches) {
214
+ for (const observationId of uniqueStrings(reflection.supportingObservationIds)) {
215
+ const indexed = observationsById.get(observationId);
216
+ if (!indexed) {
217
+ missingSupportingObservationIds.push(observationId);
218
+ continue;
219
+ }
220
+ addObservation(indexed);
221
+ }
222
+ }
223
+
224
+ const recalledObservations = Array.from(recalledByKey.values());
225
+ const recalledReflections: RecalledReflection[] = reflectionMatches.map(({ reflection, entryId, recordIndex }) => ({
226
+ reflection,
227
+ reflectionEntryId: entryId,
228
+ reflectionRecordIndex: recordIndex,
229
+ }));
230
+ const recalledReviews: RecalledReviewResult[] = reviewMatches.map(({ review, entryId }) => ({ review, reviewEntryId: entryId }));
231
+ const sourceEntries = uniqueById(recalledObservations.flatMap((match) => match.sourceEntries));
232
+ const missingSourceEntryIds = uniqueStrings(recalledObservations.flatMap((match) => match.missingSourceEntryIds));
233
+ const nonSourceEntryIds = uniqueStrings(recalledObservations.flatMap((match) => match.nonSourceEntryIds));
234
+ const uniqueMissingSupportingObservationIds = uniqueStrings(missingSupportingObservationIds);
235
+ const matchCount = directObservationMatches.length + reflectionMatches.length + reviewMatches.length;
236
+ const kinds = [directObservationMatches.length > 0, reflectionMatches.length > 0, reviewMatches.length > 0].filter(Boolean).length;
237
+
238
+ return {
239
+ status: "found",
240
+ memoryId,
241
+ kind: kinds > 1
242
+ ? "mixed"
243
+ : reviewMatches.length > 0
244
+ ? "review"
245
+ : reflectionMatches.length > 0
246
+ ? "reflection"
247
+ : "observation",
248
+ reflections: recalledReflections,
249
+ reviews: recalledReviews,
250
+ observations: recalledObservations,
251
+ sourceEntries,
252
+ missingSourceEntryIds,
253
+ nonSourceEntryIds,
254
+ missingSupportingObservationIds: uniqueMissingSupportingObservationIds,
255
+ collision: matchCount > 1,
256
+ partial: missingSourceEntryIds.length > 0 || nonSourceEntryIds.length > 0 || uniqueMissingSupportingObservationIds.length > 0,
257
+ };
258
+ }
@@ -0,0 +1,31 @@
1
+ import type { Observation, Reflection } from "./types.js";
2
+
3
+ const CONTEXT_USAGE_INSTRUCTIONS = `These are condensed memories from earlier in this session.
4
+
5
+ - Reflections: stable, long-lived facts about the user, project, decisions, and constraints. New reflection lines may include ids in brackets.
6
+ - Observations: timestamped events from the conversation history, in chronological order. Observation lines include ids in brackets.
7
+
8
+ Treat these as past records. When entries conflict, the most recent observation reflects the latest known state. Work that prior observations describe as completed should not be redone unless the user explicitly asks to revisit it.
9
+
10
+ When exact source context is needed for precision or traceability, use the recall tool with the relevant observation or reflection id. This is especially useful when a reflection materially affects a decision or is too compressed to continue confidently. Do not use recall as broad search or inject raw source unless it is needed.`;
11
+
12
+ export function observationToSummaryLine(observation: Observation): string {
13
+ return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
14
+ }
15
+
16
+ export function reflectionToSummaryLine(reflection: Reflection): string {
17
+ return `[${reflection.id}] ${reflection.content}`;
18
+ }
19
+
20
+ export function renderSummary(reflections: Reflection[], observations: Observation[]): string {
21
+ if (reflections.length === 0 && observations.length === 0) return "";
22
+
23
+ const parts: string[] = [CONTEXT_USAGE_INSTRUCTIONS];
24
+ if (reflections.length > 0) {
25
+ parts.push(`## Reflections\n${reflections.map(reflectionToSummaryLine).join("\n")}`);
26
+ }
27
+ if (observations.length > 0) {
28
+ parts.push(`## Observations\n${observations.map(observationToSummaryLine).join("\n")}`);
29
+ }
30
+ return parts.join("\n\n");
31
+ }
@@ -0,0 +1,184 @@
1
+ import {
2
+ isObservationsDroppedEntry,
3
+ isObservationsRecordedEntry,
4
+ isReflectionsRecordedEntry,
5
+ isReviewResultEntry,
6
+ type Entry,
7
+ type Relevance,
8
+ type ReviewOutcome,
9
+ type ReviewScope,
10
+ } from "./types.js";
11
+
12
+ export type MemorySearchResult = {
13
+ kind: "observation" | "reflection" | "review";
14
+ id: string;
15
+ content: string;
16
+ relevance?: Relevance;
17
+ timestamp?: string;
18
+ status?: "active" | "dropped";
19
+ scope?: ReviewScope;
20
+ outcome?: ReviewOutcome;
21
+ title?: string;
22
+ score: number;
23
+ };
24
+
25
+ export type MemorySearch = {
26
+ query: string;
27
+ results: MemorySearchResult[];
28
+ observationsSearched: number;
29
+ reflectionsSearched: number;
30
+ reviewsSearched: number;
31
+ };
32
+
33
+ type SearchCandidate = Omit<MemorySearchResult, "score"> & {
34
+ sourceIndex: number;
35
+ };
36
+
37
+ const RELEVANCE_BOOST: Record<Relevance, number> = {
38
+ low: 1,
39
+ medium: 2,
40
+ high: 3,
41
+ critical: 4,
42
+ };
43
+
44
+ function normalizeSearchText(value: string): string {
45
+ return value.normalize("NFKC").toLocaleLowerCase();
46
+ }
47
+
48
+ function terms(value: string): string[] {
49
+ return Array.from(
50
+ new Set(normalizeSearchText(value).match(/[\p{L}\p{N}][\p{L}\p{N}\p{M}_./:-]*/gu) ?? []),
51
+ );
52
+ }
53
+
54
+ function relevanceScore(
55
+ content: string,
56
+ query: string,
57
+ queryTerms: string[],
58
+ ): number {
59
+ const normalizedContent = normalizeSearchText(content);
60
+ const phrase = normalizeSearchText(query.trim());
61
+ const termMatches = queryTerms.reduce(
62
+ (total, term) => total + (normalizedContent.includes(term) ? 1 : 0),
63
+ 0,
64
+ );
65
+ const phraseBoost =
66
+ phrase.length > 0 && normalizedContent.includes(phrase) ? 5 : 0;
67
+ if (termMatches === 0 && phraseBoost === 0) return 0;
68
+ return termMatches * 10 + phraseBoost;
69
+ }
70
+
71
+ function candidates(entries: Entry[]): {
72
+ items: SearchCandidate[];
73
+ observations: number;
74
+ reflections: number;
75
+ reviews: number;
76
+ } {
77
+ const dropped = new Set<string>();
78
+ for (const entry of entries) {
79
+ if (isObservationsDroppedEntry(entry)) {
80
+ for (const id of entry.data.observationIds) dropped.add(id);
81
+ }
82
+ }
83
+
84
+ const items: SearchCandidate[] = [];
85
+ let observations = 0;
86
+ let reflections = 0;
87
+ let reviews = 0;
88
+ for (let sourceIndex = 0; sourceIndex < entries.length; sourceIndex++) {
89
+ const entry = entries[sourceIndex];
90
+ if (isObservationsRecordedEntry(entry)) {
91
+ observations += entry.data.observations.length;
92
+ for (const observation of entry.data.observations) {
93
+ items.push({
94
+ kind: "observation",
95
+ id: observation.id,
96
+ content: observation.content,
97
+ relevance: observation.relevance,
98
+ timestamp: observation.timestamp,
99
+ status: dropped.has(observation.id) ? "dropped" : "active",
100
+ sourceIndex,
101
+ });
102
+ }
103
+ }
104
+ if (isReflectionsRecordedEntry(entry)) {
105
+ reflections += entry.data.reflections.length;
106
+ for (const reflection of entry.data.reflections) {
107
+ items.push({
108
+ kind: "reflection",
109
+ id: reflection.id,
110
+ content: reflection.content,
111
+ sourceIndex,
112
+ });
113
+ }
114
+ continue;
115
+ }
116
+ if (isReviewResultEntry(entry)) {
117
+ const review = entry.data.result;
118
+ reviews++;
119
+ const content = review.outcome === "proposal"
120
+ ? [review.scope, "proposal", review.title, review.summary, review.evidence, review.conceptualDesign].join("\n")
121
+ : [review.scope, "review concluded with no proposal", review.reason, review.evidenceReviewed, review.reconsiderIf ?? ""].join("\n");
122
+ items.push({
123
+ kind: "review",
124
+ id: review.id,
125
+ content,
126
+ scope: review.scope,
127
+ outcome: review.outcome,
128
+ title: review.outcome === "proposal" ? review.title : undefined,
129
+ sourceIndex,
130
+ });
131
+ }
132
+ }
133
+ return { items, observations, reflections, reviews };
134
+ }
135
+
136
+ export function searchMemories(
137
+ entries: Entry[],
138
+ query: string,
139
+ limit = 8,
140
+ ): MemorySearch {
141
+ const normalizedQuery = query.trim();
142
+ const queryTerms = terms(normalizedQuery);
143
+ const searched = candidates(entries);
144
+ const results = searched.items
145
+ .map((candidate): MemorySearchResult | undefined => {
146
+ const lexical = relevanceScore(
147
+ candidate.content,
148
+ normalizedQuery,
149
+ queryTerms,
150
+ );
151
+ if (lexical === 0) return undefined;
152
+ const relevanceBoost = candidate.relevance
153
+ ? RELEVANCE_BOOST[candidate.relevance]
154
+ : 0;
155
+ const kindBoost = candidate.kind === "reflection" ? 3 : 0;
156
+ const recencyBoost = Math.min(
157
+ candidate.sourceIndex / Math.max(entries.length, 1),
158
+ 1,
159
+ );
160
+ return {
161
+ kind: candidate.kind,
162
+ id: candidate.id,
163
+ content: candidate.content,
164
+ relevance: candidate.relevance,
165
+ timestamp: candidate.timestamp,
166
+ status: candidate.status,
167
+ scope: candidate.scope,
168
+ outcome: candidate.outcome,
169
+ title: candidate.title,
170
+ score: lexical + relevanceBoost + kindBoost + recencyBoost,
171
+ } satisfies MemorySearchResult;
172
+ })
173
+ .filter((result): result is MemorySearchResult => result !== undefined)
174
+ .sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
175
+ .slice(0, Math.max(1, Math.min(20, Math.floor(limit))));
176
+
177
+ return {
178
+ query: normalizedQuery,
179
+ results,
180
+ observationsSearched: searched.observations,
181
+ reflectionsSearched: searched.reflections,
182
+ reviewsSearched: searched.reviews,
183
+ };
184
+ }