@mariozechner/pi-coding-agent 0.30.1 → 0.31.0

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 (301) hide show
  1. package/CHANGELOG.md +251 -2
  2. package/README.md +105 -84
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +5 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/file-processor.d.ts +3 -3
  7. package/dist/cli/file-processor.d.ts.map +1 -1
  8. package/dist/cli/file-processor.js +7 -10
  9. package/dist/cli/file-processor.js.map +1 -1
  10. package/dist/config.d.ts +9 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +18 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/agent-session.d.ts +73 -34
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +464 -210
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/auth-storage.d.ts +2 -7
  19. package/dist/core/auth-storage.d.ts.map +1 -1
  20. package/dist/core/auth-storage.js +4 -52
  21. package/dist/core/auth-storage.js.map +1 -1
  22. package/dist/core/bash-executor.d.ts +2 -2
  23. package/dist/core/bash-executor.d.ts.map +1 -1
  24. package/dist/core/bash-executor.js +2 -2
  25. package/dist/core/bash-executor.js.map +1 -1
  26. package/dist/core/compaction/branch-summarization.d.ts +84 -0
  27. package/dist/core/compaction/branch-summarization.d.ts.map +1 -0
  28. package/dist/core/compaction/branch-summarization.js +233 -0
  29. package/dist/core/compaction/branch-summarization.js.map +1 -0
  30. package/dist/core/{compaction.d.ts → compaction/compaction.d.ts} +38 -19
  31. package/dist/core/compaction/compaction.d.ts.map +1 -0
  32. package/dist/core/compaction/compaction.js +558 -0
  33. package/dist/core/compaction/compaction.js.map +1 -0
  34. package/dist/core/compaction/index.d.ts +7 -0
  35. package/dist/core/compaction/index.d.ts.map +1 -0
  36. package/dist/core/compaction/index.js +7 -0
  37. package/dist/core/compaction/index.js.map +1 -0
  38. package/dist/core/compaction/utils.d.ts +35 -0
  39. package/dist/core/compaction/utils.d.ts.map +1 -0
  40. package/dist/core/compaction/utils.js +138 -0
  41. package/dist/core/compaction/utils.js.map +1 -0
  42. package/dist/core/custom-tools/index.d.ts +2 -1
  43. package/dist/core/custom-tools/index.d.ts.map +1 -1
  44. package/dist/core/custom-tools/index.js +1 -0
  45. package/dist/core/custom-tools/index.js.map +1 -1
  46. package/dist/core/custom-tools/loader.d.ts.map +1 -1
  47. package/dist/core/custom-tools/loader.js +13 -80
  48. package/dist/core/custom-tools/loader.js.map +1 -1
  49. package/dist/core/custom-tools/types.d.ts +84 -59
  50. package/dist/core/custom-tools/types.d.ts.map +1 -1
  51. package/dist/core/custom-tools/types.js.map +1 -1
  52. package/dist/core/custom-tools/wrapper.d.ts +15 -0
  53. package/dist/core/custom-tools/wrapper.d.ts.map +1 -0
  54. package/dist/core/custom-tools/wrapper.js +23 -0
  55. package/dist/core/custom-tools/wrapper.js.map +1 -0
  56. package/dist/core/exec.d.ts +29 -0
  57. package/dist/core/exec.d.ts.map +1 -0
  58. package/dist/core/exec.js +71 -0
  59. package/dist/core/exec.js.map +1 -0
  60. package/dist/core/export-html/index.d.ts +17 -0
  61. package/dist/core/export-html/index.d.ts.map +1 -0
  62. package/dist/core/export-html/index.js +171 -0
  63. package/dist/core/export-html/index.js.map +1 -0
  64. package/dist/core/export-html/template.css +781 -0
  65. package/dist/core/export-html/template.html +54 -0
  66. package/dist/core/export-html/template.js +1185 -0
  67. package/dist/core/export-html/vendor/highlight.min.js +1213 -0
  68. package/dist/core/export-html/vendor/marked.min.js +6 -0
  69. package/dist/core/hooks/index.d.ts +4 -4
  70. package/dist/core/hooks/index.d.ts.map +1 -1
  71. package/dist/core/hooks/index.js +3 -3
  72. package/dist/core/hooks/index.js.map +1 -1
  73. package/dist/core/hooks/loader.d.ts +40 -5
  74. package/dist/core/hooks/loader.d.ts.map +1 -1
  75. package/dist/core/hooks/loader.js +43 -10
  76. package/dist/core/hooks/loader.js.map +1 -1
  77. package/dist/core/hooks/runner.d.ts +94 -18
  78. package/dist/core/hooks/runner.d.ts.map +1 -1
  79. package/dist/core/hooks/runner.js +199 -120
  80. package/dist/core/hooks/runner.js.map +1 -1
  81. package/dist/core/hooks/tool-wrapper.d.ts +1 -1
  82. package/dist/core/hooks/tool-wrapper.d.ts.map +1 -1
  83. package/dist/core/hooks/tool-wrapper.js +36 -19
  84. package/dist/core/hooks/tool-wrapper.js.map +1 -1
  85. package/dist/core/hooks/types.d.ts +407 -96
  86. package/dist/core/hooks/types.d.ts.map +1 -1
  87. package/dist/core/hooks/types.js.map +1 -1
  88. package/dist/core/index.d.ts +4 -3
  89. package/dist/core/index.d.ts.map +1 -1
  90. package/dist/core/index.js.map +1 -1
  91. package/dist/core/messages.d.ts +44 -12
  92. package/dist/core/messages.d.ts.map +1 -1
  93. package/dist/core/messages.js +82 -34
  94. package/dist/core/messages.js.map +1 -1
  95. package/dist/core/model-registry.d.ts +5 -5
  96. package/dist/core/model-registry.d.ts.map +1 -1
  97. package/dist/core/model-registry.js +7 -7
  98. package/dist/core/model-registry.js.map +1 -1
  99. package/dist/core/model-resolver.d.ts +7 -7
  100. package/dist/core/model-resolver.d.ts.map +1 -1
  101. package/dist/core/model-resolver.js +45 -14
  102. package/dist/core/model-resolver.js.map +1 -1
  103. package/dist/core/sdk.d.ts +7 -10
  104. package/dist/core/sdk.d.ts.map +1 -1
  105. package/dist/core/sdk.js +88 -32
  106. package/dist/core/sdk.js.map +1 -1
  107. package/dist/core/session-manager.d.ts +202 -36
  108. package/dist/core/session-manager.d.ts.map +1 -1
  109. package/dist/core/session-manager.js +565 -133
  110. package/dist/core/session-manager.js.map +1 -1
  111. package/dist/core/settings-manager.d.ts +9 -3
  112. package/dist/core/settings-manager.d.ts.map +1 -1
  113. package/dist/core/settings-manager.js +13 -12
  114. package/dist/core/settings-manager.js.map +1 -1
  115. package/dist/core/system-prompt.d.ts.map +1 -1
  116. package/dist/core/system-prompt.js +6 -3
  117. package/dist/core/system-prompt.js.map +1 -1
  118. package/dist/core/tools/bash.d.ts +1 -1
  119. package/dist/core/tools/bash.d.ts.map +1 -1
  120. package/dist/core/tools/bash.js.map +1 -1
  121. package/dist/core/tools/edit-diff.d.ts +33 -0
  122. package/dist/core/tools/edit-diff.d.ts.map +1 -0
  123. package/dist/core/tools/edit-diff.js +171 -0
  124. package/dist/core/tools/edit-diff.js.map +1 -0
  125. package/dist/core/tools/edit.d.ts +7 -1
  126. package/dist/core/tools/edit.d.ts.map +1 -1
  127. package/dist/core/tools/edit.js +20 -95
  128. package/dist/core/tools/edit.js.map +1 -1
  129. package/dist/core/tools/find.d.ts +1 -1
  130. package/dist/core/tools/find.d.ts.map +1 -1
  131. package/dist/core/tools/find.js.map +1 -1
  132. package/dist/core/tools/grep.d.ts +1 -1
  133. package/dist/core/tools/grep.d.ts.map +1 -1
  134. package/dist/core/tools/grep.js.map +1 -1
  135. package/dist/core/tools/index.d.ts +1 -1
  136. package/dist/core/tools/index.d.ts.map +1 -1
  137. package/dist/core/tools/index.js.map +1 -1
  138. package/dist/core/tools/ls.d.ts +1 -1
  139. package/dist/core/tools/ls.d.ts.map +1 -1
  140. package/dist/core/tools/ls.js.map +1 -1
  141. package/dist/core/tools/read.d.ts +1 -1
  142. package/dist/core/tools/read.d.ts.map +1 -1
  143. package/dist/core/tools/read.js.map +1 -1
  144. package/dist/core/tools/write.d.ts +1 -1
  145. package/dist/core/tools/write.d.ts.map +1 -1
  146. package/dist/core/tools/write.js.map +1 -1
  147. package/dist/index.d.ts +8 -7
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +5 -5
  150. package/dist/index.js.map +1 -1
  151. package/dist/main.d.ts.map +1 -1
  152. package/dist/main.js +25 -25
  153. package/dist/main.js.map +1 -1
  154. package/dist/migrations.d.ts +28 -0
  155. package/dist/migrations.d.ts.map +1 -0
  156. package/dist/migrations.js +125 -0
  157. package/dist/migrations.js.map +1 -0
  158. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  159. package/dist/modes/interactive/components/assistant-message.js +3 -4
  160. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  161. package/dist/modes/interactive/components/bash-execution.d.ts +1 -1
  162. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/bash-execution.js +6 -2
  164. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  165. package/dist/modes/interactive/components/bordered-loader.d.ts +12 -0
  166. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -0
  167. package/dist/modes/interactive/components/bordered-loader.js +30 -0
  168. package/dist/modes/interactive/components/bordered-loader.js.map +1 -0
  169. package/dist/modes/interactive/components/branch-summary-message.d.ts +14 -0
  170. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -0
  171. package/dist/modes/interactive/components/branch-summary-message.js +35 -0
  172. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -0
  173. package/dist/modes/interactive/components/compaction-summary-message.d.ts +14 -0
  174. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -0
  175. package/dist/modes/interactive/components/compaction-summary-message.js +36 -0
  176. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -0
  177. package/dist/modes/interactive/components/dynamic-border.d.ts +5 -1
  178. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  179. package/dist/modes/interactive/components/dynamic-border.js +5 -1
  180. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  181. package/dist/modes/interactive/components/footer.d.ts +12 -6
  182. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  183. package/dist/modes/interactive/components/footer.js +57 -25
  184. package/dist/modes/interactive/components/footer.js.map +1 -1
  185. package/dist/modes/interactive/components/hook-editor.d.ts +15 -0
  186. package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -0
  187. package/dist/modes/interactive/components/hook-editor.js +95 -0
  188. package/dist/modes/interactive/components/hook-editor.js.map +1 -0
  189. package/dist/modes/interactive/components/hook-message.d.ts +18 -0
  190. package/dist/modes/interactive/components/hook-message.d.ts.map +1 -0
  191. package/dist/modes/interactive/components/hook-message.js +80 -0
  192. package/dist/modes/interactive/components/hook-message.js.map +1 -0
  193. package/dist/modes/interactive/components/model-selector.d.ts +3 -3
  194. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/model-selector.js +1 -1
  196. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  197. package/dist/modes/interactive/components/tool-execution.d.ts +15 -2
  198. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  199. package/dist/modes/interactive/components/tool-execution.js +70 -21
  200. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  201. package/dist/modes/interactive/components/tree-selector.d.ts +52 -0
  202. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -0
  203. package/dist/modes/interactive/components/tree-selector.js +745 -0
  204. package/dist/modes/interactive/components/tree-selector.js.map +1 -0
  205. package/dist/modes/interactive/components/user-message-selector.d.ts +3 -3
  206. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  207. package/dist/modes/interactive/components/user-message-selector.js +1 -1
  208. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  209. package/dist/modes/interactive/components/user-message.d.ts +1 -1
  210. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  211. package/dist/modes/interactive/components/user-message.js +2 -5
  212. package/dist/modes/interactive/components/user-message.js.map +1 -1
  213. package/dist/modes/interactive/interactive-mode.d.ts +29 -12
  214. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  215. package/dist/modes/interactive/interactive-mode.js +589 -208
  216. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  217. package/dist/modes/interactive/theme/dark.json +13 -1
  218. package/dist/modes/interactive/theme/light.json +13 -1
  219. package/dist/modes/interactive/theme/theme-schema.json +34 -0
  220. package/dist/modes/interactive/theme/theme.d.ts +20 -2
  221. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  222. package/dist/modes/interactive/theme/theme.js +135 -2
  223. package/dist/modes/interactive/theme/theme.js.map +1 -1
  224. package/dist/modes/print-mode.d.ts +3 -3
  225. package/dist/modes/print-mode.d.ts.map +1 -1
  226. package/dist/modes/print-mode.js +26 -20
  227. package/dist/modes/print-mode.js.map +1 -1
  228. package/dist/modes/rpc/rpc-client.d.ts +13 -10
  229. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  230. package/dist/modes/rpc/rpc-client.js +11 -10
  231. package/dist/modes/rpc/rpc-client.js.map +1 -1
  232. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  233. package/dist/modes/rpc/rpc-mode.js +88 -35
  234. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  235. package/dist/modes/rpc/rpc-types.d.ts +30 -11
  236. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  237. package/dist/modes/rpc/rpc-types.js.map +1 -1
  238. package/dist/utils/shell.d.ts +4 -2
  239. package/dist/utils/shell.d.ts.map +1 -1
  240. package/dist/utils/shell.js +36 -7
  241. package/dist/utils/shell.js.map +1 -1
  242. package/dist/utils/tools-manager.d.ts +1 -1
  243. package/dist/utils/tools-manager.d.ts.map +1 -1
  244. package/dist/utils/tools-manager.js +2 -2
  245. package/dist/utils/tools-manager.js.map +1 -1
  246. package/docs/compaction.md +388 -0
  247. package/docs/custom-tools.md +146 -43
  248. package/docs/extension-loading.md +1004 -0
  249. package/docs/hooks.md +562 -596
  250. package/docs/rpc.md +33 -19
  251. package/docs/sdk.md +93 -21
  252. package/docs/session-tree-plan.md +441 -0
  253. package/docs/session.md +172 -21
  254. package/docs/skills.md +2 -0
  255. package/docs/theme.md +31 -2
  256. package/docs/tree.md +197 -0
  257. package/docs/tui.md +343 -0
  258. package/examples/README.md +1 -9
  259. package/examples/custom-tools/hello/index.ts +4 -3
  260. package/examples/custom-tools/question/index.ts +4 -4
  261. package/examples/custom-tools/subagent/index.ts +7 -6
  262. package/examples/custom-tools/todo/index.ts +11 -5
  263. package/examples/hooks/README.md +29 -71
  264. package/examples/hooks/auto-commit-on-exit.ts +8 -9
  265. package/examples/hooks/confirm-destructive.ts +29 -30
  266. package/examples/hooks/custom-compaction.ts +20 -21
  267. package/examples/hooks/dirty-repo-guard.ts +41 -40
  268. package/examples/hooks/file-trigger.ts +10 -5
  269. package/examples/hooks/git-checkpoint.ts +16 -12
  270. package/examples/hooks/handoff.ts +150 -0
  271. package/examples/hooks/permission-gate.ts +1 -1
  272. package/examples/hooks/protected-paths.ts +1 -1
  273. package/examples/hooks/qna.ts +119 -0
  274. package/examples/hooks/snake.ts +343 -0
  275. package/examples/hooks/status-line.ts +40 -0
  276. package/examples/sdk/01-minimal.ts +1 -1
  277. package/examples/sdk/02-custom-model.ts +1 -1
  278. package/examples/sdk/03-custom-prompt.ts +1 -1
  279. package/examples/sdk/04-skills.ts +1 -1
  280. package/examples/sdk/05-tools.ts +4 -4
  281. package/examples/sdk/06-hooks.ts +1 -1
  282. package/examples/sdk/07-context-files.ts +1 -1
  283. package/examples/sdk/08-slash-commands.ts +6 -1
  284. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  285. package/examples/sdk/10-settings.ts +1 -1
  286. package/examples/sdk/11-sessions.ts +1 -1
  287. package/examples/sdk/12-full-control.ts +4 -7
  288. package/package.json +6 -6
  289. package/dist/core/compaction.d.ts.map +0 -1
  290. package/dist/core/compaction.js +0 -412
  291. package/dist/core/compaction.js.map +0 -1
  292. package/dist/core/export-html.d.ts +0 -23
  293. package/dist/core/export-html.d.ts.map +0 -1
  294. package/dist/core/export-html.js +0 -1185
  295. package/dist/core/export-html.js.map +0 -1
  296. package/dist/modes/interactive/components/compaction.d.ts +0 -15
  297. package/dist/modes/interactive/components/compaction.d.ts.map +0 -1
  298. package/dist/modes/interactive/components/compaction.js +0 -41
  299. package/dist/modes/interactive/components/compaction.js.map +0 -1
  300. package/docs/hooks-v2.md +0 -385
  301. package/docs/session-tree.md +0 -452
