@matthewfl/pi-contemplator 0.0.10 → 0.1.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 +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 +96 -39
- package/src/agents/observer/prompts.ts +19 -10
- 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 +95 -70
- 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 +30 -37
- 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 +245 -215
- 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 +8 -19
- 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 +103 -77
- 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,31 +1,32 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { chronologicalMemories } from "./pools.js";
|
|
2
|
+
import type { Observation, Summary } from "./types.js";
|
|
2
3
|
|
|
3
|
-
const CONTEXT_USAGE_INSTRUCTIONS = `These are
|
|
4
|
+
const CONTEXT_USAGE_INSTRUCTIONS = `These are memories from earlier in this session, shown together in chronological order.
|
|
4
5
|
|
|
5
|
-
-
|
|
6
|
-
- Observations
|
|
6
|
+
- Summaries are cited, compressed memories. Their lines include their own ids in leading brackets and source citations inside the text.
|
|
7
|
+
- Observations are timestamped records from conversation history. Their lines include ids in leading brackets.
|
|
7
8
|
|
|
8
|
-
Treat these as past records. When entries conflict, the most recent
|
|
9
|
+
Treat these as past records. When entries conflict, the most recent memory reflects the latest known state. Work that a memory describes as completed should not be redone unless the user explicitly asks to revisit it.
|
|
9
10
|
|
|
10
|
-
When exact source context is needed for precision or traceability, use the recall tool with the relevant observation or
|
|
11
|
+
When exact source context is needed for precision or traceability, use the recall tool with the relevant observation or summary id. A summary's inline citations can be followed with recall. Do not use recall as broad search or inject raw source unless it is needed.`;
|
|
11
12
|
|
|
12
13
|
export function observationToSummaryLine(observation: Observation): string {
|
|
13
14
|
return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function
|
|
17
|
-
return `[${
|
|
17
|
+
export function summaryToSummaryLine(summary: Summary): string {
|
|
18
|
+
return `[${summary.id}] ${summary.timestamp} [summary] ${summary.content}`;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export function renderSummary(
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
export function renderSummary(summaries: Summary[], observations: Observation[]): string {
|
|
22
|
+
if (summaries.length === 0 && observations.length === 0) return "";
|
|
23
|
+
|
|
24
|
+
const memories = chronologicalMemories(observations, summaries).map((item) =>
|
|
25
|
+
item.kind === "observation" ? observationToSummaryLine(item.memory) : summaryToSummaryLine(item.memory),
|
|
26
|
+
);
|
|
27
|
+
return [
|
|
28
|
+
CONTEXT_USAGE_INSTRUCTIONS,
|
|
29
|
+
`## Memories (chronological)\n${memories.join("\n")}`,
|
|
30
|
+
"Remember: you can look up the details of a memory by using the recall tool with a memory id contained in square brackets.",
|
|
31
|
+
].join("\n\n");
|
|
31
32
|
}
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
isMemoryDetails,
|
|
3
3
|
isObservationsRecordedEntry,
|
|
4
|
-
isReflectionsRecordedEntry,
|
|
5
4
|
isReviewResultEntry,
|
|
5
|
+
isSummarizerCommitEntry,
|
|
6
|
+
observationRetention,
|
|
6
7
|
type Entry,
|
|
8
|
+
type MemoryVisibility,
|
|
7
9
|
type Relevance,
|
|
10
|
+
type Retention,
|
|
8
11
|
type ReviewOutcome,
|
|
9
12
|
type ReviewScope,
|
|
10
13
|
} from "./types.js";
|
|
14
|
+
import { foldLedger } from "./fold.js";
|
|
11
15
|
|
|
12
16
|
export type MemorySearchResult = {
|
|
13
|
-
kind: "observation" | "
|
|
17
|
+
kind: "observation" | "summary" | "review";
|
|
14
18
|
id: string;
|
|
15
19
|
content: string;
|
|
16
20
|
relevance?: Relevance;
|
|
21
|
+
retention?: Retention;
|
|
17
22
|
timestamp?: string;
|
|
18
|
-
|
|
23
|
+
visibility?: MemoryVisibility;
|
|
24
|
+
consumedBySummaryId?: string;
|
|
25
|
+
citedBySummaryIds?: string[];
|
|
26
|
+
sourceMemoryIds?: string[];
|
|
27
|
+
consumedMemoryIds?: string[];
|
|
19
28
|
scope?: ReviewScope;
|
|
20
29
|
outcome?: ReviewOutcome;
|
|
21
30
|
title?: string;
|
|
@@ -26,149 +35,124 @@ export type MemorySearch = {
|
|
|
26
35
|
query: string;
|
|
27
36
|
results: MemorySearchResult[];
|
|
28
37
|
observationsSearched: number;
|
|
29
|
-
|
|
38
|
+
summariesSearched: number;
|
|
30
39
|
reviewsSearched: number;
|
|
31
40
|
};
|
|
32
41
|
|
|
33
|
-
type
|
|
34
|
-
sourceIndex: number;
|
|
35
|
-
};
|
|
42
|
+
export type SearchMemoriesOptions = Record<string, never>;
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
high: 3,
|
|
41
|
-
critical: 4,
|
|
42
|
-
};
|
|
44
|
+
type SearchCandidate = Omit<MemorySearchResult, "score"> & { sourceIndex: number };
|
|
45
|
+
|
|
46
|
+
const RELEVANCE_BOOST: Record<Relevance, number> = { low: 1, medium: 2, high: 3, critical: 4 };
|
|
43
47
|
|
|
44
48
|
function normalizeSearchText(value: string): string {
|
|
45
49
|
return value.normalize("NFKC").toLocaleLowerCase();
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
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
|
-
);
|
|
53
|
+
return Array.from(new Set(normalizeSearchText(value).match(/[\p{L}\p{N}][\p{L}\p{N}\p{M}_./:-]*/gu) ?? []));
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
function relevanceScore(
|
|
55
|
-
content: string,
|
|
56
|
-
query: string,
|
|
57
|
-
queryTerms: string[],
|
|
58
|
-
): number {
|
|
56
|
+
function relevanceScore(content: string, query: string, queryTerms: string[]): number {
|
|
59
57
|
const normalizedContent = normalizeSearchText(content);
|
|
60
58
|
const phrase = normalizeSearchText(query.trim());
|
|
61
|
-
const termMatches = queryTerms.reduce(
|
|
62
|
-
|
|
63
|
-
0,
|
|
64
|
-
);
|
|
65
|
-
const phraseBoost =
|
|
66
|
-
phrase.length > 0 && normalizedContent.includes(phrase) ? 5 : 0;
|
|
59
|
+
const termMatches = queryTerms.reduce((total, term) => total + (normalizedContent.includes(term) ? 1 : 0), 0);
|
|
60
|
+
const phraseBoost = phrase.length > 0 && normalizedContent.includes(phrase) ? 5 : 0;
|
|
67
61
|
if (termMatches === 0 && phraseBoost === 0) return 0;
|
|
68
62
|
return termMatches * 10 + phraseBoost;
|
|
69
63
|
}
|
|
70
64
|
|
|
65
|
+
function reviewSearchContent(review: ReturnType<typeof foldLedger>["reviews"][number]): string {
|
|
66
|
+
return review.outcome === "proposal"
|
|
67
|
+
? [review.scope, "proposal", review.title, review.summary, review.evidence, review.conceptualDesign].join("\n")
|
|
68
|
+
: [review.scope, "review concluded with no proposal", review.reason, review.evidenceReviewed, review.reconsiderIf ?? ""].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sourceIndexes(entries: Entry[]): Map<string, number> {
|
|
72
|
+
const indexes = new Map<string, number>();
|
|
73
|
+
const add = (id: string, index: number): void => { if (!indexes.has(id)) indexes.set(id, index); };
|
|
74
|
+
for (let index = 0; index < entries.length; index++) {
|
|
75
|
+
const entry = entries[index];
|
|
76
|
+
if (entry.type === "compaction" && isMemoryDetails(entry.details)) {
|
|
77
|
+
for (const memory of entry.details.archive?.observations ?? entry.details.observations) add(memory.id, index);
|
|
78
|
+
for (const memory of entry.details.archive?.summaries ?? entry.details.summaries) add(memory.id, index);
|
|
79
|
+
for (const review of entry.details.reviews ?? []) add(review.id, index);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (isObservationsRecordedEntry(entry)) for (const memory of entry.data.observations) add(memory.id, index);
|
|
83
|
+
else if (isSummarizerCommitEntry(entry)) for (const memory of entry.data.summaries) add(memory.id, index);
|
|
84
|
+
else if (isReviewResultEntry(entry)) add(entry.data.result.id, index);
|
|
85
|
+
}
|
|
86
|
+
return indexes;
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
function candidates(entries: Entry[]): {
|
|
72
90
|
items: SearchCandidate[];
|
|
73
91
|
observations: number;
|
|
74
|
-
|
|
92
|
+
summaries: number;
|
|
75
93
|
reviews: number;
|
|
76
94
|
} {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
if (isObservationsDroppedEntry(entry)) {
|
|
80
|
-
for (const id of entry.data.observationIds) dropped.add(id);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
95
|
+
const folded = foldLedger(entries);
|
|
96
|
+
const indexes = sourceIndexes(entries);
|
|
84
97
|
const items: SearchCandidate[] = [];
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
scope: review.scope,
|
|
127
|
-
outcome: review.outcome,
|
|
128
|
-
title: review.outcome === "proposal" ? review.title : undefined,
|
|
129
|
-
sourceIndex,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
98
|
+
|
|
99
|
+
for (const observation of folded.observations) {
|
|
100
|
+
const consumedBySummaryId = folded.consumedBySummaryId.get(observation.id);
|
|
101
|
+
items.push({
|
|
102
|
+
kind: "observation",
|
|
103
|
+
id: observation.id,
|
|
104
|
+
content: observation.content,
|
|
105
|
+
relevance: observation.relevance,
|
|
106
|
+
retention: observationRetention(observation),
|
|
107
|
+
timestamp: observation.timestamp,
|
|
108
|
+
visibility: consumedBySummaryId ? "summarized" : "visible",
|
|
109
|
+
...(consumedBySummaryId ? { consumedBySummaryId } : {}),
|
|
110
|
+
citedBySummaryIds: folded.citedBySummaryIds.get(observation.id) ?? [],
|
|
111
|
+
sourceIndex: indexes.get(observation.id) ?? 0,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
for (const summary of folded.summaries) {
|
|
115
|
+
const consumedBySummaryId = folded.consumedBySummaryId.get(summary.id);
|
|
116
|
+
items.push({
|
|
117
|
+
kind: "summary",
|
|
118
|
+
id: summary.id,
|
|
119
|
+
content: summary.content,
|
|
120
|
+
visibility: consumedBySummaryId ? "summarized" : "visible",
|
|
121
|
+
...(consumedBySummaryId ? { consumedBySummaryId } : {}),
|
|
122
|
+
citedBySummaryIds: folded.citedBySummaryIds.get(summary.id) ?? [],
|
|
123
|
+
sourceMemoryIds: summary.sourceMemoryIds,
|
|
124
|
+
consumedMemoryIds: summary.consumedMemoryIds,
|
|
125
|
+
sourceIndex: indexes.get(summary.id) ?? 0,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
for (const review of folded.reviews) {
|
|
129
|
+
items.push({
|
|
130
|
+
kind: "review",
|
|
131
|
+
id: review.id,
|
|
132
|
+
content: reviewSearchContent(review),
|
|
133
|
+
citedBySummaryIds: folded.citedBySummaryIds.get(review.id) ?? [],
|
|
134
|
+
scope: review.scope,
|
|
135
|
+
outcome: review.outcome,
|
|
136
|
+
title: review.outcome === "proposal" ? review.title : undefined,
|
|
137
|
+
sourceIndex: indexes.get(review.id) ?? 0,
|
|
138
|
+
});
|
|
132
139
|
}
|
|
133
|
-
return { items, observations,
|
|
140
|
+
return { items, observations: folded.observations.length, summaries: folded.summaries.length, reviews: folded.reviews.length };
|
|
134
141
|
}
|
|
135
142
|
|
|
136
|
-
export function searchMemories(
|
|
137
|
-
entries: Entry[],
|
|
138
|
-
query: string,
|
|
139
|
-
limit = 8,
|
|
140
|
-
): MemorySearch {
|
|
143
|
+
export function searchMemories(entries: Entry[], query: string, limit = 8, _options: SearchMemoriesOptions = {}): MemorySearch {
|
|
141
144
|
const normalizedQuery = query.trim();
|
|
142
145
|
const queryTerms = terms(normalizedQuery);
|
|
143
146
|
const searched = candidates(entries);
|
|
144
147
|
const results = searched.items
|
|
145
148
|
.map((candidate): MemorySearchResult | undefined => {
|
|
146
|
-
const lexical = relevanceScore(
|
|
147
|
-
candidate.content,
|
|
148
|
-
normalizedQuery,
|
|
149
|
-
queryTerms,
|
|
150
|
-
);
|
|
149
|
+
const lexical = relevanceScore(candidate.content, normalizedQuery, queryTerms);
|
|
151
150
|
if (lexical === 0) return undefined;
|
|
152
|
-
const relevanceBoost = candidate.relevance
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
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;
|
|
151
|
+
const relevanceBoost = candidate.relevance ? RELEVANCE_BOOST[candidate.relevance] : 0;
|
|
152
|
+
const kindBoost = candidate.kind === "summary" ? 3 : 0;
|
|
153
|
+
const recencyBoost = Math.min(candidate.sourceIndex / Math.max(entries.length, 1), 1);
|
|
154
|
+
const { sourceIndex: _sourceIndex, ...result } = candidate;
|
|
155
|
+
return { ...result, score: lexical + relevanceBoost + kindBoost + recencyBoost };
|
|
172
156
|
})
|
|
173
157
|
.filter((result): result is MemorySearchResult => result !== undefined)
|
|
174
158
|
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
|
|
@@ -178,7 +162,7 @@ export function searchMemories(
|
|
|
178
162
|
query: normalizedQuery,
|
|
179
163
|
results,
|
|
180
164
|
observationsSearched: searched.observations,
|
|
181
|
-
|
|
165
|
+
summariesSearched: searched.summaries,
|
|
182
166
|
reviewsSearched: searched.reviews,
|
|
183
167
|
};
|
|
184
168
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const OM_OBSERVATIONS_RECORDED = "om.observations.recorded";
|
|
2
|
-
|
|
3
|
-
export const
|
|
2
|
+
/** Atomic output of one summarizer pass: new summaries and their consumption edges. */
|
|
3
|
+
export const OM_SUMMARIZER_COMMIT = "om.summarizer.commit";
|
|
4
4
|
export const OM_REVIEW_REQUEST = "om.review.request";
|
|
5
5
|
export const OM_REVIEW_RESULT = "om.review.result";
|
|
6
6
|
/** Persisted assistant/tool output from a short-lived structural reviewer. */
|
|
@@ -16,6 +16,10 @@ export const OM_FOLDED = "om.folded";
|
|
|
16
16
|
export const RELEVANCE_VALUES = ["low", "medium", "high", "critical"] as const;
|
|
17
17
|
export type Relevance = (typeof RELEVANCE_VALUES)[number];
|
|
18
18
|
|
|
19
|
+
export const RETENTION_VALUES = ["ephemeral", "contextual", "durable"] as const;
|
|
20
|
+
export type Retention = (typeof RETENTION_VALUES)[number];
|
|
21
|
+
export type MemoryVisibility = "visible" | "summarized";
|
|
22
|
+
|
|
19
23
|
export const MEMORY_ID_PATTERN = /^[a-f0-9]{12}$/;
|
|
20
24
|
|
|
21
25
|
export type Entry = {
|
|
@@ -37,29 +41,44 @@ export type Observation = {
|
|
|
37
41
|
content: string;
|
|
38
42
|
timestamp: string;
|
|
39
43
|
relevance: Relevance;
|
|
44
|
+
/** Missing values safely default to contextual. */
|
|
45
|
+
retention?: Retention;
|
|
40
46
|
sourceEntryIds: string[];
|
|
41
47
|
tokenCount: number;
|
|
42
48
|
};
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
/** A durable, cited summary. Its source bodies remain elsewhere in the ledger. */
|
|
51
|
+
export type Summary = {
|
|
45
52
|
id: string;
|
|
46
53
|
content: string;
|
|
47
|
-
|
|
54
|
+
/** Newest effective timestamp among the cited source memories. */
|
|
55
|
+
timestamp: string;
|
|
56
|
+
/** Every inline-cited memory id, deduplicated in first-occurrence order. */
|
|
57
|
+
sourceMemoryIds: string[];
|
|
58
|
+
/** Sources newly removed from automatic visibility by this summary. */
|
|
59
|
+
consumedMemoryIds: string[];
|
|
48
60
|
tokenCount: number;
|
|
49
61
|
};
|
|
50
62
|
|
|
51
|
-
export type
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
export type SummarizerCommitMetrics = {
|
|
64
|
+
consumedMemoryCount: number;
|
|
65
|
+
sourceTokens: number;
|
|
66
|
+
summaryTokens: number;
|
|
67
|
+
estimatedTokenReduction: number;
|
|
54
68
|
};
|
|
55
69
|
|
|
56
|
-
export type
|
|
57
|
-
|
|
70
|
+
export type SummarizerCommitEntryData = {
|
|
71
|
+
version: 1;
|
|
72
|
+
summaries: Summary[];
|
|
73
|
+
/** Entry id of the newest observation batch included in the run snapshot. */
|
|
58
74
|
coversUpToId: string;
|
|
75
|
+
createdAt: number;
|
|
76
|
+
completedWithDone: boolean;
|
|
77
|
+
metrics: SummarizerCommitMetrics;
|
|
59
78
|
};
|
|
60
79
|
|
|
61
|
-
export type
|
|
62
|
-
|
|
80
|
+
export type ObservationsRecordedEntryData = {
|
|
81
|
+
observations: Observation[];
|
|
63
82
|
coversUpToId: string;
|
|
64
83
|
};
|
|
65
84
|
|
|
@@ -127,29 +146,44 @@ export type ReviewResult = WorkflowReviewProposal | SoftwareReviewProposal | Rev
|
|
|
127
146
|
export type ReviewRequestEntryData = { request: StructuralReviewRequest };
|
|
128
147
|
export type ReviewResultEntryData = { result: ReviewResult };
|
|
129
148
|
|
|
149
|
+
/** Full durable memory state at a compaction boundary. Bodies occur once in this archive. */
|
|
150
|
+
export type MemoryArchive = {
|
|
151
|
+
observations: Observation[];
|
|
152
|
+
summaries: Summary[];
|
|
153
|
+
};
|
|
154
|
+
|
|
130
155
|
export type MemoryDetails = {
|
|
131
156
|
type: typeof OM_FOLDED;
|
|
132
|
-
|
|
157
|
+
/** Version 2 intentionally breaks the unpublished reflections/lifecycle format. */
|
|
158
|
+
version: 2;
|
|
133
159
|
fullFold: boolean;
|
|
160
|
+
/** Visible memories injected by this compaction. */
|
|
134
161
|
observations: Observation[];
|
|
135
|
-
|
|
136
|
-
/**
|
|
162
|
+
summaries: Summary[];
|
|
163
|
+
/** Complete durable graph nodes at this compaction boundary. */
|
|
164
|
+
archive?: MemoryArchive;
|
|
165
|
+
/** Reviews remain recallable/searchable but are never automatically injected. */
|
|
137
166
|
reviews?: ReviewResult[];
|
|
138
167
|
};
|
|
139
168
|
|
|
140
|
-
export type
|
|
169
|
+
export type MemoryCoverageCustomType =
|
|
141
170
|
| typeof OM_OBSERVATIONS_RECORDED
|
|
142
|
-
| typeof
|
|
143
|
-
| typeof OM_OBSERVATIONS_DROPPED
|
|
144
|
-
| typeof OM_REVIEW_REQUEST
|
|
145
|
-
| typeof OM_REVIEW_RESULT;
|
|
171
|
+
| typeof OM_SUMMARIZER_COMMIT;
|
|
146
172
|
|
|
147
173
|
export function isRelevance(value: unknown): value is Relevance {
|
|
148
174
|
return typeof value === "string" && (RELEVANCE_VALUES as readonly string[]).includes(value);
|
|
149
175
|
}
|
|
150
176
|
|
|
177
|
+
export function isRetention(value: unknown): value is Retention {
|
|
178
|
+
return typeof value === "string" && (RETENTION_VALUES as readonly string[]).includes(value);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function observationRetention(observation: Observation): Retention {
|
|
182
|
+
return observation.retention ?? "contextual";
|
|
183
|
+
}
|
|
184
|
+
|
|
151
185
|
export function isNonEmptyString(value: unknown): value is string {
|
|
152
|
-
return typeof value === "string" && value.length > 0;
|
|
186
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
153
187
|
}
|
|
154
188
|
|
|
155
189
|
export function isNonEmptyStringArray(value: unknown): value is string[] {
|
|
@@ -164,8 +198,16 @@ function isTokenCount(value: unknown): value is number {
|
|
|
164
198
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
165
199
|
}
|
|
166
200
|
|
|
201
|
+
function isCount(value: unknown): value is number {
|
|
202
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
167
205
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
168
|
-
return !!value && typeof value === "object";
|
|
206
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasUniqueMemoryIds(value: unknown, minimum = 0): value is string[] {
|
|
210
|
+
return Array.isArray(value) && value.length >= minimum && value.every(isMemoryId) && new Set(value).size === value.length;
|
|
169
211
|
}
|
|
170
212
|
|
|
171
213
|
export function isObservation(value: unknown): value is Observation {
|
|
@@ -175,45 +217,41 @@ export function isObservation(value: unknown): value is Observation {
|
|
|
175
217
|
isNonEmptyString(value.content) &&
|
|
176
218
|
isNonEmptyString(value.timestamp) &&
|
|
177
219
|
isRelevance(value.relevance) &&
|
|
220
|
+
(value.retention === undefined || isRetention(value.retention)) &&
|
|
178
221
|
isNonEmptyStringArray(value.sourceEntryIds) &&
|
|
179
222
|
isTokenCount(value.tokenCount)
|
|
180
223
|
);
|
|
181
224
|
}
|
|
182
225
|
|
|
183
|
-
export function
|
|
226
|
+
export function isSummary(value: unknown): value is Summary {
|
|
184
227
|
if (!isPlainRecord(value)) return false;
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
isNonEmptyStringArray(value.supportingObservationIds) &&
|
|
190
|
-
isTokenCount(value.tokenCount)
|
|
191
|
-
);
|
|
228
|
+
if (!isMemoryId(value.id) || !isNonEmptyString(value.content) || !isNonEmptyString(value.timestamp) || !isTokenCount(value.tokenCount)) return false;
|
|
229
|
+
if (!hasUniqueMemoryIds(value.sourceMemoryIds, 2) || !hasUniqueMemoryIds(value.consumedMemoryIds, 2)) return false;
|
|
230
|
+
const sourceIds = new Set(value.sourceMemoryIds);
|
|
231
|
+
return value.consumedMemoryIds.every((id) => sourceIds.has(id));
|
|
192
232
|
}
|
|
193
233
|
|
|
194
234
|
export function isObservationsRecordedData(value: unknown): value is ObservationsRecordedEntryData {
|
|
195
235
|
if (!isPlainRecord(value)) return false;
|
|
196
236
|
return (
|
|
197
237
|
Array.isArray(value.observations) &&
|
|
198
|
-
value.observations.length > 0 &&
|
|
199
238
|
value.observations.every(isObservation) &&
|
|
200
239
|
isNonEmptyString(value.coversUpToId)
|
|
201
240
|
);
|
|
202
241
|
}
|
|
203
242
|
|
|
204
|
-
|
|
243
|
+
function isSummarizerCommitMetrics(value: unknown): value is SummarizerCommitMetrics {
|
|
205
244
|
if (!isPlainRecord(value)) return false;
|
|
206
|
-
return (
|
|
207
|
-
|
|
208
|
-
value.reflections.length > 0 &&
|
|
209
|
-
value.reflections.every(isReflection) &&
|
|
210
|
-
isNonEmptyString(value.coversUpToId)
|
|
211
|
-
);
|
|
245
|
+
return isCount(value.consumedMemoryCount) && isTokenCount(value.sourceTokens) &&
|
|
246
|
+
isTokenCount(value.summaryTokens) && isTokenCount(value.estimatedTokenReduction);
|
|
212
247
|
}
|
|
213
248
|
|
|
214
|
-
export function
|
|
249
|
+
export function isSummarizerCommitData(value: unknown): value is SummarizerCommitEntryData {
|
|
215
250
|
if (!isPlainRecord(value)) return false;
|
|
216
|
-
return
|
|
251
|
+
return value.version === 1 && Array.isArray(value.summaries) && value.summaries.length > 0 &&
|
|
252
|
+
value.summaries.every(isSummary) && isNonEmptyString(value.coversUpToId) &&
|
|
253
|
+
typeof value.createdAt === "number" && Number.isFinite(value.createdAt) &&
|
|
254
|
+
typeof value.completedWithDone === "boolean" && isSummarizerCommitMetrics(value.metrics);
|
|
217
255
|
}
|
|
218
256
|
|
|
219
257
|
function isReviewScope(value: unknown): value is ReviewScope {
|
|
@@ -252,18 +290,22 @@ export function isReviewResult(value: unknown): value is ReviewResult {
|
|
|
252
290
|
isNonEmptyString(value.preservedBehavior) && isNonEmptyString(value.expectedEffect) && isNonEmptyString(value.uncertainties);
|
|
253
291
|
}
|
|
254
292
|
|
|
293
|
+
function isMemoryArchive(value: unknown): value is MemoryArchive {
|
|
294
|
+
if (!isPlainRecord(value)) return false;
|
|
295
|
+
return Array.isArray(value.observations) && value.observations.every(isObservation) &&
|
|
296
|
+
Array.isArray(value.summaries) && value.summaries.every(isSummary);
|
|
297
|
+
}
|
|
298
|
+
|
|
255
299
|
export function isMemoryDetails(value: unknown): value is MemoryDetails {
|
|
256
300
|
if (!isPlainRecord(value)) return false;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
Array.isArray(value.observations) &&
|
|
262
|
-
value.
|
|
263
|
-
|
|
264
|
-
value.
|
|
265
|
-
(value.reviews === undefined || (Array.isArray(value.reviews) && value.reviews.every(isReviewResult)))
|
|
266
|
-
);
|
|
301
|
+
// No v1 migration is intentional: that format belonged to the unpublished
|
|
302
|
+
// librarian/reflection design and is not semantically compatible with the
|
|
303
|
+
// summarizer consumption graph.
|
|
304
|
+
return value.type === OM_FOLDED && value.version === 2 && typeof value.fullFold === "boolean" &&
|
|
305
|
+
Array.isArray(value.observations) && value.observations.every(isObservation) &&
|
|
306
|
+
Array.isArray(value.summaries) && value.summaries.every(isSummary) &&
|
|
307
|
+
(value.archive === undefined || isMemoryArchive(value.archive)) &&
|
|
308
|
+
(value.reviews === undefined || (Array.isArray(value.reviews) && value.reviews.every(isReviewResult)));
|
|
267
309
|
}
|
|
268
310
|
|
|
269
311
|
export function isObservationsRecordedEntry(entry: Entry): entry is Entry & {
|
|
@@ -274,20 +316,12 @@ export function isObservationsRecordedEntry(entry: Entry): entry is Entry & {
|
|
|
274
316
|
return entry.type === "custom" && entry.customType === OM_OBSERVATIONS_RECORDED && isObservationsRecordedData(entry.data);
|
|
275
317
|
}
|
|
276
318
|
|
|
277
|
-
export function
|
|
319
|
+
export function isSummarizerCommitEntry(entry: Entry): entry is Entry & {
|
|
278
320
|
type: "custom";
|
|
279
|
-
customType: typeof
|
|
280
|
-
data:
|
|
321
|
+
customType: typeof OM_SUMMARIZER_COMMIT;
|
|
322
|
+
data: SummarizerCommitEntryData;
|
|
281
323
|
} {
|
|
282
|
-
return entry.type === "custom" && entry.customType ===
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export function isObservationsDroppedEntry(entry: Entry): entry is Entry & {
|
|
286
|
-
type: "custom";
|
|
287
|
-
customType: typeof OM_OBSERVATIONS_DROPPED;
|
|
288
|
-
data: ObservationsDroppedEntryData;
|
|
289
|
-
} {
|
|
290
|
-
return entry.type === "custom" && entry.customType === OM_OBSERVATIONS_DROPPED && isObservationsDroppedData(entry.data);
|
|
324
|
+
return entry.type === "custom" && entry.customType === OM_SUMMARIZER_COMMIT && isSummarizerCommitData(entry.data);
|
|
291
325
|
}
|
|
292
326
|
|
|
293
327
|
export function isReviewRequestEntry(entry: Entry): entry is Entry & {
|
|
@@ -310,22 +344,14 @@ export function buildObservationsRecordedData(
|
|
|
310
344
|
observations: Observation[],
|
|
311
345
|
coversUpToId: string,
|
|
312
346
|
): ObservationsRecordedEntryData | undefined {
|
|
313
|
-
if (
|
|
314
|
-
|
|
347
|
+
if (!isNonEmptyString(coversUpToId)) return undefined;
|
|
348
|
+
const candidate = { observations, coversUpToId };
|
|
349
|
+
return isObservationsRecordedData(candidate) ? candidate : undefined;
|
|
315
350
|
}
|
|
316
351
|
|
|
317
|
-
export function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return { reflections, coversUpToId };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
export function buildObservationsDroppedData(
|
|
326
|
-
observationIds: string[],
|
|
327
|
-
coversUpToId: string,
|
|
328
|
-
): ObservationsDroppedEntryData | undefined {
|
|
329
|
-
if (observationIds.length === 0 || !isNonEmptyString(coversUpToId)) return undefined;
|
|
330
|
-
return { observationIds, coversUpToId };
|
|
352
|
+
export function buildSummarizerCommitData(
|
|
353
|
+
data: Omit<SummarizerCommitEntryData, "version">,
|
|
354
|
+
): SummarizerCommitEntryData | undefined {
|
|
355
|
+
const candidate: SummarizerCommitEntryData = { version: 1, ...data };
|
|
356
|
+
return isSummarizerCommitData(candidate) ? candidate : undefined;
|
|
331
357
|
}
|
|
@@ -20,7 +20,7 @@ export function createCompactContextTool(runtime: Runtime) {
|
|
|
20
20
|
promptGuidelines: [
|
|
21
21
|
"Use compact_context sparingly when either substantial additional work remains and there is not enough context left to complete it, or accumulated past context has become noisy, stale, or distracting enough that you are struggling to focus on the current task or reason about it reliably.",
|
|
22
22
|
"Do not use compact_context routinely, for short tasks, or merely because the conversation is long; use it for genuine context-capacity pressure or context degradation that is interfering with the work.",
|
|
23
|
-
"Call compact_context by itself, provide short_continuation_prompt with concrete instructions for the next agent step, and stop the current turn;
|
|
23
|
+
"Call compact_context by itself, provide short_continuation_prompt with concrete instructions for the next agent step, and stop the current turn; pi-contemplator will compact the context and automatically resume with those instructions.",
|
|
24
24
|
],
|
|
25
25
|
parameters: Type.Object({
|
|
26
26
|
short_continuation_prompt: Type.String({
|