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,1796 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """ACP server adapter for Chrys."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import contextlib
9
+ from datetime import datetime
10
+ from math import isfinite
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from acp import PROTOCOL_VERSION, RequestError
14
+ from acp import schema as acp_schema
15
+ from acp.interfaces import Client
16
+
17
+ from chrys import __version__
18
+ from chrys.app.acp.bridge import (
19
+ AcpEventBridge,
20
+ acp_tool_kind,
21
+ plan_update_for_todos,
22
+ session_title_info_update,
23
+ tool_call_title,
24
+ )
25
+ from chrys.app.acp.content import AcpContentError, ContentBlock, convert_prompt_blocks
26
+ from chrys.app.acp.history import replay_session_history
27
+ from chrys.app.acp.session_manager import AcpSessionError, AcpSessionManager, ManagedSession, jsonable_dataclass
28
+ from chrys.foundation.branding import APP_DISPLAY_NAME
29
+ from chrys.foundation.events.types import (
30
+ AgentLoadFailed,
31
+ AgentLoadFinished,
32
+ AgentLoadProgress,
33
+ AgentLoadStarted,
34
+ AgentRuntimeUpdated,
35
+ ApprovalCancelled,
36
+ ApprovalModeUpdated,
37
+ ApprovalRequest,
38
+ ApprovalResponse,
39
+ ApprovalReviewed,
40
+ AskUserResponse,
41
+ AskUserTimedOut,
42
+ CompactionFinished,
43
+ CompactionStarted,
44
+ ContextCompressed,
45
+ Error,
46
+ Event,
47
+ InvocationAborted,
48
+ InvocationCascadeAborted,
49
+ InvocationCompactionCommitted,
50
+ InvocationCompactionFinished,
51
+ InvocationCompactionStarted,
52
+ InvocationContextPressure,
53
+ InvocationEvent,
54
+ InvocationPaused,
55
+ InvocationProgress,
56
+ InvocationResumed,
57
+ InvocationRetryAttempt,
58
+ InvocationStarted,
59
+ InvocationToolCallResult,
60
+ InvocationToolCallStart,
61
+ ProfileSwitched,
62
+ QuestionToUser,
63
+ RollbackResult,
64
+ SessionReady,
65
+ SessionRestored,
66
+ SessionSaved,
67
+ SessionTitleUpdated,
68
+ ToolCompacted,
69
+ UsageUpdate,
70
+ UserInjectResult,
71
+ Warning,
72
+ WorkspaceUpdated,
73
+ )
74
+ from chrys.foundation.models.ask_user import AskUserQuestion, validate_request_input_response
75
+ from chrys.foundation.text.encoding import EncodingDetector, decode_bytes
76
+ from chrys.orchestration.session_host import Cancelled, EndTurn, Errored
77
+
78
+ if TYPE_CHECKING:
79
+ from chrys.service.mutations.types import FileHashDiff
80
+
81
+ _ALLOW_OPTION_ID = "allow"
82
+ _REJECT_OPTION_ID = "reject"
83
+ _PERMISSION_REQUEST_TIMEOUT_SECONDS = 600.0
84
+
85
+ # Approval modes surfaced as standard ACP session modes (id, name, description).
86
+ _APPROVAL_MODES: tuple[tuple[str, str, str], ...] = (
87
+ ("manual", "Manual", "Every tool call that requires approval shows a prompt."),
88
+ ("auto", "Auto", "An LLM judge evaluates calls; only flagged ones prompt."),
89
+ ("bypass", "Bypass", "All tool calls are auto-approved."),
90
+ )
91
+ _APPROVAL_MODE_IDS = frozenset(mode_id for mode_id, _, _ in _APPROVAL_MODES)
92
+
93
+
94
+ def _request_input_question_payload(question: AskUserQuestion) -> dict[str, object]:
95
+ return {
96
+ "question": question.question,
97
+ "header": question.header,
98
+ "options": [{"label": option.label, "description": option.description} for option in question.options],
99
+ "multiSelect": question.multi_select,
100
+ }
101
+
102
+
103
+ class ChrysAcpServer:
104
+ """ACP request handlers backed by ``ChrysSessionHost``."""
105
+
106
+ def __init__(
107
+ self,
108
+ manager: AcpSessionManager,
109
+ *,
110
+ initial_vision: bool,
111
+ permission_timeout_seconds: float = _PERMISSION_REQUEST_TIMEOUT_SECONDS,
112
+ ) -> None:
113
+ if not isfinite(permission_timeout_seconds) or permission_timeout_seconds <= 0:
114
+ raise ValueError("permission_timeout_seconds must be finite and greater than zero.")
115
+ self._manager = manager
116
+ self._initial_vision = initial_vision
117
+ self._permission_timeout_seconds = permission_timeout_seconds
118
+ self._client: Client | None = None
119
+ self._pending_input_cancels: dict[tuple[str, str], asyncio.Future[None]] = {}
120
+ self._pending_permission_cancels: dict[tuple[str, str], asyncio.Future[None]] = {}
121
+ self._pending_permission_tasks: dict[tuple[str, str], asyncio.Task[acp_schema.RequestPermissionResponse]] = {}
122
+ self._input_cancel_tombstones: set[tuple[str, str]] = set()
123
+ self._permission_cancel_tombstones: set[tuple[str, str]] = set()
124
+
125
+ def on_connect(self, conn: Client) -> None:
126
+ """Capture the ACP client connection used for server-to-client requests."""
127
+ self._client = conn
128
+
129
+ async def initialize(
130
+ self,
131
+ protocol_version: int,
132
+ client_capabilities: acp_schema.ClientCapabilities | None = None,
133
+ client_info: acp_schema.Implementation | None = None,
134
+ **kwargs: Any,
135
+ ) -> acp_schema.InitializeResponse:
136
+ """Return Chrys ACP capabilities."""
137
+ _ = client_capabilities, client_info, kwargs
138
+ return acp_schema.InitializeResponse(
139
+ protocolVersion=min(protocol_version, PROTOCOL_VERSION),
140
+ agentInfo=acp_schema.Implementation(name=APP_DISPLAY_NAME, version=__version__),
141
+ agentCapabilities=acp_schema.AgentCapabilities(
142
+ # Keep this explicit until the ACP SDK replaces its invalid
143
+ # bare-dict auth default; the TUI normalizes omitted remote
144
+ # values in panels/acp.py::_handshake_payload.
145
+ auth=None,
146
+ loadSession=True,
147
+ promptCapabilities=acp_schema.PromptCapabilities(
148
+ embeddedContext=True,
149
+ image=self._initial_vision,
150
+ audio=False,
151
+ ),
152
+ mcpCapabilities=acp_schema.McpCapabilities(http=True, sse=False),
153
+ sessionCapabilities=acp_schema.SessionCapabilities(
154
+ close=acp_schema.SessionCloseCapabilities(),
155
+ list=acp_schema.SessionListCapabilities(),
156
+ additionalDirectories=acp_schema.SessionAdditionalDirectoriesCapabilities(),
157
+ ),
158
+ ),
159
+ )
160
+
161
+ async def _watch_session_titles(self, session: ManagedSession) -> None:
162
+ """Forward ``SessionTitleUpdated`` as ``session_info_update`` for a session's lifetime.
163
+
164
+ Title generation is a post-turn side call, so its event lands after
165
+ the prompt's event stream has already closed — only a long-lived
166
+ subscription on the session's bus can deliver it live. The
167
+ subscription dies with the host bus when the session closes.
168
+ """
169
+ session_id = session.session_id
170
+
171
+ async def _on_title_updated(event: SessionTitleUpdated) -> None:
172
+ if event.session_id != session_id or self._client is None:
173
+ return
174
+ with contextlib.suppress(Exception):
175
+ await self._client.session_update(
176
+ session_id=session_id,
177
+ update=session_title_info_update(event),
178
+ )
179
+
180
+ await session.host.event_bus.subscribe(SessionTitleUpdated, _on_title_updated)
181
+
182
+ async def _watch_nested_wait_cancellations(self, session: ManagedSession) -> None:
183
+ """Subscribe out of band so serialized prompt delivery cannot hide cancellation."""
184
+ session_id = session.session_id
185
+
186
+ async def _on_approval_cancelled(event: ApprovalCancelled) -> None:
187
+ if event.session_id != session_id:
188
+ return
189
+ self._deliver_or_record_cancellation(
190
+ self._pending_permission_cancels,
191
+ self._permission_cancel_tombstones,
192
+ (session_id, event.request_id),
193
+ )
194
+
195
+ async def _on_ask_user_cleared(event: AskUserTimedOut) -> None:
196
+ if event.session_id != session_id:
197
+ return
198
+ self._deliver_or_record_cancellation(
199
+ self._pending_input_cancels,
200
+ self._input_cancel_tombstones,
201
+ (session_id, event.request_id),
202
+ )
203
+
204
+ await session.host.event_bus.subscribe(ApprovalCancelled, _on_approval_cancelled)
205
+ await session.host.event_bus.subscribe(AskUserTimedOut, _on_ask_user_cleared)
206
+
207
+ async def new_session(
208
+ self,
209
+ cwd: str | None = None,
210
+ additional_directories: list[str] | None = None,
211
+ mcp_servers: list[acp_schema.HttpMcpServer | acp_schema.SseMcpServer | acp_schema.McpServerStdio] | None = None,
212
+ **kwargs: Any,
213
+ ) -> acp_schema.NewSessionResponse:
214
+ """Create a new Chrys session."""
215
+ _ = kwargs
216
+ # Session creation runs outside any prompt turn, so the settings
217
+ # warnings its load produced — a denied project key, say — have no
218
+ # forwarder; collect and send them once the session exists.
219
+ warnings: list[Warning] = []
220
+ try:
221
+ session = await self._manager.new_session(
222
+ cwd=cwd,
223
+ additional_directories=additional_directories,
224
+ mcp_servers=mcp_servers,
225
+ warnings=warnings,
226
+ )
227
+ except Exception as exc:
228
+ raise _request_error(exc) from exc
229
+ # The session is in the active map from here on, but its id has not
230
+ # reached the caller yet: any failure below — the sends losing the
231
+ # client, or this task being cancelled — would strand a session
232
+ # nobody can ever address, so the just-created session is closed
233
+ # before the failure propagates.
234
+ try:
235
+ await self._watch_session_titles(session)
236
+ await self._watch_nested_wait_cancellations(session)
237
+ for warning in warnings:
238
+ await self._send_warning(session.session_id, warning)
239
+ await self._send_runtime_update(session.session_id)
240
+ await self._send_plan_update(session.session_id)
241
+ except BaseException:
242
+ with contextlib.suppress(Exception):
243
+ await self._manager.close(session.session_id)
244
+ raise
245
+ return acp_schema.NewSessionResponse(
246
+ sessionId=session.session_id,
247
+ modes=self._session_mode_state(session.session_id),
248
+ models=self._session_model_state(session.session_id),
249
+ )
250
+
251
+ async def load_session(
252
+ self,
253
+ session_id: str,
254
+ cwd: str | None = None,
255
+ additional_directories: list[str] | None = None,
256
+ mcp_servers: list[acp_schema.HttpMcpServer | acp_schema.SseMcpServer | acp_schema.McpServerStdio] | None = None,
257
+ **kwargs: Any,
258
+ ) -> acp_schema.LoadSessionResponse:
259
+ """Load a saved Chrys session."""
260
+ _ = kwargs
261
+ # Same gap as ``new_session``: the load happens outside any prompt
262
+ # turn, so its settings warnings are only delivered if collected here.
263
+ warnings: list[Warning] = []
264
+ try:
265
+ result = await self._manager.load_session(
266
+ cwd=cwd,
267
+ session_id=session_id,
268
+ additional_directories=additional_directories,
269
+ mcp_servers=mcp_servers,
270
+ warnings=warnings,
271
+ )
272
+ session = result.session
273
+ # One cleanup scope for everything after the load: a session this
274
+ # request created must not stay in the active map when any of the
275
+ # follow-up sends — warnings included — loses the client, nor when
276
+ # the request task is cancelled mid-establishment.
277
+ try:
278
+ if not result.reused_existing:
279
+ # A reused active session already has a title watcher from
280
+ # the request that created it.
281
+ await self._watch_session_titles(session)
282
+ await self._watch_nested_wait_cancellations(session)
283
+ if self._client is not None:
284
+ for warning in warnings:
285
+ await self._send_warning(session.session_id, warning)
286
+ await self._send_session_restored(session.session_id)
287
+ await replay_session_history(
288
+ self._client,
289
+ self._manager.state_store,
290
+ session.session_id,
291
+ prefer_recovery=result.recovered_from_sidecar,
292
+ tool_kind_resolver=self._manager.tool_kind_resolver(session.session_id),
293
+ )
294
+ await self._send_runtime_update(session.session_id)
295
+ await self._send_plan_update(session.session_id)
296
+ except BaseException:
297
+ if not result.reused_existing:
298
+ with contextlib.suppress(Exception):
299
+ await self._manager.close(session.session_id)
300
+ raise
301
+ except Exception as exc:
302
+ raise _request_error(exc) from exc
303
+ return acp_schema.LoadSessionResponse(
304
+ modes=self._session_mode_state(session.session_id),
305
+ models=self._session_model_state(session.session_id),
306
+ )
307
+
308
+ async def list_sessions(
309
+ self,
310
+ additional_directories: list[str] | None = None,
311
+ cursor: str | None = None,
312
+ cwd: str | None = None,
313
+ **kwargs: Any,
314
+ ) -> acp_schema.ListSessionsResponse:
315
+ """List saved sessions for this workspace cwd."""
316
+ _ = kwargs
317
+ try:
318
+ sessions, next_cursor = await self._manager.list_sessions(
319
+ cwd=cwd, cursor=cursor, additional_directories=additional_directories
320
+ )
321
+ except Exception as exc:
322
+ raise _request_error(exc) from exc
323
+ return acp_schema.ListSessionsResponse(sessions=sessions, nextCursor=next_cursor)
324
+
325
+ async def prompt(
326
+ self,
327
+ prompt: list[ContentBlock],
328
+ session_id: str,
329
+ message_id: str | None = None,
330
+ **kwargs: Any,
331
+ ) -> acp_schema.PromptResponse:
332
+ """Run one ACP prompt turn."""
333
+ _ = kwargs
334
+ if self._client is None:
335
+ raise RequestError.internal_error({"details": "ACP client connection is not ready."})
336
+ bridge = AcpEventBridge()
337
+ pending_approvals: dict[str, ApprovalRequest] = {}
338
+ outcome: EndTurn | Cancelled | Errored | None = None
339
+ usage_payload: dict[str, int] | None = None
340
+ execution_started = False
341
+ try:
342
+ session = self._manager.get(session_id)
343
+ async with session.prompt_lock:
344
+ try:
345
+ # A close/delete may have started while this prompt waited for
346
+ # the per-session lock. Re-resolve before touching the host so
347
+ # a queued prompt cannot run against a closing session.
348
+ if self._manager.get(session_id) is not session:
349
+ raise AcpSessionError(f"ACP session is not active: {session_id}")
350
+ converted = convert_prompt_blocks(prompt, vision_enabled=session.host.vision_enabled)
351
+ execution_started = True
352
+ async for event in session.host.iter_turn_events(converted.message):
353
+ await self._handle_event(session_id, event, bridge, pending_approvals)
354
+ outcome = session.host.last_turn_outcome
355
+ finally:
356
+ # This snapshot stays under prompt_lock: a queued turn must
357
+ # not mutate cumulative session usage before this response
358
+ # (or error) captures the completed turn's total.
359
+ if execution_started:
360
+ usage_payload = _prompt_usage_payload(
361
+ session.host.engine.usage_publisher.make_usage_event(session_id=session_id)
362
+ )
363
+ # A cancellation tombstone only bridges to a request event
364
+ # still queued in THIS turn's per-turn stream. Judge-aborted
365
+ # approvals and requests dropped by cancellation never send
366
+ # that event, so whatever survives the stream is dead —
367
+ # sweep it or a long-lived session accumulates tombstones.
368
+ self._drop_session_tombstones(session_id)
369
+ except AcpContentError as exc:
370
+ raise RequestError.invalid_params({"details": str(exc)}) from exc
371
+ except Exception as exc:
372
+ raise _request_error(exc, usage=usage_payload) from exc
373
+
374
+ if isinstance(outcome, EndTurn):
375
+ return acp_schema.PromptResponse(
376
+ stopReason="end_turn",
377
+ userMessageId=message_id,
378
+ usage=acp_schema.Usage.model_validate(usage_payload),
379
+ )
380
+ if isinstance(outcome, Cancelled):
381
+ return acp_schema.PromptResponse(
382
+ stopReason="cancelled",
383
+ userMessageId=message_id,
384
+ usage=acp_schema.Usage.model_validate(usage_payload),
385
+ )
386
+ if isinstance(outcome, Errored):
387
+ error = outcome.error
388
+ raise RequestError.internal_error(
389
+ {
390
+ "code": error.code,
391
+ "message": error.message,
392
+ "usage": usage_payload,
393
+ }
394
+ )
395
+ raise RequestError.internal_error(
396
+ {
397
+ "details": f"{APP_DISPLAY_NAME} turn ended without an outcome.",
398
+ "usage": usage_payload,
399
+ }
400
+ )
401
+
402
+ async def cancel(self, session_id: str, **kwargs: Any) -> None:
403
+ """Cancel the active prompt for a session."""
404
+ _ = kwargs
405
+ try:
406
+ await self._manager.cancel(session_id)
407
+ except AcpSessionError:
408
+ pass
409
+ finally:
410
+ # Interrupt the engine first. A rejected approval is not turn
411
+ # terminating, so releasing the bridge wait before interruption
412
+ # could feed one extra doomed model call.
413
+ self._cancel_pending_waits(session_id)
414
+
415
+ async def close_session(self, session_id: str, **kwargs: Any) -> acp_schema.CloseSessionResponse:
416
+ """Close an active ACP session."""
417
+ _ = kwargs
418
+ # Stop prompt admission before releasing waits: once the pending
419
+ # ask-user/permission callbacks resolve, a prompt queued on the
420
+ # session's prompt lock would otherwise pass its re-check and start a
421
+ # fresh turn that manager.close then wedges behind.
422
+ await self._manager.begin_close(session_id)
423
+ # The wait release is inside the try: cancelling this RPC mid-release
424
+ # must still drop the session's cancellation state in the finally.
425
+ try:
426
+ await self._interrupt_and_cancel_pending_waits(session_id)
427
+ await self._manager.close(session_id)
428
+ except Exception as exc:
429
+ raise _request_error(exc) from exc
430
+ finally:
431
+ self._clear_cancellation_state(session_id)
432
+ return acp_schema.CloseSessionResponse()
433
+
434
+ async def set_session_mode(self, mode_id: str, session_id: str, **kwargs: Any) -> acp_schema.SetSessionModeResponse:
435
+ """Set the session approval mode (standard ACP session mode)."""
436
+ _ = kwargs
437
+ if mode_id not in _APPROVAL_MODE_IDS:
438
+ raise RequestError.invalid_params({"details": "mode_id must be manual, auto, or bypass."})
439
+ try:
440
+ result = await self._manager.set_approval_mode(session_id, mode_id)
441
+ except Exception as exc:
442
+ raise _request_error(exc) from exc
443
+ if self._client is not None:
444
+ with contextlib.suppress(Exception):
445
+ await self._client.session_update(
446
+ session_id=session_id,
447
+ update=acp_schema.CurrentModeUpdate(
448
+ sessionUpdate="current_mode_update", currentModeId=result.mode or mode_id
449
+ ),
450
+ )
451
+ return acp_schema.SetSessionModeResponse()
452
+
453
+ async def set_session_model(
454
+ self, model_id: str, session_id: str, **kwargs: Any
455
+ ) -> acp_schema.SetSessionModelResponse:
456
+ """Switch the session model (standard ACP) — per-session, no global .env write."""
457
+ _ = kwargs
458
+ try:
459
+ await self._manager.set_model_profile(session_id, model_id)
460
+ except Exception as exc:
461
+ raise _request_error(exc) from exc
462
+ await self._send_runtime_update(session_id)
463
+ return acp_schema.SetSessionModelResponse()
464
+
465
+ async def delete_session(
466
+ self,
467
+ session_id: str,
468
+ cwd: str | None = None,
469
+ additional_directories: list[str] | None = None,
470
+ **kwargs: Any,
471
+ ) -> dict[str, Any]:
472
+ """Delete a saved Chrys session scoped to the requested workspace."""
473
+ _ = kwargs
474
+ self._reject_additional_directories(additional_directories)
475
+ try:
476
+ canonical_id = await self._manager.begin_delete_session(cwd=cwd, session_id=session_id)
477
+ except Exception as exc:
478
+ raise _request_error(exc) from exc
479
+ # Deleting an active session tears it down like close: once the
480
+ # request validates, release pending client waits under the canonical
481
+ # id so the manager's prompt-lock-protected teardown cannot wedge
482
+ # behind an unresolved ask-user/permission callback. The raw id may be
483
+ # a short id that would miss the engine and the wait maps, and a
484
+ # request rejected for workspace scoping must not interrupt the turn.
485
+ # begin_delete_session also marks the session closing, so a prompt
486
+ # queued behind the released waits is rejected instead of being
487
+ # admitted into a fresh turn the teardown would wedge behind.
488
+ try:
489
+ await self._interrupt_and_cancel_pending_waits(canonical_id)
490
+ await self._manager.finish_delete_session(canonical_id)
491
+ except Exception as exc:
492
+ raise _request_error(exc) from exc
493
+ finally:
494
+ self._clear_cancellation_state(canonical_id)
495
+ return {}
496
+
497
+ async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
498
+ """Handle Chrys ACP extension requests from clients."""
499
+ if method == "session/delete":
500
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
501
+ cwd = _optional_string_param(params, "cwd")
502
+ additional_directories = _string_list_param(params, "additionalDirectories")
503
+ await self.delete_session(
504
+ session_id=session_id,
505
+ cwd=cwd,
506
+ additional_directories=additional_directories,
507
+ )
508
+ return {}
509
+ if method == "chrys/session_runtime":
510
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
511
+ return self._runtime_payload(session_id)
512
+ if method == "session/inject":
513
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
514
+ text = _string_param(params, "text")
515
+ if not text:
516
+ raise RequestError.invalid_params({"details": "text is required."})
517
+ try:
518
+ await self._manager.inject(session_id, text)
519
+ except Exception as exc:
520
+ raise _request_error(exc) from exc
521
+ return {}
522
+ if method == "session/mutations":
523
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
524
+ await self._refresh_mutation_attribution(session_id)
525
+ return self._mutations_payload(session_id)
526
+ if method == "session/diff":
527
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
528
+ path = _optional_string_param(params, "path")
529
+ turn = _optional_int_param(params, "turn")
530
+ await self._refresh_mutation_attribution(session_id)
531
+ return self._diff_payload(session_id, path=path, turn=turn)
532
+ if method == "session/rollback":
533
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
534
+ target_turn = _int_param(params, "targetTurn")
535
+ revert_changes = _bool_param(params, "revertChanges", default=False)
536
+ selected_paths = _string_list_param(params, "selectedPaths")
537
+ try:
538
+ result = await self._manager.rollback(
539
+ session_id,
540
+ target_turn=target_turn,
541
+ revert_changes=revert_changes,
542
+ selected_paths=selected_paths,
543
+ )
544
+ except Exception as exc:
545
+ raise _request_error(exc) from exc
546
+ payload = _rollback_result_payload(result)
547
+ await self._client_or_error().ext_notification("chrys/rollback_result", payload)
548
+ if target_turn > 0:
549
+ await replay_session_history(
550
+ self._client_or_error(),
551
+ self._manager.state_store,
552
+ session_id,
553
+ tool_kind_resolver=self._manager.tool_kind_resolver(session_id),
554
+ )
555
+ await self._send_runtime_update(session_id)
556
+ # Both branches: >0 re-seeds the rolled-back list, ==0 (reset
557
+ # to welcome) clears the client plan.
558
+ await self._send_plan_update(session_id)
559
+ return payload
560
+ if method == "sub_agent/retry":
561
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
562
+ invocation_id = _string_param(params, "invocationId") or _string_param(params, "invocation_id")
563
+ if not invocation_id:
564
+ raise RequestError.invalid_params({"details": "invocationId is required."})
565
+ try:
566
+ await self._manager.retry_sub_agent(session_id, invocation_id)
567
+ except Exception as exc:
568
+ raise _request_error(exc) from exc
569
+ return {}
570
+ if method == "sub_agent/abort":
571
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
572
+ invocation_id = _string_param(params, "invocationId") or _string_param(params, "invocation_id")
573
+ if not invocation_id:
574
+ raise RequestError.invalid_params({"details": "invocationId is required."})
575
+ try:
576
+ await self._manager.abort_sub_agent(session_id, invocation_id)
577
+ except Exception as exc:
578
+ raise _request_error(exc) from exc
579
+ return {}
580
+ if method == "session/skip_sleep":
581
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
582
+ call_id = _string_param(params, "callId") or _string_param(params, "call_id")
583
+ if not call_id:
584
+ raise RequestError.invalid_params({"details": "callId is required."})
585
+ try:
586
+ await self._manager.skip_sleep(session_id, call_id)
587
+ except Exception as exc:
588
+ raise _request_error(exc) from exc
589
+ return {}
590
+ if method == "session/switch_agent":
591
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
592
+ profile_name = _string_param(params, "agentProfile") or _string_param(params, "profileName")
593
+ if not profile_name:
594
+ raise RequestError.invalid_params({"details": "agentProfile is required."})
595
+ try:
596
+ result = await self._manager.switch_agent(session_id, profile_name)
597
+ except Exception as exc:
598
+ raise _request_error(exc) from exc
599
+ return _profile_switched_payload(result, session_id=session_id)
600
+ if method == "settings/reload":
601
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
602
+ # A reload happens outside any prompt turn, and ``_handle_event`` only
603
+ # runs inside one — so the warnings saying which of the client's
604
+ # values were dropped had nowhere to go and were simply lost.
605
+ warnings: list[Warning] = []
606
+ try:
607
+ await self._manager.reload_settings(session_id, warnings=warnings)
608
+ except Exception as exc:
609
+ raise _request_error(exc) from exc
610
+ for warning in warnings:
611
+ await self._send_warning(session_id, warning)
612
+ await self._send_runtime_update(session_id)
613
+ return {}
614
+ if method == "settings/options":
615
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
616
+ try:
617
+ # Reads the settings document under its file lock, so it waits
618
+ # on whatever holds it — inline, that wait is the whole ACP
619
+ # loop's, and every other session stalls behind a read.
620
+ return await asyncio.to_thread(self._manager.get_config_options, session_id or None)
621
+ except Exception as exc:
622
+ raise _request_error(exc) from exc
623
+ if method == "session/set_config_option":
624
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
625
+ key = _string_param(params, "key")
626
+ # Same gap as ``settings/reload``, and worse: this route writes the
627
+ # value to disk before reloading it, so a clamped or rejected input
628
+ # is persisted while the client is told only that it succeeded.
629
+ warnings = []
630
+ try:
631
+ result = await self._manager.apply_config_option(
632
+ session_id, key, params.get("value", ""), warnings=warnings
633
+ )
634
+ except Exception as exc:
635
+ raise _request_error(exc) from exc
636
+ for warning in warnings:
637
+ await self._send_warning(session_id, warning)
638
+ await self._send_runtime_update(session_id)
639
+ return {"sessionId": session_id, **result}
640
+ if method == "session/set_workspace":
641
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
642
+ primary_cwd = _string_param(params, "primaryCwd") or _string_param(params, "primary_cwd")
643
+ if not primary_cwd:
644
+ raise RequestError.invalid_params({"details": "primaryCwd is required."})
645
+ try:
646
+ result = await self._manager.set_workspace(session_id, primary_cwd)
647
+ except Exception as exc:
648
+ raise _request_error(exc) from exc
649
+ # A workspace change soft-restarts the agent, which can alter skills,
650
+ # MCP/runtime details, memory, and workspace-loaded context — push the
651
+ # refreshed runtime so the client doesn't keep stale state.
652
+ await self._send_runtime_update(session_id)
653
+ return {
654
+ "sessionId": result.session_id or session_id,
655
+ "primaryCwd": result.primary_cwd,
656
+ "workingDirs": result.working_dirs,
657
+ "referenceFiles": result.reference_files,
658
+ }
659
+ if method == "session/history":
660
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
661
+ cwd = _optional_string_param(params, "cwd")
662
+ try:
663
+ canonical_id, messages = await self._manager.session_history(cwd=cwd, session_id=session_id)
664
+ except Exception as exc:
665
+ raise _request_error(exc) from exc
666
+ return {"sessionId": canonical_id, "messages": messages}
667
+ if method == "profiles/agents/list":
668
+ return {"agents": self._manager.list_agent_profiles()}
669
+ if method == "profiles/agents/read":
670
+ name = _string_param(params, "name")
671
+ return {"profile": self._manager.read_agent_profile(name)}
672
+ if method == "profiles/agents/write":
673
+ profile = _dict_param(params, "profile")
674
+ try:
675
+ return self._manager.write_agent_profile(profile)
676
+ except Exception as exc:
677
+ raise _request_error(exc) from exc
678
+ if method == "profiles/agents/delete":
679
+ name = _string_param(params, "name")
680
+ try:
681
+ return self._manager.delete_agent_profile(name)
682
+ except Exception as exc:
683
+ raise _request_error(exc) from exc
684
+ if method == "profiles/agents/reset":
685
+ name = _string_param(params, "name")
686
+ try:
687
+ return self._manager.reset_agent_profile(name)
688
+ except Exception as exc:
689
+ raise _request_error(exc) from exc
690
+ if method == "profiles/models/list":
691
+ return {"models": self._manager.list_model_profiles()}
692
+ if method == "profiles/models/read":
693
+ profile_id = _string_param(params, "id") or _string_param(params, "profileId")
694
+ return {"profile": self._manager.read_model_profile(profile_id)}
695
+ if method == "profiles/models/write":
696
+ profile = _dict_param(params, "profile")
697
+ try:
698
+ return self._manager.write_model_profile(profile)
699
+ except Exception as exc:
700
+ raise _request_error(exc) from exc
701
+ if method == "profiles/models/delete":
702
+ profile_id = _string_param(params, "id") or _string_param(params, "profileId")
703
+ try:
704
+ return self._manager.delete_model_profile(profile_id)
705
+ except Exception as exc:
706
+ raise _request_error(exc) from exc
707
+ if method == "mcp/test":
708
+ config = _dict_param(params, "server")
709
+ try:
710
+ return await self._manager.test_mcp_server(config)
711
+ except Exception as exc:
712
+ raise _request_error(exc) from exc
713
+ if method == "mcp/list":
714
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
715
+ runtime = self._runtime_payload(session_id)
716
+ details = runtime.get("runtimeDetails", {})
717
+ return {
718
+ "sessionId": session_id,
719
+ "mcpTools": details.get("mcp_tools", {}),
720
+ "mcpFailures": details.get("mcp_failures", {}),
721
+ }
722
+ if method == "skills/list":
723
+ session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
724
+ runtime = self._runtime_payload(session_id)
725
+ details = runtime.get("runtimeDetails", {})
726
+ return {
727
+ "sessionId": session_id,
728
+ "skillSources": details.get("skill_sources", {}),
729
+ "skillDetails": details.get("skill_details", []),
730
+ }
731
+ raise RequestError.method_not_found(method)
732
+
733
+ async def _handle_event(
734
+ self,
735
+ session_id: str,
736
+ event: Event,
737
+ bridge: AcpEventBridge,
738
+ pending_approvals: dict[str, ApprovalRequest],
739
+ ) -> None:
740
+ if isinstance(event, ApprovalRequest):
741
+ if event.judging:
742
+ pending_approvals[event.request_id] = event
743
+ else:
744
+ await self._request_permission(session_id, event)
745
+ return
746
+ if isinstance(event, ApprovalReviewed):
747
+ await self._client_or_error().ext_notification(
748
+ "chrys/approval_reviewed",
749
+ {
750
+ "sessionId": session_id,
751
+ "requestId": event.request_id,
752
+ "approved": event.approved,
753
+ "reason": event.reason,
754
+ },
755
+ )
756
+ request = pending_approvals.pop(event.request_id, None)
757
+ if request is not None and not event.approved:
758
+ await self._request_permission(session_id, request, reason=event.reason)
759
+ return
760
+ if isinstance(event, QuestionToUser):
761
+ await self._request_input(session_id, event)
762
+ return
763
+ if await self._handle_chrys_extension_event(session_id, event):
764
+ return
765
+ for update in bridge.updates_for_event(event):
766
+ await self._client_or_error().session_update(session_id=session_id, update=update)
767
+
768
+ async def _handle_chrys_extension_event(self, session_id: str, event: Event) -> bool:
769
+ if isinstance(event, InvocationEvent) and event.origin.root.kind == "workflow_node":
770
+ return False
771
+ client = self._client_or_error()
772
+ event_session_id = _event_session_id(event, session_id)
773
+ if isinstance(event, AgentLoadStarted):
774
+ await client.ext_notification(
775
+ "chrys/agent_load_started",
776
+ {
777
+ "sessionId": event_session_id,
778
+ "operation": event.operation,
779
+ "fromProfile": event.from_profile,
780
+ "toProfile": event.to_profile,
781
+ "fromDisplayName": event.from_display_name,
782
+ "toDisplayName": event.to_display_name,
783
+ },
784
+ )
785
+ return True
786
+ if isinstance(event, AgentLoadProgress):
787
+ await client.ext_notification(
788
+ "chrys/agent_load_progress",
789
+ {
790
+ "sessionId": event_session_id,
791
+ "phase": event.phase,
792
+ "message": event.message,
793
+ "serverName": event.server_name,
794
+ "current": event.current,
795
+ "total": event.total,
796
+ "failed": event.failed,
797
+ },
798
+ )
799
+ return True
800
+ if isinstance(event, AgentLoadFinished):
801
+ await client.ext_notification(
802
+ "chrys/agent_load_finished",
803
+ {
804
+ "sessionId": event_session_id,
805
+ "operation": event.operation,
806
+ "agentProfile": event.agent_profile,
807
+ "displayName": event.display_name,
808
+ },
809
+ )
810
+ return True
811
+ if isinstance(event, AgentLoadFailed):
812
+ await client.ext_notification(
813
+ "chrys/agent_load_failed",
814
+ {
815
+ "sessionId": event_session_id,
816
+ "operation": event.operation,
817
+ "agentProfile": event.agent_profile,
818
+ "displayName": event.display_name,
819
+ "message": event.message,
820
+ },
821
+ )
822
+ return True
823
+ if isinstance(event, SessionReady):
824
+ await client.ext_notification(
825
+ "chrys/runtime_update",
826
+ {
827
+ "sessionId": event_session_id,
828
+ "runtime": {
829
+ "sessionId": event_session_id,
830
+ "agentProfile": event.agent_profile,
831
+ "displayName": event.display_name,
832
+ "modelProfileId": event.model_profile_id,
833
+ "maxContextTokens": event.max_context_tokens,
834
+ "toolNames": event.tool_names,
835
+ "toolKinds": event.tool_kinds,
836
+ "skillNames": event.skill_names,
837
+ "subAgentToolNames": event.sub_agent_tool_names,
838
+ "memoryFiles": event.memory_files,
839
+ "runtimeDetails": jsonable_dataclass(event.runtime_details),
840
+ },
841
+ },
842
+ )
843
+ return True
844
+ if isinstance(event, AgentRuntimeUpdated):
845
+ await client.ext_notification(
846
+ "chrys/runtime_update",
847
+ {
848
+ "sessionId": session_id,
849
+ "runtime": _runtime_update_payload(event),
850
+ },
851
+ )
852
+ return True
853
+ if isinstance(event, ProfileSwitched):
854
+ await client.ext_notification(
855
+ "chrys/profile_switched", _profile_switched_payload(event, session_id=session_id)
856
+ )
857
+ return True
858
+ if isinstance(event, WorkspaceUpdated):
859
+ await client.ext_notification(
860
+ "chrys/workspace_updated",
861
+ {
862
+ "sessionId": event_session_id,
863
+ "primaryCwd": event.primary_cwd,
864
+ "workingDirs": event.working_dirs,
865
+ "referenceFiles": event.reference_files,
866
+ },
867
+ )
868
+ return True
869
+ if isinstance(event, UsageUpdate):
870
+ await client.ext_notification(
871
+ "chrys/usage_update",
872
+ {
873
+ "sessionId": event_session_id,
874
+ "agentProfile": event.agent_profile,
875
+ "usageSourceId": event.usage_source_id,
876
+ "inputTokens": event.input_tokens,
877
+ "outputTokens": event.output_tokens,
878
+ "totalTokens": event.total_tokens,
879
+ "pct": event.pct,
880
+ "maxContextTokens": event.max_context_tokens,
881
+ "totalSessionTokens": event.total_session_tokens,
882
+ "totalSessionInputTokens": event.total_session_input_tokens,
883
+ "totalSessionOutputTokens": event.total_session_output_tokens,
884
+ "cacheHitTokens": event.cache_hit_tokens,
885
+ "totalSessionCacheHitTokens": event.total_session_cache_hit_tokens,
886
+ "localTokens": event.local_tokens,
887
+ "calibrationRatio": event.calibration_ratio,
888
+ "systemOverheadTokens": event.system_overhead_tokens,
889
+ },
890
+ )
891
+ return False
892
+ if isinstance(event, SessionRestored):
893
+ await client.ext_notification(
894
+ "chrys/session_restored",
895
+ {
896
+ "sessionId": event_session_id,
897
+ "agentProfile": event.agent_profile,
898
+ "displayName": event.display_name,
899
+ "messageCount": event.message_count,
900
+ "cwdWarning": event.cwd_warning,
901
+ "primaryCwd": event.primary_cwd,
902
+ },
903
+ )
904
+ return True
905
+ if isinstance(event, Error):
906
+ await client.ext_notification(
907
+ "chrys/error",
908
+ {
909
+ "sessionId": event_session_id,
910
+ "code": event.code,
911
+ "message": event.message,
912
+ "recoverable": event.recoverable,
913
+ },
914
+ )
915
+ return True
916
+ if isinstance(event, Warning):
917
+ await self._send_warning(event_session_id, event)
918
+ return True
919
+ if isinstance(event, ApprovalModeUpdated):
920
+ await client.session_update(
921
+ session_id=event_session_id,
922
+ update=acp_schema.CurrentModeUpdate(sessionUpdate="current_mode_update", currentModeId=event.mode),
923
+ )
924
+ return True
925
+ if isinstance(event, ContextCompressed):
926
+ await client.ext_notification(
927
+ "chrys/context_compressed",
928
+ {
929
+ "sessionId": event_session_id,
930
+ "compressedContextId": event.compressed_context_id,
931
+ "summary": event.summary,
932
+ "freedMessages": event.freed_messages,
933
+ "turnRange": list(event.turn_range),
934
+ "source": event.source,
935
+ },
936
+ )
937
+ return True
938
+ if isinstance(event, InvocationContextPressure):
939
+ await client.ext_notification(
940
+ "chrys/context_pressure",
941
+ {
942
+ "sessionId": event_session_id,
943
+ "reason": event.reason,
944
+ "attempts": event.attempts,
945
+ "sideCallTokens": event.side_call_tokens,
946
+ "sideCallTokenBudget": event.side_call_token_budget,
947
+ "source": event.source,
948
+ "invocationId": event.origin.invocation_id if event.origin.kind == "sub_agent" else "",
949
+ },
950
+ )
951
+ return True
952
+ if isinstance(event, ToolCompacted):
953
+ await client.ext_notification(
954
+ "chrys/tool_compacted",
955
+ {
956
+ "sessionId": event_session_id,
957
+ "compactedGroups": event.compacted_groups,
958
+ "phase": event.phase,
959
+ "turnNumbers": event.turn_numbers,
960
+ "compactedToolNames": event.compacted_tool_names,
961
+ "tokensBefore": event.tokens_before,
962
+ "tokensAfter": event.tokens_after,
963
+ "lastWordsGenerated": event.last_words_generated,
964
+ },
965
+ )
966
+ return True
967
+ if isinstance(event, CompactionStarted):
968
+ await client.ext_notification(
969
+ "chrys/compaction_started",
970
+ {
971
+ "sessionId": event_session_id,
972
+ "compactionId": event.compaction_id,
973
+ "phase": event.phase,
974
+ },
975
+ )
976
+ return True
977
+ if isinstance(event, CompactionFinished):
978
+ await client.ext_notification(
979
+ "chrys/compaction_finished",
980
+ {
981
+ "sessionId": event_session_id,
982
+ "compactionId": event.compaction_id,
983
+ "outcome": event.outcome,
984
+ "durationMs": event.duration_ms,
985
+ "lastWords": event.last_words,
986
+ "formatViolation": event.format_violation,
987
+ "failureReason": event.failure_reason,
988
+ },
989
+ )
990
+ return True
991
+ if isinstance(event, UserInjectResult):
992
+ await client.ext_notification(
993
+ "chrys/user_inject_result",
994
+ {
995
+ "sessionId": event_session_id,
996
+ "text": event.text,
997
+ "consumed": event.consumed,
998
+ "createdAt": event.created_at.isoformat()
999
+ if isinstance(event.created_at, datetime)
1000
+ else event.created_at,
1001
+ "injectionId": event.injection_id,
1002
+ },
1003
+ )
1004
+ return True
1005
+ if isinstance(event, InvocationEvent) and event.origin.kind != "sub_agent":
1006
+ return False
1007
+ # Sub-agent events are dual-path (like UsageUpdate): the Chrys extension
1008
+ # notification carries the rich nested detail, and returning False lets
1009
+ # AcpEventBridge also project standard `session/update` tool-call progress
1010
+ # onto the parent tool call — so standard-only clients aren't regressed.
1011
+ if isinstance(event, InvocationStarted):
1012
+ await client.ext_notification(
1013
+ "chrys/sub_agent_invocation_start",
1014
+ {
1015
+ "sessionId": event_session_id,
1016
+ "agentName": event.agent_name,
1017
+ "invocationId": event.origin.invocation_id,
1018
+ "toolName": event.tool_name,
1019
+ "parentCallId": event.parent_call_id,
1020
+ },
1021
+ )
1022
+ return False
1023
+ if isinstance(event, InvocationToolCallStart):
1024
+ await client.ext_notification(
1025
+ "chrys/sub_agent_tool_call_start",
1026
+ {
1027
+ "sessionId": event_session_id,
1028
+ "agentName": event.agent_name,
1029
+ "invocationId": event.origin.invocation_id,
1030
+ "toolName": event.tool_name,
1031
+ "args": event.args,
1032
+ "callId": event.call_id,
1033
+ },
1034
+ )
1035
+ return False
1036
+ if isinstance(event, InvocationToolCallResult):
1037
+ await client.ext_notification(
1038
+ "chrys/sub_agent_tool_call_result",
1039
+ {
1040
+ "sessionId": event_session_id,
1041
+ "agentName": event.agent_name,
1042
+ "invocationId": event.origin.invocation_id,
1043
+ "toolName": event.tool_name,
1044
+ "callId": event.call_id,
1045
+ "result": event.result,
1046
+ "durationMs": event.duration_ms,
1047
+ "metadata": event.metadata,
1048
+ },
1049
+ )
1050
+ return False
1051
+ if isinstance(event, InvocationProgress):
1052
+ await client.ext_notification(
1053
+ "chrys/sub_agent_progress",
1054
+ {
1055
+ "sessionId": event_session_id,
1056
+ "agentName": event.agent_name,
1057
+ "invocationId": event.origin.invocation_id,
1058
+ "toolCallCount": event.tool_call_count,
1059
+ "totalTokens": event.total_tokens,
1060
+ "totalUsageTokens": event.total_usage_tokens,
1061
+ "usageUnreportedAttempts": event.usage_unreported_attempts,
1062
+ },
1063
+ )
1064
+ return False
1065
+ if isinstance(event, InvocationRetryAttempt):
1066
+ await client.ext_notification(
1067
+ "chrys/sub_agent_retry_attempt",
1068
+ {
1069
+ "sessionId": event_session_id,
1070
+ "agentName": event.agent_name,
1071
+ "invocationId": event.origin.invocation_id,
1072
+ "message": event.message,
1073
+ "attempt": event.attempt,
1074
+ "maxAttempts": event.max_attempts,
1075
+ "delaySeconds": event.delay_seconds,
1076
+ },
1077
+ )
1078
+ return False
1079
+ if isinstance(event, InvocationCompactionStarted):
1080
+ await client.ext_notification(
1081
+ "chrys/sub_agent_compaction_started",
1082
+ {
1083
+ "sessionId": event_session_id,
1084
+ "agentName": event.agent_name,
1085
+ "invocationId": event.origin.invocation_id,
1086
+ "compactionId": event.compaction_id,
1087
+ "phase": event.phase,
1088
+ },
1089
+ )
1090
+ return False
1091
+ if isinstance(event, InvocationCompactionFinished):
1092
+ await client.ext_notification(
1093
+ "chrys/sub_agent_compaction_finished",
1094
+ {
1095
+ "sessionId": event_session_id,
1096
+ "agentName": event.agent_name,
1097
+ "invocationId": event.origin.invocation_id,
1098
+ "compactionId": event.compaction_id,
1099
+ "outcome": event.outcome,
1100
+ "durationMs": event.duration_ms,
1101
+ "formatViolation": event.format_violation,
1102
+ "failureReason": event.failure_reason,
1103
+ },
1104
+ )
1105
+ return False
1106
+ if isinstance(event, InvocationCompactionCommitted):
1107
+ # Post-commit confirmation trailing finished(ok) — a finished-ok
1108
+ # round without this notification was abandoned (spill failure).
1109
+ await client.ext_notification(
1110
+ "chrys/sub_agent_compaction_committed",
1111
+ {
1112
+ "sessionId": event_session_id,
1113
+ "agentName": event.agent_name,
1114
+ "invocationId": event.origin.invocation_id,
1115
+ "compactionId": event.compaction_id,
1116
+ "phase": event.phase,
1117
+ },
1118
+ )
1119
+ return False
1120
+ if isinstance(event, InvocationPaused):
1121
+ await client.ext_notification(
1122
+ "chrys/sub_agent_paused",
1123
+ {
1124
+ "sessionId": event_session_id,
1125
+ "agentName": event.agent_name,
1126
+ "invocationId": event.origin.invocation_id,
1127
+ "toolName": event.tool_name,
1128
+ "reason": event.reason,
1129
+ "lastError": event.last_error,
1130
+ "retryAttempts": event.retry_attempts,
1131
+ },
1132
+ )
1133
+ return False
1134
+ if isinstance(event, InvocationResumed):
1135
+ await client.ext_notification(
1136
+ "chrys/sub_agent_resumed",
1137
+ {
1138
+ "sessionId": event_session_id,
1139
+ "agentName": event.agent_name,
1140
+ "invocationId": event.origin.invocation_id,
1141
+ },
1142
+ )
1143
+ return False
1144
+ if isinstance(event, InvocationAborted):
1145
+ await client.ext_notification(
1146
+ "chrys/sub_agent_aborted",
1147
+ {
1148
+ "sessionId": event_session_id,
1149
+ "agentName": event.agent_name,
1150
+ "invocationId": event.origin.invocation_id,
1151
+ "lastError": event.last_error,
1152
+ },
1153
+ )
1154
+ return False
1155
+ if isinstance(event, InvocationCascadeAborted):
1156
+ await client.ext_notification(
1157
+ "chrys/sub_agent_cascade_aborted",
1158
+ {
1159
+ "sessionId": event_session_id,
1160
+ "agentName": event.agent_name,
1161
+ "invocationId": event.origin.invocation_id,
1162
+ },
1163
+ )
1164
+ return False
1165
+ if isinstance(event, SessionSaved):
1166
+ return False
1167
+ return False
1168
+
1169
+ def _session_mode_state(self, session_id: str) -> acp_schema.SessionModeState:
1170
+ current = self._manager.get(session_id).host.engine.approval_mode.value
1171
+ return acp_schema.SessionModeState(
1172
+ availableModes=[
1173
+ acp_schema.SessionMode(id=mode_id, name=name, description=desc)
1174
+ for mode_id, name, desc in _APPROVAL_MODES
1175
+ ],
1176
+ currentModeId=current,
1177
+ )
1178
+
1179
+ def _session_model_state(self, session_id: str) -> acp_schema.SessionModelState:
1180
+ current = self._manager.get(session_id).host.engine.runtime_details.model.profile_id
1181
+ return acp_schema.SessionModelState(
1182
+ availableModels=[
1183
+ acp_schema.ModelInfo(
1184
+ modelId=str(profile["id"]),
1185
+ name=str(profile.get("name") or profile["id"]),
1186
+ description=_model_info_description(profile),
1187
+ )
1188
+ for profile in self._manager.list_model_profiles()
1189
+ ],
1190
+ currentModelId=current,
1191
+ )
1192
+
1193
+ async def _send_session_restored(self, session_id: str) -> None:
1194
+ payload = {"sessionId": session_id}
1195
+ await self._client_or_error().ext_notification("chrys/session_restored", payload)
1196
+
1197
+ async def _send_plan_update(self, session_id: str) -> None:
1198
+ # Seed/refresh the client's plan at session-establishment points
1199
+ # (new_session, load_session, rollback) where no TodoListUpdated
1200
+ # event fires. Sent unconditionally: empty entries clear a stale
1201
+ # plan left over from a previous session in the same client pane.
1202
+ tracker = self._manager.get(session_id).host.engine.todo_tracker
1203
+ items = tracker.snapshot() if tracker is not None else ()
1204
+ await self._client_or_error().session_update(
1205
+ session_id=session_id,
1206
+ update=plan_update_for_todos(items),
1207
+ )
1208
+
1209
+ async def _send_warning(self, session_id: str, warning: Warning) -> None:
1210
+ """Send one ``chrys/warning``, whether it came from a turn or an RPC."""
1211
+ await self._client_or_error().ext_notification(
1212
+ "chrys/warning",
1213
+ {
1214
+ "sessionId": session_id,
1215
+ "code": warning.code,
1216
+ "message": warning.message,
1217
+ },
1218
+ )
1219
+
1220
+ async def _send_runtime_update(self, session_id: str) -> None:
1221
+ # Same envelope as the event-bridged senders (SessionReady /
1222
+ # AgentRuntimeUpdated): `chrys/runtime_update` is always
1223
+ # {sessionId, runtime: {...}} so a client handler reads one shape.
1224
+ await self._client_or_error().ext_notification(
1225
+ "chrys/runtime_update",
1226
+ {"sessionId": session_id, "runtime": self._runtime_payload(session_id)},
1227
+ )
1228
+
1229
+ def _runtime_payload(self, session_id: str) -> dict[str, Any]:
1230
+ session = self._manager.get(session_id)
1231
+ engine = session.host.engine
1232
+ details = engine.runtime_details
1233
+ model = details.model
1234
+ usage = engine.usage_publisher.make_usage_event(session_id=session_id)
1235
+ return {
1236
+ "sessionId": session_id,
1237
+ "agentProfile": session.profile_name,
1238
+ "displayName": session.profile_name,
1239
+ "modelProfileId": model.profile_id,
1240
+ "maxContextTokens": model.max_context_tokens,
1241
+ "inputTokens": usage.input_tokens,
1242
+ "outputTokens": usage.output_tokens,
1243
+ "totalTokens": usage.total_tokens,
1244
+ "pct": usage.pct,
1245
+ "totalSessionTokens": usage.total_session_tokens,
1246
+ "totalSessionInputTokens": usage.total_session_input_tokens,
1247
+ "totalSessionOutputTokens": usage.total_session_output_tokens,
1248
+ "cacheHitTokens": usage.cache_hit_tokens,
1249
+ "totalSessionCacheHitTokens": usage.total_session_cache_hit_tokens,
1250
+ "localTokens": usage.local_tokens,
1251
+ "calibrationRatio": usage.calibration_ratio,
1252
+ "systemOverheadTokens": usage.system_overhead_tokens,
1253
+ "runtimeDetails": jsonable_dataclass(details),
1254
+ }
1255
+
1256
+ async def _refresh_mutation_attribution(self, session_id: str) -> None:
1257
+ """Fold newly published peer claims in before a net-summary read.
1258
+
1259
+ Best-effort: failures (including unknown session ids) fall
1260
+ through — the payload builder raises the canonical error.
1261
+ """
1262
+ with contextlib.suppress(Exception):
1263
+ engine = self._manager.get(session_id).host.engine
1264
+ await engine.refresh_mutation_attribution()
1265
+
1266
+ def _mutations_payload(self, session_id: str) -> dict[str, Any]:
1267
+ session = self._manager.get(session_id)
1268
+ engine = session.host.engine
1269
+ tracker = engine.mutation_tracker
1270
+ if tracker is None:
1271
+ return {
1272
+ "sessionId": session_id,
1273
+ "currentTurn": engine.current_turn_number,
1274
+ "availableRollbackTurns": [],
1275
+ "turns": [],
1276
+ "files": [],
1277
+ }
1278
+ file_summary = tracker.get_session_file_summary()
1279
+ return {
1280
+ "sessionId": session_id,
1281
+ "currentTurn": engine.current_turn_number,
1282
+ "availableRollbackTurns": engine.available_rollback_turns(),
1283
+ "turns": [
1284
+ {
1285
+ "turnId": turn.turn_id,
1286
+ "mutationCount": len(turn.mutations),
1287
+ "mutations": [_mutation_payload(mutation) for mutation in turn.mutations],
1288
+ }
1289
+ for turn in tracker.get_all_turns()
1290
+ ],
1291
+ "files": [
1292
+ {
1293
+ "path": path,
1294
+ "beforeHash": diff.before,
1295
+ "afterHash": diff.after,
1296
+ "operation": _operation_from_hashes(diff.before, diff.after),
1297
+ # Folded provenance badges: a peer session also wrote
1298
+ # this path / the net change includes window-diff
1299
+ # inference. Foreign-only paths are excluded from
1300
+ # this net-level list entirely (raw turn payloads
1301
+ # keep them).
1302
+ "contested": diff.contested,
1303
+ "inferred": diff.inferred,
1304
+ }
1305
+ for path, diff in sorted(file_summary.items())
1306
+ # Content withheld by SnapshotPolicy (too large / binary):
1307
+ # a None hash would mis-read as create/delete.
1308
+ if not diff.content_unavailable
1309
+ ],
1310
+ }
1311
+
1312
+ def _diff_payload(self, session_id: str, *, path: str | None, turn: int | None) -> dict[str, Any]:
1313
+ session = self._manager.get(session_id)
1314
+ tracker = session.host.engine.mutation_tracker
1315
+ if tracker is None:
1316
+ return {"sessionId": session_id, "entries": []}
1317
+ summary = tracker.get_turn_file_summary(turn) if turn is not None else tracker.get_session_file_summary()
1318
+ entries = []
1319
+ for file_path, diff in sorted(summary.items()):
1320
+ if path is not None and file_path != path:
1321
+ continue
1322
+ if diff.content_unavailable:
1323
+ continue
1324
+ entries.append(_diff_entry_payload(tracker, file_path, diff))
1325
+ return {
1326
+ "sessionId": session_id,
1327
+ "turn": turn,
1328
+ "entries": entries,
1329
+ }
1330
+
1331
+ async def _request_input(self, session_id: str, event: QuestionToUser) -> None:
1332
+ client = self._client_or_error()
1333
+ event_bus = self._manager.get(session_id).host.event_bus
1334
+ loop = asyncio.get_running_loop()
1335
+ cancel_future: asyncio.Future[None] = loop.create_future()
1336
+ cancel_key = (session_id, event.request_id)
1337
+ already_cancelled = self._install_pending_cancel(
1338
+ self._pending_input_cancels,
1339
+ self._input_cancel_tombstones,
1340
+ cancel_key,
1341
+ cancel_future,
1342
+ )
1343
+ if already_cancelled:
1344
+ # The wait died before we could ask: sending the request anyway
1345
+ # would render a stale dialog on the client (nothing revokes it).
1346
+ await event_bus.publish(
1347
+ AskUserResponse(
1348
+ request_id=event.request_id,
1349
+ cancelled=True,
1350
+ session_id=event.session_id or session_id,
1351
+ )
1352
+ )
1353
+ return
1354
+
1355
+ async def _request(params: dict[str, Any]) -> object | None:
1356
+ if cancel_future.done():
1357
+ return None
1358
+ request_task = asyncio.create_task(client.ext_method("chrys/request_input", params))
1359
+ try:
1360
+ done, _pending = await asyncio.wait(
1361
+ {request_task, cancel_future},
1362
+ return_when=asyncio.FIRST_COMPLETED,
1363
+ )
1364
+ if cancel_future in done:
1365
+ return None
1366
+ try:
1367
+ return request_task.result()
1368
+ except asyncio.CancelledError:
1369
+ # Only the client callback died; the prompt itself keeps
1370
+ # running and reads this as "no answer". Cancellation of
1371
+ # this coroutine propagates out of the wait above so an
1372
+ # owner tearing the prompt down never sees a synthetic
1373
+ # response.
1374
+ return None
1375
+ except Exception:
1376
+ return None
1377
+ finally:
1378
+ if request_task.done():
1379
+ _observe_task_exception(request_task)
1380
+ else:
1381
+ request_task.cancel()
1382
+ request_task.add_done_callback(_observe_task_exception)
1383
+
1384
+ try:
1385
+ response = await _request(
1386
+ {
1387
+ "sessionId": session_id,
1388
+ "requestId": event.request_id,
1389
+ "questions": [_request_input_question_payload(item) for item in event.questions],
1390
+ "callerName": event.caller_name,
1391
+ }
1392
+ )
1393
+ finally:
1394
+ self._pending_input_cancels.pop(cancel_key, None)
1395
+ outcome = validate_request_input_response(response, questions=event.questions)
1396
+ if cancel_future.done() or not isinstance(outcome, tuple):
1397
+ response_event = AskUserResponse(request_id=event.request_id, cancelled=True)
1398
+ else:
1399
+ response_event = AskUserResponse(request_id=event.request_id, answers=outcome)
1400
+ response_event.session_id = event.session_id or session_id
1401
+ await event_bus.publish(response_event)
1402
+
1403
+ async def _request_permission(self, session_id: str, event: ApprovalRequest, *, reason: str = "") -> None:
1404
+ client = self._client_or_error()
1405
+ event_bus = self._manager.get(session_id).host.event_bus
1406
+ tool_call = acp_schema.ToolCallUpdate(
1407
+ toolCallId=event.request_id,
1408
+ title=tool_call_title(event.tool_name, event.tool_kind, event.args, intent_summary=event.intent_summary),
1409
+ kind=acp_tool_kind(event.tool_kind),
1410
+ status="pending",
1411
+ rawInput=event.args,
1412
+ rawOutput=reason or None,
1413
+ field_meta={
1414
+ "chrys": {
1415
+ "tool_name": event.tool_name,
1416
+ "tool_kind": event.tool_kind,
1417
+ }
1418
+ },
1419
+ )
1420
+ loop = asyncio.get_running_loop()
1421
+ cancel_future: asyncio.Future[None] = loop.create_future()
1422
+ cancel_key = (session_id, event.request_id)
1423
+ already_cancelled = self._install_pending_cancel(
1424
+ self._pending_permission_cancels,
1425
+ self._permission_cancel_tombstones,
1426
+ cancel_key,
1427
+ cancel_future,
1428
+ )
1429
+ if already_cancelled:
1430
+ # The wait died before we could ask: sending the request anyway
1431
+ # would render a stale dialog on the client (nothing revokes it).
1432
+ await event_bus.publish(
1433
+ ApprovalResponse(
1434
+ request_id=event.request_id,
1435
+ approved=False,
1436
+ reason="ACP permission request was cancelled.",
1437
+ session_id=event.session_id or session_id,
1438
+ )
1439
+ )
1440
+ return
1441
+ request_task = asyncio.create_task(
1442
+ client.request_permission(
1443
+ session_id=session_id,
1444
+ tool_call=tool_call,
1445
+ options=[
1446
+ acp_schema.PermissionOption(
1447
+ optionId=_ALLOW_OPTION_ID,
1448
+ name="Allow once",
1449
+ kind="allow_once",
1450
+ ),
1451
+ acp_schema.PermissionOption(
1452
+ optionId=_REJECT_OPTION_ID,
1453
+ name="Reject once",
1454
+ kind="reject_once",
1455
+ ),
1456
+ ],
1457
+ )
1458
+ )
1459
+ self._pending_permission_tasks[cancel_key] = request_task
1460
+ try:
1461
+ done, _pending = await asyncio.wait(
1462
+ {request_task, cancel_future},
1463
+ timeout=self._permission_timeout_seconds,
1464
+ return_when=asyncio.FIRST_COMPLETED,
1465
+ )
1466
+ if cancel_future in done:
1467
+ approved = False
1468
+ reason_text = "ACP permission request was cancelled."
1469
+ elif request_task in done:
1470
+ try:
1471
+ response = request_task.result()
1472
+ except asyncio.CancelledError:
1473
+ approved = False
1474
+ reason_text = "ACP permission request failed or was cancelled."
1475
+ else:
1476
+ approved = (
1477
+ isinstance(response.outcome, acp_schema.AllowedOutcome)
1478
+ and response.outcome.option_id == _ALLOW_OPTION_ID
1479
+ )
1480
+ if approved:
1481
+ reason_text = ""
1482
+ elif isinstance(response.outcome, acp_schema.DeniedOutcome):
1483
+ reason_text = "ACP permission request was cancelled."
1484
+ else:
1485
+ reason_text = "Rejected by ACP client."
1486
+ else:
1487
+ approved = False
1488
+ reason_text = "ACP permission request timed out."
1489
+ except asyncio.CancelledError:
1490
+ raise
1491
+ except Exception:
1492
+ approved = False
1493
+ reason_text = "ACP permission request failed or was cancelled."
1494
+ finally:
1495
+ if self._pending_permission_cancels.get(cancel_key) is cancel_future:
1496
+ self._pending_permission_cancels.pop(cancel_key, None)
1497
+ if self._pending_permission_tasks.get(cancel_key) is request_task:
1498
+ self._pending_permission_tasks.pop(cancel_key, None)
1499
+ if request_task.done():
1500
+ _observe_task_exception(request_task)
1501
+ else:
1502
+ request_task.cancel()
1503
+ request_task.add_done_callback(_observe_task_exception)
1504
+ await event_bus.publish(
1505
+ ApprovalResponse(
1506
+ request_id=event.request_id,
1507
+ approved=approved,
1508
+ reason=reason_text,
1509
+ session_id=event.session_id or session_id,
1510
+ )
1511
+ )
1512
+
1513
+ def _client_or_error(self) -> Client:
1514
+ if self._client is None:
1515
+ raise RequestError.internal_error({"details": "ACP client connection is not ready."})
1516
+ return self._client
1517
+
1518
+ async def _interrupt_and_cancel_pending_waits(self, session_id: str) -> None:
1519
+ try:
1520
+ await self._manager.cancel(session_id)
1521
+ except AcpSessionError:
1522
+ pass
1523
+ finally:
1524
+ self._cancel_pending_waits(session_id)
1525
+
1526
+ def _cancel_pending_waits(self, session_id: str) -> None:
1527
+ self._resolve_pending_cancels(self._pending_input_cancels, session_id)
1528
+ self._resolve_pending_cancels(self._pending_permission_cancels, session_id)
1529
+
1530
+ @staticmethod
1531
+ def _deliver_or_record_cancellation(
1532
+ pending_cancels: dict[tuple[str, str], asyncio.Future[None]],
1533
+ tombstones: set[tuple[str, str]],
1534
+ key: tuple[str, str],
1535
+ ) -> None:
1536
+ future = pending_cancels.get(key)
1537
+ if future is None:
1538
+ tombstones.add(key)
1539
+ elif not future.done():
1540
+ future.set_result(None)
1541
+
1542
+ @staticmethod
1543
+ def _install_pending_cancel(
1544
+ pending_cancels: dict[tuple[str, str], asyncio.Future[None]],
1545
+ tombstones: set[tuple[str, str]],
1546
+ key: tuple[str, str],
1547
+ future: asyncio.Future[None],
1548
+ ) -> bool:
1549
+ """Install a waiter and consume an early cancellation without an await gap.
1550
+
1551
+ Returns True when an early-cancellation tombstone was consumed: the
1552
+ wait is already dead, so the caller must not send the client a
1553
+ request nobody is waiting on.
1554
+ """
1555
+ if key in tombstones:
1556
+ tombstones.remove(key)
1557
+ future.set_result(None)
1558
+ return True
1559
+ pending_cancels[key] = future
1560
+ return False
1561
+
1562
+ def _clear_cancellation_state(self, session_id: str) -> None:
1563
+ self._cancel_pending_waits(session_id)
1564
+ self._drop_session_tombstones(session_id)
1565
+
1566
+ def _drop_session_tombstones(self, session_id: str) -> None:
1567
+ self._input_cancel_tombstones = {key for key in self._input_cancel_tombstones if key[0] != session_id}
1568
+ self._permission_cancel_tombstones = {key for key in self._permission_cancel_tombstones if key[0] != session_id}
1569
+
1570
+ @staticmethod
1571
+ def _resolve_pending_cancels(
1572
+ pending_cancels: dict[tuple[str, str], asyncio.Future[None]],
1573
+ session_id: str,
1574
+ ) -> None:
1575
+ for (pending_session_id, _request_id), future in list(pending_cancels.items()):
1576
+ if pending_session_id == session_id and not future.done():
1577
+ future.set_result(None)
1578
+
1579
+ @staticmethod
1580
+ def _reject_additional_directories(additional_directories: list[str] | None) -> None:
1581
+ if additional_directories:
1582
+ raise RequestError.invalid_params(
1583
+ {"details": f"additionalDirectories are not supported by {APP_DISPLAY_NAME} ACP phase 1."}
1584
+ )
1585
+
1586
+
1587
+ def _request_error(exc: Exception, *, usage: dict[str, int] | None = None) -> RequestError:
1588
+ if isinstance(exc, RequestError):
1589
+ if usage is None:
1590
+ return exc
1591
+ data = dict(exc.data) if isinstance(exc.data, dict) else {}
1592
+ data["usage"] = usage
1593
+ return RequestError(exc.code, str(exc), data)
1594
+ if isinstance(exc, AcpSessionError):
1595
+ data: dict[str, object] = {"details": str(exc)}
1596
+ if usage is not None:
1597
+ data["usage"] = usage
1598
+ return RequestError.invalid_params(data)
1599
+ details = str(exc)
1600
+ details = f"{type(exc).__name__}: {details}" if details else type(exc).__name__
1601
+ data: dict[str, object] = {"details": details}
1602
+ if usage is not None:
1603
+ data["usage"] = usage
1604
+ return RequestError.internal_error(data)
1605
+
1606
+
1607
+ def _prompt_usage_payload(event: UsageUpdate) -> dict[str, int]:
1608
+ """Return strict cumulative ACP spend aliases from one locked engine snapshot."""
1609
+ payload = {
1610
+ "inputTokens": max(0, event.total_session_input_tokens),
1611
+ "outputTokens": max(0, event.total_session_output_tokens),
1612
+ "totalTokens": max(0, event.total_session_tokens),
1613
+ }
1614
+ if event.total_session_cache_hit_tokens is not None:
1615
+ payload["cachedReadTokens"] = max(0, event.total_session_cache_hit_tokens)
1616
+ return payload
1617
+
1618
+
1619
+ def _observe_task_exception(task: asyncio.Task[Any]) -> None:
1620
+ if task.cancelled():
1621
+ return
1622
+ with contextlib.suppress(Exception):
1623
+ task.exception()
1624
+
1625
+
1626
+ def _runtime_update_payload(event: AgentRuntimeUpdated) -> dict[str, Any]:
1627
+ return {
1628
+ "modelProfileId": event.model_profile_id,
1629
+ "maxContextTokens": event.max_context_tokens,
1630
+ "toolNames": event.tool_names,
1631
+ "skillNames": event.skill_names,
1632
+ "memoryFiles": event.memory_files,
1633
+ "runtimeDetails": jsonable_dataclass(event.runtime_details),
1634
+ }
1635
+
1636
+
1637
+ def _rollback_result_payload(event: RollbackResult) -> dict[str, Any]:
1638
+ return {
1639
+ "sessionId": event.session_id,
1640
+ "targetTurn": event.target_turn,
1641
+ "rolledBackUserText": event.rolled_back_user_text,
1642
+ "filesReverted": event.files_reverted,
1643
+ "restoreResults": [
1644
+ {
1645
+ "path": result.path,
1646
+ "outcome": result.outcome.value,
1647
+ "reason": result.reason,
1648
+ "changed": result.changed,
1649
+ "ok": result.ok,
1650
+ }
1651
+ for result in event.restore_results
1652
+ ],
1653
+ # Files the rollback plan dropped, with a RollbackExclusionReason
1654
+ # value ("unrestorable" / "move_poisoned" / ...) — never silent.
1655
+ "exclusions": [{"path": path, "reason": reason} for path, reason in event.exclusions],
1656
+ # Advisory repo-level notices (plan warnings), plain strings.
1657
+ "warnings": list(event.warnings),
1658
+ }
1659
+
1660
+
1661
+ def _profile_switched_payload(event: ProfileSwitched, *, session_id: str) -> dict[str, Any]:
1662
+ return {
1663
+ "sessionId": session_id,
1664
+ "fromProfile": event.from_profile,
1665
+ "toProfile": event.to_profile,
1666
+ "fromDisplayName": event.from_display_name,
1667
+ "toDisplayName": event.to_display_name,
1668
+ "messageCount": event.message_count,
1669
+ "modelProfileId": event.model_profile_id,
1670
+ "maxContextTokens": event.max_context_tokens,
1671
+ "toolNames": event.tool_names,
1672
+ "skillNames": event.skill_names,
1673
+ "subAgentToolNames": event.sub_agent_tool_names,
1674
+ "memoryFiles": event.memory_files,
1675
+ "runtimeDetails": jsonable_dataclass(event.runtime_details),
1676
+ }
1677
+
1678
+
1679
+ def _mutation_payload(mutation: Any) -> dict[str, Any]:
1680
+ return {
1681
+ "path": mutation.path,
1682
+ "operation": mutation.operation.value,
1683
+ "source": mutation.source.value,
1684
+ "toolCallId": mutation.tool_call_id,
1685
+ "timestamp": mutation.timestamp,
1686
+ "oldPath": mutation.old_path,
1687
+ "beforeHash": mutation.before_hash,
1688
+ "afterHash": mutation.after_hash,
1689
+ # Why a side's content backup was withheld ("too_large" /
1690
+ # "binary"), or None when the hash is real / the file was absent.
1691
+ "beforeSkip": mutation.before_skip.value if mutation.before_skip else None,
1692
+ "afterSkip": mutation.after_skip.value if mutation.after_skip else None,
1693
+ # Attribution confidence ("proven" / "assumed" / "foreign") and
1694
+ # peer-conflict flag. Raw turn payloads stay unfiltered —
1695
+ # foreign rows remain present so clients can filter or badge
1696
+ # themselves (net-level lists exclude them instead).
1697
+ "provenance": mutation.provenance.value if mutation.provenance else None,
1698
+ "contested": mutation.contested,
1699
+ }
1700
+
1701
+
1702
+ def _diff_entry_payload(tracker: Any, path: str, diff: FileHashDiff) -> dict[str, Any]:
1703
+ before_bytes = tracker.store.read_blob(diff.before) if diff.before else None
1704
+ after_bytes = tracker.store.read_blob(diff.after) if diff.after else None
1705
+ is_binary = bool(
1706
+ (before_bytes and EncodingDetector.looks_binary(before_bytes))
1707
+ or (after_bytes and EncodingDetector.looks_binary(after_bytes))
1708
+ )
1709
+ before_text = "" if before_bytes is None or is_binary else decode_bytes(before_bytes)
1710
+ after_text = "" if after_bytes is None or is_binary else decode_bytes(after_bytes)
1711
+ return {
1712
+ "path": path,
1713
+ "operation": _operation_from_hashes(diff.before, diff.after),
1714
+ "beforeHash": diff.before,
1715
+ "afterHash": diff.after,
1716
+ "beforeText": before_text,
1717
+ "afterText": after_text,
1718
+ "isBinary": is_binary,
1719
+ "bytesChanged": diff.before != diff.after,
1720
+ # Folded provenance badges — same semantics as the ``files``
1721
+ # list in session/mutations, so diff clients can badge entries
1722
+ # without joining against that endpoint.
1723
+ "contested": diff.contested,
1724
+ "inferred": diff.inferred,
1725
+ }
1726
+
1727
+
1728
+ def _operation_from_hashes(before_hash: str | None, after_hash: str | None) -> str:
1729
+ if before_hash is None and after_hash is not None:
1730
+ return "create"
1731
+ if before_hash is not None and after_hash is None:
1732
+ return "delete"
1733
+ return "modify"
1734
+
1735
+
1736
+ def _event_session_id(event: Event, fallback: str) -> str:
1737
+ value = event.session_id
1738
+ return value if isinstance(value, str) and value else fallback
1739
+
1740
+
1741
+ def _model_info_description(profile: dict[str, Any]) -> str | None:
1742
+ parts = [str(profile[key]) for key in ("provider", "modelId") if profile.get(key)]
1743
+ return " · ".join(parts) or None
1744
+
1745
+
1746
+ def _string_param(params: dict[str, Any], key: str) -> str:
1747
+ value = params.get(key, "")
1748
+ return value if isinstance(value, str) else ""
1749
+
1750
+
1751
+ def _optional_string_param(params: dict[str, Any], key: str) -> str | None:
1752
+ value = params.get(key)
1753
+ if value is None:
1754
+ return None
1755
+ if not isinstance(value, str):
1756
+ raise RequestError.invalid_params({"details": f"{key} must be a string."})
1757
+ return value
1758
+
1759
+
1760
+ def _int_param(params: dict[str, Any], key: str) -> int:
1761
+ value = params.get(key)
1762
+ if type(value) is not int:
1763
+ raise RequestError.invalid_params({"details": f"{key} must be an integer."})
1764
+ return value
1765
+
1766
+
1767
+ def _optional_int_param(params: dict[str, Any], key: str) -> int | None:
1768
+ value = params.get(key)
1769
+ if value is None:
1770
+ return None
1771
+ if type(value) is not int:
1772
+ raise RequestError.invalid_params({"details": f"{key} must be an integer."})
1773
+ return value
1774
+
1775
+
1776
+ def _bool_param(params: dict[str, Any], key: str, *, default: bool) -> bool:
1777
+ value = params.get(key, default)
1778
+ if not isinstance(value, bool):
1779
+ raise RequestError.invalid_params({"details": f"{key} must be a boolean."})
1780
+ return value
1781
+
1782
+
1783
+ def _dict_param(params: dict[str, Any], key: str) -> dict[str, Any]:
1784
+ value = params.get(key)
1785
+ if isinstance(value, dict):
1786
+ return value
1787
+ raise RequestError.invalid_params({"details": f"{key} must be an object."})
1788
+
1789
+
1790
+ def _string_list_param(params: dict[str, Any], key: str) -> list[str] | None:
1791
+ value = params.get(key)
1792
+ if value is None:
1793
+ return None
1794
+ if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
1795
+ raise RequestError.invalid_params({"details": f"{key} must be a list of strings."})
1796
+ return value