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,1315 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
3
+ # Contains code adapted from Microsoft Agent Framework (MIT License; see NOTICE).
4
+
5
+ """Chrys-owned compaction primitives.
6
+
7
+ The group-annotation keys, grouping engine, incremental (re-)annotation,
8
+ token-count annotation, projection, and exclusion flag primitives live here.
9
+ ``chrys.service.context.compaction``'s four-phase algorithm and the other consumers
10
+ (last-words, history restore, TUI rendering) read and write message metadata
11
+ exclusively through this module.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import base64
17
+ import binascii
18
+ import json
19
+ import logging
20
+ import math
21
+ import weakref
22
+ from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
23
+ from contextlib import contextmanager
24
+ from contextvars import ContextVar
25
+ from copy import deepcopy
26
+ from dataclasses import dataclass
27
+ from typing import Any, Final, Literal, Protocol, cast, runtime_checkable
28
+
29
+ from chrys.foundation.errors import ProviderResponseError
30
+ from chrys.foundation.hosted_tools import HOSTED_WIRE_REPLAY_PROPERTY_KEYS
31
+ from chrys.foundation.models.history_markers import ANTHROPIC_THINKING_STRIPPED_KEY
32
+ from chrys.foundation.reasoning_origin import REASONING_ORIGIN_KEY
33
+ from chrys.foundation.text.images import inspect_image_dimensions
34
+ from chrys.foundation.tool_execution_stamp import EXECUTION_STAMP_KEY
35
+
36
+ from ._content import Content
37
+ from ._types import ChatResponse, Message
38
+ from .exchanges import (
39
+ TOOL_CALL_CONTENT_TYPES,
40
+ Exchange,
41
+ LiveAccessor,
42
+ is_result_only_message,
43
+ iter_exchanges,
44
+ )
45
+ from .exchanges import (
46
+ TOOL_RESULT_CONTENT_TYPES as TOOL_RESULT_CONTENT_TYPES, # re-exported; chrys.kernel imports it from here
47
+ )
48
+ from .images import is_image_content, is_image_data_uri, is_image_media_type
49
+
50
+ type GroupKind = Literal["system", "user", "assistant_text", "tool_call"]
51
+
52
+ _LIVE_ACCESSOR = LiveAccessor()
53
+
54
+ GROUP_ANNOTATION_KEY = "_group"
55
+ GROUP_ID_KEY = "id"
56
+ GROUP_KIND_KEY = "kind"
57
+ GROUP_INDEX_KEY = "index"
58
+ GROUP_HAS_REASONING_KEY = "has_reasoning"
59
+ GROUP_TOKEN_COUNT_KEY = "token_count"
60
+ GROUP_TOKEN_ESTIMATOR_VERSION_KEY = "token_estimator_v"
61
+ # Version 1 was implicit and serialized non-ASCII text as ``\\uXXXX`` escapes.
62
+ # Version 3 excluded provider-hosted wire replay mirrors. Version 4 additionally
63
+ # excludes opaque Responses reasoning state, which providers do not tokenize as
64
+ # ordinary prompt text.
65
+ TOKEN_ESTIMATOR_VERSION = 4
66
+ EXCLUDED_KEY = "_excluded"
67
+ EXCLUDE_REASON_KEY = "_exclude_reason"
68
+ SUMMARY_OF_MESSAGE_IDS_KEY = "_summary_of_message_ids"
69
+ SUMMARY_OF_GROUP_IDS_KEY = "_summary_of_group_ids"
70
+ SUMMARIZED_BY_SUMMARY_ID_KEY = "_summarized_by_summary_id"
71
+
72
+ _IMAGE_BASE_TOKENS = 85
73
+ _IMAGE_TILE_TOKENS = 170
74
+ _IMAGE_TILE_SIZE = 512
75
+ _IMAGE_FALLBACK_TOKENS = _IMAGE_BASE_TOKENS + (_IMAGE_TILE_TOKENS * 4)
76
+ _IMAGE_URI_TOKEN_PLACEHOLDER = "[image data omitted for token estimate]"
77
+ _TEXT_BUDGETED_ARGUMENT_CONTENT_TYPES = frozenset({"function_call", "search_tool_call", "mcp_server_tool_call"})
78
+ _PROJECTION_ATOMICITY_EXCLUDE_REASON = "partial tool-call group"
79
+
80
+ logger = logging.getLogger(__name__)
81
+
82
+
83
+ @runtime_checkable
84
+ class TokenizerProtocol(Protocol):
85
+ """Protocol for token counters used by token-aware compaction strategies."""
86
+
87
+ def count_tokens(self, text: str) -> int:
88
+ """Count tokens for a serialized message payload."""
89
+ ...
90
+
91
+
92
+ # The shared call/result content-type sets live in ``chrys.kernel.exchanges``
93
+ # (the exchange grammar needs them without importing this module); the import
94
+ # above re-exports them unchanged for every existing importer.
95
+
96
+
97
+ class LastWordsToolCallError(RuntimeError):
98
+ """Raised when a last-words side call responds with tool-call content.
99
+
100
+ The side call's no-tools instruction should prevent this; when the model
101
+ ignores it, the requested tool calls must not be executed, so the side
102
+ call is treated as a failed attempt and retried by the caller.
103
+ """
104
+
105
+
106
+ # Set True in a response's ``additional_properties`` when the model filled its
107
+ # context window after it began answering: the response reads as cut off
108
+ # (``length``) and keeps what it wrote. A LAST_WORDS note fails instead
109
+ # (:func:`raise_if_context_window_filled`).
110
+ CONTEXT_WINDOW_FILLED_KEY: Final = "chrys_context_window_filled"
111
+
112
+
113
+ def raise_if_context_window_filled(response: ChatResponse[Any]) -> None:
114
+ """Fail a LAST_WORDS note that filled the context window, as an overflow that is not retried.
115
+
116
+ The note was cut off and the same prompt fills the window again: the
117
+ overflow has the caller send a smaller one. Every LAST_WORDS side call
118
+ checks it, the completer's and the fallback's.
119
+ """
120
+ if response.additional_properties.get(CONTEXT_WINDOW_FILLED_KEY) is True:
121
+ raise ProviderResponseError(
122
+ "model_context_window_exceeded", "The note filled the context window before it ended.", retryable=False
123
+ )
124
+
125
+
126
+ class CompactionProjectionAtomicityError(RuntimeError):
127
+ """Raised before strict compaction commits a partially projected tool-call group."""
128
+
129
+
130
+ # Marks the current task as being inside an internal side call (today: the
131
+ # Phase-4 last-words completer) issued below ``get_response``. Response
132
+ # instrumentation that publishes or persists per-response artifacts — e.g.
133
+ # the intermediate-text callbacks wired by the engine — must stay silent for
134
+ # these requests: the side-call response is consumed internally and never
135
+ # becomes part of the conversation. A ContextVar (not a client attribute)
136
+ # so concurrent live calls on the same client are unaffected.
137
+ _INTERNAL_SIDE_CALL: ContextVar[bool] = ContextVar("chrys_internal_side_call", default=False)
138
+
139
+
140
+ def in_internal_side_call() -> bool:
141
+ """Whether the current task is inside an internal side call."""
142
+ return _INTERNAL_SIDE_CALL.get()
143
+
144
+
145
+ @contextmanager
146
+ def internal_side_call_scope() -> Iterator[None]:
147
+ """Mark the enclosed request as an internal side call."""
148
+ token = _INTERNAL_SIDE_CALL.set(True)
149
+ try:
150
+ yield
151
+ finally:
152
+ _INTERNAL_SIDE_CALL.reset(token)
153
+
154
+
155
+ def messages_contain_tool_calls(messages: Sequence[Message]) -> bool:
156
+ """Return whether any message carries tool-call content of any recognized type."""
157
+ return any(content.type in TOOL_CALL_CONTENT_TYPES for message in messages for content in message.contents)
158
+
159
+
160
+ @runtime_checkable
161
+ class LastWordsCompleter(Protocol):
162
+ """Client-owned capability: scoped natural-completion side call.
163
+
164
+ Sends ``base_messages`` plus one appended user message containing
165
+ ``instruction`` through the same client, route session, options, and
166
+ tool definitions as the live call — with ``tool_choice`` forced to
167
+ ``"none"`` when tools are present — and returns only the assistant
168
+ text. Tool-call markup in plain text is additionally suppressed by the
169
+ instruction itself. Never persists anything into history and never
170
+ mutates the live call's options.
171
+ """
172
+
173
+ async def complete_last_words(
174
+ self,
175
+ base_messages: Sequence[Message],
176
+ instruction: str,
177
+ *,
178
+ max_output_tokens: int,
179
+ on_usage: Callable[[Mapping[str, Any]], None] | None = None,
180
+ ) -> str:
181
+ """Run the side call over ``base_messages`` and return the note text.
182
+
183
+ ``on_usage`` receives the raw provider-reported ``usage_details`` of
184
+ every side-call response as soon as it is observed — including
185
+ responses the completer subsequently rejects (a refused response
186
+ still spent real tokens). It must be cheap and non-raising.
187
+ """
188
+ ...
189
+
190
+
191
+ @dataclass(frozen=True)
192
+ class CompactionCallContext:
193
+ """Per-call capabilities a client passes into a compaction strategy.
194
+
195
+ Deliberately minimal: no client object, no options object by reference,
196
+ no route/session mutation surface. Strategies see exactly the
197
+ affordances listed here.
198
+ """
199
+
200
+ last_words_completer: LastWordsCompleter | None = None
201
+ tool_definition_tokens: int = 0
202
+ """Deprecated tools-only estimate retained for public API compatibility."""
203
+ request_overhead_tokens: int = 0
204
+ """Estimate of all fixed request content outside the message list."""
205
+ next_exchange_operation_id: str | None = None
206
+ """Exchange operation already reserved for the request being compacted."""
207
+
208
+
209
+ @runtime_checkable
210
+ class CompactionAdmissionState(Protocol):
211
+ """Strategy state required to admit an outbound model request safely."""
212
+
213
+ max_context_tokens: int
214
+
215
+ @property
216
+ def last_included_tokens(self) -> int: ...
217
+
218
+ @property
219
+ def system_overhead_tokens(self) -> int: ...
220
+
221
+ @property
222
+ def calibration_ratio(self) -> float: ...
223
+
224
+
225
+ @runtime_checkable
226
+ class ContextOverflowSink(Protocol):
227
+ """Strategy that takes a provider's verdict that the context window is full.
228
+
229
+ Separate from :class:`CompactionAdmissionState` on purpose: that
230
+ protocol's isinstance check gates the output-cap clamp, which a strategy
231
+ without this method must keep.
232
+ """
233
+
234
+ def note_context_overflow(self, exc: BaseException | None = None) -> bool:
235
+ """Make the next pass compact whatever the local estimate says.
236
+
237
+ *exc* is the provider's rejection, or ``None`` when no error carried
238
+ the verdict. Returns whether compacting and resending the same
239
+ request can help.
240
+ """
241
+ ...
242
+
243
+
244
+ @runtime_checkable
245
+ class CompactionStrategy(Protocol):
246
+ """Protocol for strategies run by ``BaseChatClient`` before model calls.
247
+
248
+ .. note:: **Breaking change (Phase-4 LAST_WORDS side call).** Strategies
249
+ are now invoked as ``strategy(messages, context)`` — two positional
250
+ arguments, where ``context`` is a :class:`CompactionCallContext` or
251
+ ``None``. Strategies written against the earlier single-parameter
252
+ shape (``__call__(self, messages)``) fail loudly with ``TypeError``
253
+ and must add the ``context`` parameter (accepting and ignoring it is
254
+ valid: it only carries the optional cache-safe last-words completer).
255
+ """
256
+
257
+ async def __call__(self, messages: list[Message], context: CompactionCallContext | None = None) -> bool:
258
+ """Mutate messages in place and return whether anything changed."""
259
+ ...
260
+
261
+
262
+ def _has_content_type(message: Message, content_type: str) -> bool:
263
+ """Return whether a message contains the requested content type."""
264
+ return any(content.type == content_type for content in message.contents)
265
+
266
+
267
+ def _has_tool_call(message: Message) -> bool:
268
+ """Return whether a message contains any recognized tool invocation."""
269
+ return any(content.type in TOOL_CALL_CONTENT_TYPES for content in message.contents)
270
+
271
+
272
+ def _has_reasoning(message: Message) -> bool:
273
+ """Return whether a message contains reasoning text."""
274
+ return _has_content_type(message, "text_reasoning")
275
+
276
+
277
+ def _is_tool_call_assistant(message: Message) -> bool:
278
+ """Return whether an assistant message contains a recognized tool invocation."""
279
+ return message.role == "assistant" and _has_tool_call(message)
280
+
281
+
282
+ def _is_reasoning_only_assistant(message: Message) -> bool:
283
+ """Return whether a non-empty assistant message contains only reasoning text."""
284
+ if message.role != "assistant" or not message.contents:
285
+ return False
286
+ return all(content.type == "text_reasoning" for content in message.contents)
287
+
288
+
289
+ def _ensure_message_ids(
290
+ messages: list[Message], *, id_offset: int = 0, reserved_ids: Iterable[str] | None = None
291
+ ) -> None:
292
+ """Assign positional ids to messages that lack them, avoiding known collisions.
293
+
294
+ Ids are POSITIONAL (``msg_{index}``) and collision-checked only against
295
+ ``messages`` plus ``reserved_ids``. A later pass can re-mint an id still
296
+ present in an archived ``compressed_msgs`` block, so ``message_id`` is not
297
+ a session-wide identity. For archived/live audit deduplication, follow the
298
+ contract in the module header of ``service/session/sub_agent_transcript.py``.
299
+ """
300
+ existing_ids: set[str] = set(reserved_ids) if reserved_ids is not None else set()
301
+ existing_ids.update(message.message_id for message in messages if message.message_id)
302
+ for index, message in enumerate(messages):
303
+ if message.message_id:
304
+ continue
305
+ candidate = f"msg_{id_offset + index}"
306
+ if candidate in existing_ids:
307
+ counter = id_offset + len(messages)
308
+ candidate = f"msg_{counter}"
309
+ while candidate in existing_ids:
310
+ counter += 1
311
+ candidate = f"msg_{counter}"
312
+ message.message_id = candidate
313
+ existing_ids.add(candidate)
314
+
315
+
316
+ def _group_id_for(message: Message, group_index: int) -> str:
317
+ """Derive a group id from its message id, falling back to the group index."""
318
+ if message.message_id:
319
+ return f"group_{message.message_id}"
320
+ return f"group_index_{group_index}"
321
+
322
+
323
+ def group_messages(
324
+ messages: list[Message], *, id_offset: int = 0, reserved_ids: Iterable[str] | None = None
325
+ ) -> list[dict[str, Any]]:
326
+ """Compute group spans and metadata for annotation.
327
+
328
+ Args:
329
+ messages: The messages (or a slice of them) to group.
330
+
331
+ Keyword Args:
332
+ id_offset: Absolute starting index used when auto-assigning
333
+ ``message_id`` values, so incremental annotation of a list slice
334
+ produces ids that stay unique across the full list.
335
+ reserved_ids: Message ids that already exist outside ``messages`` (for
336
+ example in a preserved prefix). Auto-assigned ids are guaranteed
337
+ not to collide with these, preventing duplicate ids across the
338
+ full list.
339
+
340
+ Returns:
341
+ Ordered list of lightweight span dicts with keys:
342
+ ``group_id``, ``kind``, ``start_index``, ``end_index``,
343
+ ``has_reasoning``.
344
+
345
+ Exchange boundaries — which output block answers which response run,
346
+ with history markers and non-assistant roles as hard boundaries — come
347
+ from the shared exchange grammar. Spans remain a secondary,
348
+ compaction-owned partition of each exchange (see
349
+ :func:`_exchange_spans`); messages outside every exchange group by
350
+ role as before.
351
+ """
352
+ _ensure_message_ids(messages, id_offset=id_offset, reserved_ids=reserved_ids)
353
+ spans: list[dict[str, Any]] = []
354
+
355
+ def append_span(kind: GroupKind, start_index: int, end_index: int, *, has_reasoning: bool) -> None:
356
+ spans.append(
357
+ {
358
+ "group_id": _group_id_for(messages[start_index], len(spans)),
359
+ "kind": kind,
360
+ "start_index": start_index,
361
+ "end_index": end_index,
362
+ "has_reasoning": has_reasoning,
363
+ }
364
+ )
365
+
366
+ exchange_by_start = {
367
+ exchange.response_indices[0]: exchange for exchange in iter_exchanges(messages, _LIVE_ACCESSOR)
368
+ }
369
+
370
+ i = 0
371
+ while i < len(messages):
372
+ exchange = exchange_by_start.get(i)
373
+ if exchange is not None:
374
+ for kind, start_index, end_index, has_reasoning in _exchange_spans(messages, exchange):
375
+ append_span(kind, start_index, end_index, has_reasoning=has_reasoning)
376
+ i = (exchange.output_indices or exchange.response_indices)[-1] + 1
377
+ continue
378
+
379
+ current = messages[i]
380
+ if current.role == "system":
381
+ append_span("system", i, i, has_reasoning=_has_reasoning(current))
382
+ i += 1
383
+ continue
384
+ if current.role == "user":
385
+ append_span("user", i, i, has_reasoning=_has_reasoning(current))
386
+ i += 1
387
+ continue
388
+ if current.role == "tool":
389
+ # Orphan tool output answering no call run still groups as a run.
390
+ k = i + 1
391
+ while k < len(messages) and messages[k].role == "tool":
392
+ k += 1
393
+ append_span("tool_call", i, k - 1, has_reasoning=False)
394
+ i = k
395
+ continue
396
+ append_span("assistant_text", i, i, has_reasoning=_has_reasoning(current))
397
+ i += 1
398
+
399
+ return spans
400
+
401
+
402
+ def _exchange_spans(messages: list[Message], exchange: Exchange) -> list[tuple[GroupKind, int, int, bool]]:
403
+ """Secondary partition of one exchange into span tuples.
404
+
405
+ A maximal run of call-carrying assistant siblings — reasoning-only
406
+ riders included, covering the OpenAI Responses shape where reasoning
407
+ and function_call contents are co-located as well as reasoning
408
+ prefixes in their own messages — forms one ``tool_call`` span. An
409
+ ordinary text-only sibling keeps its own span and splits call runs.
410
+
411
+ Once the exchange has results, calls and results must share ONE group:
412
+ compaction excludes group-wise, and a call grouped apart from its
413
+ result lets a partial exclusion send a result without its call (or a
414
+ call without its answered result) to the provider — a hard protocol
415
+ violation. So with an output block (tool-role or result-only assistant
416
+ messages, hosted MCP included), everything from the first call span
417
+ through the end of the output block fuses into one ``tool_call`` span,
418
+ absorbing interleaved non-call siblings; with only embedded results,
419
+ the call spans fuse through the last of them. Reasoning anywhere in
420
+ the fused range counts toward the group. Text before the first call
421
+ keeps its own span, and an exchange with no results at all keeps its
422
+ split spans.
423
+ """
424
+ response_end = exchange.response_indices[-1]
425
+ segments: list[list[Any]] = []
426
+
427
+ i = exchange.response_indices[0]
428
+ while i <= response_end:
429
+ message = messages[i]
430
+ if _is_tool_call_assistant(message) or _is_reasoning_only_assistant(message):
431
+ run_start = i
432
+ saw_call = _is_tool_call_assistant(message)
433
+ i += 1
434
+ while i <= response_end and (
435
+ _is_tool_call_assistant(messages[i]) or _is_reasoning_only_assistant(messages[i])
436
+ ):
437
+ saw_call = saw_call or _is_tool_call_assistant(messages[i])
438
+ i += 1
439
+ if saw_call:
440
+ has_reasoning = any(_has_reasoning(messages[k]) for k in range(run_start, i))
441
+ segments.append(["tool_call", run_start, i - 1, has_reasoning])
442
+ else:
443
+ # Reasoning-only siblings with no call to ride keep per-message spans.
444
+ segments.extend(["assistant_text", k, k, _has_reasoning(messages[k])] for k in range(run_start, i))
445
+ continue
446
+ segments.append(["assistant_text", i, i, _has_reasoning(message)])
447
+ i += 1
448
+
449
+ output = exchange.output_indices
450
+ first_call_pos = next((pos for pos, segment in enumerate(segments) if segment[0] == "tool_call"), None)
451
+ if first_call_pos is not None and (output or exchange.embedded_results):
452
+ fuse_end = output[-1] if output else max(segment[2] for segment in segments if segment[0] == "tool_call")
453
+ absorbed = [segment for segment in segments[first_call_pos:] if segment[1] <= fuse_end]
454
+ kept_tail = segments[first_call_pos + len(absorbed) :]
455
+ has_reasoning = any(segment[3] for segment in absorbed) or any(_has_reasoning(messages[k]) for k in output)
456
+ segments[first_call_pos:] = [
457
+ ["tool_call", segments[first_call_pos][1], fuse_end, has_reasoning],
458
+ *kept_tail,
459
+ ]
460
+
461
+ return [(kind, start_index, end_index, has_reasoning) for kind, start_index, end_index, has_reasoning in segments]
462
+
463
+
464
+ def _coerce_group_kind(value: object) -> GroupKind | None:
465
+ """Coerce a raw annotation value into a known group kind.
466
+
467
+ The membership test intentionally preserves equality-scan semantics, so it
468
+ remains safe for unhashable values.
469
+ """
470
+ if value in ("system", "user", "assistant_text", "tool_call"):
471
+ return cast("GroupKind", value)
472
+ return None
473
+
474
+
475
+ def _read_group_annotation(message: Message) -> dict[str, Any] | None:
476
+ """Read a validated group annotation.
477
+
478
+ Return ``None`` unless id, kind, index and has_reasoning are present and
479
+ well-typed, and any token count is an integer.
480
+ """
481
+ raw_annotation = _read_group_annotation_raw(message)
482
+ if raw_annotation is None:
483
+ return None
484
+
485
+ group_id = raw_annotation.get(GROUP_ID_KEY)
486
+ group_kind = _coerce_group_kind(raw_annotation.get(GROUP_KIND_KEY))
487
+ group_index = raw_annotation.get(GROUP_INDEX_KEY)
488
+ has_reasoning = raw_annotation.get(GROUP_HAS_REASONING_KEY)
489
+ token_count = raw_annotation.get(GROUP_TOKEN_COUNT_KEY)
490
+ if token_count is not None and not isinstance(token_count, int):
491
+ return None
492
+ if (
493
+ not isinstance(group_id, str)
494
+ or group_kind is None
495
+ or not isinstance(group_index, int)
496
+ or not isinstance(has_reasoning, bool)
497
+ ):
498
+ return None
499
+
500
+ return raw_annotation
501
+
502
+
503
+ def _read_group_annotation_raw(message: Message) -> dict[str, Any] | None:
504
+ """Return the live annotation mapping for in-place mutation, or None when absent."""
505
+ annotation = message.additional_properties.get(GROUP_ANNOTATION_KEY)
506
+ if isinstance(annotation, Mapping):
507
+ return cast("dict[str, Any]", annotation)
508
+ return None
509
+
510
+
511
+ def _write_group_annotation(
512
+ message: Message,
513
+ *,
514
+ group_id: str,
515
+ kind: GroupKind,
516
+ index: int,
517
+ has_reasoning: bool,
518
+ ) -> None:
519
+ """Rewrite core group fields while preserving token counts and unknown fields.
520
+
521
+ This retains extensions such as ``SUMMARIZED_BY_SUMMARY_ID_KEY`` during reannotation.
522
+ """
523
+ existing_raw_annotation = _read_group_annotation_raw(message)
524
+ unknown_fields: dict[str, Any] = {}
525
+ token_count: int | None = None
526
+ if existing_raw_annotation is not None:
527
+ raw_token_count = existing_raw_annotation.get(GROUP_TOKEN_COUNT_KEY)
528
+ if isinstance(raw_token_count, int) or raw_token_count is None:
529
+ token_count = raw_token_count
530
+ unknown_fields = {
531
+ key: value
532
+ for key, value in existing_raw_annotation.items()
533
+ if key
534
+ not in {
535
+ GROUP_ID_KEY,
536
+ GROUP_KIND_KEY,
537
+ GROUP_INDEX_KEY,
538
+ GROUP_HAS_REASONING_KEY,
539
+ GROUP_TOKEN_COUNT_KEY,
540
+ }
541
+ }
542
+
543
+ annotation = {
544
+ GROUP_ID_KEY: group_id,
545
+ GROUP_KIND_KEY: kind,
546
+ GROUP_INDEX_KEY: index,
547
+ GROUP_HAS_REASONING_KEY: has_reasoning,
548
+ GROUP_TOKEN_COUNT_KEY: token_count,
549
+ }
550
+ annotation.update(unknown_fields)
551
+ message.additional_properties[GROUP_ANNOTATION_KEY] = annotation
552
+
553
+
554
+ def _group_id(message: Message) -> str | None:
555
+ """Read the group id from a valid annotation, or None when unavailable."""
556
+ annotation = _read_group_annotation(message)
557
+ if annotation is None:
558
+ return None
559
+ group_id = annotation.get(GROUP_ID_KEY)
560
+ return group_id if isinstance(group_id, str) else None
561
+
562
+
563
+ def _group_kind(message: Message) -> GroupKind | None:
564
+ """Read the group kind from a valid annotation, or None when unavailable."""
565
+ annotation = _read_group_annotation(message)
566
+ if annotation is None:
567
+ return None
568
+ return _coerce_group_kind(annotation.get(GROUP_KIND_KEY))
569
+
570
+
571
+ def _group_index(message: Message) -> int | None:
572
+ """Read the group index from a valid annotation, or None when unavailable."""
573
+ annotation = _read_group_annotation(message)
574
+ if annotation is None:
575
+ return None
576
+ group_index = annotation.get(GROUP_INDEX_KEY)
577
+ return group_index if isinstance(group_index, int) else None
578
+
579
+
580
+ def _token_count(message: Message) -> int | None:
581
+ """Read a token count only when its annotation uses the current estimator version."""
582
+ annotation = _read_group_annotation(message)
583
+ if annotation is None:
584
+ return None
585
+ estimator_version = annotation.get(GROUP_TOKEN_ESTIMATOR_VERSION_KEY)
586
+ if (
587
+ not isinstance(estimator_version, int)
588
+ or isinstance(estimator_version, bool)
589
+ or estimator_version != TOKEN_ESTIMATOR_VERSION
590
+ ):
591
+ return None
592
+ token_count = annotation.get(GROUP_TOKEN_COUNT_KEY)
593
+ return token_count if isinstance(token_count, int) else None
594
+
595
+
596
+ def _write_token_count(message: Message, token_count: int) -> None:
597
+ """Store a token count and estimator version in the existing annotation.
598
+
599
+ Silently do nothing when no annotation mapping exists.
600
+ """
601
+ annotation = _read_group_annotation_raw(message)
602
+ if annotation is None:
603
+ return
604
+ annotation[GROUP_TOKEN_COUNT_KEY] = token_count
605
+ annotation[GROUP_TOKEN_ESTIMATOR_VERSION_KEY] = TOKEN_ESTIMATOR_VERSION
606
+ message.additional_properties[GROUP_ANNOTATION_KEY] = annotation
607
+
608
+
609
+ def _ordered_group_ids_from_annotations(messages: Sequence[Message]) -> list[str]:
610
+ """Collect distinct annotated group ids in first-occurrence order."""
611
+ ordered_group_ids: list[str] = []
612
+ seen: set[str] = set()
613
+ for message in messages:
614
+ group_id = _group_id(message)
615
+ if group_id is not None and group_id not in seen:
616
+ seen.add(group_id)
617
+ ordered_group_ids.append(group_id)
618
+ return ordered_group_ids
619
+
620
+
621
+ def _first_untokenized_index(messages: Sequence[Message]) -> int | None:
622
+ """Find the first message without a current token estimate, or None when all have one."""
623
+ for index, message in enumerate(messages):
624
+ if _token_count(message) is None:
625
+ return index
626
+ return None
627
+
628
+
629
+ def _first_annotation_gaps(
630
+ messages: Sequence[Message],
631
+ *,
632
+ include_tokens: bool,
633
+ ) -> tuple[int | None, int | None]:
634
+ """Scan until the first missing group annotation or requested token count."""
635
+ first_unannotated: int | None = None
636
+ first_untokenized: int | None = None
637
+ for index, message in enumerate(messages):
638
+ missing_group_annotation = first_unannotated is None and _group_id(message) is None
639
+ missing_token_annotation = include_tokens and first_untokenized is None and _token_count(message) is None
640
+
641
+ if missing_group_annotation:
642
+ first_unannotated = index
643
+ if missing_token_annotation:
644
+ first_untokenized = index
645
+
646
+ if missing_group_annotation or missing_token_annotation:
647
+ break
648
+ return first_unannotated, first_untokenized
649
+
650
+
651
+ def _reannotation_start(messages: Sequence[Message], index: int) -> int:
652
+ """Back the start index up to a position where regrouping is exchange-safe.
653
+
654
+ First to the beginning of the group the preceding message belongs to,
655
+ then — while the rewound position can still sit inside an exchange
656
+ begun earlier — further back along the exchange's backward shape:
657
+ output-block members (tool-role or result-only assistant) may follow
658
+ output or response-run members, response-run members only other
659
+ response-run members. Call/result fusion spans whole exchanges, so a
660
+ rewind stopping inside one regroups the suffix without its earlier
661
+ call spans — a result landing after a previously annotated
662
+ ``[call, text]`` prefix would freeze the split grouping and leave the
663
+ call excludable apart from its result. A response-run member preceded
664
+ by an output-block member marks a COMPLETED earlier exchange, and
665
+ user/system roles and history markers are boundaries no exchange
666
+ crosses; stopping there keeps re-annotation per-exchange — walking
667
+ past finished exchanges would rewrite their annotations on every pass
668
+ and turn a long tool loop's incremental annotation quadratic.
669
+ """
670
+ if index <= 0:
671
+ return 0
672
+ previous_index = index - 1
673
+ previous_group_id = _group_id(messages[previous_index])
674
+ previous_group_index = _group_index(messages[previous_index])
675
+ if previous_group_id is not None:
676
+ # Key on the group OCCURRENCE (id AND index): group ids are minted
677
+ # from the start message's id, so preset message_id collisions can
678
+ # give adjacent groups the same id — an id-only walk would cross
679
+ # them before the exchange-shape boundary check below runs.
680
+ while previous_index > 0:
681
+ if (
682
+ _group_id(messages[previous_index - 1]) != previous_group_id
683
+ or _group_index(messages[previous_index - 1]) != previous_group_index
684
+ ):
685
+ break
686
+ previous_index -= 1
687
+ while previous_index > 0:
688
+ current = messages[previous_index]
689
+ preceding = messages[previous_index - 1]
690
+ if _is_output_block_member(current):
691
+ if not (_is_output_block_member(preceding) or _is_response_run_member(preceding)):
692
+ break
693
+ elif _is_response_run_member(current):
694
+ if not _is_response_run_member(preceding):
695
+ break
696
+ else:
697
+ break
698
+ previous_index -= 1
699
+ return previous_index
700
+
701
+
702
+ def _is_output_block_member(message: Message) -> bool:
703
+ """Markerless tool-role or result-only assistant message — the shapes an
704
+ exchange's output block is made of."""
705
+ if _LIVE_ACCESSOR.has_marker(message):
706
+ return False
707
+ return message.role == "tool" or is_result_only_message(message, _LIVE_ACCESSOR)
708
+
709
+
710
+ def _is_response_run_member(message: Message) -> bool:
711
+ """Markerless assistant message that is not result-only — the shapes a
712
+ response run is made of."""
713
+ return (
714
+ message.role == "assistant"
715
+ and not _LIVE_ACCESSOR.has_marker(message)
716
+ and not is_result_only_message(message, _LIVE_ACCESSOR)
717
+ )
718
+
719
+
720
+ def annotate_message_groups(
721
+ messages: list[Message],
722
+ *,
723
+ from_index: int | None = None,
724
+ force_reannotate: bool = False,
725
+ tokenizer: TokenizerProtocol | None = None,
726
+ ) -> list[str]:
727
+ """Annotate message groups while reusing existing annotations when possible.
728
+
729
+ By default, the function re-annotates only the suffix that contains new
730
+ messages and keeps previously annotated prefixes untouched. When a
731
+ ``tokenizer`` is provided, token-count annotations are also populated
732
+ incrementally.
733
+ """
734
+ if not messages:
735
+ return []
736
+
737
+ if force_reannotate:
738
+ start_index = 0
739
+ elif from_index is not None:
740
+ start_index = max(0, min(from_index, len(messages) - 1))
741
+ else:
742
+ first_unannotated_index, first_untokenized_index = _first_annotation_gaps(
743
+ messages,
744
+ include_tokens=tokenizer is not None,
745
+ )
746
+ candidate_starts = [index for index in (first_unannotated_index, first_untokenized_index) if index is not None]
747
+ if not candidate_starts:
748
+ return _ordered_group_ids_from_annotations(messages)
749
+ start_index = min(candidate_starts)
750
+
751
+ start_index = _reannotation_start(messages, start_index)
752
+
753
+ # Continue group indices from the preserved prefix when only re-annotating a suffix.
754
+ group_index_offset = 0
755
+ if start_index > 0:
756
+ previous_group_index = _group_index(messages[start_index - 1])
757
+ if previous_group_index is not None:
758
+ group_index_offset = previous_group_index + 1
759
+
760
+ reserved_ids = {message.message_id for message in messages[:start_index] if message.message_id}
761
+ spans = group_messages(messages[start_index:], id_offset=start_index, reserved_ids=reserved_ids)
762
+ for span_index, span in enumerate(spans):
763
+ group_id = str(span["group_id"])
764
+ kind = _coerce_group_kind(span["kind"])
765
+ if kind is None:
766
+ raise ValueError(f"Unexpected group kind in span: {span['kind']}")
767
+ local_start_index = int(span["start_index"])
768
+ local_end_index = int(span["end_index"])
769
+ has_reasoning = bool(span["has_reasoning"])
770
+ for idx in range(start_index + local_start_index, start_index + local_end_index + 1):
771
+ message = messages[idx]
772
+ _write_group_annotation(
773
+ message,
774
+ group_id=group_id,
775
+ kind=kind,
776
+ index=group_index_offset + span_index,
777
+ has_reasoning=has_reasoning,
778
+ )
779
+ message.additional_properties.setdefault(EXCLUDED_KEY, False)
780
+ if tokenizer is not None and _token_count(message) is None:
781
+ _write_token_count(message, _message_token_estimate(message, tokenizer))
782
+ return _ordered_group_ids_from_annotations(messages)
783
+
784
+
785
+ def _serialize_content(content: Content) -> dict[str, Any]:
786
+ """Serialize content for token estimation, excluding duplicate and opaque replay data."""
787
+ payload = content.to_dict(exclude_none=True)
788
+ payload.pop("raw_representation", None)
789
+ additional_properties = payload.get("additional_properties")
790
+ if isinstance(additional_properties, dict):
791
+ additional_properties.pop(EXECUTION_STAMP_KEY, None)
792
+ additional_properties.pop(REASONING_ORIGIN_KEY, None)
793
+ additional_properties.pop(ANTHROPIC_THINKING_STRIPPED_KEY, None)
794
+ for key in HOSTED_WIRE_REPLAY_PROPERTY_KEYS:
795
+ additional_properties.pop(key, None)
796
+ # ``items`` mirrors ``result`` for function_result content; exclude it
797
+ # to avoid double-counting tokens during estimation.
798
+ payload.pop("items", None)
799
+ # Responses reasoning items carry server-encrypted replay state alongside
800
+ # an item id. Although this blob is sent back to the provider, it is opaque
801
+ # state rather than prompt text; tokenizing its base64 representation can
802
+ # overestimate the context by thousands of tokens. Id-less protected data
803
+ # remains included because chat-completions dialects use it for replayable
804
+ # reasoning_content/reasoning_details payloads.
805
+ if content.type == "text_reasoning" and content.id is not None:
806
+ payload.pop("protected_data", None)
807
+ return _redact_serialized_image_uris(payload)
808
+
809
+
810
+ def _redact_serialized_image_uris(value: Any) -> Any:
811
+ if isinstance(value, dict):
812
+ redacted = {
813
+ key: nested
814
+ if key == "arguments" and _serialized_arguments_are_text_budgeted(value)
815
+ else _redact_serialized_image_uris(nested)
816
+ for key, nested in value.items()
817
+ }
818
+ if _serialized_dict_is_image_content(redacted) and "uri" in redacted:
819
+ redacted["uri"] = _IMAGE_URI_TOKEN_PLACEHOLDER
820
+ return redacted
821
+ if isinstance(value, list):
822
+ return [_redact_serialized_image_uris(item) for item in value]
823
+ return value
824
+
825
+
826
+ def _serialized_dict_is_image_content(value: Mapping[Any, Any]) -> bool:
827
+ content_type = value.get("type")
828
+ if content_type not in ("data", "uri"):
829
+ return False
830
+ return (
831
+ is_image_media_type(value.get("media_type"))
832
+ or is_image_media_type(_serialized_additional_media_type(value))
833
+ or is_image_data_uri(value.get("uri"))
834
+ )
835
+
836
+
837
+ def _serialized_additional_media_type(value: Mapping[Any, Any]) -> Any:
838
+ additional_properties = value.get("additional_properties")
839
+ if not isinstance(additional_properties, dict):
840
+ return None
841
+ return additional_properties.get("media_type")
842
+
843
+
844
+ def _serialized_arguments_are_text_budgeted(value: Mapping[Any, Any]) -> bool:
845
+ return value.get("type") in _TEXT_BUDGETED_ARGUMENT_CONTENT_TYPES
846
+
847
+
848
+ def _serialize_message(message: Message) -> str:
849
+ """Produce stable estimator serialization with sorted keys.
850
+
851
+ Keep non-ASCII text literal for mixed-language token counting.
852
+ """
853
+ serialized_contents = [_serialize_content(content) for content in message.contents]
854
+ payload = {
855
+ "role": message.role,
856
+ "message_id": message.message_id,
857
+ "contents": serialized_contents,
858
+ }
859
+ return json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str)
860
+
861
+
862
+ def _message_token_estimate(message: Message, tokenizer: TokenizerProtocol) -> int:
863
+ """Return serialized text tokens plus image-content estimates."""
864
+ image_tokens = estimate_message_image_tokens(message)
865
+ return tokenizer.count_tokens(_serialize_message(message)) + image_tokens
866
+
867
+
868
+ def estimate_message_tokens(message: Message, tokenizer: TokenizerProtocol) -> int:
869
+ """Estimate one message without mutating message or content metadata.
870
+
871
+ The annotation path opportunistically caches decoded image dimensions on
872
+ content objects. Scoped Phase-4 slices must remain strictly read-only
873
+ during admission and clipping calculations, so they use this variant.
874
+ """
875
+ image_tokens = sum(
876
+ _estimate_image_tokens_in_tree(content, set(), cache_dimensions=False) for content in message.contents
877
+ )
878
+ return tokenizer.count_tokens(_serialize_message(message)) + image_tokens
879
+
880
+
881
+ def estimate_message_image_tokens(message: Message) -> int:
882
+ """Estimate image tokens for direct and nested image contents."""
883
+ total = 0
884
+ stack: set[int] = set()
885
+ for content in message.contents:
886
+ total += _estimate_image_tokens_in_tree(content, stack, cache_dimensions=True)
887
+ return total
888
+
889
+
890
+ def estimate_toolresult_image_tokens(message: Message) -> int:
891
+ """Estimate image tokens for function_result.items omitted from serialization."""
892
+ total = 0
893
+ for content in message.contents:
894
+ if content.type != "function_result" or not content.items:
895
+ continue
896
+ for item in content.items:
897
+ if is_image_content(item):
898
+ total += _estimate_image_tokens(item)
899
+ return total
900
+
901
+
902
+ def _estimate_image_tokens_in_tree(value: Any, stack: set[int], *, cache_dimensions: bool) -> int:
903
+ if isinstance(value, Content):
904
+ content_id = id(value)
905
+ if content_id in stack:
906
+ return 0
907
+ stack.add(content_id)
908
+ try:
909
+ total = _estimate_image_tokens(value, cache_dimensions=cache_dimensions) if is_image_content(value) else 0
910
+ for child_value in _content_child_values(value):
911
+ total += _estimate_image_tokens_in_tree(child_value, stack, cache_dimensions=cache_dimensions)
912
+ return total
913
+ finally:
914
+ stack.remove(content_id)
915
+ if isinstance(value, Mapping):
916
+ mapping_id = id(value)
917
+ if mapping_id in stack:
918
+ return 0
919
+ stack.add(mapping_id)
920
+ try:
921
+ if _serialized_dict_is_image_content(value):
922
+ return _estimate_serialized_image_tokens(value, cache_dimensions=cache_dimensions)
923
+ total = 0
924
+ for key, nested in value.items():
925
+ if key == "arguments" and _serialized_arguments_are_text_budgeted(value):
926
+ continue
927
+ total += _estimate_image_tokens_in_tree(nested, stack, cache_dimensions=cache_dimensions)
928
+ return total
929
+ finally:
930
+ stack.remove(mapping_id)
931
+ if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
932
+ sequence_id = id(value)
933
+ if sequence_id in stack:
934
+ return 0
935
+ stack.add(sequence_id)
936
+ try:
937
+ total = 0
938
+ for nested in value:
939
+ total += _estimate_image_tokens_in_tree(nested, stack, cache_dimensions=cache_dimensions)
940
+ return total
941
+ finally:
942
+ stack.remove(sequence_id)
943
+ return 0
944
+
945
+
946
+ def _content_child_values(content: Content) -> Iterable[Any]:
947
+ # Keep this aligned with the serialized fields walked by _redact_serialized_image_uris.
948
+ return (
949
+ content.items,
950
+ content.inputs,
951
+ content.outputs,
952
+ content.output,
953
+ content.result,
954
+ )
955
+
956
+
957
+ def _estimate_image_tokens(content: Content, *, cache_dimensions: bool = True) -> int:
958
+ width = _positive_int(content.additional_properties.get("width"))
959
+ height = _positive_int(content.additional_properties.get("height"))
960
+ if width is None or height is None:
961
+ inferred_dimensions = _image_dimensions_from_data_uri(content.uri)
962
+ if inferred_dimensions is not None:
963
+ width, height = inferred_dimensions
964
+ if cache_dimensions:
965
+ _cache_content_image_dimensions(content, width, height)
966
+ if width is None or height is None:
967
+ return _IMAGE_FALLBACK_TOKENS
968
+ return _image_tokens_from_dims(width, height)
969
+
970
+
971
+ def _estimate_serialized_image_tokens(content: Mapping[Any, Any], *, cache_dimensions: bool = True) -> int:
972
+ width = _positive_int(content.get("width"))
973
+ height = _positive_int(content.get("height"))
974
+ additional_properties = content.get("additional_properties")
975
+ if isinstance(additional_properties, Mapping):
976
+ width = width or _positive_int(additional_properties.get("width"))
977
+ height = height or _positive_int(additional_properties.get("height"))
978
+ if width is None or height is None:
979
+ inferred_dimensions = _image_dimensions_from_data_uri(content.get("uri"))
980
+ if inferred_dimensions is not None:
981
+ width, height = inferred_dimensions
982
+ if cache_dimensions and isinstance(content, dict):
983
+ _cache_serialized_image_dimensions(content, width, height)
984
+ if width is None or height is None:
985
+ return _IMAGE_FALLBACK_TOKENS
986
+ return _image_tokens_from_dims(width, height)
987
+
988
+
989
+ def _image_tokens_from_dims(width: int, height: int) -> int:
990
+ tiles = max(1, math.ceil(width / _IMAGE_TILE_SIZE)) * max(1, math.ceil(height / _IMAGE_TILE_SIZE))
991
+ return _IMAGE_BASE_TOKENS + (_IMAGE_TILE_TOKENS * tiles)
992
+
993
+
994
+ def _cache_content_image_dimensions(content: Content, width: int, height: int) -> None:
995
+ content.additional_properties["width"] = width
996
+ content.additional_properties["height"] = height
997
+
998
+
999
+ def _cache_serialized_image_dimensions(content: dict[Any, Any], width: int, height: int) -> None:
1000
+ additional_properties = content.get("additional_properties")
1001
+ if not isinstance(additional_properties, dict):
1002
+ additional_properties = {}
1003
+ content["additional_properties"] = additional_properties
1004
+ additional_properties["width"] = width
1005
+ additional_properties["height"] = height
1006
+
1007
+
1008
+ def _image_dimensions_from_data_uri(uri: Any) -> tuple[int, int] | None:
1009
+ if not isinstance(uri, str):
1010
+ return None
1011
+ normalized_uri = uri.lower()
1012
+ marker = ";base64,"
1013
+ marker_index = normalized_uri.find(marker)
1014
+ if not normalized_uri.startswith("data:") or marker_index < 0:
1015
+ return None
1016
+ encoded = uri[marker_index + len(marker) :]
1017
+ try:
1018
+ data = base64.b64decode(encoded, validate=False)
1019
+ except binascii.Error, ValueError:
1020
+ return None
1021
+ try:
1022
+ return inspect_image_dimensions(data)
1023
+ except Exception:
1024
+ return None
1025
+
1026
+
1027
+ def _positive_int(value: Any) -> int | None:
1028
+ if isinstance(value, bool):
1029
+ return None
1030
+ if isinstance(value, int) and value > 0:
1031
+ return value
1032
+ if isinstance(value, str) and value.isdecimal():
1033
+ parsed = int(value)
1034
+ return parsed if parsed > 0 else None
1035
+ return None
1036
+
1037
+
1038
+ def annotate_token_counts(
1039
+ messages: list[Message],
1040
+ *,
1041
+ tokenizer: TokenizerProtocol,
1042
+ from_index: int | None = None,
1043
+ force_retokenize: bool = False,
1044
+ ) -> None:
1045
+ """Annotate token-count metadata, incrementally by default."""
1046
+ if not messages:
1047
+ return
1048
+
1049
+ # Token counts are stored inside group annotations.
1050
+ annotate_message_groups(messages, from_index=from_index)
1051
+
1052
+ if force_retokenize:
1053
+ start_index = 0
1054
+ elif from_index is not None:
1055
+ start_index = max(0, min(from_index, len(messages) - 1))
1056
+ else:
1057
+ first_untokenized_index = _first_untokenized_index(messages)
1058
+ if first_untokenized_index is None:
1059
+ return
1060
+ start_index = first_untokenized_index
1061
+
1062
+ for message in messages[start_index:]:
1063
+ _write_token_count(message, _message_token_estimate(message, tokenizer))
1064
+
1065
+
1066
+ def included_messages(messages: list[Message]) -> list[Message]:
1067
+ """Return the messages not marked as excluded, preserving their order and identity."""
1068
+ return [message for message in messages if not message.additional_properties.get(EXCLUDED_KEY, False)]
1069
+
1070
+
1071
+ def _carries_call_or_result(message: Message) -> bool:
1072
+ """Whether the message participates in the group's call/result pairing.
1073
+
1074
+ Narration-only members (assistant text fused between a call and its
1075
+ results) are wire-legal on their own; only carriers must project as an
1076
+ all-or-nothing set. Classification is content-scoped regardless of the
1077
+ kernel role carrying it: provider serializers emit call/result blocks by
1078
+ content (a user-role function_result still becomes a paired result block
1079
+ on the wire), so pairing atomicity must cover every carrier the group
1080
+ spans even when the exchange grammar treats the message as transcript
1081
+ input for boundary purposes."""
1082
+ return any(
1083
+ content.type in TOOL_CALL_CONTENT_TYPES or content.type in TOOL_RESULT_CONTENT_TYPES
1084
+ for content in message.contents
1085
+ )
1086
+
1087
+
1088
+ @dataclass(frozen=True, slots=True)
1089
+ class _PartialToolCallGroup:
1090
+ """One positional tool-call annotation occurrence with a partial exclusion.
1091
+
1092
+ ``degrade_indices`` are the absolute indices projection must exclude to
1093
+ restore atomicity: the group's call/result carriers plus its
1094
+ reasoning-bearing members (reasoning riders never float free of their
1095
+ calls). Plain narration members stay included."""
1096
+
1097
+ position: int
1098
+ degrade_indices: tuple[int, ...]
1099
+ group_id: str
1100
+ member_snapshot: tuple[weakref.ref[Message], ...]
1101
+ """Weak snapshot of the member Messages at discovery time.
1102
+
1103
+ Compared across the strategy await by :func:`_same_live_members` —
1104
+ order-insensitive, and dead members never compare equal, so a freed
1105
+ group can never vouch for a same-shaped newcomer at a recycled
1106
+ address.
1107
+ """
1108
+
1109
+
1110
+ def _same_live_members(
1111
+ left: tuple[weakref.ref[Message], ...],
1112
+ right: tuple[weakref.ref[Message], ...],
1113
+ ) -> bool:
1114
+ """Whether two member snapshots name the same set of LIVE referents.
1115
+
1116
+ Order-insensitive by design: today's semantics accept a strategy that
1117
+ merely reorders a partial group's messages, and an order-sensitive
1118
+ comparison would reclassify that as a newly introduced partial group.
1119
+ Any dead member fails the comparison outright.
1120
+ """
1121
+ left_members = [ref() for ref in left]
1122
+ right_members = [ref() for ref in right]
1123
+ if any(member is None for member in left_members) or any(member is None for member in right_members):
1124
+ return False
1125
+ return {id(member) for member in left_members} == {id(member) for member in right_members}
1126
+
1127
+
1128
+ def group_annotation_signature(message: Message) -> tuple[str, GroupKind, int, bool] | None:
1129
+ """Return the validated fields that delimit one contiguous annotation occurrence."""
1130
+ annotation = _read_group_annotation(message)
1131
+ if annotation is None:
1132
+ return None
1133
+ return (
1134
+ cast("str", annotation[GROUP_ID_KEY]),
1135
+ cast("GroupKind", annotation[GROUP_KIND_KEY]),
1136
+ cast("int", annotation[GROUP_INDEX_KEY]),
1137
+ cast("bool", annotation[GROUP_HAS_REASONING_KEY]),
1138
+ )
1139
+
1140
+
1141
+ def _partial_tool_call_groups(messages: Sequence[Message]) -> list[_PartialToolCallGroup]:
1142
+ """Find partially excluded tool-call groups without joining disjoint equal IDs."""
1143
+ partial_groups: list[_PartialToolCallGroup] = []
1144
+ index = 0
1145
+ position = 0
1146
+ while index < len(messages):
1147
+ signature = group_annotation_signature(messages[index])
1148
+ if signature is None:
1149
+ index += 1
1150
+ continue
1151
+
1152
+ # Adjacent but distinct occurrences can persist byte-identical
1153
+ # signatures; a tool-call group never resumes its assistant block after
1154
+ # its results, so an assistant message following the group's results
1155
+ # starts a new occurrence even under an unchanged signature — unless it
1156
+ # only carries results for the block. Results ride tool-role messages
1157
+ # or result-only assistant messages (hosted MCP), matching the group
1158
+ # annotator's own follower rule; other roles carrying result contents
1159
+ # are transcript input, never the group's output run.
1160
+ seen_results = messages[index].role == "tool" or is_result_only_message(messages[index], _LIVE_ACCESSOR)
1161
+ end_index = index + 1
1162
+ while end_index < len(messages) and group_annotation_signature(messages[end_index]) == signature:
1163
+ follower = messages[end_index]
1164
+ follower_is_result = follower.role == "tool" or is_result_only_message(follower, _LIVE_ACCESSOR)
1165
+ if seen_results and follower.role == "assistant" and not follower_is_result:
1166
+ break
1167
+ seen_results = seen_results or follower_is_result
1168
+ end_index += 1
1169
+
1170
+ group_id, kind, _group_index_value, _has_reasoning_value = signature
1171
+ if kind == "tool_call":
1172
+ # Atomic set: call/result carriers plus reasoning-bearing members
1173
+ # (a reasoning item must never outlive its exchange). Plain fused
1174
+ # narration is wire-legal alone and stays free in every group.
1175
+ atomic_indices = [
1176
+ i
1177
+ for i in range(index, end_index)
1178
+ if _carries_call_or_result(messages[i]) or _has_reasoning(messages[i])
1179
+ ]
1180
+ excluded = [bool(messages[i].additional_properties.get(EXCLUDED_KEY, False)) for i in atomic_indices]
1181
+ if any(excluded) and not all(excluded):
1182
+ members = messages[index:end_index]
1183
+ partial_groups.append(
1184
+ _PartialToolCallGroup(
1185
+ position=position,
1186
+ degrade_indices=tuple(atomic_indices),
1187
+ group_id=group_id,
1188
+ member_snapshot=tuple(weakref.ref(message) for message in members),
1189
+ )
1190
+ )
1191
+
1192
+ position += 1
1193
+ index = end_index
1194
+ return partial_groups
1195
+
1196
+
1197
+ def _degrade_partial_tool_call_groups(
1198
+ messages: list[Message],
1199
+ partial_groups: Sequence[_PartialToolCallGroup],
1200
+ ) -> None:
1201
+ """Exclude each partial group's surviving atomic members."""
1202
+ if not partial_groups:
1203
+ return
1204
+
1205
+ for group in partial_groups:
1206
+ for message_index in group.degrade_indices:
1207
+ message = messages[message_index]
1208
+ if not message.additional_properties.get(EXCLUDED_KEY, False):
1209
+ set_excluded(
1210
+ message,
1211
+ excluded=True,
1212
+ reason=_PROJECTION_ATOMICITY_EXCLUDE_REASON,
1213
+ )
1214
+
1215
+ logger.warning(
1216
+ "Degraded partially projected tool-call groups: positions=%s group_ids=%s",
1217
+ [group.position for group in partial_groups],
1218
+ [group.group_id for group in partial_groups],
1219
+ )
1220
+
1221
+
1222
+ def project_included_messages(messages: list[Message]) -> list[Message]:
1223
+ """Return model-input messages after atomically degrading partial tool-call groups."""
1224
+ _degrade_partial_tool_call_groups(
1225
+ messages,
1226
+ _partial_tool_call_groups(messages),
1227
+ )
1228
+ return included_messages(messages)
1229
+
1230
+
1231
+ def included_token_count(messages: list[Message]) -> int:
1232
+ """Sum included token counts; untokenized messages count as 0."""
1233
+ total = 0
1234
+ for message in included_messages(messages):
1235
+ token_count = _token_count(message)
1236
+ if token_count is not None:
1237
+ total += token_count
1238
+ return total
1239
+
1240
+
1241
+ def set_excluded(message: Message, *, excluded: bool, reason: str | None = None) -> bool:
1242
+ """Set the exclusion flag and return whether it changed.
1243
+
1244
+ A ``None`` reason leaves any previously recorded reason in place.
1245
+ """
1246
+ changed = bool(message.additional_properties.get(EXCLUDED_KEY, False)) != excluded
1247
+ if changed:
1248
+ message.additional_properties[EXCLUDED_KEY] = excluded
1249
+ if reason is not None:
1250
+ message.additional_properties[EXCLUDE_REASON_KEY] = reason
1251
+ return changed
1252
+
1253
+
1254
+ def exclude_group_ids(messages: list[Message], group_ids: set[str], *, reason: str) -> bool:
1255
+ """Exclude all messages whose annotation belongs to one of ``group_ids``."""
1256
+ changed = False
1257
+ for message in messages:
1258
+ group_id = _group_id(message)
1259
+ if group_id is not None and group_id in group_ids:
1260
+ changed = set_excluded(message, excluded=True, reason=reason) or changed
1261
+ return changed
1262
+
1263
+
1264
+ async def apply_compaction(
1265
+ messages: list[Message],
1266
+ *,
1267
+ strategy: CompactionStrategy | None,
1268
+ tokenizer: TokenizerProtocol | None = None,
1269
+ context: CompactionCallContext | None = None,
1270
+ strict_projection_atomicity: bool = False,
1271
+ ) -> list[Message]:
1272
+ """Apply configured compaction and return projected model-input messages.
1273
+
1274
+ The strategy is always called with both positional arguments
1275
+ (``strategy(messages, context)``); see the :class:`CompactionStrategy`
1276
+ breaking-change note for pre-context strategy signatures. Strict
1277
+ projection atomicity runs the strategy on a transactional copy and
1278
+ commits only when the strategy did not introduce a partial tool-call
1279
+ group. Partial groups already present before the strategy are degraded
1280
+ normally.
1281
+ """
1282
+ if strategy is None:
1283
+ return project_included_messages(messages)
1284
+
1285
+ working_messages = deepcopy(messages) if strict_projection_atomicity else messages
1286
+ annotate_message_groups(working_messages)
1287
+ if tokenizer is not None:
1288
+ annotate_token_counts(working_messages, tokenizer=tokenizer)
1289
+ preexisting_partial_snapshots = (
1290
+ [group.member_snapshot for group in _partial_tool_call_groups(working_messages)]
1291
+ if strict_projection_atomicity
1292
+ else []
1293
+ )
1294
+
1295
+ await strategy(working_messages, context)
1296
+
1297
+ partial_groups = _partial_tool_call_groups(working_messages)
1298
+ if strict_projection_atomicity:
1299
+ introduced_partial_groups = [
1300
+ group
1301
+ for group in partial_groups
1302
+ if not any(
1303
+ _same_live_members(group.member_snapshot, snapshot) for snapshot in preexisting_partial_snapshots
1304
+ )
1305
+ ]
1306
+ if introduced_partial_groups:
1307
+ raise CompactionProjectionAtomicityError(
1308
+ "Strict compaction rejected partially projected tool-call groups "
1309
+ f"at positions {[group.position for group in introduced_partial_groups]}"
1310
+ )
1311
+
1312
+ projected = project_included_messages(working_messages)
1313
+ if strict_projection_atomicity:
1314
+ messages[:] = working_messages
1315
+ return projected