@astralyn/pi 1.4.0 → 1.4.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/bundle/chunks/{chunk-BMTEURL3.js → chunk-KSKBY3VV.js} +62 -7
  3. package/dist/bundle/chunks/{chunk-TJIPCBZF.js → chunk-N77PYEXG.js} +1 -1
  4. package/dist/bundle/chunks/{virtual-modules-3CTTTQQN.js → virtual-modules-XNEYDLXF.js} +1 -1
  5. package/dist/bundle/cli.js +1 -1
  6. package/dist/bundle/index.js +1 -1
  7. package/dist/bundle/rpc-entry.js +1 -1
  8. package/dist/core/compaction/compaction.d.ts.map +1 -1
  9. package/dist/core/compaction/compaction.js +5 -5
  10. package/dist/core/compaction/compaction.js.map +1 -1
  11. package/dist/extensions/cached-compaction/index.d.ts +3 -0
  12. package/dist/extensions/cached-compaction/index.d.ts.map +1 -0
  13. package/dist/extensions/cached-compaction/index.js +91 -0
  14. package/dist/extensions/cached-compaction/index.js.map +1 -0
  15. package/dist/extensions/cached-compaction/render.d.ts +11 -0
  16. package/dist/extensions/cached-compaction/render.d.ts.map +1 -0
  17. package/dist/extensions/cached-compaction/render.js +19 -0
  18. package/dist/extensions/cached-compaction/render.js.map +1 -0
  19. package/dist/extensions/cached-compaction/summary.d.ts +15 -0
  20. package/dist/extensions/cached-compaction/summary.d.ts.map +1 -0
  21. package/dist/extensions/cached-compaction/summary.js +131 -0
  22. package/dist/extensions/cached-compaction/summary.js.map +1 -0
  23. package/dist/extensions/index.d.ts.map +1 -1
  24. package/dist/extensions/index.js +2 -0
  25. package/dist/extensions/index.js.map +1 -1
  26. package/docs/bundled/extensions/cached-compaction.md +35 -0
  27. package/docs/compaction.md +1 -1
  28. package/docs/docs.json +4 -0
  29. package/npm-shrinkwrap.json +2 -2
  30. package/package.json +1 -1
