@code-yeongyu/senpi 2026.5.15 → 2026.5.18-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/CHANGELOG.md +1172 -1161
  2. package/README.md +1 -2
  3. package/dist/cli/config-selector.d.ts.map +1 -1
  4. package/dist/cli/config-selector.js +1 -1
  5. package/dist/cli/config-selector.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +5 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +12 -3
  11. package/dist/config.js.map +1 -1
  12. package/dist/core/agent-session.d.ts +11 -0
  13. package/dist/core/agent-session.d.ts.map +1 -1
  14. package/dist/core/agent-session.js +160 -13
  15. package/dist/core/agent-session.js.map +1 -1
  16. package/dist/core/compaction/compaction.d.ts +5 -3
  17. package/dist/core/compaction/compaction.d.ts.map +1 -1
  18. package/dist/core/compaction/compaction.js +22 -14
  19. package/dist/core/compaction/compaction.js.map +1 -1
  20. package/dist/core/dynamic-prompt/verification.d.ts +31 -0
  21. package/dist/core/dynamic-prompt/verification.d.ts.map +1 -1
  22. package/dist/core/dynamic-prompt/verification.js +41 -0
  23. package/dist/core/dynamic-prompt/verification.js.map +1 -1
  24. package/dist/core/extensions/builtin/compaction/context-reduction.d.ts +97 -0
  25. package/dist/core/extensions/builtin/compaction/context-reduction.d.ts.map +1 -0
  26. package/dist/core/extensions/builtin/compaction/context-reduction.js +420 -0
  27. package/dist/core/extensions/builtin/compaction/context-reduction.js.map +1 -0
  28. package/dist/core/extensions/builtin/compaction/index.d.ts.map +1 -1
  29. package/dist/core/extensions/builtin/compaction/index.js +168 -31
  30. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  31. package/dist/core/extensions/builtin/compaction/openai-remote.d.ts +197 -0
  32. package/dist/core/extensions/builtin/compaction/openai-remote.d.ts.map +1 -0
  33. package/dist/core/extensions/builtin/compaction/openai-remote.js +690 -0
  34. package/dist/core/extensions/builtin/compaction/openai-remote.js.map +1 -0
  35. package/dist/core/extensions/builtin/compaction/prompts.d.ts +3 -3
  36. package/dist/core/extensions/builtin/compaction/prompts.d.ts.map +1 -1
  37. package/dist/core/extensions/builtin/compaction/prompts.js +0 -22
  38. package/dist/core/extensions/builtin/compaction/prompts.js.map +1 -1
  39. package/dist/core/extensions/builtin/compaction/repair-tool-pairs.d.ts +4 -0
  40. package/dist/core/extensions/builtin/compaction/repair-tool-pairs.d.ts.map +1 -0
  41. package/dist/core/extensions/builtin/compaction/repair-tool-pairs.js +48 -0
  42. package/dist/core/extensions/builtin/compaction/repair-tool-pairs.js.map +1 -0
  43. package/dist/core/extensions/builtin/compaction/speculative.d.ts +3 -1
  44. package/dist/core/extensions/builtin/compaction/speculative.d.ts.map +1 -1
  45. package/dist/core/extensions/builtin/compaction/speculative.js +80 -33
  46. package/dist/core/extensions/builtin/compaction/speculative.js.map +1 -1
  47. package/dist/core/extensions/builtin/compaction/todo-bridge.d.ts +8 -0
  48. package/dist/core/extensions/builtin/compaction/todo-bridge.d.ts.map +1 -1
  49. package/dist/core/extensions/builtin/compaction/todo-bridge.js +12 -6
  50. package/dist/core/extensions/builtin/compaction/todo-bridge.js.map +1 -1
  51. package/dist/core/extensions/builtin/diff.d.ts.map +1 -1
  52. package/dist/core/extensions/builtin/diff.js +1 -1
  53. package/dist/core/extensions/builtin/diff.js.map +1 -1
  54. package/dist/core/extensions/builtin/gpt-apply-patch/preview-format.d.ts.map +1 -1
  55. package/dist/core/extensions/builtin/gpt-apply-patch/preview-format.js +5 -128
  56. package/dist/core/extensions/builtin/gpt-apply-patch/preview-format.js.map +1 -1
  57. package/dist/core/extensions/builtin/index.d.ts.map +1 -1
  58. package/dist/core/extensions/builtin/index.js +0 -2
  59. package/dist/core/extensions/builtin/index.js.map +1 -1
  60. package/dist/core/extensions/builtin/openai-web-search/index.d.ts +6 -2
  61. package/dist/core/extensions/builtin/openai-web-search/index.d.ts.map +1 -1
  62. package/dist/core/extensions/builtin/openai-web-search/index.js +82 -10
  63. package/dist/core/extensions/builtin/openai-web-search/index.js.map +1 -1
  64. package/dist/core/extensions/builtin/permission-system/prompt.d.ts.map +1 -1
  65. package/dist/core/extensions/builtin/permission-system/prompt.js +0 -5
  66. package/dist/core/extensions/builtin/permission-system/prompt.js.map +1 -1
  67. package/dist/core/extensions/builtin/system-messages.d.ts +1 -1
  68. package/dist/core/extensions/builtin/system-messages.d.ts.map +1 -1
  69. package/dist/core/extensions/builtin/system-messages.js.map +1 -1
  70. package/dist/core/extensions/builtin/tool-pair-guard/index.d.ts +1 -1
  71. package/dist/core/extensions/builtin/tool-pair-guard/index.d.ts.map +1 -1
  72. package/dist/core/extensions/builtin/tool-pair-guard/index.js +8 -4
  73. package/dist/core/extensions/builtin/tool-pair-guard/index.js.map +1 -1
  74. package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-chat-completions-payload.d.ts +3 -0
  75. package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-chat-completions-payload.d.ts.map +1 -0
  76. package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-chat-completions-payload.js +89 -0
  77. package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-chat-completions-payload.js.map +1 -0
  78. package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-responses-payload.d.ts +3 -0
  79. package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-responses-payload.d.ts.map +1 -0
  80. package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-responses-payload.js +122 -0
  81. package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-responses-payload.js.map +1 -0
  82. package/dist/core/extensions/loader.d.ts.map +1 -1
  83. package/dist/core/extensions/loader.js +2 -0
  84. package/dist/core/extensions/loader.js.map +1 -1
  85. package/dist/core/extensions/runner.d.ts +3 -0
  86. package/dist/core/extensions/runner.d.ts.map +1 -1
  87. package/dist/core/extensions/runner.js +18 -0
  88. package/dist/core/extensions/runner.js.map +1 -1
  89. package/dist/core/extensions/types.d.ts +22 -0
  90. package/dist/core/extensions/types.d.ts.map +1 -1
  91. package/dist/core/extensions/types.js.map +1 -1
  92. package/dist/core/messages.d.ts +3 -3
  93. package/dist/core/messages.d.ts.map +1 -1
  94. package/dist/core/messages.js +5 -10
  95. package/dist/core/messages.js.map +1 -1
  96. package/dist/core/model-registry.d.ts +1 -0
  97. package/dist/core/model-registry.d.ts.map +1 -1
  98. package/dist/core/model-registry.js +66 -9
  99. package/dist/core/model-registry.js.map +1 -1
  100. package/dist/core/package-manager.d.ts +5 -0
  101. package/dist/core/package-manager.d.ts.map +1 -1
  102. package/dist/core/package-manager.js +72 -31
  103. package/dist/core/package-manager.js.map +1 -1
  104. package/dist/core/prompt-templates.d.ts.map +1 -1
  105. package/dist/core/prompt-templates.js +6 -4
  106. package/dist/core/prompt-templates.js.map +1 -1
  107. package/dist/core/sdk.d.ts +1 -1
  108. package/dist/core/sdk.d.ts.map +1 -1
  109. package/dist/core/sdk.js +7 -22
  110. package/dist/core/sdk.js.map +1 -1
  111. package/dist/core/session-manager.d.ts.map +1 -1
  112. package/dist/core/session-manager.js +39 -9
  113. package/dist/core/session-manager.js.map +1 -1
  114. package/dist/core/settings-manager.d.ts +0 -5
  115. package/dist/core/settings-manager.d.ts.map +1 -1
  116. package/dist/core/settings-manager.js.map +1 -1
  117. package/dist/core/skills.d.ts.map +1 -1
  118. package/dist/core/skills.js +2 -5
  119. package/dist/core/skills.js.map +1 -1
  120. package/dist/core/system-prompt.d.ts.map +1 -1
  121. package/dist/core/system-prompt.js +3 -2
  122. package/dist/core/system-prompt.js.map +1 -1
  123. package/dist/core/thinking-levels.d.ts +6 -0
  124. package/dist/core/thinking-levels.d.ts.map +1 -0
  125. package/dist/core/thinking-levels.js +36 -0
  126. package/dist/core/thinking-levels.js.map +1 -0
  127. package/dist/core/tools/bash.d.ts.map +1 -1
  128. package/dist/core/tools/bash.js +15 -1
  129. package/dist/core/tools/bash.js.map +1 -1
  130. package/dist/core/tools/diff-render.d.ts +13 -0
  131. package/dist/core/tools/diff-render.d.ts.map +1 -0
  132. package/dist/core/tools/diff-render.js +130 -0
  133. package/dist/core/tools/diff-render.js.map +1 -0
  134. package/dist/core/tools/edit.d.ts.map +1 -1
  135. package/dist/core/tools/edit.js +8 -3
  136. package/dist/core/tools/edit.js.map +1 -1
  137. package/dist/core/tools/write.d.ts.map +1 -1
  138. package/dist/core/tools/write.js +28 -7
  139. package/dist/core/tools/write.js.map +1 -1
  140. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  141. package/dist/modes/interactive/components/compaction-summary-message.js +20 -2
  142. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  143. package/dist/modes/interactive/components/config-selector.d.ts +2 -2
  144. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  145. package/dist/modes/interactive/components/config-selector.js +7 -4
  146. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  147. package/dist/modes/interactive/components/footer.d.ts +0 -1
  148. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  149. package/dist/modes/interactive/components/footer.js +42 -44
  150. package/dist/modes/interactive/components/footer.js.map +1 -1
  151. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  152. package/dist/modes/interactive/components/keybinding-hints.js +3 -1
  153. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  154. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  155. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  156. package/dist/modes/interactive/interactive-mode.js +177 -82
  157. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  158. package/dist/modes/interactive/session-info-format.d.ts +3 -0
  159. package/dist/modes/interactive/session-info-format.d.ts.map +1 -0
  160. package/dist/modes/interactive/session-info-format.js +44 -0
  161. package/dist/modes/interactive/session-info-format.js.map +1 -0
  162. package/dist/modes/interactive/working-status.d.ts +21 -0
  163. package/dist/modes/interactive/working-status.d.ts.map +1 -0
  164. package/dist/modes/interactive/working-status.js +71 -0
  165. package/dist/modes/interactive/working-status.js.map +1 -0
  166. package/dist/package-manager-cli.d.ts.map +1 -1
  167. package/dist/package-manager-cli.js +3 -4
  168. package/dist/package-manager-cli.js.map +1 -1
  169. package/dist/senpi +5 -1
  170. package/dist/utils/child-process.d.ts +7 -1
  171. package/dist/utils/child-process.d.ts.map +1 -1
  172. package/dist/utils/child-process.js +60 -7
  173. package/dist/utils/child-process.js.map +1 -1
  174. package/dist/utils/clipboard-image.d.ts.map +1 -1
  175. package/dist/utils/clipboard-image.js +1 -1
  176. package/dist/utils/clipboard-image.js.map +1 -1
  177. package/dist/utils/tools-manager.d.ts.map +1 -1
  178. package/dist/utils/tools-manager.js +4 -1
  179. package/dist/utils/tools-manager.js.map +1 -1
  180. package/docs/custom-provider.md +55 -0
  181. package/docs/extensions.md +1 -2
  182. package/docs/index.md +0 -1
  183. package/docs/models.md +9 -0
  184. package/docs/sdk.md +0 -1
  185. package/docs/settings.md +2 -32
  186. package/docs/skills.md +3 -4
  187. package/docs/termux.md +2 -2
  188. package/docs/usage.md +1 -1
  189. package/examples/README.md +1 -1
  190. package/examples/extensions/README.md +0 -1
  191. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  192. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  193. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  194. package/examples/extensions/overlay-qa-tests.ts +1 -1
  195. package/examples/extensions/sandbox/package-lock.json +2 -2
  196. package/examples/extensions/sandbox/package.json +1 -1
  197. package/examples/extensions/with-deps/package-lock.json +2 -2
  198. package/examples/extensions/with-deps/package.json +1 -1
  199. package/package.json +6 -6
  200. package/dist/core/extensions/builtin/background-task/cancel-tool.d.ts +0 -10
  201. package/dist/core/extensions/builtin/background-task/cancel-tool.d.ts.map +0 -1
  202. package/dist/core/extensions/builtin/background-task/cancel-tool.js +0 -109
  203. package/dist/core/extensions/builtin/background-task/cancel-tool.js.map +0 -1
  204. package/dist/core/extensions/builtin/background-task/index.d.ts +0 -3
  205. package/dist/core/extensions/builtin/background-task/index.d.ts.map +0 -1
  206. package/dist/core/extensions/builtin/background-task/index.js +0 -207
  207. package/dist/core/extensions/builtin/background-task/index.js.map +0 -1
  208. package/dist/core/extensions/builtin/background-task/manager.d.ts +0 -17
  209. package/dist/core/extensions/builtin/background-task/manager.d.ts.map +0 -1
  210. package/dist/core/extensions/builtin/background-task/manager.js +0 -114
  211. package/dist/core/extensions/builtin/background-task/manager.js.map +0 -1
  212. package/dist/core/extensions/builtin/background-task/notification.d.ts +0 -22
  213. package/dist/core/extensions/builtin/background-task/notification.d.ts.map +0 -1
  214. package/dist/core/extensions/builtin/background-task/notification.js +0 -105
  215. package/dist/core/extensions/builtin/background-task/notification.js.map +0 -1
  216. package/dist/core/extensions/builtin/background-task/output-tool.d.ts +0 -11
  217. package/dist/core/extensions/builtin/background-task/output-tool.d.ts.map +0 -1
  218. package/dist/core/extensions/builtin/background-task/output-tool.js +0 -127
  219. package/dist/core/extensions/builtin/background-task/output-tool.js.map +0 -1
  220. package/dist/core/extensions/builtin/background-task/spawner.d.ts +0 -8
  221. package/dist/core/extensions/builtin/background-task/spawner.d.ts.map +0 -1
  222. package/dist/core/extensions/builtin/background-task/spawner.js +0 -207
  223. package/dist/core/extensions/builtin/background-task/spawner.js.map +0 -1
  224. package/dist/core/extensions/builtin/background-task/task-tool.d.ts +0 -20
  225. package/dist/core/extensions/builtin/background-task/task-tool.d.ts.map +0 -1
  226. package/dist/core/extensions/builtin/background-task/task-tool.js +0 -302
  227. package/dist/core/extensions/builtin/background-task/task-tool.js.map +0 -1
  228. package/dist/core/extensions/builtin/background-task/types.d.ts +0 -72
  229. package/dist/core/extensions/builtin/background-task/types.d.ts.map +0 -1
  230. package/dist/core/extensions/builtin/background-task/types.js +0 -32
  231. package/dist/core/extensions/builtin/background-task/types.js.map +0 -1
  232. package/docs/agents.md +0 -348
  233. package/examples/extensions/subagent/README.md +0 -172
  234. package/examples/extensions/subagent/agents/planner.md +0 -37
  235. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  236. package/examples/extensions/subagent/agents/scout.md +0 -50
  237. package/examples/extensions/subagent/agents/worker.md +0 -24
  238. package/examples/extensions/subagent/agents.ts +0 -126
  239. package/examples/extensions/subagent/index.ts +0 -987
  240. package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
  241. package/examples/extensions/subagent/prompts/implement.md +0 -10
  242. package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