@@ -1,412 +0,0 @@
1
- /**
2
- * Context compaction for long sessions.
3
- *
4
- * Pure functions for compaction logic. The session manager handles I/O,
5
- * and after compaction the session is reloaded.
6
- */
7
- import { complete } from "@mariozechner/pi-ai";
8
- import { messageTransformer } from "./messages.js";
9
- export const DEFAULT_COMPACTION_SETTINGS = {
10
- enabled: true,
11
- reserveTokens: 16384,
12
- keepRecentTokens: 20000,
13
- };
14
- // ============================================================================
15
- // Token calculation
16
- // ============================================================================
17
- /**
18
- * Calculate total context tokens from usage.
19
- * Uses the native totalTokens field when available, falls back to computing from components.
20
- */
21
- export function calculateContextTokens(usage) {
22
- return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
23
- }
24
- /**
25
- * Get usage from an assistant message if available.
26
- * Skips aborted and error messages as they don't have valid usage data.
27
- */
28
- function getAssistantUsage(msg) {
29
- if (msg.role === "assistant" && "usage" in msg) {
30
- const assistantMsg = msg;
31
- if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
32
- return assistantMsg.usage;
33
- }
34
- }
35
- return null;
36
- }
37
- /**
38
- * Find the last non-aborted assistant message usage from session entries.
39
- */
40
- export function getLastAssistantUsage(entries) {
41
- for (let i = entries.length - 1; i >= 0; i--) {
42
- const entry = entries[i];
43
- if (entry.type === "message") {
44
- const usage = getAssistantUsage(entry.message);
45
- if (usage)
46
- return usage;
47
- }
48
- }
49
- return null;
50
- }
51
- /**
52
- * Check if compaction should trigger based on context usage.
53
- */
54
- export function shouldCompact(contextTokens, contextWindow, settings) {
55
- if (!settings.enabled)
56
- return false;
57
- return contextTokens > contextWindow - settings.reserveTokens;
58
- }
59
- // ============================================================================
60
- // Cut point detection
61
- // ============================================================================
62
- /**
63
- * Estimate token count for a message using chars/4 heuristic.
64
- * This is conservative (overestimates tokens).
65
- */
66
- export function estimateTokens(message) {
67
- let chars = 0;
68
- // Handle bashExecution messages
69
- if (message.role === "bashExecution") {
70
- const bash = message;
71
- chars = bash.command.length + bash.output.length;
72
- return Math.ceil(chars / 4);
73
- }
74
- // Handle user messages
75
- if (message.role === "user") {
76
- const content = message.content;
77
- if (typeof content === "string") {
78
- chars = content.length;
79
- }
80
- else if (Array.isArray(content)) {
81
- for (const block of content) {
82
- if (block.type === "text" && block.text) {
83
- chars += block.text.length;
84
- }
85
- }
86
- }
87
- return Math.ceil(chars / 4);
88
- }
89
- // Handle assistant messages
90
- if (message.role === "assistant") {
91
- const assistant = message;
92
- for (const block of assistant.content) {
93
- if (block.type === "text") {
94
- chars += block.text.length;
95
- }
96
- else if (block.type === "thinking") {
97
- chars += block.thinking.length;
98
- }
99
- else if (block.type === "toolCall") {
100
- chars += block.name.length + JSON.stringify(block.arguments).length;
101
- }
102
- }
103
- return Math.ceil(chars / 4);
104
- }
105
- // Handle tool results
106
- if (message.role === "toolResult") {
107
- const toolResult = message;
108
- for (const block of toolResult.content) {
109
- if (block.type === "text" && block.text) {
110
- chars += block.text.length;
111
- }
112
- }
113
- return Math.ceil(chars / 4);
114
- }
115
- return 0;
116
- }
117
- /**
118
- * Find valid cut points: indices of user, assistant, or bashExecution messages.
119
- * Never cut at tool results (they must follow their tool call).
120
- * When we cut at an assistant message with tool calls, its tool results follow it
121
- * and will be kept.
122
- * BashExecutionMessage is treated like a user message (user-initiated context).
123
- */
124
- function findValidCutPoints(entries, startIndex, endIndex) {
125
- const cutPoints = [];
126
- for (let i = startIndex; i < endIndex; i++) {
127
- const entry = entries[i];
128
- if (entry.type === "message") {
129
- const role = entry.message.role;
130
- // user, assistant, and bashExecution are valid cut points
131
- // toolResult must stay with its preceding tool call
132
- if (role === "user" || role === "assistant" || role === "bashExecution") {
133
- cutPoints.push(i);
134
- }
135
- }
136
- }
137
- return cutPoints;
138
- }
139
- /**
140
- * Find the user message (or bashExecution) that starts the turn containing the given entry index.
141
- * Returns -1 if no turn start found before the index.
142
- * BashExecutionMessage is treated like a user message for turn boundaries.
143
- */
144
- export function findTurnStartIndex(entries, entryIndex, startIndex) {
145
- for (let i = entryIndex; i >= startIndex; i--) {
146
- const entry = entries[i];
147
- if (entry.type === "message") {
148
- const role = entry.message.role;
149
- if (role === "user" || role === "bashExecution") {
150
- return i;
151
- }
152
- }
153
- }
154
- return -1;
155
- }
156
- /**
157
- * Find the cut point in session entries that keeps approximately `keepRecentTokens`.
158
- *
159
- * Algorithm: Walk backwards from newest, accumulating estimated message sizes.
160
- * Stop when we've accumulated >= keepRecentTokens. Cut at that point.
161
- *
162
- * Can cut at user OR assistant messages (never tool results). When cutting at an
163
- * assistant message with tool calls, its tool results come after and will be kept.
164
- *
165
- * Returns CutPointResult with:
166
- * - firstKeptEntryIndex: the entry index to start keeping from
167
- * - turnStartIndex: if cutting mid-turn, the user message that started that turn
168
- * - isSplitTurn: whether we're cutting in the middle of a turn
169
- *
170
- * Only considers entries between `startIndex` and `endIndex` (exclusive).
171
- */
172
- export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
173
- const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
174
- if (cutPoints.length === 0) {
175
- return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };
176
- }
177
- // Walk backwards from newest, accumulating estimated message sizes
178
- let accumulatedTokens = 0;
179
- let cutIndex = startIndex; // Default: keep everything in range
180
- for (let i = endIndex - 1; i >= startIndex; i--) {
181
- const entry = entries[i];
182
- if (entry.type !== "message")
183
- continue;
184
- // Estimate this message's size
185
- const messageTokens = estimateTokens(entry.message);
186
- accumulatedTokens += messageTokens;
187
- // Check if we've exceeded the budget
188
- if (accumulatedTokens >= keepRecentTokens) {
189
- // Find the closest valid cut point at or after this entry
190
- for (let c = 0; c < cutPoints.length; c++) {
191
- if (cutPoints[c] >= i) {
192
- cutIndex = cutPoints[c];
193
- break;
194
- }
195
- }
196
- break;
197
- }
198
- }
199
- // Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)
200
- while (cutIndex > startIndex) {
201
- const prevEntry = entries[cutIndex - 1];
202
- // Stop at compaction boundaries
203
- if (prevEntry.type === "compaction") {
204
- break;
205
- }
206
- if (prevEntry.type === "message") {
207
- // Stop if we hit any message
208
- break;
209
- }
210
- // Include this non-message entry (bash, settings change, etc.)
211
- cutIndex--;
212
- }
213
- // Determine if this is a split turn
214
- const cutEntry = entries[cutIndex];
215
- const isUserMessage = cutEntry.type === "message" && cutEntry.message.role === "user";
216
- const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
217
- return {
218
- firstKeptEntryIndex: cutIndex,
219
- turnStartIndex,
220
- isSplitTurn: !isUserMessage && turnStartIndex !== -1,
221
- };
222
- }
223
- // ============================================================================
224
- // Summarization
225
- // ============================================================================
226
- const SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
227
-
228
- Include:
229
- - Current progress and key decisions made
230
- - Important context, constraints, or user preferences
231
- - Absolute file paths of any relevant files that were read or modified
232
- - What remains to be done (clear next steps)
233
- - Any critical data, examples, or references needed to continue
234
-
235
- Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;
236
- /**
237
- * Generate a summary of the conversation using the LLM.
238
- */
239
- export async function generateSummary(currentMessages, model, reserveTokens, apiKey, signal, customInstructions) {
240
- const maxTokens = Math.floor(0.8 * reserveTokens);
241
- const prompt = customInstructions
242
- ? `${SUMMARIZATION_PROMPT}\n\nAdditional focus: ${customInstructions}`
243
- : SUMMARIZATION_PROMPT;
244
- // Transform custom messages (like bashExecution) to LLM-compatible messages
245
- const transformedMessages = messageTransformer(currentMessages);
246
- const summarizationMessages = [
247
- ...transformedMessages,
248
- {
249
- role: "user",
250
- content: [{ type: "text", text: prompt }],
251
- timestamp: Date.now(),
252
- },
253
- ];
254
- const response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });
255
- const textContent = response.content
256
- .filter((c) => c.type === "text")
257
- .map((c) => c.text)
258
- .join("\n");
259
- return textContent;
260
- }
261
- export function prepareCompaction(entries, settings) {
262
- if (entries.length > 0 && entries[entries.length - 1].type === "compaction") {
263
- return null;
264
- }
265
- let prevCompactionIndex = -1;
266
- for (let i = entries.length - 1; i >= 0; i--) {
267
- if (entries[i].type === "compaction") {
268
- prevCompactionIndex = i;
269
- break;
270
- }
271
- }
272
- const boundaryStart = prevCompactionIndex + 1;
273
- const boundaryEnd = entries.length;
274
- const lastUsage = getLastAssistantUsage(entries);
275
- const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;
276
- const cutPoint = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
277
- const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
278
- // Messages to summarize (will be discarded after summary)
279
- const messagesToSummarize = [];
280
- for (let i = boundaryStart; i < historyEnd; i++) {
281
- const entry = entries[i];
282
- if (entry.type === "message") {
283
- messagesToSummarize.push(entry.message);
284
- }
285
- }
286
- // Messages to keep (recent turns, kept after summary)
287
- const messagesToKeep = [];
288
- for (let i = cutPoint.firstKeptEntryIndex; i < boundaryEnd; i++) {
289
- const entry = entries[i];
290
- if (entry.type === "message") {
291
- messagesToKeep.push(entry.message);
292
- }
293
- }
294
- return { cutPoint, messagesToSummarize, messagesToKeep, tokensBefore, boundaryStart };
295
- }
296
- // ============================================================================
297
- // Main compaction function
298
- // ============================================================================
299
- const TURN_PREFIX_SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION for a split turn.
300
- This is the PREFIX of a turn that was too large to keep in full. The SUFFIX (recent work) is being kept.
301
-
302
- Create a handoff summary that captures:
303
- - What the user originally asked for in this turn
304
- - Key decisions and progress made early in this turn
305
- - Important context needed to understand the kept suffix
306
-
307
- Be concise. Focus on information needed to understand the retained recent work.`;
308
- /**
309
- * Calculate compaction and generate summary.
310
- * Returns the CompactionEntry to append to the session file.
311
- *
312
- * @param entries - All session entries
313
- * @param model - Model to use for summarization
314
- * @param settings - Compaction settings
315
- * @param apiKey - API key for LLM
316
- * @param signal - Optional abort signal
317
- * @param customInstructions - Optional custom focus for the summary
318
- */
319
- export async function compact(entries, model, settings, apiKey, signal, customInstructions) {
320
- // Don't compact if the last entry is already a compaction
321
- if (entries.length > 0 && entries[entries.length - 1].type === "compaction") {
322
- throw new Error("Already compacted");
323
- }
324
- // Find previous compaction boundary
325
- let prevCompactionIndex = -1;
326
- for (let i = entries.length - 1; i >= 0; i--) {
327
- if (entries[i].type === "compaction") {
328
- prevCompactionIndex = i;
329
- break;
330
- }
331
- }
332
- const boundaryStart = prevCompactionIndex + 1;
333
- const boundaryEnd = entries.length;
334
- // Get token count before compaction
335
- const lastUsage = getLastAssistantUsage(entries);
336
- const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;
337
- // Find cut point (entry index) within the valid range
338
- const cutResult = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
339
- // Extract messages for history summary (before the turn that contains the cut point)
340
- const historyEnd = cutResult.isSplitTurn ? cutResult.turnStartIndex : cutResult.firstKeptEntryIndex;
341
- const historyMessages = [];
342
- for (let i = boundaryStart; i < historyEnd; i++) {
343
- const entry = entries[i];
344
- if (entry.type === "message") {
345
- historyMessages.push(entry.message);
346
- }
347
- }
348
- // Include previous summary if there was a compaction
349
- if (prevCompactionIndex >= 0) {
350
- const prevCompaction = entries[prevCompactionIndex];
351
- historyMessages.unshift({
352
- role: "user",
353
- content: `Previous session summary:\n${prevCompaction.summary}`,
354
- timestamp: Date.now(),
355
- });
356
- }
357
- // Extract messages for turn prefix summary (if splitting a turn)
358
- const turnPrefixMessages = [];
359
- if (cutResult.isSplitTurn) {
360
- for (let i = cutResult.turnStartIndex; i < cutResult.firstKeptEntryIndex; i++) {
361
- const entry = entries[i];
362
- if (entry.type === "message") {
363
- turnPrefixMessages.push(entry.message);
364
- }
365
- }
366
- }
367
- // Generate summaries (can be parallel if both needed) and merge into one
368
- let summary;
369
- if (cutResult.isSplitTurn && turnPrefixMessages.length > 0) {
370
- // Generate both summaries in parallel
371
- const [historyResult, turnPrefixResult] = await Promise.all([
372
- historyMessages.length > 0
373
- ? generateSummary(historyMessages, model, settings.reserveTokens, apiKey, signal, customInstructions)
374
- : Promise.resolve("No prior history."),
375
- generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal),
376
- ]);
377
- // Merge into single summary
378
- summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
379
- }
380
- else {
381
- // Just generate history summary
382
- summary = await generateSummary(historyMessages, model, settings.reserveTokens, apiKey, signal, customInstructions);
383
- }
384
- return {
385
- type: "compaction",
386
- timestamp: new Date().toISOString(),
387
- summary,
388
- firstKeptEntryIndex: cutResult.firstKeptEntryIndex,
389
- tokensBefore,
390
- };
391
- }
392
- /**
393
- * Generate a summary for a turn prefix (when splitting a turn).
394
- */
395
- async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, signal) {
396
- const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
397
- const transformedMessages = messageTransformer(messages);
398
- const summarizationMessages = [
399
- ...transformedMessages,
400
- {
401
- role: "user",
402
- content: [{ type: "text", text: TURN_PREFIX_SUMMARIZATION_PROMPT }],
403
- timestamp: Date.now(),
404
- },
405
- ];
406
- const response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });
407
- return response.content
408
- .filter((c) => c.type === "text")
409
- .map((c) => c.text)
410
- .join("\n");
411
- }
412
- //# sourceMappingURL=compaction.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../src/core/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context compaction for long sessions.\n *\n * Pure functions for compaction logic. The session manager handles I/O,\n * and after compaction the session is reloaded.\n */\n\nimport type { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Model, Usage } from \"@mariozechner/pi-ai\";\nimport { complete } from \"@mariozechner/pi-ai\";\nimport { messageTransformer } from \"./messages.js\";\nimport type { CompactionEntry, SessionEntry } from \"./session-manager.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n};\n\n// ============================================================================\n// Token calculation\n// ============================================================================\n\n/**\n * Calculate total context tokens from usage.\n * Uses the native totalTokens field when available, falls back to computing from components.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n * Skips aborted and error messages as they don't have valid usage data.\n */\nfunction getAssistantUsage(msg: AppMessage): Usage | null {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Check if compaction should trigger based on context usage.\n */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tif (!settings.enabled) return false;\n\treturn contextTokens > contextWindow - settings.reserveTokens;\n}\n\n// ============================================================================\n// Cut point detection\n// ============================================================================\n\n/**\n * Estimate token count for a message using chars/4 heuristic.\n * This is conservative (overestimates tokens).\n */\nexport function estimateTokens(message: AppMessage): number {\n\tlet chars = 0;\n\n\t// Handle bashExecution messages\n\tif (message.role === \"bashExecution\") {\n\t\tconst bash = message as unknown as { command: string; output: string };\n\t\tchars = bash.command.length + bash.output.length;\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle user messages\n\tif (message.role === \"user\") {\n\t\tconst content = (message as { content: string | Array<{ type: string; text?: string }> }).content;\n\t\tif (typeof content === \"string\") {\n\t\t\tchars = content.length;\n\t\t} else if (Array.isArray(content)) {\n\t\t\tfor (const block of content) {\n\t\t\t\tif (block.type === \"text\" && block.text) {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle assistant messages\n\tif (message.role === \"assistant\") {\n\t\tconst assistant = message as AssistantMessage;\n\t\tfor (const block of assistant.content) {\n\t\t\tif (block.type === \"text\") {\n\t\t\t\tchars += block.text.length;\n\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\tchars += block.thinking.length;\n\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\tchars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle tool results\n\tif (message.role === \"toolResult\") {\n\t\tconst toolResult = message as { content: Array<{ type: string; text?: string }> };\n\t\tfor (const block of toolResult.content) {\n\t\t\tif (block.type === \"text\" && block.text) {\n\t\t\t\tchars += block.text.length;\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\treturn 0;\n}\n\n/**\n * Find valid cut points: indices of user, assistant, or bashExecution messages.\n * Never cut at tool results (they must follow their tool call).\n * When we cut at an assistant message with tool calls, its tool results follow it\n * and will be kept.\n * BashExecutionMessage is treated like a user message (user-initiated context).\n */\nfunction findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst cutPoints: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\t// user, assistant, and bashExecution are valid cut points\n\t\t\t// toolResult must stay with its preceding tool call\n\t\t\tif (role === \"user\" || role === \"assistant\" || role === \"bashExecution\") {\n\t\t\t\tcutPoints.push(i);\n\t\t\t}\n\t\t}\n\t}\n\treturn cutPoints;\n}\n\n/**\n * Find the user message (or bashExecution) that starts the turn containing the given entry index.\n * Returns -1 if no turn start found before the index.\n * BashExecutionMessage is treated like a user message for turn boundaries.\n */\nexport function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number {\n\tfor (let i = entryIndex; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\tif (role === \"user\" || role === \"bashExecution\") {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t}\n\treturn -1;\n}\n\nexport interface CutPointResult {\n\t/** Index of first entry to keep */\n\tfirstKeptEntryIndex: number;\n\t/** Index of user message that starts the turn being split, or -1 if not splitting */\n\tturnStartIndex: number;\n\t/** Whether this cut splits a turn (cut point is not a user message) */\n\tisSplitTurn: boolean;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n *\n * Algorithm: Walk backwards from newest, accumulating estimated message sizes.\n * Stop when we've accumulated >= keepRecentTokens. Cut at that point.\n *\n * Can cut at user OR assistant messages (never tool results). When cutting at an\n * assistant message with tool calls, its tool results come after and will be kept.\n *\n * Returns CutPointResult with:\n * - firstKeptEntryIndex: the entry index to start keeping from\n * - turnStartIndex: if cutting mid-turn, the user message that started that turn\n * - isSplitTurn: whether we're cutting in the middle of a turn\n *\n * Only considers entries between `startIndex` and `endIndex` (exclusive).\n */\nexport function findCutPoint(\n\tentries: SessionEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): CutPointResult {\n\tconst cutPoints = findValidCutPoints(entries, startIndex, endIndex);\n\n\tif (cutPoints.length === 0) {\n\t\treturn { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };\n\t}\n\n\t// Walk backwards from newest, accumulating estimated message sizes\n\tlet accumulatedTokens = 0;\n\tlet cutIndex = startIndex; // Default: keep everything in range\n\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type !== \"message\") continue;\n\n\t\t// Estimate this message's size\n\t\tconst messageTokens = estimateTokens(entry.message);\n\t\taccumulatedTokens += messageTokens;\n\n\t\t// Check if we've exceeded the budget\n\t\tif (accumulatedTokens >= keepRecentTokens) {\n\t\t\t// Find the closest valid cut point at or after this entry\n\t\t\tfor (let c = 0; c < cutPoints.length; c++) {\n\t\t\t\tif (cutPoints[c] >= i) {\n\t\t\t\t\tcutIndex = cutPoints[c];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\t// Stop at compaction boundaries\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\t// Stop if we hit any message\n\t\t\tbreak;\n\t\t}\n\t\t// Include this non-message entry (bash, settings change, etc.)\n\t\tcutIndex--;\n\t}\n\n\t// Determine if this is a split turn\n\tconst cutEntry = entries[cutIndex];\n\tconst isUserMessage = cutEntry.type === \"message\" && cutEntry.message.role === \"user\";\n\tconst turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);\n\n\treturn {\n\t\tfirstKeptEntryIndex: cutIndex,\n\t\tturnStartIndex,\n\t\tisSplitTurn: !isUserMessage && turnStartIndex !== -1,\n\t};\n}\n\n// ============================================================================\n// Summarization\n// ============================================================================\n\nconst SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.\n\nInclude:\n- Current progress and key decisions made\n- Important context, constraints, or user preferences\n- Absolute file paths of any relevant files that were read or modified\n- What remains to be done (clear next steps)\n- Any critical data, examples, or references needed to continue\n\nBe concise, structured, and focused on helping the next LLM seamlessly continue the work.`;\n\n/**\n * Generate a summary of the conversation using the LLM.\n */\nexport async function generateSummary(\n\tcurrentMessages: AppMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<string> {\n\tconst maxTokens = Math.floor(0.8 * reserveTokens);\n\n\tconst prompt = customInstructions\n\t\t? `${SUMMARIZATION_PROMPT}\\n\\nAdditional focus: ${customInstructions}`\n\t\t: SUMMARIZATION_PROMPT;\n\n\t// Transform custom messages (like bashExecution) to LLM-compatible messages\n\tconst transformedMessages = messageTransformer(currentMessages);\n\n\tconst summarizationMessages = [\n\t\t...transformedMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: prompt }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });\n\n\tconst textContent = response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n\n\treturn textContent;\n}\n\n// ============================================================================\n// Compaction Preparation (for hooks)\n// ============================================================================\n\nexport interface CompactionPreparation {\n\tcutPoint: CutPointResult;\n\t/** Messages that will be summarized and discarded */\n\tmessagesToSummarize: AppMessage[];\n\t/** Messages that will be kept after the summary (recent turns) */\n\tmessagesToKeep: AppMessage[];\n\ttokensBefore: number;\n\tboundaryStart: number;\n}\n\nexport function prepareCompaction(entries: SessionEntry[], settings: CompactionSettings): CompactionPreparation | null {\n\tif (entries.length > 0 && entries[entries.length - 1].type === \"compaction\") {\n\t\treturn null;\n\t}\n\n\tlet prevCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst boundaryStart = prevCompactionIndex + 1;\n\tconst boundaryEnd = entries.length;\n\n\tconst lastUsage = getLastAssistantUsage(entries);\n\tconst tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;\n\n\tconst cutPoint = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\tconst historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;\n\n\t// Messages to summarize (will be discarded after summary)\n\tconst messagesToSummarize: AppMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tmessagesToSummarize.push(entry.message);\n\t\t}\n\t}\n\n\t// Messages to keep (recent turns, kept after summary)\n\tconst messagesToKeep: AppMessage[] = [];\n\tfor (let i = cutPoint.firstKeptEntryIndex; i < boundaryEnd; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tmessagesToKeep.push(entry.message);\n\t\t}\n\t}\n\n\treturn { cutPoint, messagesToSummarize, messagesToKeep, tokensBefore, boundaryStart };\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\nconst TURN_PREFIX_SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION for a split turn. \nThis is the PREFIX of a turn that was too large to keep in full. The SUFFIX (recent work) is being kept.\n\nCreate a handoff summary that captures:\n- What the user originally asked for in this turn\n- Key decisions and progress made early in this turn\n- Important context needed to understand the kept suffix\n\nBe concise. Focus on information needed to understand the retained recent work.`;\n\n/**\n * Calculate compaction and generate summary.\n * Returns the CompactionEntry to append to the session file.\n *\n * @param entries - All session entries\n * @param model - Model to use for summarization\n * @param settings - Compaction settings\n * @param apiKey - API key for LLM\n * @param signal - Optional abort signal\n * @param customInstructions - Optional custom focus for the summary\n */\nexport async function compact(\n\tentries: SessionEntry[],\n\tmodel: Model<any>,\n\tsettings: CompactionSettings,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<CompactionEntry> {\n\t// Don't compact if the last entry is already a compaction\n\tif (entries.length > 0 && entries[entries.length - 1].type === \"compaction\") {\n\t\tthrow new Error(\"Already compacted\");\n\t}\n\n\t// Find previous compaction boundary\n\tlet prevCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst boundaryStart = prevCompactionIndex + 1;\n\tconst boundaryEnd = entries.length;\n\n\t// Get token count before compaction\n\tconst lastUsage = getLastAssistantUsage(entries);\n\tconst tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;\n\n\t// Find cut point (entry index) within the valid range\n\tconst cutResult = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Extract messages for history summary (before the turn that contains the cut point)\n\tconst historyEnd = cutResult.isSplitTurn ? cutResult.turnStartIndex : cutResult.firstKeptEntryIndex;\n\tconst historyMessages: AppMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\thistoryMessages.push(entry.message);\n\t\t}\n\t}\n\n\t// Include previous summary if there was a compaction\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\thistoryMessages.unshift({\n\t\t\trole: \"user\",\n\t\t\tcontent: `Previous session summary:\\n${prevCompaction.summary}`,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t// Extract messages for turn prefix summary (if splitting a turn)\n\tconst turnPrefixMessages: AppMessage[] = [];\n\tif (cutResult.isSplitTurn) {\n\t\tfor (let i = cutResult.turnStartIndex; i < cutResult.firstKeptEntryIndex; i++) {\n\t\t\tconst entry = entries[i];\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tturnPrefixMessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Generate summaries (can be parallel if both needed) and merge into one\n\tlet summary: string;\n\n\tif (cutResult.isSplitTurn && turnPrefixMessages.length > 0) {\n\t\t// Generate both summaries in parallel\n\t\tconst [historyResult, turnPrefixResult] = await Promise.all([\n\t\t\thistoryMessages.length > 0\n\t\t\t\t? generateSummary(historyMessages, model, settings.reserveTokens, apiKey, signal, customInstructions)\n\t\t\t\t: Promise.resolve(\"No prior history.\"),\n\t\t\tgenerateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal),\n\t\t]);\n\t\t// Merge into single summary\n\t\tsummary = `${historyResult}\\n\\n---\\n\\n**Turn Context (split turn):**\\n\\n${turnPrefixResult}`;\n\t} else {\n\t\t// Just generate history summary\n\t\tsummary = await generateSummary(\n\t\t\thistoryMessages,\n\t\t\tmodel,\n\t\t\tsettings.reserveTokens,\n\t\t\tapiKey,\n\t\t\tsignal,\n\t\t\tcustomInstructions,\n\t\t);\n\t}\n\n\treturn {\n\t\ttype: \"compaction\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tsummary,\n\t\tfirstKeptEntryIndex: cutResult.firstKeptEntryIndex,\n\t\ttokensBefore,\n\t};\n}\n\n/**\n * Generate a summary for a turn prefix (when splitting a turn).\n */\nasync function generateTurnPrefixSummary(\n\tmessages: AppMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n): Promise<string> {\n\tconst maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix\n\n\tconst transformedMessages = messageTransformer(messages);\n\tconst summarizationMessages = [\n\t\t...transformedMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: TURN_PREFIX_SUMMARIZATION_PROMPT }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });\n\n\treturn response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n}\n"]}
@@ -1,23 +0,0 @@
1
- import type { AgentState } from "@mariozechner/pi-agent-core";
2
- import type { SessionManager } from "./session-manager.js";
3
- export interface ExportOptions {
4
- outputPath?: string;
5
- themeName?: string;
6
- }
7
- /**
8
- * Export session to HTML using SessionManager and AgentState.
9
- * Used by TUI's /export command.
10
- * @param sessionManager The session manager
11
- * @param state The agent state
12
- * @param options Export options including output path and theme name
13
- */
14
- export declare function exportSessionToHtml(sessionManager: SessionManager, state: AgentState, options?: ExportOptions | string): string;
15
- /**
16
- * Export session file to HTML (standalone, without AgentState).
17
- * Auto-detects format: session manager format or streaming event format.
18
- * Used by CLI for exporting arbitrary session files.
19
- * @param inputPath Path to the session file
20
- * @param options Export options including output path and theme name
21
- */
22
- export declare function exportFromFile(inputPath: string, options?: ExportOptions | string): string;
23
- //# sourceMappingURL=export-html.d.ts.map