pi-blackhole 0.4.2 → 0.4.3

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 (87) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +11660 -0
  3. package/dist/index.js.map +1 -0
  4. package/example-config.json +1 -1
  5. package/index.ts +37 -63
  6. package/package.json +21 -9
  7. package/src/commands/cleanup.ts +279 -240
  8. package/src/commands/memory.ts +236 -184
  9. package/src/commands/pi-vcc.ts +202 -152
  10. package/src/commands/vcc-recall.ts +126 -95
  11. package/src/core/brief.ts +167 -33
  12. package/src/core/build-sections.ts +8 -2
  13. package/src/core/config-env.ts +117 -0
  14. package/src/core/content.ts +31 -7
  15. package/src/core/drill-down.ts +41 -11
  16. package/src/core/filter-noise.ts +9 -3
  17. package/src/core/format-recall.ts +15 -6
  18. package/src/core/format.ts +14 -4
  19. package/src/core/lineage.ts +9 -3
  20. package/src/core/load-messages.ts +24 -5
  21. package/src/core/normalize.ts +38 -14
  22. package/src/core/recall-scope.ts +11 -3
  23. package/src/core/render-entries.ts +22 -6
  24. package/src/core/sanitize.ts +5 -1
  25. package/src/core/search-entries.ts +111 -19
  26. package/src/core/settings.ts +1 -3
  27. package/src/core/summarize.ts +42 -21
  28. package/src/core/unified-config.ts +549 -411
  29. package/src/extract/commits.ts +4 -2
  30. package/src/extract/files.ts +10 -5
  31. package/src/extract/goals.ts +7 -2
  32. package/src/hooks/before-compact.ts +210 -88
  33. package/src/om/agents/dropper/agent.ts +380 -265
  34. package/src/om/agents/dropper/coverage.ts +102 -82
  35. package/src/om/agents/observer/agent.ts +242 -206
  36. package/src/om/agents/reflector/agent.ts +212 -153
  37. package/src/om/cleanup.ts +239 -218
  38. package/src/om/clipboard.ts +59 -51
  39. package/src/om/compaction-trigger.ts +448 -333
  40. package/src/om/config.ts +13 -6
  41. package/src/om/configure-overlay.ts +518 -355
  42. package/src/om/consolidation.ts +1460 -953
  43. package/src/om/cooldown.ts +75 -65
  44. package/src/om/debug-log.ts +86 -68
  45. package/src/om/ids.ts +1 -1
  46. package/src/om/ledger/fold.ts +89 -78
  47. package/src/om/ledger/progress.ts +181 -153
  48. package/src/om/ledger/projection.ts +248 -185
  49. package/src/om/ledger/recall.ts +247 -196
  50. package/src/om/ledger/render-summary.ts +79 -50
  51. package/src/om/ledger/types.ts +146 -117
  52. package/src/om/model-budget.ts +23 -13
  53. package/src/om/pending.ts +243 -179
  54. package/src/om/provider-stream.ts +52 -7
  55. package/src/om/retryable-error.ts +12 -16
  56. package/src/om/reverse-recall.ts +97 -91
  57. package/src/om/runtime.ts +474 -375
  58. package/src/om/serialize.ts +190 -166
  59. package/src/om/status-overlay.ts +246 -195
  60. package/src/om/tokens.ts +28 -21
  61. package/src/pi-base/blackhole-settings.ts +437 -0
  62. package/src/pi-base/config-manager.ts +440 -0
  63. package/src/pi-base/config.ts +469 -0
  64. package/src/pi-base/env.ts +43 -0
  65. package/src/pi-base/paths.ts +47 -0
  66. package/src/pi-base/settings/body.ts +1648 -0
  67. package/src/pi-base/settings/fields/action.ts +43 -0
  68. package/src/pi-base/settings/fields/boolean.ts +47 -0
  69. package/src/pi-base/settings/fields/custom.ts +72 -0
  70. package/src/pi-base/settings/fields/enum.ts +310 -0
  71. package/src/pi-base/settings/fields/index.ts +46 -0
  72. package/src/pi-base/settings/fields/model.ts +452 -0
  73. package/src/pi-base/settings/fields/string.ts +527 -0
  74. package/src/pi-base/settings/fields/text.ts +115 -0
  75. package/src/pi-base/settings/frame.ts +197 -0
  76. package/src/pi-base/settings/index.ts +77 -0
  77. package/src/pi-base/settings/inline-edit.ts +313 -0
  78. package/src/pi-base/settings/modal.ts +152 -0
  79. package/src/pi-base/settings/types.ts +500 -0
  80. package/src/pi-base/settings/validate-field.ts +113 -0
  81. package/src/pi-base/shell.ts +117 -0
  82. package/src/pi-base/types.ts +6 -0
  83. package/src/pi-base/ui.ts +32 -0
  84. package/src/tools/recall.ts +347 -225
  85. package/src/types.ts +20 -3
  86. package/tsup.config.ts +23 -0
  87. package/vitest.config.ts +15 -15
