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,1311 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Hardened, owned ACP client connection for one external sub-agent attempt."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import contextlib
9
+ import logging
10
+ from collections.abc import Sequence
11
+ from typing import Any, Protocol, cast
12
+
13
+ from acp import PROTOCOL_VERSION, Client
14
+ from acp.client.connection import ClientSideConnection
15
+ from acp.exceptions import RequestError
16
+ from acp.schema import (
17
+ AgentCapabilities,
18
+ AllowedOutcome,
19
+ ClientCapabilities,
20
+ CreateTerminalResponse,
21
+ DeniedOutcome,
22
+ EnvVariable,
23
+ FileSystemCapabilities,
24
+ KillTerminalResponse,
25
+ NewSessionResponse,
26
+ PermissionOption,
27
+ PromptResponse,
28
+ ReadTextFileResponse,
29
+ ReleaseTerminalResponse,
30
+ RequestPermissionResponse,
31
+ SessionConfigOptionBoolean,
32
+ SessionConfigOptionSelect,
33
+ SessionNotification,
34
+ TerminalOutputResponse,
35
+ TextContentBlock,
36
+ ToolCallUpdate,
37
+ WaitForTerminalExitResponse,
38
+ WriteTextFileResponse,
39
+ )
40
+ from acp.task import (
41
+ InMemoryMessageQueue,
42
+ MessageQueue,
43
+ MessageStateStore,
44
+ NotificationRunner,
45
+ RequestRunner,
46
+ TaskSupervisor,
47
+ )
48
+ from pydantic import ValidationError
49
+
50
+ from chrys.foundation.models.ask_user import validate_request_input_params
51
+
52
+ from . import _transport, protocol
53
+ from ._transport import (
54
+ _CURRENT_INBOUND_ID,
55
+ _AcpDisconnectError,
56
+ _AcpProtocolLimitError,
57
+ _BackpressureDispatcher,
58
+ _consume_future_exception,
59
+ _FrameObserver,
60
+ _FramingPump,
61
+ _ObserverBuffer,
62
+ _PauseTransport,
63
+ _PromptBarrier,
64
+ _reap_task,
65
+ _RetainedSender,
66
+ _TrackingStateStore,
67
+ _valid_usage_int,
68
+ )
69
+ from .errors import (
70
+ AcpAuthRequiredError,
71
+ AcpClientError,
72
+ AcpConfigError,
73
+ AcpConnectError,
74
+ AcpIdleTimeoutError,
75
+ AcpOperation,
76
+ AcpSpawnError,
77
+ AcpTransportError,
78
+ classify_acp_error,
79
+ is_best_effort_option_rejection,
80
+ )
81
+ from .protocol import _validate_payload_caps
82
+ from .protocol import encode_protocol_json as encode_protocol_json
83
+ from .protocol import parse_protocol_json as parse_protocol_json
84
+ from .protocol import validate_json_rpc_envelope as validate_json_rpc_envelope
85
+ from .protocol import validate_json_scalar_tree as validate_json_scalar_tree
86
+ from .spawn import ACP_STDIO_LIMIT_BYTES, AcpSpawnResult, spawn_acp_process, validate_agent_spec
87
+ from .spec import AcpAgentSpec, AcpHandshakeInfo, AcpPromptOutcome, PermissionDecision
88
+
89
+ logger = logging.getLogger(__name__)
90
+
91
+ _MAX_PERMISSION_OPTIONS = 128
92
+ _CLOSE_RPC_TIMEOUT_SECONDS = 2.0
93
+ _FINAL_DRAIN_TIMEOUT_SECONDS = 30.0
94
+
95
+ _INITIALIZE_METHOD = "initialize"
96
+ _NEW_SESSION_METHOD = "session/new"
97
+ # The selected outcome carries only an option id, so the id's kind is the
98
+ # sole proof that the wire outcome matches the callback's decision.
99
+ _DECISION_OPTION_KINDS: dict[str, frozenset[str]] = {
100
+ "allow": frozenset({"allow_once", "allow_always"}),
101
+ "deny": frozenset({"reject_once", "reject_always"}),
102
+ }
103
+ _META_COLLISION_KEYS = frozenset(
104
+ {
105
+ "options",
106
+ "sessionId",
107
+ "session_id",
108
+ "toolCall",
109
+ "tool_call",
110
+ "kwargs",
111
+ "method",
112
+ "params",
113
+ }
114
+ )
115
+
116
+
117
+ class AcpClientCallbacks(Protocol):
118
+ """Orchestration-owned callbacks; the service layer publishes no events."""
119
+
120
+ async def on_update(self, seq: int, notification: SessionNotification) -> None: ...
121
+
122
+ async def on_permission_request(
123
+ self,
124
+ tool_call: ToolCallUpdate,
125
+ options: Sequence[PermissionOption],
126
+ ) -> PermissionDecision: ...
127
+
128
+ async def on_ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]: ...
129
+
130
+
131
+ class AcpUpdateSink(Protocol):
132
+ """Small observer seam consumed by the PR-2 translator."""
133
+
134
+ async def put(self, seq: int, notification: SessionNotification) -> None: ...
135
+
136
+
137
+ class AcpClientWaitController(Protocol):
138
+ """PR-2 broker seam for resolving human waits during transport teardown."""
139
+
140
+ async def cancel_pending_waits(self) -> None: ...
141
+
142
+
143
+ class _CallbackUpdateSink:
144
+ def __init__(self, callbacks: AcpClientCallbacks) -> None:
145
+ self._callbacks = callbacks
146
+
147
+ async def put(self, seq: int, notification: SessionNotification) -> None:
148
+ await self._callbacks.on_update(seq, notification)
149
+
150
+
151
+ class AcpAgentClient:
152
+ """One-process ACP client with strict protocol and lifecycle ownership."""
153
+
154
+ def __init__(
155
+ self,
156
+ spec: AcpAgentSpec,
157
+ callbacks: AcpClientCallbacks,
158
+ *,
159
+ update_sink: AcpUpdateSink | None = None,
160
+ wait_controller: AcpClientWaitController | None = None,
161
+ ) -> None:
162
+ self._spec = spec
163
+ self._callbacks = callbacks
164
+ self._update_sink = update_sink or _CallbackUpdateSink(callbacks)
165
+ self._wait_controller = wait_controller
166
+ self._spawn: AcpSpawnResult | None = None
167
+ self._conn: ClientSideConnection | None = None
168
+ self._store: _TrackingStateStore | None = None
169
+ self._sender: _RetainedSender | None = None
170
+ self._dispatcher: _BackpressureDispatcher | None = None
171
+ self._supervisor: TaskSupervisor | None = None
172
+ self._observer: _FrameObserver | None = None
173
+ self._updates: _ObserverBuffer | None = None
174
+ self._consumer_task: asyncio.Task[None] | None = None
175
+ self._pump: _FramingPump | None = None
176
+ self._terminal_failure: asyncio.Future[BaseException] | None = None
177
+ self._completion_future: asyncio.Future[None] | None = None
178
+ self._handshake_deadline: float | None = None
179
+ self._stateful = False
180
+ self._closing = False
181
+ self._session_id: str | None = None
182
+ self._announced_session_id: str | None = None
183
+ self._session_response_claimed = False
184
+ self._agent_info: Any | None = None
185
+ self._auth_methods: tuple[Any, ...] = ()
186
+ self._capabilities = AgentCapabilities()
187
+ self._connect_started = False
188
+ self._prompt_started = False
189
+ self._cancel_requested = False
190
+ self._cancel_task: asyncio.Task[None] | None = None
191
+ self._force_close_task: asyncio.Task[None] | None = None
192
+ self._failure_close_task: asyncio.Task[None] | None = None
193
+ self._activity_event: asyncio.Event | None = None
194
+ self._last_activity = 0.0
195
+
196
+ @property
197
+ def stderr_tail(self) -> str:
198
+ return self._spawn.stderr_tail if self._spawn is not None else ""
199
+
200
+ @property
201
+ def stateful_phase_started(self) -> bool:
202
+ return self._stateful
203
+
204
+ async def connect(self) -> None:
205
+ """Spawn and initialize; roll back every acquired resource on failure."""
206
+ if self._conn is not None:
207
+ raise RuntimeError("ACP client is already connected.")
208
+ if self._closing:
209
+ raise RuntimeError("ACP client is already closed.")
210
+ # Claimed synchronously before the first await: _conn is only
211
+ # installed after spawning, so two concurrent calls would otherwise
212
+ # both spawn and the second installation would strand the first
213
+ # child beyond force_close's reach.
214
+ if self._connect_started:
215
+ raise RuntimeError("An ACP connection attempt has already been started.")
216
+ self._connect_started = True
217
+ validate_agent_spec(self._spec)
218
+ loop = asyncio.get_running_loop()
219
+ self._handshake_deadline = loop.time() + self._spec.handshake_timeout_seconds
220
+ self._terminal_failure = loop.create_future()
221
+ self._activity_event = asyncio.Event()
222
+ self._last_activity = loop.time()
223
+ try:
224
+ async with asyncio.timeout_at(self._handshake_deadline):
225
+ spawn = await spawn_acp_process(self._spec)
226
+ if self._closing:
227
+ # A force_close that completed during the spawn await saw
228
+ # nothing to release, and its cached task never runs
229
+ # again; this late acquisition must be disposed here.
230
+ await _dispose_spawn(spawn)
231
+ raise AcpConnectError("The ACP client was closed.")
232
+ # No suspension between the check above and installation, so a
233
+ # force_close starting any later sees the full component set.
234
+ self._spawn = spawn
235
+ self._install_connection()
236
+ initialize = await self._require_conn().initialize(
237
+ protocol_version=PROTOCOL_VERSION,
238
+ client_capabilities=ClientCapabilities(
239
+ fs=FileSystemCapabilities(read_text_file=False, write_text_file=False),
240
+ terminal=False,
241
+ ),
242
+ client_info=self._spec.client_info,
243
+ )
244
+ raw_result = self._require_store().latest_raw_result(_INITIALIZE_METHOD)
245
+ raw_version = raw_result.get("protocolVersion") if raw_result is not None else None
246
+ if type(raw_version) is not int or raw_version != PROTOCOL_VERSION:
247
+ raise AcpConfigError("The ACP agent returned an unsupported protocol version.")
248
+ self._agent_info = initialize.agent_info
249
+ self._auth_methods = tuple(initialize.auth_methods or ())
250
+ self._capabilities = initialize.agent_capabilities or AgentCapabilities()
251
+ terminal = self._require_terminal_failure()
252
+ if terminal.done():
253
+ raise terminal.result()
254
+ except asyncio.CancelledError:
255
+ await self.force_close()
256
+ raise
257
+ except BaseException as exc:
258
+ classified, failure = self._classified_connect_failure(exc)
259
+ await self.force_close()
260
+ raise classified from failure
261
+
262
+ def _classified_connect_failure(self, exc: BaseException) -> tuple[AcpClientError, BaseException]:
263
+ """Pick the classified failure and root cause for a failed connect.
264
+
265
+ The terminal future usually holds the most specific failure (the pump
266
+ sees protocol violations the SDK surfaces only as generic closes), but
267
+ a deterministic local classification must not be displaced by it — an
268
+ agent closing stdout right after a bad initialize response would
269
+ otherwise flip from config-rejected to retryable.
270
+ """
271
+ local = classify_acp_error(exc, operation=AcpOperation.INITIALIZE, stateful=False)
272
+ # AcpSpawnError is in the preserved set for the close-vs-spawn race: a
273
+ # force_close landing while process creation is pending settles the
274
+ # terminal with the retryable close classification, which must not
275
+ # displace a deterministic launch failure such as ENOENT.
276
+ if isinstance(local, AcpConfigError | AcpAuthRequiredError | AcpSpawnError):
277
+ return local, exc
278
+ terminal = self._terminal_failure
279
+ if terminal is not None and terminal.done():
280
+ root = terminal.result()
281
+ return classify_acp_error(root, operation=AcpOperation.INITIALIZE, stateful=False), root
282
+ return local, exc
283
+
284
+ async def open_session(self) -> AcpHandshakeInfo:
285
+ """Create exactly one fresh remote session and apply configured options."""
286
+ # Single-use: a second session/new would race the pump announcement and
287
+ # every session-id comparison site against the first session's id.
288
+ if self._stateful:
289
+ raise RuntimeError("An ACP session has already been opened for this client.")
290
+ # A background failure that settled after connect returned must fail
291
+ # here: the child may still be running with the connection already
292
+ # known dead, and enqueueing session/new would create an orphaned
293
+ # remote session no later frame can reach. Nothing stateful has been
294
+ # sent yet, so the connect-phase classification (retryable) applies.
295
+ terminal = self._require_terminal_failure()
296
+ if terminal.done():
297
+ root = terminal.result()
298
+ raise classify_acp_error(root, operation=AcpOperation.NEW_SESSION, stateful=False) from root
299
+ self._stateful = True
300
+ conn = self._require_conn()
301
+ try:
302
+ async with asyncio.timeout_at(self._require_handshake_deadline()):
303
+ additional = list(dict.fromkeys(self._spec.additional_directories))
304
+ supported = (
305
+ self._capabilities.session_capabilities is not None
306
+ and self._capabilities.session_capabilities.additional_directories is not None
307
+ )
308
+ if additional and not supported:
309
+ logger.warning("ACP agent does not advertise additionalDirectories; sending cwd only")
310
+ additional = []
311
+ response = await conn.new_session(
312
+ cwd=self._spec.cwd,
313
+ additional_directories=additional or None,
314
+ mcp_servers=[],
315
+ )
316
+ # The SDK schema accepts any str; an empty id would send ""
317
+ # outbound while _active_session_id() reads it as no-session,
318
+ # rejecting every callback on a "successfully" opened client.
319
+ # The id is retained and echoed on every session frame, so it
320
+ # rides the same cap as retained JSON-RPC ids.
321
+ session_id = response.session_id
322
+ if type(session_id) is not str or not 0 < len(session_id) <= protocol._MAX_REQUEST_ID_CHARS:
323
+ raise AcpConfigError("The ACP agent returned an invalid session id.")
324
+ self._session_id = session_id
325
+ final_options = await self._apply_session_options(response)
326
+ terminal = self._require_terminal_failure()
327
+ if terminal.done():
328
+ raise terminal.result()
329
+ except asyncio.CancelledError:
330
+ await self.force_close()
331
+ raise
332
+ except BaseException as exc:
333
+ raise classify_acp_error(exc, operation=AcpOperation.NEW_SESSION, stateful=True) from exc
334
+
335
+ if self._cancel_requested:
336
+ await self.cancel()
337
+ return AcpHandshakeInfo(
338
+ session_id=self._session_id,
339
+ agent_info=self._agent_info,
340
+ auth_methods=self._auth_methods,
341
+ capabilities=self._capabilities,
342
+ modes=response.modes,
343
+ models=response.models,
344
+ config_options=final_options,
345
+ )
346
+
347
+ async def _apply_session_options(
348
+ self, response: Any
349
+ ) -> tuple[SessionConfigOptionSelect | SessionConfigOptionBoolean, ...]:
350
+ conn = self._require_conn()
351
+ session_id = self._require_session_id()
352
+ if self._spec.session_mode:
353
+ advertised = response.modes is not None and self._spec.session_mode in {
354
+ mode.id for mode in response.modes.available_modes
355
+ }
356
+ if not advertised:
357
+ if self._spec.best_effort_options:
358
+ logger.warning("Configured ACP session mode is not advertised; continuing best-effort")
359
+ else:
360
+ raise AcpConfigError("The configured ACP session mode is not advertised.")
361
+ else:
362
+ try:
363
+ await conn.set_session_mode(mode_id=self._spec.session_mode, session_id=session_id)
364
+ except RequestError as exc:
365
+ if self._spec.best_effort_options and is_best_effort_option_rejection(exc):
366
+ logger.warning("ACP agent deterministically rejected session mode; continuing best-effort")
367
+ else:
368
+ raise classify_acp_error(exc, operation=AcpOperation.SET_MODE, stateful=True) from exc
369
+ except Exception as exc:
370
+ raise classify_acp_error(exc, operation=AcpOperation.SET_MODE, stateful=True) from exc
371
+ else:
372
+ # The session/new snapshot predates this accepted switch;
373
+ # patch it so the handshake reports the applied state
374
+ # (config options already get this via the returned sets).
375
+ response.modes.current_mode_id = self._spec.session_mode
376
+
377
+ if self._spec.model_id:
378
+ advertised = response.models is not None and self._spec.model_id in {
379
+ model.model_id for model in response.models.available_models
380
+ }
381
+ if not advertised:
382
+ logger.warning("Configured ACP model is not advertised; continuing with the agent default")
383
+ else:
384
+ try:
385
+ await conn.set_session_model(model_id=self._spec.model_id, session_id=session_id)
386
+ except RequestError as exc:
387
+ if is_best_effort_option_rejection(exc):
388
+ logger.warning("ACP agent rejected the configured model; continuing with the agent default")
389
+ else:
390
+ raise classify_acp_error(exc, operation=AcpOperation.SET_MODEL, stateful=True) from exc
391
+ except Exception as exc:
392
+ raise classify_acp_error(exc, operation=AcpOperation.SET_MODEL, stateful=True) from exc
393
+ else:
394
+ # Same snapshot-staleness rule as the mode above.
395
+ response.models.current_model_id = self._spec.model_id
396
+
397
+ # Config options may depend on one another, and every set response
398
+ # carries the complete replacement set, so each option validates
399
+ # against the latest returned state rather than the session/new list.
400
+ current_options: tuple[SessionConfigOptionSelect | SessionConfigOptionBoolean, ...] = tuple(
401
+ response.config_options or ()
402
+ )
403
+ for option_id, value in self._spec.config_options.items():
404
+ if option_id not in {option.id for option in current_options}:
405
+ if self._spec.best_effort_options:
406
+ logger.warning("Configured ACP option %r is not advertised; continuing best-effort", option_id)
407
+ continue
408
+ raise AcpConfigError("A configured ACP session option is not advertised.")
409
+ try:
410
+ result = await conn.set_config_option(
411
+ config_id=option_id,
412
+ session_id=session_id,
413
+ value=value,
414
+ )
415
+ except RequestError as exc:
416
+ if self._spec.best_effort_options and is_best_effort_option_rejection(exc):
417
+ logger.warning("ACP agent rejected option %r; continuing best-effort", option_id)
418
+ continue
419
+ raise classify_acp_error(exc, operation=AcpOperation.SET_CONFIG_OPTION, stateful=True) from exc
420
+ except Exception as exc:
421
+ raise classify_acp_error(exc, operation=AcpOperation.SET_CONFIG_OPTION, stateful=True) from exc
422
+ if result is not None and result.config_options is not None:
423
+ current_options = tuple(result.config_options)
424
+ return current_options
425
+
426
+ async def prompt(self, text: str) -> AcpPromptOutcome:
427
+ """Prompt once and return only after the exact response barrier drains."""
428
+ if type(text) is not str:
429
+ raise AcpConfigError("ACP prompt text must be a string.")
430
+ try:
431
+ validate_json_scalar_tree(text)
432
+ except ValueError as exc:
433
+ raise AcpConfigError("ACP prompt text is not valid Unicode.", cause=exc) from exc
434
+ # One prompt per session, claimed synchronously: a second call would
435
+ # replace the shared completion future and reset the store's prompt
436
+ # registration (cross-wiring barriers and ids under concurrency), and
437
+ # ACP usage is session-cumulative, so a sequential reuse would report
438
+ # cumulative totals as per-attempt usage.
439
+ if self._prompt_started:
440
+ raise RuntimeError("An ACP prompt has already been started for this client.")
441
+ self._prompt_started = True
442
+ conn = self._require_conn()
443
+ store = self._require_store()
444
+ terminal = self._require_terminal_failure()
445
+ loop = asyncio.get_running_loop()
446
+ self._completion_future = loop.create_future()
447
+ self._completion_future.add_done_callback(_consume_future_exception)
448
+ store.reset_prompt()
449
+ self._touch_activity()
450
+ # An already-settled terminal failure must fail before create_task:
451
+ # conn.prompt registers and enqueues session/prompt at its first
452
+ # yield, and cancelling the caller never removes the queued sender
453
+ # item — the remote could execute a prompt after the client failed.
454
+ if terminal.done():
455
+ await self._fail_prompt_without_response(None, terminal.result())
456
+ sdk_prompt_task = asyncio.create_task(
457
+ conn.prompt(
458
+ prompt=[TextContentBlock(type="text", text=text)],
459
+ session_id=self._require_session_id(),
460
+ ),
461
+ name="chrys.acp.prompt",
462
+ )
463
+ registration_task = asyncio.create_task(store.wait_for_prompt_id(), name="chrys.acp.prompt-registration")
464
+ watchdog = self._start_idle_watchdog()
465
+ try:
466
+ registered, _ = await asyncio.wait(
467
+ {registration_task, terminal},
468
+ return_when=asyncio.FIRST_COMPLETED,
469
+ )
470
+ if terminal in registered and registration_task not in registered:
471
+ registration_task.cancel()
472
+ await _reap_task(registration_task)
473
+ await self._fail_prompt_without_response(sdk_prompt_task, terminal.result())
474
+ prompt_id = await registration_task
475
+
476
+ done, _ = await asyncio.wait(
477
+ {sdk_prompt_task, terminal},
478
+ return_when=asyncio.FIRST_COMPLETED,
479
+ )
480
+ record = store.record_for(prompt_id)
481
+ response_observed = record is not None and record.response_observed
482
+ if terminal in done and not response_observed:
483
+ await self._fail_prompt_without_response(sdk_prompt_task, terminal.result())
484
+
485
+ usage = record.usage if record is not None else None
486
+ response = None
487
+ response_error: BaseException | None = None
488
+ try:
489
+ response = await sdk_prompt_task
490
+ except asyncio.CancelledError as exc:
491
+ # This await suspends when the response was observed on the
492
+ # wire while the SDK task is still pending (e.g. a settled
493
+ # terminal woke the wait above). A caller cancellation lands
494
+ # here and must reach the outer handler, which owns the
495
+ # force-close-and-reraise; only an independently cancelled
496
+ # SDK task stays a classified transport failure.
497
+ current = asyncio.current_task()
498
+ if current is not None and current.cancelling():
499
+ raise
500
+ response_error = exc
501
+ except BaseException as exc:
502
+ response_error = exc
503
+
504
+ # The final drain must stay bounded even with the idle watchdog
505
+ # disabled (idle_timeout_seconds=0): a stuck update sink would
506
+ # otherwise hold prompt() open forever after the response arrived.
507
+ drain_error: BaseException | None = None
508
+ completion = self._require_completion_future()
509
+ drained, _ = await asyncio.wait({completion}, timeout=_FINAL_DRAIN_TIMEOUT_SECONDS)
510
+ if completion in drained:
511
+ try:
512
+ completion.result()
513
+ except BaseException as exc:
514
+ drain_error = exc
515
+ else:
516
+ drain_error = TimeoutError("ACP update processing did not drain after the prompt response.")
517
+
518
+ if drain_error is not None:
519
+ error = AcpTransportError(
520
+ "ACP updates failed before prompt completion.", cause=drain_error, usage=usage
521
+ )
522
+ await self.force_close()
523
+ raise error from drain_error
524
+ # The response sealed the attempt — success AND error frames both
525
+ # end the turn: outstanding inbound callbacks must not outlive it.
526
+ await self._finish_sealed_prompt()
527
+ if terminal.done():
528
+ root = terminal.result()
529
+ if not isinstance(root, _AcpDisconnectError):
530
+ # Response-wins is reserved for EOF after the exact
531
+ # response; any other settled terminal failure (malformed
532
+ # frame, oversized notification, idle expiry that raced
533
+ # the response) is a protocol failure the caller must
534
+ # see — it beats even an in-band error response — with
535
+ # the observed usage preserved.
536
+ classified = classify_acp_error(root, operation=AcpOperation.PROMPT, stateful=True)
537
+ if isinstance(classified, AcpTransportError):
538
+ classified.usage = usage
539
+ await self.force_close()
540
+ raise classified from root
541
+ if response_error is not None:
542
+ classified = classify_acp_error(
543
+ response_error,
544
+ operation=AcpOperation.PROMPT,
545
+ stateful=True,
546
+ )
547
+ if isinstance(classified, AcpTransportError):
548
+ classified.usage = usage
549
+ await self.force_close()
550
+ elif terminal.done():
551
+ # Only the deferred EOF-after-response settles here (the
552
+ # arbitration above raised every other root). The exact
553
+ # error response still wins the classification, but the
554
+ # known-dead transport must be closed before raising:
555
+ # the deferred disconnect spawned no failure-close.
556
+ await self.force_close()
557
+ raise classified from response_error
558
+ if terminal.done():
559
+ # Only the deferred EOF-after-response reaches here: the
560
+ # response wins, and this close releases the dead transport.
561
+ await self.force_close()
562
+ # A missing response is possible only on the error path raised immediately above.
563
+ prompt_response = cast(PromptResponse, response)
564
+ return AcpPromptOutcome(
565
+ stop_reason=prompt_response.stop_reason,
566
+ usage=usage,
567
+ user_message_id=prompt_response.user_message_id,
568
+ )
569
+ except asyncio.CancelledError:
570
+ store.reject_all_outgoing(ConnectionError("ACP prompt was cancelled."))
571
+ sdk_prompt_task.cancel()
572
+ await _reap_task(sdk_prompt_task)
573
+ completion = self._completion_future
574
+ if completion is not None and not completion.done():
575
+ completion.cancel()
576
+ await self.force_close()
577
+ raise
578
+ finally:
579
+ if watchdog is not None:
580
+ watchdog.cancel()
581
+ await _reap_task(watchdog)
582
+ if not registration_task.done():
583
+ registration_task.cancel()
584
+ await _reap_task(registration_task)
585
+
586
+ async def _finish_sealed_prompt(self) -> None:
587
+ """Release inbound waits once the exact prompt response is observed.
588
+
589
+ In-flight human callbacks are cancelled and PR-2 waits torn down here
590
+ on the success path; every failure path reaches the same releases via
591
+ the force-close ladder instead.
592
+ """
593
+ if self._wait_controller is not None:
594
+ # Run the PR-2 seam as a task: an inline await could swallow an
595
+ # outer cancellation inside the seam and stall the caller. Unlike
596
+ # the close ladder's _reap_task, this bounded wait must PRESERVE a
597
+ # caller cancellation: prompt()'s CancelledError handler owns the
598
+ # force-close-and-reraise, and suppressing here would hand a
599
+ # successful outcome to a cancelled caller.
600
+ waits_task = asyncio.create_task(
601
+ self._wait_controller.cancel_pending_waits(),
602
+ name="chrys.acp.cancel-waits",
603
+ )
604
+ waits_task.add_done_callback(_consume_future_exception)
605
+ try:
606
+ _done, pending = await asyncio.wait({waits_task}, timeout=_transport._REQUEST_DRAIN_TIMEOUT_SECONDS)
607
+ except asyncio.CancelledError:
608
+ waits_task.cancel()
609
+ raise
610
+ for stuck in pending:
611
+ stuck.cancel()
612
+ if self._dispatcher is not None:
613
+ await self._dispatcher.cancel_runners()
614
+
615
+ async def _fail_prompt_without_response(
616
+ self,
617
+ sdk_prompt_task: asyncio.Task[Any] | None,
618
+ failure: BaseException,
619
+ ) -> None:
620
+ self._require_store().reject_all_outgoing(ConnectionError("ACP prompt failed."))
621
+ if sdk_prompt_task is not None:
622
+ sdk_prompt_task.cancel()
623
+ await _reap_task(sdk_prompt_task)
624
+ completion = self._completion_future
625
+ if completion is not None:
626
+ if not completion.done():
627
+ completion.set_exception(failure)
628
+ with contextlib.suppress(BaseException):
629
+ await completion
630
+ await self.force_close()
631
+ if isinstance(failure, _AcpDisconnectError):
632
+ raise AcpTransportError("The ACP process closed before returning a prompt response.", cause=failure)
633
+ if isinstance(failure, AcpClientError):
634
+ raise failure
635
+ raise AcpTransportError("The ACP transport failed before a prompt response.", cause=failure) from failure
636
+
637
+ async def cancel(self) -> None:
638
+ """Fire a session-cancel notification and return without waiting for the peer."""
639
+ self._cancel_requested = True
640
+ if self._conn is None or self._session_id is None or self._closing:
641
+ return
642
+ if self._cancel_task is not None and not self._cancel_task.done():
643
+ return
644
+
645
+ async def send_cancel() -> None:
646
+ with contextlib.suppress(Exception):
647
+ await self._require_conn().cancel(session_id=self._require_session_id())
648
+
649
+ self._cancel_task = asyncio.create_task(send_cancel(), name="chrys.acp.cancel")
650
+
651
+ async def aclose(self) -> None:
652
+ """Attempt a bounded session close, then always execute the force-close ladder."""
653
+ try:
654
+ if self._conn is not None and self._session_id is not None and not self._closing:
655
+ try:
656
+ async with asyncio.timeout(_CLOSE_RPC_TIMEOUT_SECONDS):
657
+ await self._conn.close_session(session_id=self._session_id)
658
+ except RequestError as exc:
659
+ if not (type(exc.code) is int and exc.code == -32601):
660
+ logger.debug("ACP session/close failed", exc_info=True)
661
+ except Exception:
662
+ logger.debug("ACP session/close failed", exc_info=True)
663
+ finally:
664
+ await self.force_close()
665
+
666
+ async def force_close(self) -> None:
667
+ """Idempotently close SDK tasks and terminate the retained process tree."""
668
+ if self._force_close_task is None:
669
+ self._force_close_task = asyncio.create_task(self._force_close_impl(), name="chrys.acp.force-close")
670
+ task = self._force_close_task
671
+ try:
672
+ await asyncio.shield(task)
673
+ except asyncio.CancelledError:
674
+ await asyncio.shield(task)
675
+ raise
676
+
677
+ async def _force_close_impl(self) -> None:
678
+ self._closing = True
679
+ close_failure: AcpClientError = (
680
+ AcpTransportError("The ACP client was closed.")
681
+ if self._stateful
682
+ else AcpConnectError("The ACP client was closed.")
683
+ )
684
+ terminal = self._terminal_failure
685
+ if terminal is not None and not terminal.done():
686
+ terminal.set_result(close_failure)
687
+ completion = self._completion_future
688
+ if completion is not None and not completion.done():
689
+ completion.set_exception(close_failure)
690
+ pump = self._pump
691
+ if pump is not None:
692
+ await pump.stop()
693
+ dispatcher = self._dispatcher
694
+ sender = self._sender
695
+ if dispatcher is not None:
696
+ dispatcher.close_admission()
697
+ if self._observer is not None:
698
+ self._observer.clear()
699
+ if self._wait_controller is not None:
700
+ # Run the PR-2 seam as a task: an inline await could swallow the
701
+ # timeout's single cancellation and stall the ladder forever.
702
+ waits_task = asyncio.create_task(
703
+ self._wait_controller.cancel_pending_waits(),
704
+ name="chrys.acp.cancel-waits",
705
+ )
706
+ await _reap_task(waits_task, timeout_seconds=_transport._REQUEST_DRAIN_TIMEOUT_SECONDS)
707
+ if dispatcher is not None:
708
+ await dispatcher.drain_runners()
709
+ if self._cancel_task is not None:
710
+ self._cancel_task.cancel()
711
+ await _reap_task(self._cancel_task)
712
+ if sender is not None:
713
+ sender.begin_close()
714
+
715
+ close_failed = False
716
+ if self._conn is not None:
717
+ close_task = asyncio.create_task(self._conn.close(), name="chrys.acp.sdk-close")
718
+ # asyncio.wait, never `asyncio.timeout: await close_task`: the
719
+ # timeout's single cancellation would forward into close_task,
720
+ # where conn.close()'s own suppress(CancelledError) awaits (our
721
+ # dispatcher.stop, the SDK supervisor.shutdown) can consume it
722
+ # while a wedged sender keeps close_task pending forever.
723
+ close_failed = True
724
+ with contextlib.suppress(asyncio.CancelledError, Exception):
725
+ done, _ = await asyncio.wait({close_task}, timeout=_transport._CLOSE_STAGE_TIMEOUT_SECONDS)
726
+ if close_task in done:
727
+ close_failed = close_task.cancelled() or close_task.exception() is not None
728
+ if close_failed:
729
+ close_task.cancel()
730
+ await _reap_task(close_task)
731
+ if close_failed:
732
+ if self._store is not None:
733
+ self._store.reject_all_outgoing(ConnectionError("ACP connection close failed."))
734
+ if sender is not None:
735
+ await sender.emergency_stop()
736
+ if dispatcher is not None:
737
+ await dispatcher.emergency_stop()
738
+ # The SDK only shuts its supervisor down after its sender close
739
+ # succeeds; a wedged close would otherwise leak the receive task.
740
+ # Run shutdown as a task: awaited inline, the SDK's own
741
+ # suppress(CancelledError) would swallow the timeout's single
742
+ # cancellation and the next stuck runner would hang the ladder.
743
+ supervisor = self._supervisor
744
+ if supervisor is not None:
745
+ shutdown_task = asyncio.create_task(
746
+ supervisor.shutdown(),
747
+ name="chrys.acp.supervisor-shutdown",
748
+ )
749
+ await _reap_task(shutdown_task)
750
+
751
+ spawn = self._spawn
752
+ try:
753
+ if spawn is not None:
754
+ exited = False
755
+ try:
756
+ spawn.process.close_stdin()
757
+ exited = await _wait_process_bounded(spawn, _transport._CLOSE_STAGE_TIMEOUT_SECONDS)
758
+ if not exited:
759
+ spawn.process.terminate_tree()
760
+ exited = await _wait_process_bounded(spawn, _transport._CLOSE_STAGE_TIMEOUT_SECONDS)
761
+ except OSError:
762
+ # A failed OS-level wait must not abort the ladder: this
763
+ # close task is cached, so an escape here would leave every
764
+ # later force_close re-raising with resources still held.
765
+ pass
766
+ finally:
767
+ # Descendants may survive stdin EOF or SIGTERM even when
768
+ # the direct child exits; the owned process group/Job stays
769
+ # the authority, so always finish with a kill sweep.
770
+ spawn.process.kill_tree()
771
+ if not exited:
772
+ with contextlib.suppress(OSError):
773
+ await _wait_process_bounded(spawn, _transport._CLOSE_STAGE_TIMEOUT_SECONDS)
774
+ finally:
775
+ if self._updates is not None:
776
+ self._updates.close()
777
+ if self._consumer_task is not None:
778
+ self._consumer_task.cancel()
779
+ await _reap_task(self._consumer_task)
780
+ if spawn is not None:
781
+ await spawn.stderr.stop()
782
+ spawn.process.close_transports()
783
+
784
+ def _install_connection(self) -> None:
785
+ spawn = self._spawn
786
+ if spawn is None:
787
+ raise RuntimeError("ACP process has not been spawned.")
788
+ sdk_reader = asyncio.StreamReader(limit=ACP_STDIO_LIMIT_BYTES)
789
+ pause_transport = _PauseTransport()
790
+ # StreamReader uses only pause/resume hooks here, which _PauseTransport implements.
791
+ sdk_reader.set_transport(cast(asyncio.BaseTransport, pause_transport))
792
+ self._store = _TrackingStateStore()
793
+ self._updates = _ObserverBuffer()
794
+ self._observer = _FrameObserver(
795
+ store=self._store,
796
+ updates=self._updates,
797
+ active_session=self._active_session_id,
798
+ fail=self._fail,
799
+ activity=self._touch_activity,
800
+ )
801
+ self._pump = _FramingPump(
802
+ spawn.process.stdout,
803
+ sdk_reader,
804
+ pause_transport,
805
+ stateful=lambda: self._stateful,
806
+ closing=lambda: self._closing,
807
+ preflight=self._preflight_request,
808
+ on_response=self._observe_response_at_pump,
809
+ active_session=self._active_session_id,
810
+ fail=self._fail,
811
+ )
812
+
813
+ def sender_factory(writer: asyncio.StreamWriter, supervisor: TaskSupervisor) -> _RetainedSender:
814
+ self._supervisor = supervisor
815
+ self._sender = _RetainedSender(
816
+ writer,
817
+ supervisor,
818
+ store=self._require_store(),
819
+ before_response_send=self._require_observer().before_response_send,
820
+ on_failure=self._fail,
821
+ )
822
+ return self._sender
823
+
824
+ def dispatcher_factory(
825
+ queue: MessageQueue,
826
+ supervisor: TaskSupervisor,
827
+ state: MessageStateStore,
828
+ request_runner: RequestRunner,
829
+ notification_runner: NotificationRunner,
830
+ ) -> _BackpressureDispatcher:
831
+ self._dispatcher = _BackpressureDispatcher(
832
+ queue=queue,
833
+ supervisor=supervisor,
834
+ store=state,
835
+ request_runner=request_runner,
836
+ notification_runner=notification_runner,
837
+ on_failure=self._fail,
838
+ )
839
+ return self._dispatcher
840
+
841
+ # AcpAgentClient implements the SDK Client protocol; decorators obscure structural matching from ty.
842
+ self._conn = ClientSideConnection(
843
+ cast(Client, self),
844
+ spawn.process.stdin,
845
+ sdk_reader,
846
+ queue=InMemoryMessageQueue(maxsize=256),
847
+ state_store=self._store,
848
+ sender_factory=sender_factory,
849
+ dispatcher_factory=dispatcher_factory,
850
+ observers=[self._observer],
851
+ receive_timeout=None,
852
+ )
853
+ self._pump.start()
854
+ self._consumer_task = asyncio.create_task(self._consume_updates(), name="chrys.acp.updates")
855
+
856
+ async def _consume_updates(self) -> None:
857
+ sequence = 0
858
+ try:
859
+ while True:
860
+ item = await self._require_updates().get()
861
+ if isinstance(item, _PromptBarrier):
862
+ completion = self._completion_future
863
+ if (
864
+ completion is not None
865
+ and not completion.done()
866
+ and self._store is not None
867
+ and self._store.prompt_id == item.request_id
868
+ ):
869
+ completion.set_result(None)
870
+ continue
871
+ # Residual guard behind the observer's intake filter — a raw
872
+ # peek, so a foreign frame is dropped before its shape can
873
+ # fail validation and terminate the attempt.
874
+ if item.get("sessionId") != self._active_session_id():
875
+ logger.warning("Dropping ACP session/update for a foreign session")
876
+ continue
877
+ if not _valid_raw_usage_update(item):
878
+ continue
879
+ notification = SessionNotification.model_validate(item)
880
+ sequence += 1
881
+ await self._update_sink.put(sequence, notification)
882
+ except asyncio.CancelledError:
883
+ raise
884
+ except EOFError:
885
+ return
886
+ except BaseException as exc:
887
+ failure = AcpTransportError("ACP update validation or callback failed.", cause=exc)
888
+ completion = self._completion_future
889
+ if completion is not None and not completion.done():
890
+ completion.set_exception(failure)
891
+ self._fail(failure)
892
+
893
+ def _fail(self, failure: BaseException) -> None:
894
+ if not self._stateful and isinstance(failure, AcpTransportError):
895
+ if isinstance(failure, _AcpProtocolLimitError):
896
+ # Cap and budget violations replay identically on every
897
+ # respawn (same executable, same greeting), so the connect
898
+ # window must surface them as deterministic incompatibility,
899
+ # not as a retryable connect failure.
900
+ failure = AcpConfigError(failure.detail, cause=failure.cause or failure)
901
+ else:
902
+ failure = AcpConnectError(failure.detail, cause=failure.cause or failure)
903
+ terminal = self._terminal_failure
904
+ if terminal is not None and not terminal.done():
905
+ terminal.set_result(failure)
906
+ completion = self._completion_future
907
+ response_observed = False
908
+ if self._store is not None and self._store.prompt_id is not None:
909
+ record = self._store.record_for(self._store.prompt_id)
910
+ response_observed = record is not None and record.response_observed
911
+ defer_disconnect = isinstance(failure, _AcpDisconnectError) and response_observed
912
+ if completion is not None and not completion.done() and not defer_disconnect:
913
+ completion.set_exception(failure)
914
+ if isinstance(failure, AcpTransportError | AcpConnectError) and not self._closing and not defer_disconnect:
915
+ self._failure_close_task = asyncio.create_task(self.force_close(), name="chrys.acp.failure-close")
916
+
917
+ def _active_session_id(self) -> str | None:
918
+ """The authoritative session id, or the pump-announced one before assignment."""
919
+ return self._session_id or self._announced_session_id
920
+
921
+ def _observe_response_at_pump(self, message: dict[str, Any]) -> None:
922
+ """Track a matching response synchronously, in pump frame order.
923
+
924
+ A response coalesced with EOF in one transport read is processed by
925
+ the pump without yielding, so the SDK observer would mark
926
+ ``response_observed`` only after the EOF classification already ran —
927
+ misclassifying a completed prompt, or a deterministic error response,
928
+ as a transport failure. Observation therefore happens here; sealing
929
+ and the update barrier stay in the SDK observer, which is the only
930
+ point ordered against the queued updates.
931
+ """
932
+ store = self._store
933
+ request_id = message.get("id")
934
+ if store is not None and type(request_id) is int:
935
+ try:
936
+ store.observe_response(request_id, message)
937
+ except AcpTransportError:
938
+ raise
939
+ except BaseException as exc:
940
+ raise AcpTransportError(
941
+ "The ACP frame observer failed.",
942
+ cause=exc,
943
+ ) from exc
944
+ self._sniff_session_response(message)
945
+
946
+ def _sniff_session_response(self, message: dict[str, Any]) -> None:
947
+ """Capture the announced session id at the pump, ahead of SDK routing.
948
+
949
+ ``_session_id`` is assigned only when ``open_session`` resumes after
950
+ the SDK resolves ``session/new``; an agent may write a session-bound
951
+ request back-to-back with that response, and the pump would preflight
952
+ it before the assignment lands. The pump orders frames serially, so
953
+ sniffing the response stream here closes the race for every session
954
+ check downstream (preflight, observer, callbacks, update consumer).
955
+ """
956
+ if self._session_id is not None or self._announced_session_id is not None:
957
+ return
958
+ store = self._store
959
+ request_id = message.get("id")
960
+ if store is None or type(request_id) is not int:
961
+ return
962
+ record = store.record_for(request_id)
963
+ if record is None or record.method != _NEW_SESSION_METHOD or not record.write_committed:
964
+ return
965
+ # First-response claim: the SDK ignores duplicate response ids, but the
966
+ # pump sees every frame before routing — after a failed session/new, a
967
+ # duplicate success frame with the same id must not announce.
968
+ if self._session_response_claimed:
969
+ return
970
+ self._session_response_claimed = True
971
+ result = message.get("result")
972
+ if type(result) is not dict:
973
+ return
974
+ # The announced id is retained and compared on every subsequent frame
975
+ # BEFORE the store's caps run SDK-side, so an uncapped result must
976
+ # never announce; downstream validation still fails the open attempt.
977
+ try:
978
+ _validate_payload_caps(result)
979
+ except ValueError:
980
+ return
981
+ # Caps bound only size and shape; open_session accepts this result
982
+ # solely when it validates as NewSessionResponse, so announcement
983
+ # must clear the same schema gate before exposing the id.
984
+ try:
985
+ NewSessionResponse.model_validate(result)
986
+ except ValidationError:
987
+ return
988
+ session_id = result.get("sessionId")
989
+ if type(session_id) is str and 0 < len(session_id) <= protocol._MAX_REQUEST_ID_CHARS:
990
+ self._announced_session_id = session_id
991
+
992
+ def _touch_activity(self) -> None:
993
+ with contextlib.suppress(RuntimeError):
994
+ self._last_activity = asyncio.get_running_loop().time()
995
+ if self._activity_event is not None:
996
+ self._activity_event.set()
997
+
998
+ def _start_idle_watchdog(self) -> asyncio.Task[None] | None:
999
+ if self._spec.idle_timeout_seconds == 0:
1000
+ return None
1001
+ return asyncio.create_task(self._idle_watchdog(), name="chrys.acp.idle-watchdog")
1002
+
1003
+ async def _idle_watchdog(self) -> None:
1004
+ timeout_seconds = self._spec.idle_timeout_seconds
1005
+ event = self._activity_event
1006
+ if event is None:
1007
+ return
1008
+ while True:
1009
+ event.clear()
1010
+ # The exact prompt response ends the agent's idle responsibility;
1011
+ # draining already-received updates through a slow local sink is
1012
+ # governed by the final-drain bound, never blamed on the agent.
1013
+ if self._prompt_response_sealed():
1014
+ return
1015
+ observer = self._observer
1016
+ if observer is not None and observer.human_wait_count:
1017
+ await event.wait()
1018
+ continue
1019
+ remaining = timeout_seconds - (asyncio.get_running_loop().time() - self._last_activity)
1020
+ if remaining <= 0:
1021
+ self._fail(AcpIdleTimeoutError("The ACP agent stopped producing protocol activity."))
1022
+ return
1023
+ try:
1024
+ async with asyncio.timeout(remaining):
1025
+ await event.wait()
1026
+ except TimeoutError:
1027
+ # The seal and the expiry timer can land on the same wake;
1028
+ # a sealed attempt must never fail as agent-idle.
1029
+ if self._prompt_response_sealed():
1030
+ return
1031
+ self._fail(AcpIdleTimeoutError("The ACP agent stopped producing protocol activity."))
1032
+ return
1033
+
1034
+ def _prompt_response_sealed(self) -> bool:
1035
+ observer = self._observer
1036
+ if observer is not None and observer.prompt_sealed:
1037
+ return True
1038
+ # The pump marks the prompt record strictly before the SDK observer
1039
+ # runs the seal; a response already observed on the wire must never
1040
+ # lose to idle expiry inside that scheduling window.
1041
+ store = self._store
1042
+ if store is None or store.prompt_id is None:
1043
+ return False
1044
+ record = store.record_for(store.prompt_id)
1045
+ return record is not None and record.response_observed
1046
+
1047
+ def _preflight_request(self, message: dict[str, Any]) -> dict[str, Any]:
1048
+ try:
1049
+ params = message.get("params")
1050
+ if type(params) is not dict:
1051
+ raise ValueError("ACP request params must be an object.")
1052
+ _validate_payload_caps(params)
1053
+ active = self._active_session_id()
1054
+ if active is None or type(params.get("sessionId")) is not str or params["sessionId"] != active:
1055
+ raise ValueError("ACP request is not bound to the active session.")
1056
+ meta = params.get("_meta")
1057
+ if meta is not None and (type(meta) is not dict or _META_COLLISION_KEYS.intersection(meta)):
1058
+ raise ValueError("ACP request metadata collides with callback fields.")
1059
+ method = message["method"]
1060
+ if method == protocol._PERMISSION_METHOD:
1061
+ _preflight_permission(params)
1062
+ elif method == protocol._ASK_USER_METHOD:
1063
+ _preflight_ask_user(params)
1064
+ except ValueError:
1065
+ replacement = dict(message)
1066
+ replacement["params"] = {}
1067
+ return replacement
1068
+ return message
1069
+
1070
+ async def request_permission(
1071
+ self,
1072
+ options: list[PermissionOption],
1073
+ session_id: str,
1074
+ tool_call: ToolCallUpdate,
1075
+ **kwargs: Any,
1076
+ ) -> RequestPermissionResponse:
1077
+ if session_id != self._active_session_id():
1078
+ raise RequestError.invalid_params({"details": "Foreign ACP session."})
1079
+ request_id = _CURRENT_INBOUND_ID.get()
1080
+ observer = self._require_observer()
1081
+ if not observer.human_wait_admitted(request_id):
1082
+ return RequestPermissionResponse(outcome=DeniedOutcome(outcome="cancelled"))
1083
+ options_by_id: dict[str, PermissionOption] = {}
1084
+ for option in options:
1085
+ if option.option_id in options_by_id:
1086
+ # The wire outcome names only an option id, so duplicate ids
1087
+ # hand the agent the choice of what a selection meant — no
1088
+ # response can be transmitted faithfully.
1089
+ observer.clear_human_wait(request_id)
1090
+ raise RequestError.invalid_params({"details": "Duplicate ACP permission option ids."})
1091
+ options_by_id[option.option_id] = option
1092
+ try:
1093
+ try:
1094
+ decision = await self._callbacks.on_permission_request(tool_call, tuple(options))
1095
+ except asyncio.CancelledError:
1096
+ raise
1097
+ except Exception as exc:
1098
+ self._fail(AcpTransportError("The ACP permission callback failed.", cause=exc))
1099
+ raise
1100
+ finally:
1101
+ observer.clear_human_wait(request_id)
1102
+ if decision.action == "cancelled" or decision.option_id is None:
1103
+ return RequestPermissionResponse(outcome=DeniedOutcome(outcome="cancelled"))
1104
+ selected = options_by_id.get(decision.option_id)
1105
+ if selected is None:
1106
+ return RequestPermissionResponse(outcome=DeniedOutcome(outcome="cancelled"))
1107
+ if selected.kind not in _DECISION_OPTION_KINDS.get(decision.action, frozenset()):
1108
+ # An id of the wrong kind must never transmute the decision —
1109
+ # deny stays deny even when the callback names an allow option.
1110
+ logger.warning("An ACP permission decision does not match its selected option kind; cancelling")
1111
+ return RequestPermissionResponse(outcome=DeniedOutcome(outcome="cancelled"))
1112
+ return RequestPermissionResponse(
1113
+ outcome=AllowedOutcome(outcome="selected", option_id=decision.option_id),
1114
+ )
1115
+
1116
+ async def session_update(self, **kwargs: Any) -> None:
1117
+ return
1118
+
1119
+ async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
1120
+ request_id = _CURRENT_INBOUND_ID.get()
1121
+ try:
1122
+ if method != "chrys/request_input":
1123
+ raise RequestError.method_not_found(method)
1124
+ active = self._active_session_id()
1125
+ if active is None:
1126
+ raise RequestError.invalid_params({"details": "Foreign ACP session."})
1127
+ _validate_ext_params(method, params, active)
1128
+ if not self._require_observer().human_wait_admitted(request_id):
1129
+ return {"cancelled": True}
1130
+ try:
1131
+ return await self._callbacks.on_ext_method(method, params)
1132
+ except asyncio.CancelledError:
1133
+ raise
1134
+ except Exception as exc:
1135
+ self._fail(AcpTransportError("The ACP request-input callback failed.", cause=exc))
1136
+ raise
1137
+ finally:
1138
+ if method == "chrys/request_input":
1139
+ self._require_observer().clear_human_wait(request_id)
1140
+
1141
+ async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
1142
+ return
1143
+
1144
+ async def write_text_file(
1145
+ self,
1146
+ content: str,
1147
+ path: str,
1148
+ session_id: str,
1149
+ **kwargs: Any,
1150
+ ) -> WriteTextFileResponse | None:
1151
+ raise RequestError.method_not_found("fs/write_text_file")
1152
+
1153
+ async def read_text_file(
1154
+ self,
1155
+ path: str,
1156
+ session_id: str,
1157
+ limit: int | None = None,
1158
+ line: int | None = None,
1159
+ **kwargs: Any,
1160
+ ) -> ReadTextFileResponse:
1161
+ raise RequestError.method_not_found("fs/read_text_file")
1162
+
1163
+ async def create_terminal(
1164
+ self,
1165
+ command: str,
1166
+ session_id: str,
1167
+ args: list[str] | None = None,
1168
+ cwd: str | None = None,
1169
+ env: list[EnvVariable] | None = None,
1170
+ output_byte_limit: int | None = None,
1171
+ **kwargs: Any,
1172
+ ) -> CreateTerminalResponse:
1173
+ raise RequestError.method_not_found("terminal/create")
1174
+
1175
+ async def terminal_output(
1176
+ self,
1177
+ session_id: str,
1178
+ terminal_id: str,
1179
+ **kwargs: Any,
1180
+ ) -> TerminalOutputResponse:
1181
+ raise RequestError.method_not_found("terminal/output")
1182
+
1183
+ async def release_terminal(
1184
+ self,
1185
+ session_id: str,
1186
+ terminal_id: str,
1187
+ **kwargs: Any,
1188
+ ) -> ReleaseTerminalResponse | None:
1189
+ raise RequestError.method_not_found("terminal/release")
1190
+
1191
+ async def wait_for_terminal_exit(
1192
+ self,
1193
+ session_id: str,
1194
+ terminal_id: str,
1195
+ **kwargs: Any,
1196
+ ) -> WaitForTerminalExitResponse:
1197
+ raise RequestError.method_not_found("terminal/wait_for_exit")
1198
+
1199
+ async def kill_terminal(
1200
+ self,
1201
+ session_id: str,
1202
+ terminal_id: str,
1203
+ **kwargs: Any,
1204
+ ) -> KillTerminalResponse | None:
1205
+ raise RequestError.method_not_found("terminal/kill")
1206
+
1207
+ def on_connect(self, conn: Any) -> None:
1208
+ return
1209
+
1210
+ def _require_conn(self) -> ClientSideConnection:
1211
+ if self._conn is None:
1212
+ raise RuntimeError("ACP client is not connected.")
1213
+ return self._conn
1214
+
1215
+ def _require_store(self) -> _TrackingStateStore:
1216
+ if self._store is None:
1217
+ raise RuntimeError("ACP state store is not installed.")
1218
+ return self._store
1219
+
1220
+ def _require_observer(self) -> _FrameObserver:
1221
+ if self._observer is None:
1222
+ raise RuntimeError("ACP frame observer is not installed.")
1223
+ return self._observer
1224
+
1225
+ def _require_updates(self) -> _ObserverBuffer:
1226
+ if self._updates is None:
1227
+ raise RuntimeError("ACP update buffer is not installed.")
1228
+ return self._updates
1229
+
1230
+ def _require_terminal_failure(self) -> asyncio.Future[BaseException]:
1231
+ if self._terminal_failure is None:
1232
+ raise RuntimeError("ACP terminal failure future is not installed.")
1233
+ return self._terminal_failure
1234
+
1235
+ def _require_completion_future(self) -> asyncio.Future[None]:
1236
+ if self._completion_future is None:
1237
+ raise RuntimeError("ACP completion future is not installed.")
1238
+ return self._completion_future
1239
+
1240
+ def _require_session_id(self) -> str:
1241
+ if self._session_id is None:
1242
+ raise RuntimeError("ACP session is not open.")
1243
+ return self._session_id
1244
+
1245
+ def _require_handshake_deadline(self) -> float:
1246
+ if self._handshake_deadline is None:
1247
+ raise RuntimeError("ACP handshake has not started.")
1248
+ return self._handshake_deadline
1249
+
1250
+
1251
+ def _preflight_permission(params: dict[str, Any]) -> None:
1252
+ options = params.get("options")
1253
+ tool_call = params.get("toolCall")
1254
+ if type(options) is not list or len(options) > _MAX_PERMISSION_OPTIONS or type(tool_call) is not dict:
1255
+ raise ValueError("Malformed ACP permission request.")
1256
+ for option in options:
1257
+ if type(option) is not dict:
1258
+ raise ValueError("Malformed ACP permission option.")
1259
+ if type(option.get("optionId")) is not str or type(option.get("name")) is not str:
1260
+ raise ValueError("Malformed ACP permission option.")
1261
+ for field in ("toolCallId",):
1262
+ if type(tool_call.get(field)) is not str:
1263
+ raise ValueError("Malformed ACP permission tool call.")
1264
+ if "title" in tool_call and tool_call["title"] is not None and type(tool_call["title"]) is not str:
1265
+ raise ValueError("Malformed ACP permission title.")
1266
+
1267
+
1268
+ def _preflight_ask_user(params: dict[str, Any]) -> None:
1269
+ validate_request_input_params(params)
1270
+
1271
+
1272
+ def _validate_ext_params(method: str, params: dict[str, Any], session_id: str) -> None:
1273
+ if type(method) is not str or type(params) is not dict:
1274
+ raise RequestError.invalid_params({"details": "Malformed ACP extension request."})
1275
+ try:
1276
+ validate_json_scalar_tree(params)
1277
+ _validate_payload_caps(params)
1278
+ if type(params.get("sessionId")) is not str or params["sessionId"] != session_id:
1279
+ raise ValueError
1280
+ if method == "chrys/request_input":
1281
+ _preflight_ask_user(params)
1282
+ except ValueError as exc:
1283
+ raise RequestError.invalid_params({"details": "Malformed ACP extension request."}) from exc
1284
+
1285
+
1286
+ def _valid_raw_usage_update(params: dict[str, Any]) -> bool:
1287
+ update = params.get("update")
1288
+ if type(update) is not dict or update.get("sessionUpdate") != "usage_update":
1289
+ return True
1290
+ return _valid_usage_int(update.get("used")) and _valid_usage_int(update.get("size"))
1291
+
1292
+
1293
+ async def _dispose_spawn(spawn: AcpSpawnResult) -> None:
1294
+ """Release a spawn acquired after an already-completed force_close ran."""
1295
+ spawn.process.kill_tree()
1296
+ try:
1297
+ await _wait_process_bounded(spawn, _transport._CLOSE_STAGE_TIMEOUT_SECONDS)
1298
+ finally:
1299
+ await spawn.stderr.stop()
1300
+ spawn.process.close_transports()
1301
+
1302
+
1303
+ async def _wait_process_bounded(spawn: AcpSpawnResult, timeout_seconds: float) -> bool:
1304
+ if spawn.process.returncode is not None:
1305
+ return True
1306
+ try:
1307
+ async with asyncio.timeout(timeout_seconds):
1308
+ await spawn.process.wait()
1309
+ except TimeoutError:
1310
+ return False
1311
+ return True