iCode-TUI 0.29.1__py3-none-any.whl

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 (1138) hide show
  1. chrys/__init__.py +16 -0
  2. chrys/app/__init__.py +3 -0
  3. chrys/app/acp/__init__.py +3 -0
  4. chrys/app/acp/bridge.py +664 -0
  5. chrys/app/acp/content.py +132 -0
  6. chrys/app/acp/doc/frontend-api.md +425 -0
  7. chrys/app/acp/history.py +585 -0
  8. chrys/app/acp/server.py +1796 -0
  9. chrys/app/acp/session_manager.py +1815 -0
  10. chrys/app/acp/tool_status.py +45 -0
  11. chrys/app/cli/__init__.py +3 -0
  12. chrys/app/cli/acp.py +178 -0
  13. chrys/app/cli/app.py +165 -0
  14. chrys/app/cli/approvals.py +101 -0
  15. chrys/app/cli/headless.py +223 -0
  16. chrys/app/cli/launch_cwd.py +42 -0
  17. chrys/app/cli/profiles.py +298 -0
  18. chrys/app/cli/progress.py +857 -0
  19. chrys/app/cli/run.py +425 -0
  20. chrys/app/cli/serve.py +1593 -0
  21. chrys/app/cli/tool_summary.py +225 -0
  22. chrys/app/cli/trajectory.py +201 -0
  23. chrys/app/cli/workflow.py +601 -0
  24. chrys/app/features/__init__.py +3 -0
  25. chrys/app/features/buddy/__init__.py +3 -0
  26. chrys/app/features/buddy/actions.py +68 -0
  27. chrys/app/features/buddy/animation.py +76 -0
  28. chrys/app/features/buddy/commands.py +196 -0
  29. chrys/app/features/buddy/docs/design.md +187 -0
  30. chrys/app/features/buddy/hatchery.py +80 -0
  31. chrys/app/features/buddy/lifecycle.py +41 -0
  32. chrys/app/features/buddy/model.py +265 -0
  33. chrys/app/features/buddy/pixel_renderer.py +113 -0
  34. chrys/app/features/buddy/pixel_sprites.py +279 -0
  35. chrys/app/features/buddy/portrait.py +76 -0
  36. chrys/app/features/buddy/progression.py +58 -0
  37. chrys/app/features/buddy/replies.py +191 -0
  38. chrys/app/features/buddy/sprites/alien.toml +70 -0
  39. chrys/app/features/buddy/sprites/axolotl.toml +71 -0
  40. chrys/app/features/buddy/sprites/bat.toml +69 -0
  41. chrys/app/features/buddy/sprites/bee.toml +69 -0
  42. chrys/app/features/buddy/sprites/cactus.toml +73 -0
  43. chrys/app/features/buddy/sprites/capybara.toml +70 -0
  44. chrys/app/features/buddy/sprites/cat.toml +69 -0
  45. chrys/app/features/buddy/sprites/crab.toml +70 -0
  46. chrys/app/features/buddy/sprites/dragon.toml +71 -0
  47. chrys/app/features/buddy/sprites/duck.toml +69 -0
  48. chrys/app/features/buddy/sprites/fox.toml +69 -0
  49. chrys/app/features/buddy/sprites/frog.toml +70 -0
  50. chrys/app/features/buddy/sprites/ghost.toml +70 -0
  51. chrys/app/features/buddy/sprites/goose.toml +69 -0
  52. chrys/app/features/buddy/sprites/jelly.toml +70 -0
  53. chrys/app/features/buddy/sprites/llama.toml +70 -0
  54. chrys/app/features/buddy/sprites/mushroom.toml +69 -0
  55. chrys/app/features/buddy/sprites/octopus.toml +70 -0
  56. chrys/app/features/buddy/sprites/owl.toml +70 -0
  57. chrys/app/features/buddy/sprites/panda.toml +71 -0
  58. chrys/app/features/buddy/sprites/penguin.toml +69 -0
  59. chrys/app/features/buddy/sprites/rabbit.toml +70 -0
  60. chrys/app/features/buddy/sprites/robot.toml +72 -0
  61. chrys/app/features/buddy/sprites/shark.toml +70 -0
  62. chrys/app/features/buddy/sprites/snail.toml +69 -0
  63. chrys/app/features/buddy/sprites/snake.toml +69 -0
  64. chrys/app/features/buddy/sprites/tabby.toml +69 -0
  65. chrys/app/features/buddy/sprites/turtle.toml +69 -0
  66. chrys/app/features/buddy/store.py +143 -0
  67. chrys/app/features/session_title/__init__.py +7 -0
  68. chrys/app/features/session_title/updater.py +582 -0
  69. chrys/app/installer.py +651 -0
  70. chrys/app/parsing.py +17 -0
  71. chrys/app/tui/__init__.py +3 -0
  72. chrys/app/tui/app.py +1287 -0
  73. chrys/app/tui/behaviors/__init__.py +3 -0
  74. chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
  75. chrys/app/tui/behaviors/insert_clipboard.py +73 -0
  76. chrys/app/tui/behaviors/right_click_copy.py +146 -0
  77. chrys/app/tui/binding_display.py +139 -0
  78. chrys/app/tui/buddy_messages.py +17 -0
  79. chrys/app/tui/buddy_reply.py +86 -0
  80. chrys/app/tui/chrys.tcss +549 -0
  81. chrys/app/tui/clipboard.py +109 -0
  82. chrys/app/tui/copy_messages.py +10 -0
  83. chrys/app/tui/i18n.py +230 -0
  84. chrys/app/tui/language.py +58 -0
  85. chrys/app/tui/notifications/__init__.py +12 -0
  86. chrys/app/tui/notifications/drivers.py +180 -0
  87. chrys/app/tui/notifications/service.py +126 -0
  88. chrys/app/tui/notifications/settings.py +141 -0
  89. chrys/app/tui/screens/__init__.py +3 -0
  90. chrys/app/tui/screens/agents/__init__.py +8 -0
  91. chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
  92. chrys/app/tui/screens/agents/config.py +2142 -0
  93. chrys/app/tui/screens/agents/config.tcss +169 -0
  94. chrys/app/tui/screens/agents/panels/__init__.py +3 -0
  95. chrys/app/tui/screens/agents/panels/acp.py +1176 -0
  96. chrys/app/tui/screens/agents/panels/basic.py +497 -0
  97. chrys/app/tui/screens/agents/panels/compaction.py +231 -0
  98. chrys/app/tui/screens/agents/panels/config_card.py +218 -0
  99. chrys/app/tui/screens/agents/panels/instructions.py +91 -0
  100. chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
  101. chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
  102. chrys/app/tui/screens/agents/panels/memory.py +684 -0
  103. chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
  104. chrys/app/tui/screens/agents/panels/skills.py +806 -0
  105. chrys/app/tui/screens/agents/panels/subagents.py +652 -0
  106. chrys/app/tui/screens/agents/panels/tools.py +205 -0
  107. chrys/app/tui/screens/agents/picker.py +103 -0
  108. chrys/app/tui/screens/agents/skill_paths.py +38 -0
  109. chrys/app/tui/screens/agents/validation_messages.py +452 -0
  110. chrys/app/tui/screens/dialogs/__init__.py +35 -0
  111. chrys/app/tui/screens/dialogs/agent_load.py +834 -0
  112. chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
  113. chrys/app/tui/screens/dialogs/app_mode.py +55 -0
  114. chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
  115. chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
  116. chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
  117. chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
  118. chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
  119. chrys/app/tui/screens/dialogs/approval/body.py +110 -0
  120. chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
  121. chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
  122. chrys/app/tui/screens/dialogs/ask_user.py +148 -0
  123. chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
  124. chrys/app/tui/screens/dialogs/base.py +84 -0
  125. chrys/app/tui/screens/dialogs/confirm.py +184 -0
  126. chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
  127. chrys/app/tui/screens/dialogs/connection_test.py +183 -0
  128. chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
  129. chrys/app/tui/screens/dialogs/diagram.py +102 -0
  130. chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
  131. chrys/app/tui/screens/dialogs/editor.py +378 -0
  132. chrys/app/tui/screens/dialogs/editor.tcss +132 -0
  133. chrys/app/tui/screens/dialogs/file_picker.py +582 -0
  134. chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
  135. chrys/app/tui/screens/dialogs/fork_session.py +219 -0
  136. chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
  137. chrys/app/tui/screens/dialogs/image_compression.py +102 -0
  138. chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
  139. chrys/app/tui/screens/dialogs/man_page.py +172 -0
  140. chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
  141. chrys/app/tui/screens/dialogs/new_folder.py +277 -0
  142. chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
  143. chrys/app/tui/screens/dialogs/notice.tcss +14 -0
  144. chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
  145. chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
  146. chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
  147. chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
  148. chrys/app/tui/screens/dialogs/session_title.py +110 -0
  149. chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
  150. chrys/app/tui/screens/dialogs/tool_view.py +207 -0
  151. chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
  152. chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
  153. chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
  154. chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
  155. chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
  156. chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
  157. chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
  158. chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
  159. chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
  160. chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
  161. chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
  162. chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
  163. chrys/app/tui/screens/diff/__init__.py +22 -0
  164. chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
  165. chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
  166. chrys/app/tui/screens/diff/screen.py +1532 -0
  167. chrys/app/tui/screens/diff/screen.tcss +104 -0
  168. chrys/app/tui/screens/guides/__init__.py +7 -0
  169. chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
  170. chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
  171. chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
  172. chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
  173. chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
  174. chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
  175. chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
  176. chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
  177. chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
  178. chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
  179. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
  180. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
  181. chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
  182. chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
  183. chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
  184. chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
  185. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
  186. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
  187. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
  188. chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
  189. chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
  190. chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
  191. chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
  192. chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
  193. chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
  194. chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
  195. chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
  196. chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
  197. chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
  198. chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
  199. chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
  200. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
  201. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
  202. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
  203. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
  204. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
  205. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
  206. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
  207. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
  208. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
  209. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
  210. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
  211. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
  212. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
  213. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
  214. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
  215. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
  216. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
  217. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
  218. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
  219. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
  220. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
  221. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
  222. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
  223. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
  224. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
  225. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
  226. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
  227. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
  228. chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
  229. chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
  230. chrys/app/tui/screens/guides/index.py +215 -0
  231. chrys/app/tui/screens/guides/reader.py +173 -0
  232. chrys/app/tui/screens/guides/screen.py +442 -0
  233. chrys/app/tui/screens/guides/screen.tcss +52 -0
  234. chrys/app/tui/screens/logs/__init__.py +7 -0
  235. chrys/app/tui/screens/logs/screen.py +504 -0
  236. chrys/app/tui/screens/logs/screen.tcss +45 -0
  237. chrys/app/tui/screens/main/__init__.py +7 -0
  238. chrys/app/tui/screens/main/action_availability.py +30 -0
  239. chrys/app/tui/screens/main/buddy_command.py +143 -0
  240. chrys/app/tui/screens/main/commands.py +1073 -0
  241. chrys/app/tui/screens/main/config_actions.py +464 -0
  242. chrys/app/tui/screens/main/copy_actions.py +117 -0
  243. chrys/app/tui/screens/main/dialog_controllers.py +826 -0
  244. chrys/app/tui/screens/main/dialog_gateway.py +166 -0
  245. chrys/app/tui/screens/main/diff_controller.py +478 -0
  246. chrys/app/tui/screens/main/engine_read_model.py +184 -0
  247. chrys/app/tui/screens/main/event_handlers.py +2014 -0
  248. chrys/app/tui/screens/main/input_flow.py +241 -0
  249. chrys/app/tui/screens/main/live_diff.py +244 -0
  250. chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
  251. chrys/app/tui/screens/main/model_indicator.py +217 -0
  252. chrys/app/tui/screens/main/navigation.py +343 -0
  253. chrys/app/tui/screens/main/ports.py +571 -0
  254. chrys/app/tui/screens/main/recent_dirs.py +123 -0
  255. chrys/app/tui/screens/main/rollback_controller.py +586 -0
  256. chrys/app/tui/screens/main/runtime_info.py +71 -0
  257. chrys/app/tui/screens/main/screen.py +2265 -0
  258. chrys/app/tui/screens/main/screen.tcss +21 -0
  259. chrys/app/tui/screens/main/session_handlers.py +1127 -0
  260. chrys/app/tui/screens/main/session_title.py +239 -0
  261. chrys/app/tui/screens/main/settings_coordinator.py +365 -0
  262. chrys/app/tui/screens/main/settings_persistence.py +184 -0
  263. chrys/app/tui/screens/main/shell_mode.py +112 -0
  264. chrys/app/tui/screens/main/state.py +289 -0
  265. chrys/app/tui/screens/main/submit_state.py +11 -0
  266. chrys/app/tui/screens/main/subscriptions.py +177 -0
  267. chrys/app/tui/screens/main/suggestions.py +982 -0
  268. chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
  269. chrys/app/tui/screens/main/view_adapter.py +1567 -0
  270. chrys/app/tui/screens/main/workflow_browser.py +747 -0
  271. chrys/app/tui/screens/main/workflow_content.py +248 -0
  272. chrys/app/tui/screens/main/workflow_controller.py +240 -0
  273. chrys/app/tui/screens/main/workflow_feedback.py +137 -0
  274. chrys/app/tui/screens/main/workflow_flow.py +49 -0
  275. chrys/app/tui/screens/main/workflow_run_control.py +456 -0
  276. chrys/app/tui/screens/main/workflow_session_view.py +316 -0
  277. chrys/app/tui/screens/main/workspace_actions.py +272 -0
  278. chrys/app/tui/screens/main/workspace_branch.py +194 -0
  279. chrys/app/tui/screens/menus/__init__.py +8 -0
  280. chrys/app/tui/screens/menus/languages.py +114 -0
  281. chrys/app/tui/screens/models/__init__.py +7 -0
  282. chrys/app/tui/screens/models/picker.py +147 -0
  283. chrys/app/tui/screens/models/screen.py +2043 -0
  284. chrys/app/tui/screens/models/screen.tcss +255 -0
  285. chrys/app/tui/screens/sessions/__init__.py +8 -0
  286. chrys/app/tui/screens/sessions/presenter.py +586 -0
  287. chrys/app/tui/screens/sessions/screen.py +779 -0
  288. chrys/app/tui/screens/sessions/screen.tcss +127 -0
  289. chrys/app/tui/screens/settings/__init__.py +29 -0
  290. chrys/app/tui/screens/settings/danger.py +26 -0
  291. chrys/app/tui/screens/settings/dialog.py +281 -0
  292. chrys/app/tui/screens/settings/layout.py +519 -0
  293. chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
  294. chrys/app/tui/screens/settings/panes/__init__.py +3 -0
  295. chrys/app/tui/screens/settings/panes/notifications.py +248 -0
  296. chrys/app/tui/screens/settings/panes/sessions.py +148 -0
  297. chrys/app/tui/screens/settings/ports.py +55 -0
  298. chrys/app/tui/screens/settings/provenance.py +102 -0
  299. chrys/app/tui/screens/settings/rows.py +546 -0
  300. chrys/app/tui/screens/settings/settings.tcss +406 -0
  301. chrys/app/tui/screens/themes/__init__.py +8 -0
  302. chrys/app/tui/screens/themes/dialogs.py +330 -0
  303. chrys/app/tui/screens/themes/editor.py +632 -0
  304. chrys/app/tui/screens/themes/editor.tcss +268 -0
  305. chrys/app/tui/screens/themes/messages.py +95 -0
  306. chrys/app/tui/screens/themes/palette.py +694 -0
  307. chrys/app/tui/screens/themes/panel.py +113 -0
  308. chrys/app/tui/screens/themes/picker.py +203 -0
  309. chrys/app/tui/screens/themes/save.py +191 -0
  310. chrys/app/tui/support/__init__.py +3 -0
  311. chrys/app/tui/support/file_manager.py +64 -0
  312. chrys/app/tui/support/gc_freeze.py +1114 -0
  313. chrys/app/tui/support/path_shortcuts.py +50 -0
  314. chrys/app/tui/support/prompt_history.py +298 -0
  315. chrys/app/tui/support/workspace_mru.py +476 -0
  316. chrys/app/tui/terminal/__init__.py +6 -0
  317. chrys/app/tui/terminal/_pty_backend.py +114 -0
  318. chrys/app/tui/terminal/emulator/__init__.py +24 -0
  319. chrys/app/tui/terminal/emulator/buffer.py +456 -0
  320. chrys/app/tui/terminal/emulator/charsets.py +113 -0
  321. chrys/app/tui/terminal/emulator/core.py +963 -0
  322. chrys/app/tui/terminal/emulator/input.py +342 -0
  323. chrys/app/tui/terminal/emulator/parser.py +227 -0
  324. chrys/app/tui/terminal/emulator/pen.py +158 -0
  325. chrys/app/tui/terminal/launcher.py +455 -0
  326. chrys/app/tui/terminal/leaked_reports.py +44 -0
  327. chrys/app/tui/terminal/panel.py +323 -0
  328. chrys/app/tui/terminal/pty_process.py +405 -0
  329. chrys/app/tui/terminal/rendering.py +151 -0
  330. chrys/app/tui/terminal/shell.py +273 -0
  331. chrys/app/tui/terminal/shell_integration.py +199 -0
  332. chrys/app/tui/terminal/title.py +224 -0
  333. chrys/app/tui/terminal/widget.py +654 -0
  334. chrys/app/tui/theme.py +422 -0
  335. chrys/app/tui/theme_loader.py +201 -0
  336. chrys/app/tui/theme_stylesheet.py +49 -0
  337. chrys/app/tui/themes/__init__.py +3 -0
  338. chrys/app/tui/themes/document.py +120 -0
  339. chrys/app/tui/themes/preview.py +170 -0
  340. chrys/app/tui/themes/store.py +176 -0
  341. chrys/app/tui/toasts.py +69 -0
  342. chrys/app/tui/util/__init__.py +3 -0
  343. chrys/app/tui/util/context_pressure.py +146 -0
  344. chrys/app/tui/util/debug_log.py +292 -0
  345. chrys/app/tui/util/diff_entries.py +64 -0
  346. chrys/app/tui/util/formatting.py +53 -0
  347. chrys/app/tui/util/git_branch.py +326 -0
  348. chrys/app/tui/util/invocation_progress.py +57 -0
  349. chrys/app/tui/util/logo.py +32 -0
  350. chrys/app/tui/util/message_gate.py +35 -0
  351. chrys/app/tui/util/mutation_sources.py +14 -0
  352. chrys/app/tui/util/picker_text.py +27 -0
  353. chrys/app/tui/util/removal.py +47 -0
  354. chrys/app/tui/util/rich_style.py +36 -0
  355. chrys/app/tui/util/source_text.py +12 -0
  356. chrys/app/tui/util/static_update.py +88 -0
  357. chrys/app/tui/util/todo_format.py +51 -0
  358. chrys/app/tui/util/visibility.py +214 -0
  359. chrys/app/tui/widgets/__init__.py +60 -0
  360. chrys/app/tui/widgets/ask_user_controls.py +652 -0
  361. chrys/app/tui/widgets/ask_user_markdown.py +17 -0
  362. chrys/app/tui/widgets/ask_user_prompt.py +606 -0
  363. chrys/app/tui/widgets/buttons.py +52 -0
  364. chrys/app/tui/widgets/chat/__init__.py +3 -0
  365. chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
  366. chrys/app/tui/widgets/chat/chrome.py +296 -0
  367. chrys/app/tui/widgets/chat/compaction_card.py +353 -0
  368. chrys/app/tui/widgets/chat/context_fold.py +77 -0
  369. chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
  370. chrys/app/tui/widgets/chat/image_preview.py +214 -0
  371. chrys/app/tui/widgets/chat/live_renderer.py +383 -0
  372. chrys/app/tui/widgets/chat/messages.py +1039 -0
  373. chrys/app/tui/widgets/chat/panel.py +1769 -0
  374. chrys/app/tui/widgets/chat/ports.py +245 -0
  375. chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
  376. chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
  377. chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
  378. chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
  379. chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
  380. chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
  381. chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
  382. chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
  383. chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
  384. chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
  385. chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
  386. chrys/app/tui/widgets/chat/renderers/search.py +348 -0
  387. chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
  388. chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
  389. chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
  390. chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
  391. chrys/app/tui/widgets/chat/renderers/web.py +69 -0
  392. chrys/app/tui/widgets/chat/replay.py +1605 -0
  393. chrys/app/tui/widgets/chat/replay_mount.py +322 -0
  394. chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
  395. chrys/app/tui/widgets/chat/selection_controller.py +435 -0
  396. chrys/app/tui/widgets/chat/session_json.py +508 -0
  397. chrys/app/tui/widgets/chat/toc_model.py +156 -0
  398. chrys/app/tui/widgets/chat/tool_call.py +2478 -0
  399. chrys/app/tui/widgets/chat/tool_registry.py +753 -0
  400. chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
  401. chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
  402. chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
  403. chrys/app/tui/widgets/checkbox.py +24 -0
  404. chrys/app/tui/widgets/chrome/__init__.py +3 -0
  405. chrys/app/tui/widgets/chrome/app_header.py +283 -0
  406. chrys/app/tui/widgets/chrome/commands.py +95 -0
  407. chrys/app/tui/widgets/chrome/file_index.py +68 -0
  408. chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
  409. chrys/app/tui/widgets/chrome/footer.py +284 -0
  410. chrys/app/tui/widgets/chrome/image_paste.py +351 -0
  411. chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
  412. chrys/app/tui/widgets/chrome/status_bar.py +911 -0
  413. chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
  414. chrys/app/tui/widgets/click_affordance.py +24 -0
  415. chrys/app/tui/widgets/color_picker/__init__.py +254 -0
  416. chrys/app/tui/widgets/color_picker/controls.py +171 -0
  417. chrys/app/tui/widgets/color_picker/model.py +89 -0
  418. chrys/app/tui/widgets/dialog_buttons.py +58 -0
  419. chrys/app/tui/widgets/dialog_scroll.py +24 -0
  420. chrys/app/tui/widgets/diff_view/__init__.py +20 -0
  421. chrys/app/tui/widgets/diff_view/cells.py +59 -0
  422. chrys/app/tui/widgets/diff_view/code.py +280 -0
  423. chrys/app/tui/widgets/diff_view/compute.py +286 -0
  424. chrys/app/tui/widgets/diff_view/gutter.py +161 -0
  425. chrys/app/tui/widgets/diff_view/inline.py +77 -0
  426. chrys/app/tui/widgets/diff_view/palette.py +87 -0
  427. chrys/app/tui/widgets/diff_view/protocols.py +24 -0
  428. chrys/app/tui/widgets/diff_view/rows.py +138 -0
  429. chrys/app/tui/widgets/diff_view/unified.py +267 -0
  430. chrys/app/tui/widgets/diff_view/widget.py +209 -0
  431. chrys/app/tui/widgets/editor/__init__.py +79 -0
  432. chrys/app/tui/widgets/editor/coordinates.py +30 -0
  433. chrys/app/tui/widgets/editor/highlighter.py +139 -0
  434. chrys/app/tui/widgets/editor/keymaps.py +357 -0
  435. chrys/app/tui/widgets/editor/types.py +216 -0
  436. chrys/app/tui/widgets/editor/vim.py +1195 -0
  437. chrys/app/tui/widgets/editor/widget.py +401 -0
  438. chrys/app/tui/widgets/file_preview.py +58 -0
  439. chrys/app/tui/widgets/hatch.py +138 -0
  440. chrys/app/tui/widgets/input.py +66 -0
  441. chrys/app/tui/widgets/loading.py +125 -0
  442. chrys/app/tui/widgets/markdown/__init__.py +25 -0
  443. chrys/app/tui/widgets/markdown/_utils.py +21 -0
  444. chrys/app/tui/widgets/markdown/anchors.py +71 -0
  445. chrys/app/tui/widgets/markdown/blocks.py +115 -0
  446. chrys/app/tui/widgets/markdown/copyable.py +163 -0
  447. chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
  448. chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
  449. chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
  450. chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
  451. chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
  452. chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
  453. chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
  454. chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
  455. chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
  456. chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
  457. chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
  458. chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
  459. chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
  460. chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
  461. chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
  462. chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
  463. chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
  464. chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
  465. chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
  466. chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
  467. chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
  468. chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
  469. chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
  470. chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
  471. chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
  472. chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
  473. chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
  474. chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
  475. chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
  476. chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
  477. chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
  478. chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
  479. chrys/app/tui/widgets/markdown/links.py +47 -0
  480. chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
  481. chrys/app/tui/widgets/markdown/math/layout.py +739 -0
  482. chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
  483. chrys/app/tui/widgets/markdown/math/parser.py +325 -0
  484. chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
  485. chrys/app/tui/widgets/markdown/navigator.py +80 -0
  486. chrys/app/tui/widgets/markdown/parser.py +1057 -0
  487. chrys/app/tui/widgets/markdown/stream.py +73 -0
  488. chrys/app/tui/widgets/markdown/toc.py +98 -0
  489. chrys/app/tui/widgets/markdown/viewer.py +146 -0
  490. chrys/app/tui/widgets/markdown/widget.py +2046 -0
  491. chrys/app/tui/widgets/marquee.py +101 -0
  492. chrys/app/tui/widgets/option_menu.py +82 -0
  493. chrys/app/tui/widgets/page_navigator.py +118 -0
  494. chrys/app/tui/widgets/select.py +196 -0
  495. chrys/app/tui/widgets/selection.py +59 -0
  496. chrys/app/tui/widgets/sidebar/__init__.py +3 -0
  497. chrys/app/tui/widgets/sidebar/buddy.py +367 -0
  498. chrys/app/tui/widgets/sidebar/context.py +482 -0
  499. chrys/app/tui/widgets/sidebar/debug.py +195 -0
  500. chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
  501. chrys/app/tui/widgets/sidebar/panel.py +198 -0
  502. chrys/app/tui/widgets/sidebar/tasks.py +198 -0
  503. chrys/app/tui/widgets/sidebar/toc.py +166 -0
  504. chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
  505. chrys/app/tui/widgets/syntax_theme.py +85 -0
  506. chrys/app/tui/widgets/text_area.py +258 -0
  507. chrys/app/tui/widgets/trajectory/__init__.py +8 -0
  508. chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
  509. chrys/app/tui/widgets/trajectory/insights.py +1266 -0
  510. chrys/app/tui/widgets/trajectory/overview.py +1101 -0
  511. chrys/app/tui/widgets/trajectory/panel.py +983 -0
  512. chrys/app/tui/widgets/trajectory/presentation.py +311 -0
  513. chrys/app/tui/widgets/trajectory/session_info.py +124 -0
  514. chrys/app/tui/widgets/trajectory/text_view.py +124 -0
  515. chrys/app/tui/widgets/trajectory/timeline.py +380 -0
  516. chrys/app/tui/widgets/welcome.py +126 -0
  517. chrys/app/tui/widgets/workflow/__init__.py +5 -0
  518. chrys/app/tui/widgets/workflow/edges.py +89 -0
  519. chrys/app/tui/widgets/workflow/graph.py +869 -0
  520. chrys/app/tui/widgets/workflow/info.py +310 -0
  521. chrys/app/tui/widgets/workflow/node_view.py +27 -0
  522. chrys/app/tui/widgets/workflow/output.py +225 -0
  523. chrys/app/tui/widgets/workflow/panel.py +658 -0
  524. chrys/app/tui/widgets/workflow/projector.py +324 -0
  525. chrys/app/tui/widgets/workflow/records.py +69 -0
  526. chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
  527. chrys/app/tui/widgets/workflow/selection.py +151 -0
  528. chrys/app/tui/widgets/workflow/source.py +65 -0
  529. chrys/app/tui/widgets/workflow/text.py +257 -0
  530. chrys/app/tui/widgets/workflow/values.py +317 -0
  531. chrys/foundation/__init__.py +0 -0
  532. chrys/foundation/branding.py +15 -0
  533. chrys/foundation/config/__init__.py +7 -0
  534. chrys/foundation/config/coercion.py +374 -0
  535. chrys/foundation/config/context.py +34 -0
  536. chrys/foundation/config/env_file.py +141 -0
  537. chrys/foundation/config/env_layers.py +370 -0
  538. chrys/foundation/config/migrations.py +521 -0
  539. chrys/foundation/config/notification_events.py +22 -0
  540. chrys/foundation/config/process_settings.py +332 -0
  541. chrys/foundation/config/runtime_pointer.py +133 -0
  542. chrys/foundation/config/settings.py +1952 -0
  543. chrys/foundation/config/settings_store.py +996 -0
  544. chrys/foundation/config/spec.py +396 -0
  545. chrys/foundation/config/user_settings.py +140 -0
  546. chrys/foundation/config/warnings.py +262 -0
  547. chrys/foundation/config/web_values.py +135 -0
  548. chrys/foundation/config/yaml_store.py +487 -0
  549. chrys/foundation/errors/__init__.py +68 -0
  550. chrys/foundation/errors/_legacy.py +117 -0
  551. chrys/foundation/errors/_text.py +13 -0
  552. chrys/foundation/errors/_walk.py +85 -0
  553. chrys/foundation/errors/classify.py +278 -0
  554. chrys/foundation/errors/display.py +274 -0
  555. chrys/foundation/errors/formatting.py +283 -0
  556. chrys/foundation/errors/kinds.py +67 -0
  557. chrys/foundation/errors/network.py +497 -0
  558. chrys/foundation/errors/provider.py +445 -0
  559. chrys/foundation/errors/route.py +85 -0
  560. chrys/foundation/events/__init__.py +8 -0
  561. chrys/foundation/events/bus.py +143 -0
  562. chrys/foundation/events/types.py +1629 -0
  563. chrys/foundation/events/workflow.py +38 -0
  564. chrys/foundation/hosted_tools.py +142 -0
  565. chrys/foundation/i18n/__init__.py +32 -0
  566. chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
  567. chrys/foundation/i18n/catalogs.py +42 -0
  568. chrys/foundation/i18n/formatting.py +202 -0
  569. chrys/foundation/i18n/locale.py +92 -0
  570. chrys/foundation/i18n/localizer.py +187 -0
  571. chrys/foundation/i18n/messages.py +268 -0
  572. chrys/foundation/io/__init__.py +3 -0
  573. chrys/foundation/io/result_content.py +237 -0
  574. chrys/foundation/models/__init__.py +3 -0
  575. chrys/foundation/models/approval_reuse.py +23 -0
  576. chrys/foundation/models/ask_user.py +468 -0
  577. chrys/foundation/models/execution.py +18 -0
  578. chrys/foundation/models/history_markers.py +115 -0
  579. chrys/foundation/models/invocations.py +63 -0
  580. chrys/foundation/models/mutation_scope.py +28 -0
  581. chrys/foundation/models/session_env.py +87 -0
  582. chrys/foundation/models/session_surface.py +29 -0
  583. chrys/foundation/models/todos.py +74 -0
  584. chrys/foundation/models/turns.py +125 -0
  585. chrys/foundation/models/workflow_session.py +89 -0
  586. chrys/foundation/models/workspace.py +59 -0
  587. chrys/foundation/net/__init__.py +3 -0
  588. chrys/foundation/net/address_class.py +57 -0
  589. chrys/foundation/net/route_probe.py +94 -0
  590. chrys/foundation/net/url.py +107 -0
  591. chrys/foundation/observability/__init__.py +3 -0
  592. chrys/foundation/observability/exporters.py +174 -0
  593. chrys/foundation/observability/gate.py +31 -0
  594. chrys/foundation/observability/setup.py +497 -0
  595. chrys/foundation/observability/sink.py +228 -0
  596. chrys/foundation/patches/__init__.py +19 -0
  597. chrys/foundation/patches/mdurl_cache.py +59 -0
  598. chrys/foundation/patches/patcher.py +287 -0
  599. chrys/foundation/patches/staged_members.py +205 -0
  600. chrys/foundation/patches/textual_block_border.py +105 -0
  601. chrys/foundation/patches/textual_button_ansi.py +65 -0
  602. chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
  603. chrys/foundation/patches/textual_callback_cache.py +248 -0
  604. chrys/foundation/patches/textual_callback_dispatch.py +109 -0
  605. chrys/foundation/patches/textual_compositor_cjk.py +566 -0
  606. chrys/foundation/patches/textual_dispatch_cache.py +150 -0
  607. chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
  608. chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
  609. chrys/foundation/patches/textual_lru_acyclic.py +241 -0
  610. chrys/foundation/patches/textual_message_pump.py +136 -0
  611. chrys/foundation/patches/textual_node_diet.py +875 -0
  612. chrys/foundation/patches/textual_one_shot_timer.py +48 -0
  613. chrys/foundation/patches/textual_option_list.py +90 -0
  614. chrys/foundation/patches/textual_precompose.py +89 -0
  615. chrys/foundation/patches/textual_pruned_tabs.py +69 -0
  616. chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
  617. chrys/foundation/patches/textual_removed_node_caches.py +86 -0
  618. chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
  619. chrys/foundation/patches/textual_selection_extract.py +48 -0
  620. chrys/foundation/patches/textual_strip_cycles.py +119 -0
  621. chrys/foundation/patches/textual_tab_selection.py +1611 -0
  622. chrys/foundation/patches/textual_timer_skip.py +70 -0
  623. chrys/foundation/patches/textual_utf8_decoder.py +106 -0
  624. chrys/foundation/patches/textual_win_sleep.py +167 -0
  625. chrys/foundation/patches/watchdog_fsevents.py +70 -0
  626. chrys/foundation/patches/watchdog_windows.py +52 -0
  627. chrys/foundation/platform/__init__.py +556 -0
  628. chrys/foundation/platform/c_api.py +32 -0
  629. chrys/foundation/platform/child_reap.py +107 -0
  630. chrys/foundation/platform/command_line.py +95 -0
  631. chrys/foundation/platform/files.py +1470 -0
  632. chrys/foundation/platform/output_capture.py +113 -0
  633. chrys/foundation/platform/paths.py +193 -0
  634. chrys/foundation/platform/process.py +1468 -0
  635. chrys/foundation/platform/pty_output.py +19 -0
  636. chrys/foundation/platform/runtime_paths.py +281 -0
  637. chrys/foundation/platform/win_console.py +129 -0
  638. chrys/foundation/reasoning_origin.py +67 -0
  639. chrys/foundation/recovery.py +16 -0
  640. chrys/foundation/retry.py +278 -0
  641. chrys/foundation/text/__init__.py +3 -0
  642. chrys/foundation/text/encoding.py +603 -0
  643. chrys/foundation/text/images.py +304 -0
  644. chrys/foundation/text/lines.py +24 -0
  645. chrys/foundation/text/mentions.py +90 -0
  646. chrys/foundation/text/model_json.py +25 -0
  647. chrys/foundation/text/reading_order.py +20 -0
  648. chrys/foundation/text/tokenizer.py +25 -0
  649. chrys/foundation/text/tool_output.py +287 -0
  650. chrys/foundation/text/yaml_io.py +62 -0
  651. chrys/foundation/tool_call_context.py +202 -0
  652. chrys/foundation/tool_execution_stamp.py +184 -0
  653. chrys/foundation/tool_invocation_order.py +48 -0
  654. chrys/foundation/tool_kinds.py +102 -0
  655. chrys/foundation/tool_result_metadata.py +287 -0
  656. chrys/foundation/trajectory/__init__.py +10 -0
  657. chrys/foundation/trajectory/context.py +481 -0
  658. chrys/foundation/trajectory/envelope.py +571 -0
  659. chrys/foundation/trajectory/event_types.py +294 -0
  660. chrys/foundation/trajectory/finalizers.py +46 -0
  661. chrys/foundation/trajectory/fingerprint.py +62 -0
  662. chrys/foundation/trajectory/ids.py +155 -0
  663. chrys/foundation/trajectory/keys.py +99 -0
  664. chrys/foundation/trajectory/lease.py +173 -0
  665. chrys/foundation/trajectory/metadata.py +72 -0
  666. chrys/foundation/trajectory/reader.py +271 -0
  667. chrys/foundation/trajectory/recovery.py +225 -0
  668. chrys/foundation/trajectory/revisions.py +152 -0
  669. chrys/foundation/trajectory/segments.py +225 -0
  670. chrys/foundation/trajectory/tools.py +117 -0
  671. chrys/foundation/trajectory/usage.py +151 -0
  672. chrys/foundation/trajectory/writer.py +972 -0
  673. chrys/foundation/trajectory_timing.py +97 -0
  674. chrys/foundation/util/__init__.py +3 -0
  675. chrys/foundation/util/async_tasks.py +29 -0
  676. chrys/foundation/util/chrys_headers.py +23 -0
  677. chrys/foundation/util/env_templates.py +104 -0
  678. chrys/foundation/util/filenames.py +96 -0
  679. chrys/foundation/util/header_charset.py +138 -0
  680. chrys/foundation/util/httpx_helpers.py +12 -0
  681. chrys/foundation/util/lock.py +236 -0
  682. chrys/foundation/util/once_close.py +46 -0
  683. chrys/foundation/util/session_ids.py +29 -0
  684. chrys/foundation/util/shell_tokens.py +88 -0
  685. chrys/foundation/util/sub_agent_context.py +38 -0
  686. chrys/foundation/util/time.py +35 -0
  687. chrys/foundation/util/unicode_scalars.py +36 -0
  688. chrys/foundation/vendor/__init__.py +43 -0
  689. chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
  690. chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
  691. chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
  692. chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
  693. chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
  694. chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
  695. chrys/kernel/__init__.py +256 -0
  696. chrys/kernel/_content.py +1866 -0
  697. chrys/kernel/_loop_recorder.py +631 -0
  698. chrys/kernel/_null_overlay.py +1026 -0
  699. chrys/kernel/_result_ceiling.py +216 -0
  700. chrys/kernel/_serialization.py +431 -0
  701. chrys/kernel/_tool_arg_errors.py +1629 -0
  702. chrys/kernel/_tool_execution.py +1172 -0
  703. chrys/kernel/_tool_expansion.py +26 -0
  704. chrys/kernel/_types.py +2598 -0
  705. chrys/kernel/agent.py +994 -0
  706. chrys/kernel/client.py +1084 -0
  707. chrys/kernel/compaction.py +1315 -0
  708. chrys/kernel/exceptions.py +211 -0
  709. chrys/kernel/exchanges.py +683 -0
  710. chrys/kernel/identity.py +95 -0
  711. chrys/kernel/images.py +36 -0
  712. chrys/kernel/instrumentation.py +1618 -0
  713. chrys/kernel/loop.py +2308 -0
  714. chrys/kernel/middleware.py +616 -0
  715. chrys/kernel/sessions.py +604 -0
  716. chrys/kernel/tools.py +1061 -0
  717. chrys/kernel/types.py +126 -0
  718. chrys/orchestration/__init__.py +3 -0
  719. chrys/orchestration/engine/__init__.py +3 -0
  720. chrys/orchestration/engine/assembly.py +236 -0
  721. chrys/orchestration/engine/build/__init__.py +3 -0
  722. chrys/orchestration/engine/build/builder.py +1061 -0
  723. chrys/orchestration/engine/build/construction.py +408 -0
  724. chrys/orchestration/engine/build/loaded.py +139 -0
  725. chrys/orchestration/engine/engine.py +789 -0
  726. chrys/orchestration/engine/execution.py +771 -0
  727. chrys/orchestration/engine/loader.py +865 -0
  728. chrys/orchestration/engine/rollback.py +918 -0
  729. chrys/orchestration/engine/run/__init__.py +3 -0
  730. chrys/orchestration/engine/run/active_injection.py +419 -0
  731. chrys/orchestration/engine/run/attachments.py +519 -0
  732. chrys/orchestration/engine/run/bindings.py +1095 -0
  733. chrys/orchestration/engine/run/coordinator.py +1062 -0
  734. chrys/orchestration/engine/run/finalizer.py +417 -0
  735. chrys/orchestration/engine/run/input_refs.py +60 -0
  736. chrys/orchestration/engine/run/prompt_content.py +457 -0
  737. chrys/orchestration/engine/run/resume.py +299 -0
  738. chrys/orchestration/engine/run/retry.py +948 -0
  739. chrys/orchestration/engine/run/runner.py +625 -0
  740. chrys/orchestration/engine/run/runtime_skills.py +146 -0
  741. chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
  742. chrys/orchestration/engine/run/turn_hooks.py +258 -0
  743. chrys/orchestration/engine/run/turn_state.py +149 -0
  744. chrys/orchestration/engine/run/working_dir.py +50 -0
  745. chrys/orchestration/engine/session_lifecycle.py +2013 -0
  746. chrys/orchestration/engine/state/__init__.py +3 -0
  747. chrys/orchestration/engine/state/active_session.py +183 -0
  748. chrys/orchestration/engine/state/controls.py +639 -0
  749. chrys/orchestration/engine/state/current_agent.py +26 -0
  750. chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
  751. chrys/orchestration/engine/state/machine.py +193 -0
  752. chrys/orchestration/engine/state/session_writer.py +434 -0
  753. chrys/orchestration/engine/trajectory.py +714 -0
  754. chrys/orchestration/engine/usage.py +118 -0
  755. chrys/orchestration/invoker/__init__.py +3 -0
  756. chrys/orchestration/invoker/acp.py +727 -0
  757. chrys/orchestration/invoker/acp_protocol.py +1115 -0
  758. chrys/orchestration/invoker/acp_spec.py +79 -0
  759. chrys/orchestration/invoker/attempts.py +994 -0
  760. chrys/orchestration/invoker/child_compaction.py +131 -0
  761. chrys/orchestration/invoker/child_history.py +99 -0
  762. chrys/orchestration/invoker/contracts.py +253 -0
  763. chrys/orchestration/invoker/evidence.py +94 -0
  764. chrys/orchestration/invoker/kernel.py +343 -0
  765. chrys/orchestration/invoker/origin.py +79 -0
  766. chrys/orchestration/invoker/resources.py +343 -0
  767. chrys/orchestration/invoker/runtime.py +299 -0
  768. chrys/orchestration/invoker/usage.py +80 -0
  769. chrys/orchestration/session_hooks.py +117 -0
  770. chrys/orchestration/session_host.py +1023 -0
  771. chrys/orchestration/session_resources.py +95 -0
  772. chrys/orchestration/session_usage.py +219 -0
  773. chrys/orchestration/startup.py +255 -0
  774. chrys/orchestration/sub_agents/__init__.py +3 -0
  775. chrys/orchestration/sub_agents/acp_policy.py +269 -0
  776. chrys/orchestration/sub_agents/kernel_policy.py +848 -0
  777. chrys/orchestration/sub_agents/registration.py +44 -0
  778. chrys/orchestration/sub_agents/shell.py +337 -0
  779. chrys/orchestration/sub_agents/tools.py +2228 -0
  780. chrys/orchestration/workflows/__init__.py +3 -0
  781. chrys/orchestration/workflows/agent_archive.py +156 -0
  782. chrys/orchestration/workflows/agent_node.py +894 -0
  783. chrys/orchestration/workflows/agent_node_build.py +687 -0
  784. chrys/orchestration/workflows/catalog.py +332 -0
  785. chrys/orchestration/workflows/coordinator.py +771 -0
  786. chrys/orchestration/workflows/hooks.py +176 -0
  787. chrys/orchestration/workflows/preview.py +322 -0
  788. chrys/orchestration/workflows/rollback.py +151 -0
  789. chrys/orchestration/workflows/runner.py +986 -0
  790. chrys/orchestration/workflows/session.py +390 -0
  791. chrys/orchestration/workflows/settings.py +97 -0
  792. chrys/orchestration/workflows/validation.py +674 -0
  793. chrys/orchestration/workflows/worker_client.py +1002 -0
  794. chrys/service/__init__.py +3 -0
  795. chrys/service/acp_client/__init__.py +65 -0
  796. chrys/service/acp_client/_transport.py +922 -0
  797. chrys/service/acp_client/client.py +1311 -0
  798. chrys/service/acp_client/errors.py +224 -0
  799. chrys/service/acp_client/protocol.py +220 -0
  800. chrys/service/acp_client/spawn.py +239 -0
  801. chrys/service/acp_client/spec.py +100 -0
  802. chrys/service/agent_middleware/__init__.py +60 -0
  803. chrys/service/agent_middleware/_metadata_keys.py +52 -0
  804. chrys/service/agent_middleware/control/__init__.py +3 -0
  805. chrys/service/agent_middleware/control/approval.py +769 -0
  806. chrys/service/agent_middleware/control/approval_hooks.py +62 -0
  807. chrys/service/agent_middleware/control/ask_user.py +207 -0
  808. chrys/service/agent_middleware/control/interrupt.py +50 -0
  809. chrys/service/agent_middleware/control/sleep.py +118 -0
  810. chrys/service/agent_middleware/control/todo.py +117 -0
  811. chrys/service/agent_middleware/events/__init__.py +3 -0
  812. chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
  813. chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
  814. chrys/service/agent_middleware/events/intermediate_text.py +128 -0
  815. chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
  816. chrys/service/agent_middleware/events/result_persistence.py +98 -0
  817. chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
  818. chrys/service/agent_middleware/events/tool_error_text.py +22 -0
  819. chrys/service/agent_middleware/events/tool_events.py +779 -0
  820. chrys/service/agent_middleware/injection.py +510 -0
  821. chrys/service/agent_middleware/reminders/__init__.py +91 -0
  822. chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
  823. chrys/service/agent_middleware/reminders/base.py +25 -0
  824. chrys/service/agent_middleware/reminders/context_usage.py +74 -0
  825. chrys/service/agent_middleware/reminders/file_change.py +63 -0
  826. chrys/service/agent_middleware/reminders/mcp.py +39 -0
  827. chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
  828. chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
  829. chrys/service/agent_middleware/reminders/skills.py +44 -0
  830. chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
  831. chrys/service/agent_middleware/reminders/todo.py +35 -0
  832. chrys/service/agent_middleware/reminders/turn_line.py +48 -0
  833. chrys/service/agent_middleware/response_validation.py +1380 -0
  834. chrys/service/agent_middleware/system_reminder.py +1107 -0
  835. chrys/service/agent_middleware/validators.py +396 -0
  836. chrys/service/analytics/__init__.py +124 -0
  837. chrys/service/analytics/_actions.py +896 -0
  838. chrys/service/analytics/_context_evidence.py +357 -0
  839. chrys/service/analytics/_critical_path.py +179 -0
  840. chrys/service/analytics/_facts.py +919 -0
  841. chrys/service/analytics/_insights.py +769 -0
  842. chrys/service/analytics/_metric_ops.py +83 -0
  843. chrys/service/analytics/_session_projection.py +460 -0
  844. chrys/service/analytics/_timeline.py +434 -0
  845. chrys/service/analytics/_turn_graph.py +1237 -0
  846. chrys/service/analytics/_turns.py +1377 -0
  847. chrys/service/analytics/_workflows.py +171 -0
  848. chrys/service/analytics/aggregation.py +1474 -0
  849. chrys/service/analytics/classification.py +128 -0
  850. chrys/service/analytics/export.py +803 -0
  851. chrys/service/analytics/findings.py +282 -0
  852. chrys/service/analytics/math.py +79 -0
  853. chrys/service/analytics/model.py +886 -0
  854. chrys/service/analytics/reader.py +321 -0
  855. chrys/service/approval/__init__.py +3 -0
  856. chrys/service/approval/arbitration.py +102 -0
  857. chrys/service/approval/command_identity.py +109 -0
  858. chrys/service/approval/correlation.py +57 -0
  859. chrys/service/approval/grant_store.py +108 -0
  860. chrys/service/approval/judge.py +589 -0
  861. chrys/service/approval/level.py +15 -0
  862. chrys/service/approval/policy.py +209 -0
  863. chrys/service/approval/reuse.py +223 -0
  864. chrys/service/approval/reuse_binding.py +133 -0
  865. chrys/service/approval/safety_classifier.py +536 -0
  866. chrys/service/approval/turn_context.py +49 -0
  867. chrys/service/context/__init__.py +3 -0
  868. chrys/service/context/compaction/__init__.py +38 -0
  869. chrys/service/context/compaction/breaker.py +156 -0
  870. chrys/service/context/compaction/budgets.py +93 -0
  871. chrys/service/context/compaction/calibration.py +77 -0
  872. chrys/service/context/compaction/compression.py +690 -0
  873. chrys/service/context/compaction/current_turn_drop.py +458 -0
  874. chrys/service/context/compaction/events.py +201 -0
  875. chrys/service/context/compaction/exclusions.py +261 -0
  876. chrys/service/context/compaction/groups.py +361 -0
  877. chrys/service/context/compaction/last_words.py +1996 -0
  878. chrys/service/context/compaction/last_words_state.py +752 -0
  879. chrys/service/context/compaction/scoped.py +472 -0
  880. chrys/service/context/compaction/spill.py +1947 -0
  881. chrys/service/context/compaction/strategy.py +1116 -0
  882. chrys/service/context/compaction/summaries.py +319 -0
  883. chrys/service/context/compaction/turns.py +375 -0
  884. chrys/service/context/manager.py +174 -0
  885. chrys/service/context/memory_loader.py +792 -0
  886. chrys/service/context/middleware/__init__.py +3 -0
  887. chrys/service/context/middleware/usage.py +332 -0
  888. chrys/service/context/providers/__init__.py +3 -0
  889. chrys/service/context/providers/context_management.py +432 -0
  890. chrys/service/context/providers/history.py +647 -0
  891. chrys/service/context/providers/memory.py +50 -0
  892. chrys/service/context/tool_content.py +22 -0
  893. chrys/service/hooks/__init__.py +37 -0
  894. chrys/service/hooks/detached_worker.py +118 -0
  895. chrys/service/hooks/docs/README.md +14 -0
  896. chrys/service/hooks/docs/authoring.md +684 -0
  897. chrys/service/hooks/docs/configuration.md +499 -0
  898. chrys/service/hooks/docs/design.md +359 -0
  899. chrys/service/hooks/docs/project-hooks.md +348 -0
  900. chrys/service/hooks/events.py +157 -0
  901. chrys/service/hooks/loader.py +665 -0
  902. chrys/service/hooks/manager.py +807 -0
  903. chrys/service/hooks/matcher.py +111 -0
  904. chrys/service/hooks/outbox.py +294 -0
  905. chrys/service/hooks/runner.py +572 -0
  906. chrys/service/hooks/schema.py +407 -0
  907. chrys/service/llm/__init__.py +7 -0
  908. chrys/service/llm/_structured_outputs.py +192 -0
  909. chrys/service/llm/anthropic_messages/__init__.py +9 -0
  910. chrys/service/llm/anthropic_messages/client.py +254 -0
  911. chrys/service/llm/anthropic_messages/decode.py +331 -0
  912. chrys/service/llm/anthropic_messages/history.py +279 -0
  913. chrys/service/llm/anthropic_messages/request.py +323 -0
  914. chrys/service/llm/anthropic_messages/server_tools.py +319 -0
  915. chrys/service/llm/anthropic_messages/stream.py +297 -0
  916. chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
  917. chrys/service/llm/chat_completions/__init__.py +9 -0
  918. chrys/service/llm/chat_completions/client.py +369 -0
  919. chrys/service/llm/chat_completions/decode.py +338 -0
  920. chrys/service/llm/chat_completions/history.py +657 -0
  921. chrys/service/llm/chat_completions/reasoning.py +244 -0
  922. chrys/service/llm/chat_completions/request.py +192 -0
  923. chrys/service/llm/chat_completions/stream.py +546 -0
  924. chrys/service/llm/chat_completions/validation.py +308 -0
  925. chrys/service/llm/clients.py +613 -0
  926. chrys/service/llm/images.py +65 -0
  927. chrys/service/llm/mock.py +413 -0
  928. chrys/service/llm/observer.py +446 -0
  929. chrys/service/llm/one_shot.py +106 -0
  930. chrys/service/llm/openai_exceptions.py +93 -0
  931. chrys/service/llm/openai_responses/__init__.py +10 -0
  932. chrys/service/llm/openai_responses/client.py +323 -0
  933. chrys/service/llm/openai_responses/decode.py +432 -0
  934. chrys/service/llm/openai_responses/history.py +519 -0
  935. chrys/service/llm/openai_responses/hosted.py +625 -0
  936. chrys/service/llm/openai_responses/replay.py +629 -0
  937. chrys/service/llm/openai_responses/request.py +342 -0
  938. chrys/service/llm/openai_responses/stream.py +847 -0
  939. chrys/service/llm/openai_timestamps.py +57 -0
  940. chrys/service/llm/providers.py +107 -0
  941. chrys/service/llm/proxy_route.py +54 -0
  942. chrys/service/llm/raw_http_log.py +309 -0
  943. chrys/service/llm/route_facts.py +63 -0
  944. chrys/service/llm/route_sessions.py +49 -0
  945. chrys/service/llm/sdk_anthropic.py +18 -0
  946. chrys/service/llm/sdk_headers.py +47 -0
  947. chrys/service/llm/sdk_openai.py +18 -0
  948. chrys/service/llm/sdk_retry.py +59 -0
  949. chrys/service/llm/wire_client.py +246 -0
  950. chrys/service/mcp/__init__.py +3 -0
  951. chrys/service/mcp/_connection.py +235 -0
  952. chrys/service/mcp/_http_transport.py +350 -0
  953. chrys/service/mcp/_progressive.py +459 -0
  954. chrys/service/mcp/_stdio_transport.py +614 -0
  955. chrys/service/mcp/_tool_mixins.py +96 -0
  956. chrys/service/mcp/adapter.py +1031 -0
  957. chrys/service/mcp/cache.py +476 -0
  958. chrys/service/mcp/content.py +32 -0
  959. chrys/service/mcp/errors.py +169 -0
  960. chrys/service/mcp/owned.py +1330 -0
  961. chrys/service/mcp/result_limits.py +168 -0
  962. chrys/service/mcp/thinking_warning.py +95 -0
  963. chrys/service/mcp/validation.py +74 -0
  964. chrys/service/mutations/__init__.py +84 -0
  965. chrys/service/mutations/coordination.py +894 -0
  966. chrys/service/mutations/detector.py +458 -0
  967. chrys/service/mutations/git_calibrator.py +397 -0
  968. chrys/service/mutations/git_state.py +647 -0
  969. chrys/service/mutations/pipeline.py +779 -0
  970. chrys/service/mutations/scanner.py +466 -0
  971. chrys/service/mutations/snapshot_files.py +233 -0
  972. chrys/service/mutations/store.py +625 -0
  973. chrys/service/mutations/tool_names.py +15 -0
  974. chrys/service/mutations/trace.py +246 -0
  975. chrys/service/mutations/tracker.py +1289 -0
  976. chrys/service/mutations/types.py +559 -0
  977. chrys/service/mutations/workspace_changes.py +1583 -0
  978. chrys/service/profiles/__init__.py +2 -0
  979. chrys/service/profiles/agents/__init__.py +3 -0
  980. chrys/service/profiles/agents/builtins/Code.yaml +199 -0
  981. chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
  982. chrys/service/profiles/agents/builtins/General.yaml +114 -0
  983. chrys/service/profiles/agents/builtins/QA.yaml +128 -0
  984. chrys/service/profiles/agents/loader.py +808 -0
  985. chrys/service/profiles/agents/registry.py +727 -0
  986. chrys/service/profiles/agents/schema.py +371 -0
  987. chrys/service/profiles/agents/serializer.py +196 -0
  988. chrys/service/profiles/coercion.py +22 -0
  989. chrys/service/profiles/models/__init__.py +3 -0
  990. chrys/service/profiles/models/env_bridge.py +157 -0
  991. chrys/service/profiles/models/loader.py +282 -0
  992. chrys/service/profiles/models/options.py +438 -0
  993. chrys/service/profiles/models/registry.py +104 -0
  994. chrys/service/profiles/models/resolver.py +286 -0
  995. chrys/service/profiles/models/schema.py +91 -0
  996. chrys/service/profiles/models/serializer.py +76 -0
  997. chrys/service/session/__init__.py +3 -0
  998. chrys/service/session/checkpoint.py +156 -0
  999. chrys/service/session/history.py +1616 -0
  1000. chrys/service/session/message_metadata.py +151 -0
  1001. chrys/service/session/persistence.py +435 -0
  1002. chrys/service/session/runtime_metadata.py +210 -0
  1003. chrys/service/session/sub_agent_logs.py +1069 -0
  1004. chrys/service/session/sub_agent_transcript.py +654 -0
  1005. chrys/service/skills/__init__.py +3 -0
  1006. chrys/service/skills/adapter.py +408 -0
  1007. chrys/service/skills/constants.py +8 -0
  1008. chrys/service/skills/loader.py +575 -0
  1009. chrys/service/skills/model.py +130 -0
  1010. chrys/service/skills/provider.py +841 -0
  1011. chrys/service/skills/runner.py +390 -0
  1012. chrys/service/state/__init__.py +3 -0
  1013. chrys/service/state/_fork.py +710 -0
  1014. chrys/service/state/_session_files.py +175 -0
  1015. chrys/service/state/_session_meta.py +552 -0
  1016. chrys/service/state/locks.py +154 -0
  1017. chrys/service/state/serializers.py +156 -0
  1018. chrys/service/state/session_catalog.py +361 -0
  1019. chrys/service/state/session_listing.py +84 -0
  1020. chrys/service/state/session_migration.py +446 -0
  1021. chrys/service/state/session_mru.py +315 -0
  1022. chrys/service/state/store.py +2132 -0
  1023. chrys/service/state/workflow.py +158 -0
  1024. chrys/service/text_blocks.py +47 -0
  1025. chrys/service/todos/__init__.py +3 -0
  1026. chrys/service/todos/reminder.py +31 -0
  1027. chrys/service/todos/tracker.py +58 -0
  1028. chrys/service/tools/__init__.py +3 -0
  1029. chrys/service/tools/approval_targets.py +92 -0
  1030. chrys/service/tools/builtins/__init__.py +3 -0
  1031. chrys/service/tools/builtins/ask_user.py +326 -0
  1032. chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
  1033. chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
  1034. chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
  1035. chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
  1036. chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
  1037. chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
  1038. chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
  1039. chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
  1040. chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
  1041. chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
  1042. chrys/service/tools/builtins/doc_converter/registry.py +55 -0
  1043. chrys/service/tools/builtins/filesystem.py +1071 -0
  1044. chrys/service/tools/builtins/search.py +1265 -0
  1045. chrys/service/tools/builtins/shell.py +967 -0
  1046. chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
  1047. chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
  1048. chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
  1049. chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
  1050. chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
  1051. chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
  1052. chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
  1053. chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
  1054. chrys/service/tools/builtins/shell_filter.py +24 -0
  1055. chrys/service/tools/builtins/sleep.py +91 -0
  1056. chrys/service/tools/builtins/todo.py +64 -0
  1057. chrys/service/tools/builtins/web/__init__.py +6 -0
  1058. chrys/service/tools/builtins/web/build.py +268 -0
  1059. chrys/service/tools/builtins/web/config.py +338 -0
  1060. chrys/service/tools/builtins/web/credentials.py +37 -0
  1061. chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
  1062. chrys/service/tools/builtins/web/fetch/cache.py +68 -0
  1063. chrys/service/tools/builtins/web/fetch/convert.py +56 -0
  1064. chrys/service/tools/builtins/web/fetch/tool.py +223 -0
  1065. chrys/service/tools/builtins/web/hosted.py +61 -0
  1066. chrys/service/tools/builtins/web/http.py +330 -0
  1067. chrys/service/tools/builtins/web/progress.py +18 -0
  1068. chrys/service/tools/builtins/web/search/__init__.py +3 -0
  1069. chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
  1070. chrys/service/tools/builtins/web/search/html.py +152 -0
  1071. chrys/service/tools/builtins/web/search/json_api.py +227 -0
  1072. chrys/service/tools/builtins/web/search/providers.py +77 -0
  1073. chrys/service/tools/builtins/web/search/tool.py +218 -0
  1074. chrys/service/tools/builtins/web/search/types.py +28 -0
  1075. chrys/service/tools/builtins/web/tls_identity.py +66 -0
  1076. chrys/service/tools/kinds.py +115 -0
  1077. chrys/service/tools/names.py +47 -0
  1078. chrys/service/tools/registry.py +242 -0
  1079. chrys/service/tools/result_metadata.py +103 -0
  1080. chrys/service/tools/session_artifacts.py +196 -0
  1081. chrys/service/tools/spill.py +218 -0
  1082. chrys/service/tools/workspace_paths.py +81 -0
  1083. chrys/service/trajectory/__init__.py +11 -0
  1084. chrys/service/trajectory/approvals.py +201 -0
  1085. chrys/service/trajectory/compaction.py +420 -0
  1086. chrys/service/trajectory/fork.py +75 -0
  1087. chrys/service/trajectory/hooks.py +217 -0
  1088. chrys/service/trajectory/items.py +41 -0
  1089. chrys/service/trajectory/preparation.py +374 -0
  1090. chrys/service/trajectory/retries.py +95 -0
  1091. chrys/service/trajectory/revisions.py +129 -0
  1092. chrys/service/trajectory/segmented.py +163 -0
  1093. chrys/service/trajectory/session.py +762 -0
  1094. chrys/service/trajectory/state.py +83 -0
  1095. chrys/service/trajectory/sub_agents.py +169 -0
  1096. chrys/service/trajectory/tombstone.py +262 -0
  1097. chrys/service/trajectory/tools.py +323 -0
  1098. chrys/service/trajectory/validation.py +130 -0
  1099. chrys/service/trajectory/waits.py +161 -0
  1100. chrys/service/trajectory/workflow.py +148 -0
  1101. chrys/service/vision.py +128 -0
  1102. chrys/service/workflows/__init__.py +3 -0
  1103. chrys/service/workflows/admission.py +151 -0
  1104. chrys/service/workflows/artifacts.py +109 -0
  1105. chrys/service/workflows/asks.py +130 -0
  1106. chrys/service/workflows/builtins/__init__.py +12 -0
  1107. chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
  1108. chrys/service/workflows/builtins/demo-workflow.py +756 -0
  1109. chrys/service/workflows/discovery.py +554 -0
  1110. chrys/service/workflows/environment.py +284 -0
  1111. chrys/service/workflows/graph.py +430 -0
  1112. chrys/service/workflows/history.py +82 -0
  1113. chrys/service/workflows/interpreter.py +107 -0
  1114. chrys/service/workflows/journal.py +334 -0
  1115. chrys/service/workflows/layout.py +56 -0
  1116. chrys/service/workflows/ledger.py +149 -0
  1117. chrys/service/workflows/model_selection.py +18 -0
  1118. chrys/service/workflows/orphans.py +209 -0
  1119. chrys/service/workflows/outcomes.py +46 -0
  1120. chrys/service/workflows/protocol.py +210 -0
  1121. chrys/service/workflows/records.py +142 -0
  1122. chrys/service/workflows/scheduler.py +770 -0
  1123. chrys/service/workflows/sdk/__init__.py +51 -0
  1124. chrys/service/workflows/sdk/_ask.py +167 -0
  1125. chrys/service/workflows/sdk/_builder.py +781 -0
  1126. chrys/service/workflows/sdk/_values.py +92 -0
  1127. chrys/service/workflows/sdk_artifact.py +74 -0
  1128. chrys/service/workflows/store.py +494 -0
  1129. chrys/service/workflows/transcript.py +102 -0
  1130. chrys/service/workflows/values.py +73 -0
  1131. chrys/service/workflows/worker_host.py +1820 -0
  1132. chrys/workflows.py +38 -0
  1133. icode_tui-0.29.1.dist-info/METADATA +104 -0
  1134. icode_tui-0.29.1.dist-info/RECORD +1138 -0
  1135. icode_tui-0.29.1.dist-info/WHEEL +4 -0
  1136. icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
  1137. icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
  1138. icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