@@ -1,3 +1,42 @@
1
+ export const TEST_DISCIPLINE_RULES = [
2
+ {
3
+ id: "deterministic-tests",
4
+ concern: "test-determinism",
5
+ directive: "When you read or edit test code, treat nondeterminism as a bug; tests must not pass by timing luck.",
6
+ },
7
+ {
8
+ id: "fixed-wait-ban",
9
+ concern: "async-test-orchestration",
10
+ directive: "Unless time itself is the behavior under test, fixed sleeps, polling delays, and wait-for-time patterns are forbidden.",
11
+ },
12
+ {
13
+ id: "event-timeout-pattern",
14
+ concern: "async-test-orchestration",
15
+ directive: "For async behavior, subscribe to the exact event or state change before triggering the action, then await that signal with a bounded timeout.",
16
+ },
17
+ {
18
+ id: "mock-contract-integrity",
19
+ concern: "mock-contracts",
20
+ directive: "Mocks must preserve the contract being asserted; do not isolate so heavily that the integration under test cannot fail.",
21
+ },
22
+ {
23
+ id: "prompt-behavior-coverage",
24
+ concern: "prompt-tests",
25
+ directive: "Prompt tests must assert behavior, decisions, structure, or parsed rule data rather than merely pinning an exact prompt sentence.",
26
+ },
27
+ {
28
+ id: "single-pass-runner",
29
+ concern: "test-runner",
30
+ directive: "Run the relevant test command once and make that pass reliable; for Bun test targets, bun test must pass in a single run.",
31
+ },
32
+ ];
33
+ export function buildTestDisciplineSection() {
34
+ const lines = ["### Test Discipline"];
35
+ for (const rule of TEST_DISCIPLINE_RULES) {
36
+ lines.push(`- ${rule.directive}`);
37
+ }
38
+ return lines.join("\n");
39
+ }
1
40
  export function buildVerificationSection() {
2
41
  return `## Verification
3
42
 
@@ -7,6 +46,8 @@ Tier the scope, never the rigor.
7
46
  - V2 — single-domain behavioral edits: diagnostics on changed files in parallel, related tests, one execution of the affected runnable entry point when one exists.
8
47
  - V3 — multi-file or cross-cutting work: diagnostics on every changed file, related tests, build, manual exercise of user-visible behavior through its real surface.
9
48
 
49
+ ${buildTestDisciplineSection()}
50
+
10
51
  "Should pass" is not verification. Reporting clean output without running the validator is a violation. Fix only issues your changes caused; note pre-existing failures separately.`;
11
52
  }
