@matthewfl/pi-contemplator 0.0.9 → 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.
Files changed (44) hide show
  1. package/README.md +17 -11
  2. package/package.json +8 -6
  3. package/src/agents/contemplator/agent.ts +325 -91
  4. package/src/agents/contemplator/prompts.ts +6 -6
  5. package/src/agents/observer/agent.ts +14 -6
  6. package/src/agents/observer/prompts.ts +16 -7
  7. package/src/agents/reviewer/agent.ts +24 -4
  8. package/src/agents/reviewer/prompts.ts +1 -1
  9. package/src/agents/reviewer/tools.ts +24 -9
  10. package/src/agents/stream-errors.ts +1 -1
  11. package/src/agents/summarizer/agent.ts +597 -0
  12. package/src/agents/summarizer/prompts.ts +46 -0
  13. package/src/agents/summarizer/sampling.ts +80 -0
  14. package/src/commands/contemplator-view.ts +22 -1
  15. package/src/commands/settings.ts +73 -69
  16. package/src/commands/status.ts +60 -36
  17. package/src/commands/summarizer-view.ts +58 -0
  18. package/src/commands/view.ts +22 -10
  19. package/src/config.ts +25 -32
  20. package/src/hooks/compaction-hook.ts +36 -19
  21. package/src/hooks/compaction-resume.ts +4 -4
  22. package/src/hooks/compaction-trigger.ts +96 -56
  23. package/src/hooks/consolidation-trigger.ts +213 -196
  24. package/src/memory-citations.ts +37 -0
  25. package/src/required-tool-choice.ts +28 -0
  26. package/src/runtime.ts +116 -33
  27. package/src/session-ledger/fold.ts +82 -53
  28. package/src/session-ledger/index.ts +1 -0
  29. package/src/session-ledger/pools.ts +77 -0
  30. package/src/session-ledger/progress.ts +7 -18
  31. package/src/session-ledger/projection.ts +45 -177
  32. package/src/session-ledger/recall.ts +129 -127
  33. package/src/session-ledger/render-summary.ts +20 -19
  34. package/src/session-ledger/search.ts +99 -115
  35. package/src/session-ledger/types.ts +102 -75
  36. package/src/tools/compact-context.ts +1 -1
  37. package/src/tools/recall-observation.ts +99 -459
  38. package/src/tools/search-memories.ts +31 -72
  39. package/src/agents/dropper/agent.ts +0 -291
  40. package/src/agents/dropper/coverage.ts +0 -128
  41. package/src/agents/dropper/pool.ts +0 -67
  42. package/src/agents/dropper/prompts.ts +0 -48
  43. package/src/agents/reflector/agent.ts +0 -213
  44. package/src/agents/reflector/prompts.ts +0 -81
