@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.
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/package.json +60 -0
- package/src/agents/contemplator/agent.ts +718 -0
- package/src/agents/contemplator/prompts.ts +212 -0
- package/src/agents/dropper/agent.ts +291 -0
- package/src/agents/dropper/coverage.ts +128 -0
- package/src/agents/dropper/pool.ts +67 -0
- package/src/agents/dropper/prompts.ts +48 -0
- package/src/agents/observer/agent.ts +207 -0
- package/src/agents/observer/prompts.ts +119 -0
- package/src/agents/reflector/agent.ts +213 -0
- package/src/agents/reflector/prompts.ts +81 -0
- package/src/agents/reviewer/agent.ts +187 -0
- package/src/agents/reviewer/history-tools.ts +337 -0
- package/src/agents/reviewer/prompts.ts +135 -0
- package/src/agents/reviewer/tools.ts +84 -0
- package/src/agents/stream-errors.ts +22 -0
- package/src/clipboard.ts +63 -0
- package/src/commands/contemplator-view.ts +128 -0
- package/src/commands/reviewer-view.ts +89 -0
- package/src/commands/settings.ts +257 -0
- package/src/commands/status.ts +176 -0
- package/src/commands/view.ts +171 -0
- package/src/config.ts +284 -0
- package/src/debug-log.ts +72 -0
- package/src/hooks/compaction-hook.ts +99 -0
- package/src/hooks/compaction-resume.ts +124 -0
- package/src/hooks/compaction-trigger.ts +122 -0
- package/src/hooks/consolidation-trigger.ts +488 -0
- package/src/ids.ts +5 -0
- package/src/index.ts +32 -0
- package/src/model-budget.ts +16 -0
- package/src/runtime.ts +316 -0
- package/src/serialize.ts +274 -0
- package/src/session-ledger/fold.ts +115 -0
- package/src/session-ledger/index.ts +7 -0
- package/src/session-ledger/progress.ts +156 -0
- package/src/session-ledger/projection.ts +243 -0
- package/src/session-ledger/recall.ts +258 -0
- package/src/session-ledger/render-summary.ts +31 -0
- package/src/session-ledger/search.ts +184 -0
- package/src/session-ledger/types.ts +329 -0
- package/src/tokens.ts +27 -0
- package/src/tools/compact-context.ts +54 -0
- package/src/tools/recall-observation.ts +532 -0
- package/src/tools/search-memories.ts +131 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/** Build the contemplator prompt as one readable template with review-only blocks. */
|
|
2
|
+
export function buildContemplatorSystemPrompt(
|
|
3
|
+
reviewerEnabled: boolean,
|
|
4
|
+
): string {
|
|
5
|
+
return `You are the background contemplator supporting a primary agent. You are the System 2 thinker: slower, more deliberative, and focused on the larger shape of the problem while the primary agent handles the immediate work.
|
|
6
|
+
|
|
7
|
+
Neither you nor the primary agent should be assumed to know the correct solution. You are jointly exploring a problem space from different perspectives. The primary agent interacts with the actual environment and carries out the work. You maintain a longer-term view of the reasoning, evidence, assumptions, alternatives, unresolved questions${reviewerEnabled ? ", and recurring structural patterns" : ""} that emerge over time.
|
|
8
|
+
|
|
9
|
+
You receive incremental observations and reflections produced by other agent loops. Some memories summarize user messages. Pay extra attention to memories about the user’s intent, priorities, constraints, corrections, and desired outcome.
|
|
10
|
+
|
|
11
|
+
You see only the memory ledger, not the primary agent’s live activity. Your understanding may be incomplete or slightly stale. Do not infer inactivity, failure, or lack of progress from missing recent results. A result may simply not have reached memory yet.
|
|
12
|
+
|
|
13
|
+
${reviewerEnabled ? `Each memory has an identifier. Cite relevant memory identifiers in every probe or review request so the primary agent or reviewer can recover the supporting context.` : `Each memory has an identifier. Cite relevant memory identifiers in every probe so the primary agent can recover the context behind your question.`}
|
|
14
|
+
|
|
15
|
+
Maintain an evolving understanding of:
|
|
16
|
+
|
|
17
|
+
- what the user is ultimately trying to accomplish;
|
|
18
|
+
- what has actually been observed so far;
|
|
19
|
+
- the primary agent’s apparent direction;
|
|
20
|
+
- assumptions on which the current direction depends;
|
|
21
|
+
- important details that remain unknown or unconfirmed;
|
|
22
|
+
- relevant alternatives that remain open;
|
|
23
|
+
- connections, contradictions, and recurring patterns across memories;
|
|
24
|
+
- ways the problem could be framed or decomposed differently${reviewerEnabled ?
|
|
25
|
+
`;\n- whether repeated local work may indicate a structural problem in the workflow or software.` : `.`}
|
|
26
|
+
|
|
27
|
+
Your central responsibility is to identify gaps in the current thinking.
|
|
28
|
+
|
|
29
|
+
A reasoning gap may include:
|
|
30
|
+
|
|
31
|
+
- depending on a detail that has not been established;
|
|
32
|
+
- treating one possible explanation as though alternatives have been ruled out;
|
|
33
|
+
- moving from an observation to a conclusion without a clear connection;
|
|
34
|
+
- relying on an assumption whose failure would undermine the current direction;
|
|
35
|
+
- overlooking an important part of the user’s request;
|
|
36
|
+
- exploring one region of the solution space while neglecting a meaningfully different possibility;
|
|
37
|
+
- accepting an idea without obtaining evidence that distinguishes it from competing ideas when that distinction matters;
|
|
38
|
+
- failing to connect relevant information from separate memories;
|
|
39
|
+
- continuing while depending on an uncertainty that later decisions require to be resolved.
|
|
40
|
+
|
|
41
|
+
Do not treat the absence of a recent test result, tool result, or implementation update as a reasoning gap. A gap exists when the recorded reasoning depends on missing knowledge, not merely when the ledger lacks the latest execution details.
|
|
42
|
+
|
|
43
|
+
Do not encourage speculation merely because several possibilities can be imagined. When the user’s direction is clear and action is cheap, safe, and reversible, direct progress may be more informative than further hypothesis formation. Distinguish uncertainty that must be resolved before proceeding from uncertainty that can be resolved naturally by doing the work.
|
|
44
|
+
|
|
45
|
+
Your other high-value responsibility is reality-checking. Compare recent claims, interpretations, and assumptions with earlier recorded evidence. When a recent claim conflicts with an earlier observation, reflection, user-intent memory, or previously supported conclusion, make the contradiction visible and cite the memories on both sides.
|
|
46
|
+
|
|
47
|
+
Also look for opportunities to help the primary agent:
|
|
48
|
+
|
|
49
|
+
- remain aligned with the user’s actual direction;
|
|
50
|
+
- distinguish observations from interpretations;
|
|
51
|
+
- expose an assumption that should not yet be treated as settled;
|
|
52
|
+
- consider a relevant alternative that has not received meaningful attention;
|
|
53
|
+
- identify what evidence would distinguish between competing explanations when the distinction affects the work;
|
|
54
|
+
- break the problem into smaller questions that can be explored independently;
|
|
55
|
+
- find a more revealing or efficient way to explore the problem;
|
|
56
|
+
- recognize a clear, unproductive loop;
|
|
57
|
+
- reconsider a direction weakened by specific evidence${reviewerEnabled ?
|
|
58
|
+
`;\n- recognize when repeated local work indicates a deeper workflow or software-design issue.` : `.`}
|
|
59
|
+
|
|
60
|
+
Prefer asking one probing question over prescribing a solution. Do not assume that you know the answer and are guiding the primary agent toward it. Ask questions that help both agents discover what is missing, what remains possible, and what evidence or reframing would meaningfully improve the work.
|
|
61
|
+
|
|
62
|
+
A useful probe may ask:
|
|
63
|
+
|
|
64
|
+
- What assumption is this direction depending on?
|
|
65
|
+
- Which relevant possibility has not yet been ruled out?
|
|
66
|
+
- What observation would distinguish the current explanation from an alternative?
|
|
67
|
+
- Is a conclusion stronger than the evidence recorded for it?
|
|
68
|
+
- Is there a smaller question that would clarify the larger problem?
|
|
69
|
+
- Are several attempts failing because they share the same hidden premise?
|
|
70
|
+
- Has an important user constraint disappeared from the current framing?
|
|
71
|
+
- Is continued reasoning producing new information, or could a direct interaction resolve the uncertainty more reliably?
|
|
72
|
+
|
|
73
|
+
Questions should be grounded in the actual memories rather than generic problem-solving advice.
|
|
74
|
+
|
|
75
|
+
You have access to search_memories for finding older ${reviewerEnabled ? `observations, reflections, and durable review results` : `observations and reflections`} on the current branch. Use it when the updates provided do not contain enough context, searching with distinctive terms rather than broad questions. Results include memory identifiers that you can cite in a probe${reviewerEnabled ? ` or review request` : ``}.
|
|
76
|
+
|
|
77
|
+
You also have recall for recovering exact source context behind a specific memory identifier. Use it when a result is important but compressed.
|
|
78
|
+
|
|
79
|
+
${reviewerEnabled ?
|
|
80
|
+
`Specific recorded evidence that contradicts or materially weakens the current approach should produce a probe unless the issue is better handled by a deeper structural review. A concrete reasoning gap that the primary agent appears to depend upon should also produce a probe unless it reveals a recurring structural problem deserving review.` :
|
|
81
|
+
`Specific recorded evidence that contradicts or materially weakens the current approach should produce a probe. A concrete reasoning gap that the primary agent appears to depend upon should also produce a probe.`}
|
|
82
|
+
|
|
83
|
+
Pay particular attention to unproductive loops. A loop may be present when multiple memories show the primary agent:
|
|
84
|
+
|
|
85
|
+
- returning to the same obstacle through different superficial approaches;
|
|
86
|
+
- avoiding a difficult uncertainty by introducing increasingly fragile shortcuts;
|
|
87
|
+
- abandoning an idea because evaluating it appears difficult, then spending more effort on alternatives that fail for related reasons;
|
|
88
|
+
- changing approaches without gaining information that distinguishes between likely explanations;
|
|
89
|
+
- repeatedly addressing consequences without reconsidering the assumption or framing that produces them${reviewerEnabled ?
|
|
90
|
+
`;\n- repeatedly reconstructing the same information or operation without preserving a reusable result.` : `.`}
|
|
91
|
+
|
|
92
|
+
Do not infer a loop from a single failure, silence, elapsed time, token count, or missing recent results. A loop must be supported by a clear pattern across multiple memories. Activity measurements may support the diagnosis, but they are not proof by themselves.
|
|
93
|
+
|
|
94
|
+
You may probe a previously raised theme again when new memories show that the primary agent remains caught in an unproductive pattern. Do not merely repeat the earlier question. Use the accumulated evidence to ask about the larger assumption, uncertainty, decomposition, feedback process, or missing reusable structure keeping the loop in place.
|
|
95
|
+
|
|
96
|
+
${
|
|
97
|
+
reviewerEnabled
|
|
98
|
+
? `You have two forms of intervention.
|
|
99
|
+
|
|
100
|
+
Use send_probe when one concise, memory-grounded question could materially improve the primary agent’s next reasoning round by exposing:
|
|
101
|
+
|
|
102
|
+
- a contradiction;
|
|
103
|
+
- a consequential reasoning gap;
|
|
104
|
+
- drift from user intent;
|
|
105
|
+
- an overlooked possibility;
|
|
106
|
+
- an unproductive loop;
|
|
107
|
+
- or a reason to stop speculating and obtain a direct result.
|
|
108
|
+
|
|
109
|
+
Use request_review when multiple memories suggest a recurring structural problem that deserves independent investigation and, if supported, a durable conceptual proposal.
|
|
110
|
+
|
|
111
|
+
Choose scope "workflow" when the suspected structural problem concerns how the primary agent performs the work. Examples include:
|
|
112
|
+
|
|
113
|
+
- repeated reconstruction of the same information;
|
|
114
|
+
- excessive reasoning about something that could be determined directly;
|
|
115
|
+
- repeated manual searches, traces, transformations, or correlations;
|
|
116
|
+
- an unnecessarily slow or low-information feedback loop;
|
|
117
|
+
- repeated one-off scripts that should become a reusable capability;
|
|
118
|
+
- failure to preserve a useful result or representation;
|
|
119
|
+
- a process that is unreliable, token-intensive, difficult to reproduce, or difficult to review.
|
|
120
|
+
|
|
121
|
+
Choose scope "software" when the suspected structural problem concerns the software being produced. Examples include:
|
|
122
|
+
|
|
123
|
+
- repeated special cases;
|
|
124
|
+
- duplicated concepts or behavior;
|
|
125
|
+
- several fixes involving the same missing invariant;
|
|
126
|
+
- unclear responsibility boundaries;
|
|
127
|
+
- unsuitable state or data representations;
|
|
128
|
+
- recurring workarounds;
|
|
129
|
+
- local fixes that may indicate a missing abstraction or structural redesign.
|
|
130
|
+
|
|
131
|
+
A review request should identify:
|
|
132
|
+
|
|
133
|
+
- the memories revealing the pattern;
|
|
134
|
+
- the suspected structural concern;
|
|
135
|
+
- what the reviewer should determine;
|
|
136
|
+
- relevant user constraints or uncertainties.
|
|
137
|
+
|
|
138
|
+
Do not design the solution in the review request. State the concern as a possibility. The reviewer must independently recall and search memories, examine supporting and contrary evidence, and decide whether a proposal is justified.
|
|
139
|
+
|
|
140
|
+
Do not request a review based only on a single inconvenience, one failed attempt, silence, elapsed time, token count, or generic best practice. The request should be grounded in a recurring pattern or an especially consequential structural issue.
|
|
141
|
+
|
|
142
|
+
Before requesting a review, search for an existing review result or proposal that may already address the concern. When one exists, prefer citing it in a probe. Request another review only when new memories reveal a material limitation, a substantially different problem, or a need to revise or extend the earlier design.`
|
|
143
|
+
: ``
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Do not:
|
|
147
|
+
|
|
148
|
+
- behave as though you already know the correct solution;
|
|
149
|
+
- ask leading questions that smuggle in an unsupported conclusion;
|
|
150
|
+
- judge the primary agent’s pacing from silence or delayed memory;
|
|
151
|
+
- declare the work complete, correct, mature, or successful;
|
|
152
|
+
- remind the primary agent to perform routine tasks;
|
|
153
|
+
- manage implementation step by step;
|
|
154
|
+
- focus on tests, commands, files, syntax, programming language, or other low-level details unless they reveal a broader reasoning gap, strategic pattern, or bottleneck;
|
|
155
|
+
- repeat memories as a status summary;
|
|
156
|
+
- offer encouragement, praise, or generic advice;
|
|
157
|
+
- invent details absent from the memories;
|
|
158
|
+
- send several questions or competing suggestions at once;
|
|
159
|
+
- send a question that could have been written without seeing the relevant memories${reviewerEnabled ?
|
|
160
|
+
`;\n- request a structural review merely because a design improvement is theoretically possible that is irrelevant to the task at hand.` : `.`}
|
|
161
|
+
|
|
162
|
+
When one high-signal question could materially improve the exploration of the problem, call send_probe with one concise, natural-language question.
|
|
163
|
+
|
|
164
|
+
A good probe should:
|
|
165
|
+
|
|
166
|
+
- cite the relevant memory identifiers;
|
|
167
|
+
- identify the concrete gap, contradiction, pattern, or overlooked possibility;
|
|
168
|
+
- ask one focused question;
|
|
169
|
+
- help clarify what is known, what is assumed, or what should be explored;
|
|
170
|
+
- remain useful even if the primary agent has progressed since the memories were recorded.
|
|
171
|
+
|
|
172
|
+
${
|
|
173
|
+
reviewerEnabled
|
|
174
|
+
? `When a recurring structural pattern deserves deeper independent analysis, call request_review instead.
|
|
175
|
+
|
|
176
|
+
A good review request should:
|
|
177
|
+
|
|
178
|
+
- select exactly one scope;
|
|
179
|
+
- cite the memories showing the pattern;
|
|
180
|
+
- describe the suspected issue without assuming the diagnosis is correct;
|
|
181
|
+
- explain what the reviewer should investigate;
|
|
182
|
+
- preserve relevant user intent and constraints;
|
|
183
|
+
- leave the conceptual design to the reviewer.`
|
|
184
|
+
: ``
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
${reviewerEnabled ?
|
|
188
|
+
`Your interventions are asynchronous. Use no more than one intervention per update: either send_probe, request_review, or neither.` :
|
|
189
|
+
`Your probes are delivered asynchronously. Send no more than one probe per update.`}
|
|
190
|
+
|
|
191
|
+
Prioritize:
|
|
192
|
+
|
|
193
|
+
1. Gaps between what the current reasoning depends upon and what has actually been established.
|
|
194
|
+
2. Contradictions between recent claims and earlier evidence.
|
|
195
|
+
3. Misalignment with recorded user intent.
|
|
196
|
+
4. Relevant alternatives or parts of the problem receiving insufficient consideration.
|
|
197
|
+
5. Clear unproductive loops supported by multiple memories.
|
|
198
|
+
6. Connections that reveal a better framing, decomposition, or way to reduce uncertainty.
|
|
199
|
+
${
|
|
200
|
+
reviewerEnabled
|
|
201
|
+
? `7. Recurring workflow problems that may deserve a durable workflow review.
|
|
202
|
+
8. Recurring software-design symptoms that may deserve a durable software review.`
|
|
203
|
+
: ``
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
If no specific, grounded, materially useful intervention exists, do not call an intervention tool.`
|
|
207
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
208
|
+
.trim();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Full prompt retained for callers and tests that enable structural reviews. */
|
|
212
|
+
export const CONTEMPLATOR_SYSTEM = buildContemplatorSystemPrompt(true);
|
|
@@ -0,0 +1,291 @@
|
|
|
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 { AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "../../model-budget.js";
|
|
8
|
+
import { logAgentStreamError } from "../stream-errors.js";
|
|
9
|
+
import { reflectionToSummaryLine, type Observation, type Reflection } from "../../session-ledger/index.js";
|
|
10
|
+
import { DROPPER_SYSTEM } from "./prompts.js";
|
|
11
|
+
import {
|
|
12
|
+
REFLECTION_COVERAGE_DROP_RANK,
|
|
13
|
+
coverageTierForObservation,
|
|
14
|
+
reflectionCoverageMap,
|
|
15
|
+
summarizeCoverageByRelevance,
|
|
16
|
+
summarizeCoverageByRelevanceForIds,
|
|
17
|
+
observationToDropperLine,
|
|
18
|
+
} from "./coverage.js";
|
|
19
|
+
import { observationPoolMetrics } from "./pool.js";
|
|
20
|
+
import type { LlmUsageInput } from "../../runtime.js";
|
|
21
|
+
export {
|
|
22
|
+
maxDropCountForPool,
|
|
23
|
+
observationPoolFullness,
|
|
24
|
+
observationPoolMetrics,
|
|
25
|
+
} from "./pool.js";
|
|
26
|
+
export type { ObservationPoolMetrics } from "./pool.js";
|
|
27
|
+
export {
|
|
28
|
+
REFLECTION_COVERAGE_TIERS,
|
|
29
|
+
coverageTierForObservation,
|
|
30
|
+
emptyCoverageSummaryByRelevance,
|
|
31
|
+
observationToDropperLine,
|
|
32
|
+
reflectionCoverageMap,
|
|
33
|
+
reflectionCoverageTierForCount,
|
|
34
|
+
reflectionSupportCounts,
|
|
35
|
+
summarizeCoverageByRelevance,
|
|
36
|
+
summarizeCoverageByRelevanceForIds,
|
|
37
|
+
summarizeCoverageTransitionsByRelevance,
|
|
38
|
+
} from "./coverage.js";
|
|
39
|
+
export type { CoverageSummaryByRelevance, CoverageTransitionSummaryByRelevance, ReflectionCoverageTier } from "./coverage.js";
|
|
40
|
+
|
|
41
|
+
interface RunDropperArgs {
|
|
42
|
+
model: Model<any>;
|
|
43
|
+
apiKey: string;
|
|
44
|
+
headers?: Record<string, string>;
|
|
45
|
+
reflections: Reflection[];
|
|
46
|
+
observations: Observation[];
|
|
47
|
+
targetTokens: number;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
agentLoop?: typeof agentLoop;
|
|
50
|
+
maxTurns?: number;
|
|
51
|
+
thinkingLevel?: ModelThinkingLevel;
|
|
52
|
+
recordUsage?: (usage: LlmUsageInput) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const RELEVANCE_DROP_RANK: Record<Observation["relevance"], number> = {
|
|
56
|
+
low: 0,
|
|
57
|
+
medium: 1,
|
|
58
|
+
high: 2,
|
|
59
|
+
critical: 3,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const DropObservationsSchema = Type.Object({
|
|
63
|
+
ids: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
64
|
+
reason: Type.Optional(Type.String()),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
type DropObservationsArgs = Static<typeof DropObservationsSchema>;
|
|
68
|
+
|
|
69
|
+
function joinOrEmpty(items: string[]): string {
|
|
70
|
+
return items.length ? items.join("\n") : "(none yet)";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function relevanceCounts(observations: readonly Observation[]): Record<Observation["relevance"], number> {
|
|
74
|
+
return observations.reduce<Record<Observation["relevance"], number>>((counts, observation) => {
|
|
75
|
+
counts[observation.relevance]++;
|
|
76
|
+
return counts;
|
|
77
|
+
}, { low: 0, medium: 0, high: 0, critical: 0 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function normalizeDropObservationIds(
|
|
81
|
+
ids: readonly string[] | undefined,
|
|
82
|
+
observations: readonly Observation[],
|
|
83
|
+
): string[] | undefined {
|
|
84
|
+
if (!ids || ids.length === 0) return undefined;
|
|
85
|
+
const allowed = new Map(observations.map((observation) => [observation.id, observation]));
|
|
86
|
+
const result: string[] = [];
|
|
87
|
+
const seen = new Set<string>();
|
|
88
|
+
for (const id of ids) {
|
|
89
|
+
const observation = allowed.get(id);
|
|
90
|
+
if (!observation) continue;
|
|
91
|
+
if (seen.has(id)) continue;
|
|
92
|
+
seen.add(id);
|
|
93
|
+
result.push(id);
|
|
94
|
+
}
|
|
95
|
+
return result.length > 0 ? result : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function timestampRank(timestamp: string): number {
|
|
99
|
+
const parsed = Date.parse(timestamp);
|
|
100
|
+
return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function selectDropCandidates(
|
|
104
|
+
ids: readonly string[],
|
|
105
|
+
observations: readonly Observation[],
|
|
106
|
+
maxDrops: number,
|
|
107
|
+
reflections: readonly Reflection[] = [],
|
|
108
|
+
): string[] {
|
|
109
|
+
if (maxDrops <= 0 || ids.length === 0) return [];
|
|
110
|
+
|
|
111
|
+
const byId = new Map(observations.map((observation) => [observation.id, observation]));
|
|
112
|
+
const coverageById = reflectionCoverageMap(observations, reflections);
|
|
113
|
+
const firstProposalIndex = new Map<string, number>();
|
|
114
|
+
for (let i = 0; i < ids.length; i++) {
|
|
115
|
+
const id = ids[i];
|
|
116
|
+
if (!firstProposalIndex.has(id)) firstProposalIndex.set(id, i);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Array.from(firstProposalIndex.entries())
|
|
120
|
+
.map(([id, index]) => ({ id, index, observation: byId.get(id) }))
|
|
121
|
+
.filter((candidate): candidate is { id: string; index: number; observation: Observation } =>
|
|
122
|
+
candidate.observation !== undefined
|
|
123
|
+
)
|
|
124
|
+
.sort((a, b) => {
|
|
125
|
+
const coverageDelta = REFLECTION_COVERAGE_DROP_RANK[coverageTierForObservation(a.observation, coverageById)]
|
|
126
|
+
- REFLECTION_COVERAGE_DROP_RANK[coverageTierForObservation(b.observation, coverageById)];
|
|
127
|
+
const relevanceDelta = RELEVANCE_DROP_RANK[a.observation.relevance] - RELEVANCE_DROP_RANK[b.observation.relevance];
|
|
128
|
+
const ageDelta = timestampRank(a.observation.timestamp) - timestampRank(b.observation.timestamp);
|
|
129
|
+
return coverageDelta || relevanceDelta || ageDelta || a.index - b.index;
|
|
130
|
+
})
|
|
131
|
+
.slice(0, maxDrops)
|
|
132
|
+
.map((candidate) => candidate.id);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function runDropper(args: RunDropperArgs): Promise<string[] | undefined> {
|
|
136
|
+
const { model, apiKey, headers, reflections, observations, targetTokens, signal } = args;
|
|
137
|
+
if (observations.length === 0) return undefined;
|
|
138
|
+
|
|
139
|
+
const metrics = observationPoolMetrics(observations, targetTokens);
|
|
140
|
+
const { observationTokens, fullness, tokensOverTarget, maxDropsAllowed } = metrics;
|
|
141
|
+
const coverageById = reflectionCoverageMap(observations, reflections);
|
|
142
|
+
const coverageSummaryByRelevance = summarizeCoverageByRelevance(observations, coverageById);
|
|
143
|
+
debugLog("dropper.agent_start", {
|
|
144
|
+
activeObservationCount: observations.length,
|
|
145
|
+
reflectionCount: reflections.length,
|
|
146
|
+
observationTokens,
|
|
147
|
+
targetTokens,
|
|
148
|
+
tokensOverTarget,
|
|
149
|
+
fullness,
|
|
150
|
+
maxDropsAllowed,
|
|
151
|
+
relevanceCounts: relevanceCounts(observations),
|
|
152
|
+
coverageSummaryByRelevance,
|
|
153
|
+
});
|
|
154
|
+
if (maxDropsAllowed <= 0) {
|
|
155
|
+
debugLog("dropper.result", {
|
|
156
|
+
reason: "not_over_target",
|
|
157
|
+
toolCallCount: 0,
|
|
158
|
+
rawRequestedIdsCount: 0,
|
|
159
|
+
acceptedCandidateCount: 0,
|
|
160
|
+
selectedDropsCount: 0,
|
|
161
|
+
selectedDropTokens: 0,
|
|
162
|
+
selectedCoverageSummaryByRelevance: summarizeCoverageByRelevanceForIds([], observations, coverageById),
|
|
163
|
+
maxDropsAllowed,
|
|
164
|
+
});
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const proposedDropIds: string[] = [];
|
|
169
|
+
const proposed = new Set<string>();
|
|
170
|
+
const allowed = new Map(observations.map((observation) => [observation.id, observation]));
|
|
171
|
+
let toolCallCount = 0;
|
|
172
|
+
let rawRequestedIdsCount = 0;
|
|
173
|
+
let missingIdsCount = 0;
|
|
174
|
+
let criticalCandidateIdsCount = 0;
|
|
175
|
+
let duplicateInRequestCount = 0;
|
|
176
|
+
let duplicateInRunCount = 0;
|
|
177
|
+
|
|
178
|
+
const dropObservations: AgentTool<typeof DropObservationsSchema> = {
|
|
179
|
+
name: "drop_observations",
|
|
180
|
+
label: "Drop observations",
|
|
181
|
+
description: "Propose active observation ids that are safe to remove from compacted memory.",
|
|
182
|
+
parameters: DropObservationsSchema,
|
|
183
|
+
execute: async (_id, params: DropObservationsArgs) => {
|
|
184
|
+
toolCallCount++;
|
|
185
|
+
rawRequestedIdsCount += params.ids.length;
|
|
186
|
+
const seenInRequest = new Set<string>();
|
|
187
|
+
let added = 0;
|
|
188
|
+
let requestMissingIds = 0;
|
|
189
|
+
let requestCriticalCandidateIds = 0;
|
|
190
|
+
let requestDuplicateIds = 0;
|
|
191
|
+
let requestDuplicateInRunIds = 0;
|
|
192
|
+
for (const id of params.ids) {
|
|
193
|
+
const observation = allowed.get(id);
|
|
194
|
+
if (!observation) {
|
|
195
|
+
missingIdsCount++;
|
|
196
|
+
requestMissingIds++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (seenInRequest.has(id)) {
|
|
200
|
+
duplicateInRequestCount++;
|
|
201
|
+
requestDuplicateIds++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
seenInRequest.add(id);
|
|
205
|
+
if (proposed.has(id)) {
|
|
206
|
+
duplicateInRunCount++;
|
|
207
|
+
requestDuplicateInRunIds++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
proposed.add(id);
|
|
211
|
+
proposedDropIds.push(id);
|
|
212
|
+
if (observation.relevance === "critical") {
|
|
213
|
+
criticalCandidateIdsCount++;
|
|
214
|
+
requestCriticalCandidateIds++;
|
|
215
|
+
}
|
|
216
|
+
added++;
|
|
217
|
+
}
|
|
218
|
+
debugLog("dropper.tool_call", {
|
|
219
|
+
toolCallCount,
|
|
220
|
+
rawRequestedIdsCount: params.ids.length,
|
|
221
|
+
acceptedIdsCount: added,
|
|
222
|
+
missingIdsCount: requestMissingIds,
|
|
223
|
+
criticalCandidateIdsCount: requestCriticalCandidateIds,
|
|
224
|
+
duplicateInRequestCount: requestDuplicateIds,
|
|
225
|
+
duplicateInRunCount: requestDuplicateInRunIds,
|
|
226
|
+
totalCandidates: proposedDropIds.length,
|
|
227
|
+
maxDropsAllowed,
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
content: [{ type: "text", text: `Queued ${added} drop candidate${added === 1 ? "" : "s"}. Candidates this run: ${proposedDropIds.length}. Maximum drops allowed: ${maxDropsAllowed}.` }],
|
|
231
|
+
details: { added, totalCandidates: proposedDropIds.length, maxDropsAllowed },
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const fullnessPercent = Math.round(fullness * 100);
|
|
237
|
+
const userText = `CURRENT REFLECTIONS:\n${joinOrEmpty(reflections.map(reflectionToSummaryLine))}\n\nCURRENT OBSERVATIONS:\n${joinOrEmpty(observations.map((observation) => observationToDropperLine(observation, coverageTierForObservation(observation, coverageById))))}\n\nActive observation pool: ~${observationTokens.toLocaleString()} tokens; target: ~${targetTokens.toLocaleString()} tokens; fullness against target: ~${fullnessPercent.toLocaleString()}%; over target by ~${tokensOverTarget.toLocaleString()} tokens.\nMaximum drops allowed this run: ${maxDropsAllowed.toLocaleString()} observation${maxDropsAllowed === 1 ? "" : "s"}. This maximum is sized to move the active pool toward the target if every proposed drop is clearly safe.\nThis maximum is a hard upper bound, not a target. Drop fewer or none if fewer observations are clearly safe.`;
|
|
238
|
+
const prompts: Message[] = [{ role: "user", content: [{ type: "text", text: userText }], timestamp: Date.now() }];
|
|
239
|
+
const context: AgentContext = { systemPrompt: DROPPER_SYSTEM, messages: [], tools: [dropObservations as AgentTool<any>] };
|
|
240
|
+
const reasoning = (model as { reasoning?: unknown }).reasoning;
|
|
241
|
+
const thinkingLevel = args.thinkingLevel ?? "low";
|
|
242
|
+
const effectiveMaxTurns = args.maxTurns && args.maxTurns > 0 ? args.maxTurns : undefined;
|
|
243
|
+
let turnCount = 0;
|
|
244
|
+
const config: AgentLoopConfig = {
|
|
245
|
+
model,
|
|
246
|
+
apiKey,
|
|
247
|
+
headers,
|
|
248
|
+
maxTokens: boundedMaxTokens(model, AGENT_LOOP_MAX_TOKENS),
|
|
249
|
+
convertToLlm: (msgs) => msgs as Message[],
|
|
250
|
+
toolExecution: "sequential",
|
|
251
|
+
...(reasoning && thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
|
|
252
|
+
...(effectiveMaxTurns !== undefined ? { shouldStopAfterTurn: () => ++turnCount >= effectiveMaxTurns } : {}),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const loop = args.agentLoop ?? agentLoop;
|
|
256
|
+
const stream = loop(prompts, context, config, signal, streamSimple);
|
|
257
|
+
for await (const event of stream) {
|
|
258
|
+
// Tool execution collects candidate ids.
|
|
259
|
+
logAgentStreamError("dropper", event);
|
|
260
|
+
}
|
|
261
|
+
const result = await stream.result();
|
|
262
|
+
if (args.recordUsage) {
|
|
263
|
+
for (const message of result) {
|
|
264
|
+
if (message.role === "assistant" && message.usage) args.recordUsage(message.usage);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const droppedIds = selectDropCandidates(proposedDropIds, observations, maxDropsAllowed, reflections);
|
|
268
|
+
const reason = droppedIds.length > 0
|
|
269
|
+
? "selected_nonempty"
|
|
270
|
+
: toolCallCount === 0
|
|
271
|
+
? "no_tool_call"
|
|
272
|
+
: proposedDropIds.length === 0
|
|
273
|
+
? "all_filtered"
|
|
274
|
+
: "selected_empty";
|
|
275
|
+
const selectedDropTokens = droppedIds.reduce((sum, id) => sum + (allowed.get(id)?.tokenCount ?? 0), 0);
|
|
276
|
+
debugLog("dropper.result", {
|
|
277
|
+
reason,
|
|
278
|
+
toolCallCount,
|
|
279
|
+
rawRequestedIdsCount,
|
|
280
|
+
missingIdsCount,
|
|
281
|
+
criticalCandidateIdsCount,
|
|
282
|
+
duplicateInRequestCount,
|
|
283
|
+
duplicateInRunCount,
|
|
284
|
+
acceptedCandidateCount: proposedDropIds.length,
|
|
285
|
+
selectedDropsCount: droppedIds.length,
|
|
286
|
+
selectedDropTokens,
|
|
287
|
+
selectedCoverageSummaryByRelevance: summarizeCoverageByRelevanceForIds(droppedIds, observations, coverageById),
|
|
288
|
+
maxDropsAllowed,
|
|
289
|
+
});
|
|
290
|
+
return droppedIds.length > 0 ? droppedIds : undefined;
|
|
291
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Observation, Reflection } from "../../session-ledger/index.js";
|
|
2
|
+
|
|
3
|
+
export const REFLECTION_COVERAGE_TIERS = ["none", "partial", "strong"] as const;
|
|
4
|
+
export type ReflectionCoverageTier = typeof REFLECTION_COVERAGE_TIERS[number];
|
|
5
|
+
|
|
6
|
+
type Relevance = Observation["relevance"];
|
|
7
|
+
|
|
8
|
+
type CoverageBucket = Record<ReflectionCoverageTier, { count: number; tokens: number }>;
|
|
9
|
+
export type CoverageSummaryByRelevance = Record<Relevance, CoverageBucket>;
|
|
10
|
+
export type CoverageTransitionSummaryByRelevance = Record<Relevance, Record<string, { count: number; tokens: number }>>;
|
|
11
|
+
|
|
12
|
+
export const REFLECTION_COVERAGE_DROP_RANK: Record<ReflectionCoverageTier, number> = {
|
|
13
|
+
strong: 0,
|
|
14
|
+
partial: 1,
|
|
15
|
+
none: 2,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function reflectionSupportCounts(reflections: readonly Reflection[]): Map<string, number> {
|
|
19
|
+
const counts = new Map<string, number>();
|
|
20
|
+
for (const reflection of reflections) {
|
|
21
|
+
const uniqueIds = new Set(reflection.supportingObservationIds);
|
|
22
|
+
for (const id of uniqueIds) counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
23
|
+
}
|
|
24
|
+
return counts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function reflectionCoverageTierForCount(count: number): ReflectionCoverageTier {
|
|
28
|
+
if (count <= 0) return "none";
|
|
29
|
+
if (count === 1) return "partial";
|
|
30
|
+
return "strong";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function reflectionCoverageMap(
|
|
34
|
+
observations: readonly Observation[],
|
|
35
|
+
reflections: readonly Reflection[],
|
|
36
|
+
): Map<string, ReflectionCoverageTier> {
|
|
37
|
+
const counts = reflectionSupportCounts(reflections);
|
|
38
|
+
return new Map(observations.map((observation) => [
|
|
39
|
+
observation.id,
|
|
40
|
+
reflectionCoverageTierForCount(counts.get(observation.id) ?? 0),
|
|
41
|
+
]));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function emptyCoverageBucket(): CoverageBucket {
|
|
45
|
+
return {
|
|
46
|
+
none: { count: 0, tokens: 0 },
|
|
47
|
+
partial: { count: 0, tokens: 0 },
|
|
48
|
+
strong: { count: 0, tokens: 0 },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function emptyCoverageSummaryByRelevance(): CoverageSummaryByRelevance {
|
|
53
|
+
return {
|
|
54
|
+
low: emptyCoverageBucket(),
|
|
55
|
+
medium: emptyCoverageBucket(),
|
|
56
|
+
high: emptyCoverageBucket(),
|
|
57
|
+
critical: emptyCoverageBucket(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function summarizeCoverageByRelevance(
|
|
62
|
+
observations: readonly Observation[],
|
|
63
|
+
coverageById: ReadonlyMap<string, ReflectionCoverageTier>,
|
|
64
|
+
): CoverageSummaryByRelevance {
|
|
65
|
+
const summary = emptyCoverageSummaryByRelevance();
|
|
66
|
+
for (const observation of observations) {
|
|
67
|
+
const tier = coverageById.get(observation.id) ?? "none";
|
|
68
|
+
const bucket = summary[observation.relevance][tier];
|
|
69
|
+
bucket.count++;
|
|
70
|
+
bucket.tokens += observation.tokenCount;
|
|
71
|
+
}
|
|
72
|
+
return summary;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function summarizeCoverageByRelevanceForIds(
|
|
76
|
+
ids: readonly string[],
|
|
77
|
+
observations: readonly Observation[],
|
|
78
|
+
coverageById: ReadonlyMap<string, ReflectionCoverageTier>,
|
|
79
|
+
): CoverageSummaryByRelevance {
|
|
80
|
+
const byId = new Map(observations.map((observation) => [observation.id, observation]));
|
|
81
|
+
const selected = ids.flatMap((id) => {
|
|
82
|
+
const observation = byId.get(id);
|
|
83
|
+
return observation ? [observation] : [];
|
|
84
|
+
});
|
|
85
|
+
return summarizeCoverageByRelevance(selected, coverageById);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function emptyCoverageTransitionSummaryByRelevance(): CoverageTransitionSummaryByRelevance {
|
|
89
|
+
return {
|
|
90
|
+
low: {},
|
|
91
|
+
medium: {},
|
|
92
|
+
high: {},
|
|
93
|
+
critical: {},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function summarizeCoverageTransitionsByRelevance(
|
|
98
|
+
observations: readonly Observation[],
|
|
99
|
+
beforeCoverageById: ReadonlyMap<string, ReflectionCoverageTier>,
|
|
100
|
+
afterCoverageById: ReadonlyMap<string, ReflectionCoverageTier>,
|
|
101
|
+
): CoverageTransitionSummaryByRelevance {
|
|
102
|
+
const summary = emptyCoverageTransitionSummaryByRelevance();
|
|
103
|
+
for (const observation of observations) {
|
|
104
|
+
const before = beforeCoverageById.get(observation.id) ?? "none";
|
|
105
|
+
const after = afterCoverageById.get(observation.id) ?? "none";
|
|
106
|
+
if (before === after) continue;
|
|
107
|
+
const key = `${before}->${after}`;
|
|
108
|
+
const bucket = summary[observation.relevance][key] ?? { count: 0, tokens: 0 };
|
|
109
|
+
bucket.count++;
|
|
110
|
+
bucket.tokens += observation.tokenCount;
|
|
111
|
+
summary[observation.relevance][key] = bucket;
|
|
112
|
+
}
|
|
113
|
+
return summary;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function observationToDropperLine(
|
|
117
|
+
observation: Observation,
|
|
118
|
+
coverage: ReflectionCoverageTier,
|
|
119
|
+
): string {
|
|
120
|
+
return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] [coverage: ${coverage}] ${observation.content}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function coverageTierForObservation(
|
|
124
|
+
observation: Observation,
|
|
125
|
+
coverageById: ReadonlyMap<string, ReflectionCoverageTier>,
|
|
126
|
+
): ReflectionCoverageTier {
|
|
127
|
+
return coverageById.get(observation.id) ?? "none";
|
|
128
|
+
}
|