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,1380 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """ResponseValidationMiddleware — retries on malformed agent responses.
4
+
5
+ Some LLM / inference backends (vLLM, custom routers, quantized model
6
+ weights) occasionally produce structurally invalid final responses:
7
+ empty content arrays, whitespace-only text, or text channels that leaked
8
+ tool-call markers like ``minimax:tool_call ... </minimax:tool_call>``.
9
+ These terminate the agent tool loop with a useless final message.
10
+
11
+ This :class:`ChatMiddleware` sits at the **innermost** position of the
12
+ chat middleware chain (closest to ``_inner_get_response``) so its
13
+ retries are invisible to the outer instrumentation — ``UsageTracking``,
14
+ ``SystemReminder``, and the loop-level ``LoopRecorder`` all fire exactly
15
+ once per turn regardless of how many retries happen here.
16
+
17
+ Strategy
18
+ --------
19
+ * Run the chat call via ``call_next()``.
20
+ * For streaming: return a lazy validating proxy. It emits one raw,
21
+ content-free heartbeat for every provider update while draining the inner
22
+ :class:`ResponseStream`, then finalises and validates. Transport errors
23
+ propagate unchanged so the outer
24
+ :class:`~chrys.foundation.retry.StreamRetryLoop` handles them.
25
+ * Validate the finalised :class:`ChatResponse` with a
26
+ :class:`ResponseValidator`.
27
+ * On a valid response, the proxy replays the buffered model-content
28
+ updates. Its finalizer returns the already-finalised response verbatim
29
+ so the inner provider's ``result_hooks`` (telemetry, usage) do NOT fire
30
+ twice.
31
+ * On a terminal, non-retryable invalid response, raise
32
+ :class:`TerminalResponseValidationError` so the TurnBindings observer records a
33
+ normal turn error and persists an error marker for replay. Retryable
34
+ verdicts flagged ``terminal_on_giveup`` (reasoning-only responses with
35
+ no visible answer) take the same raise once retries are exhausted —
36
+ their scrubbed response would be blank, and returning it would read as
37
+ a successful empty answer instead of the failure it is.
38
+ * In service-storage mode failures are never replayed in place: retryable
39
+ verdicts raise :class:`RetryableResponseValidationError` to the whole-run
40
+ retry boundary, with the same budget and identical-reason short-circuit
41
+ applied across those outer attempts (give-up is always terminal there).
42
+ * On an invalid local response, warn, sleep with exponential-ish backoff,
43
+ and ``call_next()`` again — up to :data:`MAX_RETRIES` additional raw
44
+ requests inside the same logical wire call.
45
+ * On the **final** attempt we return the exhausted response — but with
46
+ ``function_call`` contents stripped first. Without stripping, an
47
+ exhausted-but-still-malformed response carrying real ``function_call``
48
+ items would be executed by the tool loop, the loop would iterate, and
49
+ a model that's stuck in a bad state would re-trigger the full
50
+ validation retry cycle on every iteration (5 retries after 5
51
+ retries, indefinitely). Stripping forces ``_extract_function_calls``
52
+ to return ``[]`` so the tool loop exits cleanly with whatever (still-
53
+ bad) text we got.
54
+ * On **both** the valid and give-up paths we additionally drop
55
+ structurally-empty assistant outputs (``contents=[]`` or
56
+ whitespace-only-text messages, plus their corresponding updates).
57
+ ``DefaultResponseValidator`` only inspects the *final* assistant
58
+ message, so a response shaped like ``[empty, valid]`` would otherwise
59
+ pass validation with a stray ``content: []`` leading message that
60
+ lands in persisted ``session.json`` next to the engine's turn marker.
61
+ The update-level pass is group-aware (mirroring
62
+ ``_process_update``'s role / ``message_id`` boundary rules) so a
63
+ metadata-bearing terminal update sharing a non-empty group survives
64
+ and the rebuild keeps ``finish_reason`` / ``response_id``.
65
+ """
66
+
67
+ from __future__ import annotations
68
+
69
+ import asyncio
70
+ import inspect
71
+ import logging
72
+ import math
73
+ from collections.abc import Mapping
74
+ from dataclasses import dataclass
75
+ from typing import TYPE_CHECKING, Any, Protocol
76
+
77
+ from chrys.foundation.errors import ContinuationVerdictError, ProviderResponseError
78
+ from chrys.foundation.hosted_tools import (
79
+ HeldHostedEvidence,
80
+ HostedRetrySafety,
81
+ HostedToolPhase,
82
+ HostedToolStatus,
83
+ normalize_hosted_tool_status,
84
+ )
85
+ from chrys.foundation.retry import RetryAttemptInfo
86
+ from chrys.foundation.trajectory.event_types import ValidationReason
87
+ from chrys.kernel import (
88
+ ChatResponse,
89
+ ChatResponseUpdate,
90
+ ContextOverflowSink,
91
+ Message,
92
+ ResponseStream,
93
+ in_internal_side_call,
94
+ normalize_stream_usage,
95
+ resolve_storage_mode_and_handles,
96
+ )
97
+ from chrys.kernel.exchanges import (
98
+ TOOL_CALL_CONTENT_TYPES,
99
+ TOOL_RESULT_CONTENT_TYPES,
100
+ EmptyIdPolicy,
101
+ LiveAccessor,
102
+ NoneIdPolicy,
103
+ PairingKey,
104
+ PairingPolicy,
105
+ iter_exchanges,
106
+ namespaced_pairing_key,
107
+ pair_results,
108
+ )
109
+ from chrys.kernel.middleware import ChatMiddleware
110
+ from chrys.service.agent_middleware.events.hosted_tools import (
111
+ HostedToolArgsOp,
112
+ HostedToolProgressOp,
113
+ HostedToolResultOp,
114
+ HostedToolStartOp,
115
+ HostedToolStatusOp,
116
+ ResponsePresentationPlan,
117
+ adapt_hosted_tool,
118
+ )
119
+ from chrys.service.agent_middleware.validators import DefaultResponseValidator, ValidationResult
120
+ from chrys.service.trajectory.validation import ValidationTrace
121
+
122
+ if TYPE_CHECKING:
123
+ from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
124
+
125
+ from chrys.kernel.middleware import ChatContext
126
+ from chrys.service.agent_middleware.validators import ResponseValidator
127
+
128
+ logger = logging.getLogger(__name__)
129
+
130
+
131
+ # Maximum number of retries on top of the initial attempt. Three retries
132
+ # means up to four total LLM calls per turn in the worst case. On the
133
+ # final (fourth) attempt we return whatever we got, valid or not.
134
+ MAX_RETRIES = 3
135
+
136
+ # Backoff schedule (seconds) — indexed by attempt number. A missing
137
+ # slot reuses the last value. Deliberately short since the dominant
138
+ # cost is the LLM call itself, not the sleep.
139
+ DEFAULT_BACKOFF_SCHEDULE: tuple[float, ...] = (1.0, 2.0, 4.0, 8.0, 16.0)
140
+
141
+
142
+ class ResponseValidationError(Exception):
143
+ """Base for validation failures whose rejected attempt consumed billed tokens.
144
+
145
+ ``usage_details`` carries the failed attempt's provider usage so the
146
+ usage-tracking middleware can bill it before the exception continues to
147
+ its owner (the pass observer error path or the whole-run retry boundary).
148
+ """
149
+
150
+ def __init__(self, reason: str, *, usage_details: dict[str, Any] | None = None) -> None:
151
+ super().__init__(reason)
152
+ self.usage_details = usage_details
153
+
154
+
155
+ class TerminalResponseValidationError(ResponseValidationError, RuntimeError):
156
+ """Raised when response validation fails in a non-retryable way."""
157
+
158
+
159
+ @dataclass(frozen=True, slots=True)
160
+ class ValidationRetryExemption:
161
+ """Independent stored-mode validation retry budget metadata."""
162
+
163
+ attempt: int
164
+ max_attempts: int
165
+ delay_seconds: int
166
+
167
+
168
+ class RetryableResponseValidationError(ResponseValidationError, ConnectionError, ContinuationVerdictError):
169
+ """A stored response failed validation and must retry at the outer boundary.
170
+
171
+ ``hosted_commits`` names the provider-hosted tool calls (hosted MCP,
172
+ hosted shell) the rejected response already executed server-side. Those
173
+ exchanges never reach the loop recorder — the invalid response is
174
+ swallowed here before the kernel loop sees it — so this field is the
175
+ only carrier by which the whole-run retry owner can honour them as
176
+ commit points instead of re-creating the request and running the side
177
+ effects a second time.
178
+ """
179
+
180
+ # Read by the kernel's retry wrappers through the classifier: this failure
181
+ # judged a TERMINAL response, so a live continuation token now refers to a
182
+ # completed (immutable) response — a retry must issue a fresh request,
183
+ # never re-poll the same invalid completion.
184
+ invalidates_continuation_token = True
185
+
186
+ def __init__(
187
+ self,
188
+ reason: str,
189
+ *,
190
+ exemption: ValidationRetryExemption,
191
+ usage_details: dict[str, Any] | None = None,
192
+ hosted_commits: tuple[str, ...] = (),
193
+ ) -> None:
194
+ super().__init__(reason, usage_details=usage_details)
195
+ self.exemption = exemption
196
+ self.hosted_commits = hosted_commits
197
+
198
+
199
+ def hosted_commits_from_error(exc: BaseException) -> tuple[str, ...]:
200
+ """Hosted tool calls the failed attempt behind *exc* already executed."""
201
+ if isinstance(exc, RetryableResponseValidationError):
202
+ return exc.hosted_commits
203
+ return ()
204
+
205
+
206
+ class ResponseValidationObservationHook(Protocol):
207
+ """Optional observer for raw content and validation-attempt boundaries."""
208
+
209
+ async def begin_response(self, *, response_index: int | None = None, batch_id: int | None = None) -> None: ...
210
+
211
+ async def observe_contents(self, contents: Sequence[Any], *, is_final: bool = False) -> None: ...
212
+
213
+ async def attempt_started(self, *, continuation: bool = False) -> None: ...
214
+
215
+ async def attempt_rejected(self, reason: str = "") -> None: ...
216
+
217
+ async def attempt_accepted(self, messages: Sequence[Message]) -> None: ...
218
+
219
+
220
+ _LIVE_ACCESSOR = LiveAccessor()
221
+
222
+
223
+ def _hosted_commit_labels(response: ChatResponse) -> tuple[str, ...]:
224
+ """Deduplicated display names of side-effectful hosted calls in *response*."""
225
+ labels = _hosted_commit_labels_from_plan(ResponsePresentationPlan.from_messages(response.messages or []))
226
+ if labels:
227
+ return labels
228
+ return _hosted_commit_labels_from_contents(
229
+ content for message in response.messages or [] for content in message.contents
230
+ )
231
+
232
+
233
+ def _hosted_commit_labels_from_contents(
234
+ candidates: Iterable[Any],
235
+ call_labels: Mapping[str, str] | None = None,
236
+ ) -> tuple[str, ...]:
237
+ """Deduplicated display names of side-effectful hosted calls in *candidates*."""
238
+ labels: list[str] = []
239
+ for label, _key in _hosted_commits_from_contents(candidates, call_labels):
240
+ if label not in labels:
241
+ labels.append(label)
242
+ return tuple(labels)
243
+
244
+
245
+ def _hosted_commits_from_contents(
246
+ candidates: Iterable[Any],
247
+ call_labels: Mapping[str, str] | None = None,
248
+ ) -> list[tuple[str, PairingKey | None]]:
249
+ """Display name and pairing key of each side-effectful hosted call content in *candidates*.
250
+
251
+ Result contents carry no tool name of their own, so each is labelled
252
+ via its paired call when one is present in the same batch.
253
+ """
254
+ contents = [content for content in candidates if content.provider_hosted]
255
+ names_by_call = dict(call_labels or {})
256
+ names_by_call.update(
257
+ {
258
+ content.call_id: content.tool_name
259
+ for content in contents
260
+ if content.type in TOOL_CALL_CONTENT_TYPES and content.call_id and content.tool_name
261
+ }
262
+ )
263
+ commits: list[tuple[str, PairingKey | None]] = []
264
+ for content in contents:
265
+ if content.type in TOOL_CALL_CONTENT_TYPES:
266
+ view = adapt_hosted_tool(content)
267
+ elif content.type in TOOL_RESULT_CONTENT_TYPES:
268
+ view = adapt_hosted_tool(None, content)
269
+ else:
270
+ continue
271
+ if not _view_is_hosted_commit(view):
272
+ continue
273
+ label = view.tool_name
274
+ if content.type in TOOL_RESULT_CONTENT_TYPES:
275
+ label = names_by_call.get(content.call_id) or content.tool_name or content.type
276
+ commits.append((label, namespaced_pairing_key(content.type, _LIVE_ACCESSOR.raw_id(content))))
277
+ return commits
278
+
279
+
280
+ # Recoverable hosted work: a call counts once a result with its id answers it; an id-less occurrence
281
+ # answers nothing, so its call is never counted as kept.
282
+ _KEPT_HOSTED_PAIRING_POLICY = PairingPolicy(
283
+ call_types=TOOL_CALL_CONTENT_TYPES,
284
+ include_informational_calls=True,
285
+ result_types=TOOL_RESULT_CONTENT_TYPES,
286
+ none_id=NoneIdPolicy.IGNORE,
287
+ empty_id=EmptyIdPolicy.IGNORE,
288
+ malformed_id="treat_as_none",
289
+ )
290
+
291
+
292
+ def _landed_exchange_answered_keys(messages: Sequence[Message], landed: Sequence[Message]) -> set[PairingKey]:
293
+ """Pairing keys answered by the exchange of *messages* whose response holds *landed*'s Content objects.
294
+
295
+ Identity, not ids, finds the exchange: an earlier exchange reusing the same call id is other work.
296
+ """
297
+ landed_contents = {id(content) for message in landed for content in message.contents}
298
+ for exchange in reversed(list(iter_exchanges(messages, _LIVE_ACCESSOR))):
299
+ if any(
300
+ id(content) in landed_contents
301
+ for index in exchange.response_indices
302
+ for content in messages[index].contents
303
+ ):
304
+ return pair_results(messages, exchange, _LIVE_ACCESSOR, _KEPT_HOSTED_PAIRING_POLICY).answered_keys
305
+ return set()
306
+
307
+
308
+ def _view_is_hosted_commit(view: Any) -> bool:
309
+ """Return whether one adapted view proves retry-blocking execution."""
310
+ if view.retry_safety not in {HostedRetrySafety.SIDE_EFFECTFUL, HostedRetrySafety.UNKNOWN}:
311
+ return False
312
+ status = normalize_hosted_tool_status(view.status)
313
+ return view.phase in {
314
+ HostedToolPhase.START,
315
+ HostedToolPhase.DELTA,
316
+ HostedToolPhase.SNAPSHOT,
317
+ HostedToolPhase.TERMINAL,
318
+ } or status in {
319
+ HostedToolStatus.RUNNING,
320
+ HostedToolStatus.COMPLETED,
321
+ HostedToolStatus.FAILED,
322
+ HostedToolStatus.INTERRUPTED,
323
+ }
324
+
325
+
326
+ def _hosted_commit_labels_from_plan(plan: ResponsePresentationPlan) -> tuple[str, ...]:
327
+ """Return retry-blocking labels from exchange-paired presentation views."""
328
+ labels: list[str] = []
329
+ for operation in plan.operations:
330
+ if not isinstance(
331
+ operation,
332
+ HostedToolStartOp | HostedToolArgsOp | HostedToolProgressOp | HostedToolStatusOp | HostedToolResultOp,
333
+ ):
334
+ continue
335
+ view = operation.view
336
+ if not _view_is_hosted_commit(view):
337
+ continue
338
+ label = view.tool_name or view.family or "hosted_tool"
339
+ if label not in labels:
340
+ labels.append(label)
341
+ return tuple(labels)
342
+
343
+
344
+ def _note_output_truncated(context: ChatContext, verdict: ValidationResult, response: ChatResponse) -> None:
345
+ """Tell the compaction strategy when the input left the reply no room.
346
+
347
+ A length stop with nothing generated stays terminal; the next request
348
+ compacts first. A reasoning-only length stop is not this: the model did
349
+ generate, so the window was not full. Neither is an empty reply whose
350
+ usage counts reasoning tokens the API did not return.
351
+ """
352
+ if verdict.code != ValidationReason.OUTPUT_TRUNCATED:
353
+ return
354
+ if (response.usage_details or {}).get("reasoning_output_token_count"):
355
+ return
356
+ strategy = context.kwargs.get("compaction_strategy")
357
+ if isinstance(strategy, ContextOverflowSink):
358
+ strategy.note_context_overflow()
359
+
360
+
361
+ class ResponseValidationMiddleware(ChatMiddleware):
362
+ """Retry the LLM call on structurally invalid final responses.
363
+
364
+ Retries assume the failure is *transient* — random sampling produced
365
+ a one-off bad output that a re-roll will fix. When the producer is
366
+ *deterministically* stuck (chat-template / KV-cache mismatch, fine-
367
+ tuning quirk, prefix-cache hit at temperature 0), every retry sees
368
+ the same input and returns the same bad output, so retrying just
369
+ burns latency before the inevitable give-up. To avoid that, this
370
+ middleware fails fast: when attempt N+1's
371
+ :attr:`ValidationResult.reason` exactly matches attempt N's, the
372
+ loop short-circuits to the give-up path immediately instead of
373
+ completing the remaining retries.
374
+
375
+ In service-storage mode a failed attempt is never replayed in place —
376
+ it raises to the whole-run retry boundary instead — so the retry
377
+ budget and the identical-reason short-circuit are carried on the
378
+ instance across those outer attempts; once either trips, the failure
379
+ is raised terminally.
380
+
381
+ Class attributes :attr:`_MAX_RETRIES` and :attr:`_BACKOFF_SCHEDULE`
382
+ hold defaults that can be monkeypatched in tests (mirrors the pattern
383
+ used by :class:`~chrys.orchestration.invoker.attempts.AttemptRunner`). Constructor arguments
384
+ override these defaults per-instance.
385
+
386
+ Parameters
387
+ ----------
388
+ validator:
389
+ Component that inspects each :class:`ChatResponse` and returns
390
+ :class:`~chrys.service.agent_middleware.validators.ValidationResult`.
391
+ Defaults to :class:`DefaultResponseValidator` with the three
392
+ built-in rules (empty contents, whitespace text, leaked tool
393
+ calls).
394
+ max_retries:
395
+ Maximum number of retries on top of the initial attempt.
396
+ ``None`` → use :attr:`_MAX_RETRIES` (default :data:`MAX_RETRIES`).
397
+ backoff_schedule:
398
+ Sleep duration (seconds) between attempts, indexed by attempt
399
+ number. Past the last entry the last value is reused. ``None``
400
+ → use :attr:`_BACKOFF_SCHEDULE` (default
401
+ :data:`DEFAULT_BACKOFF_SCHEDULE`).
402
+ publish_retry:
403
+ Optional observer fired before each validation replay sleep. Production
404
+ orchestration leaves this unset: logical wire-retry events have one
405
+ publisher, the kernel policy's ``on_retry`` callback.
406
+ observation_hook:
407
+ Optional raw-content and attempt-lifecycle observer. Callback failures
408
+ are isolated from validation and hosted commit-point tracking.
409
+ """
410
+
411
+ # Class-level defaults so tests can monkeypatch via
412
+ # ``monkeypatch.setattr(ResponseValidationMiddleware, "_BACKOFF_SCHEDULE", (0,))``
413
+ # without having to plumb constructor kwargs through every test fixture.
414
+ _MAX_RETRIES: int = MAX_RETRIES
415
+ _BACKOFF_SCHEDULE: tuple[float, ...] = DEFAULT_BACKOFF_SCHEDULE
416
+
417
+ def __init__(
418
+ self,
419
+ *,
420
+ validator: ResponseValidator | None = None,
421
+ max_retries: int | None = None,
422
+ backoff_schedule: Sequence[float] | None = None,
423
+ publish_retry: Callable[[RetryAttemptInfo], Awaitable[None]] | None = None,
424
+ observation_hook: ResponseValidationObservationHook | None = None,
425
+ ) -> None:
426
+ self._validator: ResponseValidator = validator or DefaultResponseValidator()
427
+ self._max_retries = max(0, int(max_retries if max_retries is not None else type(self)._MAX_RETRIES))
428
+ schedule = backoff_schedule if backoff_schedule is not None else type(self)._BACKOFF_SCHEDULE
429
+ self._backoff = tuple(schedule) or (0.0,)
430
+ self._publish_retry = publish_retry
431
+ self._observation_hook = observation_hook
432
+ # Service-storage failures retry at the whole-run boundary, so each
433
+ # outer attempt re-enters :meth:`process` with fresh loop state. The
434
+ # instance outlives those attempts; carrying the count and last reason
435
+ # here keeps the retry budget and the deterministic-failure
436
+ # short-circuit effective in that mode.
437
+ self._service_retry_count = 0
438
+ self._service_retry_reason: str | None = None
439
+ # Provider-hosted tool calls observed executing (hosted MCP, hosted
440
+ # shell) at two scopes matched to what each retry owner discards:
441
+ # the run scope backs the whole-run gates (a whole-run retry restores
442
+ # pre-run history, so ANY hosted work this run would re-execute); the
443
+ # wire-attempt scope backs the kernel's per-wire replay veto (an
444
+ # in-place replay only re-sends the current request, so only hosted
445
+ # work from the aborted attempt itself is at risk — an attempt being
446
+ # the request that started a response plus the polls that resume it).
447
+ # The attempt's commits also keep their pairing keys, so a pass
448
+ # resumed from repaired history can tell which ones that history
449
+ # already answers.
450
+ self._hosted_commits_seen: list[str] = []
451
+ self._hosted_commits_in_flight: list[str] = []
452
+ self._hosted_commit_keys_in_flight: list[tuple[str, PairingKey | None]] = []
453
+ self._hosted_call_labels_in_flight: dict[str, str] = {}
454
+
455
+ def set_observation_hook(self, hook: ResponseValidationObservationHook | None) -> None:
456
+ """Attach the run-scoped hosted-presentation observer."""
457
+ self._observation_hook = hook
458
+
459
+ # ------------------------------------------------------------------
460
+ # Hosted-commit observation (retry-owner probes)
461
+ # ------------------------------------------------------------------
462
+
463
+ def hosted_commits_observed(self) -> tuple[str, ...]:
464
+ """Hosted tool calls executed since the retry baseline (run/pass start)."""
465
+ return tuple(self._hosted_commits_seen)
466
+
467
+ def hosted_commits_in_flight(self) -> tuple[str, ...]:
468
+ """Hosted tool calls executed within the current wire attempt, polls that resumed it included."""
469
+ return tuple(self._hosted_commits_in_flight)
470
+
471
+ def hosted_commits_in_flight_unkept(
472
+ self, kept: Sequence[Message], landed: Sequence[Message] | None
473
+ ) -> tuple[str, ...]:
474
+ """Hosted tool calls of the current wire attempt that *kept* does not answer.
475
+
476
+ *kept* is the history a resumed pass sends; *landed* is the attempt's response as the tool loop
477
+ landed it, ``None`` while the request is still in flight. Only the exchange holding that response
478
+ counts: its answered hosted calls show the model their results instead of running again. A call
479
+ without a pairing id never counts as kept.
480
+ """
481
+ answered = set() if landed is None else _landed_exchange_answered_keys(kept, landed)
482
+ labels: list[str] = []
483
+ for label, key in self._hosted_commit_keys_in_flight:
484
+ if (key is None or key not in answered) and label not in labels:
485
+ labels.append(label)
486
+ return tuple(labels)
487
+
488
+ def reset_hosted_commit_observations(self) -> None:
489
+ """Forget observed hosted work — fired when the retry baseline resets."""
490
+ self._hosted_commits_seen.clear()
491
+ self._hosted_commits_in_flight.clear()
492
+ self._hosted_commit_keys_in_flight.clear()
493
+ self._hosted_call_labels_in_flight.clear()
494
+
495
+ def begin_pass_hosted_baseline(self, *, resumes_background_response: bool) -> None:
496
+ """Start a pass's hosted-work baseline.
497
+
498
+ A pass that polls a background response an earlier pass left running keeps the observations: that
499
+ response's hosted calls may already have run, and if the poll ends without the response, a fresh
500
+ request would run them again.
501
+ """
502
+ if not resumes_background_response:
503
+ self.reset_hosted_commit_observations()
504
+
505
+ def _begin_wire_attempt(self, *, continuation: bool) -> None:
506
+ # A poll resumes the response an earlier attempt started: the hosted
507
+ # work that response ran stays at risk until it ends, since a fresh
508
+ # request would run it again.
509
+ if continuation:
510
+ return
511
+ self._hosted_commits_in_flight.clear()
512
+ self._hosted_commit_keys_in_flight.clear()
513
+ self._hosted_call_labels_in_flight.clear()
514
+
515
+ def _observe_hosted_contents(self, contents: Iterable[Any]) -> None:
516
+ observed = list(contents)
517
+ self._hosted_call_labels_in_flight.update(
518
+ {
519
+ content.call_id: content.tool_name
520
+ for content in observed
521
+ if content.provider_hosted
522
+ and content.type in TOOL_CALL_CONTENT_TYPES
523
+ and content.call_id
524
+ and content.tool_name
525
+ }
526
+ )
527
+ for commit in _hosted_commits_from_contents(observed, self._hosted_call_labels_in_flight):
528
+ label = commit[0]
529
+ if label not in self._hosted_commits_seen:
530
+ self._hosted_commits_seen.append(label)
531
+ if label not in self._hosted_commits_in_flight:
532
+ self._hosted_commits_in_flight.append(label)
533
+ if commit not in self._hosted_commit_keys_in_flight:
534
+ self._hosted_commit_keys_in_flight.append(commit)
535
+
536
+ async def _observe_failed_response(self, error: ProviderResponseError) -> None:
537
+ """Count the hosted work a failed response showed but never yielded.
538
+
539
+ The adapter raised before the response landed, so this is the only
540
+ place the retry gates learn that the work already ran.
541
+ """
542
+ if contents := list(error.observed_contents):
543
+ self._observe_hosted_contents(contents)
544
+ await self._notify_contents_observed(contents, is_final=True)
545
+
546
+ async def _notify_contents_observed(self, contents: Sequence[Any], *, is_final: bool) -> None:
547
+ if self._observation_hook is None or in_internal_side_call():
548
+ return
549
+ try:
550
+ await self._observation_hook.observe_contents(contents, is_final=is_final)
551
+ except Exception:
552
+ logger.exception("response validation observation hook raised while observing contents")
553
+
554
+ async def _notify_response_started(self, *, continuation: bool) -> None:
555
+ if self._observation_hook is None or in_internal_side_call() or continuation:
556
+ return
557
+ try:
558
+ await self._observation_hook.begin_response()
559
+ except Exception:
560
+ logger.exception("response validation observation hook raised at response start")
561
+
562
+ async def _notify_attempt_started(self, *, continuation: bool = False) -> None:
563
+ if self._observation_hook is None or in_internal_side_call():
564
+ return
565
+ try:
566
+ await self._observation_hook.attempt_started(continuation=continuation)
567
+ except Exception:
568
+ logger.exception("response validation observation hook raised at attempt start")
569
+
570
+ async def _notify_attempt_rejected(self, reason: str) -> None:
571
+ if self._observation_hook is None or in_internal_side_call():
572
+ return
573
+ try:
574
+ await self._observation_hook.attempt_rejected(reason)
575
+ except Exception:
576
+ logger.exception("response validation observation hook raised at attempt rejection")
577
+
578
+ async def _notify_attempt_accepted(self, messages: Sequence[Message]) -> None:
579
+ if self._observation_hook is None or in_internal_side_call():
580
+ return
581
+ try:
582
+ await self._observation_hook.attempt_accepted(messages)
583
+ except Exception:
584
+ logger.exception("response validation observation hook raised at attempt acceptance")
585
+
586
+ # ------------------------------------------------------------------
587
+ # ChatMiddleware interface
588
+ # ------------------------------------------------------------------
589
+
590
+ async def process(
591
+ self,
592
+ context: ChatContext,
593
+ call_next: Callable[[], Awaitable[None]],
594
+ ) -> None:
595
+ try:
596
+ stores_by_default = bool(context.client.STORES_BY_DEFAULT)
597
+ except AttributeError:
598
+ stores_by_default = False
599
+ try:
600
+ force_stateless = bool(context.client.FORCES_STATELESS)
601
+ except AttributeError:
602
+ force_stateless = False
603
+ nested_client_kwargs = context.kwargs.get("client_kwargs")
604
+ storage_kwargs = nested_client_kwargs if isinstance(nested_client_kwargs, Mapping) else context.kwargs
605
+ service_side = resolve_storage_mode_and_handles(
606
+ context.options,
607
+ stores_by_default=stores_by_default,
608
+ client_kwargs=storage_kwargs,
609
+ force_stateless=force_stateless,
610
+ ).service_side
611
+ continuation = isinstance(context.options, Mapping) and context.options.get("continuation_token") is not None
612
+ await self._notify_response_started(continuation=continuation)
613
+ validation = ValidationTrace.open(context.kwargs)
614
+ if context.stream:
615
+ # Return a lazy validating proxy immediately so raw, content-free provider
616
+ # heartbeats can reach the outer stream-idle watchdog during the inner drain.
617
+ # All model content remains buffered until its finalized response validates.
618
+ context.result = self._validated_response_stream(
619
+ context, call_next, service_side=service_side, validation=validation
620
+ )
621
+ return
622
+
623
+ # Total attempts = 1 initial + max_retries retries. The loop
624
+ # variable 0..max_retries tracks the retry count; attempt=0 is
625
+ # the initial call.
626
+ #
627
+ # ``previous_reason`` carries the prior attempt's
628
+ # :attr:`ValidationResult.reason` so :meth:`_should_give_up` can
629
+ # detect a deterministic-stuck producer (same failure mode twice
630
+ # in a row) and short-circuit to give-up instead of burning the
631
+ # remaining retries on what will almost certainly be the same
632
+ # bad output.
633
+ previous_reason: str | None = None
634
+ for attempt in range(self._max_retries + 1):
635
+ continuation = (
636
+ isinstance(context.options, Mapping) and context.options.get("continuation_token") is not None
637
+ )
638
+ self._begin_wire_attempt(continuation=continuation)
639
+ await self._notify_attempt_started(continuation=continuation)
640
+ try:
641
+ await call_next()
642
+ except ProviderResponseError as error:
643
+ await self._observe_failed_response(error)
644
+ raise
645
+ result = context.result
646
+ if not isinstance(result, ChatResponse):
647
+ return
648
+ observed_contents = [content for message in result.messages or [] for content in message.contents]
649
+ self._observe_hosted_contents(observed_contents)
650
+ await self._notify_contents_observed(observed_contents, is_final=True)
651
+ if result.continuation_token is not None:
652
+ await self._notify_attempt_accepted(result.messages or [])
653
+ return
654
+
655
+ verdict = self._validator.validate(result)
656
+ _note_output_truncated(context, verdict, result)
657
+ hosted_commits = () if verdict.ok else _hosted_commit_labels(result)
658
+ give_up = not verdict.ok and self._gives_up(
659
+ verdict,
660
+ attempt=attempt,
661
+ previous_reason=previous_reason,
662
+ hosted_committed=bool(hosted_commits),
663
+ service_side=service_side,
664
+ )
665
+ if validation is not None:
666
+ await validation.finished(
667
+ accepted=verdict.ok,
668
+ reason_code=None if verdict.ok else verdict.code,
669
+ retryable=None if verdict.ok else verdict.retryable,
670
+ gave_up=give_up,
671
+ )
672
+ if verdict.ok:
673
+ self.reset_service_retry_state()
674
+ # Even on a valid response, drop structurally-empty
675
+ # assistant messages. The default validator only
676
+ # inspects the FINAL assistant message, so a response
677
+ # shaped like ``[empty, valid]`` passes validation
678
+ # while still carrying a stray ``content: []`` message
679
+ # that would otherwise land in persisted history.
680
+ # Streaming providers/layers can also create empty
681
+ # assistant placeholders at message_id boundaries that
682
+ # never get populated — same fix.
683
+ _drop_empty_assistant_outputs(result, [])
684
+ await self._notify_attempt_accepted(result.messages or [])
685
+ return
686
+ if service_side:
687
+ await self._notify_attempt_rejected(verdict.reason)
688
+ self._service_side_failure(verdict.reason, result, code=verdict.code, give_up=give_up)
689
+ if hosted_commits:
690
+ # The rejected response already executed provider-hosted tool
691
+ # calls server-side; replaying the request in place would run
692
+ # those side effects a second time. Give up now — the scrubbed
693
+ # response keeps the hosted transcript in history.
694
+ logger.warning(
695
+ "Response validation would retry, but provider-hosted tool call(s) "
696
+ "already executed (%s) — not re-sending the request: %s",
697
+ ", ".join(hosted_commits),
698
+ verdict.reason,
699
+ )
700
+ if give_up:
701
+ # A concluded cycle — terminal raise or scrubbed acceptance —
702
+ # must not leave stale service-side carry-over behind a later
703
+ # storage-mode flip.
704
+ self.reset_service_retry_state()
705
+ if not verdict.retryable or verdict.terminal_on_giveup:
706
+ await self._notify_attempt_rejected(verdict.reason)
707
+ self._raise_terminal(verdict.reason, result, code=verdict.code)
708
+ # Give-up path: scrub the exhausted response so neither
709
+ # the tool loop NOR persisted history is
710
+ # polluted by the malformed output:
711
+ #
712
+ # * function_call contents are stripped so
713
+ # ``_extract_function_calls`` returns ``[]`` and the
714
+ # loop exits cleanly (without this the model could
715
+ # re-trigger the full validation cycle every iteration).
716
+ # * empty / whitespace-only assistant messages are
717
+ # dropped (function_call stripping may itself empty a
718
+ # message — drop those too) so they don't land in
719
+ # ``session.json`` next to the engine's turn marker
720
+ # as a stray ``content: []`` message.
721
+ _strip_function_calls(result, [])
722
+ _drop_empty_assistant_outputs(result, [])
723
+ await self._notify_attempt_accepted(result.messages or [])
724
+ return
725
+
726
+ # Invalid + retries remaining: emit retry event + sleep.
727
+ previous_reason = verdict.reason
728
+ await self._notify_attempt_rejected(verdict.reason)
729
+ await self._note_retry(verdict.reason, attempt, validation=validation)
730
+
731
+ def _validated_response_stream(
732
+ self,
733
+ context: ChatContext,
734
+ call_next: Callable[[], Awaitable[None]],
735
+ *,
736
+ service_side: bool,
737
+ validation: ValidationTrace | None = None,
738
+ ) -> ResponseStream[ChatResponseUpdate, ChatResponse]:
739
+ """Validate lazily while emitting safe transport progress for every update."""
740
+ final_response: ChatResponse | None = None
741
+ # The inner stream of the attempt being read; None once that attempt
742
+ # was accepted or closed for a retry.
743
+ active_result: ResponseStream[ChatResponseUpdate, ChatResponse] | None = None
744
+ proxy: ResponseStream[ChatResponseUpdate, ChatResponse]
745
+
746
+ def _select_response(
747
+ response: ChatResponse,
748
+ result_hooks: Sequence[Callable[[ChatResponse], Any]],
749
+ ) -> None:
750
+ nonlocal final_response
751
+ final_response = response
752
+ context.result = proxy
753
+ # The provider/inner hooks originally finalize before hooks installed
754
+ # by outer middleware. Outer hooks are already present on the lazy
755
+ # proxy by the time this attempt succeeds, so prepend the inner phase.
756
+ proxy._result_hooks[:0] = result_hooks
757
+
758
+ async def _updates() -> AsyncIterator[ChatResponseUpdate]:
759
+ nonlocal active_result
760
+ previous_reason: str | None = None
761
+ for attempt in range(self._max_retries + 1):
762
+ continuation = (
763
+ isinstance(context.options, Mapping) and context.options.get("continuation_token") is not None
764
+ )
765
+ self._begin_wire_attempt(continuation=continuation)
766
+ await self._notify_attempt_started(continuation=continuation)
767
+ updates: list[ChatResponseUpdate] = []
768
+ try:
769
+ await call_next()
770
+ result = context.result
771
+ if not isinstance(result, ResponseStream):
772
+ raise ValueError("Streaming response validation requires a ResponseStream result.")
773
+ active_result = result
774
+
775
+ await result
776
+ inner_hooks = _collect_and_clear_hooks(result)
777
+ async for update in result:
778
+ if isinstance(evidence := update.raw_representation, HeldHostedEvidence):
779
+ # Hosted work the adapter holds behind an
780
+ # unfinished call already ran: the retry gates
781
+ # count it now. The update that releases it in
782
+ # order presents it and keeps it for the replay.
783
+ self._observe_hosted_contents(evidence.contents)
784
+ yield _stream_heartbeat_for(update)
785
+ continue
786
+ # Record hosted executions the moment their updates
787
+ # land — a stall or transport drop after this point
788
+ # must reach the retry owners as commit evidence even
789
+ # though the buffered content never validated.
790
+ observed_contents = list(update.contents or [])
791
+ self._observe_hosted_contents(observed_contents)
792
+ await self._notify_contents_observed(observed_contents, is_final=False)
793
+ updates.append(update)
794
+ yield _stream_heartbeat_for(update)
795
+ except ProviderResponseError as error:
796
+ await self._observe_failed_response(error)
797
+ raise
798
+ final = await _preview_finalize(result, updates)
799
+ final_contents = [content for message in final.messages or [] for content in message.contents]
800
+ self._observe_hosted_contents(final_contents)
801
+ await self._notify_contents_observed(final_contents, is_final=True)
802
+
803
+ if final.continuation_token is not None:
804
+ _select_response(final, inner_hooks)
805
+ await self._notify_attempt_accepted(final.messages or [])
806
+ active_result = None
807
+ return
808
+
809
+ verdict = self._validator.validate(final)
810
+ _note_output_truncated(context, verdict, final)
811
+ hosted_commits = () if verdict.ok else _hosted_commit_labels(final)
812
+ give_up = not verdict.ok and self._gives_up(
813
+ verdict,
814
+ attempt=attempt,
815
+ previous_reason=previous_reason,
816
+ hosted_committed=bool(hosted_commits),
817
+ service_side=service_side,
818
+ )
819
+ if validation is not None:
820
+ await validation.finished(
821
+ accepted=verdict.ok,
822
+ reason_code=None if verdict.ok else verdict.code,
823
+ retryable=None if verdict.ok else verdict.retryable,
824
+ gave_up=give_up,
825
+ )
826
+ if verdict.ok:
827
+ self.reset_service_retry_state()
828
+ _drop_empty_assistant_outputs(final, updates)
829
+ _select_response(final, inner_hooks)
830
+ await self._notify_attempt_accepted(final.messages or [])
831
+ for update in updates:
832
+ if not _is_stream_heartbeat(update):
833
+ yield update
834
+ active_result = None
835
+ return
836
+
837
+ if service_side:
838
+ await self._notify_attempt_rejected(verdict.reason)
839
+ self._service_side_failure(
840
+ verdict.reason,
841
+ final,
842
+ code=verdict.code,
843
+ give_up=give_up,
844
+ stream_usage_chunks=_usage_chunks_from_updates(updates),
845
+ )
846
+ if hosted_commits:
847
+ # Same rule as the blocking path: hosted side effects
848
+ # in the rejected response forbid an in-place re-send.
849
+ logger.warning(
850
+ "Response validation would retry, but provider-hosted tool call(s) "
851
+ "already executed (%s) — not re-sending the request: %s",
852
+ ", ".join(hosted_commits),
853
+ verdict.reason,
854
+ )
855
+ if give_up:
856
+ # A concluded cycle — terminal raise or scrubbed
857
+ # acceptance — must not leave stale service-side
858
+ # carry-over behind a later storage-mode flip.
859
+ self.reset_service_retry_state()
860
+ if not verdict.retryable or verdict.terminal_on_giveup:
861
+ await self._notify_attempt_rejected(verdict.reason)
862
+ self._raise_terminal(
863
+ verdict.reason,
864
+ final,
865
+ code=verdict.code,
866
+ stream_usage_chunks=_usage_chunks_from_updates(updates),
867
+ )
868
+ _strip_function_calls(final, updates)
869
+ _drop_empty_assistant_outputs(final, updates)
870
+ _select_response(final, inner_hooks)
871
+ await self._notify_attempt_accepted(final.messages or [])
872
+ for update in updates:
873
+ if not _is_stream_heartbeat(update):
874
+ yield update
875
+ active_result = None
876
+ return
877
+
878
+ previous_reason = verdict.reason
879
+ await self._notify_attempt_rejected(verdict.reason)
880
+ await result.aclose()
881
+ active_result = None
882
+ await self._note_retry(verdict.reason, attempt, validation=validation)
883
+
884
+ async def _close_active_result() -> None:
885
+ # A proxy left mid-attempt closes the inner stream that attempt
886
+ # was reading: after a failed or cancelled pull, and on an
887
+ # explicit close. It is the proxy's cleanup hook, never the
888
+ # generator's exit: finalization closes each abandoned generator
889
+ # in a task of its own, and an exit closing another generator
890
+ # would collide with that generator's own close.
891
+ nonlocal active_result
892
+ reading, active_result = active_result, None
893
+ if reading is not None:
894
+ await reading.aclose()
895
+
896
+ def _finalizer(_updates: Sequence[ChatResponseUpdate]) -> ChatResponse:
897
+ if final_response is None:
898
+ raise RuntimeError("Streaming response validation ended without a final response.")
899
+ return final_response
900
+
901
+ proxy = ResponseStream(_updates(), finalizer=_finalizer).with_cleanup_hook(_close_active_result)
902
+ return proxy
903
+
904
+ # ------------------------------------------------------------------
905
+ # Internal helpers
906
+ # ------------------------------------------------------------------
907
+
908
+ def _is_final_attempt(self, attempt: int) -> bool:
909
+ return attempt >= self._max_retries
910
+
911
+ def _gives_up(
912
+ self,
913
+ verdict: ValidationResult,
914
+ *,
915
+ attempt: int,
916
+ previous_reason: str | None,
917
+ hosted_committed: bool,
918
+ service_side: bool,
919
+ ) -> bool:
920
+ """Whether this rejected verdict ends the cycle instead of retrying again.
921
+
922
+ Service-storage mode cannot replay in place — the whole run retries at
923
+ the pass boundary — so its budget is the instance-carried count and
924
+ reason rather than this loop's. It is the same decision either way, and
925
+ taking it here is what lets the recorded verdict state it.
926
+ """
927
+ if service_side:
928
+ return self._should_give_up(
929
+ self._service_retry_count, verdict.reason, self._service_retry_reason, retryable=verdict.retryable
930
+ )
931
+ return hosted_committed or self._should_give_up(
932
+ attempt, verdict.reason, previous_reason, retryable=verdict.retryable
933
+ )
934
+
935
+ def _should_give_up(
936
+ self, attempt: int, reason: str, previous_reason: str | None, *, retryable: bool = True
937
+ ) -> bool:
938
+ """Stop retrying — non-retryable, out of attempts, or the producer is stuck.
939
+
940
+ Three short-circuit signals:
941
+
942
+ * **Non-retryable** — the validator marked the failure terminal
943
+ (``ValidationResult.retryable is False``, e.g. the response hit
944
+ the output token limit with no content). Re-issuing the same
945
+ request cannot help, so give up on the spot.
946
+ * **Final attempt** — :meth:`_is_final_attempt` (out of retries).
947
+ * **Repeat failure** — the new ``reason`` exactly matches the
948
+ prior attempt's. Same input + same reason almost always means
949
+ the producer is deterministic-stuck, so further retries are
950
+ pure latency tax. Logs a distinct warning so operators can
951
+ tell a fail-fast give-up apart from an exhausted-retry one.
952
+ """
953
+ if not retryable:
954
+ return True
955
+ if self._is_final_attempt(attempt):
956
+ return True
957
+ if previous_reason is not None and reason == previous_reason:
958
+ logger.warning(
959
+ "Response validation failed identically on attempt %d/%d; "
960
+ "giving up early (deterministic failure suspected): %s",
961
+ attempt + 1,
962
+ self._max_retries + 1,
963
+ reason,
964
+ )
965
+ return True
966
+ return False
967
+
968
+ def _delay_for(self, attempt: int) -> float:
969
+ idx = min(attempt, len(self._backoff) - 1)
970
+ return max(0.0, float(self._backoff[idx]))
971
+
972
+ async def _note_retry(self, reason: str, attempt: int, *, validation: ValidationTrace | None = None) -> None:
973
+ """Emit the retry event + log, then sleep the backoff delay."""
974
+ delay = self._delay_for(attempt)
975
+ if validation is not None:
976
+ await validation.retry_scheduled(delay_seconds=delay)
977
+ next_attempt = attempt + 1 # 1-based for the event / log
978
+ logger.warning(
979
+ "Response validation failed (attempt %d/%d), retrying in %.1fs: %s",
980
+ next_attempt,
981
+ self._max_retries,
982
+ delay,
983
+ reason,
984
+ )
985
+ if self._publish_retry is not None:
986
+ try:
987
+ await self._publish_retry(
988
+ RetryAttemptInfo(
989
+ reason=reason,
990
+ attempt=next_attempt,
991
+ max_attempts=self._max_retries,
992
+ delay_seconds=delay,
993
+ )
994
+ )
995
+ except Exception:
996
+ # Telemetry must never break the retry loop.
997
+ logger.exception("publish_retry callback raised")
998
+ if delay > 0:
999
+ await asyncio.sleep(delay)
1000
+ if validation is not None:
1001
+ await validation.retry_started()
1002
+
1003
+ def reset_service_retry_state(self) -> None:
1004
+ """Discard carried service-side retry state.
1005
+
1006
+ Called internally whenever a validation cycle concludes (success,
1007
+ give-up, terminal), and by the orchestration pass observer at the start of
1008
+ every user-initiated run cycle — an aborted cycle (interrupt during
1009
+ outer backoff, unrelated exception) exits through none of the internal
1010
+ reset points, and its leftover count/reason must not judge the next
1011
+ independent run. Whole-run retry attempts WITHIN one cycle share the
1012
+ state; that is the point of carrying it.
1013
+ """
1014
+ self._service_retry_count = 0
1015
+ self._service_retry_reason = None
1016
+
1017
+ def _service_side_failure(
1018
+ self,
1019
+ reason: str,
1020
+ response: ChatResponse,
1021
+ *,
1022
+ code: str,
1023
+ give_up: bool,
1024
+ stream_usage_chunks: list[dict[str, Any]] | None = None,
1025
+ ) -> None:
1026
+ """Route a service-side validation failure through the retry policy.
1027
+
1028
+ ``give_up`` is the decision :meth:`_gives_up` already took against the
1029
+ instance-carried count/reason. Give-up is always terminal here: the
1030
+ service holds the unscrubbed conversation state, so returning a locally
1031
+ scrubbed response could desynchronize a stored continuation. The
1032
+ terminal raise resets the carried state so a later user-initiated run
1033
+ starts with a fresh budget.
1034
+ """
1035
+ if give_up:
1036
+ self.reset_service_retry_state()
1037
+ self._raise_terminal(reason, response, code=code, stream_usage_chunks=stream_usage_chunks)
1038
+ self._service_retry_count += 1
1039
+ self._service_retry_reason = reason
1040
+ self._raise_service_retry(reason, response, stream_usage_chunks=stream_usage_chunks)
1041
+
1042
+ def _raise_terminal(
1043
+ self,
1044
+ reason: str,
1045
+ response: ChatResponse,
1046
+ *,
1047
+ code: str,
1048
+ stream_usage_chunks: list[dict[str, Any]] | None = None,
1049
+ ) -> None:
1050
+ """Raise a terminal validation failure into the pass observer error path.
1051
+
1052
+ A filtered response is raised as the provider's own content-filter
1053
+ failure: it is classified as filtered, and a live continuation token
1054
+ names a finished response that a retry must not poll.
1055
+ """
1056
+ logger.warning("Response validation failed terminally (giving up): %s", reason)
1057
+ usage_details = None
1058
+ if stream_usage_chunks is not None:
1059
+ normalized = normalize_stream_usage(stream_usage_chunks)
1060
+ usage_details = dict(normalized) if normalized else None
1061
+ if usage_details is None and response.usage_details:
1062
+ usage_details = dict(response.usage_details)
1063
+ error = TerminalResponseValidationError(reason, usage_details=usage_details)
1064
+ if code == ValidationReason.CONTENT_FILTERED:
1065
+ raise error from ProviderResponseError(
1066
+ "content_filter", reason, retryable=False, invalidates_continuation_token=True
1067
+ )
1068
+ raise error
1069
+
1070
+ def _raise_service_retry(
1071
+ self,
1072
+ reason: str,
1073
+ response: ChatResponse,
1074
+ *,
1075
+ stream_usage_chunks: list[dict[str, Any]] | None = None,
1076
+ ) -> None:
1077
+ """Raise a stored validation failure to the whole-run retry owner."""
1078
+ usage_details = None
1079
+ if stream_usage_chunks is not None:
1080
+ normalized = normalize_stream_usage(stream_usage_chunks)
1081
+ usage_details = dict(normalized) if normalized else None
1082
+ if usage_details is None and response.usage_details:
1083
+ usage_details = dict(response.usage_details)
1084
+ raise RetryableResponseValidationError(
1085
+ reason,
1086
+ exemption=ValidationRetryExemption(
1087
+ attempt=self._service_retry_count,
1088
+ max_attempts=self._max_retries,
1089
+ delay_seconds=math.ceil(self._delay_for(self._service_retry_count - 1)),
1090
+ ),
1091
+ usage_details=usage_details,
1092
+ hosted_commits=_hosted_commit_labels(response),
1093
+ )
1094
+
1095
+
1096
+ # ---------------------------------------------------------------------------
1097
+ # Inner-stream finaliser + replay helpers
1098
+ # ---------------------------------------------------------------------------
1099
+
1100
+
1101
+ def _collect_and_clear_hooks(
1102
+ stream: ResponseStream[ChatResponseUpdate, ChatResponse],
1103
+ ) -> list[Callable[[ChatResponse], Any]]:
1104
+ """Walk a (possibly wrapped) ResponseStream chain; collect + clear hooks.
1105
+
1106
+ ``BaseChatClient.get_response`` may wrap a provider's stream in
1107
+ ``ResponseStream.from_awaitable`` (e.g. when a compaction override is
1108
+ active), in which case the hook installed by ``_inner_get_response``
1109
+ sits on the *inner* stream, not the outer wrapper. Walk the
1110
+ ``_inner_stream`` chain, collecting hooks from each layer and
1111
+ clearing them so neither the layer's own ``get_final_response`` nor
1112
+ the auto-finalise path inside ``__anext__`` fires them during our
1113
+ preview drain.
1114
+
1115
+ The caller should ``await stream`` first so ``_inner_stream`` is
1116
+ populated on any ``from_awaitable`` wrappers.
1117
+ """
1118
+ hooks_by_layer: list[list[Callable[[ChatResponse], Any]]] = []
1119
+ seen: set[int] = set()
1120
+ cur: ResponseStream[Any, Any] | None = stream
1121
+ while cur is not None and id(cur) not in seen:
1122
+ seen.add(id(cur))
1123
+ hooks = cur._result_hooks
1124
+ if hooks:
1125
+ hooks_by_layer.append(list(hooks))
1126
+ cur._result_hooks = []
1127
+ cur = getattr(cur, "_inner_stream", None)
1128
+ # ResponseStream finalizes wrapped streams from the innermost layer outward.
1129
+ # Recreate that phase order when all hooks move onto the validating proxy.
1130
+ return [hook for hooks in reversed(hooks_by_layer) for hook in hooks]
1131
+
1132
+
1133
+ async def _preview_finalize(
1134
+ inner: ResponseStream[ChatResponseUpdate, ChatResponse],
1135
+ updates: Sequence[ChatResponseUpdate],
1136
+ ) -> ChatResponse:
1137
+ """Produce a :class:`ChatResponse` from *updates* for validation only.
1138
+
1139
+ Uses the inner stream's ``_finalizer`` if set (so provider-specific
1140
+ finalisation logic, e.g. ``output_format_type`` parsing, still runs)
1141
+ but deliberately does **not** invoke the inner's ``_result_hooks``.
1142
+ Those hooks are reserved for the caller-visible finalisation that
1143
+ runs once we've decided to return the response.
1144
+ """
1145
+ finalizer = getattr(inner, "_finalizer", None)
1146
+ if finalizer is not None:
1147
+ result = finalizer(updates)
1148
+ if inspect.isawaitable(result):
1149
+ result = await result
1150
+ if isinstance(result, ChatResponse):
1151
+ return result
1152
+ # Fallback: build the response directly. Keeps the middleware
1153
+ # robust against custom ResponseStream subclasses that stash the
1154
+ # finalizer elsewhere.
1155
+ return ChatResponse.from_updates(list(updates))
1156
+
1157
+
1158
+ def _is_stream_heartbeat(update: ChatResponseUpdate) -> bool:
1159
+ """Return whether *update* is the explicit opaque transport heartbeat."""
1160
+ return update.is_transport_heartbeat
1161
+
1162
+
1163
+ def _stream_heartbeat_for(update: ChatResponseUpdate) -> ChatResponseUpdate:
1164
+ """Return one semantics-free progress update for every provider update.
1165
+
1166
+ The continuation token rides along: it is transport bookkeeping, not
1167
+ model content, and the retry owner must learn a background response id
1168
+ as soon as the provider announces it — content buffering must not delay
1169
+ that past a mid-stream disconnect.
1170
+ """
1171
+ heartbeat = ChatResponseUpdate.transport_heartbeat()
1172
+ heartbeat.continuation_token = update.continuation_token
1173
+ return heartbeat
1174
+
1175
+
1176
+ def _strip_function_calls(
1177
+ response: ChatResponse,
1178
+ updates: list[ChatResponseUpdate],
1179
+ ) -> None:
1180
+ """Remove actionable ``function_call`` contents from *response* and *updates*.
1181
+
1182
+ Used on the validation **give-up** path only — never on a valid
1183
+ response, since stripping would suppress real tool calls the model
1184
+ legitimately wants to execute. Without this, an exhausted-but-
1185
+ still-malformed response carrying a real ``function_call`` would be
1186
+ handed to the tool loop, the loop would iterate, and a model stuck
1187
+ in a bad state would re-trigger the full validation cycle every
1188
+ iteration. Stripping forces ``_extract_function_calls`` to return
1189
+ ``[]`` so the loop exits cleanly.
1190
+
1191
+ Mutates ``Message.contents`` and ``ChatResponseUpdate.contents`` in
1192
+ place. Informational hosted calls and other content types (text,
1193
+ function_result, image, …) are preserved.
1194
+ """
1195
+ for msg in response.messages:
1196
+ if any(c.type == "function_call" and not c.informational_only for c in msg.contents):
1197
+ msg.contents = [c for c in msg.contents if c.type != "function_call" or c.informational_only]
1198
+ for upd in updates:
1199
+ if upd.contents and any(c.type == "function_call" and not c.informational_only for c in upd.contents):
1200
+ upd.contents = [c for c in upd.contents if c.type != "function_call" or c.informational_only]
1201
+
1202
+
1203
+ def _usage_chunks_from_updates(updates: Sequence[ChatResponseUpdate]) -> list[dict[str, Any]]:
1204
+ """Return provider usage payloads from streamed updates."""
1205
+ return [
1206
+ dict(content.usage_details)
1207
+ for update in updates
1208
+ for content in update.contents
1209
+ if content.type == "usage" and content.usage_details
1210
+ ]
1211
+
1212
+
1213
+ def _drop_empty_assistant_outputs(
1214
+ response: ChatResponse,
1215
+ updates: list[ChatResponseUpdate],
1216
+ ) -> None:
1217
+ """Drop structurally-empty assistant messages and their updates.
1218
+
1219
+ Run on **every** response (valid + give-up). Two reasons:
1220
+
1221
+ 1. ``DefaultResponseValidator`` only inspects the *final* assistant
1222
+ message, so a response shaped like ``[empty, valid]`` passes
1223
+ validation while still carrying a stray ``content: []`` leading
1224
+ message that would land in persisted history.
1225
+ 2. The kernel streaming finalizer rebuilds the final
1226
+ persisted response with ``ChatResponse.from_updates(updates)``
1227
+ from stream updates. If we only scrub
1228
+ ``response.messages`` the streaming path would re-create the
1229
+ empty message from the leftover updates and we'd be back where
1230
+ we started. Mirroring the drop on *updates* prevents that.
1231
+
1232
+ "Empty" means: ``contents`` is ``[]`` OR every content item is
1233
+ text and the concatenated text is empty / whitespace-only. Any
1234
+ non-text content (image, function_result, audio, …) is treated as
1235
+ a useful payload and the message/update is kept.
1236
+
1237
+ The update-level pass is **group-aware**: updates are grouped by
1238
+ ``(role, message_id)`` using the same boundary rules as the kernel
1239
+ stream grouping logic, and an entire group is
1240
+ dropped only when every update in it contributes empty content.
1241
+ A real provider's terminal "stop" update (empty contents +
1242
+ ``finish_reason``) typically shares the previous text update's
1243
+ group, so it survives the drop and its metadata reaches the
1244
+ rebuild — only fully-empty placeholder groups (e.g. an unfilled
1245
+ ``message_id`` boundary) are removed.
1246
+
1247
+ Mutates ``response.messages`` and *updates* in place.
1248
+ """
1249
+ response.messages = [m for m in response.messages if not _is_empty_assistant_message(m)]
1250
+
1251
+ if not updates:
1252
+ return
1253
+ keep_mask = _compute_update_keep_mask(updates)
1254
+ cleaned: list[ChatResponseUpdate] = [u for u, keep in zip(updates, keep_mask, strict=True) if keep]
1255
+ updates.clear()
1256
+ updates.extend(cleaned)
1257
+
1258
+
1259
+ def _compute_update_keep_mask(updates: list[ChatResponseUpdate]) -> list[bool]:
1260
+ """Group *updates* by their effective ``(role, message_id)`` and decide
1261
+ which updates to keep.
1262
+
1263
+ Mirrors ``_process_update``'s message-boundary rules: a new group
1264
+ starts when the update has a ``message_id`` that differs from the
1265
+ current group's ``message_id``, OR when the update has a ``role``
1266
+ that differs from the current group's role. Otherwise the update
1267
+ appends to the current group.
1268
+
1269
+ A group is "empty assistant" if its effective role resolves to
1270
+ assistant (or unset, since ``_process_update`` defaults to
1271
+ assistant) AND every update in the group has empty / whitespace-
1272
+ only-text contents. Such groups are dropped wholesale — including
1273
+ any metadata-bearing terminal updates inside them, since with no
1274
+ text/non-text content there is nothing for the metadata to attach
1275
+ to in the rebuild.
1276
+
1277
+ Returns a boolean mask of the same length as *updates*.
1278
+ """
1279
+ if not updates:
1280
+ return []
1281
+
1282
+ # Pass 1: assign each update to a group index, mirroring
1283
+ # ``_process_update`` exactly. Two key invariants from the kernel
1284
+ # stream rebuild logic:
1285
+ #
1286
+ # * Every new message begins life as ``Message("assistant", [])`` —
1287
+ # default role assistant, no message_id. The current update's
1288
+ # ``role`` / ``message_id`` then OVERRIDE that placeholder state.
1289
+ # So whenever we open a new group we MUST reset ``cur_role`` to
1290
+ # ``"assistant"`` and ``cur_message_id`` to ``None`` *before*
1291
+ # applying the new update — otherwise a role-less new-message_id
1292
+ # update inherits the previous group's role, and a same-id
1293
+ # follow-up update can split incorrectly.
1294
+ # * Boundary checks reference the previous message's *final* state
1295
+ # (after all its updates have been applied). ``cur_role`` /
1296
+ # ``cur_message_id`` track that state for the current group.
1297
+ group_idx: list[int] = []
1298
+ group_role: list[str] = []
1299
+ cur_role: str = "assistant"
1300
+ cur_message_id: str | None = None
1301
+ cur_idx = -1
1302
+ for upd in updates:
1303
+ new_group = (
1304
+ cur_idx < 0
1305
+ or (upd.message_id and cur_message_id and upd.message_id != cur_message_id)
1306
+ or (upd.role and upd.role != cur_role)
1307
+ )
1308
+ if new_group:
1309
+ cur_idx += 1
1310
+ # New placeholder: kernel default role + no inherited id.
1311
+ cur_role = "assistant"
1312
+ cur_message_id = None
1313
+ group_role.append(cur_role)
1314
+ group_idx.append(cur_idx)
1315
+ if upd.role:
1316
+ cur_role = upd.role
1317
+ # Update the group's recorded role so subsequent comparisons
1318
+ # use the latest known role for this group.
1319
+ group_role[cur_idx] = cur_role
1320
+ if upd.message_id:
1321
+ cur_message_id = upd.message_id
1322
+
1323
+ # Pass 2: per-group, check if all updates contribute empty content.
1324
+ group_count = cur_idx + 1
1325
+ group_has_content: list[bool] = [False] * group_count
1326
+ for upd, gi in zip(updates, group_idx, strict=True):
1327
+ if _update_has_useful_content(upd):
1328
+ group_has_content[gi] = True
1329
+
1330
+ # Pass 3: build keep mask. Only assistant groups are scrubbed —
1331
+ # tool / user / system groups are kept regardless of content so we
1332
+ # never drop kernel- or caller-injected non-assistant turns.
1333
+ keep_mask: list[bool] = []
1334
+ for gi in group_idx:
1335
+ is_assistant_group = group_role[gi] == "assistant"
1336
+ keep_mask.append((not is_assistant_group) or group_has_content[gi])
1337
+ return keep_mask
1338
+
1339
+
1340
+ def _update_has_useful_content(upd: ChatResponseUpdate) -> bool:
1341
+ """Return ``True`` if *upd*'s contents include anything non-empty.
1342
+
1343
+ Non-text content (image, function_call, function_result, usage, …)
1344
+ counts as useful. Text content counts only if the concatenated text
1345
+ has at least one non-whitespace character. Empty contents → not
1346
+ useful.
1347
+ """
1348
+ contents = upd.contents or []
1349
+ if not contents:
1350
+ return False
1351
+ text_acc: list[str] = []
1352
+ for c in contents:
1353
+ ctype = c.type
1354
+ if ctype != "text":
1355
+ return True
1356
+ text_acc.append(c.text or "")
1357
+ return bool("".join(text_acc).strip())
1358
+
1359
+
1360
+ def _is_empty_assistant_message(msg: Message) -> bool:
1361
+ """Return ``True`` if *msg* is an assistant message with no useful payload.
1362
+
1363
+ "No useful payload" means:
1364
+
1365
+ * ``contents`` is empty (``[]``), OR
1366
+ * every content item is text and the concatenated text is empty or
1367
+ whitespace-only.
1368
+
1369
+ A message carrying any non-text content (image, function_result,
1370
+ audio, …) is NOT considered empty even if the text fragments are
1371
+ blank — preserving multimodal payloads that may still be useful.
1372
+ """
1373
+ if msg.role != "assistant":
1374
+ return False
1375
+ if not msg.contents:
1376
+ return True
1377
+ for c in msg.contents:
1378
+ if c.type != "text":
1379
+ return False
1380
+ return not "".join((c.text or "") for c in msg.contents).strip()