@@ -5,7 +5,12 @@
5
5
  * Modified by pi-vcc-om: detects agent_end stopReason="error" in the stream
6
6
  * and throws if the API errored without collecting any drop candidates.
7
7
  */
8
- import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentTool } from "@earendil-works/pi-agent-core";
8
+ import {
9
+ agentLoop,
10
+ type AgentContext,
11
+ type AgentLoopConfig,
12
+ type AgentTool,
13
+ } from "@earendil-works/pi-agent-core";
9
14
  import type { Message, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
10
15
  import { createBridgeStreamFn } from "../../provider-stream.js";
11
16
  import { streamSimple } from "@earendil-works/pi-ai/compat";
@@ -13,319 +18,429 @@ import { Type } from "typebox";
13
18
  import type { Static } from "typebox";
14
19
  import { debugLog } from "../../debug-log.js";
15
20
  import { AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "../../model-budget.js";
16
- import { reflectionToSummaryLine, type Observation, type Reflection } from "../../ledger/index.js";
21
+ import {
22
+ reflectionToSummaryLine,
23
+ type Observation,
24
+ type Reflection,
25
+ } from "../../ledger/index.js";
17
26
  import { DROPPER_SYSTEM } from "./prompts.js";
18
27
  import {
19
- REFLECTION_COVERAGE_DROP_RANK,
20
- coverageTierForObservation,
21
- observationToDropperLine,
22
- reflectionCoverageMap,
23
- summarizeCoverageByRelevance,
24
- summarizeCoverageByRelevanceForIds,
28
+ REFLECTION_COVERAGE_DROP_RANK,
29
+ coverageTierForObservation,
30
+ observationToDropperLine,
31
+ reflectionCoverageMap,
32
+ summarizeCoverageByRelevance,
33
+ summarizeCoverageByRelevanceForIds,
25
34
  } from "./coverage.js";
26
35
 
27
36
  interface RunDropperArgs {
28
- model: Model<any>;
29
- apiKey: string;
30
- headers?: Record<string, string>;
31
- reflections: Reflection[];
32
- observations: Observation[];
33
- /** Compact summary of existing active observations for context. */
34
- existingObservationsSummary?: string;
35
- budgetTokens: number;
36
- signal?: AbortSignal;
37
- agentLoop?: typeof agentLoop;
38
- /** Optional custom stream function bypassing agentLoop's default streamSimple.
39
- * Used by the Symbol.for bridge to access native pi-ai provider registrations
40
- * from jiti-loaded consolidation agents. */
41
- streamFn?: (model: any, context: any, options: any) => any;
42
- maxTurns?: number;
43
- thinkingLevel?: ModelThinkingLevel;
37
+ model: Model<any>;
38
+ apiKey: string;
39
+ headers?: Record<string, string>;
40
+ reflections: Reflection[];
41
+ observations: Observation[];
42
+ /** Compact summary of existing active observations for context. */
43
+ existingObservationsSummary?: string;
44
+ budgetTokens: number;
45
+ /** Minimum pool fullness (fraction of budget) before dropping is allowed.
46
+ * Defaults to DROP_SKIP_FULLNESS (0.1) when unset. */
47
+ skipFullness?: number;
48
+ signal?: AbortSignal;
49
+ agentLoop?: typeof agentLoop;
50
+ /** Optional custom stream function bypassing agentLoop's default streamSimple.
51
+ * Used by the Symbol.for bridge to access native pi-ai provider registrations
52
+ * from jiti-loaded consolidation agents. */
53
+ streamFn?: (model: any, context: any, options: any) => any;
54
+ maxTurns?: number;
55
+ thinkingLevel?: ModelThinkingLevel;
44
56
  }
45
57
 
46
- const DROP_SKIP_FULLNESS = 0.10;
47
- const DROP_LOW_URGENCY_FULLNESS = 0.30;
48
- const DROP_MEDIUM_URGENCY_FULLNESS = 0.60;
49
- const DROP_MAX_FULLNESS = 1.00;
50
- const DROP_MIN_RATIO = 0.10;
51
- const DROP_MAX_RATIO = 0.50;
58
+ const DROP_SKIP_FULLNESS = 0.1;
59
+ const DROP_LOW_URGENCY_FULLNESS = 0.3;
60
+ const DROP_MEDIUM_URGENCY_FULLNESS = 0.6;
61
+ const DROP_MAX_FULLNESS = 1.0;
62
+ const DROP_MIN_RATIO = 0.1;
63
+ const DROP_MAX_RATIO = 0.5;
52
64
 
53
65
  export type DropUrgency = "low" | "medium" | "high";
54
66
 
55
67
  const RELEVANCE_DROP_RANK: Record<Observation["relevance"], number> = {
56
- low: 0,
57
- medium: 1,
58
- high: 2,
59
- critical: 3,
68
+ low: 0,
69
+ medium: 1,
70
+ high: 2,
71
+ critical: 3,
60
72
  };
61
73
 
62
74
  const DropObservationsSchema = Type.Object({
63
- ids: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
64
- reason: Type.Optional(Type.String()),
75
+ ids: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
76
+ reason: Type.Optional(Type.String()),
65
77
  });
66
78
 
67
79
  type DropObservationsArgs = Static<typeof DropObservationsSchema>;
68
80
 
69
81
  function joinOrEmpty(items: string[]): string {
70
- return items.length ? items.join("\n") : "(none yet)";
82
+ return items.length ? items.join("\n") : "(none yet)";
71
83
  }
72
84
 
73
- export function observationPoolFullness(observationTokens: number, budgetTokens: number): number {
74
- if (!Number.isFinite(observationTokens) || observationTokens <= 0) return 0;
75
- if (!Number.isFinite(budgetTokens) || budgetTokens <= 0) return 0;
76
- return observationTokens / budgetTokens;
85
+ export function observationPoolFullness(
86
+ observationTokens: number,
87
+ budgetTokens: number,
88
+ ): number {
89
+ if (!Number.isFinite(observationTokens) || observationTokens <= 0) return 0;
90
+ if (!Number.isFinite(budgetTokens) || budgetTokens <= 0) return 0;
91
+ return observationTokens / budgetTokens;
77
92
  }
78
93
 
79
94
  export function dropUrgencyForFullness(fullness: number): DropUrgency {
80
- if (fullness < DROP_LOW_URGENCY_FULLNESS) return "low";
81
- if (fullness < DROP_MEDIUM_URGENCY_FULLNESS) return "medium";
82
- return "high";
95
+ if (fullness < DROP_LOW_URGENCY_FULLNESS) return "low";
96
+ if (fullness < DROP_MEDIUM_URGENCY_FULLNESS) return "medium";
97
+ return "high";
83
98
  }
84
99
 
85
- export function maxDropCountForPool(observations: readonly Observation[], observationTokens: number, budgetTokens: number): number {
86
- const droppableCount = observations.filter((observation) => observation.relevance !== "critical").length;
87
- if (droppableCount === 0) return 0;
100
+ export function maxDropCountForPool(
101
+ observations: readonly Observation[],
102
+ observationTokens: number,
103
+ budgetTokens: number,
104
+ skipFullness: number = DROP_SKIP_FULLNESS,
105
+ ): number {
106
+ const droppableCount = observations.filter(
107
+ (observation) => observation.relevance !== "critical",
108
+ ).length;
109
+ if (droppableCount === 0) return 0;
88
110
 
89
- const fullness = observationPoolFullness(observationTokens, budgetTokens);
90
- if (fullness < DROP_SKIP_FULLNESS) return 0;
111
+ const fullness = observationPoolFullness(observationTokens, budgetTokens);
112
+ if (fullness < skipFullness) return 0;
91
113
 
92
- const cappedFullness = Math.min(DROP_MAX_FULLNESS, Math.max(DROP_SKIP_FULLNESS, fullness));
93
- const dropRatio = DROP_MIN_RATIO
94
- + ((cappedFullness - DROP_SKIP_FULLNESS) / (DROP_MAX_FULLNESS - DROP_SKIP_FULLNESS))
95
- * (DROP_MAX_RATIO - DROP_MIN_RATIO);
96
- return Math.max(1, Math.floor(droppableCount * dropRatio));
114
+ const cappedFullness = Math.min(
115
+ DROP_MAX_FULLNESS,
116
+ Math.max(skipFullness, fullness),
117
+ );
118
+ const dropRatio =
119
+ DROP_MIN_RATIO +
120
+ ((cappedFullness - skipFullness) / (DROP_MAX_FULLNESS - skipFullness)) *
121
+ (DROP_MAX_RATIO - DROP_MIN_RATIO);
122
+ return Math.max(1, Math.floor(droppableCount * dropRatio));
97
123
  }
98
124
 
99
- function relevanceCounts(observations: readonly Observation[]): Record<Observation["relevance"], number> {
100
- return observations.reduce<Record<Observation["relevance"], number>>((counts, observation) => {
101
- if (observation.relevance in counts) counts[observation.relevance]++;
102
- return counts;
103
- }, { low: 0, medium: 0, high: 0, critical: 0 });
125
+ function relevanceCounts(
126
+ observations: readonly Observation[],
127
+ ): Record<Observation["relevance"], number> {
128
+ return observations.reduce<Record<Observation["relevance"], number>>(
129
+ (counts, observation) => {
130
+ if (observation.relevance in counts) counts[observation.relevance]++;
131
+ return counts;
132
+ },
133
+ { low: 0, medium: 0, high: 0, critical: 0 },
134
+ );
104
135
  }
105
136
 
106
137
  export function normalizeDropObservationIds(
107
- ids: readonly string[] | undefined,
108
- observations: readonly Observation[],
138
+ ids: readonly string[] | undefined,
139
+ observations: readonly Observation[],
109
140
  ): string[] | undefined {
110
- if (!ids || ids.length === 0) return undefined;
111
- const allowed = new Map(observations.map((observation) => [observation.id, observation]));
112
- const result: string[] = [];
113
- const seen = new Set<string>();
114
- for (const id of ids) {
115
- const observation = allowed.get(id);
116
- if (!observation) continue;
117
- if (seen.has(id)) continue;
118
- seen.add(id);
119
- result.push(id);
120
- }
121
- return result.length > 0 ? result : undefined;
141
+ if (!ids || ids.length === 0) return undefined;
142
+ const allowed = new Map(
143
+ observations.map((observation) => [observation.id, observation]),
144
+ );
145
+ const result: string[] = [];
146
+ const seen = new Set<string>();
147
+ for (const id of ids) {
148
+ const observation = allowed.get(id);
149
+ if (!observation) continue;
150
+ if (seen.has(id)) continue;
151
+ seen.add(id);
152
+ result.push(id);
153
+ }
154
+ return result.length > 0 ? result : undefined;
122
155
  }
123
156
 
124
157
  function timestampRank(timestamp: string): number {
125
- const parsed = Date.parse(timestamp);
126
- return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
158
+ const parsed = Date.parse(timestamp);
159
+ return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
127
160
  }
128
161
 
129
162
  export function selectDropCandidates(
130
- ids: readonly string[],
131
- observations: readonly Observation[],
132
- maxDrops: number,
133
- reflections: readonly Reflection[] = [],
163
+ ids: readonly string[],
164
+ observations: readonly Observation[],
165
+ maxDrops: number,
166
+ reflections: readonly Reflection[] = [],
134
167
  ): string[] {
135
- if (maxDrops <= 0 || ids.length === 0) return [];
168
+ if (maxDrops <= 0 || ids.length === 0) return [];
136
169
 
137
- const byId = new Map(observations.map((observation) => [observation.id, observation]));
138
- const coverageById = reflectionCoverageMap(observations, reflections);
139
- const firstProposalIndex = new Map<string, number>();
140
- for (let i = 0; i < ids.length; i++) {
141
- const id = ids[i];
142
- if (!firstProposalIndex.has(id)) firstProposalIndex.set(id, i);
143
- }
170
+ const byId = new Map(
171
+ observations.map((observation) => [observation.id, observation]),
172
+ );
173
+ const coverageById = reflectionCoverageMap(observations, reflections);
174
+ const firstProposalIndex = new Map<string, number>();
175
+ for (let i = 0; i < ids.length; i++) {
176
+ const id = ids[i];
177
+ if (!firstProposalIndex.has(id)) firstProposalIndex.set(id, i);
178
+ }
144
179
 
145
- return Array.from(firstProposalIndex.entries())
146
- .map(([id, index]) => ({ id, index, observation: byId.get(id) }))
147
- .filter((candidate): candidate is { id: string; index: number; observation: Observation } =>
148
- candidate.observation !== undefined
149
- )
150
- .sort((a, b) => {
151
- const coverageDelta = REFLECTION_COVERAGE_DROP_RANK[coverageTierForObservation(a.observation, coverageById)]
152
- - REFLECTION_COVERAGE_DROP_RANK[coverageTierForObservation(b.observation, coverageById)];
153
- const relevanceDelta = RELEVANCE_DROP_RANK[a.observation.relevance] - RELEVANCE_DROP_RANK[b.observation.relevance];
154
- const aAge = timestampRank(a.observation.timestamp);
155
- const bAge = timestampRank(b.observation.timestamp);
156
- const ageDelta = aAge === bAge ? 0 : aAge - bAge;
157
- return coverageDelta || relevanceDelta || ageDelta || a.index - b.index;
158
- })
159
- .slice(0, maxDrops)
160
- .map((candidate) => candidate.id);
180
+ return Array.from(firstProposalIndex.entries())
181
+ .map(([id, index]) => ({ id, index, observation: byId.get(id) }))
182
+ .filter(
183
+ (
184
+ candidate,
185
+ ): candidate is { id: string; index: number; observation: Observation } =>
186
+ candidate.observation !== undefined,
187
+ )
188
+ .sort((a, b) => {
189
+ const coverageDelta =
190
+ REFLECTION_COVERAGE_DROP_RANK[
191
+ coverageTierForObservation(a.observation, coverageById)
192
+ ] -
193
+ REFLECTION_COVERAGE_DROP_RANK[
194
+ coverageTierForObservation(b.observation, coverageById)
195
+ ];
196
+ const relevanceDelta =
197
+ RELEVANCE_DROP_RANK[a.observation.relevance] -
198
+ RELEVANCE_DROP_RANK[b.observation.relevance];
199
+ const aAge = timestampRank(a.observation.timestamp);
200
+ const bAge = timestampRank(b.observation.timestamp);
201
+ const ageDelta = aAge === bAge ? 0 : aAge - bAge;
202
+ return coverageDelta || relevanceDelta || ageDelta || a.index - b.index;
203
+ })
204
+ .slice(0, maxDrops)
205
+ .map((candidate) => candidate.id);
161
206
  }
162
207
 
163
- export async function runDropper(args: RunDropperArgs): Promise<string[] | undefined> {
164
- const { model, apiKey, headers, reflections, observations, budgetTokens, signal } = args;
165
- if (observations.length === 0) return undefined;
208
+ export async function runDropper(
209
+ args: RunDropperArgs,
210
+ ): Promise<string[] | undefined> {
211
+ const {
212
+ model,
213
+ apiKey,
214
+ headers,
215
+ reflections,
216
+ observations,
217
+ budgetTokens,
218
+ skipFullness,
219
+ signal,
220
+ } = args;
221
+ if (observations.length === 0) return undefined;
166
222
 
167
- const observationTokens = observations.reduce((sum, observation) => sum + observation.tokenCount, 0);
168
- const fullness = observationPoolFullness(observationTokens, budgetTokens);
169
- const urgency = dropUrgencyForFullness(fullness);
170
- const maxDropsAllowed = maxDropCountForPool(observations, observationTokens, budgetTokens);
171
- const coverageById = reflectionCoverageMap(observations, reflections);
172
- const coverageSummaryByRelevance = summarizeCoverageByRelevance(observations, coverageById);
173
- debugLog("dropper.agent_start", {
174
- activeObservationCount: observations.length,
175
- reflectionCount: reflections.length,
176
- observationTokens,
177
- budgetTokens,
178
- fullness,
179
- urgency,
180
- maxDropsAllowed,
181
- relevanceCounts: relevanceCounts(observations),
182
- coverageSummaryByRelevance,
183
- });
184
- if (maxDropsAllowed <= 0) {
185
- debugLog("dropper.result", {
186
- reason: "not_over_target",
187
- toolCallCount: 0,
188
- rawRequestedIdsCount: 0,
189
- acceptedCandidateCount: 0,
190
- selectedDropsCount: 0,
191
- selectedDropTokens: 0,
192
- selectedCoverageSummaryByRelevance: summarizeCoverageByRelevanceForIds([], observations, coverageById),
193
- maxDropsAllowed,
194
- });
195
- return undefined;
196
- }
223
+ const observationTokens = observations.reduce(
224
+ (sum, observation) => sum + observation.tokenCount,
225
+ 0,
226
+ );
227
+ const fullness = observationPoolFullness(observationTokens, budgetTokens);
228
+ const urgency = dropUrgencyForFullness(fullness);
229
+ const maxDropsAllowed = maxDropCountForPool(
230
+ observations,
231
+ observationTokens,
232
+ budgetTokens,
233
+ skipFullness,
234
+ );
235
+ const coverageById = reflectionCoverageMap(observations, reflections);
236
+ const coverageSummaryByRelevance = summarizeCoverageByRelevance(
237
+ observations,
238
+ coverageById,
239
+ );
240
+ debugLog("dropper.agent_start", {
241
+ activeObservationCount: observations.length,
242
+ reflectionCount: reflections.length,
243
+ observationTokens,
244
+ budgetTokens,
245
+ fullness,
246
+ urgency,
247
+ maxDropsAllowed,
248
+ relevanceCounts: relevanceCounts(observations),
249
+ coverageSummaryByRelevance,
250
+ });
251
+ if (maxDropsAllowed <= 0) {
252
+ debugLog("dropper.result", {
253
+ reason: "not_over_target",
254
+ toolCallCount: 0,
255
+ rawRequestedIdsCount: 0,
256
+ acceptedCandidateCount: 0,
257
+ selectedDropsCount: 0,
258
+ selectedDropTokens: 0,
259
+ selectedCoverageSummaryByRelevance: summarizeCoverageByRelevanceForIds(
260
+ [],
261
+ observations,
262
+ coverageById,
263
+ ),
264
+ maxDropsAllowed,
265
+ });
266
+ return undefined;
267
+ }
197
268
 
198
- const proposedDropIds: string[] = [];
199
- const proposed = new Set<string>();
200
- const allowed = new Map(observations.map((observation) => [observation.id, observation]));
201
- let toolCallCount = 0;
202
- let rawRequestedIdsCount = 0;
203
- let missingIdsCount = 0;
204
- let criticalCandidateIdsCount = 0;
205
- let duplicateInRequestCount = 0;
206
- let duplicateInRunCount = 0;
269
+ const proposedDropIds: string[] = [];
270
+ const proposed = new Set<string>();
271
+ const allowed = new Map(
272
+ observations.map((observation) => [observation.id, observation]),
273
+ );
274
+ let toolCallCount = 0;
275
+ let rawRequestedIdsCount = 0;
276
+ let missingIdsCount = 0;
277
+ let criticalCandidateIdsCount = 0;
278
+ let duplicateInRequestCount = 0;
279
+ let duplicateInRunCount = 0;
207
280
 
208
- const dropObservations: AgentTool<typeof DropObservationsSchema> = {
209
- name: "drop_observations",
210
- label: "Drop observations",
211
- description: "Propose active observation ids that are safe to remove from compacted memory.",
212
- parameters: DropObservationsSchema,
213
- execute: async (_id, params: DropObservationsArgs) => {
214
- toolCallCount++;
215
- rawRequestedIdsCount += params.ids.length;
216
- const seenInRequest = new Set<string>();
217
- let added = 0;
218
- let requestMissingIds = 0;
219
- let requestCriticalCandidateIds = 0;
220
- let requestDuplicateIds = 0;
221
- let requestDuplicateInRunIds = 0;
222
- for (const id of params.ids) {
223
- const observation = allowed.get(id);
224
- if (!observation) {
225
- missingIdsCount++;
226
- requestMissingIds++;
227
- continue;
228
- }
229
- if (seenInRequest.has(id)) {
230
- duplicateInRequestCount++;
231
- requestDuplicateIds++;
232
- continue;
233
- }
234
- seenInRequest.add(id);
235
- if (proposed.has(id)) {
236
- duplicateInRunCount++;
237
- requestDuplicateInRunIds++;
238
- continue;
239
- }
240
- proposed.add(id);
241
- proposedDropIds.push(id);
242
- if (observation.relevance === "critical") {
243
- criticalCandidateIdsCount++;
244
- requestCriticalCandidateIds++;
245
- }
246
- added++;
247
- }
248
- debugLog("dropper.tool_call", {
249
- toolCallCount,
250
- rawRequestedIdsCount: params.ids.length,
251
- acceptedIdsCount: added,
252
- missingIdsCount: requestMissingIds,
253
- criticalCandidateIdsCount: requestCriticalCandidateIds,
254
- duplicateInRequestCount: requestDuplicateIds,
255
- duplicateInRunCount: requestDuplicateInRunIds,
256
- totalCandidates: proposedDropIds.length,
257
- maxDropsAllowed,
258
- });
259
- return {
260
- content: [{ type: "text", text: `Queued ${added} drop candidate${added === 1 ? "" : "s"}. Candidates this run: ${proposedDropIds.length}. Maximum drops allowed: ${maxDropsAllowed}.` }],
261
- details: { added, totalCandidates: proposedDropIds.length, maxDropsAllowed },
262
- };
263
- },
264
- };
281
+ const dropObservations: AgentTool<typeof DropObservationsSchema> = {
282
+ name: "drop_observations",
283
+ label: "Drop observations",
284
+ description:
285
+ "Propose active observation ids that are safe to remove from compacted memory.",
286
+ parameters: DropObservationsSchema,
287
+ execute: async (_id, params: DropObservationsArgs) => {
288
+ toolCallCount++;
289
+ rawRequestedIdsCount += params.ids.length;
290
+ const seenInRequest = new Set<string>();
291
+ let added = 0;
292
+ let requestMissingIds = 0;
293
+ let requestCriticalCandidateIds = 0;
294
+ let requestDuplicateIds = 0;
295
+ let requestDuplicateInRunIds = 0;
296
+ for (const id of params.ids) {
297
+ const observation = allowed.get(id);
298
+ if (!observation) {
299
+ missingIdsCount++;
300
+ requestMissingIds++;
301
+ continue;
302
+ }
303
+ if (seenInRequest.has(id)) {
304
+ duplicateInRequestCount++;
305
+ requestDuplicateIds++;
306
+ continue;
307
+ }
308
+ seenInRequest.add(id);
309
+ if (proposed.has(id)) {
310
+ duplicateInRunCount++;
311
+ requestDuplicateInRunIds++;
312
+ continue;
313
+ }
314
+ proposed.add(id);
315
+ proposedDropIds.push(id);
316
+ if (observation.relevance === "critical") {
317
+ criticalCandidateIdsCount++;
318
+ requestCriticalCandidateIds++;
319
+ }
320
+ added++;
321
+ }
322
+ debugLog("dropper.tool_call", {
323
+ toolCallCount,
324
+ rawRequestedIdsCount: params.ids.length,
325
+ acceptedIdsCount: added,
326
+ missingIdsCount: requestMissingIds,
327
+ criticalCandidateIdsCount: requestCriticalCandidateIds,
328
+ duplicateInRequestCount: requestDuplicateIds,
329
+ duplicateInRunCount: requestDuplicateInRunIds,
330
+ totalCandidates: proposedDropIds.length,
331
+ maxDropsAllowed,
332
+ });
333
+ return {
334
+ content: [
335
+ {
336
+ type: "text",
337
+ text: `Queued ${added} drop candidate${added === 1 ? "" : "s"}. Candidates this run: ${proposedDropIds.length}. Maximum drops allowed: ${maxDropsAllowed}.`,
338
+ },
339
+ ],
340
+ details: {
341
+ added,
342
+ totalCandidates: proposedDropIds.length,
343
+ maxDropsAllowed,
344
+ },
345
+ };
346
+ },
347
+ };
265
348
 
266
- const fullnessPercent = Math.round(fullness * 100);
267
- const existingObservationsContext = args.existingObservationsSummary
268
- ? `EXISTING ACTIVE OBSERVATIONS (for context only — these are NOT candidates for dropping):\n${args.existingObservationsSummary}\n\n`
269
- : '';
349
+ const fullnessPercent = Math.round(fullness * 100);
350
+ const existingObservationsContext = args.existingObservationsSummary
351
+ ? `EXISTING ACTIVE OBSERVATIONS (for context only — these are NOT candidates for dropping):\n${args.existingObservationsSummary}\n\n`
352
+ : "";
270
353
 
271
- const userText = `CURRENT REFLECTIONS:\n${joinOrEmpty(reflections.map(reflectionToSummaryLine))}\n\n${existingObservationsContext}NEW OBSERVATIONS TO EVALUATE FOR DROPPING:\n${joinOrEmpty(observations.map((observation) => observationToDropperLine(observation, coverageTierForObservation(observation, coverageById))))}\n\nObservation pool pressure: ~${observationTokens.toLocaleString()} tokens; target budget: ~${budgetTokens.toLocaleString()} tokens; fullness: ~${fullnessPercent.toLocaleString()}%.\nDrop urgency: ${urgency}.\nMaximum drops allowed this run: ${maxDropsAllowed.toLocaleString()} observation${maxDropsAllowed === 1 ? "" : "s"}.\nThis maximum is a hard upper bound, not a target. Drop fewer or none if fewer observations are clearly safe.`;
272
- const prompts: Message[] = [{ role: "user", content: [{ type: "text", text: userText }], timestamp: Date.now() }];
273
- const context: AgentContext = { systemPrompt: DROPPER_SYSTEM, messages: [], tools: [dropObservations as AgentTool<any>] };
274
- const reasoning = (model as { reasoning?: unknown }).reasoning;
275
- const thinkingLevel = args.thinkingLevel ?? "low";
276
- const effectiveMaxTurns = args.maxTurns && args.maxTurns > 0 ? args.maxTurns : undefined;
277
- let turnCount = 0;
278
- const config: AgentLoopConfig = {
279
- model,
280
- apiKey,
281
- headers,
282
- maxTokens: boundedMaxTokens(model, AGENT_LOOP_MAX_TOKENS),
283
- convertToLlm: (msgs) => msgs as Message[],
284
- toolExecution: "sequential",
285
- ...(reasoning && thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
286
- ...(effectiveMaxTurns !== undefined ? { shouldStopAfterTurn: () => ++turnCount >= effectiveMaxTurns } : {}),
287
- };
354
+ const userText = `CURRENT REFLECTIONS:\n${joinOrEmpty(reflections.map(reflectionToSummaryLine))}\n\n${existingObservationsContext}NEW OBSERVATIONS TO EVALUATE FOR DROPPING:\n${joinOrEmpty(observations.map((observation) => observationToDropperLine(observation, coverageTierForObservation(observation, coverageById))))}\n\nObservation pool pressure: ~${observationTokens.toLocaleString()} tokens; target budget: ~${budgetTokens.toLocaleString()} tokens; fullness: ~${fullnessPercent.toLocaleString()}%.\nDrop urgency: ${urgency}.\nMaximum drops allowed this run: ${maxDropsAllowed.toLocaleString()} observation${maxDropsAllowed === 1 ? "" : "s"}.\nThis maximum is a hard upper bound, not a target. Drop fewer or none if fewer observations are clearly safe.`;
355
+ const prompts: Message[] = [
356
+ {
357
+ role: "user",
358
+ content: [{ type: "text", text: userText }],
359
+ timestamp: Date.now(),
360
+ },
361
+ ];
362
+ const context: AgentContext = {
363
+ systemPrompt: DROPPER_SYSTEM,
364
+ messages: [],
365
+ tools: [dropObservations as AgentTool<any>],
366
+ };
367
+ const reasoning = (model as { reasoning?: unknown }).reasoning;
368
+ const thinkingLevel = args.thinkingLevel ?? "low";
369
+ const effectiveMaxTurns =
370
+ args.maxTurns && args.maxTurns > 0 ? args.maxTurns : undefined;
371
+ let turnCount = 0;
372
+ const config: AgentLoopConfig = {
373
+ model,
374
+ apiKey,
375
+ headers,
376
+ maxTokens: boundedMaxTokens(model, AGENT_LOOP_MAX_TOKENS),
377
+ convertToLlm: (msgs) => msgs as Message[],
378
+ toolExecution: "sequential",
379
+ ...(reasoning && thinkingLevel !== "off"
380
+ ? { reasoning: thinkingLevel }
381
+ : {}),
382
+ ...(effectiveMaxTurns !== undefined
383
+ ? { shouldStopAfterTurn: () => ++turnCount >= effectiveMaxTurns }
384
+ : {}),
385
+ };
288
386
 
289
- const loop = args.agentLoop ?? agentLoop;
290
- // ── Bridge stream function ──
291
- const bridgeStreamFn = createBridgeStreamFn(streamSimple);
292
- const streamFn = args.streamFn ?? bridgeStreamFn;
293
- const stream = loop(prompts, context, config, signal, streamFn);
294
- let agentError: string | undefined;
295
- for await (const event of stream) {
296
- // Tool execution collects candidate ids.
297
- if (event.type === "agent_end") {
298
- const msgs = ((event as any).messages || []) as Array<{ stopReason?: string; errorMessage?: string }>;
299
- const lastMsg = msgs[msgs.length - 1];
300
- if (lastMsg?.stopReason === "error") {
301
- agentError = lastMsg.errorMessage ?? "Unknown API error";
302
- }
303
- }
304
- }
305
- await stream.result();
306
- if (agentError && proposedDropIds.length === 0) throw new Error(`Dropper API error: ${agentError}`);
307
- const droppedIds = selectDropCandidates(proposedDropIds, observations, maxDropsAllowed, reflections);
308
- const reason = droppedIds.length > 0
309
- ? "selected_nonempty"
310
- : toolCallCount === 0
311
- ? "no_tool_call"
312
- : proposedDropIds.length === 0
313
- ? "all_filtered"
314
- : "selected_empty";
315
- const selectedDropTokens = droppedIds.reduce((sum, id) => sum + (allowed.get(id)?.tokenCount ?? 0), 0);
316
- debugLog("dropper.result", {
317
- reason,
318
- toolCallCount,
319
- rawRequestedIdsCount,
320
- missingIdsCount,
321
- criticalCandidateIdsCount,
322
- duplicateInRequestCount,
323
- duplicateInRunCount,
324
- acceptedCandidateCount: proposedDropIds.length,
325
- selectedDropsCount: droppedIds.length,
326
- selectedDropTokens,
327
- selectedCoverageSummaryByRelevance: summarizeCoverageByRelevanceForIds(droppedIds, observations, coverageById),
328
- maxDropsAllowed,
329
- });
330
- return droppedIds.length > 0 ? droppedIds : undefined;
387
+ const loop = args.agentLoop ?? agentLoop;
388
+ // ── Bridge stream function ──
389
+ const bridgeStreamFn = createBridgeStreamFn(streamSimple);
390
+ const streamFn = args.streamFn ?? bridgeStreamFn;
391
+ const stream = loop(prompts, context, config, signal, streamFn);
392
+ let agentError: string | undefined;
393
+ for await (const event of stream) {
394
+ // Tool execution collects candidate ids.
395
+ if (event.type === "agent_end") {
396
+ const msgs = ((event as any).messages || []) as Array<{
397
+ stopReason?: string;
398
+ errorMessage?: string;
399
+ }>;
400
+ const lastMsg = msgs[msgs.length - 1];
401
+ if (lastMsg?.stopReason === "error") {
402
+ agentError = lastMsg.errorMessage ?? "Unknown API error";
403
+ }
404
+ }
405
+ }
406
+ await stream.result();
407
+ if (agentError && proposedDropIds.length === 0)
408
+ throw new Error(`Dropper API error: ${agentError}`);
409
+ const droppedIds = selectDropCandidates(
410
+ proposedDropIds,
411
+ observations,
412
+ maxDropsAllowed,
413
+ reflections,
414
+ );
415
+ const reason =
416
+ droppedIds.length > 0
417
+ ? "selected_nonempty"
418
+ : toolCallCount === 0
419
+ ? "no_tool_call"
420
+ : proposedDropIds.length === 0
421
+ ? "all_filtered"
422
+ : "selected_empty";
423
+ const selectedDropTokens = droppedIds.reduce(
424
+ (sum, id) => sum + (allowed.get(id)?.tokenCount ?? 0),
425
+ 0,
426
+ );
427
+ debugLog("dropper.result", {
428
+ reason,
429
+ toolCallCount,
430
+ rawRequestedIdsCount,
431
+ missingIdsCount,
432
+ criticalCandidateIdsCount,
433
+ duplicateInRequestCount,
434
+ duplicateInRunCount,
435
+ acceptedCandidateCount: proposedDropIds.length,
436
+ selectedDropsCount: droppedIds.length,
437
+ selectedDropTokens,
438
+ selectedCoverageSummaryByRelevance: summarizeCoverageByRelevanceForIds(
439
+ droppedIds,
440
+ observations,
441
+ coverageById,
442
+ ),
443
+ maxDropsAllowed,
444
+ });
445
+ return droppedIds.length > 0 ? droppedIds : undefined;
331
446
  }