@@ -0,0 +1,19 @@
1
+ import { Text } from "@earendil-works/pi-tui";
2
+ function formatTokens(count) {
3
+ if (count >= 1_000_000)
4
+ return `${(count / 1_000_000).toFixed(1)}m`;
5
+ if (count >= 1000)
6
+ return `${(count / 1000).toFixed(1)}k`;
7
+ return String(count);
8
+ }
9
+ export const renderCachedCompactionEntry = (entry, _options, theme) => {
10
+ const data = entry.data;
11
+ if (!data)
12
+ return undefined;
13
+ const promptTokens = data.input + data.cacheRead + data.cacheWrite;
14
+ const percentage = promptTokens > 0 ? ((100 * data.cacheRead) / promptTokens).toFixed(1) : "0.0";
15
+ const title = `Cached compaction · ${data.reason === "manual" ? "manual" : "automatic"}`;
16
+ const stats = `Cache ${formatTokens(data.cacheRead)}/${formatTokens(promptTokens)} (${percentage}%) · output ${formatTokens(data.output)} · ${(data.durationMs / 1000).toFixed(1)}s`;
17
+ return new Text(`${theme.fg("accent", title)}\n${theme.fg("dim", stats)}`, 1, 0);
18
+ };
19
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../../src/extensions/cached-compaction/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAY9C,SAAS,YAAY,CAAC,KAAa;IAClC,IAAI,KAAK,IAAI,SAAS;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IACpE,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAC1D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACtB,CAAC;AAED,MAAM,CAAC,MAAM,2BAA2B,GAAwC,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAC1G,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;IACxB,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;IACnE,MAAM,UAAU,GAAG,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACjG,MAAM,KAAK,GAAG,uBAAuB,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IACzF,MAAM,KAAK,GAAG,SAAS,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,YAAY,CAAC,YAAY,CAAC,KAAK,UAAU,eAAe,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IACrL,OAAO,IAAI,IAAI,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAClF,CAAC,CAAC","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport type { EntryRenderer } from \"../../core/extensions/types.ts\";\n\nexport interface CachedCompactionCard {\n\treason: \"manual\" | \"threshold\";\n\tcacheRead: number;\n\tinput: number;\n\tcacheWrite: number;\n\toutput: number;\n\tdurationMs: number;\n}\n\nfunction formatTokens(count: number): string {\n\tif (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}m`;\n\tif (count >= 1000) return `${(count / 1000).toFixed(1)}k`;\n\treturn String(count);\n}\n\nexport const renderCachedCompactionEntry: EntryRenderer<CachedCompactionCard> = (entry, _options, theme) => {\n\tconst data = entry.data;\n\tif (!data) return undefined;\n\tconst promptTokens = data.input + data.cacheRead + data.cacheWrite;\n\tconst percentage = promptTokens > 0 ? ((100 * data.cacheRead) / promptTokens).toFixed(1) : \"0.0\";\n\tconst title = `Cached compaction · ${data.reason === \"manual\" ? \"manual\" : \"automatic\"}`;\n\tconst stats = `Cache ${formatTokens(data.cacheRead)}/${formatTokens(promptTokens)} (${percentage}%) · output ${formatTokens(data.output)} · ${(data.durationMs / 1000).toFixed(1)}s`;\n\treturn new Text(`${theme.fg(\"accent\", title)}\\n${theme.fg(\"dim\", stats)}`, 1, 0);\n};\n"]}
@@ -0,0 +1,15 @@
1
+ import { type Message } from "@earendil-works/pi-ai";
2
+ import type { CompactionDetails, FileOperations } from "../../core/compaction/index.ts";
3
+ import { type SessionEntry } from "../../core/session-manager.ts";
4
+ export interface KeptStart {
5
+ text: string;
6
+ fromEnd: number;
7
+ count: number;
8
+ }
9
+ export declare function keptStart(entries: SessionEntry[], firstKeptEntryId: string, conversation: Message[]): KeptStart | undefined;
10
+ export declare function summaryInstruction(kept: KeptStart, customInstructions?: string): string;
11
+ export declare function checkpointSummary(text: string, fileOps: FileOperations, entries: SessionEntry[]): {
12
+ summary: string;
13
+ details: CompactionDetails;
14
+ };
15
+ //# sourceMappingURL=summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../../src/extensions/cached-compaction/summary.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAExF,OAAO,EAAoD,KAAK,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAEpH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACd;AA2BD,wBAAgB,SAAS,CACxB,OAAO,EAAE,YAAY,EAAE,EACvB,gBAAgB,EAAE,MAAM,EACxB,YAAY,EAAE,OAAO,EAAE,GACrB,SAAS,GAAG,SAAS,CAevB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG,MAAM,CAoDvF;AAMD,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EAAE,GACrB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAE,CAyBjD"}
@@ -0,0 +1,131 @@
1
+ import { contentText } from "@earendil-works/pi-ai";
2
+ import { convertToLlm } from "../../core/messages.js";
3
+ import { buildSessionProjection, getLatestCompactionEntry } from "../../core/session-manager.js";
4
+ function sameMessage(candidate, projected) {
5
+ const serialized = JSON.stringify(candidate);
6
+ return convertToLlm([projected]).some((message) => JSON.stringify(message) === serialized);
7
+ }
8
+ function messagePreview(message) {
9
+ let text;
10
+ if (message.role === "user") {
11
+ text = contentText(message.content);
12
+ }
13
+ else if (message.role === "assistant") {
14
+ text = message.content
15
+ .flatMap((block) => block.type === "text"
16
+ ? [block.text]
17
+ : block.type === "toolCall"
18
+ ? [`[tool call: ${block.name} ${JSON.stringify(block.arguments)}]`]
19
+ : [])
20
+ .join("\n");
21
+ }
22
+ text = text?.trim();
23
+ if (!text)
24
+ return undefined;
25
+ return text.length > 400 ? `${text.slice(0, 400)}…` : text;
26
+ }
27
+ export function keptStart(entries, firstKeptEntryId, conversation) {
28
+ const projected = buildSessionProjection(entries).entries;
29
+ const start = projected.findIndex((entry) => entry.sourceEntry.id === firstKeptEntryId);
30
+ if (start === -1)
31
+ return undefined;
32
+ let at = -1;
33
+ for (const message of projected.slice(start).flatMap((entry) => entry.messages)) {
34
+ at = conversation.findLastIndex((candidate) => sameMessage(candidate, message));
35
+ if (at !== -1)
36
+ break;
37
+ }
38
+ if (at === -1)
39
+ return undefined;
40
+ const previews = conversation.slice(at).map(messagePreview);
41
+ const text = previews[0];
42
+ return text
43
+ ? { text, fromEnd: previews.filter((preview) => preview === text).length, count: previews.length }
44
+ : undefined;
45
+ }
46
+ export function summaryInstruction(kept, customInstructions) {
47
+ const repeated = kept.fromEnd > 1
48
+ ? ` Several retained messages begin with this text. The boundary is occurrence ${kept.fromEnd}, counting backward from the end of the conversation.`
49
+ : "";
50
+ const toolCalls = kept.text.includes("[tool call: ")
51
+ ? " Tool calls are written as [tool call: name arguments]."
52
+ : "";
53
+ const focus = customInstructions?.trim() ? `\n\nAdditional focus from the user:\n${customInstructions.trim()}` : "";
54
+ return `Stop working on the current task. Do not call any tools.
55
+
56
+ Write a structured context checkpoint summary of the earlier part of this conversation. Another LLM will continue the task from your summary, followed by the last ${kept.count} messages, which will remain in context verbatim.
57
+
58
+ The retained messages begin with the message that starts with this text.${repeated}${toolCalls}
59
+
60
+ <kept-start>
61
+ ${kept.text}
62
+ </kept-start>
63
+
64
+ Summarize only the messages before that boundary, as if the conversation had ended immediately before it. Work occurring at or after the boundary has not happened as far as this summary is concerned: do not list it as completed. If it was already planned, include it under Next Steps instead.
65
+
66
+ If the conversation contains an earlier checkpoint summary, fold its still-relevant facts into this summary and update its progress and next steps.
67
+
68
+ Use exactly this structure:
69
+
70
+ ## Goal
71
+ [What the user is trying to accomplish. Include multiple goals if applicable.]
72
+
73
+ ## Constraints & Preferences
74
+ - [Requirements, preferences, and explicit constraints from the user]
75
+ - [Use "(none)" when appropriate]
76
+
77
+ ## Progress
78
+ ### Done
79
+ - [x] [Work completed before the boundary]
80
+
81
+ ### In Progress
82
+ - [ ] [Work underway at the boundary]
83
+
84
+ ### Blocked
85
+ - [Issues preventing progress, or "(none)"]
86
+
87
+ ## Key Decisions
88
+ - **[Decision]**: [Rationale]
89
+
90
+ ## Next Steps
91
+ 1. [Ordered steps needed to continue]
92
+
93
+ ## Critical Context
94
+ - [Exact information needed to continue: file paths, symbols, commands, important tool results, error messages, and relevant technical details]
95
+
96
+ Be concise but preserve information required to continue without rereading the original conversation. Keep exact file paths, function names, commands, and error messages. Do not include file-operation lists; they will be added automatically. Return only the summary.${focus}`;
97
+ }
98
+ function isPlainObject(value) {
99
+ return typeof value === "object" && value !== null && !Array.isArray(value);
100
+ }
101
+ export function checkpointSummary(text, fileOps, entries) {
102
+ const read = new Set(fileOps.read);
103
+ const modified = new Set([...fileOps.written, ...fileOps.edited]);
104
+ const previousDetails = getLatestCompactionEntry(entries)?.details;
105
+ if (isPlainObject(previousDetails)) {
106
+ const { readFiles, modifiedFiles } = previousDetails;
107
+ if (Array.isArray(readFiles)) {
108
+ for (const file of readFiles)
109
+ if (typeof file === "string")
110
+ read.add(file);
111
+ }
112
+ if (Array.isArray(modifiedFiles)) {
113
+ for (const file of modifiedFiles)
114
+ if (typeof file === "string")
115
+ modified.add(file);
116
+ }
117
+ }
118
+ const details = {
119
+ readFiles: [...read].filter((file) => !modified.has(file)).sort(),
120
+ modifiedFiles: [...modified].sort(),
121
+ };
122
+ const sections = [
123
+ text.replace(/<(read-files|modified-files)>[\s\S]*?<\/\1>/g, "").trim(),
124
+ details.readFiles.length > 0 ? `<read-files>\n${details.readFiles.join("\n")}\n</read-files>` : "",
125
+ details.modifiedFiles.length > 0
126
+ ? `<modified-files>\n${details.modifiedFiles.join("\n")}\n</modified-files>`
127
+ : "",
128
+ ];
129
+ return { summary: sections.filter(Boolean).join("\n\n"), details };
130
+ }
131
+ //# sourceMappingURL=summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary.js","sourceRoot":"","sources":["../../../src/extensions/cached-compaction/summary.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport { contentText, type Message } from \"@earendil-works/pi-ai\";\nimport type { CompactionDetails, FileOperations } from \"../../core/compaction/index.ts\";\nimport { convertToLlm } from \"../../core/messages.ts\";\nimport { buildSessionProjection, getLatestCompactionEntry, type SessionEntry } from \"../../core/session-manager.ts\";\n\nexport interface KeptStart {\n\ttext: string;\n\tfromEnd: number;\n\tcount: number;\n}\n\nfunction sameMessage(candidate: Message, projected: AgentMessage): boolean {\n\tconst serialized = JSON.stringify(candidate);\n\treturn convertToLlm([projected]).some((message) => JSON.stringify(message) === serialized);\n}\n\nfunction messagePreview(message: Message): string | undefined {\n\tlet text: string | undefined;\n\tif (message.role === \"user\") {\n\t\ttext = contentText(message.content);\n\t} else if (message.role === \"assistant\") {\n\t\ttext = message.content\n\t\t\t.flatMap((block) =>\n\t\t\t\tblock.type === \"text\"\n\t\t\t\t\t? [block.text]\n\t\t\t\t\t: block.type === \"toolCall\"\n\t\t\t\t\t\t? [`[tool call: ${block.name} ${JSON.stringify(block.arguments)}]`]\n\t\t\t\t\t\t: [],\n\t\t\t)\n\t\t\t.join(\"\\n\");\n\t}\n\ttext = text?.trim();\n\tif (!text) return undefined;\n\treturn text.length > 400 ? `${text.slice(0, 400)}…` : text;\n}\n\nexport function keptStart(\n\tentries: SessionEntry[],\n\tfirstKeptEntryId: string,\n\tconversation: Message[],\n): KeptStart | undefined {\n\tconst projected = buildSessionProjection(entries).entries;\n\tconst start = projected.findIndex((entry) => entry.sourceEntry.id === firstKeptEntryId);\n\tif (start === -1) return undefined;\n\tlet at = -1;\n\tfor (const message of projected.slice(start).flatMap((entry) => entry.messages)) {\n\t\tat = conversation.findLastIndex((candidate) => sameMessage(candidate, message));\n\t\tif (at !== -1) break;\n\t}\n\tif (at === -1) return undefined;\n\tconst previews = conversation.slice(at).map(messagePreview);\n\tconst text = previews[0];\n\treturn text\n\t\t? { text, fromEnd: previews.filter((preview) => preview === text).length, count: previews.length }\n\t\t: undefined;\n}\n\nexport function summaryInstruction(kept: KeptStart, customInstructions?: string): string {\n\tconst repeated =\n\t\tkept.fromEnd > 1\n\t\t\t? ` Several retained messages begin with this text. The boundary is occurrence ${kept.fromEnd}, counting backward from the end of the conversation.`\n\t\t\t: \"\";\n\tconst toolCalls = kept.text.includes(\"[tool call: \")\n\t\t? \" Tool calls are written as [tool call: name arguments].\"\n\t\t: \"\";\n\tconst focus = customInstructions?.trim() ? `\\n\\nAdditional focus from the user:\\n${customInstructions.trim()}` : \"\";\n\treturn `Stop working on the current task. Do not call any tools.\n\nWrite a structured context checkpoint summary of the earlier part of this conversation. Another LLM will continue the task from your summary, followed by the last ${kept.count} messages, which will remain in context verbatim.\n\nThe retained messages begin with the message that starts with this text.${repeated}${toolCalls}\n\n<kept-start>\n${kept.text}\n</kept-start>\n\nSummarize only the messages before that boundary, as if the conversation had ended immediately before it. Work occurring at or after the boundary has not happened as far as this summary is concerned: do not list it as completed. If it was already planned, include it under Next Steps instead.\n\nIf the conversation contains an earlier checkpoint summary, fold its still-relevant facts into this summary and update its progress and next steps.\n\nUse exactly this structure:\n\n## Goal\n[What the user is trying to accomplish. Include multiple goals if applicable.]\n\n## Constraints & Preferences\n- [Requirements, preferences, and explicit constraints from the user]\n- [Use \"(none)\" when appropriate]\n\n## Progress\n### Done\n- [x] [Work completed before the boundary]\n\n### In Progress\n- [ ] [Work underway at the boundary]\n\n### Blocked\n- [Issues preventing progress, or \"(none)\"]\n\n## Key Decisions\n- **[Decision]**: [Rationale]\n\n## Next Steps\n1. [Ordered steps needed to continue]\n\n## Critical Context\n- [Exact information needed to continue: file paths, symbols, commands, important tool results, error messages, and relevant technical details]\n\nBe concise but preserve information required to continue without rereading the original conversation. Keep exact file paths, function names, commands, and error messages. Do not include file-operation lists; they will be added automatically. Return only the summary.${focus}`;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function checkpointSummary(\n\ttext: string,\n\tfileOps: FileOperations,\n\tentries: SessionEntry[],\n): { summary: string; details: CompactionDetails } {\n\tconst read = new Set(fileOps.read);\n\tconst modified = new Set([...fileOps.written, ...fileOps.edited]);\n\tconst previousDetails: unknown = getLatestCompactionEntry(entries)?.details;\n\tif (isPlainObject(previousDetails)) {\n\t\tconst { readFiles, modifiedFiles } = previousDetails;\n\t\tif (Array.isArray(readFiles)) {\n\t\t\tfor (const file of readFiles) if (typeof file === \"string\") read.add(file);\n\t\t}\n\t\tif (Array.isArray(modifiedFiles)) {\n\t\t\tfor (const file of modifiedFiles) if (typeof file === \"string\") modified.add(file);\n\t\t}\n\t}\n\tconst details = {\n\t\treadFiles: [...read].filter((file) => !modified.has(file)).sort(),\n\t\tmodifiedFiles: [...modified].sort(),\n\t};\n\tconst sections = [\n\t\ttext.replace(/<(read-files|modified-files)>[\\s\\S]*?<\\/\\1>/g, \"\").trim(),\n\t\tdetails.readFiles.length > 0 ? `<read-files>\\n${details.readFiles.join(\"\\n\")}\\n</read-files>` : \"\",\n\t\tdetails.modifiedFiles.length > 0\n\t\t\t? `<modified-files>\\n${details.modifiedFiles.join(\"\\n\")}\\n</modified-files>`\n\t\t\t: \"\",\n\t];\n\treturn { summary: sections.filter(Boolean).join(\"\\n\\n\"), details };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAYnE,eAAO,MAAM,iBAAiB,EAAE,eAAe,EAW9C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAanE,eAAO,MAAM,iBAAiB,EAAE,eAAe,EAY9C,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import btwExtension from "./btw/index.js";
2
+ import cachedCompactionExtension from "./cached-compaction/index.js";
2
3
  import codemodeExtension from "./codemode/index.js";