@@ -1,48 +0,0 @@
1
- export const DROPPER_SYSTEM = `You are the dropper agent for a coding assistant.
2
-
3
- These records are the ONLY information the assistant will have about past interactions once the raw conversation is compacted out of context. Dropping the wrong observation can make future work repeat, contradict, or misremember the user. Take this seriously.
4
-
5
- Your job is to identify only the safest active observations to remove from compacted memory by calling drop_observations with their ids. Default action is KEEP. When uncertain, keep the observation.
6
-
7
- Active-memory framing. Dropping an observation removes it from active compacted memory; it does not erase the ledger history or source evidence. Still, future compressed context will no longer show the observation, so only drop it when its durable meaning is safely captured elsewhere or it is genuinely low-signal and carries no unique future value.
8
-
9
- The user message includes the active observation pool target and "Maximum drops allowed this run". The maximum is a hard upper bound sized to move the pool toward the target if every proposed drop is clearly safe. It is not a target. Do not try to fill it. Drop fewer or none when fewer observations are safely removable. When the active pool is far over target, make a thorough pass over safe candidates rather than stopping after a few obvious examples.
10
-
11
- What to drop, in priority order:
12
- - Redundant observations whose durable meaning is already captured by current reflections with equivalent fidelity.
13
- - Superseded observations where a later observation clearly replaces the older state.
14
- - Repeated routine tool acknowledgements or low-signal progress updates that do not carry decisions, constraints, exact errors, or user-specific facts.
15
- - Older observations that no longer carry working context and are covered by a reflection or a newer observation.
16
-
17
- Age-gradient rule. Recent observations carry working context the assistant may still need; older observations have usually been summarized elsewhere or are no longer load-bearing. Prefer older safe drops before newer working context, but age alone is not enough to drop important or uniquely load-bearing observations.
18
-
19
- Reflection coverage guidance. Each observation line includes [coverage: none|partial|strong]. Coverage is evidence, not an automatic decision:
20
- - none: no current reflection cites this observation id. Be cautious, especially for high or critical observations.
21
- - partial: one current reflection cites this observation id. Compare the observation to the reflection before dropping.
22
- - strong: two or more current reflections cite this observation id. This is stronger evidence that the durable meaning is preserved, but you must still keep uniquely load-bearing or uncertain observations.
23
-
24
- Relevance guidance. Relevance is importance/resistance, not an absolute keep/drop lock:
25
- - low: consider first, but drop only when it carries no unique detail, decision, state, error, identifier, or user-specific fact.
26
- - medium: drop when redundant with reflections or other observations, or when the work state is clearly obsolete.
27
- - high: drop only when clearly superseded or already captured by a reflection with equivalent fidelity.
28
- - critical: highest importance and strongest resistance. Do not drop fresh or uniquely load-bearing critical observations. Critical observations may be dropped only with strong semantic evidence such as age plus partial/strong reflection coverage, supersession by newer memory, redundancy, or clear obsolescence.
29
-
30
- User assertions and concrete completions must be preserved unless a current reflection or newer observation preserves the exact assertion/completion and its important details with equivalent fidelity.
31
-
32
- Preservation floor. Regardless of relevance label, budget pressure, coverage, or age, do not drop observations that uniquely carry any of the following:
33
- - User preferences, constraints, corrections, or identity/role facts.
34
- - Concrete completions that future runs must not redo.
35
- - Named identifiers, file paths, function names, package names, tickets, commit SHAs, handles, or exact commands.
36
- - Exact error messages, diagnostic output, or test failure names.
37
- - Architectural or technical decisions and their rationale.
38
- - Dates of specific events, deadlines, meetings, migrations, or incidents.
39
- - Current unresolved blockers, TODOs, partial work, or decisions waiting on the user.
40
- - Non-standard user terminology or unusual phrasing needed for future recognition.
41
-
42
- What you cannot do:
43
- - You cannot merge observations.
44
- - You cannot rewrite or edit observations.
45
- - You cannot add new observations or reflections.
46
- - You can only call drop_observations with ids from the current observations list.
47
-
48
- Do not force drops you do not believe in. If no observations are safe to drop, do not call the tool and reply briefly. Hitting the budget or maximum count is less important than preserving load-bearing memory.`;
@@ -1,213 +0,0 @@
1
- import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentTool } from "@earendil-works/pi-agent-core";
2
- import type { Message, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
3
- import { Type } from "@earendil-works/pi-ai";
4
- import { streamSimple } from "@earendil-works/pi-ai/compat";
5
- import type { Static } from "typebox";
6
- import { debugLog } from "../../debug-log.js";
7
- import { hashId } from "../../ids.js";
8
- import { logAgentStreamError } from "../stream-errors.js";
9
- import { AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "../../model-budget.js";
10
- import { truncateRecordContent } from "../../serialize.js";
11
- import { REFLECTOR_SYSTEM } from "./prompts.js";
12
- import { estimateStringTokens } from "../../tokens.js";
13
- import { reflectionToSummaryLine, type Observation, type Reflection } from "../../session-ledger/index.js";
14
- import type { LlmUsageInput } from "../../runtime.js";
15
- import {
16
- coverageTierForObservation,
17
- reflectionCoverageMap,
18
- summarizeCoverageByRelevance,
19
- summarizeCoverageTransitionsByRelevance,
20
- type ReflectionCoverageTier,
21
- } from "../dropper/coverage.js";
22
-
23
- interface RunReflectorArgs {
24
- model: Model<any>;
25
- apiKey: string;
26
- headers?: Record<string, string>;
27
- reflections: Reflection[];
28
- observations: Observation[];
29
- signal?: AbortSignal;
30
- agentLoop?: typeof agentLoop;
31
- maxTurns?: number;
32
- thinkingLevel?: ModelThinkingLevel;
33
- recordUsage?: (usage: LlmUsageInput) => void;
34
- }
35
-
36
- const RecordReflectionsSchema = Type.Object({
37
- reflections: Type.Array(
38
- Type.Object({
39
- content: Type.String({ minLength: 1 }),
40
- supportingObservationIds: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
41
- }),
42
- { minItems: 1 },
43
- ),
44
- });
45
-
46
- type RecordReflectionsArgs = Static<typeof RecordReflectionsSchema>;
47
-
48
- function joinOrEmpty(items: string[]): string {
49
- return items.length ? items.join("\n") : "(none yet)";
50
- }
51
-
52
- export function observationToReflectorLine(
53
- observation: Observation,
54
- coverage: ReflectionCoverageTier,
55
- ): string {
56
- return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] [coverage: ${coverage}] ${observation.content}`;
57
- }
58
-
59
- export function summarizeSupportIdCounts(reflections: readonly Reflection[]): {
60
- reflectionCount: number;
61
- totalSupportIds: number;
62
- minSupportIds: number;
63
- maxSupportIds: number;
64
- averageSupportIds: number;
65
- histogram: Record<string, number>;
66
- } {
67
- if (reflections.length === 0) {
68
- return { reflectionCount: 0, totalSupportIds: 0, minSupportIds: 0, maxSupportIds: 0, averageSupportIds: 0, histogram: {} };
69
- }
70
- const counts = reflections.map((reflection) => reflection.supportingObservationIds.length);
71
- const totalSupportIds = counts.reduce((sum, count) => sum + count, 0);
72
- const histogram: Record<string, number> = {};
73
- for (const count of counts) histogram[String(count)] = (histogram[String(count)] ?? 0) + 1;
74
- return {
75
- reflectionCount: reflections.length,
76
- totalSupportIds,
77
- minSupportIds: Math.min(...counts),
78
- maxSupportIds: Math.max(...counts),
79
- averageSupportIds: totalSupportIds / reflections.length,
80
- histogram,
81
- };
82
- }
83
-
84
- export function normalizeSupportingObservationIds(
85
- supportingObservationIds: readonly string[] | undefined,
86
- allowedObservationIds: readonly string[],
87
- ): string[] | undefined {
88
- if (!supportingObservationIds || supportingObservationIds.length === 0) return undefined;
89
- const allowedOrder = new Map<string, number>();
90
- for (let i = 0; i < allowedObservationIds.length; i++) {
91
- if (!allowedOrder.has(allowedObservationIds[i])) allowedOrder.set(allowedObservationIds[i], i);
92
- }
93
-
94
- const seen = new Set<string>();
95
- for (const id of supportingObservationIds) {
96
- if (!allowedOrder.has(id)) return undefined;
97
- seen.add(id);
98
- }
99
- if (seen.size === 0) return undefined;
100
- return Array.from(seen).sort((a, b) => (allowedOrder.get(a) ?? 0) - (allowedOrder.get(b) ?? 0));
101
- }
102
-
103
- function normalizeReflectionContent(content: string): string | undefined {
104
- const normalized = truncateRecordContent(content.trim());
105
- if (!normalized || /\r|\n/.test(normalized)) return undefined;
106
- return normalized;
107
- }
108
-
109
- export async function runReflector(args: RunReflectorArgs): Promise<Reflection[] | undefined> {
110
- const { model, apiKey, headers, reflections, observations, signal } = args;
111
- if (observations.length === 0) return undefined;
112
-
113
- const coverageById = reflectionCoverageMap(observations, reflections);
114
- debugLog("reflector.agent_start", {
115
- activeObservationCount: observations.length,
116
- reflectionCount: reflections.length,
117
- coverageSummaryByRelevance: summarizeCoverageByRelevance(observations, coverageById),
118
- });
119
-
120
- const allowedObservationIds = observations.map((observation) => observation.id);
121
- const existingReflectionIds = new Set(reflections.map((reflection) => reflection.id));
122
- const accumulated = new Map<string, Reflection>();
123
- let toolCallCount = 0;
124
- let rawProposedReflectionCount = 0;
125
- let acceptedReflectionCount = 0;
126
- let duplicateReflectionCount = 0;
127
- let rejectedReflectionCount = 0;
128
-
129
- const recordReflections: AgentTool<typeof RecordReflectionsSchema> = {
130
- name: "record_reflections",
131
- label: "Record reflections",
132
- description: "Record new durable reflections with supporting observation ids.",
133
- parameters: RecordReflectionsSchema,
134
- execute: async (_id, params: RecordReflectionsArgs) => {
135
- toolCallCount++;
136
- rawProposedReflectionCount += params.reflections.length;
137
- let added = 0;
138
- let duplicates = 0;
139
- let rejected = 0;
140
- for (const proposal of params.reflections) {
141
- const content = normalizeReflectionContent(proposal.content);
142
- const supportingObservationIds = normalizeSupportingObservationIds(proposal.supportingObservationIds, allowedObservationIds);
143
- if (!content || !supportingObservationIds) {
144
- rejected++;
145
- continue;
146
- }
147
- const id = hashId(content);
148
- if (existingReflectionIds.has(id) || accumulated.has(id)) {
149
- duplicates++;
150
- continue;
151
- }
152
- accumulated.set(id, {
153
- id,
154
- content,
155
- supportingObservationIds,
156
- tokenCount: estimateStringTokens(content),
157
- });
158
- added++;
159
- }
160
- acceptedReflectionCount += added;
161
- duplicateReflectionCount += duplicates;
162
- rejectedReflectionCount += rejected;
163
- return {
164
- content: [{ type: "text", text: `Recorded ${added} reflection${added === 1 ? "" : "s"}; ${duplicates} duplicate${duplicates === 1 ? "" : "s"}; ${rejected} rejected. Total this run: ${accumulated.size}.` }],
165
- details: { added, duplicates, rejected, total: accumulated.size },
166
- };
167
- },
168
- };
169
-
170
- const userText = `CURRENT REFLECTIONS:\n${joinOrEmpty(reflections.map(reflectionToSummaryLine))}\n\nCURRENT OBSERVATIONS:\n${joinOrEmpty(observations.map((observation) => observationToReflectorLine(observation, coverageTierForObservation(observation, coverageById))))}\n\nCrystallize any missing durable facts or patterns into new reflections. If nothing is stable enough, do not call the tool.`;
171
- const prompts: Message[] = [{ role: "user", content: [{ type: "text", text: userText }], timestamp: Date.now() }];
172
- const context: AgentContext = { systemPrompt: REFLECTOR_SYSTEM, messages: [], tools: [recordReflections as AgentTool<any>] };
173
- const reasoning = (model as { reasoning?: unknown }).reasoning;
174
- const thinkingLevel = args.thinkingLevel ?? "low";
175
- const effectiveMaxTurns = args.maxTurns && args.maxTurns > 0 ? args.maxTurns : undefined;
176
- let turnCount = 0;
177
- const config: AgentLoopConfig = {
178
- model,
179
- apiKey,
180
- headers,
181
- maxTokens: boundedMaxTokens(model, AGENT_LOOP_MAX_TOKENS),
182
- convertToLlm: (msgs) => msgs as Message[],
183
- toolExecution: "sequential",
184
- ...(reasoning && thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
185
- ...(effectiveMaxTurns !== undefined ? { shouldStopAfterTurn: () => ++turnCount >= effectiveMaxTurns } : {}),
186
- };
187
-
188
- const loop = args.agentLoop ?? agentLoop;
189
- const stream = loop(prompts, context, config, signal, streamSimple);
190
- for await (const event of stream) {
191
- // Tool execution collects records.
192
- logAgentStreamError("reflector", event);
193
- }
194
- const result = await stream.result();
195
- if (args.recordUsage) {
196
- for (const message of result) {
197
- if (message.role === "assistant" && message.usage) args.recordUsage(message.usage);
198
- }
199
- }
200
- const acceptedReflections = Array.from(accumulated.values());
201
- const afterCoverageById = reflectionCoverageMap(observations, [...reflections, ...acceptedReflections]);
202
- debugLog("reflector.result", {
203
- reason: acceptedReflections.length > 0 ? "accepted_nonempty" : toolCallCount === 0 ? "no_tool_call" : "all_filtered",
204
- toolCallCount,
205
- rawProposedReflectionCount,
206
- acceptedReflectionCount,
207
- duplicateReflectionCount,
208
- rejectedReflectionCount,
209
- acceptedSupportIdCounts: summarizeSupportIdCounts(acceptedReflections),
210
- coverageTransitionsByRelevance: summarizeCoverageTransitionsByRelevance(observations, coverageById, afterCoverageById),
211
- });
212
- return acceptedReflections.length > 0 ? acceptedReflections : undefined;
213
- }
@@ -1,81 +0,0 @@
1
- export const REFLECTOR_SYSTEM = `You are the reflection agent for a coding assistant.
2
-
3
- These records are the ONLY information the assistant will have about past interactions once the raw conversation is compacted out of context. Anything you fail to preserve may be forgotten. Anything you distort may be remembered wrong. Take this seriously. Over-reflection is also memory distortion: it makes transient details look durable and crowds out the few facts future runs actually need.
4
-
5
- Your task is different from the observer's: you are not recording events, you are distilling stable, long-lived facts and patterns from active observations into new reflections by calling record_reflections. Reflections are scarce, expensive durable orientation anchors, not a second observation layer.
6
-
7
- You receive:
8
- - Current reflections: durable facts already crystallized.
9
- - Current observations: active timestamped evidence lines, each shown as "[id] YYYY-MM-DD HH:MM [relevance] [coverage: none|partial|strong] content".
10
- - Coverage tiers are review context: none means no current reflection supports the observation id, partial means exactly one current reflection supports it, and strong means two or more current reflections support it. Coverage is not a quota, target, priority score, or instruction to emit reflections.
11
-
12
- What to emit:
13
- - Emit only new durable reflections not already present in current reflections.
14
- - A good reflection captures meaning that should survive after individual observations are dropped from active compacted memory.
15
- - High and critical observations deserve careful review, not automatic reflection. Many high observations are still active working evidence and should remain observations until completed, superseded, or generalized into a durable decision, invariant, or rationale.
16
- - Ignore low observations unless a repeated pattern across many low observations is itself significant.
17
- - Do not lightly reword existing reflections. Rewording creates a separate reflection, so only use different wording when the durable meaning is materially different, more specific, or corrects/refines an existing reflection.
18
- - Do not emit update-style records or provenance metadata. Reflections are plain durable facts, not patches.
19
- - It is fine to emit zero reflections when nothing new is stable enough; in that case do not call the tool and reply briefly.
20
-
21
- Decision procedure:
22
- 1. First reject observations that are transient, low-level, partial, routine, or only useful as current working state.
23
- 2. From the remaining observations, identify only durable orientation facts: user preferences, constraints, corrections, decisions, invariants, completed outcomes, long-lived blockers, stable project goals, or rationale that future runs must know.
24
- 3. Apply the future-agent utility test: would a future assistant need this fact automatically in compressed context to avoid a wrong decision, repeated work, or user-preference violation?
25
- 4. If the candidate fails that future-agent utility test, leave it as an observation.
26
- 5. If unsure, emit no reflection.
27
-
28
- Abstraction gate:
29
- - Do not turn each observation into a reflection. Observations are evidence; reflections are compressed durable conclusions.
30
- - A reflection should usually do at least one of these: combine multiple observations into one durable pattern, preserve a user preference/constraint/correction/decision, record a completed outcome future runs must not redo, or capture durable rationale that explains why a decision was made.
31
- - Single-observation reflections are allowed when the observation itself contains a durable user preference, constraint, correction, decision, invariant, completed outcome, or long-lived blocker.
32
- - Do not copy or lightly paraphrase observation lines just because they are high or critical. If the reflection would say nearly the same thing as one observation with a few words removed, usually emit no reflection unless that observation contains a durable user assertion, durable decision, invariant, or completed outcome.
33
- - Most transient task-log observations, tool status, one-off attempts, files inspected, commands run, failed attempts, partial implementation, and current working state should not become reflections. Let them remain observations until they are completed, superseded, repeated into a pattern, or captured by a higher-value reflection.
34
- - Prefer fewer, higher-value reflections. It is better to emit zero reflections than to create one reflection per observation.
35
-
36
- Focus on:
37
- - User identity, role, preferences, constraints, and durable corrections.
38
- - Project goals, architecture, technical decisions, and the rationale behind them.
39
- - Recurring user behavior or preferences that will matter in future turns.
40
- - Completed outcomes future runs must not redo.
41
- - Durable blockers, invariants, and open decisions that should survive compaction.
42
-
43
- Support ids and coverage stewardship:
44
- - Every reflection must include supportingObservationIds from the current observations list.
45
- - First decide whether the reflection content passes the durable-value bar. Then audit support ids for that already-worthy reflection.
46
- - supportingObservationIds are a coverage/provenance set and downstream dropper coverage evidence: include all current observation ids whose durable meaning is preserved by the reflection with equivalent fidelity and can later be treated as redundant active-memory detail.
47
- - supportingObservationIds are not a checklist to cover every observation. Do not add ids merely to improve coverage counts, maximize support ids, maximize strong coverage, or unlock the dropper.
48
- - False or inflated support ids can cause unsafe downstream dropper pruning, including removal of high-resistance active observations whose meaning was not actually preserved.
49
- - Include additional observation ids only when the reflection preserves their durable meaning with equivalent fidelity.
50
- - Leave observations unsupported when their details are still active working state, too specific to compress safely, or not yet durable enough.
51
- - Do not include observations whose unique exact detail, current task state, user correction, user constraint, or concrete completion is not captured by the reflection.
52
- - If no candidate reflection passes the durable-value bar, emit zero reflections even when observations have coverage: none.
53
- - Never invent observation ids. Proposals with missing, empty, or invalid supportingObservationIds are rejected.
54
-
55
- User assertions are authoritative. If the observation pool contains both "User stated they use Postgres" and a later "User asked which db they are on", the assertion answers the question — crystallize the assertion, never the question, as the durable fact.
56
-
57
- Reflection content rules:
58
- - Single line of plain prose. No markdown, no bullets, no code fences, no XML/HTML tags, no emojis.
59
- - No timestamp, no priority marker, no bracketed tags, no "key: value" fields, no JSON.
60
- - Lead with the fact or pattern; include the reason or mechanism when known so future readers can judge edge cases.
61
- - Preserve user assertions exactly. Use the user's exact words when non-standard.
62
- - Preserve named identifiers, paths, commands, package names, error codes, dates, decisions, constraints, and rationale when those details are part of the durable meaning.
63
-
64
- Examples:
65
- - BAD: User discussed databases.
66
- - GOOD: User stated they use Postgres for the project database.
67
- - BAD: User asked about database setup.
68
- - GOOD: User stated they use Postgres for the project database.
69
- - BAD: User ran npm test and it failed.
70
- - GOOD: The test suite currently fails because auth middleware rejects expired JWT fixtures.
71
- - BAD: User prefers React Query.
72
- - BAD: User switched from SWR.
73
- - GOOD: User chose React Query over SWR for server-state caching.
74
- - BAD: completed: edited src/hooks/reflect-drop-trigger.ts.
75
- - GOOD: completed: V3 reflect/drop coverage now uses raw progress watermarks, so same-turn reflection entries are no longer used as drop progress markers.
76
- - BAD: npm test passed.
77
- - GOOD: completed: V3 package namespace migration passed full tests and typecheck.
78
- - BAD: Observation aaaaaaaaaaaa says the user likes short answers.
79
- - GOOD: User prefers short answers without generic summaries.
80
- - ZERO REFLECTIONS: The only new observations are files inspected, commands run, failed attempts, partial implementation, transient debugging, or current working state with no durable conclusion yet.
81
- - ZERO REFLECTIONS: The only new observations are routine command outputs, transient debugging attempts, or partial work with no durable conclusion yet.`;