@pi-unipi/compactor 0.1.7 → 0.2.2

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.
@@ -3,6 +3,14 @@
3
3
  *
4
4
  * Each tool is registered via pi.registerTool() with proper TypeBox schemas
5
5
  * so the LLM can discover and invoke them.
6
+ *
7
+ * New naming convention (v0.2.0):
8
+ * compact, session_recall, sandbox, sandbox_file, sandbox_batch,
9
+ * content_index, content_search, content_fetch, compactor_stats, compactor_doctor
10
+ *
11
+ * Old names kept as deprecated aliases for backward compatibility:
12
+ * vcc_recall, ctx_execute, ctx_execute_file, ctx_batch_execute,
13
+ * ctx_index, ctx_search, ctx_fetch_and_index, ctx_stats, ctx_doctor
6
14
  */
7
15
 
8
16
  import { Type, type Static } from "@sinclair/typebox";
@@ -17,9 +25,10 @@ import { ctxSearch, type CtxSearchInput } from "./ctx-search.js";
17
25
  import { ctxFetchAndIndex, type CtxFetchAndIndexInput } from "./ctx-fetch-and-index.js";
18
26
  import { ctxStats, type CtxStatsResult } from "./ctx-stats.js";
19
27
  import { ctxDoctor, type DoctorResult } from "./ctx-doctor.js";
28
+ import { contextBudgetTool } from "./context-budget.js";
20
29
  import type { SessionDB } from "../session/db.js";
21
30
  import type { ContentStore } from "../store/index.js";
22
- import type { NormalizedBlock } from "../types.js";
31
+ import type { NormalizedBlock, RuntimeCounters } from "../types.js";
23
32
 
24
33
  // --- TypeBox Schemas for each tool ---
25
34
 
@@ -37,9 +46,11 @@ const LanguageSchema = Type.Union([
37
46
  Type.Literal("elixir"),
38
47
  ]);
39
48
 
40
- const CompactParams = Type.Object({});
49
+ const CompactParams = Type.Object({
50
+ dryRun: Type.Optional(Type.Boolean({ description: "If true, report what would be compacted without actually compacting" })),
51
+ });
41
52
 
42
- const VccRecallParams = Type.Object({
53
+ const RecallParams = Type.Object({
43
54
  query: Type.String({ description: "Search query for session history" }),
44
55
  mode: Type.Optional(Type.Union([Type.Literal("bm25"), Type.Literal("regex")], {
45
56
  description: "Search mode: bm25 (default) or regex fallback",
@@ -49,19 +60,19 @@ const VccRecallParams = Type.Object({
49
60
  expand: Type.Optional(Type.Boolean({ description: "Return full message content for hits" })),
50
61
  });
51
62
 
52
- const CtxExecuteParams = Type.Object({
63
+ const SandboxParams = Type.Object({
53
64
  language: LanguageSchema,
54
65
  code: Type.String({ description: "Code to execute in the sandbox" }),
55
66
  timeout: Type.Optional(Type.Number({ description: "Timeout in ms (default 30000)", minimum: 1000 })),
56
67
  });
57
68
 
58
- const CtxExecuteFileParams = Type.Object({
69
+ const SandboxFileParams = Type.Object({
59
70
  language: LanguageSchema,
60
71
  path: Type.String({ description: "Path to file to execute" }),
61
72
  timeout: Type.Optional(Type.Number({ description: "Timeout in ms (default 30000)", minimum: 1000 })),
62
73
  });
63
74
 
64
- const CtxBatchExecuteParams = Type.Object({
75
+ const SandboxBatchParams = Type.Object({
65
76
  items: Type.Array(
66
77
  Type.Union([
67
78
  Type.Object({
@@ -80,7 +91,7 @@ const CtxBatchExecuteParams = Type.Object({
80
91
  ),
81
92
  });
82
93
 
83
- const CtxIndexParams = Type.Object({
94
+ const ContentIndexParams = Type.Object({
84
95
  label: Type.String({ description: "Label for the indexed content" }),
85
96
  content: Type.Optional(Type.String({ description: "Content to index (or use filePath)" })),
86
97
  filePath: Type.Optional(Type.String({ description: "Path to file to index" })),
@@ -92,21 +103,21 @@ const CtxIndexParams = Type.Object({
92
103
  chunkSize: Type.Optional(Type.Number({ description: "Chunk size in characters", minimum: 100 })),
93
104
  });
94
105
 
95
- const CtxSearchParams = Type.Object({
106
+ const ContentSearchParams = Type.Object({
96
107
  query: Type.String({ description: "Search query against indexed content" }),
97
108
  limit: Type.Optional(Type.Number({ description: "Max results (default 10)", minimum: 1 })),
98
109
  offset: Type.Optional(Type.Number({ description: "Pagination offset", minimum: 0 })),
99
110
  });
100
111
 
101
- const CtxFetchAndIndexParams = Type.Object({
112
+ const ContentFetchParams = Type.Object({
102
113
  url: Type.String({ description: "URL to fetch, convert to markdown, and index" }),
103
114
  label: Type.Optional(Type.String({ description: "Label for the indexed content" })),
104
115
  chunkSize: Type.Optional(Type.Number({ description: "Chunk size in characters", minimum: 100 })),
105
116
  });
106
117
 
107
- const CtxStatsParams = Type.Object({});
118
+ const StatsParams = Type.Object({});
108
119
 
109
- const CtxDoctorParams = Type.Object({});
120
+ const DoctorParams = Type.Object({});
110
121
 
111
122
  // --- Helpers ---
112
123
 
@@ -125,6 +136,23 @@ function jsonResult(data: unknown, label?: string): any {
125
136
  };
126
137
  }
127
138
 
139
+ /** Log a deprecation warning when old tool names are used. */
140
+ function deprecationLog(_oldName: string, _newName: string): void {
141
+ // Deprecation logging disabled — was writing to stdout causing TUI rendering issues.
142
+ }
143
+
144
+ // --- Old schema names for backward compat aliases ---
145
+
146
+ const VccRecallParams = RecallParams;
147
+ const CtxExecuteParams = SandboxParams;
148
+ const CtxExecuteFileParams = SandboxFileParams;
149
+ const CtxBatchExecuteParams = SandboxBatchParams;
150
+ const CtxIndexParams = ContentIndexParams;
151
+ const CtxSearchParams = ContentSearchParams;
152
+ const CtxFetchAndIndexParams = ContentFetchParams;
153
+ const CtxStatsParams = StatsParams;
154
+ const CtxDoctorParams = DoctorParams;
155
+
128
156
  // --- Registration ---
129
157
 
130
158
  export interface CompactorToolDeps {
@@ -132,6 +160,7 @@ export interface CompactorToolDeps {
132
160
  contentStore: ContentStore | null;
133
161
  getSessionId: () => string;
134
162
  getBlocks: () => NormalizedBlock[];
163
+ getCounters?: () => RuntimeCounters;
135
164
  }
136
165
 
137
166
  /**
@@ -139,222 +168,228 @@ export interface CompactorToolDeps {
139
168
  * Call this during session_start after services are initialized.
140
169
  */
141
170
  export function registerCompactorTools(pi: ExtensionAPI, deps: CompactorToolDeps): void {
142
- // 1. compact — trigger manual compaction
143
- pi.registerTool({
171
+ // 1. compact — trigger manual compaction (with optional dryRun)
172
+ pi.registerTool(({
144
173
  name: "compact",
145
174
  label: "Compact",
146
- description: "Trigger manual context compaction. Reduces session history while preserving continuity.",
175
+ description: "Trigger manual context compaction. Reduces session history while preserving continuity. Use dryRun:true to preview without compacting.",
147
176
  parameters: CompactParams,
148
- async execute(): Promise<any> {
177
+ async execute(_toolCallId: string, params: any): Promise<any> {
178
+ if (params.dryRun) {
179
+ const blocks = deps.getBlocks();
180
+ const totalMessages = blocks.length;
181
+ const estimated = Math.round(totalMessages * 0.15);
182
+ return jsonResult({
183
+ dryRun: true,
184
+ wouldCompact: totalMessages,
185
+ estimatedKept: estimated,
186
+ message: `Would compact ${totalMessages} messages → ~${estimated} kept.`,
187
+ }, "Dry run — no compaction performed");
188
+ }
189
+ const c = deps.getCounters?.();
190
+ if (c) { c.compactions++; }
149
191
  const result = compactTool();
150
192
  return jsonResult(result, "Compaction triggered");
151
193
  },
152
- });
153
-
154
- // 2. vcc_recall — search session history
155
- pi.registerTool({
156
- name: "vcc_recall",
157
- label: "Recall",
158
- description:
159
- "Search session history using BM25 or regex. Find previous goals, files, commits, and context.",
160
- parameters: VccRecallParams,
161
- async execute(_toolCallId, params: Static<typeof VccRecallParams>): Promise<any> {
162
- const blocks = deps.getBlocks();
163
- const input: RecallInput = {
164
- query: params.query,
165
- mode: params.mode,
166
- limit: params.limit,
167
- offset: params.offset,
168
- expand: params.expand,
169
- };
170
- const result = vccRecall(blocks, input);
171
- if (result.hits.length === 0) {
172
- return textResult(`No results found for "${result.query}".`);
194
+ } as any));
195
+
196
+ // 2. session_recall (new) / vcc_recall (deprecated) — search session history
197
+ const recallExec = async (_toolCallId: string, params: any): Promise<any> => {
198
+ const c = deps.getCounters?.();
199
+ if (c) { c.recallQueries++; }
200
+ const blocks = deps.getBlocks();
201
+ const input: RecallInput = {
202
+ query: params.query,
203
+ mode: params.mode,
204
+ limit: params.limit,
205
+ offset: params.offset,
206
+ expand: params.expand,
207
+ };
208
+ const result = vccRecall(blocks, input);
209
+ if (result.hits.length === 0) {
210
+ return textResult(`No results found for "${result.query}".`);
211
+ }
212
+ const lines = result.hits.map(
213
+ (h, i) =>
214
+ `[${i + 1}/${result.total}] score=${h.score.toFixed(2)} kind=${h.kind}\n${h.text}`,
215
+ );
216
+ return textResult(
217
+ `Found ${result.total} results for "${result.query}":\n\n${lines.join("\n\n")}`,
218
+ result as unknown as Record<string, unknown>,
219
+ );
220
+ };
221
+ pi.registerTool({ name: "session_recall", label: "Session Recall", description: "Search session history using BM25 or regex. Find previous goals, files, commits, and context.", parameters: RecallParams, execute: recallExec } as any);
222
+ pi.registerTool({ name: "vcc_recall", label: "Session Recall", description: "Search session history using BM25 or regex. (DEPRECATED: use session_recall instead)", parameters: VccRecallParams, async execute(tcId: string, p: any) { deprecationLog("vcc_recall", "session_recall"); return recallExec(tcId, p); } } as any);
223
+
224
+ // 3. sandbox (new) / ctx_execute (deprecated) — run code in sandbox
225
+ const sandboxExec = async (_toolCallId: string, params: any): Promise<any> => {
226
+ try {
227
+ const c = deps.getCounters?.();
228
+ if (c) { c.sandboxRuns++; }
229
+ const result = await ctxExecute(params as CtxExecuteInput);
230
+ const parts: string[] = [];
231
+ if (result.stdout) parts.push(result.stdout);
232
+ if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
233
+ if (result.timedOut) parts.push("[timed out]");
234
+ if (result.exitCode !== 0) parts.push(`[exit code: ${result.exitCode}]`);
235
+ return textResult(parts.join("\n") || "(no output)", result as unknown as Record<string, unknown>);
236
+ } catch (err) {
237
+ return textResult(`Execution error: ${err}`, { error: true });
238
+ }
239
+ };
240
+ pi.registerTool({ name: "sandbox", label: "Sandbox", description: "Run code in a sandboxed environment. Supports 11 languages. Only stdout enters context.", parameters: SandboxParams, execute: sandboxExec } as any);
241
+ pi.registerTool({ name: "ctx_execute", label: "Sandbox", description: "Run code in sandbox. (DEPRECATED: use sandbox instead)", parameters: CtxExecuteParams, async execute(tcId: string, p: any) { deprecationLog("ctx_execute", "sandbox"); return sandboxExec(tcId, p); } } as any);
242
+
243
+ // 4. sandbox_file (new) / ctx_execute_file (deprecated) — execute file
244
+ const sandboxFileExec = async (_toolCallId: string, params: any): Promise<any> => {
245
+ try {
246
+ const c = deps.getCounters?.();
247
+ if (c) { c.sandboxRuns++; }
248
+ const result = await ctxExecuteFile(params as CtxExecuteFileInput);
249
+ const parts: string[] = [];
250
+ if (result.stdout) parts.push(result.stdout);
251
+ if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
252
+ if (result.timedOut) parts.push("[timed out]");
253
+ return textResult(parts.join("\n") || "(no output)", result as unknown as Record<string, unknown>);
254
+ } catch (err) {
255
+ return textResult(`Execution error: ${err}`, { error: true });
256
+ }
257
+ };
258
+ pi.registerTool({ name: "sandbox_file", label: "Sandbox File", description: "Execute a file in the sandbox. File content is injected as FILE_CONTENT variable.", parameters: SandboxFileParams, execute: sandboxFileExec } as any);
259
+ pi.registerTool({ name: "ctx_execute_file", label: "Sandbox File", description: "Execute file in sandbox. (DEPRECATED: use sandbox_file instead)", parameters: CtxExecuteFileParams, async execute(tcId: string, p: any) { deprecationLog("ctx_execute_file", "sandbox_file"); return sandboxFileExec(tcId, p); } } as any);
260
+
261
+ // 5. sandbox_batch (new) / ctx_batch_execute (deprecated) — atomic batch
262
+ const sandboxBatchExec = async (_toolCallId: string, params: any): Promise<any> => {
263
+ try {
264
+ const c = deps.getCounters?.();
265
+ if (c) { c.sandboxRuns++; c.searchQueries++; }
266
+ const result = await ctxBatchExecute(deps.contentStore!, params.items as BatchItem[]);
267
+ const summaries = result.results.map((r, i) => {
268
+ if (r.type === "execute") {
269
+ const s = r.result.stdout?.slice(0, 200) || "(no output)";
270
+ return `[${i}] execute → ${r.result.exitCode === 0 ? "ok" : "fail"}: ${s}`;
271
+ }
272
+ return `[${i}] search → ${r.results.length} results`;
273
+ });
274
+ return textResult(`Batch results (${result.results.length} items):\n${summaries.join("\n")}`, result as unknown as Record<string, unknown>);
275
+ } catch (err) {
276
+ return textResult(`Batch error: ${err}`, { error: true });
277
+ }
278
+ };
279
+ pi.registerTool({ name: "sandbox_batch", label: "Sandbox Batch", description: "Run multiple code executions and searches atomically as a batch.", parameters: SandboxBatchParams, execute: sandboxBatchExec } as any);
280
+ pi.registerTool({ name: "ctx_batch_execute", label: "Sandbox Batch", description: "Run batch operations. (DEPRECATED: use sandbox_batch instead)", parameters: CtxBatchExecuteParams, async execute(tcId: string, p: any) { deprecationLog("ctx_batch_execute", "sandbox_batch"); return sandboxBatchExec(tcId, p); } } as any);
281
+
282
+ // 6. content_index (new) / ctx_index (deprecated) — index content into FTS5
283
+ const contentIndexExec = async (_toolCallId: string, params: any): Promise<any> => {
284
+ try {
285
+ const result = await ctxIndex(deps.contentStore!, params as CtxIndexInput);
286
+ return textResult(
287
+ `Indexed "${result.label}": ${result.totalChunks} chunks (${result.codeChunks} code)`,
288
+ result as unknown as Record<string, unknown>,
289
+ );
290
+ } catch (err) {
291
+ return textResult(`Index error: ${err}`, { error: true });
292
+ }
293
+ };
294
+ pi.registerTool({ name: "content_index", label: "Content Index", description: "Chunk content or a file and index into FTS5 for fast search.", parameters: ContentIndexParams, execute: contentIndexExec } as any);
295
+ pi.registerTool({ name: "ctx_index", label: "Content Index", description: "Index content into FTS5. (DEPRECATED: use content_index instead)", parameters: CtxIndexParams, async execute(tcId: string, p: any) { deprecationLog("ctx_index", "content_index"); return contentIndexExec(tcId, p); } } as any);
296
+
297
+ // 7. content_search (new) / ctx_search (deprecated) — query FTS5 content store
298
+ const contentSearchExec = async (_toolCallId: string, params: any): Promise<any> => {
299
+ try {
300
+ const c = deps.getCounters?.();
301
+ if (c) { c.searchQueries++; }
302
+ const results = await ctxSearch(deps.contentStore!, params as CtxSearchInput);
303
+ if (results.length === 0) {
304
+ return textResult(`No results for "${params.query}".`);
173
305
  }
174
- const lines = result.hits.map(
175
- (h, i) =>
176
- `[${i + 1}/${result.total}] score=${h.score.toFixed(2)} kind=${h.kind}\n${h.text}`,
306
+ const lines = results.map(
307
+ (r, i) =>
308
+ `[${i + 1}] ${r.title} (rank: ${r.rank.toFixed(3)})\n${r.content.slice(0, 300)}`,
309
+ );
310
+ return textResult(
311
+ `Found ${results.length} results:\n\n${lines.join("\n\n")}`,
312
+ { results } as unknown as Record<string, unknown>,
177
313
  );
314
+ } catch (err) {
315
+ return textResult(`Search error: ${err}`, { error: true });
316
+ }
317
+ };
318
+ pi.registerTool({ name: "content_search", label: "Content Search", description: "Search indexed content using FTS5 full-text search.", parameters: ContentSearchParams, execute: contentSearchExec } as any);
319
+ pi.registerTool({ name: "ctx_search", label: "Content Search", description: "Search indexed content. (DEPRECATED: use content_search instead)", parameters: CtxSearchParams, async execute(tcId: string, p: any) { deprecationLog("ctx_search", "content_search"); return contentSearchExec(tcId, p); } } as any);
320
+
321
+ // 8. content_fetch (new) / ctx_fetch_and_index (deprecated) — fetch URL
322
+ const contentFetchExec = async (_toolCallId: string, params: any): Promise<any> => {
323
+ try {
324
+ const result = await ctxFetchAndIndex(deps.contentStore!, params as CtxFetchAndIndexInput);
178
325
  return textResult(
179
- `Found ${result.total} results for "${result.query}":\n\n${lines.join("\n\n")}`,
326
+ `Fetched and indexed "${result.label}": ${result.totalChunks} chunks`,
180
327
  result as unknown as Record<string, unknown>,
181
328
  );
182
- },
183
- });
184
-
185
- // 3. ctx_execute — run code in sandbox
186
- pi.registerTool({
187
- name: "ctx_execute",
188
- label: "Execute",
189
- description:
190
- "Run code in a sandboxed environment. Supports 11 languages. Only stdout enters context.",
191
- parameters: CtxExecuteParams,
192
- async execute(_toolCallId, params: Static<typeof CtxExecuteParams>): Promise<any> {
193
- try {
194
- const result = await ctxExecute(params as CtxExecuteInput);
195
- const parts: string[] = [];
196
- if (result.stdout) parts.push(result.stdout);
197
- if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
198
- if (result.timedOut) parts.push("[timed out]");
199
- if (result.exitCode !== 0) parts.push(`[exit code: ${result.exitCode}]`);
200
- return textResult(parts.join("\n") || "(no output)", result as unknown as Record<string, unknown>);
201
- } catch (err) {
202
- return textResult(`Execution error: ${err}`, { error: true });
203
- }
204
- },
205
- });
206
-
207
- // 4. ctx_execute_file — execute file with FILE_CONTENT
208
- pi.registerTool({
209
- name: "ctx_execute_file",
210
- label: "Execute File",
211
- description: "Execute a file in the sandbox. File content is injected as FILE_CONTENT variable.",
212
- parameters: CtxExecuteFileParams,
213
- async execute(_toolCallId, params: Static<typeof CtxExecuteFileParams>): Promise<any> {
214
- try {
215
- const result = await ctxExecuteFile(params as CtxExecuteFileInput);
216
- const parts: string[] = [];
217
- if (result.stdout) parts.push(result.stdout);
218
- if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
219
- if (result.timedOut) parts.push("[timed out]");
220
- return textResult(parts.join("\n") || "(no output)", result as unknown as Record<string, unknown>);
221
- } catch (err) {
222
- return textResult(`Execution error: ${err}`, { error: true });
223
- }
224
- },
225
- });
226
-
227
- // 5. ctx_batch_execute — atomic batch
228
- pi.registerTool({
229
- name: "ctx_batch_execute",
230
- label: "Batch Execute",
231
- description: "Run multiple code executions and searches atomically as a batch.",
232
- parameters: CtxBatchExecuteParams,
233
- async execute(_toolCallId, params: Static<typeof CtxBatchExecuteParams>): Promise<any> {
234
- try {
235
- const result = await ctxBatchExecute(params.items as BatchItem[]);
236
- const summaries = result.results.map((r, i) => {
237
- if (r.type === "execute") {
238
- const s = r.result.stdout?.slice(0, 200) || "(no output)";
239
- return `[${i}] execute → ${r.result.exitCode === 0 ? "ok" : "fail"}: ${s}`;
240
- }
241
- return `[${i}] search → ${r.results.length} results`;
242
- });
243
- return textResult(`Batch results (${result.results.length} items):\n${summaries.join("\n")}`, result as unknown as Record<string, unknown>);
244
- } catch (err) {
245
- return textResult(`Batch error: ${err}`, { error: true });
246
- }
247
- },
248
- });
249
-
250
- // 6. ctx_index — index content into FTS5
251
- pi.registerTool({
252
- name: "ctx_index",
253
- label: "Index",
254
- description: "Chunk content or a file and index into FTS5 for fast search.",
255
- parameters: CtxIndexParams,
256
- async execute(_toolCallId, params: Static<typeof CtxIndexParams>): Promise<any> {
257
- try {
258
- const result = await ctxIndex(params as CtxIndexInput);
259
- return textResult(
260
- `Indexed "${result.label}": ${result.totalChunks} chunks (${result.codeChunks} code)`,
261
- result as unknown as Record<string, unknown>,
262
- );
263
- } catch (err) {
264
- return textResult(`Index error: ${err}`, { error: true });
265
- }
266
- },
267
- });
268
-
269
- // 7. ctx_search — query FTS5 content store
270
- pi.registerTool({
271
- name: "ctx_search",
272
- label: "Search",
273
- description: "Search indexed content using FTS5 full-text search.",
274
- parameters: CtxSearchParams,
275
- async execute(_toolCallId, params: Static<typeof CtxSearchParams>): Promise<any> {
276
- try {
277
- const results = await ctxSearch(params as CtxSearchInput);
278
- if (results.length === 0) {
279
- return textResult(`No results for "${params.query}".`);
280
- }
281
- const lines = results.map(
282
- (r, i) =>
283
- `[${i + 1}] ${r.title} (rank: ${r.rank.toFixed(3)})\n${r.content.slice(0, 300)}`,
284
- );
285
- return textResult(
286
- `Found ${results.length} results:\n\n${lines.join("\n\n")}`,
287
- { results } as unknown as Record<string, unknown>,
288
- );
289
- } catch (err) {
290
- return textResult(`Search error: ${err}`, { error: true });
291
- }
292
- },
293
- });
294
-
295
- // 8. ctx_fetch_and_index — fetch URL → markdown → index
296
- pi.registerTool({
297
- name: "ctx_fetch_and_index",
298
- label: "Fetch & Index",
299
- description: "Fetch a URL, convert to markdown, and index into FTS5 content store.",
300
- parameters: CtxFetchAndIndexParams,
301
- async execute(_toolCallId, params: Static<typeof CtxFetchAndIndexParams>): Promise<any> {
302
- try {
303
- const result = await ctxFetchAndIndex(params as CtxFetchAndIndexInput);
304
- return textResult(
305
- `Fetched and indexed "${result.label}": ${result.totalChunks} chunks`,
306
- result as unknown as Record<string, unknown>,
307
- );
308
- } catch (err) {
309
- return textResult(`Fetch error: ${err}`, { error: true });
310
- }
311
- },
312
- });
313
-
314
- // 9. ctx_stats — context savings dashboard
315
- pi.registerTool({
316
- name: "ctx_stats",
317
- label: "Stats",
318
- description: "Show context savings dashboard — session events, compactions, indexed content.",
319
- parameters: CtxStatsParams,
320
- async execute(): Promise<any> {
321
- try {
322
- const result = await ctxStats(deps.sessionDB, deps.contentStore!, deps.getSessionId());
323
- const lines = [
324
- `📊 Compactor Stats`,
325
- `Session events: ${result.sessionEvents}`,
326
- `Compactions: ${result.compactions}`,
327
- `Tokens saved: ${result.tokensSaved}`,
328
- `Indexed docs: ${result.indexedDocs} (${result.indexedChunks} chunks)`,
329
- `Sandbox runs: ${result.sandboxRuns}`,
330
- `Search queries: ${result.searchQueries}`,
331
- ];
332
- return textResult(lines.join("\n"), result as unknown as Record<string, unknown>);
333
- } catch (err) {
334
- return textResult(`Stats error: ${err}`, { error: true });
335
- }
336
- },
337
- });
338
-
339
- // 10. ctx_doctor — diagnostics checklist
340
- pi.registerTool({
341
- name: "ctx_doctor",
342
- label: "Doctor",
343
- description: "Run diagnostics checklist — validate config, DB, FTS5, runtimes.",
344
- parameters: CtxDoctorParams,
329
+ } catch (err) {
330
+ return textResult(`Fetch error: ${err}`, { error: true });
331
+ }
332
+ };
333
+ pi.registerTool({ name: "content_fetch", label: "Content Fetch", description: "Fetch a URL, convert to markdown, and index into FTS5 content store.", parameters: ContentFetchParams, execute: contentFetchExec } as any);
334
+ pi.registerTool({ name: "ctx_fetch_and_index", label: "Content Fetch", description: "Fetch URL and index. (DEPRECATED: use content_fetch instead)", parameters: CtxFetchAndIndexParams, async execute(tcId: string, p: any) { deprecationLog("ctx_fetch_and_index", "content_fetch"); return contentFetchExec(tcId, p); } } as any);
335
+
336
+ // 9. compactor_stats (new) / ctx_stats (deprecated) — context savings dashboard
337
+ const statsExec = async (): Promise<any> => {
338
+ try {
339
+ const result = await ctxStats(deps.sessionDB, deps.contentStore!, deps.getSessionId(), deps.getCounters?.());
340
+ const lines = [
341
+ `📊 Compactor Stats`,
342
+ `Session events: ${result.sessionEvents}`,
343
+ `Compactions: ${result.compactions}`,
344
+ `Tokens saved: ${result.tokensSaved}`,
345
+ `Indexed docs: ${result.indexedDocs} (${result.indexedChunks} chunks)`,
346
+ `Sandbox runs: ${result.sandboxRuns}`,
347
+ `Search queries: ${result.searchQueries}`,
348
+ ];
349
+ return textResult(lines.join("\n"), result as unknown as Record<string, unknown>);
350
+ } catch (err) {
351
+ return textResult(`Stats error: ${err}`, { error: true });
352
+ }
353
+ };
354
+ pi.registerTool({ name: "compactor_stats", label: "Compactor Stats", description: "Show context savings dashboard session events, compactions, indexed content.", parameters: StatsParams, execute: statsExec } as any);
355
+ pi.registerTool({ name: "ctx_stats", label: "Compactor Stats", description: "Show stats dashboard. (DEPRECATED: use compactor_stats instead)", parameters: CtxStatsParams, async execute() { deprecationLog("ctx_stats", "compactor_stats"); return statsExec(); } } as any);
356
+
357
+ // 10. compactor_doctor (new) / ctx_doctor (deprecated) — diagnostics checklist
358
+ const doctorExec = async (): Promise<any> => {
359
+ try {
360
+ const result = await ctxDoctor(deps.sessionDB, deps.contentStore!);
361
+ const icon = (s: string) => (s === "pass" ? "✅" : s === "warn" ? "⚠️" : "❌");
362
+ const lines = [
363
+ result.healthy ? "🩺 All checks passed" : "🩺 Issues found",
364
+ "",
365
+ ...result.checks.map((c) => `${icon(c.status)} ${c.name}: ${c.message}`),
366
+ ];
367
+ return jsonResult(result, lines.join("\n"));
368
+ } catch (err) {
369
+ return textResult(`Doctor error: ${err}`, { error: true });
370
+ }
371
+ };
372
+ pi.registerTool({ name: "compactor_doctor", label: "Compactor Doctor", description: "Run diagnostics checklist — validate config, DB, FTS5, runtimes.", parameters: DoctorParams, execute: doctorExec } as any);
373
+ pi.registerTool({ name: "ctx_doctor", label: "Compactor Doctor", description: "Run diagnostics. (DEPRECATED: use compactor_doctor instead)", parameters: CtxDoctorParams, async execute() { deprecationLog("ctx_doctor", "compactor_doctor"); return doctorExec(); } } as any);
374
+
375
+ // 11. context_budget — estimate remaining context window
376
+ pi.registerTool(({
377
+ name: "context_budget",
378
+ label: "Context Budget",
379
+ description: "Estimate remaining context window (% full, tokens left) and get advice on whether to compact.",
380
+ parameters: Type.Object({}),
345
381
  async execute(): Promise<any> {
346
- try {
347
- const result = await ctxDoctor(deps.sessionDB, deps.contentStore!);
348
- const icon = (s: string) => (s === "pass" ? "✅" : s === "warn" ? "⚠️" : "❌");
349
- const lines = [
350
- result.healthy ? "🩺 All checks passed" : "🩺 Issues found",
351
- "",
352
- ...result.checks.map((c) => `${icon(c.status)} ${c.name}: ${c.message}`),
353
- ];
354
- return jsonResult(result, lines.join("\n"));
355
- } catch (err) {
356
- return textResult(`Doctor error: ${err}`, { error: true });
357
- }
382
+ const blocks = deps.getBlocks();
383
+ const estimatedTokens = blocks.reduce((sum, b) => {
384
+ const text = b.kind === "tool_call"
385
+ ? `${b.name} ${JSON.stringify((b as any).args ?? {})}`
386
+ : b.kind === "tool_result"
387
+ ? `${b.name} ${(b as any).text ?? ""}`
388
+ : (b as any).text ?? "";
389
+ return sum + Math.ceil(text.length / 4);
390
+ }, 0);
391
+ const message = contextBudgetTool(estimatedTokens);
392
+ return textResult(message);
358
393
  },
359
- });
394
+ } as any));
360
395
  }