3
4
  import deepwikiExtension from "./deepwiki/index.js";
4
5
  import llamaExtension from "./llama/index.js";
@@ -12,6 +13,7 @@ export const builtInExtensions = [
12
13
  { name: "codemode", factory: codemodeExtension, replaceable: true, builtin: true },
13
14
  { name: "mcp", factory: mcpExtension, replaceable: true, builtin: true },
14
15
  { name: "tool-search", factory: toolSearchExtension, replaceable: true, builtin: true },
16
+ { name: "cached-compaction", factory: cachedCompactionExtension, replaceable: true, builtin: true },
15
17
  { name: "llama.cpp", factory: llamaExtension, builtin: true },
16
18
  { name: "btw", factory: btwExtension, builtin: true },
17
19
  { name: "deepwiki", factory: deepwikiExtension, builtin: true },
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AACA,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,cAAc,MAAM,kBAAkB,CAAC;AAC9C,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,eAAe,MAAM,mBAAmB,CAAC;AAChD,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,mBAAmB,MAAM,wBAAwB,CAAC;AAEzD,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IACnD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;IAClF,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;IACxE,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;IACvF,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE;IAC7D,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE;IACrD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE;IAC/D,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE;IAC/D,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE;IAC/D,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE;IACnE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE;CAC3D,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport btwExtension from \"./btw/index.ts\";\nimport codemodeExtension from \"./codemode/index.ts\";\nimport deepwikiExtension from \"./deepwiki/index.ts\";\nimport llamaExtension from \"./llama/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport providerExtension from \"./provider/index.ts\";\nimport questionExtension from \"./question/index.ts\";\nimport searchExtension from \"./search/index.ts\";\nimport statuslineExtension from \"./statusline/index.ts\";\nimport toolSearchExtension from \"./tool-search/index.ts\";\n\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"codemode\", factory: codemodeExtension, replaceable: true, builtin: true },\n\t{ name: \"mcp\", factory: mcpExtension, replaceable: true, builtin: true },\n\t{ name: \"tool-search\", factory: toolSearchExtension, replaceable: true, builtin: true },\n\t{ name: \"llama.cpp\", factory: llamaExtension, builtin: true },\n\t{ name: \"btw\", factory: btwExtension, builtin: true },\n\t{ name: \"deepwiki\", factory: deepwikiExtension, builtin: true },\n\t{ name: \"provider\", factory: providerExtension, builtin: true },\n\t{ name: \"question\", factory: questionExtension, builtin: true },\n\t{ name: \"statusline\", factory: statuslineExtension, builtin: true },\n\t{ name: \"search\", factory: searchExtension, builtin: true },\n];\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AACA,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,yBAAyB,MAAM,8BAA8B,CAAC;AACrE,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,cAAc,MAAM,kBAAkB,CAAC;AAC9C,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,eAAe,MAAM,mBAAmB,CAAC;AAChD,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,mBAAmB,MAAM,wBAAwB,CAAC;AAEzD,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IACnD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;IAClF,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;IACxE,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;IACvF,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,yBAAyB,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;IACnG,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE;IAC7D,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE;IACrD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE;IAC/D,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE;IAC/D,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE;IAC/D,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE;IACnE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE;CAC3D,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport btwExtension from \"./btw/index.ts\";\nimport cachedCompactionExtension from \"./cached-compaction/index.ts\";\nimport codemodeExtension from \"./codemode/index.ts\";\nimport deepwikiExtension from \"./deepwiki/index.ts\";\nimport llamaExtension from \"./llama/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport providerExtension from \"./provider/index.ts\";\nimport questionExtension from \"./question/index.ts\";\nimport searchExtension from \"./search/index.ts\";\nimport statuslineExtension from \"./statusline/index.ts\";\nimport toolSearchExtension from \"./tool-search/index.ts\";\n\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"codemode\", factory: codemodeExtension, replaceable: true, builtin: true },\n\t{ name: \"mcp\", factory: mcpExtension, replaceable: true, builtin: true },\n\t{ name: \"tool-search\", factory: toolSearchExtension, replaceable: true, builtin: true },\n\t{ name: \"cached-compaction\", factory: cachedCompactionExtension, replaceable: true, builtin: true },\n\t{ name: \"llama.cpp\", factory: llamaExtension, builtin: true },\n\t{ name: \"btw\", factory: btwExtension, builtin: true },\n\t{ name: \"deepwiki\", factory: deepwikiExtension, builtin: true },\n\t{ name: \"provider\", factory: providerExtension, builtin: true },\n\t{ name: \"question\", factory: questionExtension, builtin: true },\n\t{ name: \"statusline\", factory: statuslineExtension, builtin: true },\n\t{ name: \"search\", factory: searchExtension, builtin: true },\n];\n"]}
@@ -0,0 +1,35 @@
1
+ # Cached compaction
2
+
3
+ The bundled `cached-compaction` extension replaces summary generation for manual `/compact` and automatic threshold compaction. Pi still owns the cut point, session persistence, recent-message retention, and context rebuilding.
4
+
5
+ Instead of flattening the older messages into a new summarizer prompt and truncating each tool result, the extension sends the current prepared conversation plus one user message asking for a structured checkpoint. This lets the provider reuse its prompt cache and lets the summary model read the complete tool output that was present in the conversation.
6
+
7
+ Caching is an optimization, not a prerequisite. The extension does not estimate cache expiry or adapt requests to individual providers. A cold cache can make this request more expensive than native compaction, but produces the same kind of checkpoint without a separate provider-specific path.
8
+
9
+ ## Summary boundary
10
+
11
+ The extension uses Pi's `firstKeptEntryId` unchanged. Because the summary model also sees the retained recent messages, its instruction quotes the first retained message as `<kept-start>` and explicitly excludes that message and everything after it from the summary. Repeated opening text is identified by its occurrence counting backward from the end. If no usable text or tool-call boundary exists, native compaction runs instead.
12
+
13
+ The summary keeps Pi's Goal, Constraints & Preferences, Progress, Key Decisions, Next Steps, and Critical Context sections. Optional `/compact [instructions]` focus is appended to the instruction. Read and modified file lists are computed from session operations and previous compaction details rather than generated by the model.
14
+
15
+ ## Fallbacks
16
+
17
+ - Overflow recovery always uses native compaction because the complete context has already exceeded the model window.
18
+ - A missing boundary, request failure, empty summary, tool call, or output-length stop falls back to native compaction.
19
+ - Cancelling the summary cancels that compaction without starting a second native summary request.
20
+ - Branch summarization through `/tree` is unchanged.
21
+
22
+ Native compaction accepts the same native file-list shape in extension-provided compaction details, so alternating between cached and native compaction does not reset file history.
23
+
24
+ ## Result card
25
+
26
+ A successful cached-path compaction adds a persistent display-only card after the summary:
27
+
28
+ ```text
29
+ Cached compaction · manual
30
+ Cache 641.2k/648.8k (98.8%) · output 2.8k · 18.4s
31
+ ```
32
+
33
+ Automatic compaction is labelled `automatic`. The cache figures are the response's actual `cacheRead` divided by `input + cacheRead + cacheWrite`; the card also shows output tokens and elapsed time. It contains no cost estimate and never enters model context. Native fallback adds no card.
34
+
35
+ The extension registers no commands or settings.
@@ -17,7 +17,7 @@ Pi has two summarization mechanisms:
17
17
  | Compaction | Context exceeds threshold, or `/compact` | Summarize old messages to free up context |