@@ -0,0 +1,1116 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Turn-aware four-phase compaction strategy.
4
+
5
+ Progressive compaction with escalating aggressiveness:
6
+
7
+ **Phase 1** — Old turn tool results are summarised oldest-turn-first (keeping
8
+ call arguments, replacing results with one-line summaries).
9
+
10
+ **Phase 2** — Old turn tool-call groups are removed entirely (args + results),
11
+ oldest-turn-first. Only the user/assistant text messages survive per turn.
12
+
13
+ **Phase 3** — Completed text-heavy turns are compressed oldest-turn-first
14
+ when Phase 1/2 cannot reach the request budget because there are no old tool
15
+ groups left to trim.
16
+
17
+ **Phase 4** — All current-turn tool-call and inline assistant-text messages
18
+ are dropped in a single shot and replaced by a ``<system-reminder>[LAST_WORDS]
19
+ …</system-reminder>`` block appended to the user message's content array by
20
+ ``SystemReminderMiddleware``. The note is produced by
21
+ :class:`chrys.service.context.compaction.last_words.LastWordsGenerator` (always LLM-generated —
22
+ no programmatic fallback; transient generator failures retry the same
23
+ compaction input with backoff). On subsequent Phase 4 triggers
24
+ within the same turn the previous note + newly-dropped work are fed back
25
+ to the generator so the note accumulates insight without re-summarising
26
+ from scratch.
27
+
28
+ Turn boundaries come from ``_resolve_turns`` (built on
29
+ ``chrys.foundation.models.turns``): only real openers — user messages
30
+ without a mid-turn flag — split turns, so injections and synthetic
31
+ ``continue`` nudges stay inside the turn they arrived in, and the marker
32
+ region of the provider state decides degraded (opener-less) shapes.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import asyncio
38
+ import logging
39
+ import uuid
40
+ from dataclasses import dataclass
41
+ from pathlib import Path
42
+ from typing import TYPE_CHECKING, Any, Protocol
43
+
44
+ from chrys.foundation.errors import context_overflow_limit, iter_explicit_graph
45
+ from chrys.foundation.text.tokenizer import MixedLanguageTokenizer
46
+ from chrys.foundation.trajectory.context import current_trajectory
47
+ from chrys.foundation.trajectory.event_types import CompactionSkipReason
48
+ from chrys.kernel import (
49
+ Message,
50
+ annotate_message_groups,
51
+ annotate_token_counts,
52
+ included_token_count,
53
+ )
54
+ from chrys.service.profiles.models.schema import DEFAULT_MAX_CONTEXT_TOKENS
55
+ from chrys.service.trajectory.compaction import (
56
+ TOKEN_MEASUREMENT_SOURCE,
57
+ CompactionRunTrace,
58
+ bind_compaction_operation,
59
+ record_compaction_skipped,
60
+ reset_compaction_operation,
61
+ )
62
+
63
+ from .breaker import DropBreakerController
64
+ from .calibration import TokenCalibration
65
+ from .compression import CompressionEngine
66
+ from .current_turn_drop import CurrentTurnDropRound
67
+ from .events import (
68
+ CachedCompressedContextSummary,
69
+ CachedSummary,
70
+ CompactionInfo,
71
+ CompressInfo,
72
+ PendingCompression,
73
+ PreCompactInfo,
74
+ )
75
+ from .exclusions import ExclusionLedger, _ExclusionAnchor
76
+ from .groups import (
77
+ _any_tool_groups_in_range,
78
+ _dedup_message_ids,
79
+ _group_kind_map,
80
+ _group_messages_by_id,
81
+ _group_start_indices,
82
+ _ordered_group_ids,
83
+ _tool_groups_in_range,
84
+ )
85
+ from .last_words import LastWordsGenerationError
86
+ from .last_words_state import DropRoundBreakerState, LastWordsState, Phase4RetrySnapshot
87
+ from .scoped import ScopedGroup
88
+ from .spill import SpillQuota
89
+ from .summaries import _compact_group, _remove_group
90
+ from .turns import _resolve_turns
91
+
92
+ _log = logging.getLogger(__name__)
93
+
94
+ if TYPE_CHECKING:
95
+ from collections.abc import Awaitable, Callable
96
+
97
+ from chrys.kernel import CompactionCallContext, LastWordsCompleter, TokenizerProtocol
98
+
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # Strategy
102
+ # ---------------------------------------------------------------------------
103
+
104
+
105
+ class LastWordsGeneratorLike(Protocol):
106
+ """Strategy-facing contract of ``LastWordsGenerator`` (mirrored by test stubs)."""
107
+
108
+ async def generate(
109
+ self,
110
+ scoped_groups: list[ScopedGroup],
111
+ previous_last_words: str | None,
112
+ *,
113
+ degraded_opener: bool,
114
+ has_continuation_nudges: bool,
115
+ completer: LastWordsCompleter | None = None,
116
+ tokenizer: TokenizerProtocol | None = None,
117
+ system_overhead_tokens: int = 0,
118
+ tool_definition_tokens: int = 0,
119
+ request_overhead_tokens: int = 0,
120
+ calibration_ratio: float = 1.0,
121
+ spend_side_call_tokens: Callable[[int], bool] | None = None,
122
+ ) -> str: ...
123
+
124
+ async def publish_breaker_trip(self, failure_reason: str) -> None: ...
125
+
126
+ async def publish_committed(self) -> None: ...
127
+
128
+
129
+ class ReminderMiddlewareLike(Protocol):
130
+ """Strategy-facing contract of ``SystemReminderMiddleware`` (mirrored by test stubs)."""
131
+
132
+ def renders_last_words(self, state: object) -> bool: ...
133
+
134
+ def refresh_last_words_reminder(self, messages: list[Any]) -> int | None: ...
135
+
136
+ def restore_folded_reminders(self, messages: list[Message]) -> int | None: ...
137
+
138
+
139
+ @dataclass(frozen=True, slots=True)
140
+ class CompactionRetrySnapshot:
141
+ """Attempt rollback state split by compaction lifetime.
142
+
143
+ Cross-turn folds published after ``published_fold_count`` are monotonic
144
+ commits and get silently replayed onto restored history. Phase-4 note
145
+ content is attempt-local and rolls back. Breaker/pressure accounting is
146
+ intentionally owned by the LAST_WORDS state's logical-turn slot and is
147
+ not represented here.
148
+ """
149
+
150
+ anchors: tuple[_ExclusionAnchor, ...]
151
+ published_fold_count: int
152
+ phase4: Phase4RetrySnapshot | None
153
+
154
+
155
+ class UnifiedContextStrategy:
156
+ """Unified strategy for intra-turn compaction AND cross-turn compression.
157
+
158
+ Owns **all** context reduction during a tool loop. Compression requests
159
+ from the ``compress_context`` tool are queued via :meth:`queue_compression`
160
+ and processed inside :meth:`__call__` on FunctionInvocationLayer's message list —
161
+ guaranteeing that ``_excluded`` flags are set on shared objects and
162
+ ``project_included_messages`` filters them out on the next API call.
163
+
164
+ **Compaction** (4-phase, budget-triggered):
165
+
166
+ 1. Estimate context usage as ``(included_tokens * ratio) / max_context``.
167
+ 2. If usage < ``trigger_pct``, no-op, unless the provider has reported the
168
+ context window full since the last completed pass
169
+ (:meth:`note_context_overflow`). Past the trigger, a call that cannot
170
+ start a pass (compaction disabled, no resolvable turn) logs and records
171
+ ``compaction.skipped`` once per stretch above the trigger.
172
+ 3. **Phase 1-4**: trim old tools, compress old text turns, then drop
173
+ current-turn tool work only as the final fallback.
174
+
175
+ **Compression** (agent-requested via ``compress_context`` tool):
176
+
177
+ Queued by :meth:`queue_compression`, processed before the compaction
178
+ phases. Sets ``_excluded`` on fold-range messages and replaces
179
+ ``state["messages"]`` for persistence.
180
+
181
+ Args:
182
+ max_context_tokens: Maximum context window size (e.g. 200_000).
183
+ trigger_pct: Usage fraction that triggers compaction (e.g. 0.85).
184
+ target_pct: Usage fraction to compact down to (e.g. 0.50).
185
+ tokenizer: Token estimator. Defaults to ``MixedLanguageTokenizer``.
186
+ on_compaction: Async callback fired after compaction phases.
187
+ on_compress: Async callback fired after compression.
188
+ compaction_enabled: When ``False``, skip budget-triggered compaction
189
+ but still process queued compressions.
190
+ """
191
+
192
+ def __init__(
193
+ self,
194
+ *,
195
+ max_context_tokens: int = DEFAULT_MAX_CONTEXT_TOKENS,
196
+ trigger_pct: float = 0.85,
197
+ target_pct: float = 0.50,
198
+ tokenizer: TokenizerProtocol | None = None,
199
+ on_compaction: Callable[[CompactionInfo], Awaitable[None]] | None = None,
200
+ on_pre_compact: Callable[[PreCompactInfo], Awaitable[None]] | None = None,
201
+ on_compress: Callable[[CompressInfo], Awaitable[None]] | None = None,
202
+ on_context_pressure: Callable[[str, DropRoundBreakerState, int], Awaitable[None] | None] | None = None,
203
+ spill_quota: SpillQuota | None = None,
204
+ spill_root: Path | None = None,
205
+ spill_record_dir: Path | None = None,
206
+ spill_session_id: str = "",
207
+ debug_log_dir: Path | None = None,
208
+ phase4_side_call_token_budget: int | None = -1,
209
+ compaction_enabled: bool = True,
210
+ ) -> None:
211
+ if max_context_tokens <= 0:
212
+ raise ValueError("max_context_tokens must be positive")
213
+ if compaction_enabled:
214
+ if not 0.0 < trigger_pct <= 1.0:
215
+ raise ValueError("trigger_pct must be in (0.0, 1.0]")
216
+ if not 0.0 < target_pct < trigger_pct:
217
+ raise ValueError("target_pct must be in (0.0, trigger_pct)")
218
+ self.max_context_tokens = max_context_tokens
219
+ self.trigger_pct = trigger_pct
220
+ self.target_pct = target_pct
221
+ self._compaction_enabled = compaction_enabled
222
+ self._tokenizer: TokenizerProtocol = tokenizer or MixedLanguageTokenizer()
223
+ self._on_compaction = on_compaction
224
+ self._on_pre_compact = on_pre_compact
225
+ self._on_compress = on_compress
226
+ # The trajectory run of the compaction pass in flight (one per
227
+ # triggered ``__call__``); phases report into it.
228
+ self._trajectory_run: CompactionRunTrace | None = None
229
+ # The skip (reason, session) already reported for the current
230
+ # stretch above the trigger; cleared when usage drops below it or a
231
+ # pass starts.
232
+ self._reported_skip: tuple[str, str | None] | None = None
233
+ # Set when the provider rejected a request for overflowing the context
234
+ # window; forces the next pass whatever the estimate says and clears
235
+ # only when one completes. Per instance on purpose: retry rollback
236
+ # keeps it, and it is never persisted.
237
+ self._context_overflow_pending = False
238
+ self._on_context_pressure = on_context_pressure
239
+ self._spill_quota = spill_quota
240
+ self._spill_root = spill_root
241
+ self._spill_record_dir = spill_record_dir or Path("compactions") / "dropped"
242
+ self._spill_session_id = spill_session_id
243
+ self._debug_log_dir = debug_log_dir
244
+ # Negative (or None, kept for call-site compatibility) = unlimited:
245
+ # the per-turn round limit is the guard, not a spend cap.
246
+ self._phase4_side_call_token_budget = (
247
+ -1 if phase4_side_call_token_budget is None else phase4_side_call_token_budget
248
+ )
249
+ self._last_included_tokens: int = 0
250
+ # Phase 4 LAST_WORDS collaborators (wired post-construction by
251
+ # AgentBuilder so ContextManager can keep a simple signature).
252
+ self._reminder_middleware: ReminderMiddlewareLike | None = None
253
+ self._last_words_state: LastWordsState | None = None
254
+ self._last_words_generator: LastWordsGeneratorLike | None = None
255
+ self._persist_recovery_now: Callable[[], Awaitable[bool]] | None = None
256
+ # Detached on_compaction deliveries scheduled when cancellation
257
+ # interrupts the committed publish — anchored here because asyncio
258
+ # holds only weak references to tasks.
259
+ self._detached_deliveries: set[asyncio.Future] = set()
260
+ self._calibration = TokenCalibration(self)
261
+ self._exclusions = ExclusionLedger()
262
+ self._compression = CompressionEngine(self, self._exclusions)
263
+ self._breaker = DropBreakerController(self)
264
+ self._current_turn_drop = CurrentTurnDropRound(self, self._breaker, self._exclusions)
265
+
266
+ @property
267
+ def _calibration_ratio(self) -> float:
268
+ return self._calibration._calibration_ratio
269
+
270
+ @_calibration_ratio.setter
271
+ def _calibration_ratio(self, value: float) -> None:
272
+ self._calibration._calibration_ratio = value
273
+
274
+ @property
275
+ def _overhead_initialized(self) -> bool:
276
+ return self._calibration._overhead_initialized
277
+
278
+ @_overhead_initialized.setter
279
+ def _overhead_initialized(self, value: bool) -> None:
280
+ self._calibration._overhead_initialized = value
281
+
282
+ @property
283
+ def _system_overhead(self) -> int:
284
+ return self._calibration._system_overhead
285
+
286
+ @_system_overhead.setter
287
+ def _system_overhead(self, value: int) -> None:
288
+ self._calibration._system_overhead = value
289
+
290
+ @property
291
+ def _request_overhead_floor(self) -> int:
292
+ return self._calibration._request_overhead_floor
293
+
294
+ @_request_overhead_floor.setter
295
+ def _request_overhead_floor(self, value: int) -> None:
296
+ self._calibration._request_overhead_floor = value
297
+
298
+ @property
299
+ def _summary_cache(self) -> dict[str, CachedSummary]:
300
+ return self._exclusions._summary_cache
301
+
302
+ @_summary_cache.setter
303
+ def _summary_cache(self, value: dict[str, CachedSummary]) -> None:
304
+ self._exclusions._summary_cache = value
305
+
306
+ @property
307
+ def _removed_group_ids(self) -> set[str]:
308
+ return self._exclusions._removed_group_ids
309
+
310
+ @_removed_group_ids.setter
311
+ def _removed_group_ids(self, value: set[str]) -> None:
312
+ self._exclusions._removed_group_ids = value
313
+
314
+ @property
315
+ def _excluded_anchors(self) -> list[_ExclusionAnchor]:
316
+ return self._exclusions._excluded_anchors
317
+
318
+ @_excluded_anchors.setter
319
+ def _excluded_anchors(self, value: list[_ExclusionAnchor]) -> None:
320
+ self._exclusions._excluded_anchors = value
321
+
322
+ @property
323
+ def _state(self) -> dict[str, Any] | None:
324
+ return self._compression._state
325
+
326
+ @_state.setter
327
+ def _state(self, value: dict[str, Any] | None) -> None:
328
+ self._compression._state = value
329
+
330
+ @property
331
+ def _pending_compressions(self) -> list[PendingCompression]:
332
+ return self._compression._pending_compressions
333
+
334
+ @_pending_compressions.setter
335
+ def _pending_compressions(self, value: list[PendingCompression]) -> None:
336
+ self._compression._pending_compressions = value
337
+
338
+ @property
339
+ def _compressed_context_cache(self) -> dict[str, CachedCompressedContextSummary]:
340
+ return self._compression._compressed_context_cache
341
+
342
+ @_compressed_context_cache.setter
343
+ def _compressed_context_cache(self, value: dict[str, CachedCompressedContextSummary]) -> None:
344
+ self._compression._compressed_context_cache = value
345
+
346
+ @property
347
+ def _context_pressure_tasks(self) -> set[asyncio.Future[None]]:
348
+ return self._breaker._context_pressure_tasks
349
+
350
+ @_context_pressure_tasks.setter
351
+ def _context_pressure_tasks(self, value: set[asyncio.Future[None]]) -> None:
352
+ self._breaker._context_pressure_tasks = value
353
+
354
+ # ------------------------------------------------------------------
355
+ # Phase 4 wiring (called by AgentBuilder after middleware + generator exist)
356
+ # ------------------------------------------------------------------
357
+
358
+ def bind_reminder(self, middleware: ReminderMiddlewareLike, last_words: LastWordsState) -> None:
359
+ """Bind the ``SystemReminderMiddleware`` and the LAST_WORDS state it renders, together.
360
+
361
+ Phase 4 writes its note to *last_words* and drops history only when
362
+ both are bound, so the next request carries the note in place of the
363
+ dropped history. A state the middleware does not render is refused.
364
+ """
365
+ if not middleware.renders_last_words(last_words):
366
+ raise ValueError("The reminder middleware does not render this LAST_WORDS state.")
367
+ self._reminder_middleware = middleware
368
+ self._last_words_state = last_words
369
+
370
+ def set_last_words_generator(self, generator: LastWordsGeneratorLike) -> None:
371
+ """Bind the ``LastWordsGenerator`` used by Phase 4."""
372
+ self._last_words_generator = generator
373
+
374
+ def set_recovery_persistence_callback(self, callback: Callable[[], Awaitable[bool]]) -> None:
375
+ """Bind the main engine's strict recovery-sidecar durability callback."""
376
+ self._persist_recovery_now = callback
377
+
378
+ def _write_reminder_debug(self) -> None:
379
+ """Dump the LAST_WORDS reminder exactly as the next model call injects it.
380
+
381
+ Owner-only (0600) like the spill records: the rendered reminder
382
+ carries the progress note, which summarises session content.
383
+ """
384
+ if self._debug_log_dir is None or self._last_words_state is None:
385
+ return
386
+ try:
387
+ text = self._last_words_state.render_last_words_reminder_text()
388
+ if not text:
389
+ return
390
+ import os
391
+ from datetime import UTC, datetime
392
+
393
+ self._debug_log_dir.mkdir(parents=True, exist_ok=True)
394
+ ts = datetime.now(UTC).strftime("%H%M%S_%f")[:-3]
395
+ path = self._debug_log_dir / f"reminder_{ts}_{uuid.uuid4().hex[:6]}.log"
396
+ descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
397
+ with os.fdopen(descriptor, "w", encoding="utf-8") as output:
398
+ output.write(text)
399
+ except Exception:
400
+ _log.debug("Failed to write LAST_WORDS reminder debug dump", exc_info=True)
401
+
402
+ def _spend_side_call_tokens(self, estimated_tokens: int) -> bool:
403
+ """Delegate the test-visible side-call charging seam."""
404
+ return self._breaker.spend_side_call_tokens(estimated_tokens)
405
+
406
+ async def _notify_compaction(self, info: CompactionInfo) -> None:
407
+ """Record a finished phase on the trajectory, then deliver the UI callback."""
408
+ await self._notify_compaction_for_run(info, self._trajectory_run)
409
+
410
+ async def _notify_compaction_for_run(self, info: CompactionInfo, run: CompactionRunTrace | None) -> None:
411
+ """As :meth:`_notify_compaction`, against the run *info* was measured under.
412
+
413
+ A detached delivery outlives the pass that scheduled it, so reading
414
+ the live handle at delivery time would hang the phase off whatever
415
+ run is current by then — or drop it once the pass has cleared it.
416
+ """
417
+ if run is not None:
418
+ await run.phase_finished(
419
+ phase=info.phase,
420
+ groups_compacted=info.compacted_groups,
421
+ turn_numbers=info.turn_numbers,
422
+ tool_names=info.tool_names,
423
+ tokens_before=info.tokens_before,
424
+ tokens_after=info.tokens_after,
425
+ last_words_generated=info.last_words_generated,
426
+ consumed_item_ids=info.consumed_item_ids if info.phase == "phase4" else None,
427
+ )
428
+ if self._on_compaction is not None:
429
+ await self._on_compaction(info)
430
+
431
+ async def _notify_compress(
432
+ self,
433
+ info: CompressInfo,
434
+ *,
435
+ trigger: str,
436
+ tokens_before: int,
437
+ tokens_after: int,
438
+ token_source: str = TOKEN_MEASUREMENT_SOURCE,
439
+ ) -> None:
440
+ """Record a cross-turn fold on the trajectory, then deliver the UI callback.
441
+
442
+ Inside a triggered pass the fold is a phase of that run; a standalone
443
+ fold (agent-requested, forced after a run) is a run of its own.
444
+ """
445
+ run = self._trajectory_run
446
+ turn_numbers = list(range(info.turn_range[0], info.turn_range[1] + 1)) if info.turn_range != (0, 0) else []
447
+ if run is not None:
448
+ await run.phase_finished(
449
+ phase="phase3",
450
+ groups_compacted=1,
451
+ turn_numbers=turn_numbers,
452
+ tool_names=[],
453
+ tokens_before=tokens_before,
454
+ tokens_after=tokens_after,
455
+ messages_freed=info.freed_messages,
456
+ compressed_context_id=info.compressed_context_id,
457
+ )
458
+ else:
459
+ await CompactionRunTrace.record_compression(
460
+ trigger=trigger,
461
+ compressed_context_id=info.compressed_context_id,
462
+ messages_freed=info.freed_messages,
463
+ tokens_before=tokens_before,
464
+ tokens_after=tokens_after,
465
+ token_source=token_source,
466
+ )
467
+ if self._on_compress is not None:
468
+ await self._on_compress(info)
469
+
470
+ async def _fire_pre_compact(self, trigger: str, *, usage_pct: float, tokens_before: int) -> None:
471
+ if self._on_pre_compact is None:
472
+ return
473
+ await self._on_pre_compact(
474
+ PreCompactInfo(
475
+ trigger=trigger,
476
+ usage_pct=usage_pct,
477
+ tokens_before=tokens_before,
478
+ trajectory_operation_id=(self._trajectory_run.run_id if self._trajectory_run is not None else None),
479
+ )
480
+ )
481
+
482
+ # ------------------------------------------------------------------
483
+ # State binding (called by CompressibleHistoryProvider.before_run)
484
+ # ------------------------------------------------------------------
485
+
486
+ def bind_state(self, state: dict[str, Any]) -> None:
487
+ """Bind to the history provider's state dict for this agent.run() cycle."""
488
+ self._compression.bind_state(state)
489
+
490
+ # ------------------------------------------------------------------
491
+ # Cross-turn compression API
492
+ # ------------------------------------------------------------------
493
+
494
+ def queue_compression(self, marker_id: str, summary_text: str) -> tuple[str, int]:
495
+ """Queue a compression request. Returns ``(compressed_context_id, freed_count)``.
496
+
497
+ Validates the marker exists in state (raises ``ValueError`` if not).
498
+ The actual compression is executed inside :meth:`__call__`.
499
+ """
500
+ return self._compression.queue_compression(marker_id, summary_text)
501
+
502
+ def list_compressed(self) -> dict[str, Any]:
503
+ """Return compressed blocks and available markers, accounting for pending compressions."""
504
+ return self._compression.list_compressed()
505
+
506
+ async def force_compress(
507
+ self,
508
+ marker_id: str,
509
+ summary: str,
510
+ source: str = "auto",
511
+ *,
512
+ usage_pct: float = 0.0,
513
+ tokens_before: int = 0,
514
+ ) -> str | None:
515
+ """Execute compression immediately (for force-compress in after_run).
516
+
517
+ No tool loop is active, so ``_excluded`` flags are not needed.
518
+ """
519
+ return await self._compression.force_compress(
520
+ marker_id,
521
+ summary,
522
+ source,
523
+ usage_pct=usage_pct,
524
+ tokens_before=tokens_before,
525
+ )
526
+
527
+ # ------------------------------------------------------------------
528
+ # After-run persistence (called by CompressibleHistoryProvider.after_run)
529
+ # ------------------------------------------------------------------
530
+
531
+ def persist_exclusions_to_state(self, state_messages: list[Message]) -> None:
532
+ """Propagate tool-loop exclusion flags to state messages.
533
+
534
+ History messages reach the wire as per-call views sharing the
535
+ state original's ``additional_properties`` dict and content
536
+ objects, so a flag set on the wire copy is already on the stored
537
+ message and the anchor's shared-content-ids tier re-sets it
538
+ idempotently. The load-bearing case is streaming: finalizers
539
+ rebuild current-turn response messages from updates before they
540
+ are stored, so their flags only reach state through the
541
+ structural anchors.
542
+
543
+ Anchors are single-use: one snapshot pass, one persist pass. The
544
+ list is cleared after consumption because under-trigger runs return
545
+ before ``_snapshot_excluded_anchors`` — without the clear, a stale
546
+ list replays against ever-longer state on every subsequent
547
+ after_run, and the newest-first structural fallback would bind a
548
+ later structural twin (a repeated identical tool call or
549
+ byte-equal assistant text) and silently exclude a live message.
550
+ The anchors deliberately survive a run whose after_run never fired
551
+ (interrupt before storage): the next after_run is their one chance
552
+ to reach the stored twins.
553
+ """
554
+ self._exclusions.persist_to_state(state_messages)
555
+
556
+ def snapshot_retry_state(self) -> CompactionRetrySnapshot:
557
+ """Capture compaction state at retry-attempt entry.
558
+
559
+ A Phase-4 commit inside the attempt records anchors describing the
560
+ current-turn messages it dropped. When a transient error rolls the
561
+ attempt back, history is restored to the pre-attempt snapshot and
562
+ those messages never reach state — but the anchors would survive,
563
+ and the successful retry can emit identical calls/text, so the
564
+ after_run persist's newest-first structural fallback would bind the
565
+ retry's fresh live messages and silently exclude them. The retry
566
+ loop snapshots this alongside history and restores both together.
567
+ Published cross-turn folds are different: Phase 3 only folds completed
568
+ turns, so a transient provider failure must preserve them across the
569
+ restored pre-input history baseline. The journal boundary lets
570
+ restore replay only folds committed by this outer attempt.
571
+
572
+ LAST_WORDS note/todo/manifest content describes current-turn messages
573
+ dropped by Phase 4 and therefore rolls back with those messages.
574
+ Breaker and pressure accounting remain monotonic per logical turn.
575
+ """
576
+ phase4 = self._last_words_state.snapshot_phase4_retry_state() if self._last_words_state is not None else None
577
+ return CompactionRetrySnapshot(
578
+ anchors=self._exclusions.snapshot_retry_state(),
579
+ published_fold_count=self._compression.published_fold_count(),
580
+ phase4=phase4,
581
+ )
582
+
583
+ def restore_retry_state(self, snapshot: CompactionRetrySnapshot) -> None:
584
+ """Restore the anchors captured by :meth:`snapshot_retry_state`.
585
+
586
+ Discards anchors created by the rolled-back attempt while keeping
587
+ ones that predate it (anchors surviving an interrupted prior run
588
+ still get their one after_run persist). Phase-4 content is restored
589
+ before published completed-turn folds are silently replayed onto the
590
+ executor-restored provider state.
591
+ """
592
+ self._exclusions.restore_retry_state(snapshot.anchors)
593
+ if self._last_words_state is not None and snapshot.phase4 is not None:
594
+ self._last_words_state.restore_phase4_retry_state(snapshot.phase4)
595
+ self._compression.replay_published_folds(snapshot.published_fold_count)
596
+ if self._state is not None:
597
+ self._exclusions.discard_unreferenced_summaries(self._state.get("messages", []))
598
+
599
+ async def flush_pending_compressions(self) -> bool:
600
+ """Process pending cross-turn compressions on bound state.
601
+
602
+ Called by ``after_run`` when the LLM called ``compress_context``
603
+ right before a successful run ended, and by the next executor cycle
604
+ before its retry snapshot when a failed run left that request
605
+ queued. Unlike ``__call__``'s compression path, this does not set
606
+ ``_excluded`` on a framework message list; subsequent provider input
607
+ is built from the already-folded state.
608
+ """
609
+ return await self._compression.flush_pending_compressions()
610
+
611
+ @property
612
+ def last_included_tokens(self) -> int:
613
+ """Estimated included token count from the last ``__call__``.
614
+
615
+ Uses the same ``MixedLanguageTokenizer`` as the threshold check,
616
+ so callers can use it instead of the API's ``input_token_count``
617
+ to avoid calibration mismatch.
618
+ """
619
+ return self._last_included_tokens
620
+
621
+ def calibrate(self, api_input_tokens: int, *, local_included: int = 0) -> None:
622
+ """Update system overhead and calibration ratio from an API response.
623
+
624
+ Uses an **additive + multiplicative** model::
625
+
626
+ estimated_api_tokens = (local + system_overhead) * ratio
627
+
628
+ * ``system_overhead`` (fixed) captures the system prompt + tool
629
+ definitions — computed once on the first API response and kept
630
+ constant for the session.
631
+ * ``ratio`` (updated each call) captures only tokenizer drift —
632
+ should stay close to 1.0.
633
+
634
+ On the first call the overhead is learned. When the heuristic
635
+ overestimates enough that ``local > api``, overhead is clamped to
636
+ zero and the ratio is calibrated immediately to absorb the full gap.
637
+
638
+ Args:
639
+ api_input_tokens: The API's ``input_token_count``.
640
+ local_included: Optional override for the local baseline.
641
+ """
642
+ local = local_included if local_included > 0 else self._last_included_tokens
643
+ self._calibration.calibrate(api_input_tokens, local_included=local)
644
+
645
+ @property
646
+ def calibration_ratio(self) -> float:
647
+ """Tokenizer accuracy ratio (should be close to 1.0).
648
+
649
+ > 1.0 = local tokenizer underestimates.
650
+ < 1.0 = local tokenizer overestimates.
651
+ Updated by :meth:`calibrate` after each API response.
652
+ """
653
+ return self._calibration._calibration_ratio
654
+
655
+ @property
656
+ def calibration_initialized(self) -> bool:
657
+ """Whether a provider response initialized calibration state."""
658
+ return self._calibration._overhead_initialized
659
+
660
+ def restore_calibration(self, system_overhead_tokens: object, calibration_ratio: object) -> bool:
661
+ """Restore validated calibration values from provenance-gated state."""
662
+ return self._calibration.restore(system_overhead_tokens, calibration_ratio)
663
+
664
+ @property
665
+ def tokenizer(self) -> TokenizerProtocol:
666
+ """Tokenizer shared by compaction and per-call budget estimation."""
667
+ return self._tokenizer
668
+
669
+ @property
670
+ def system_overhead_tokens(self) -> int:
671
+ """Estimated fixed token overhead (system prompt + tool definitions).
672
+
673
+ Computed as ``api_input_tokens - local_conversation_tokens`` after
674
+ each API response. The true overhead is constant within a session;
675
+ small fluctuations reflect tokenizer inaccuracy on conversation content.
676
+ """
677
+ return self._calibration._system_overhead
678
+
679
+ @property
680
+ def estimated_context_input_tokens(self) -> int:
681
+ """Estimated input occupancy for the most recently prepared request.
682
+
683
+ This uses the same request-overhead floor and tokenizer calibration as
684
+ admission and compaction checks, so it remains usable when a provider's
685
+ reported input usage includes hidden hosted-tool execution.
686
+ """
687
+ return self._calibration.estimated_tokens(self._last_included_tokens)
688
+
689
+ def _usage_pct(self, included: int) -> float:
690
+ """Compute effective context usage as a fraction of max_context_tokens."""
691
+ return self._calibration.usage_pct(included)
692
+
693
+ def note_context_overflow(self, exc: BaseException | None = None) -> bool:
694
+ """Make the next pass run even below the trigger: the provider found the window full.
695
+
696
+ Not for a failure of compaction's own last-words call (owner-terminal;
697
+ its fallback text names the context window), nor while compaction is
698
+ off, which would only record a misleading skip. Returns whether to
699
+ compact and resend the same request: not when the provider names a
700
+ smaller window than this one, since requests sized for this window
701
+ keep overflowing until the user sets the right one; the failure then
702
+ tells the user which value to set.
703
+ """
704
+ if exc is not None and any(isinstance(node, LastWordsGenerationError) for node in iter_explicit_graph(exc)):
705
+ return False
706
+ if not self._compaction_enabled:
707
+ return False
708
+ self._context_overflow_pending = True
709
+ limit = context_overflow_limit(exc) if exc is not None else None
710
+ return limit is None or limit >= self.max_context_tokens
711
+
712
+ async def __call__(self, messages: list[Message], context: CompactionCallContext | None = None) -> bool:
713
+ # max() folds the retained legacy ``tool_definition_tokens`` spelling:
714
+ # external constructors of CompactionCallContext may populate only it.
715
+ self._request_overhead_floor = (
716
+ max(context.request_overhead_tokens, context.tool_definition_tokens) if context is not None else 0
717
+ )
718
+ compressions_processed = await self._run_queued_compressions(messages)
719
+ if compressions_processed:
720
+ refreshed_index = self._restore_folded_reminders(messages)
721
+ if refreshed_index is not None:
722
+ annotate_token_counts(messages, tokenizer=self._tokenizer, from_index=refreshed_index)
723
+ self._reset_stale_exclusions(messages)
724
+ self._reinject_compressed_context_summaries(messages)
725
+ current = self._annotate_and_count(messages)
726
+ usage = self._usage_pct(current)
727
+ below_trigger = usage < self.trigger_pct
728
+ overflow_forced = self._context_overflow_pending
729
+
730
+ if below_trigger and not overflow_forced:
731
+ self._reported_skip = None
732
+ return compressions_processed
733
+
734
+ if not self._compaction_enabled:
735
+ self._report_skip(CompactionSkipReason.DISABLED, current)
736
+ return compressions_processed
737
+
738
+ tokens_before = current
739
+ resolved = _resolve_turns(messages, self._state)
740
+
741
+ if not resolved.spans:
742
+ # ``compaction.skipped`` reports usage past the trigger; a pass the
743
+ # overflow forced below it just waits for the next call.
744
+ if below_trigger:
745
+ self._reported_skip = None
746
+ else:
747
+ self._report_skip(CompactionSkipReason.TURNS_UNRESOLVED, current)
748
+ return compressions_processed
749
+ self._reported_skip = None
750
+
751
+ # Only REAL previous turns are eligible for P1/P2 mechanical
752
+ # truncation — mid-turn user messages (injections, nudges) no longer
753
+ # split the current task into fake "previous" turns. The span COUNT
754
+ # is insertion-stable (summaries are assistant messages and never
755
+ # add or remove turns), so iterating by index over re-resolved
756
+ # snapshots is safe.
757
+ previous_count = len(resolved.spans) - 1
758
+
759
+ run = CompactionRunTrace.open(
760
+ next_exchange_operation_id=context.next_exchange_operation_id if context is not None else None
761
+ )
762
+ self._trajectory_run = run
763
+ compaction_token = bind_compaction_operation(run.run_id if run is not None else None)
764
+ try:
765
+ if run is not None:
766
+ await run.started(
767
+ trigger="context_overflow" if overflow_forced else "usage_threshold",
768
+ tokens_before=tokens_before,
769
+ )
770
+ changed = await self._run_phase1(
771
+ messages,
772
+ previous_count=previous_count,
773
+ usage=usage,
774
+ tokens_before=tokens_before,
775
+ )
776
+ changed |= await self._run_phase2(messages)
777
+ changed |= await self._run_phase3(messages)
778
+ changed |= await self._run_phase4(messages, context)
779
+ except BaseException:
780
+ # Interrupted (or failed) mid-pass: only this path can close the
781
+ # run, and a started run with no terminal marker reads as one that
782
+ # is still going.
783
+ if run is not None:
784
+ run.finished_soon()
785
+ raise
786
+ finally:
787
+ reset_compaction_operation(compaction_token)
788
+ self._trajectory_run = None
789
+ self._finalize_pass(messages, changed, entry_tokens=current)
790
+ self._context_overflow_pending = False
791
+ if run is not None:
792
+ await run.finished(tokens_before=tokens_before, tokens_after=self._last_included_tokens)
793
+ return changed or compressions_processed
794
+
795
+ def _report_skip(self, reason_code: str, included: int) -> None:
796
+ """Say why usage past the trigger started no pass, once per stretch above it."""
797
+ # Per session too: a restore that keeps this strategy starts a
798
+ # session whose trajectory has not been told yet.
799
+ trajectory = current_trajectory()
800
+ skip = (reason_code, trajectory.session_id if trajectory is not None else None)
801
+ if self._reported_skip == skip:
802
+ return
803
+ self._reported_skip = skip
804
+ estimated = self._calibration.estimated_tokens(included)
805
+ # round(): the derived fraction is trigger_tokens / window, and ceil()
806
+ # would turn its float error into one token too many.
807
+ trigger_tokens = round(self.trigger_pct * self.max_context_tokens)
808
+ _log.log(
809
+ logging.INFO if reason_code == CompactionSkipReason.DISABLED else logging.WARNING,
810
+ "Compaction skipped past its trigger: reason=%s, estimated_input_tokens=%d, trigger_tokens=%d, "
811
+ "max_context_tokens=%d",
812
+ reason_code,
813
+ estimated,
814
+ trigger_tokens,
815
+ self.max_context_tokens,
816
+ )
817
+ record_compaction_skipped(
818
+ reason_code=reason_code,
819
+ estimated_input_tokens=estimated,
820
+ trigger_tokens=trigger_tokens,
821
+ max_context_tokens=self.max_context_tokens,
822
+ )
823
+
824
+ async def _run_queued_compressions(self, messages: list[Message]) -> bool:
825
+ return await self._compression.run_queued(messages)
826
+
827
+ def _restore_folded_reminders(self, messages: list[Message]) -> int | None:
828
+ """Re-attach catalog reminders whose carrier a fold just excluded.
829
+
830
+ Returns the rebuilt index; token counts from there on are stale.
831
+ """
832
+ if self._reminder_middleware is None:
833
+ return None
834
+ return self._reminder_middleware.restore_folded_reminders(messages)
835
+
836
+ def _reset_stale_exclusions(self, messages: list[Message]) -> None:
837
+ # ---- Step 1: Handle stale _excluded flags ----
838
+ #
839
+ # ``project_included_messages`` filters messages with _excluded=True,
840
+ # and those flags can outlive a single compaction pass on shared
841
+ # Message objects or restored state.
842
+ #
843
+ # If this strategy has previously compacted groups, re-inject any
844
+ # cached summaries missing from the current list so excluded originals
845
+ # still have a visible replacement. Flags on compacted messages are
846
+ # kept, preventing redundant re-compaction and duplicate events.
847
+ #
848
+ # If this strategy has never compacted (fresh instance or different
849
+ # instance), reset stale foreign _excluded flags while preserving
850
+ # persisted Chrys compaction reasons.
851
+ self._exclusions.reset_stale_exclusions(messages)
852
+
853
+ def _annotate_and_count(self, messages: list[Message]) -> int:
854
+ # De-duplicate message IDs to prevent cross-run group merging
855
+ _dedup_message_ids(messages)
856
+
857
+ # Re-annotate groups with force so group IDs reflect the deduped
858
+ # message IDs.
859
+ annotate_message_groups(messages, force_reannotate=True)
860
+ annotate_token_counts(messages, tokenizer=self._tokenizer)
861
+
862
+ current = included_token_count(messages)
863
+ self._last_included_tokens = current
864
+
865
+ return current
866
+
867
+ def _trim_groups_to_target(
868
+ self,
869
+ messages: list[Message],
870
+ group_ids: list[str],
871
+ grouped: dict[str, list[Message]],
872
+ operation: Callable[[list[Message], str, list[Message]], list[str] | None],
873
+ ) -> tuple[bool, int, list[str]]:
874
+ changed = False
875
+ compacted_count = 0
876
+ tool_names: list[str] = []
877
+ for group_id in group_ids:
878
+ group_msgs = grouped.get(group_id, [])
879
+ result = operation(messages, group_id, group_msgs)
880
+ if result:
881
+ changed = True
882
+ compacted_count += 1
883
+ tool_names.extend(result)
884
+
885
+ if self._usage_pct(included_token_count(messages)) <= self.target_pct:
886
+ break
887
+
888
+ return changed, compacted_count, tool_names
889
+
890
+ async def _run_phase1(
891
+ self,
892
+ messages: list[Message],
893
+ *,
894
+ previous_count: int,
895
+ usage: float,
896
+ tokens_before: int,
897
+ ) -> bool:
898
+ changed = False
899
+
900
+ # ---- Phase 1: Summarise previous turns' tool results ----
901
+ if previous_count:
902
+ await self._fire_pre_compact("phase1", usage_pct=usage, tokens_before=tokens_before)
903
+ p1_count = 0
904
+ p1_tool_names: list[str] = []
905
+ p1_turn_numbers: list[int] = []
906
+
907
+ for turn_idx in range(previous_count):
908
+ # ONE COHERENT SNAPSHOT per turn iteration: spans and all four
909
+ # group projections recomputed together. _compact_group both
910
+ # inserts a summary message and re-annotates the suffix, so
911
+ # projections from different mutation generations mix stale
912
+ # group ids with fresh indices — stale spans silently skip a
913
+ # later turn's shifted tail groups (under-compaction) and
914
+ # mis-bucket them into the wrong turn's event numbers.
915
+ resolved = _resolve_turns(messages, self._state)
916
+ span = resolved.previous[turn_idx]
917
+ ordered_ids = _ordered_group_ids(messages)
918
+ kinds = _group_kind_map(messages)
919
+ grouped = _group_messages_by_id(messages)
920
+ starts = _group_start_indices(messages)
921
+ turn_groups = _tool_groups_in_range(span.start, span.end, ordered_ids, kinds, starts, grouped)
922
+
923
+ turn_compacted, turn_count, turn_tool_names = self._trim_groups_to_target(
924
+ messages,
925
+ turn_groups,
926
+ grouped,
927
+ self._compact_group,
928
+ )
929
+ if turn_compacted:
930
+ changed = True
931
+ p1_count += turn_count
932
+ p1_tool_names.extend(turn_tool_names)
933
+
934
+ if turn_compacted:
935
+ p1_turn_numbers.append(span.absolute_number)
936
+
937
+ if self._usage_pct(included_token_count(messages)) <= self.target_pct:
938
+ break
939
+
940
+ if p1_count > 0:
941
+ await self._notify_compaction(
942
+ CompactionInfo(
943
+ compacted_groups=p1_count,
944
+ phase="phase1",
945
+ turn_numbers=p1_turn_numbers,
946
+ tool_names=p1_tool_names,
947
+ tokens_before=tokens_before,
948
+ tokens_after=included_token_count(messages),
949
+ )
950
+ )
951
+
952
+ return changed
953
+
954
+ async def _run_phase2(self, messages: list[Message]) -> bool:
955
+ changed = False
956
+
957
+ # ---- Phase 2: Remove previous turns' tool-call groups entirely ----
958
+ p2_count = 0
959
+ p2_tool_names: list[str] = []
960
+ p2_turn_numbers: list[int] = []
961
+
962
+ if self._usage_pct(included_token_count(messages)) > self.target_pct:
963
+ p2_tokens_before = included_token_count(messages)
964
+ # Re-resolve at phase entry — Phase 1 insertions may have shifted indices
965
+ resolved = _resolve_turns(messages, self._state)
966
+ if len(resolved.spans) > 1:
967
+ await self._fire_pre_compact(
968
+ "phase2",
969
+ usage_pct=self._usage_pct(p2_tokens_before),
970
+ tokens_before=p2_tokens_before,
971
+ )
972
+ ordered_ids = _ordered_group_ids(messages)
973
+ kinds = _group_kind_map(messages)
974
+
975
+ # _remove_group does not insert messages, so the phase-entry
976
+ # spans stay valid across iterations.
977
+ for span in resolved.previous:
978
+ grouped = _group_messages_by_id(messages)
979
+ starts = _group_start_indices(messages)
980
+ # Use _any_ variant: Phase 1 already excluded originals,
981
+ # but summaries are still visible and need removal.
982
+ turn_groups = _any_tool_groups_in_range(span.start, span.end, ordered_ids, kinds, starts)
983
+
984
+ turn_removed, turn_count, turn_tool_names = self._trim_groups_to_target(
985
+ messages,
986
+ turn_groups,
987
+ grouped,
988
+ self._remove_group,
989
+ )
990
+ if turn_removed:
991
+ changed = True
992
+ p2_count += turn_count
993
+ p2_tool_names.extend(turn_tool_names)
994
+
995
+ if turn_removed:
996
+ p2_turn_numbers.append(span.absolute_number)
997
+
998
+ if self._usage_pct(included_token_count(messages)) <= self.target_pct:
999
+ break
1000
+
1001
+ if p2_count > 0:
1002
+ await self._notify_compaction(
1003
+ CompactionInfo(
1004
+ compacted_groups=p2_count,
1005
+ phase="phase2",
1006
+ turn_numbers=p2_turn_numbers,
1007
+ tool_names=p2_tool_names,
1008
+ tokens_before=p2_tokens_before,
1009
+ tokens_after=included_token_count(messages),
1010
+ )
1011
+ )
1012
+
1013
+ return changed
1014
+
1015
+ async def _run_phase3(self, messages: list[Message]) -> bool:
1016
+ changed = False
1017
+
1018
+ # ---- Phase 3: fold old text turns before touching current work ----
1019
+ #
1020
+ # Phase 1/2 only reduce tool-call groups. Once old tool results have
1021
+ # been summarised/removed, long-running text-heavy sessions can still
1022
+ # exceed the request window with no remaining tool group to compact.
1023
+ # Fold completed turns by marker here, before Phase 4 considers
1024
+ # dropping current-turn tool work.
1025
+ p3_count = 0
1026
+ if self._usage_pct(included_token_count(messages)) > self.target_pct:
1027
+ while self._usage_pct(included_token_count(messages)) > self.target_pct:
1028
+ p3_tokens_before = included_token_count(messages)
1029
+ progressed, compressed = await self._emergency_compress_oldest_turn(
1030
+ messages,
1031
+ usage_pct=self._usage_pct(p3_tokens_before),
1032
+ tokens_before=p3_tokens_before,
1033
+ )
1034
+ if not progressed:
1035
+ break
1036
+ if not compressed:
1037
+ continue
1038
+ p3_count += 1
1039
+ changed = True
1040
+ self._restore_folded_reminders(messages)
1041
+ _dedup_message_ids(messages)
1042
+ annotate_message_groups(messages, force_reannotate=True)
1043
+ annotate_token_counts(messages, tokenizer=self._tokenizer, force_retokenize=True)
1044
+
1045
+ return changed
1046
+
1047
+ async def _run_phase4(
1048
+ self,
1049
+ messages: list[Message],
1050
+ context: CompactionCallContext | None,
1051
+ ) -> bool:
1052
+ return await self._current_turn_drop.run(messages, context)
1053
+
1054
+ def _finalize_pass(self, messages: list[Message], changed: bool, *, entry_tokens: int) -> None:
1055
+ post_compaction = included_token_count(messages)
1056
+ self._last_included_tokens = post_compaction if changed else entry_tokens
1057
+
1058
+ if changed:
1059
+ self._write_reminder_debug()
1060
+
1061
+ # Final snapshot for after_run propagation — Phase-4 rounds already
1062
+ # snapshotted inside their synchronous commit blocks; this pass adds
1063
+ # exclusions from the non-Phase-4 paths above.
1064
+ self._snapshot_excluded_anchors(messages)
1065
+
1066
+ def _snapshot_excluded_anchors(self, messages: list[Message]) -> None:
1067
+ """Delegate the test-visible exclusion-anchor snapshot seam."""
1068
+ self._exclusions.snapshot_excluded_anchors(messages)
1069
+
1070
+ async def _emergency_compress_oldest_turn(
1071
+ self,
1072
+ messages: list[Message],
1073
+ *,
1074
+ usage_pct: float,
1075
+ tokens_before: int,
1076
+ ) -> tuple[bool, bool]:
1077
+ return await self._compression.emergency_compress_oldest_turn(
1078
+ messages,
1079
+ usage_pct=usage_pct,
1080
+ tokens_before=tokens_before,
1081
+ )
1082
+
1083
+ def _reinject_compressed_context_summaries(self, messages: list[Message]) -> None:
1084
+ """Delegate the test-visible compressed-summary reinjection seam."""
1085
+ self._compression.reinject_summaries(messages)
1086
+
1087
+ def _compact_group(
1088
+ self,
1089
+ messages: list[Message],
1090
+ group_id: str,
1091
+ group_msgs: list[Message],
1092
+ ) -> list[str] | None:
1093
+ """Compact one Phase-1 group and commit its replacement to owned history."""
1094
+ result = _compact_group(
1095
+ messages,
1096
+ group_id,
1097
+ group_msgs,
1098
+ ledger=self._exclusions,
1099
+ tokenizer=self._tokenizer,
1100
+ )
1101
+ if result is not None and self._state is not None:
1102
+ # Phase 1 only replaces completed-turn history. Commit its visible
1103
+ # replacement synchronously with exclusion: a failed/cancelled
1104
+ # provider call skips after_run but still saves the excluded state.
1105
+ self._exclusions.reinject_cached_summaries(self._state.get("messages", []))
1106
+ self._compression._record_pre_output_history_len()
1107
+ return result
1108
+
1109
+ def _remove_group(
1110
+ self,
1111
+ messages: list[Message],
1112
+ group_id: str,
1113
+ group_msgs: list[Message],
1114
+ ) -> list[str] | None:
1115
+ """Delegate the test-visible Phase-2 group operation."""
1116
+ return _remove_group(messages, group_id, group_msgs, ledger=self._exclusions)