12
53
  //# sourceMappingURL=verification.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"verification.js","sourceRoot":"","sources":["../../../src/core/dynamic-prompt/verification.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,wBAAwB,GAAW;IAClD,OAAO;;;;;;;;oLAQ4K,CAAC;AAAA,CACpL","sourcesContent":["export function buildVerificationSection(): string {\n\treturn `## Verification\n\nTier the scope, never the rigor.\n\n- V1 — single-file non-behavioral edits: diagnostics on that file. Done.\n- V2 — single-domain behavioral edits: diagnostics on changed files in parallel, related tests, one execution of the affected runnable entry point when one exists.\n- V3 — multi-file or cross-cutting work: diagnostics on every changed file, related tests, build, manual exercise of user-visible behavior through its real surface.\n\n\"Should pass\" is not verification. Reporting clean output without running the validator is a violation. Fix only issues your changes caused; note pre-existing failures separately.`;\n}\n"]}
1
+ {"version":3,"file":"verification.js","sourceRoot":"","sources":["../../../src/core/dynamic-prompt/verification.ts"],"names":[],"mappings":"AAYA,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACpC;QACC,EAAE,EAAE,qBAAqB;QACzB,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,qGAAqG;KAChH;IACD;QACC,EAAE,EAAE,gBAAgB;QACpB,OAAO,EAAE,0BAA0B;QACnC,SAAS,EACR,wHAAwH;KACzH;IACD;QACC,EAAE,EAAE,uBAAuB;QAC3B,OAAO,EAAE,0BAA0B;QACnC,SAAS,EACR,+IAA+I;KAChJ;IACD;QACC,EAAE,EAAE,yBAAyB;QAC7B,OAAO,EAAE,gBAAgB;QACzB,SAAS,EACR,yHAAyH;KAC1H;IACD;QACC,EAAE,EAAE,0BAA0B;QAC9B,OAAO,EAAE,cAAc;QACvB,SAAS,EACR,mIAAmI;KACpI;IACD;QACC,EAAE,EAAE,oBAAoB;QACxB,OAAO,EAAE,aAAa;QACtB,SAAS,EACR,2HAA2H;KAC5H;CACgD,CAAC;AAEnD,MAAM,UAAU,0BAA0B,GAAW;IACpD,MAAM,KAAK,GAAG,CAAC,qBAAqB,CAAC,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,qBAAqB,EAAE,CAAC;QAC1C,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACxB;AAED,MAAM,UAAU,wBAAwB,GAAW;IAClD,OAAO;;;;;;;;EAQN,0BAA0B,EAAE;;oLAEsJ,CAAC;AAAA,CACpL","sourcesContent":["export type TestDisciplineRule = {\n\tid:\n\t\t| \"deterministic-tests\"\n\t\t| \"fixed-wait-ban\"\n\t\t| \"event-timeout-pattern\"\n\t\t| \"mock-contract-integrity\"\n\t\t| \"prompt-behavior-coverage\"\n\t\t| \"single-pass-runner\";\n\tconcern: \"test-determinism\" | \"async-test-orchestration\" | \"mock-contracts\" | \"prompt-tests\" | \"test-runner\";\n\tdirective: string;\n};\n\nexport const TEST_DISCIPLINE_RULES = [\n\t{\n\t\tid: \"deterministic-tests\",\n\t\tconcern: \"test-determinism\",\n\t\tdirective: \"When you read or edit test code, treat nondeterminism as a bug; tests must not pass by timing luck.\",\n\t},\n\t{\n\t\tid: \"fixed-wait-ban\",\n\t\tconcern: \"async-test-orchestration\",\n\t\tdirective:\n\t\t\t\"Unless time itself is the behavior under test, fixed sleeps, polling delays, and wait-for-time patterns are forbidden.\",\n\t},\n\t{\n\t\tid: \"event-timeout-pattern\",\n\t\tconcern: \"async-test-orchestration\",\n\t\tdirective:\n\t\t\t\"For async behavior, subscribe to the exact event or state change before triggering the action, then await that signal with a bounded timeout.\",\n\t},\n\t{\n\t\tid: \"mock-contract-integrity\",\n\t\tconcern: \"mock-contracts\",\n\t\tdirective:\n\t\t\t\"Mocks must preserve the contract being asserted; do not isolate so heavily that the integration under test cannot fail.\",\n\t},\n\t{\n\t\tid: \"prompt-behavior-coverage\",\n\t\tconcern: \"prompt-tests\",\n\t\tdirective:\n\t\t\t\"Prompt tests must assert behavior, decisions, structure, or parsed rule data rather than merely pinning an exact prompt sentence.\",\n\t},\n\t{\n\t\tid: \"single-pass-runner\",\n\t\tconcern: \"test-runner\",\n\t\tdirective:\n\t\t\t\"Run the relevant test command once and make that pass reliable; for Bun test targets, bun test must pass in a single run.\",\n\t},\n] as const satisfies readonly TestDisciplineRule[];\n\nexport function buildTestDisciplineSection(): string {\n\tconst lines = [\"### Test Discipline\"];\n\tfor (const rule of TEST_DISCIPLINE_RULES) {\n\t\tlines.push(`- ${rule.directive}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport function buildVerificationSection(): string {\n\treturn `## Verification\n\nTier the scope, never the rigor.\n\n- V1 — single-file non-behavioral edits: diagnostics on that file. Done.\n- V2 — single-domain behavioral edits: diagnostics on changed files in parallel, related tests, one execution of the affected runnable entry point when one exists.\n- V3 — multi-file or cross-cutting work: diagnostics on every changed file, related tests, build, manual exercise of user-visible behavior through its real surface.\n\n${buildTestDisciplineSection()}\n\n\"Should pass\" is not verification. Reporting clean output without running the validator is a violation. Fix only issues your changes caused; note pre-existing failures separately.`;\n}\n"]}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Deterministic, no-LLM context reductions applied before compaction summarization.
3
+ *
4
+ * Ported from plugsuits' `context-collapse` and `micro-compact` patterns and
5
+ * adapted to the senpi `AgentMessage` shape. Three independent transforms:
6
+ *
7
+ * 1. {@link collapseConsecutiveToolResults} — runs of same-kind read/grep/shell
8
+ * tool result payloads are replaced with a single one-line label so the
9
+ * summarizer pays for the shape, not for the bytes.
10
+ * 2. {@link microCompactAssistantText} — older long assistant text answers are
11
+ * truncated and tagged with a `[response shrunk]` marker.
12
+ * 3. {@link clearOldToolResults} — keep the last N tool results in full, replace
13
+ * older clearable tool result content with `[tool result cleared]`.
14
+ *
15
+ * Each transform is pure (`messages` in → new array out, no in-place mutation
16
+ * beyond freshly cloned messages) and returns aggregated token-savings stats.
17
+ *
18
+ * {@link reduceContextMessages} composes the three transforms in order.
19
+ */
20
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
21
+ export type CollapsedGroupKind = "read" | "search" | "shell";
22
+ export interface CollapsedGroup {
23
+ type: CollapsedGroupKind;
24
+ count: number;
25
+ label: string;
26
+ originalTokens: number;
27
+ collapsedTokens: number;
28
+ }
29
+ export interface CollapseConsecutiveOptions {
30
+ minGroupSize?: number;
31
+ protectRecentMessages?: number;
32
+ readToolNames?: string[];
33
+ searchToolNames?: string[];
34
+ shellToolNames?: string[];
35
+ }
36
+ export interface CollapseConsecutiveResult {
37
+ messages: AgentMessage[];
38
+ groups: CollapsedGroup[];
39
+ tokensSaved: number;
40
+ }
41
+ export interface MicroCompactAssistantOptions {
42
+ protectRecentTokens?: number;
43
+ maxAssistantTextTokens?: number;
44
+ minSavingsTokens?: number;
45
+ replacementTemplate?: string;
46
+ }
47
+ export interface MicroCompactAssistantResult {
48
+ messages: AgentMessage[];
49
+ tokensSaved: number;
50
+ messagesModified: number;
51
+ }
52
+ export interface ClearOldToolResultsOptions {
53
+ keepRecent?: number;
54
+ clearableToolNames?: string[];
55
+ replacementText?: string;
56
+ }
57
+ export interface ClearOldToolResultsResult {
58
+ messages: AgentMessage[];
59
+ tokensSaved: number;
60
+ toolResultsCleared: number;
61
+ }
62
+ export interface ReduceContextOptions {
63
+ collapse?: false | CollapseConsecutiveOptions;
64
+ shrinkAssistant?: false | MicroCompactAssistantOptions;
65
+ clearToolResults?: false | ClearOldToolResultsOptions;
66
+ }
67
+ export interface ReduceContextResult {
68
+ messages: AgentMessage[];
69
+ tokensSaved: number;
70
+ groupsCollapsed: number;
71
+ messagesShrunk: number;
72
+ toolResultsCleared: number;
73
+ }
74
+ /**
75
+ * Default options passed to {@link reduceContextMessages} when the builtin
76
+ * compaction extension's `context` hook decides to run a reduction pass.
77
+ *
78
+ * Each value is chosen to be strictly more conservative than the corresponding
79
+ * plugsuits default — protect more of the recent tail, raise the per-message
80
+ * shrink threshold, and keep more recent tool results intact — so a single
81
+ * shared default is safe to apply across normal coding sessions without making
82
+ * targeted reductions less effective.
83
+ */
84
+ export declare const BUILTIN_CONTEXT_REDUCTION_OPTIONS: ReduceContextOptions;
85
+ export declare const BUILTIN_CONTEXT_REDUCTION_GATE_RATIO = 0.5;
86
+ export interface ShouldApplyContextReductionInput {
87
+ usageTokens: number | null;
88
+ contextWindow: number;
89
+ gateRatio?: number;
90
+ isProviderNativeCompactionPath?: boolean;
91
+ }
92
+ export declare function shouldApplyContextReduction(input: ShouldApplyContextReductionInput): boolean;
93
+ export declare function collapseConsecutiveToolResults(messages: AgentMessage[], options?: CollapseConsecutiveOptions): CollapseConsecutiveResult;
94
+ export declare function microCompactAssistantText(messages: AgentMessage[], options?: MicroCompactAssistantOptions): MicroCompactAssistantResult;
95
+ export declare function clearOldToolResults(messages: AgentMessage[], options?: ClearOldToolResultsOptions): ClearOldToolResultsResult;
96
+ export declare function reduceContextMessages(messages: AgentMessage[], options?: ReduceContextOptions): ReduceContextResult;
97
+ //# sourceMappingURL=context-reduction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-reduction.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/compaction/context-reduction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAoClE,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,0BAA0B;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,4BAA4B;IAC5C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,2BAA2B;IAC3C,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,0BAA0B;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,EAAE,KAAK,GAAG,0BAA0B,CAAC;IAC9C,eAAe,CAAC,EAAE,KAAK,GAAG,4BAA4B,CAAC;IACvD,gBAAgB,CAAC,EAAE,KAAK,GAAG,0BAA0B,CAAC;CACtD;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC,EAAE,oBAa/C,CAAC;AAEF,eAAO,MAAM,oCAAoC,MAAM,CAAC;AAExD,MAAM,WAAW,gCAAgC;IAChD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8BAA8B,CAAC,EAAE,OAAO,CAAC;CACzC;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,gCAAgC,GAAG,OAAO,CAM5F;AA2KD,wBAAgB,8BAA8B,CAC7C,QAAQ,EAAE,YAAY,EAAE,EACxB,OAAO,GAAE,0BAA+B,GACtC,yBAAyB,CA8C3B;AA6CD,wBAAgB,yBAAyB,CACxC,QAAQ,EAAE,YAAY,EAAE,EACxB,OAAO,GAAE,4BAAiC,GACxC,2BAA2B,CAgC7B;AAED,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,YAAY,EAAE,EACxB,OAAO,GAAE,0BAA+B,GACtC,yBAAyB,CAyC3B;AAED,wBAAgB,qBAAqB,CACpC,QAAQ,EAAE,YAAY,EAAE,EACxB,OAAO,GAAE,oBAAyB,GAChC,mBAAmB,CA2BrB","sourcesContent":["/**\n * Deterministic, no-LLM context reductions applied before compaction summarization.\n *\n * Ported from plugsuits' `context-collapse` and `micro-compact` patterns and\n * adapted to the senpi `AgentMessage` shape. Three independent transforms:\n *\n * 1. {@link collapseConsecutiveToolResults} — runs of same-kind read/grep/shell\n * tool result payloads are replaced with a single one-line label so the\n * summarizer pays for the shape, not for the bytes.\n * 2. {@link microCompactAssistantText} — older long assistant text answers are\n * truncated and tagged with a `[response shrunk]` marker.\n * 3. {@link clearOldToolResults} — keep the last N tool results in full, replace\n * older clearable tool result content with `[tool result cleared]`.\n *\n * Each transform is pure (`messages` in → new array out, no in-place mutation\n * beyond freshly cloned messages) and returns aggregated token-savings stats.\n *\n * {@link reduceContextMessages} composes the three transforms in order.\n */\n\nimport type { AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport type { AssistantMessage, ImageContent, TextContent, ToolResultMessage } from \"@earendil-works/pi-ai\";\n\nconst DEFAULT_READ_TOOL_NAMES = [\"read\", \"Read\", \"read_file\"];\nconst DEFAULT_SEARCH_TOOL_NAMES = [\"grep\", \"Grep\", \"glob\", \"Glob\"];\nconst DEFAULT_SHELL_TOOL_NAMES = [\"bash\", \"Bash\", \"shell\", \"shell_execute\"];\nconst DEFAULT_CLEARABLE_TOOL_NAMES = [\n\t\"read\",\n\t\"Read\",\n\t\"read_file\",\n\t\"write\",\n\t\"Write\",\n\t\"edit\",\n\t\"Edit\",\n\t\"grep\",\n\t\"Grep\",\n\t\"glob\",\n\t\"Glob\",\n\t\"bash\",\n\t\"Bash\",\n\t\"shell\",\n];\n\nconst DEFAULT_MIN_GROUP_SIZE = 2;\nconst DEFAULT_PROTECT_RECENT_MESSAGES = 5;\nconst DEFAULT_PROTECT_RECENT_TOKENS = 2000;\nconst DEFAULT_MAX_ASSISTANT_TEXT_TOKENS = 500;\nconst DEFAULT_MIN_SAVINGS_TOKENS = 100;\nconst DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;\nconst DEFAULT_CLEARED_PLACEHOLDER = \"[tool result cleared]\";\nconst DEFAULT_REPLACEMENT_TEMPLATE = \"[response shrunk — {original_tokens} → {shrunk_tokens} tokens]\";\n\nconst MAX_HINTS_IN_LABEL = 5;\nconst MAX_HINT_LENGTH = 80;\nconst SHRUNK_RESPONSE_RATIO = 0.3;\n\nexport type CollapsedGroupKind = \"read\" | \"search\" | \"shell\";\n\nexport interface CollapsedGroup {\n\ttype: CollapsedGroupKind;\n\tcount: number;\n\tlabel: string;\n\toriginalTokens: number;\n\tcollapsedTokens: number;\n}\n\nexport interface CollapseConsecutiveOptions {\n\tminGroupSize?: number;\n\tprotectRecentMessages?: number;\n\treadToolNames?: string[];\n\tsearchToolNames?: string[];\n\tshellToolNames?: string[];\n}\n\nexport interface CollapseConsecutiveResult {\n\tmessages: AgentMessage[];\n\tgroups: CollapsedGroup[];\n\ttokensSaved: number;\n}\n\nexport interface MicroCompactAssistantOptions {\n\tprotectRecentTokens?: number;\n\tmaxAssistantTextTokens?: number;\n\tminSavingsTokens?: number;\n\treplacementTemplate?: string;\n}\n\nexport interface MicroCompactAssistantResult {\n\tmessages: AgentMessage[];\n\ttokensSaved: number;\n\tmessagesModified: number;\n}\n\nexport interface ClearOldToolResultsOptions {\n\tkeepRecent?: number;\n\tclearableToolNames?: string[];\n\treplacementText?: string;\n}\n\nexport interface ClearOldToolResultsResult {\n\tmessages: AgentMessage[];\n\ttokensSaved: number;\n\ttoolResultsCleared: number;\n}\n\nexport interface ReduceContextOptions {\n\tcollapse?: false | CollapseConsecutiveOptions;\n\tshrinkAssistant?: false | MicroCompactAssistantOptions;\n\tclearToolResults?: false | ClearOldToolResultsOptions;\n}\n\nexport interface ReduceContextResult {\n\tmessages: AgentMessage[];\n\ttokensSaved: number;\n\tgroupsCollapsed: number;\n\tmessagesShrunk: number;\n\ttoolResultsCleared: number;\n}\n\n/**\n * Default options passed to {@link reduceContextMessages} when the builtin\n * compaction extension's `context` hook decides to run a reduction pass.\n *\n * Each value is chosen to be strictly more conservative than the corresponding\n * plugsuits default — protect more of the recent tail, raise the per-message\n * shrink threshold, and keep more recent tool results intact — so a single\n * shared default is safe to apply across normal coding sessions without making\n * targeted reductions less effective.\n */\nexport const BUILTIN_CONTEXT_REDUCTION_OPTIONS: ReduceContextOptions = {\n\tcollapse: {\n\t\tminGroupSize: DEFAULT_MIN_GROUP_SIZE,\n\t\tprotectRecentMessages: DEFAULT_PROTECT_RECENT_MESSAGES,\n\t},\n\tshrinkAssistant: {\n\t\tprotectRecentTokens: 3000,\n\t\tmaxAssistantTextTokens: 800,\n\t\tminSavingsTokens: DEFAULT_MIN_SAVINGS_TOKENS,\n\t},\n\tclearToolResults: {\n\t\tkeepRecent: 6,\n\t},\n};\n\nexport const BUILTIN_CONTEXT_REDUCTION_GATE_RATIO = 0.5;\n\nexport interface ShouldApplyContextReductionInput {\n\tusageTokens: number | null;\n\tcontextWindow: number;\n\tgateRatio?: number;\n\tisProviderNativeCompactionPath?: boolean;\n}\n\nexport function shouldApplyContextReduction(input: ShouldApplyContextReductionInput): boolean {\n\tconst gate = input.gateRatio ?? BUILTIN_CONTEXT_REDUCTION_GATE_RATIO;\n\tif (input.isProviderNativeCompactionPath === true) return false;\n\tif (input.usageTokens === null) return false;\n\tif (input.contextWindow <= 0) return false;\n\treturn input.usageTokens >= input.contextWindow * gate;\n}\n\nfunction approxTextTokens(text: string): number {\n\tif (!text) return 0;\n\treturn Math.ceil(text.length / 4);\n}\n\nfunction extractContentText(content: (TextContent | ImageContent)[] | undefined): string {\n\tif (!Array.isArray(content)) return \"\";\n\tlet out = \"\";\n\tfor (const part of content) {\n\t\tif (part.type === \"text\") out += part.text;\n\t}\n\treturn out;\n}\n\nfunction extractMessageText(message: AgentMessage): string {\n\tif (message.role === \"user\") {\n\t\tif (typeof message.content === \"string\") return message.content;\n\t\treturn extractContentText(message.content as (TextContent | ImageContent)[]);\n\t}\n\tif (message.role === \"assistant\") {\n\t\tlet out = \"\";\n\t\tfor (const block of message.content) {\n\t\t\tif (block.type === \"text\") out += block.text;\n\t\t\telse if (block.type === \"toolCall\") out += `${block.name} ${JSON.stringify(block.arguments)}`;\n\t\t}\n\t\treturn out;\n\t}\n\tif (message.role === \"toolResult\") {\n\t\treturn extractContentText(message.content);\n\t}\n\treturn \"\";\n}\n\ninterface ToolNameSets {\n\tread: Set<string>;\n\tsearch: Set<string>;\n\tshell: Set<string>;\n}\n\nfunction classifyTool(name: string, sets: ToolNameSets): CollapsedGroupKind | null {\n\tif (sets.read.has(name)) return \"read\";\n\tif (sets.search.has(name)) return \"search\";\n\tif (sets.shell.has(name)) return \"shell\";\n\treturn null;\n}\n\ninterface FirstToolCall {\n\tid: string;\n\tname: string;\n\targs: Record<string, unknown>;\n}\n\nfunction getFirstToolCallFromAssistant(message: AgentMessage): FirstToolCall | null {\n\tif (message.role !== \"assistant\") return null;\n\tfor (const block of message.content) {\n\t\tif (block.type === \"toolCall\") {\n\t\t\treturn { id: block.id, name: block.name, args: block.arguments as Record<string, unknown> };\n\t\t}\n\t}\n\treturn null;\n}\n\ninterface CollapsibleOperation {\n\ttype: CollapsedGroupKind;\n\ttoolName: string;\n\tassistantIndex: number;\n\tresultIndex: number;\n\thint?: string;\n\tresultText: string;\n}\n\nfunction truncateHint(value: string): string {\n\tif (value.length <= MAX_HINT_LENGTH) return value;\n\treturn `${value.slice(0, MAX_HINT_LENGTH - 1)}…`;\n}\n\nfunction extractHint(type: CollapsedGroupKind, args: Record<string, unknown>): string | undefined {\n\tif (type === \"read\") {\n\t\tconst path = args.path ?? args.file_path ?? args.filePath;\n\t\tif (typeof path === \"string\" && path.length > 0) return truncateHint(path);\n\t\treturn undefined;\n\t}\n\tif (type === \"search\") {\n\t\tconst path = typeof args.path === \"string\" ? args.path : undefined;\n\t\tconst pattern =\n\t\t\t(typeof args.pattern === \"string\" && args.pattern) ||\n\t\t\t(typeof args.glob === \"string\" && args.glob) ||\n\t\t\t(typeof args.query === \"string\" && args.query) ||\n\t\t\tundefined;\n\t\tif (path && pattern) return truncateHint(`${path}:${pattern}`);\n\t\tif (path) return truncateHint(path);\n\t\tif (pattern) return truncateHint(pattern as string);\n\t\treturn undefined;\n\t}\n\tconst command = args.command ?? args.cmd;\n\tif (typeof command === \"string\" && command.length > 0) return truncateHint(command);\n\treturn undefined;\n}\n\nfunction buildGroupLabel(type: CollapsedGroupKind, operations: CollapsibleOperation[]): string {\n\tconst hints: string[] = [];\n\tfor (const op of operations) {\n\t\tif (op.hint && hints.length < MAX_HINTS_IN_LABEL) hints.push(op.hint);\n\t}\n\tconst noun = type === \"read\" ? \"read results\" : type === \"search\" ? \"search results\" : \"shell results\";\n\tif (hints.length === 0) return `[${operations.length} ${noun}]`;\n\tconst more = operations.length - hints.length;\n\tconst moreSuffix = more > 0 ? `, and ${more} more` : \"\";\n\treturn `[${operations.length} ${noun}: ${hints.join(\", \")}${moreSuffix}]`;\n}\n\nfunction collectCollapsibleOperations(\n\tmessages: AgentMessage[],\n\tcollapseLimit: number,\n\tsets: ToolNameSets,\n): CollapsibleOperation[] {\n\tconst operations: CollapsibleOperation[] = [];\n\tlet i = 0;\n\twhile (i < collapseLimit) {\n\t\tconst assistant = messages[i];\n\t\tconst call = getFirstToolCallFromAssistant(assistant);\n\t\tif (!call) {\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst next = messages[i + 1];\n\t\tconst resultInWindow = i + 1 < collapseLimit;\n\t\tif (!next || next.role !== \"toolResult\" || next.toolCallId !== call.id || !resultInWindow) {\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst type = classifyTool(call.name, sets);\n\t\tif (!type) {\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\toperations.push({\n\t\t\ttype,\n\t\t\ttoolName: call.name,\n\t\t\tassistantIndex: i,\n\t\t\tresultIndex: i + 1,\n\t\t\thint: extractHint(type, call.args),\n\t\t\tresultText: extractContentText((next as ToolResultMessage).content),\n\t\t});\n\t\ti += 2;\n\t}\n\treturn operations;\n}\n\nfunction groupConsecutiveOperations(operations: CollapsibleOperation[]): CollapsibleOperation[][] {\n\tconst groups: CollapsibleOperation[][] = [];\n\tlet current: CollapsibleOperation[] = [];\n\tfor (const op of operations) {\n\t\tif (current.length === 0) {\n\t\t\tcurrent.push(op);\n\t\t\tcontinue;\n\t\t}\n\t\tconst prev = current[current.length - 1];\n\t\tif (op.type === prev.type && op.assistantIndex === prev.resultIndex + 1) {\n\t\t\tcurrent.push(op);\n\t\t\tcontinue;\n\t\t}\n\t\tgroups.push(current);\n\t\tcurrent = [op];\n\t}\n\tif (current.length > 0) groups.push(current);\n\treturn groups;\n}\n\nexport function collapseConsecutiveToolResults(\n\tmessages: AgentMessage[],\n\toptions: CollapseConsecutiveOptions = {},\n): CollapseConsecutiveResult {\n\tconst minGroupSize = Math.max(1, options.minGroupSize ?? DEFAULT_MIN_GROUP_SIZE);\n\tconst protectRecentMessages = Math.max(0, options.protectRecentMessages ?? DEFAULT_PROTECT_RECENT_MESSAGES);\n\tconst sets: ToolNameSets = {\n\t\tread: new Set(options.readToolNames ?? DEFAULT_READ_TOOL_NAMES),\n\t\tsearch: new Set(options.searchToolNames ?? DEFAULT_SEARCH_TOOL_NAMES),\n\t\tshell: new Set(options.shellToolNames ?? DEFAULT_SHELL_TOOL_NAMES),\n\t};\n\n\tconst collapseLimit = Math.max(0, messages.length - protectRecentMessages);\n\tconst operations = collectCollapsibleOperations(messages, collapseLimit, sets);\n\tconst operationGroups = groupConsecutiveOperations(operations);\n\n\tconst collapsedGroups: CollapsedGroup[] = [];\n\tconst nextMessages = messages.slice();\n\tlet tokensSaved = 0;\n\n\tfor (const group of operationGroups) {\n\t\tif (group.length < minGroupSize) continue;\n\t\tconst label = buildGroupLabel(group[0].type, group);\n\t\tlet originalTokens = 0;\n\t\tlet collapsedTokens = 0;\n\t\tfor (const op of group) {\n\t\t\tconst original = approxTextTokens(op.resultText);\n\t\t\tconst collapsed = approxTextTokens(label);\n\t\t\toriginalTokens += original;\n\t\t\tcollapsedTokens += collapsed;\n\t\t\tconst result = nextMessages[op.resultIndex];\n\t\t\tif (result.role !== \"toolResult\") continue;\n\t\t\tconst images = result.content.filter((c): c is ImageContent => c.type === \"image\");\n\t\t\tnextMessages[op.resultIndex] = {\n\t\t\t\t...result,\n\t\t\t\tcontent: [{ type: \"text\", text: label } as TextContent, ...images],\n\t\t\t};\n\t\t}\n\t\ttokensSaved += Math.max(0, originalTokens - collapsedTokens);\n\t\tcollapsedGroups.push({\n\t\t\ttype: group[0].type,\n\t\t\tcount: group.length,\n\t\t\tlabel,\n\t\t\toriginalTokens,\n\t\t\tcollapsedTokens,\n\t\t});\n\t}\n\n\treturn { messages: nextMessages, groups: collapsedGroups, tokensSaved };\n}\n\nfunction resolveProtectedFromIndex(messages: AgentMessage[], protectRecentTokens: number): number {\n\tif (messages.length === 0) return 0;\n\tlet recentTokens = 0;\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst tokens = approxTextTokens(extractMessageText(messages[i]));\n\t\tif (recentTokens + tokens > protectRecentTokens) return i + 1;\n\t\trecentTokens += tokens;\n\t\tif (i === 0) return 0;\n\t}\n\treturn messages.length;\n}\n\nfunction renderReplacementText(template: string, originalTokens: number, shrunkTokens: number): string {\n\treturn template\n\t\t.split(\"{original_tokens}\")\n\t\t.join(String(originalTokens))\n\t\t.split(\"{shrunk_tokens}\")\n\t\t.join(String(shrunkTokens));\n}\n\nfunction buildShrunkText(\n\toriginalText: string,\n\toriginalTokens: number,\n\tmaxAssistantTextTokens: number,\n\ttemplate: string,\n): { text: string; tokens: number } {\n\tconst targetTextTokens = Math.max(1, Math.floor(maxAssistantTextTokens * SHRUNK_RESPONSE_RATIO));\n\tconst ratio = originalTokens > 0 ? targetTextTokens / originalTokens : 0;\n\tconst targetChars = Math.max(0, Math.floor(originalText.length * ratio));\n\tconst truncatedText = originalText.slice(0, targetChars);\n\tlet shrunkTokens = 0;\n\tlet shrunkText = \"\";\n\tfor (let iteration = 0; iteration < 5; iteration += 1) {\n\t\tconst replacement = renderReplacementText(template, originalTokens, shrunkTokens);\n\t\tconst candidate = truncatedText.length > 0 ? `${truncatedText}\\n\\n${replacement}` : replacement;\n\t\tconst tokens = approxTextTokens(candidate);\n\t\tshrunkText = candidate;\n\t\tif (tokens === shrunkTokens) return { text: candidate, tokens };\n\t\tshrunkTokens = tokens;\n\t}\n\treturn { text: shrunkText, tokens: approxTextTokens(shrunkText) };\n}\n\nexport function microCompactAssistantText(\n\tmessages: AgentMessage[],\n\toptions: MicroCompactAssistantOptions = {},\n): MicroCompactAssistantResult {\n\tconst protectRecentTokens = Math.max(0, options.protectRecentTokens ?? DEFAULT_PROTECT_RECENT_TOKENS);\n\tconst maxAssistantTextTokens = Math.max(0, options.maxAssistantTextTokens ?? DEFAULT_MAX_ASSISTANT_TEXT_TOKENS);\n\tconst minSavingsTokens = Math.max(0, options.minSavingsTokens ?? DEFAULT_MIN_SAVINGS_TOKENS);\n\tconst template = options.replacementTemplate ?? DEFAULT_REPLACEMENT_TEMPLATE;\n\n\tconst protectedFromIndex = resolveProtectedFromIndex(messages, protectRecentTokens);\n\tconst result = messages.slice();\n\tlet messagesModified = 0;\n\tlet tokensSaved = 0;\n\n\tfor (let i = 0; i < protectedFromIndex; i += 1) {\n\t\tconst msg = result[i];\n\t\tif (msg.role !== \"assistant\") continue;\n\t\tconst assistant = msg as AssistantMessage;\n\t\tconst allText = assistant.content.length > 0 && assistant.content.every((c) => c.type === \"text\");\n\t\tif (!allText) continue;\n\t\tconst originalText = assistant.content.map((c) => (c.type === \"text\" ? c.text : \"\")).join(\"\\n\");\n\t\tconst originalTokens = approxTextTokens(originalText);\n\t\tif (originalTokens <= maxAssistantTextTokens) continue;\n\t\tconst shrunk = buildShrunkText(originalText, originalTokens, maxAssistantTextTokens, template);\n\t\tconst saved = originalTokens - shrunk.tokens;\n\t\tif (saved < minSavingsTokens) continue;\n\t\tresult[i] = {\n\t\t\t...assistant,\n\t\t\tcontent: [{ type: \"text\", text: shrunk.text } as TextContent],\n\t\t};\n\t\ttokensSaved += saved;\n\t\tmessagesModified += 1;\n\t}\n\n\treturn { messages: result, tokensSaved, messagesModified };\n}\n\nexport function clearOldToolResults(\n\tmessages: AgentMessage[],\n\toptions: ClearOldToolResultsOptions = {},\n): ClearOldToolResultsResult {\n\tconst keepRecent = Math.max(0, options.keepRecent ?? DEFAULT_KEEP_RECENT_TOOL_RESULTS);\n\tconst clearable = new Set(options.clearableToolNames ?? DEFAULT_CLEARABLE_TOOL_NAMES);\n\tconst replacementText = options.replacementText ?? DEFAULT_CLEARED_PLACEHOLDER;\n\n\tconst clearableIndices: number[] = [];\n\tfor (let i = 0; i < messages.length; i += 1) {\n\t\tconst msg = messages[i];\n\t\tif (msg.role === \"toolResult\" && clearable.has(msg.toolName)) {\n\t\t\tclearableIndices.push(i);\n\t\t}\n\t}\n\n\tconst clearUntil = Math.max(0, clearableIndices.length - keepRecent);\n\tif (clearUntil === 0) {\n\t\treturn { messages: messages.slice(), tokensSaved: 0, toolResultsCleared: 0 };\n\t}\n\n\tconst result = messages.slice();\n\tlet tokensSaved = 0;\n\tlet toolResultsCleared = 0;\n\tconst replacementTokens = approxTextTokens(replacementText);\n\n\tfor (let k = 0; k < clearUntil; k += 1) {\n\t\tconst idx = clearableIndices[k];\n\t\tconst msg = result[idx];\n\t\tif (msg.role !== \"toolResult\") continue;\n\t\tconst original = msg as ToolResultMessage;\n\t\tconst originalText = extractContentText(original.content);\n\t\tconst originalTokens = approxTextTokens(originalText);\n\t\tconst images = original.content.filter((c): c is ImageContent => c.type === \"image\");\n\t\tresult[idx] = {\n\t\t\t...original,\n\t\t\tcontent: [{ type: \"text\", text: replacementText } as TextContent, ...images],\n\t\t};\n\t\tconst savings = originalTokens - replacementTokens;\n\t\tif (savings > 0) tokensSaved += savings;\n\t\ttoolResultsCleared += 1;\n\t}\n\n\treturn { messages: result, tokensSaved, toolResultsCleared };\n}\n\nexport function reduceContextMessages(\n\tmessages: AgentMessage[],\n\toptions: ReduceContextOptions = {},\n): ReduceContextResult {\n\tlet current = messages;\n\tlet tokensSaved = 0;\n\tlet groupsCollapsed = 0;\n\tlet messagesShrunk = 0;\n\tlet toolResultsCleared = 0;\n\n\tif (options.collapse !== false) {\n\t\tconst collapsed = collapseConsecutiveToolResults(current, options.collapse ?? undefined);\n\t\tcurrent = collapsed.messages;\n\t\ttokensSaved += collapsed.tokensSaved;\n\t\tgroupsCollapsed += collapsed.groups.length;\n\t}\n\tif (options.shrinkAssistant !== false) {\n\t\tconst shrunk = microCompactAssistantText(current, options.shrinkAssistant ?? undefined);\n\t\tcurrent = shrunk.messages;\n\t\ttokensSaved += shrunk.tokensSaved;\n\t\tmessagesShrunk += shrunk.messagesModified;\n\t}\n\tif (options.clearToolResults !== false) {\n\t\tconst cleared = clearOldToolResults(current, options.clearToolResults ?? undefined);\n\t\tcurrent = cleared.messages;\n\t\ttokensSaved += cleared.tokensSaved;\n\t\ttoolResultsCleared += cleared.toolResultsCleared;\n\t}\n\n\treturn { messages: current, tokensSaved, groupsCollapsed, messagesShrunk, toolResultsCleared };\n}\n"]}
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Deterministic, no-LLM context reductions applied before compaction summarization.
3
+ *
4
+ * Ported from plugsuits' `context-collapse` and `micro-compact` patterns and
5
+ * adapted to the senpi `AgentMessage` shape. Three independent transforms:
6
+ *
7
+ * 1. {@link collapseConsecutiveToolResults} — runs of same-kind read/grep/shell
8
+ * tool result payloads are replaced with a single one-line label so the
9
+ * summarizer pays for the shape, not for the bytes.
10
+ * 2. {@link microCompactAssistantText} — older long assistant text answers are
11
+ * truncated and tagged with a `[response shrunk]` marker.
12
+ * 3. {@link clearOldToolResults} — keep the last N tool results in full, replace
13
+ * older clearable tool result content with `[tool result cleared]`.
14
+ *
15
+ * Each transform is pure (`messages` in → new array out, no in-place mutation
16
+ * beyond freshly cloned messages) and returns aggregated token-savings stats.
17
+ *
18
+ * {@link reduceContextMessages} composes the three transforms in order.
19
+ */
20
+ const DEFAULT_READ_TOOL_NAMES = ["read", "Read", "read_file"];
21
+ const DEFAULT_SEARCH_TOOL_NAMES = ["grep", "Grep", "glob", "Glob"];
22
+ const DEFAULT_SHELL_TOOL_NAMES = ["bash", "Bash", "shell", "shell_execute"];
23
+ const DEFAULT_CLEARABLE_TOOL_NAMES = [
24
+ "read",
25
+ "Read",
26
+ "read_file",
27
+ "write",
28
+ "Write",
29
+ "edit",
30
+ "Edit",
31
+ "grep",
32
+ "Grep",
33
+ "glob",
34
+ "Glob",
35
+ "bash",
36
+ "Bash",
37
+ "shell",
38
+ ];
39
+ const DEFAULT_MIN_GROUP_SIZE = 2;
40
+ const DEFAULT_PROTECT_RECENT_MESSAGES = 5;
41
+ const DEFAULT_PROTECT_RECENT_TOKENS = 2000;
42
+ const DEFAULT_MAX_ASSISTANT_TEXT_TOKENS = 500;
43
+ const DEFAULT_MIN_SAVINGS_TOKENS = 100;
44
+ const DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;
45
+ const DEFAULT_CLEARED_PLACEHOLDER = "[tool result cleared]";
46
+ const DEFAULT_REPLACEMENT_TEMPLATE = "[response shrunk — {original_tokens} → {shrunk_tokens} tokens]";
47
+ const MAX_HINTS_IN_LABEL = 5;
48
+ const MAX_HINT_LENGTH = 80;
49
+ const SHRUNK_RESPONSE_RATIO = 0.3;
50
+ /**
51
+ * Default options passed to {@link reduceContextMessages} when the builtin
52
+ * compaction extension's `context` hook decides to run a reduction pass.
53
+ *
54
+ * Each value is chosen to be strictly more conservative than the corresponding
55
+ * plugsuits default — protect more of the recent tail, raise the per-message
56
+ * shrink threshold, and keep more recent tool results intact — so a single
57
+ * shared default is safe to apply across normal coding sessions without making
58
+ * targeted reductions less effective.
59
+ */
60
+ export const BUILTIN_CONTEXT_REDUCTION_OPTIONS = {
61
+ collapse: {
62
+ minGroupSize: DEFAULT_MIN_GROUP_SIZE,
63
+ protectRecentMessages: DEFAULT_PROTECT_RECENT_MESSAGES,
64
+ },
65
+ shrinkAssistant: {
66
+ protectRecentTokens: 3000,
67
+ maxAssistantTextTokens: 800,
68
+ minSavingsTokens: DEFAULT_MIN_SAVINGS_TOKENS,
69
+ },
70
+ clearToolResults: {
71
+ keepRecent: 6,
72
+ },
73
+ };
74
+ export const BUILTIN_CONTEXT_REDUCTION_GATE_RATIO = 0.5;
75
+ export function shouldApplyContextReduction(input) {
76
+ const gate = input.gateRatio ?? BUILTIN_CONTEXT_REDUCTION_GATE_RATIO;
77
+ if (input.isProviderNativeCompactionPath === true)
78
+ return false;
79
+ if (input.usageTokens === null)
80
+ return false;
81
+ if (input.contextWindow <= 0)
82
+ return false;
83
+ return input.usageTokens >= input.contextWindow * gate;
84
+ }
85
+ function approxTextTokens(text) {
86
+ if (!text)
87
+ return 0;
88
+ return Math.ceil(text.length / 4);
89
+ }
90
+ function extractContentText(content) {
91
+ if (!Array.isArray(content))
92
+ return "";
93
+ let out = "";
94
+ for (const part of content) {
95
+ if (part.type === "text")
96
+ out += part.text;
97
+ }
98
+ return out;
99
+ }
100
+ function extractMessageText(message) {
101
+ if (message.role === "user") {
102
+ if (typeof message.content === "string")
103
+ return message.content;
104
+ return extractContentText(message.content);
105
+ }
106
+ if (message.role === "assistant") {
107
+ let out = "";
108
+ for (const block of message.content) {
109
+ if (block.type === "text")
110
+ out += block.text;
111
+ else if (block.type === "toolCall")
112
+ out += `${block.name} ${JSON.stringify(block.arguments)}`;
113
+ }
114
+ return out;
115
+ }
116
+ if (message.role === "toolResult") {
117
+ return extractContentText(message.content);
118
+ }
119
+ return "";
120
+ }
121
+ function classifyTool(name, sets) {
122
+ if (sets.read.has(name))
123
+ return "read";
124
+ if (sets.search.has(name))
125
+ return "search";
126
+ if (sets.shell.has(name))
127
+ return "shell";
128
+ return null;
129
+ }
130
+ function getFirstToolCallFromAssistant(message) {
131
+ if (message.role !== "assistant")
132
+ return null;
133
+ for (const block of message.content) {
134
+ if (block.type === "toolCall") {
135
+ return { id: block.id, name: block.name, args: block.arguments };
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+ function truncateHint(value) {
141
+ if (value.length <= MAX_HINT_LENGTH)
142
+ return value;
143
+ return `${value.slice(0, MAX_HINT_LENGTH - 1)}…`;
144
+ }
145
+ function extractHint(type, args) {
146
+ if (type === "read") {
147
+ const path = args.path ?? args.file_path ?? args.filePath;
148
+ if (typeof path === "string" && path.length > 0)
149
+ return truncateHint(path);
150
+ return undefined;
151
+ }
152
+ if (type === "search") {
153
+ const path = typeof args.path === "string" ? args.path : undefined;
154
+ const pattern = (typeof args.pattern === "string" && args.pattern) ||
155
+ (typeof args.glob === "string" && args.glob) ||
156
+ (typeof args.query === "string" && args.query) ||
157
+ undefined;
158
+ if (path && pattern)
159
+ return truncateHint(`${path}:${pattern}`);
160
+ if (path)
161
+ return truncateHint(path);
162
+ if (pattern)
163
+ return truncateHint(pattern);
164
+ return undefined;
165
+ }
166
+ const command = args.command ?? args.cmd;
167
+ if (typeof command === "string" && command.length > 0)
168
+ return truncateHint(command);
169
+ return undefined;
170
+ }
171
+ function buildGroupLabel(type, operations) {
172
+ const hints = [];
173
+ for (const op of operations) {
174
+ if (op.hint && hints.length < MAX_HINTS_IN_LABEL)
175
+ hints.push(op.hint);
176
+ }
177
+ const noun = type === "read" ? "read results" : type === "search" ? "search results" : "shell results";
178
+ if (hints.length === 0)
179
+ return `[${operations.length} ${noun}]`;
180
+ const more = operations.length - hints.length;
181
+ const moreSuffix = more > 0 ? `, and ${more} more` : "";
182
+ return `[${operations.length} ${noun}: ${hints.join(", ")}${moreSuffix}]`;
183
+ }
184
+ function collectCollapsibleOperations(messages, collapseLimit, sets) {
185
+ const operations = [];
186
+ let i = 0;
187
+ while (i < collapseLimit) {
188
+ const assistant = messages[i];
189
+ const call = getFirstToolCallFromAssistant(assistant);
190
+ if (!call) {
191
+ i += 1;
192
+ continue;
193
+ }
194
+ const next = messages[i + 1];
195
+ const resultInWindow = i + 1 < collapseLimit;
196
+ if (!next || next.role !== "toolResult" || next.toolCallId !== call.id || !resultInWindow) {
197
+ i += 1;
198
+ continue;
199
+ }
200
+ const type = classifyTool(call.name, sets);
201
+ if (!type) {
202
+ i += 1;
203
+ continue;
204
+ }
205
+ operations.push({
206
+ type,
207
+ toolName: call.name,
208
+ assistantIndex: i,
209
+ resultIndex: i + 1,
210
+ hint: extractHint(type, call.args),
211
+ resultText: extractContentText(next.content),
212
+ });
213
+ i += 2;
214
+ }
215
+ return operations;
216
+ }
217
+ function groupConsecutiveOperations(operations) {
218
+ const groups = [];
219
+ let current = [];
220
+ for (const op of operations) {
221
+ if (current.length === 0) {
222
+ current.push(op);
223
+ continue;
224
+ }
225
+ const prev = current[current.length - 1];
226
+ if (op.type === prev.type && op.assistantIndex === prev.resultIndex + 1) {
227
+ current.push(op);
228
+ continue;
229
+ }
230
+ groups.push(current);
231
+ current = [op];
232
+ }
233
+ if (current.length > 0)
234
+ groups.push(current);
235
+ return groups;
236
+ }
237
+ export function collapseConsecutiveToolResults(messages, options = {}) {
238
+ const minGroupSize = Math.max(1, options.minGroupSize ?? DEFAULT_MIN_GROUP_SIZE);
239
+ const protectRecentMessages = Math.max(0, options.protectRecentMessages ?? DEFAULT_PROTECT_RECENT_MESSAGES);
240
+ const sets = {
241
+ read: new Set(options.readToolNames ?? DEFAULT_READ_TOOL_NAMES),
242
+ search: new Set(options.searchToolNames ?? DEFAULT_SEARCH_TOOL_NAMES),
243
+ shell: new Set(options.shellToolNames ?? DEFAULT_SHELL_TOOL_NAMES),
244
+ };
245
+ const collapseLimit = Math.max(0, messages.length - protectRecentMessages);
246
+ const operations = collectCollapsibleOperations(messages, collapseLimit, sets);
247
+ const operationGroups = groupConsecutiveOperations(operations);
248
+ const collapsedGroups = [];
249
+ const nextMessages = messages.slice();
250
+ let tokensSaved = 0;
251
+ for (const group of operationGroups) {
252
+ if (group.length < minGroupSize)
253
+ continue;
254
+ const label = buildGroupLabel(group[0].type, group);
255
+ let originalTokens = 0;
256
+ let collapsedTokens = 0;
257
+ for (const op of group) {
258
+ const original = approxTextTokens(op.resultText);
259
+ const collapsed = approxTextTokens(label);
260
+ originalTokens += original;
261
+ collapsedTokens += collapsed;
262
+ const result = nextMessages[op.resultIndex];
263
+ if (result.role !== "toolResult")
264
+ continue;
265
+ const images = result.content.filter((c) => c.type === "image");
266
+ nextMessages[op.resultIndex] = {
267
+ ...result,
268
+ content: [{ type: "text", text: label }, ...images],
269
+ };
270
+ }
271
+ tokensSaved += Math.max(0, originalTokens - collapsedTokens);
272
+ collapsedGroups.push({
273
+ type: group[0].type,
274
+ count: group.length,
275
+ label,
276
+ originalTokens,
277
+ collapsedTokens,
278
+ });
279
+ }
280
+ return { messages: nextMessages, groups: collapsedGroups, tokensSaved };
281
+ }
282
+ function resolveProtectedFromIndex(messages, protectRecentTokens) {
283
+ if (messages.length === 0)
284
+ return 0;
285
+ let recentTokens = 0;
286
+ for (let i = messages.length - 1; i >= 0; i--) {
287
+ const tokens = approxTextTokens(extractMessageText(messages[i]));
288
+ if (recentTokens + tokens > protectRecentTokens)
289
+ return i + 1;
290
+ recentTokens += tokens;
291
+ if (i === 0)
292
+ return 0;
293
+ }
294
+ return messages.length;
295
+ }
296
+ function renderReplacementText(template, originalTokens, shrunkTokens) {
297
+ return template
298
+ .split("{original_tokens}")
299
+ .join(String(originalTokens))
300
+ .split("{shrunk_tokens}")
301
+ .join(String(shrunkTokens));
302
+ }
303
+ function buildShrunkText(originalText, originalTokens, maxAssistantTextTokens, template) {
304
+ const targetTextTokens = Math.max(1, Math.floor(maxAssistantTextTokens * SHRUNK_RESPONSE_RATIO));
305
+ const ratio = originalTokens > 0 ? targetTextTokens / originalTokens : 0;
306
+ const targetChars = Math.max(0, Math.floor(originalText.length * ratio));
307
+ const truncatedText = originalText.slice(0, targetChars);
308
+ let shrunkTokens = 0;
309
+ let shrunkText = "";
310
+ for (let iteration = 0; iteration < 5; iteration += 1) {
311
+ const replacement = renderReplacementText(template, originalTokens, shrunkTokens);
312
+ const candidate = truncatedText.length > 0 ? `${truncatedText}\n\n${replacement}` : replacement;
313
+ const tokens = approxTextTokens(candidate);
314
+ shrunkText = candidate;
315
+ if (tokens === shrunkTokens)
316
+ return { text: candidate, tokens };
317
+ shrunkTokens = tokens;
318
+ }
319
+ return { text: shrunkText, tokens: approxTextTokens(shrunkText) };
320
+ }
321
+ export function microCompactAssistantText(messages, options = {}) {
322
+ const protectRecentTokens = Math.max(0, options.protectRecentTokens ?? DEFAULT_PROTECT_RECENT_TOKENS);
323
+ const maxAssistantTextTokens = Math.max(0, options.maxAssistantTextTokens ?? DEFAULT_MAX_ASSISTANT_TEXT_TOKENS);
324
+ const minSavingsTokens = Math.max(0, options.minSavingsTokens ?? DEFAULT_MIN_SAVINGS_TOKENS);
325
+ const template = options.replacementTemplate ?? DEFAULT_REPLACEMENT_TEMPLATE;
326
+ const protectedFromIndex = resolveProtectedFromIndex(messages, protectRecentTokens);
327
+ const result = messages.slice();
328
+ let messagesModified = 0;
329
+ let tokensSaved = 0;
330
+ for (let i = 0; i < protectedFromIndex; i += 1) {
331
+ const msg = result[i];
332
+ if (msg.role !== "assistant")
333
+ continue;
334
+ const assistant = msg;
335
+ const allText = assistant.content.length > 0 && assistant.content.every((c) => c.type === "text");
336
+ if (!allText)
337
+ continue;
338
+ const originalText = assistant.content.map((c) => (c.type === "text" ? c.text : "")).join("\n");
339
+ const originalTokens = approxTextTokens(originalText);
340
+ if (originalTokens <= maxAssistantTextTokens)
341
+ continue;
342
+ const shrunk = buildShrunkText(originalText, originalTokens, maxAssistantTextTokens, template);
343
+ const saved = originalTokens - shrunk.tokens;
344
+ if (saved < minSavingsTokens)
345
+ continue;
346
+ result[i] = {
347
+ ...assistant,
348
+ content: [{ type: "text", text: shrunk.text }],
349
+ };
350
+ tokensSaved += saved;
351
+ messagesModified += 1;
352
+ }
353
+ return { messages: result, tokensSaved, messagesModified };
354
+ }
355
+ export function clearOldToolResults(messages, options = {}) {
356
+ const keepRecent = Math.max(0, options.keepRecent ?? DEFAULT_KEEP_RECENT_TOOL_RESULTS);
357
+ const clearable = new Set(options.clearableToolNames ?? DEFAULT_CLEARABLE_TOOL_NAMES);
358
+ const replacementText = options.replacementText ?? DEFAULT_CLEARED_PLACEHOLDER;
359
+ const clearableIndices = [];
360
+ for (let i = 0; i < messages.length; i += 1) {
361
+ const msg = messages[i];
362
+ if (msg.role === "toolResult" && clearable.has(msg.toolName)) {
363
+ clearableIndices.push(i);
364
+ }
365
+ }
366
+ const clearUntil = Math.max(0, clearableIndices.length - keepRecent);
367
+ if (clearUntil === 0) {
368
+ return { messages: messages.slice(), tokensSaved: 0, toolResultsCleared: 0 };
369
+ }
370
+ const result = messages.slice();
371
+ let tokensSaved = 0;
372
+ let toolResultsCleared = 0;
373
+ const replacementTokens = approxTextTokens(replacementText);
374
+ for (let k = 0; k < clearUntil; k += 1) {
375
+ const idx = clearableIndices[k];
376
+ const msg = result[idx];
377
+ if (msg.role !== "toolResult")
378
+ continue;
379
+ const original = msg;
380
+ const originalText = extractContentText(original.content);
381
+ const originalTokens = approxTextTokens(originalText);
382
+ const images = original.content.filter((c) => c.type === "image");
383
+ result[idx] = {
384
+ ...original,
385
+ content: [{ type: "text", text: replacementText }, ...images],
386
+ };
387
+ const savings = originalTokens - replacementTokens;
388
+ if (savings > 0)
389
+ tokensSaved += savings;
390
+ toolResultsCleared += 1;
391
+ }
392
+ return { messages: result, tokensSaved, toolResultsCleared };
393
+ }
394
+ export function reduceContextMessages(messages, options = {}) {
395
+ let current = messages;
396
+ let tokensSaved = 0;
397
+ let groupsCollapsed = 0;
398
+ let messagesShrunk = 0;
399
+ let toolResultsCleared = 0;
400
+ if (options.collapse !== false) {
401
+ const collapsed = collapseConsecutiveToolResults(current, options.collapse ?? undefined);
402
+ current = collapsed.messages;
403
+ tokensSaved += collapsed.tokensSaved;
404
+ groupsCollapsed += collapsed.groups.length;
405
+ }
406
+ if (options.shrinkAssistant !== false) {
407
+ const shrunk = microCompactAssistantText(current, options.shrinkAssistant ?? undefined);
408
+ current = shrunk.messages;
409
+ tokensSaved += shrunk.tokensSaved;
410
+ messagesShrunk += shrunk.messagesModified;
411
+ }
412
+ if (options.clearToolResults !== false) {
413
+ const cleared = clearOldToolResults(current, options.clearToolResults ?? undefined);
414
+ current = cleared.messages;
415
+ tokensSaved += cleared.tokensSaved;
416
+ toolResultsCleared += cleared.toolResultsCleared;
417
+ }
418
+ return { messages: current, tokensSaved, groupsCollapsed, messagesShrunk, toolResultsCleared };
419
+ }
420
+ //# sourceMappingURL=context-reduction.js.map