18
18
  | Branch summarization | `/tree` navigation | Preserve context when switching branches |
19
19
 
20
- Both use the same structured summary format and track file operations cumulatively. Summarization requests disable prompt-cache writes because these one-off prompts are unlikely to be reused.
20
+ Both use the same structured summary format and track file operations cumulatively. Native summarization requests disable prompt-cache writes because these one-off prompts are unlikely to be reused. The bundled [Cached compaction](bundled/extensions/cached-compaction.md) extension instead summarizes the prepared conversation directly for manual and threshold compaction, allowing provider prompt-cache reuse; overflow recovery and failed cached-path attempts retain the native behavior.
21
21
 
22
22
  ## Compaction
23
23
 
package/docs/docs.json CHANGED
@@ -72,6 +72,10 @@
72
72
  "title": "BTW",
73
73
  "path": "bundled/extensions/btw.md"
74
74
  },
75
+ {
76
+ "title": "Cached compaction",
77
+ "path": "bundled/extensions/cached-compaction.md"
78
+ },
75
79
  {
76
80
  "title": "DeepWiki",
77
81
  "path": "bundled/extensions/deepwiki.md"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@astralyn/pi",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@astralyn/pi",
9
- "version": "1.4.0",
9
+ "version": "1.4.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@earendil-works/pi-agent-core": "1.4.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astralyn/pi",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {