@mariozechner/pi-coding-agent 0.23.1 → 0.23.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 (41) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +89 -148
  3. package/dist/cli/file-processor.d.ts +1 -1
  4. package/dist/cli/file-processor.d.ts.map +1 -1
  5. package/dist/cli/file-processor.js +12 -21
  6. package/dist/cli/file-processor.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +3 -1
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +27 -6
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/hooks/loader.d.ts.map +1 -1
  12. package/dist/core/hooks/loader.js +25 -1
  13. package/dist/core/hooks/loader.js.map +1 -1
  14. package/dist/core/hooks/types.d.ts +2 -1
  15. package/dist/core/hooks/types.d.ts.map +1 -1
  16. package/dist/core/hooks/types.js.map +1 -1
  17. package/dist/core/system-prompt.d.ts.map +1 -1
  18. package/dist/core/system-prompt.js +3 -3
  19. package/dist/core/system-prompt.js.map +1 -1
  20. package/dist/core/tools/read.d.ts.map +1 -1
  21. package/dist/core/tools/read.js +3 -19
  22. package/dist/core/tools/read.js.map +1 -1
  23. package/dist/main.d.ts.map +1 -1
  24. package/dist/main.js +3 -3
  25. package/dist/main.js.map +1 -1
  26. package/dist/utils/mime.d.ts +2 -0
  27. package/dist/utils/mime.d.ts.map +1 -0
  28. package/dist/utils/mime.js +26 -0
  29. package/dist/utils/mime.js.map +1 -0
  30. package/docs/custom-tools.md +19 -1
  31. package/docs/hooks.md +39 -19
  32. package/docs/rpc.md +14 -0
  33. package/docs/skills.md +148 -52
  34. package/docs/theme.md +23 -21
  35. package/package.json +5 -4
  36. package/docs/compaction-new.md +0 -387
  37. package/docs/compaction-strategies.ts +0 -502
  38. package/docs/compaction.md +0 -519
  39. package/docs/gemini.md +0 -255
  40. package/docs/truncation.md +0 -235
  41. package/docs/undercompaction.md +0 -313
@@ -1,502 +0,0 @@
1
- /**
2
- * CLI tool to test different compaction strategies on session fixtures.
3
- *
4
- * Usage:
5
- * npx tsx test/compaction-strategies.ts [fixture-name]
6
- *
7
- * Examples:
8
- * npx tsx test/compaction-strategies.ts large-session
9
- * npx tsx test/compaction-strategies.ts before-compaction
10
- *
11
- * Output:
12
- * test/compaction-results/[fixture]-[strategy].md
13
- */
14
-
15
- import * as fs from "fs";
16
- import * as path from "path";
17
- import { fileURLToPath } from "url";
18
-
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = path.dirname(__filename);
21
-
22
- import { complete, getModel, type UserMessage } from "@mariozechner/pi-ai";
23
-
24
- // ============================================================================
25
- // Types
26
- // ============================================================================
27
-
28
- interface SessionEntry {
29
- type: string;
30
- timestamp: string;
31
- message?: {
32
- role: string;
33
- content: unknown;
34
- stopReason?: string;
35
- };
36
- }
37
-
38
- interface SimpleMessage {
39
- role: "user" | "assistant";
40
- content: string;
41
- tokens: number; // estimated
42
- }
43
-
44
- interface SliceSummary {
45
- sliceIndex: number;
46
- summary: string;
47
- tokens: number;
48
- }
49
-
50
- interface StrategyResult {
51
- name: string;
52
- summary: string;
53
- totalInputTokens: number;
54
- totalOutputTokens: number;
55
- numCalls: number;
56
- timeMs: number;
57
- }
58
-
59
- // ============================================================================
60
- // Config
61
- // ============================================================================
62
-
63
- const MODEL = getModel("anthropic", "claude-sonnet-4-5");
64
- const SLICE_TOKENS = 10000; // target tokens per slice (smaller for testing)
65
- const SUMMARY_BUDGET = 2000; // max tokens for each summary call
66
- const FINAL_SUMMARY_BUDGET = 4000; // max tokens for final/stitched summary
67
-
68
- // ============================================================================
69
- // Utilities
70
- // ============================================================================
71
-
72
- function estimateTokens(text: string): number {
73
- return Math.ceil(text.length / 4);
74
- }
75
-
76
- function extractTextContent(content: unknown): string {
77
- if (typeof content === "string") return content;
78
- if (Array.isArray(content)) {
79
- return content
80
- .map((block) => {
81
- if (typeof block === "string") return block;
82
- if (block.type === "text") return block.text || "";
83
- if (block.type === "tool_use")
84
- return `[Tool: ${block.name}]\n${JSON.stringify(block.arguments || block.input, null, 2)}`;
85
- if (block.type === "tool_result") {
86
- const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
87
- return `[Tool Result: ${block.tool_use_id}]\n${text.slice(0, 2000)}${text.length > 2000 ? "..." : ""}`;
88
- }
89
- if (block.type === "thinking") return `[Thinking]\n${block.thinking}`;
90
- return "";
91
- })
92
- .filter(Boolean)
93
- .join("\n");
94
- }
95
- return JSON.stringify(content);
96
- }
97
-
98
- function loadSession(fixturePath: string): SimpleMessage[] {
99
- const content = fs.readFileSync(fixturePath, "utf-8");
100
- const lines = content.trim().split("\n");
101
- const messages: SimpleMessage[] = [];
102
-
103
- for (const line of lines) {
104
- try {
105
- const entry: SessionEntry = JSON.parse(line);
106
- if (entry.type === "message" && entry.message) {
107
- const role = entry.message.role;
108
- if (role !== "user" && role !== "assistant") continue;
109
- if (entry.message.stopReason === "aborted" || entry.message.stopReason === "error") continue;
110
-
111
- const text = extractTextContent(entry.message.content);
112
- if (!text.trim()) continue;
113
-
114
- messages.push({
115
- role: role as "user" | "assistant",
116
- content: text,
117
- tokens: estimateTokens(text),
118
- });
119
- }
120
- } catch {
121
- // skip malformed lines
122
- }
123
- }
124
-
125
- return messages;
126
- }
127
-
128
- function segmentByTokens(messages: SimpleMessage[], sliceTokens: number): SimpleMessage[][] {
129
- const slices: SimpleMessage[][] = [];
130
- let current: SimpleMessage[] = [];
131
- let currentTokens = 0;
132
-
133
- for (const msg of messages) {
134
- if (currentTokens + msg.tokens > sliceTokens && current.length > 0) {
135
- slices.push(current);
136
- current = [];
137
- currentTokens = 0;
138
- }
139
- current.push(msg);
140
- currentTokens += msg.tokens;
141
- }
142
-
143
- if (current.length > 0) {
144
- slices.push(current);
145
- }
146
-
147
- return slices;
148
- }
149
-
150
- function messagesToTranscript(messages: SimpleMessage[]): string {
151
- return messages
152
- .map((m) => {
153
- const prefix = m.role === "user" ? "USER:" : "ASSISTANT:";
154
- return `${prefix}\n${m.content}`;
155
- })
156
- .join("\n\n---\n\n");
157
- }
158
-
159
- async function callLLM(
160
- systemPrompt: string,
161
- userPrompt: string,
162
- maxTokens: number,
163
- ): Promise<{ text: string; inputTokens: number; outputTokens: number }> {
164
- const apiKey = process.env.ANTHROPIC_API_KEY;
165
- if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set");
166
-
167
- const messages: UserMessage[] = [
168
- {
169
- role: "user",
170
- content: userPrompt,
171
- timestamp: Date.now(),
172
- },
173
- ];
174
-
175
- const result = await complete(
176
- MODEL,
177
- {
178
- system: systemPrompt,
179
- messages,
180
- },
181
- {
182
- maxTokens,
183
- apiKey,
184
- },
185
- );
186
-
187
- const text = result.content
188
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
189
- .map((c) => c.text)
190
- .join("\n");
191
-
192
- return {
193
- text,
194
- inputTokens: result.usage.input + result.usage.cacheRead,
195
- outputTokens: result.usage.output,
196
- };
197
- }
198
-
199
- // ============================================================================
200
- // Strategy 1: Single-shot (current approach)
201
- // ============================================================================
202
-
203
- const SINGLE_SHOT_SYSTEM = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
204
-
205
- Include:
206
- - Current progress and key decisions made
207
- - Important context, constraints, or user preferences
208
- - Absolute file paths of any relevant files that were read or modified
209
- - What remains to be done (clear next steps)
210
- - Any critical data, examples, or references needed to continue
211
-
212
- Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;
213
-
214
- async function strategySingleShot(messages: SimpleMessage[]): Promise<StrategyResult> {
215
- const start = Date.now();
216
- const transcript = messagesToTranscript(messages);
217
-
218
- const { text, inputTokens, outputTokens } = await callLLM(
219
- SINGLE_SHOT_SYSTEM,
220
- `Here is the conversation to summarize:\n\n<conversation>\n${transcript}\n</conversation>\n\nProvide your summary now:`,
221
- FINAL_SUMMARY_BUDGET,
222
- );
223
-
224
- return {
225
- name: "single-shot",
226
- summary: text,
227
- totalInputTokens: inputTokens,
228
- totalOutputTokens: outputTokens,
229
- numCalls: 1,
230
- timeMs: Date.now() - start,
231
- };
232
- }
233
-
234
- // ============================================================================
235
- // Strategy 2: Parallel slices with LLM stitch
236
- // ============================================================================
237
-
238
- const SLICE_SYSTEM = `You are summarizing one segment of a longer coding session.
239
- Be concise but capture key information: user requests, files modified, decisions made, errors fixed.
240
- Preserve file paths and important code snippets.`;
241
-
242
- const STITCH_SYSTEM = `You are combining multiple chronological summaries of a coding session into one coherent handoff document.
243
- Remove redundancy. Preserve all file paths and key details. Emphasize the most recent work (last segment).`;
244
-
245
- async function strategyParallelStitch(messages: SimpleMessage[]): Promise<StrategyResult> {
246
- const start = Date.now();
247
- const slices = segmentByTokens(messages, SLICE_TOKENS);
248
- let totalInput = 0;
249
- let totalOutput = 0;
250
-
251
- console.log(` Parallel: ${slices.length} slices`);
252
-
253
- // Summarize all slices in parallel
254
- const sliceSummaries = await Promise.all(
255
- slices.map(async (slice, i) => {
256
- const isLast = i === slices.length - 1;
257
- const transcript = messagesToTranscript(slice);
258
- const prompt = `Segment ${i + 1} of ${slices.length}${isLast ? " (MOST RECENT)" : ""}:
259
-
260
- ${transcript}
261
-
262
- ${isLast ? "This is the most recent activity. Be detailed about current state and next steps." : "Summarize the key points from this segment."}`;
263
-
264
- const { text, inputTokens, outputTokens } = await callLLM(SLICE_SYSTEM, prompt, SUMMARY_BUDGET);
265
- totalInput += inputTokens;
266
- totalOutput += outputTokens;
267
-
268
- return { sliceIndex: i, summary: text, tokens: estimateTokens(text) };
269
- }),
270
- );
271
-
272
- // Stitch summaries together
273
- const stitchPrompt = sliceSummaries.map((s) => `=== Segment ${s.sliceIndex + 1} ===\n${s.summary}`).join("\n\n");
274
-
275
- const {
276
- text: finalSummary,
277
- inputTokens,
278
- outputTokens,
279
- } = await callLLM(
280
- STITCH_SYSTEM,
281
- `Combine these ${sliceSummaries.length} chronological segment summaries into one unified handoff summary:\n\n${stitchPrompt}`,
282
- FINAL_SUMMARY_BUDGET,
283
- );
284
- totalInput += inputTokens;
285
- totalOutput += outputTokens;
286
-
287
- return {
288
- name: "parallel-stitch",
289
- summary: finalSummary,
290
- totalInputTokens: totalInput,
291
- totalOutputTokens: totalOutput,
292
- numCalls: slices.length + 1,
293
- timeMs: Date.now() - start,
294
- };
295
- }
296
-
297
- // ============================================================================
298
- // Strategy 3: Sequential slices with accumulated context
299
- // ============================================================================
300
-
301
- const SEQUENTIAL_SYSTEM = `You are summarizing one segment of a longer coding session.
302
- You may be given summaries of earlier segments for context.
303
- Create a summary of THIS segment's content. Do not repeat information from previous summaries.
304
- Be concise but capture: user requests, files modified, decisions made, errors fixed.`;
305
-
306
- async function strategySequentialAccumulated(messages: SimpleMessage[]): Promise<StrategyResult> {
307
- const start = Date.now();
308
- const slices = segmentByTokens(messages, SLICE_TOKENS);
309
- let totalInput = 0;
310
- let totalOutput = 0;
311
-
312
- console.log(` Sequential: ${slices.length} slices`);
313
-
314
- const sliceSummaries: SliceSummary[] = [];
315
-
316
- for (let i = 0; i < slices.length; i++) {
317
- const slice = slices[i];
318
- const isLast = i === slices.length - 1;
319
- const transcript = messagesToTranscript(slice);
320
-
321
- // Build context from previous summaries
322
- const previousContext =
323
- sliceSummaries.length > 0
324
- ? `Previous segments summary:\n${sliceSummaries.map((s) => `[Segment ${s.sliceIndex + 1}] ${s.summary}`).join("\n\n")}\n\n---\n\n`
325
- : "";
326
-
327
- const prompt = `${previousContext}Current segment (${i + 1} of ${slices.length})${isLast ? " - MOST RECENT" : ""}:
328
-
329
- ${transcript}
330
-
331
- ${isLast ? "This is the most recent activity. Be detailed about current state, pending work, and next steps." : "Summarize the key NEW information from this segment (don't repeat what's in previous summaries)."}`;
332
-
333
- const { text, inputTokens, outputTokens } = await callLLM(
334
- SEQUENTIAL_SYSTEM,
335
- prompt,
336
- isLast ? FINAL_SUMMARY_BUDGET : SUMMARY_BUDGET,
337
- );
338
- totalInput += inputTokens;
339
- totalOutput += outputTokens;
340
-
341
- sliceSummaries.push({
342
- sliceIndex: i,
343
- summary: text,
344
- tokens: estimateTokens(text),
345
- });
346
-
347
- console.log(` Slice ${i + 1}/${slices.length} done`);
348
- }
349
-
350
- // Combine all slice summaries into final output
351
- const finalSummary = sliceSummaries.map((s) => `## Segment ${s.sliceIndex + 1}\n\n${s.summary}`).join("\n\n---\n\n");
352
-
353
- return {
354
- name: "sequential-accumulated",
355
- summary: finalSummary,
356
- totalInputTokens: totalInput,
357
- totalOutputTokens: totalOutput,
358
- numCalls: slices.length,
359
- timeMs: Date.now() - start,
360
- };
361
- }
362
-
363
- // ============================================================================
364
- // Strategy 4: Sequential with rolling summary
365
- // ============================================================================
366
-
367
- const ROLLING_SYSTEM = `You are creating a rolling summary of a coding session.
368
- Given a previous summary and new conversation content, produce an UPDATED summary that incorporates the new information.
369
- Keep the summary focused and under the token budget. Condense older details as needed to make room for recent work.`;
370
-
371
- async function strategySequentialRolling(messages: SimpleMessage[]): Promise<StrategyResult> {
372
- const start = Date.now();
373
- const slices = segmentByTokens(messages, SLICE_TOKENS);
374
- let totalInput = 0;
375
- let totalOutput = 0;
376
-
377
- console.log(` Rolling: ${slices.length} slices`);
378
-
379
- let runningSummary = "";
380
-
381
- for (let i = 0; i < slices.length; i++) {
382
- const slice = slices[i];
383
- const isLast = i === slices.length - 1;
384
- const transcript = messagesToTranscript(slice);
385
-
386
- const prompt = runningSummary
387
- ? `Current summary so far:\n${runningSummary}\n\n---\n\nNew content (segment ${i + 1} of ${slices.length}):\n${transcript}\n\n${isLast ? "This is the final segment. Produce the complete handoff summary with emphasis on current state and next steps." : "Update the summary to incorporate this new content. Condense older details if needed."}`
388
- : `First segment of the conversation:\n${transcript}\n\nCreate an initial summary capturing the key points.`;
389
-
390
- const { text, inputTokens, outputTokens } = await callLLM(
391
- ROLLING_SYSTEM,
392
- prompt,
393
- isLast ? FINAL_SUMMARY_BUDGET : SUMMARY_BUDGET,
394
- );
395
- totalInput += inputTokens;
396
- totalOutput += outputTokens;
397
-
398
- runningSummary = text;
399
- console.log(` Slice ${i + 1}/${slices.length} done`);
400
- }
401
-
402
- return {
403
- name: "sequential-rolling",
404
- summary: runningSummary,
405
- totalInputTokens: totalInput,
406
- totalOutputTokens: totalOutput,
407
- numCalls: slices.length,
408
- timeMs: Date.now() - start,
409
- };
410
- }
411
-
412
- // ============================================================================
413
- // Main
414
- // ============================================================================
415
-
416
- async function main() {
417
- const fixtureName = process.argv[2] || "large-session";
418
- const fixturesDir = path.join(__dirname, "fixtures");
419
- const fixturePath = path.join(fixturesDir, `${fixtureName}.jsonl`);
420
-
421
- if (!fs.existsSync(fixturePath)) {
422
- console.error(`Fixture not found: ${fixturePath}`);
423
- console.error(`Available fixtures:`);
424
- for (const f of fs.readdirSync(fixturesDir).filter((f) => f.endsWith(".jsonl"))) {
425
- console.error(` - ${f.replace(".jsonl", "")}`);
426
- }
427
- process.exit(1);
428
- }
429
-
430
- console.log(`Loading fixture: ${fixtureName}`);
431
- const messages = loadSession(fixturePath);
432
- const totalTokens = messages.reduce((sum, m) => sum + m.tokens, 0);
433
- console.log(` ${messages.length} messages, ~${totalTokens} tokens\n`);
434
-
435
- const resultsDir = path.join(__dirname, "compaction-results");
436
- fs.mkdirSync(resultsDir, { recursive: true });
437
-
438
- const strategies: Array<{
439
- name: string;
440
- fn: (msgs: SimpleMessage[]) => Promise<StrategyResult>;
441
- }> = [
442
- { name: "single-shot", fn: strategySingleShot },
443
- { name: "parallel-stitch", fn: strategyParallelStitch },
444
- { name: "sequential-accumulated", fn: strategySequentialAccumulated },
445
- { name: "sequential-rolling", fn: strategySequentialRolling },
446
- ];
447
-
448
- const results: StrategyResult[] = [];
449
-
450
- for (const strategy of strategies) {
451
- console.log(`Running strategy: ${strategy.name}`);
452
- try {
453
- const result = await strategy.fn(messages);
454
- results.push(result);
455
-
456
- // Write individual result
457
- const outputPath = path.join(resultsDir, `${fixtureName}-${strategy.name}.md`);
458
- const output = `# Compaction Result: ${strategy.name}
459
-
460
- ## Stats
461
- - Input tokens: ${result.totalInputTokens}
462
- - Output tokens: ${result.totalOutputTokens}
463
- - API calls: ${result.numCalls}
464
- - Time: ${result.timeMs}ms
465
-
466
- ## Summary
467
-
468
- ${result.summary}
469
- `;
470
- fs.writeFileSync(outputPath, output);
471
- console.log(` ✓ Wrote ${outputPath}\n`);
472
- } catch (err) {
473
- console.error(` ✗ Failed: ${err}\n`);
474
- }
475
- }
476
-
477
- // Write comparison summary
478
- const comparisonPath = path.join(resultsDir, `${fixtureName}-comparison.md`);
479
- const comparison = `# Compaction Strategy Comparison: ${fixtureName}
480
-
481
- ## Input
482
- - Messages: ${messages.length}
483
- - Estimated tokens: ${totalTokens}
484
-
485
- ## Results
486
-
487
- | Strategy | Input Tokens | Output Tokens | API Calls | Time (ms) |
488
- |----------|-------------|---------------|-----------|-----------|
489
- ${results.map((r) => `| ${r.name} | ${r.totalInputTokens} | ${r.totalOutputTokens} | ${r.numCalls} | ${r.timeMs} |`).join("\n")}
490
-
491
- ## Summaries
492
-
493
- ${results.map((r) => `### ${r.name}\n\n${r.summary}\n`).join("\n---\n\n")}
494
- `;
495
- fs.writeFileSync(comparisonPath, comparison);
496
- console.log(`Wrote comparison: ${comparisonPath}`);
497
- }
498
-
499
- main().catch((err) => {
500
- console.error(err);
501
- process.exit(1);
502
- });