@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,329 @@
1
+ export const OM_OBSERVATIONS_RECORDED = "om.observations.recorded";
2
+ export const OM_REFLECTIONS_RECORDED = "om.reflections.recorded";
3
+ export const OM_OBSERVATIONS_DROPPED = "om.observations.dropped";
4
+ export const OM_REVIEW_REQUEST = "om.review.request";
5
+ export const OM_REVIEW_RESULT = "om.review.result";
6
+ /** Persisted assistant/tool output from a short-lived structural reviewer. */
7
+ export const OM_REVIEWER_MESSAGE = "om.reviewer.message";
8
+ /** Snapshot of a reviewer transcript retained across primary-session compaction. */
9
+ export const OM_REVIEWER_STATE = "om.reviewer.state";
10
+ /** Compact proposal notice queued for the primary agent. */
11
+ export const OM_REVIEWER_NOTICE = "om.reviewer.notice";
12
+ export const OM_FOLDED = "om.folded";
13
+
14
+ export const RELEVANCE_VALUES = ["low", "medium", "high", "critical"] as const;
15
+ export type Relevance = (typeof RELEVANCE_VALUES)[number];
16
+
17
+ export const MEMORY_ID_PATTERN = /^[a-f0-9]{12}$/;
18
+
19
+ export type Entry = {
20
+ type: string;
21
+ id: string;
22
+ timestamp?: string;
23
+ message?: unknown;
24
+ content?: unknown;
25
+ customType?: string;
26
+ summary?: unknown;
27
+ fromId?: string;
28
+ data?: unknown;
29
+ details?: unknown;
30
+ firstKeptEntryId?: string;
31
+ };
32
+
33
+ export type Observation = {
34
+ id: string;
35
+ content: string;
36
+ timestamp: string;
37
+ relevance: Relevance;
38
+ sourceEntryIds: string[];
39
+ tokenCount: number;
40
+ };
41
+
42
+ export type Reflection = {
43
+ id: string;
44
+ content: string;
45
+ supportingObservationIds: string[];
46
+ tokenCount: number;
47
+ };
48
+
49
+ export type ObservationsRecordedEntryData = {
50
+ observations: Observation[];
51
+ coversUpToId: string;
52
+ };
53
+
54
+ export type ReflectionsRecordedEntryData = {
55
+ reflections: Reflection[];
56
+ coversUpToId: string;
57
+ };
58
+
59
+ export type ObservationsDroppedEntryData = {
60
+ observationIds: string[];
61
+ coversUpToId: string;
62
+ };
63
+
64
+ export type ReviewScope = "workflow" | "software";
65
+ export type ReviewOutcome = "proposal" | "no_proposal";
66
+
67
+ export type StructuralReviewRequest = {
68
+ id: string;
69
+ scope: ReviewScope;
70
+ evidence: string;
71
+ concern: string;
72
+ reviewFocus: string;
73
+ constraints?: string;
74
+ createdAt: number;
75
+ requestedBy: "contemplator";
76
+ };
77
+
78
+ type ReviewResultBase = {
79
+ id: string;
80
+ version: 1;
81
+ reviewRequestId: string;
82
+ scope: ReviewScope;
83
+ outcome: ReviewOutcome;
84
+ createdAt: number;
85
+ requestedBy: "contemplator";
86
+ };
87
+
88
+ export type WorkflowReviewProposal = ReviewResultBase & {
89
+ outcome: "proposal";
90
+ proposalKind: "workflow";
91
+ title: string;
92
+ summary: string;
93
+ evidence: string;
94
+ inefficiency: string;
95
+ conceptualDesign: string;
96
+ inputs?: string;
97
+ outputs?: string;
98
+ integration?: string;
99
+ expectedEffect: string;
100
+ uncertainties: string;
101
+ };
102
+
103
+ export type SoftwareReviewProposal = ReviewResultBase & {
104
+ outcome: "proposal";
105
+ proposalKind: "software";
106
+ title: string;
107
+ summary: string;
108
+ evidence: string;
109
+ structuralIssue: string;
110
+ conceptualDesign: string;
111
+ preservedBehavior: string;
112
+ expectedEffect: string;
113
+ uncertainties: string;
114
+ };
115
+
116
+ export type ReviewNoProposal = ReviewResultBase & {
117
+ outcome: "no_proposal";
118
+ reason: string;
119
+ evidenceReviewed: string;
120
+ reconsiderIf?: string;
121
+ };
122
+
123
+ export type ReviewResult = WorkflowReviewProposal | SoftwareReviewProposal | ReviewNoProposal;
124
+
125
+ export type ReviewRequestEntryData = { request: StructuralReviewRequest };
126
+ export type ReviewResultEntryData = { result: ReviewResult };
127
+
128
+ export type MemoryDetails = {
129
+ type: typeof OM_FOLDED;
130
+ version: 1;
131
+ fullFold: boolean;
132
+ observations: Observation[];
133
+ reflections: Reflection[];
134
+ /** Optional so compactions written before review results remain readable. */
135
+ reviews?: ReviewResult[];
136
+ };
137
+
138
+ export type V3MemoryCustomType =
139
+ | typeof OM_OBSERVATIONS_RECORDED
140
+ | typeof OM_REFLECTIONS_RECORDED
141
+ | typeof OM_OBSERVATIONS_DROPPED
142
+ | typeof OM_REVIEW_REQUEST
143
+ | typeof OM_REVIEW_RESULT;
144
+
145
+ export function isRelevance(value: unknown): value is Relevance {
146
+ return typeof value === "string" && (RELEVANCE_VALUES as readonly string[]).includes(value);
147
+ }
148
+
149
+ export function isNonEmptyString(value: unknown): value is string {
150
+ return typeof value === "string" && value.length > 0;
151
+ }
152
+
153
+ export function isNonEmptyStringArray(value: unknown): value is string[] {
154
+ return Array.isArray(value) && value.length > 0 && value.every(isNonEmptyString);
155
+ }
156
+
157
+ export function isMemoryId(value: unknown): value is string {
158
+ return typeof value === "string" && MEMORY_ID_PATTERN.test(value);
159
+ }
160
+
161
+ function isTokenCount(value: unknown): value is number {
162
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
163
+ }
164
+
165
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
166
+ return !!value && typeof value === "object";
167
+ }
168
+
169
+ export function isObservation(value: unknown): value is Observation {
170
+ if (!isPlainRecord(value)) return false;
171
+ return (
172
+ isMemoryId(value.id) &&
173
+ isNonEmptyString(value.content) &&
174
+ isNonEmptyString(value.timestamp) &&
175
+ isRelevance(value.relevance) &&
176
+ isNonEmptyStringArray(value.sourceEntryIds) &&
177
+ isTokenCount(value.tokenCount)
178
+ );
179
+ }
180
+
181
+ export function isReflection(value: unknown): value is Reflection {
182
+ if (!isPlainRecord(value)) return false;
183
+ return (
184
+ isMemoryId(value.id) &&
185
+ isNonEmptyString(value.content) &&
186
+ !/\r|\n/.test(value.content) &&
187
+ isNonEmptyStringArray(value.supportingObservationIds) &&
188
+ isTokenCount(value.tokenCount)
189
+ );
190
+ }
191
+
192
+ export function isObservationsRecordedData(value: unknown): value is ObservationsRecordedEntryData {
193
+ if (!isPlainRecord(value)) return false;
194
+ return (
195
+ Array.isArray(value.observations) &&
196
+ value.observations.length > 0 &&
197
+ value.observations.every(isObservation) &&
198
+ isNonEmptyString(value.coversUpToId)
199
+ );
200
+ }
201
+
202
+ export function isReflectionsRecordedData(value: unknown): value is ReflectionsRecordedEntryData {
203
+ if (!isPlainRecord(value)) return false;
204
+ return (
205
+ Array.isArray(value.reflections) &&
206
+ value.reflections.length > 0 &&
207
+ value.reflections.every(isReflection) &&
208
+ isNonEmptyString(value.coversUpToId)
209
+ );
210
+ }
211
+
212
+ export function isObservationsDroppedData(value: unknown): value is ObservationsDroppedEntryData {
213
+ if (!isPlainRecord(value)) return false;
214
+ return isNonEmptyStringArray(value.observationIds) && isNonEmptyString(value.coversUpToId);
215
+ }
216
+
217
+ function isReviewScope(value: unknown): value is ReviewScope {
218
+ return value === "workflow" || value === "software";
219
+ }
220
+
221
+ function isOptionalString(value: unknown): boolean {
222
+ return value === undefined || isNonEmptyString(value);
223
+ }
224
+
225
+ export function isStructuralReviewRequest(value: unknown): value is StructuralReviewRequest {
226
+ if (!isPlainRecord(value)) return false;
227
+ return isNonEmptyString(value.id) && isReviewScope(value.scope) &&
228
+ isNonEmptyString(value.evidence) && isNonEmptyString(value.concern) &&
229
+ isNonEmptyString(value.reviewFocus) && isOptionalString(value.constraints) &&
230
+ typeof value.createdAt === "number" && Number.isFinite(value.createdAt) && value.requestedBy === "contemplator";
231
+ }
232
+
233
+ export function isReviewResult(value: unknown): value is ReviewResult {
234
+ if (!isPlainRecord(value)) return false;
235
+ const base = isMemoryId(value.id) && value.version === 1 && isNonEmptyString(value.reviewRequestId) &&
236
+ isReviewScope(value.scope) && (value.outcome === "proposal" || value.outcome === "no_proposal") &&
237
+ typeof value.createdAt === "number" && Number.isFinite(value.createdAt) && value.requestedBy === "contemplator";
238
+ if (!base) return false;
239
+ if (value.outcome === "no_proposal") {
240
+ return isNonEmptyString(value.reason) && isNonEmptyString(value.evidenceReviewed) && isOptionalString(value.reconsiderIf);
241
+ }
242
+ if (value.scope === "workflow") {
243
+ return value.proposalKind === "workflow" && isNonEmptyString(value.title) && isNonEmptyString(value.summary) &&
244
+ isNonEmptyString(value.evidence) && isNonEmptyString(value.inefficiency) && isNonEmptyString(value.conceptualDesign) &&
245
+ isOptionalString(value.inputs) && isOptionalString(value.outputs) && isOptionalString(value.integration) &&
246
+ isNonEmptyString(value.expectedEffect) && isNonEmptyString(value.uncertainties);
247
+ }
248
+ return value.proposalKind === "software" && isNonEmptyString(value.title) && isNonEmptyString(value.summary) &&
249
+ isNonEmptyString(value.evidence) && isNonEmptyString(value.structuralIssue) && isNonEmptyString(value.conceptualDesign) &&
250
+ isNonEmptyString(value.preservedBehavior) && isNonEmptyString(value.expectedEffect) && isNonEmptyString(value.uncertainties);
251
+ }
252
+
253
+ export function isMemoryDetails(value: unknown): value is MemoryDetails {
254
+ if (!isPlainRecord(value)) return false;
255
+ return (
256
+ value.type === OM_FOLDED &&
257
+ value.version === 1 &&
258
+ typeof value.fullFold === "boolean" &&
259
+ Array.isArray(value.observations) &&
260
+ value.observations.every(isObservation) &&
261
+ Array.isArray(value.reflections) &&
262
+ value.reflections.every(isReflection) &&
263
+ (value.reviews === undefined || (Array.isArray(value.reviews) && value.reviews.every(isReviewResult)))
264
+ );
265
+ }
266
+
267
+ export function isObservationsRecordedEntry(entry: Entry): entry is Entry & {
268
+ type: "custom";
269
+ customType: typeof OM_OBSERVATIONS_RECORDED;
270
+ data: ObservationsRecordedEntryData;
271
+ } {
272
+ return entry.type === "custom" && entry.customType === OM_OBSERVATIONS_RECORDED && isObservationsRecordedData(entry.data);
273
+ }
274
+
275
+ export function isReflectionsRecordedEntry(entry: Entry): entry is Entry & {
276
+ type: "custom";
277
+ customType: typeof OM_REFLECTIONS_RECORDED;
278
+ data: ReflectionsRecordedEntryData;
279
+ } {
280
+ return entry.type === "custom" && entry.customType === OM_REFLECTIONS_RECORDED && isReflectionsRecordedData(entry.data);
281
+ }
282
+
283
+ export function isObservationsDroppedEntry(entry: Entry): entry is Entry & {
284
+ type: "custom";
285
+ customType: typeof OM_OBSERVATIONS_DROPPED;
286
+ data: ObservationsDroppedEntryData;
287
+ } {
288
+ return entry.type === "custom" && entry.customType === OM_OBSERVATIONS_DROPPED && isObservationsDroppedData(entry.data);
289
+ }
290
+
291
+ export function isReviewRequestEntry(entry: Entry): entry is Entry & {
292
+ type: "custom";
293
+ customType: typeof OM_REVIEW_REQUEST;
294
+ data: ReviewRequestEntryData;
295
+ } {
296
+ return entry.type === "custom" && entry.customType === OM_REVIEW_REQUEST && isPlainRecord(entry.data) && isStructuralReviewRequest(entry.data.request);
297
+ }
298
+
299
+ export function isReviewResultEntry(entry: Entry): entry is Entry & {
300
+ type: "custom";
301
+ customType: typeof OM_REVIEW_RESULT;
302
+ data: ReviewResultEntryData;
303
+ } {
304
+ return entry.type === "custom" && entry.customType === OM_REVIEW_RESULT && isPlainRecord(entry.data) && isReviewResult(entry.data.result);
305
+ }
306
+
307
+ export function buildObservationsRecordedData(
308
+ observations: Observation[],
309
+ coversUpToId: string,
310
+ ): ObservationsRecordedEntryData | undefined {
311
+ if (observations.length === 0 || !isNonEmptyString(coversUpToId)) return undefined;
312
+ return { observations, coversUpToId };
313
+ }
314
+
315
+ export function buildReflectionsRecordedData(
316
+ reflections: Reflection[],
317
+ coversUpToId: string,
318
+ ): ReflectionsRecordedEntryData | undefined {
319
+ if (reflections.length === 0 || !isNonEmptyString(coversUpToId)) return undefined;
320
+ return { reflections, coversUpToId };
321
+ }
322
+
323
+ export function buildObservationsDroppedData(
324
+ observationIds: string[],
325
+ coversUpToId: string,
326
+ ): ObservationsDroppedEntryData | undefined {
327
+ if (observationIds.length === 0 || !isNonEmptyString(coversUpToId)) return undefined;
328
+ return { observationIds, coversUpToId };
329
+ }
package/src/tokens.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { estimateTokens as estimateMessageTokens } from "@earendil-works/pi-coding-agent";
2
+
3
+ export function estimateStringTokens(text: string): number {
4
+ return Math.ceil(text.length / 4);
5
+ }
6
+
7
+ export function estimateEntryTokens(entry: { type: string; message?: unknown; content?: unknown; summary?: unknown }): number {
8
+ if (entry.type === "message" && entry.message) {
9
+ return estimateMessageTokens(entry.message as Parameters<typeof estimateMessageTokens>[0]);
10
+ }
11
+ if (entry.type === "custom_message" && entry.content) {
12
+ const content = entry.content;
13
+ if (typeof content === "string") return estimateStringTokens(content);
14
+ if (Array.isArray(content)) {
15
+ let total = 0;
16
+ for (const block of content) {
17
+ if (block.type === "text" && block.text) total += estimateStringTokens(block.text);
18
+ }
19
+ return total;
20
+ }
21
+ }
22
+ if (entry.type === "branch_summary" && typeof entry.summary === "string") {
23
+ return estimateStringTokens(entry.summary);
24
+ }
25
+ return 0;
26
+ }
27
+
@@ -0,0 +1,54 @@
1
+ import { Type } from "@earendil-works/pi-ai";
2
+ import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import type { Runtime } from "../runtime.js";
4
+
5
+ export const COMPACT_CONTEXT_TOOL_NAME = "compact_context";
6
+ export const COMPACT_CONTEXT_DESCRIPTION =
7
+ "Force manual compaction. Use sparingly when substantial work remains but the remaining context is insufficient, or when accumulated context has become noisy or stale enough to impair focus and reliable reasoning.";
8
+
9
+ export type CompactContextDetails = {
10
+ status: "scheduled" | "already_pending" | "in_progress";
11
+ };
12
+
13
+ export function createCompactContextTool(runtime: Runtime) {
14
+ return defineTool({
15
+ name: COMPACT_CONTEXT_TOOL_NAME,
16
+ label: "Compact context",
17
+ description: COMPACT_CONTEXT_DESCRIPTION,
18
+ promptSnippet:
19
+ "Force manual context compaction when substantial work remains but context is running out, or when context degradation is impairing focus",
20
+ promptGuidelines: [
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
+ "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 and stop the current turn; observational memory will compact the context and automatically resume the task.",
24
+ ],
25
+ parameters: Type.Object({}),
26
+ async execute() {
27
+ if (runtime.compactInFlight) {
28
+ return {
29
+ content: [{ type: "text" as const, text: "Context compaction is already in progress. Stop this turn and wait for automatic resume." }],
30
+ details: { status: "in_progress" } as CompactContextDetails,
31
+ terminate: true,
32
+ };
33
+ }
34
+ if (runtime.compactRequested) {
35
+ return {
36
+ content: [{ type: "text" as const, text: "Context compaction is already scheduled. Stop this turn and wait for automatic resume." }],
37
+ details: { status: "already_pending" } as CompactContextDetails,
38
+ terminate: true,
39
+ };
40
+ }
41
+
42
+ runtime.compactRequested = true;
43
+ return {
44
+ content: [{ type: "text" as const, text: "Context compaction scheduled. Stop this turn; the task will resume automatically after compaction." }],
45
+ details: { status: "scheduled" } as CompactContextDetails,
46
+ terminate: true,
47
+ };
48
+ },
49
+ });
50
+ }
51
+
52
+ export function registerCompactContextTool(pi: ExtensionAPI, runtime: Runtime): void {
53
+ pi.registerTool(createCompactContextTool(runtime));
54
+ }