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,1115 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """ACP update translation and permission protocols owned by the ACP backend."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import base64
9
+ import binascii
10
+ import contextlib
11
+ import hashlib
12
+ import json
13
+ import logging
14
+ import math
15
+ import time
16
+ from collections import deque
17
+ from collections.abc import Callable, Mapping, Sequence
18
+ from dataclasses import dataclass, field
19
+ from typing import TYPE_CHECKING, Any, Literal
20
+
21
+ from acp.exceptions import RequestError
22
+ from acp.schema import (
23
+ AgentMessageChunk,
24
+ AudioContentBlock,
25
+ ContentToolCallContent,
26
+ EmbeddedResourceContentBlock,
27
+ FileEditToolCallContent,
28
+ ImageContentBlock,
29
+ PermissionOption,
30
+ ResourceContentBlock,
31
+ SessionNotification,
32
+ TerminalToolCallContent,
33
+ TextContentBlock,
34
+ ToolCallProgress,
35
+ ToolCallStart,
36
+ ToolCallUpdate,
37
+ UsageUpdate,
38
+ )
39
+
40
+ from chrys.foundation.events.types import (
41
+ ApprovalCancelled,
42
+ ApprovalRequest,
43
+ ApprovalResponse,
44
+ AskUserResponse,
45
+ AskUserTimedOut,
46
+ InvocationMessage,
47
+ InvocationProgress,
48
+ InvocationToolCallResult,
49
+ InvocationToolCallStart,
50
+ QuestionToUser,
51
+ )
52
+ from chrys.foundation.models.ask_user import Cancelled as AskUserCancelled
53
+ from chrys.foundation.models.ask_user import (
54
+ parse_ask_user_answers,
55
+ validate_request_input_params,
56
+ validate_request_input_response,
57
+ )
58
+ from chrys.foundation.models.invocations import InvocationOrigin
59
+ from chrys.foundation.text.images import MAX_IMAGE_BYTES
60
+ from chrys.foundation.tool_result_metadata import TOOL_FAILED_METADATA_KEY, TOOL_INTERRUPTED_METADATA_KEY
61
+ from chrys.foundation.trajectory.context import TrajectoryContext
62
+ from chrys.foundation.trajectory.event_types import WaitCategory
63
+ from chrys.kernel import Content
64
+ from chrys.service.acp_client.errors import (
65
+ AcpTransportError,
66
+ )
67
+ from chrys.service.acp_client.spec import PermissionDecision
68
+ from chrys.service.approval.arbitration import ApprovalDecisionArbiter, ApprovalJudgeInput
69
+ from chrys.service.approval.correlation import OneShotCorrelation
70
+ from chrys.service.approval.policy import ApprovalMode
71
+ from chrys.service.trajectory.approvals import ApprovalDecider, ApprovalTrace
72
+ from chrys.service.trajectory.waits import WaitOutcome, WaitTrace
73
+
74
+ if TYPE_CHECKING:
75
+ from chrys.foundation.events.bus import EventBus
76
+ from chrys.service.approval.judge import ApprovalJudge
77
+ from chrys.service.approval.turn_context import TurnContextReader
78
+
79
+ logger = logging.getLogger(__name__)
80
+
81
+ _MAX_CALLS = 5_000
82
+ _MAX_FIELD_CHARS = 2_000
83
+ _MAX_VALUE_DEPTH = 16
84
+ _MAX_COLLECTION_ITEMS = 256
85
+ _AUDIT_RING_ITEMS = 500
86
+ _AUDIT_RING_BYTES = 256 * 1024
87
+ _ALLOW_KINDS = ("allow_once", "allow_always")
88
+ _REJECT_KINDS = ("reject_once", "reject_always")
89
+ _MAX_BASE64_IMAGE_CHARS = ((MAX_IMAGE_BYTES + 2) // 3) * 4
90
+ # A maximum-size accepted image expands to 4 MiB in base64. Keep one
91
+ # additional MiB for its data-URI prefix and the surrounding bounded record.
92
+ _MAX_ASSEMBLED_BYTES = _MAX_BASE64_IMAGE_CHARS + 1024 * 1024
93
+
94
+
95
+ def display_tool_kind(kind: str | None) -> str:
96
+ """Map ACP presentation kinds to Chrys display/judge kinds."""
97
+ if kind == "execute":
98
+ return "shell"
99
+ if kind == "read":
100
+ return "filesystem.read"
101
+ if kind in {"edit", "delete", "move"}:
102
+ return "filesystem.write"
103
+ if kind == "search":
104
+ return "search"
105
+ return ""
106
+
107
+
108
+ def preview_text(value: Any, *, limit: int = _MAX_FIELD_CHARS) -> str:
109
+ if isinstance(value, str):
110
+ text = value
111
+ else:
112
+ try:
113
+ text = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
114
+ except Exception:
115
+ text = str(value)
116
+ if len(text) <= limit:
117
+ return text
118
+ digest = hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()[:16]
119
+ return f"{text[: limit - 28]}… [sha256:{digest}]"
120
+
121
+
122
+ def _bounded_value(value: Any, *, depth: int = 0) -> Any:
123
+ """Return a JSON-safe bounded value before it enters events or persistence."""
124
+ if depth >= _MAX_VALUE_DEPTH:
125
+ return "[depth limit]"
126
+ if value is None or type(value) is bool:
127
+ return value
128
+ if type(value) is int:
129
+ rendered = str(value)
130
+ return value if len(rendered) <= _MAX_FIELD_CHARS else preview_text(rendered)
131
+ if type(value) is float:
132
+ return value if math.isfinite(value) else preview_text(value)
133
+ if isinstance(value, str):
134
+ return preview_text(value)
135
+ if hasattr(value, "model_dump"):
136
+ return _bounded_value(value.model_dump(mode="json", by_alias=True), depth=depth)
137
+ if isinstance(value, Mapping):
138
+ result: dict[str, Any] = {}
139
+ for index, (key, item) in enumerate(value.items()):
140
+ if index >= _MAX_COLLECTION_ITEMS:
141
+ result["…"] = "[item limit]"
142
+ break
143
+ result[preview_text(key, limit=200)] = _bounded_value(item, depth=depth + 1)
144
+ return result
145
+ if isinstance(value, Sequence) and not isinstance(value, bytes | bytearray):
146
+ return [_bounded_value(item, depth=depth + 1) for item in value[:_MAX_COLLECTION_ITEMS]]
147
+ return preview_text(value)
148
+
149
+
150
+ def canonical_raw_input(value: Any) -> dict[str, Any]:
151
+ bounded = _bounded_value(value)
152
+ if isinstance(bounded, dict):
153
+ return bounded
154
+ return {"input": preview_text(bounded)}
155
+
156
+
157
+ def _raw_args_dict(value: Any) -> dict[str, Any]:
158
+ """Dict-shaped raw arguments for approval/judge inputs — never truncated.
159
+
160
+ The judge and the approval dialog must see the arguments the remote will
161
+ execute (§9.2): a preview cap here would let a destructive suffix hide
162
+ beyond the truncation point and be approved unseen. Size is already
163
+ bounded upstream — permission frames ride the client's per-payload caps
164
+ before SDK routing — so the untruncated form is safe to publish.
165
+ canonical_raw_input stays for retained records and audit appends (§7.4).
166
+ """
167
+ if isinstance(value, Mapping):
168
+ return dict(value)
169
+ return {"input": value}
170
+
171
+
172
+ def _content_block_text(block: Any) -> str:
173
+ if isinstance(block, TextContentBlock):
174
+ return block.text
175
+ if isinstance(block, ResourceContentBlock):
176
+ return f"[resource: {block.uri}]"
177
+ if isinstance(block, EmbeddedResourceContentBlock):
178
+ resource = block.resource
179
+ uri = getattr(resource, "uri", "")
180
+ return f"[resource: {uri}]" if uri else "[resource]"
181
+ if isinstance(block, ImageContentBlock | AudioContentBlock):
182
+ return ""
183
+ return preview_text(block)
184
+
185
+
186
+ def _structured_tool_content(content: Sequence[Any]) -> tuple[list[Content], list[dict[str, Any]]]:
187
+ """Extract images and resource descriptors before bounded text projection."""
188
+ image_contents: list[Content] = []
189
+ artifacts: list[dict[str, Any]] = []
190
+ for item in content:
191
+ if not isinstance(item, ContentToolCallContent):
192
+ continue
193
+ block = item.content
194
+ if isinstance(block, ImageContentBlock):
195
+ media_type = block.mime_type.strip().lower() or "image/png"
196
+ if block.data:
197
+ if len(block.data) > _MAX_BASE64_IMAGE_CHARS:
198
+ raise AcpTransportError("The ACP agent returned an image that exceeds the supported size limit.")
199
+ try:
200
+ data = base64.b64decode(block.data, validate=True)
201
+ except (binascii.Error, ValueError) as exc:
202
+ raise AcpTransportError("The ACP agent returned invalid base64 image content.") from exc
203
+ if not data:
204
+ raise AcpTransportError("The ACP agent returned empty image content.")
205
+ if len(data) > MAX_IMAGE_BYTES:
206
+ raise AcpTransportError("The ACP agent returned an image that exceeds the supported size limit.")
207
+ image_contents.append(Content.from_data(data=data, media_type=media_type))
208
+ elif block.uri:
209
+ image_contents.append(Content.from_uri(uri=block.uri, media_type=media_type))
210
+ continue
211
+ if isinstance(block, ResourceContentBlock):
212
+ descriptor: dict[str, Any] = {"name": block.name, "path": block.uri}
213
+ if block.mime_type:
214
+ descriptor["mime"] = block.mime_type
215
+ if block.size is not None:
216
+ descriptor["size"] = block.size
217
+ artifacts.append(descriptor)
218
+ continue
219
+ if isinstance(block, EmbeddedResourceContentBlock):
220
+ resource = block.resource
221
+ uri = resource.uri
222
+ descriptor = {"name": uri.rstrip("/").rsplit("/", 1)[-1] or uri, "path": uri}
223
+ if resource.mime_type:
224
+ descriptor["mime"] = resource.mime_type
225
+ artifacts.append(descriptor)
226
+ return image_contents, artifacts
227
+
228
+
229
+ def _tool_content_text(content: Sequence[Any] | None) -> str:
230
+ rendered: list[str] = []
231
+ for item in content or ():
232
+ if isinstance(item, ContentToolCallContent):
233
+ text = _content_block_text(item.content)
234
+ elif isinstance(item, FileEditToolCallContent):
235
+ text = f"edited {item.path}"
236
+ elif isinstance(item, TerminalToolCallContent):
237
+ text = f"[terminal {item.terminal_id}]"
238
+ else:
239
+ text = preview_text(item)
240
+ if text:
241
+ rendered.append(text)
242
+ return preview_text("\n".join(rendered))
243
+
244
+
245
+ @dataclass(slots=True)
246
+ class _CallRecord:
247
+ local_call_id: str
248
+ title: str = "tool"
249
+ kind: str = ""
250
+ raw_input: dict[str, Any] = field(default_factory=dict)
251
+ raw_output: Any = None
252
+ content: list[Any] = field(default_factory=list)
253
+ image_contents: list[Content] = field(default_factory=list)
254
+ artifacts: list[dict[str, Any]] = field(default_factory=list)
255
+ locations: list[Any] = field(default_factory=list)
256
+ status: str = ""
257
+ started_at: float = field(default_factory=time.monotonic)
258
+ start_published: bool = False
259
+ terminal_published: bool = False
260
+ last_start_signature: str = ""
261
+ provider_hosted: bool = False
262
+ hosted_family: str = ""
263
+ provider: str = ""
264
+ provider_item_type: str = ""
265
+ provider_call_id: str = ""
266
+ provider_status: str = ""
267
+
268
+
269
+ def _explicit_hosted_extension(update: ToolCallStart | ToolCallProgress) -> dict[str, Any]:
270
+ """Read hosted presentation metadata only when the remote explicitly sends it."""
271
+ metadata = update.field_meta or {}
272
+ chrys_meta = metadata.get("chrys")
273
+ if not isinstance(chrys_meta, Mapping):
274
+ return {}
275
+ family = chrys_meta.get("hosted_family", chrys_meta.get("hostedFamily"))
276
+ provider_hosted = chrys_meta.get("provider_hosted", chrys_meta.get("providerHosted")) is True
277
+ if not provider_hosted and not isinstance(family, str):
278
+ return {}
279
+
280
+ extension: dict[str, Any] = {"provider_hosted": provider_hosted}
281
+ if isinstance(family, str):
282
+ extension["hosted_family"] = family
283
+
284
+ def _copy_text_value(target_key: str, snake_key: str, camel_key: str) -> None:
285
+ value = chrys_meta.get(snake_key, chrys_meta.get(camel_key))
286
+ if isinstance(value, str):
287
+ extension[target_key] = value
288
+
289
+ _copy_text_value("provider", "provider", "provider")
290
+ _copy_text_value("provider_item_type", "provider_item_type", "providerItemType")
291
+ _copy_text_value("provider_call_id", "provider_call_id", "providerCallId")
292
+ _copy_text_value("provider_status", "provider_status", "providerStatus")
293
+ return extension
294
+
295
+
296
+ class AcpUpdateTranslator:
297
+ """Merge ACP partial updates into bounded Chrys events and final text."""
298
+
299
+ def __init__(
300
+ self,
301
+ *,
302
+ event_bus: EventBus | None,
303
+ session_id: str | None,
304
+ agent_name: str,
305
+ invocation_id: str,
306
+ attempt: int,
307
+ origin: InvocationOrigin,
308
+ result_mode: Literal["last_segment", "transcript"] = "last_segment",
309
+ completed_before: int = 0,
310
+ spend_before: int = 0,
311
+ unreported_before: int = 0,
312
+ ) -> None:
313
+ self._bus = event_bus
314
+ self._session_id = session_id
315
+ self.origin = origin
316
+ self.agent_name = agent_name
317
+ self.invocation_id = invocation_id
318
+ self.attempt = attempt
319
+ self.result_mode = result_mode
320
+ self.calls: dict[str, _CallRecord] = {}
321
+ self._current_call_ids: dict[str, str] = {}
322
+ self._call_occurrences: dict[str, int] = {}
323
+ self._next_fallback_call_id = 1
324
+ self._completed_before = completed_before
325
+ self._completed_here = 0
326
+ self._spend = spend_before
327
+ self._unreported = unreported_before
328
+ self.latest_context_used = 0
329
+ self.latest_context_size: int | None = None
330
+ self._segments: list[str] = []
331
+ self._open_segment = ""
332
+ self._text_finalized = False
333
+ self._unpublished_final_text = ""
334
+ self._message_id: str | None = None
335
+ self._retained_bytes = 0
336
+ self._retained_image_bytes_by_call: dict[str, int] = {}
337
+ self._audit: deque[dict[str, Any]] = deque()
338
+ # Include the JSON list delimiters so the persisted ring itself,
339
+ # rather than only the sum of its entries, stays within the cap.
340
+ self._audit_bytes = 2
341
+ self.on_audit_changed: Callable[[], None] | None = None
342
+
343
+ @property
344
+ def completed_count(self) -> int:
345
+ return self._completed_before + self._completed_here
346
+
347
+ @property
348
+ def translated_updates(self) -> list[dict[str, Any]]:
349
+ return list(self._audit)
350
+
351
+ async def put(self, seq: int, notification: SessionNotification) -> None:
352
+ await self.on_update(seq, notification)
353
+
354
+ async def on_update(self, seq: int, notification: SessionNotification) -> None:
355
+ update = notification.update
356
+ self._append_audit(seq, update)
357
+ if isinstance(update, ToolCallStart | ToolCallProgress):
358
+ # Only a ToolCallStart seals the open segment (§7.5): progress
359
+ # merges tool state and must not split an assistant message that
360
+ # keeps streaming around a running call — last_segment would
361
+ # otherwise return just the suffix.
362
+ if isinstance(update, ToolCallStart):
363
+ await self._seal_segment(publish=True)
364
+ await self._merge_tool(update)
365
+ elif isinstance(update, AgentMessageChunk):
366
+ await self._consume_message(update)
367
+ elif isinstance(update, UsageUpdate):
368
+ # PR-1 validates raw exact-int usage before SDK coercion. Keep this
369
+ # defensive check for direct translator tests and custom sinks.
370
+ if type(update.used) is int and type(update.size) is int and update.used >= 0 and update.size >= 0:
371
+ self.latest_context_used = update.used
372
+ self.latest_context_size = update.size
373
+ await self.publish_progress()
374
+
375
+ def _append_audit(self, seq: int, update: Any) -> None:
376
+ self._append_audit_item({"seq": seq, "update": _bounded_value(update.model_dump(mode="json", by_alias=True))})
377
+
378
+ def record_permission_request(self, *, title: str, kind: str, raw_input: Any, outcome: str) -> None:
379
+ """Append a permission request to the audit ring.
380
+
381
+ ACP permits ``session/request_permission`` before any ``ToolCallStart``;
382
+ without this entry a standalone request's rawInput would reach no
383
+ durable artifact (judge logging is deliberately ``log_dir=None``, §9.2
384
+ — the owner-only envelope is the place remote rawInput is audited).
385
+ """
386
+ self._append_audit_item(
387
+ {
388
+ "seq": None,
389
+ "update": {
390
+ "sessionUpdate": "permission_request",
391
+ "title": preview_text(title),
392
+ "kind": preview_text(kind),
393
+ "rawInput": _bounded_value(raw_input),
394
+ "outcome": outcome,
395
+ },
396
+ }
397
+ )
398
+
399
+ def _append_audit_item(self, item: dict[str, Any]) -> None:
400
+ item["attempt"] = self.attempt
401
+ size = len(json.dumps(item, ensure_ascii=False, separators=(",", ":"), default=str).encode())
402
+ if size > _AUDIT_RING_BYTES:
403
+ item = {"attempt": self.attempt, "seq": item.get("seq"), "update": preview_text(item)}
404
+ size = len(json.dumps(item, ensure_ascii=False).encode())
405
+ added = size + (1 if self._audit else 0)
406
+ while self._audit and (len(self._audit) >= _AUDIT_RING_ITEMS or self._audit_bytes + added > _AUDIT_RING_BYTES):
407
+ removed = self._audit.popleft()
408
+ removed_size = len(json.dumps(removed, ensure_ascii=False, separators=(",", ":")).encode())
409
+ self._audit_bytes -= removed_size + (1 if self._audit else 0)
410
+ added = size + (1 if self._audit else 0)
411
+ if self._audit_bytes + added <= _AUDIT_RING_BYTES:
412
+ self._audit.append(item)
413
+ self._audit_bytes += added
414
+ if self.on_audit_changed is not None:
415
+ self.on_audit_changed()
416
+
417
+ def _charge(self, value: Any) -> None:
418
+ self._retained_bytes += len(json.dumps(value, ensure_ascii=False, default=str).encode())
419
+ if self._retained_bytes > _MAX_ASSEMBLED_BYTES:
420
+ raise AcpTransportError("The ACP agent exceeded the translated update budget.")
421
+
422
+ def _charge_tool_update(self, call_id: str, value: Any, image_uris: Sequence[str]) -> None:
423
+ update_bytes = len(json.dumps(value, ensure_ascii=False, default=str).encode())
424
+ image_bytes = len(json.dumps(image_uris, ensure_ascii=False).encode()) if image_uris else 0
425
+ previous_image_bytes = self._retained_image_bytes_by_call.get(call_id, 0)
426
+ retained_bytes = self._retained_bytes - previous_image_bytes + image_bytes + update_bytes
427
+ if retained_bytes > _MAX_ASSEMBLED_BYTES:
428
+ raise AcpTransportError("The ACP agent exceeded the translated update budget.")
429
+ self._retained_bytes = retained_bytes
430
+ if image_bytes:
431
+ self._retained_image_bytes_by_call[call_id] = image_bytes
432
+ else:
433
+ self._retained_image_bytes_by_call.pop(call_id, None)
434
+
435
+ async def _merge_tool(self, update: ToolCallStart | ToolCallProgress) -> None:
436
+ remote_id = update.tool_call_id
437
+ local_id = self._current_call_ids.get(remote_id, "")
438
+ record = self.calls.get(local_id)
439
+ status = getattr(update, "status", None)
440
+ starts_new_occurrence = record is None or (
441
+ record.terminal_published
442
+ and (
443
+ isinstance(update, ToolCallStart) or (isinstance(status, str) and status not in {"completed", "failed"})
444
+ )
445
+ )
446
+ if starts_new_occurrence:
447
+ if len(self.calls) >= _MAX_CALLS:
448
+ raise AcpTransportError("The ACP agent exceeded the tool-call update budget.")
449
+ occurrence = self._call_occurrences.get(remote_id, 0) + 1
450
+ self._call_occurrences[remote_id] = occurrence
451
+ preferred_id = f"a{self.attempt}:{remote_id}" + (f":{occurrence}" if occurrence > 1 else "")
452
+ local_id = preferred_id
453
+ while local_id in self.calls:
454
+ local_id = f"a{self.attempt}:occurrence:{self._next_fallback_call_id}"
455
+ self._next_fallback_call_id += 1
456
+ record = _CallRecord(local_call_id=local_id)
457
+ self.calls[local_id] = record
458
+ self._current_call_ids[remote_id] = local_id
459
+
460
+ title = getattr(update, "title", None)
461
+ kind = getattr(update, "kind", None)
462
+ raw_input = getattr(update, "raw_input", None)
463
+ raw_output = getattr(update, "raw_output", None)
464
+ content = getattr(update, "content", None)
465
+ locations = getattr(update, "locations", None)
466
+ if title is not None:
467
+ record.title = preview_text(title) or "tool"
468
+ if kind is not None:
469
+ record.kind = display_tool_kind(kind)
470
+ if raw_input is not None:
471
+ record.raw_input = canonical_raw_input(raw_input)
472
+ if raw_output is not None:
473
+ record.raw_output = _bounded_value(raw_output)
474
+ if content is not None:
475
+ record.image_contents, record.artifacts = _structured_tool_content(content)
476
+ record.content = list(_bounded_value(content) or [])
477
+ if locations is not None:
478
+ record.locations = list(_bounded_value(locations) or [])
479
+ if status is not None:
480
+ record.status = status
481
+ hosted_extension = _explicit_hosted_extension(update)
482
+ if hosted_extension:
483
+ record.provider_hosted = bool(hosted_extension["provider_hosted"])
484
+ if "hosted_family" in hosted_extension:
485
+ record.hosted_family = str(hosted_extension["hosted_family"])
486
+ if "provider" in hosted_extension:
487
+ record.provider = str(hosted_extension["provider"])
488
+ if "provider_item_type" in hosted_extension:
489
+ record.provider_item_type = str(hosted_extension["provider_item_type"])
490
+ if "provider_call_id" in hosted_extension:
491
+ record.provider_call_id = str(hosted_extension["provider_call_id"])
492
+ if "provider_status" in hosted_extension:
493
+ record.provider_status = str(hosted_extension["provider_status"])
494
+ self._charge_tool_update(
495
+ record.local_call_id,
496
+ {
497
+ "title": record.title,
498
+ "kind": record.kind,
499
+ "input": record.raw_input,
500
+ "output": record.raw_output,
501
+ "content": record.content,
502
+ "artifacts": record.artifacts,
503
+ "locations": record.locations,
504
+ },
505
+ [image.uri or "" for image in record.image_contents],
506
+ )
507
+
508
+ await self._publish_start(record)
509
+ if record.status in {"completed", "failed"}:
510
+ await self._publish_terminal(record)
511
+
512
+ async def _publish_start(self, record: _CallRecord, *, ensure: bool = False) -> None:
513
+ signature = json.dumps(
514
+ [record.title, record.kind, record.raw_input],
515
+ ensure_ascii=False,
516
+ sort_keys=True,
517
+ separators=(",", ":"),
518
+ )
519
+ if not ensure and (
520
+ record.terminal_published or (record.start_published and signature == record.last_start_signature)
521
+ ):
522
+ return
523
+ record.start_published = True
524
+ record.last_start_signature = signature
525
+ if self._bus is not None:
526
+ await self._bus.publish(
527
+ InvocationToolCallStart(
528
+ origin=self.origin,
529
+ agent_name=self.agent_name,
530
+ tool_name=record.title,
531
+ tool_kind=record.kind,
532
+ args=dict(record.raw_input),
533
+ call_id=record.local_call_id,
534
+ provider_hosted=record.provider_hosted,
535
+ hosted_family=record.hosted_family,
536
+ provider=record.provider,
537
+ provider_item_type=record.provider_item_type,
538
+ provider_call_id=record.provider_call_id,
539
+ provider_status=record.provider_status,
540
+ session_id=self._session_id,
541
+ )
542
+ )
543
+
544
+ async def _publish_terminal(self, record: _CallRecord, *, interrupted: bool = False) -> None:
545
+ if record.terminal_published:
546
+ return
547
+ # Claim the terminal synchronously, before any await. The live update
548
+ # consumer and a cancel-path flush_interrupted() run as separate tasks;
549
+ # if the guard above and the claim below straddled an await (the
550
+ # _publish_start / bus.publish points), both could pass the guard and
551
+ # double-deliver a InvocationToolCallResult (and double-count). Claiming
552
+ # up front serializes them — the loser returns at the guard. The
553
+ # ensure=True start below intentionally bypasses this same flag.
554
+ record.terminal_published = True
555
+ self._completed_here += 1
556
+ try:
557
+ await self._publish_start(record, ensure=True)
558
+ if interrupted:
559
+ result = "(interrupted)"
560
+ metadata = {TOOL_INTERRUPTED_METADATA_KEY: True}
561
+ provider_status = "interrupted"
562
+ else:
563
+ result = _tool_content_text_from_bounded(record.content)
564
+ if not result:
565
+ result = preview_text(record.raw_output)
566
+ failed = record.status == "failed"
567
+ metadata = {TOOL_FAILED_METADATA_KEY: True} if failed else {}
568
+ provider_status = record.provider_status
569
+ if self._bus is not None:
570
+ await self._bus.publish(
571
+ InvocationToolCallResult(
572
+ origin=self.origin,
573
+ agent_name=self.agent_name,
574
+ tool_name=record.title,
575
+ call_id=record.local_call_id,
576
+ result=result,
577
+ image_contents=list(record.image_contents),
578
+ duration_ms=max(0, int((time.monotonic() - record.started_at) * 1_000)),
579
+ metadata=metadata,
580
+ artifacts=[dict(artifact) for artifact in record.artifacts],
581
+ provider_hosted=record.provider_hosted,
582
+ hosted_family=record.hosted_family,
583
+ provider=record.provider,
584
+ provider_item_type=record.provider_item_type,
585
+ provider_call_id=record.provider_call_id,
586
+ provider_status=provider_status,
587
+ session_id=self._session_id,
588
+ )
589
+ )
590
+ except asyncio.CancelledError:
591
+ # A cancellation mid-publish must not eat the terminal: release the
592
+ # claim so the cancel path's flush_interrupted() re-delivers (a
593
+ # duplicate for early subscribers beats a permanently dangling card).
594
+ record.terminal_published = False
595
+ self._completed_here -= 1
596
+ raise
597
+ await self.publish_progress()
598
+
599
+ async def flush_interrupted(self) -> None:
600
+ for record in list(self.calls.values()):
601
+ if not record.terminal_published:
602
+ await self._publish_terminal(record, interrupted=True)
603
+
604
+ async def _consume_message(self, update: AgentMessageChunk) -> None:
605
+ if update.message_id is not None and self._message_id is not None and update.message_id != self._message_id:
606
+ await self._seal_segment(publish=True)
607
+ if update.message_id is not None:
608
+ self._message_id = update.message_id
609
+ text = _content_block_text(update.content)
610
+ if text:
611
+ self._charge(text)
612
+ self._open_segment += text
613
+
614
+ def _take_open_segment(self) -> str:
615
+ segment = self._open_segment
616
+ self._open_segment = ""
617
+ if segment.strip():
618
+ self._segments.append(segment)
619
+ return segment
620
+
621
+ async def _seal_segment(self, *, publish: bool) -> None:
622
+ segment = self._take_open_segment()
623
+ if publish and segment.strip() and self._bus is not None:
624
+ await self._bus.publish(
625
+ InvocationMessage(
626
+ is_final=False,
627
+ is_intermediate=True,
628
+ origin=self.origin,
629
+ agent_name=self.agent_name,
630
+ text=segment,
631
+ session_id=self._session_id,
632
+ )
633
+ )
634
+
635
+ def result_text(self) -> str:
636
+ self._finalize_text()
637
+ segments = [segment for segment in self._segments if segment.strip()]
638
+ if not segments:
639
+ return ""
640
+ if self.result_mode == "transcript":
641
+ return "\n\n".join(segments)
642
+ return segments[-1]
643
+
644
+ def unpublished_final_text(self) -> str:
645
+ """Return the final segment that was not emitted as an intermediate event."""
646
+ self._finalize_text()
647
+ return self._unpublished_final_text
648
+
649
+ def _finalize_text(self) -> None:
650
+ if self._text_finalized:
651
+ return
652
+ self._text_finalized = True
653
+ segment = self._take_open_segment()
654
+ self._unpublished_final_text = segment if segment.strip() else ""
655
+
656
+ def partial_text(self) -> str:
657
+ segments = [*self._segments]
658
+ if self._open_segment.strip():
659
+ segments.append(self._open_segment)
660
+ return "\n\n".join(segment for segment in segments if segment.strip())
661
+
662
+ async def finalize_usage(self, *, spend: int, unreported_attempts: int) -> None:
663
+ self._spend = spend
664
+ self._unreported = unreported_attempts
665
+ await self.publish_progress()
666
+
667
+ async def publish_progress(self) -> None:
668
+ if self._bus is not None:
669
+ await self._bus.publish(
670
+ InvocationProgress(
671
+ origin=self.origin,
672
+ agent_name=self.agent_name,
673
+ tool_call_count=self.completed_count,
674
+ total_tokens=self.latest_context_used,
675
+ total_usage_tokens=self._spend,
676
+ usage_unreported_attempts=self._unreported,
677
+ session_id=self._session_id,
678
+ )
679
+ )
680
+
681
+
682
+ def _tool_content_text_from_bounded(content: Sequence[Any]) -> str:
683
+ rendered: list[str] = []
684
+ for item in content:
685
+ if not isinstance(item, Mapping):
686
+ rendered.append(preview_text(item))
687
+ continue
688
+ item_type = item.get("type")
689
+ if item_type == "content":
690
+ block = item.get("content")
691
+ if isinstance(block, Mapping):
692
+ if block.get("type") == "text":
693
+ rendered.append(str(block.get("text", "")))
694
+ elif block.get("type") == "resource_link":
695
+ rendered.append(f"[resource: {block.get('uri', '')}]")
696
+ elif block.get("type") == "resource":
697
+ rendered.append("[resource]")
698
+ elif item_type == "diff":
699
+ rendered.append(f"edited {item.get('path', '')}")
700
+ elif item_type == "terminal":
701
+ rendered.append(f"[terminal {item.get('terminalId', item.get('terminal_id', ''))}]")
702
+ return preview_text("\n".join(part for part in rendered if part))
703
+
704
+
705
+ class AcpPermissionBroker:
706
+ """Translate remote permissions and ask-user requests into Chrys events."""
707
+
708
+ def __init__(
709
+ self,
710
+ *,
711
+ event_bus: EventBus,
712
+ session_id: str | None,
713
+ caller_name: str,
714
+ mode_getter: Callable[[], ApprovalMode],
715
+ turn_context: TurnContextReader,
716
+ workspace_roots: Sequence[str],
717
+ workspace_cwd: str,
718
+ approval_judge: ApprovalJudge | None,
719
+ ask_user_timeout_seconds: float | None,
720
+ allow_user_interaction: bool = True,
721
+ trajectory_context: TrajectoryContext | None = None,
722
+ trajectory_boundary_operation_id: str | None = None,
723
+ ) -> None:
724
+ self._bus = event_bus
725
+ self._session_id = session_id
726
+ self._caller_name = caller_name
727
+ self._mode_getter = mode_getter
728
+ self._turn_context = turn_context
729
+ self._workspace_roots = list(workspace_roots)
730
+ self._workspace_cwd = workspace_cwd
731
+ self._judge = approval_judge
732
+ self._ask_timeout = ask_user_timeout_seconds
733
+ self._allow_user_interaction = allow_user_interaction
734
+ self._trajectory_context = trajectory_context
735
+ self._trajectory_boundary_operation_id = trajectory_boundary_operation_id
736
+ self._translator: AcpUpdateTranslator | None = None
737
+ self._arbiter = ApprovalDecisionArbiter(event_bus, session_id=session_id)
738
+ self._permission_waits: dict[str, asyncio.Future[ApprovalResponse]] = {}
739
+ self._ask_waits: dict[str, asyncio.Future[AskUserResponse]] = {}
740
+ # Latched by cascade_abort() synchronously, ahead of the backgrounded
741
+ # cancel_pending_waits(). Any in-flight or freshly-arriving permission
742
+ # decision consults it so an already-settled "allow" cannot slip
743
+ # through after the global interrupt.
744
+ self._aborted = False
745
+
746
+ def mark_aborted(self) -> None:
747
+ """Latch the abort flag synchronously (called from cascade_abort)."""
748
+ self._aborted = True
749
+
750
+ def set_translator(self, translator: AcpUpdateTranslator) -> None:
751
+ self._translator = translator
752
+
753
+ def bind_trajectory(self, context: TrajectoryContext | None, *, boundary_operation_id: str | None) -> None:
754
+ """Bind the caller's current pass before any remote permission requests arrive."""
755
+ self._trajectory_context = context
756
+ self._trajectory_boundary_operation_id = boundary_operation_id
757
+
758
+ async def on_update(self, seq: int, notification: SessionNotification) -> None:
759
+ translator = self._translator
760
+ if translator is not None:
761
+ await translator.on_update(seq, notification)
762
+
763
+ async def on_permission_request(
764
+ self,
765
+ tool_call: ToolCallUpdate,
766
+ options: Sequence[PermissionOption],
767
+ ) -> PermissionDecision:
768
+ # Every permission request lands in the owner-only audit ring with its
769
+ # outcome — including ones the remote sends before any ToolCallStart,
770
+ # which would otherwise reach no durable artifact.
771
+ decision = PermissionDecision.cancelled()
772
+ try:
773
+ decision = await self._decide_permission(tool_call, options)
774
+ return decision
775
+ finally:
776
+ self._record_permission_audit(tool_call, decision)
777
+
778
+ def _record_permission_audit(self, tool_call: ToolCallUpdate, decision: PermissionDecision) -> None:
779
+ translator = self._translator
780
+ if translator is None:
781
+ return
782
+ outcome = {"allow": "allowed", "deny": "denied"}.get(decision.action, "cancelled")
783
+ translator.record_permission_request(
784
+ title=str(tool_call.title or "tool"),
785
+ kind=display_tool_kind(tool_call.kind),
786
+ raw_input=tool_call.raw_input,
787
+ outcome=outcome,
788
+ )
789
+
790
+ async def _decide_permission(
791
+ self,
792
+ tool_call: ToolCallUpdate,
793
+ options: Sequence[PermissionOption],
794
+ ) -> PermissionDecision:
795
+ # A cascade abort already fired: never open a fresh permission — even a
796
+ # BYPASS-mode auto-allow — after the global interrupt.
797
+ if self._aborted:
798
+ return PermissionDecision.cancelled()
799
+ allow = _select_option(options, _ALLOW_KINDS)
800
+ reject = _select_option(options, _REJECT_KINDS)
801
+ # Polarity is checked before every mode, including BYPASS.
802
+ if allow is None:
803
+ return PermissionDecision.deny(reject.option_id) if reject is not None else PermissionDecision.cancelled()
804
+ mode = self._mode_getter()
805
+ if mode == ApprovalMode.BYPASS:
806
+ return PermissionDecision.allow(allow.option_id)
807
+
808
+ approval_trace = ApprovalTrace.open_for_operation(
809
+ context=self._trajectory_context,
810
+ target_operation_id=self._trajectory_boundary_operation_id,
811
+ )
812
+ async with OneShotCorrelation(self._bus, ApprovalResponse) as correlation:
813
+ request_id = correlation.request_id
814
+ future = correlation.future
815
+ self._permission_waits[request_id] = future
816
+ judging = mode == ApprovalMode.AUTO and self._judge is not None
817
+ judge_title, judge_kind, raw_args = _permission_judge_fields(tool_call)
818
+ presentation_title = preview_text(tool_call.title or "tool", limit=_MAX_FIELD_CHARS - 4)
819
+ published_args = _raw_args_dict(tool_call.raw_input)
820
+ request_publish_started = False
821
+ try:
822
+ if judging:
823
+ await self._arbiter.ensure_subscription()
824
+ if approval_trace is not None:
825
+ await approval_trace.requested(
826
+ tool_name=f"acp:{presentation_title}",
827
+ approval_mode=mode.value,
828
+ approval_level="require",
829
+ )
830
+ request_publish_started = True
831
+ await self._bus.publish(
832
+ ApprovalRequest(
833
+ request_id=request_id,
834
+ caller_name=self._caller_name,
835
+ tool_name=f"acp:{presentation_title}",
836
+ tool_kind="",
837
+ # judge_kind folds in the chrys _meta enhancement (nested
838
+ # chrys declares kind precisely); the "remote" fallback
839
+ # keeps this non-empty for every bridged request so the
840
+ # dialog renders the friendly header even when the remote
841
+ # claims no recognizable kind.
842
+ presentation_kind=judge_kind or "remote",
843
+ args=published_args,
844
+ intent_summary=f"Allow remote tool: {presentation_title}",
845
+ user_message=self._turn_context.user_message,
846
+ workspace_roots=list(self._workspace_roots),
847
+ workspace_cwd=self._workspace_cwd,
848
+ judging=judging,
849
+ session_id=self._session_id,
850
+ )
851
+ )
852
+ except asyncio.CancelledError:
853
+ self._permission_waits.pop(request_id, None)
854
+ if approval_trace is not None:
855
+ approval_trace.interrupted_soon(reason_code="cancelled")
856
+ if request_publish_started:
857
+ await self._publish_initial_dialog_cleanup(
858
+ ApprovalCancelled(request_id=request_id, session_id=self._session_id)
859
+ )
860
+ return PermissionDecision.cancelled()
861
+ except BaseException:
862
+ self._permission_waits.pop(request_id, None)
863
+ if approval_trace is not None:
864
+ approval_trace.interrupted_soon(reason_code="failed")
865
+ raise
866
+ judge_task: asyncio.Task[None] | None = None
867
+ try:
868
+ if judging:
869
+ judge_task = asyncio.create_task(
870
+ self._arbiter.judge(
871
+ request_id=request_id,
872
+ judge=self._judge,
873
+ judge_input=ApprovalJudgeInput(
874
+ tool_name=judge_title,
875
+ tool_kind=judge_kind,
876
+ args=raw_args,
877
+ user_message=self._turn_context.user_message,
878
+ user_messages=self._turn_context.user_messages,
879
+ workspace_roots=list(self._workspace_roots),
880
+ ),
881
+ decision_future=future,
882
+ approved_value=ApprovalResponse(
883
+ request_id=request_id,
884
+ approved=True,
885
+ session_id=self._session_id,
886
+ ),
887
+ log_dir=None,
888
+ ),
889
+ )
890
+ except BaseException:
891
+ self._permission_waits.pop(request_id, None)
892
+ if approval_trace is not None:
893
+ approval_trace.interrupted_soon(reason_code="failed")
894
+ raise
895
+ try:
896
+ response = await future
897
+ except asyncio.CancelledError:
898
+ should_notify = self._permission_waits.pop(request_id, None) is not None
899
+ if approval_trace is not None:
900
+ approval_trace.interrupted_soon(reason_code="cancelled")
901
+ if should_notify:
902
+ await self._bus.publish(ApprovalCancelled(request_id=request_id, session_id=self._session_id))
903
+ return PermissionDecision.cancelled()
904
+ finally:
905
+ self._permission_waits.pop(request_id, None)
906
+ if judge_task is not None and not judge_task.done():
907
+ judge_task.cancel()
908
+ with contextlib.suppress(asyncio.CancelledError, Exception):
909
+ await judge_task
910
+
911
+ # A cascade abort that landed while this permission was in flight must
912
+ # never resolve to allow, even when the ApprovalResponse future was
913
+ # already settled (or its resolving continuation raced ahead of the
914
+ # backgrounded cancel_pending_waits): cascade_abort calls
915
+ # broker.mark_aborted() synchronously before yielding, so by the time
916
+ # `await future` returns the flag is visible. Honoring the approval
917
+ # here would let the remote run a tool after the global interrupt.
918
+ if self._aborted:
919
+ if approval_trace is not None:
920
+ approval_trace.interrupted_soon(reason_code="cascade_abort")
921
+ return PermissionDecision.cancelled()
922
+ if response.approved:
923
+ if approval_trace is not None:
924
+ await approval_trace.resolved(
925
+ approved=True,
926
+ decider=ApprovalDecider.USER if correlation.resolved_by_event else ApprovalDecider.JUDGE,
927
+ reason_code="approved",
928
+ arguments_modified=False,
929
+ )
930
+ return PermissionDecision.allow(allow.option_id)
931
+ if approval_trace is not None:
932
+ await approval_trace.resolved(
933
+ approved=False,
934
+ decider=ApprovalDecider.USER if correlation.resolved_by_event else ApprovalDecider.JUDGE,
935
+ reason_code="rejected",
936
+ arguments_modified=False,
937
+ )
938
+ return PermissionDecision.deny(reject.option_id) if reject is not None else PermissionDecision.cancelled()
939
+
940
+ async def on_ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
941
+ if method != "chrys/request_input":
942
+ raise RequestError.method_not_found(method)
943
+ try:
944
+ questions = validate_request_input_params(params)
945
+ except ValueError as exc:
946
+ raise RequestError.invalid_params({"method": method}) from exc
947
+ if not self._allow_user_interaction:
948
+ # The same cancellation reply serves aborted and expired requests.
949
+ # Do not publish QuestionToUser: noninteractive owners have no
950
+ # response channel to service it.
951
+ return {"cancelled": True}
952
+ caller = params.get("callerName")
953
+ caller_name = preview_text(caller, limit=_MAX_FIELD_CHARS) if isinstance(caller, str) else self._caller_name
954
+
955
+ # A cascade abort already fired: do not open a fresh question dialog
956
+ # (symmetric with _decide_permission). Return the same cancellation the
957
+ # timeout/cancel paths use so the remote cannot proceed post-interrupt.
958
+ if self._aborted:
959
+ return {"cancelled": True}
960
+ wait = WaitTrace.open(
961
+ WaitCategory.USER_INPUT,
962
+ target_operation_id=self._trajectory_boundary_operation_id,
963
+ context=self._trajectory_context,
964
+ )
965
+ async with OneShotCorrelation(self._bus, AskUserResponse) as correlation:
966
+ self._ask_waits[correlation.request_id] = correlation.future
967
+ question_publish_started = False
968
+ try:
969
+ if wait is not None:
970
+ await wait.started()
971
+ question_publish_started = True
972
+ await self._bus.publish(
973
+ QuestionToUser(
974
+ questions=questions,
975
+ request_id=correlation.request_id,
976
+ caller_name=caller_name or self._caller_name,
977
+ session_id=self._session_id,
978
+ )
979
+ )
980
+ except asyncio.CancelledError:
981
+ self._ask_waits.pop(correlation.request_id, None)
982
+ if wait is not None:
983
+ wait.finished_soon(outcome=WaitOutcome.CANCELLED)
984
+ if question_publish_started:
985
+ await self._publish_initial_dialog_cleanup(
986
+ AskUserTimedOut(request_id=correlation.request_id, session_id=self._session_id)
987
+ )
988
+ return {"cancelled": True}
989
+ except BaseException:
990
+ self._ask_waits.pop(correlation.request_id, None)
991
+ if wait is not None:
992
+ wait.finished_soon(outcome=WaitOutcome.FAILED)
993
+ raise
994
+ try:
995
+ if self._ask_timeout is None:
996
+ response = await correlation.future
997
+ else:
998
+ async with asyncio.timeout(self._ask_timeout):
999
+ response = await correlation.future
1000
+ except TimeoutError:
1001
+ if wait is not None:
1002
+ wait.finished_soon(outcome=WaitOutcome.TIMED_OUT)
1003
+ await self._bus.publish(AskUserTimedOut(request_id=correlation.request_id, session_id=self._session_id))
1004
+ return {"cancelled": True}
1005
+ except asyncio.CancelledError:
1006
+ should_notify = self._ask_waits.pop(correlation.request_id, None) is not None
1007
+ if wait is not None:
1008
+ wait.finished_soon(outcome=WaitOutcome.CANCELLED)
1009
+ if should_notify:
1010
+ await self._bus.publish(
1011
+ AskUserTimedOut(
1012
+ request_id=correlation.request_id,
1013
+ session_id=self._session_id,
1014
+ )
1015
+ )
1016
+ return {"cancelled": True}
1017
+ finally:
1018
+ self._ask_waits.pop(correlation.request_id, None)
1019
+ # An abort that latched while the answer future was settling (its
1020
+ # continuation racing ahead of cancel_pending_waits) must not hand the
1021
+ # remote a real answer after the global interrupt.
1022
+ if self._aborted:
1023
+ if wait is not None:
1024
+ await wait.finished(outcome=WaitOutcome.CANCELLED)
1025
+ return {"cancelled": True}
1026
+ if wait is not None:
1027
+ await wait.finished()
1028
+ if response.cancelled:
1029
+ return {"cancelled": True}
1030
+ answers = parse_ask_user_answers(response.answers or (), question_count=len(questions))
1031
+ payload: dict[str, Any] = {
1032
+ "answers": [{"values": list(answer.values), "note": answer.note} for answer in answers],
1033
+ "cancelled": False,
1034
+ }
1035
+ if isinstance(validate_request_input_response(payload, questions=questions), AskUserCancelled):
1036
+ return {"cancelled": True}
1037
+ return payload
1038
+
1039
+ async def _publish_initial_dialog_cleanup(self, event: ApprovalCancelled | AskUserTimedOut) -> None:
1040
+ """Finish cleanup even when cancellation interrupted the opening publish."""
1041
+ task = asyncio.create_task(self._bus.publish(event))
1042
+ with contextlib.suppress(asyncio.CancelledError):
1043
+ await drain_acp_task(task)
1044
+
1045
+ async def cancel_pending_waits(self) -> None:
1046
+ permission_waits = list(self._permission_waits.items())
1047
+ ask_waits = list(self._ask_waits.items())
1048
+ for request_id, future in permission_waits:
1049
+ if self._permission_waits.pop(request_id, None) is None:
1050
+ continue
1051
+ if not future.done():
1052
+ future.cancel()
1053
+ await self._bus.publish(ApprovalCancelled(request_id=request_id, session_id=self._session_id))
1054
+ for request_id, future in ask_waits:
1055
+ if self._ask_waits.pop(request_id, None) is None:
1056
+ continue
1057
+ if not future.done():
1058
+ future.cancel()
1059
+ await self._bus.publish(AskUserTimedOut(request_id=request_id, session_id=self._session_id))
1060
+
1061
+ async def close(self) -> None:
1062
+ await self.cancel_pending_waits()
1063
+ await self._arbiter.close()
1064
+
1065
+
1066
+ def _select_option(
1067
+ options: Sequence[PermissionOption],
1068
+ preferred_kinds: Sequence[str],
1069
+ ) -> PermissionOption | None:
1070
+ for kind in preferred_kinds:
1071
+ for option in options:
1072
+ if option.kind == kind:
1073
+ return option
1074
+ return None
1075
+
1076
+
1077
+ def _permission_judge_fields(tool_call: ToolCallUpdate) -> tuple[str, str, dict[str, Any]]:
1078
+ title = preview_text(tool_call.title or "tool", limit=_MAX_FIELD_CHARS)
1079
+ kind = display_tool_kind(tool_call.kind)
1080
+ metadata = tool_call.field_meta or {}
1081
+ chrys_meta = metadata.get("chrys")
1082
+ if isinstance(chrys_meta, Mapping):
1083
+ remote_name = chrys_meta.get("tool_name", chrys_meta.get("toolName"))
1084
+ remote_kind = chrys_meta.get("tool_kind", chrys_meta.get("toolKind"))
1085
+ if isinstance(remote_name, str) and remote_name:
1086
+ title = preview_text(remote_name, limit=_MAX_FIELD_CHARS)
1087
+ if isinstance(remote_kind, str):
1088
+ kind = preview_text(remote_kind, limit=_MAX_FIELD_CHARS)
1089
+ return title, kind, _raw_args_dict(tool_call.raw_input)
1090
+
1091
+
1092
+ async def drain_acp_task(task: asyncio.Future[Any]) -> None:
1093
+ """Await *task* to completion, absorbing external cancels of the awaiter.
1094
+
1095
+ ``asyncio.shield`` only stops an external cancel from cancelling the SHIELDED
1096
+ task — the awaiter still receives ``CancelledError``, so a single
1097
+ ``await asyncio.shield(task)`` abandons *task* the instant a SECOND interrupt
1098
+ (or shutdown) lands on the awaiter. Re-await in a loop until the task is
1099
+ genuinely done, then re-raise ``CancelledError`` if any external cancel was
1100
+ absorbed. A cancel raised inside the task's own body still surfaces via
1101
+ ``task.result()``.
1102
+
1103
+ This is the single shield-loop the ACP teardown/flush/cascade-publish paths
1104
+ all funnel through, so no future call site can reintroduce the naive
1105
+ single-shield hazard.
1106
+ """
1107
+ cancelled = False
1108
+ while not task.done():
1109
+ try:
1110
+ await asyncio.shield(task)
1111
+ except asyncio.CancelledError:
1112
+ cancelled = True
1113
+ task.result()
1114
+ if cancelled:
1115
+ raise asyncio.CancelledError