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,1031 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """MCP adapter — wraps MCP server tools as Chrys FunctionTools.
4
+
5
+ Uses Chrys-owned MCPStdioTool and MCPStreamableHTTPTool implementations to
6
+ connect to MCP servers and expose their tools as FunctionTool instances.
7
+
8
+ Failure model
9
+ -------------
10
+ ``connect()`` raises :class:`MCPConnectionError` on failure; ``connect_all()``
11
+ catches ordinary per-server connection failures so one unavailable server
12
+ cannot block the agent build. Invalid or colliding tool names are configuration
13
+ errors and fail closed through :class:`MCPToolConfigurationError`; otherwise
14
+ the provider may reject the request or Chrys's last-wins tool map could execute
15
+ the wrong function. Ordinary failures are published to the ``EventBus`` as
16
+ ``Warning`` events (``code=
17
+ "mcp.connect_failed"``) and retained on :attr:`MCPAdapter.failures`.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import contextlib
24
+ import dataclasses
25
+ import logging
26
+ import re
27
+ from collections import Counter, deque
28
+ from collections.abc import Awaitable, Callable, Collection, Mapping
29
+ from html import escape as xml_escape
30
+ from typing import TYPE_CHECKING, Any
31
+
32
+ from chrys.foundation.branding import APP_DISPLAY_NAME
33
+ from chrys.foundation.tool_call_context import get_tool_context
34
+ from chrys.kernel.tools import FunctionTool
35
+ from chrys.service.mcp import _connection
36
+ from chrys.service.mcp._connection import DEFAULT_CONNECT_TIMEOUT_SECONDS as DEFAULT_CONNECT_TIMEOUT_SECONDS
37
+ from chrys.service.mcp._progressive import ProgressiveMCPResumeProvider, _ProgressiveMCPExposure
38
+ from chrys.service.mcp._stdio_transport import MAX_BANNER_LINE_CHARS as MAX_BANNER_LINE_CHARS
39
+ from chrys.service.mcp._stdio_transport import MAX_BANNER_LINES_CAPTURED as MAX_BANNER_LINES_CAPTURED
40
+ from chrys.service.mcp._stdio_transport import MAX_STDIO_PATH_DISPLAY_CHARS as MAX_STDIO_PATH_DISPLAY_CHARS
41
+ from chrys.service.mcp._stdio_transport import MAX_STDIO_STDERR_BYTES_CAPTURED as MAX_STDIO_STDERR_BYTES_CAPTURED
42
+ from chrys.service.mcp._stdio_transport import MAX_STDIO_STDERR_PREVIEW_LINES as MAX_STDIO_STDERR_PREVIEW_LINES
43
+ from chrys.service.mcp._stdio_transport import _StdioFailureDiagnostics
44
+ from chrys.service.mcp._stdio_transport import tolerant_stdio_client as tolerant_stdio_client
45
+ from chrys.service.mcp.cache import MCPConnectionCache, MCPConnectionLease, clone_mcp_function_tool
46
+ from chrys.service.mcp.errors import (
47
+ MCPConnectionError,
48
+ MCPToolConfigurationError,
49
+ MCPToolNameAmbiguityError,
50
+ MCPToolNameCollisionError,
51
+ MCPToolNameValidationError,
52
+ )
53
+ from chrys.service.mcp.owned import _MCP_REMOTE_NAME_KEY, _mcp_config_names_for
54
+ from chrys.service.mcp.result_limits import (
55
+ DEFAULT_MCP_TOOL_RESULT_MAX_TOKENS as DEFAULT_MCP_TOOL_RESULT_MAX_TOKENS,
56
+ )
57
+ from chrys.service.mcp.result_limits import resolve_mcp_result_cap
58
+ from chrys.service.mcp.validation import validate_mcp_exposed_tool_name
59
+
60
+ if TYPE_CHECKING:
61
+ from pathlib import Path
62
+
63
+ from chrys.foundation.events.bus import EventBus
64
+ from chrys.service.mcp.owned import MCPTool
65
+ from chrys.service.profiles.agents.schema import MCPServerConfig
66
+
67
+ logger = logging.getLogger(__name__)
68
+
69
+ # Hard timeouts that bound how long a misbehaving MCP server can hold up the
70
+ # UI / agent build. The MCP SDK's per-request timeout
71
+ # (``read_timeout_seconds``) defaults to ``None`` (wait forever), and a
72
+ # server that emits welcome banner text instead of a valid ``initialize``
73
+ # response will leave the client blocked indefinitely on
74
+ # ``response_stream_reader.receive()``. These floors are injected into
75
+ # the MCP tool's ``request_timeout`` when the user didn't set one, so the
76
+ # SDK's own timeout fires from *inside* MCPTool's lifecycle-owner
77
+ # task. We intentionally do **not** wrap ``__aenter__`` in
78
+ # ``asyncio.wait_for`` from the outside: the lifecycle owner is a
79
+ # separate ``asyncio.Task`` (see ``MCPTool._run_lifecycle_owner``) which
80
+ # ``wait_for`` cannot cancel safely (anyio cancel scopes are scoped per
81
+ # task and cross-task cancellation raises "Attempted to exit cancel scope
82
+ # in a different task"). ``MCPServerConfig.request_timeout`` (when set)
83
+ # always takes precedence — these are only the defaults applied when no
84
+ # per-server timeout is configured.
85
+ DEFAULT_TEST_TIMEOUT_SECONDS = 30
86
+
87
+ # Cap per-server InitializeResult.instructions rendered into the
88
+ # <mcp_instructions> reminder. The value is server-controlled and repeated
89
+ # on every LLM call, so it must stay bounded no matter what the server
90
+ # sends. The budget is charged against the *rendered* (escaped + indented)
91
+ # text — capping the raw input instead would let escape expansion multiply
92
+ # the cap (e.g. every '"' becomes '&quot;'). 4k rendered characters fits
93
+ # real-world server instructions comfortably; the aggregate is bounded by
94
+ # the number of user-configured servers.
95
+ MCP_INSTRUCTIONS_CHAR_LIMIT = 4_000
96
+
97
+ # A trailing entity fragment left behind when a rendered line is cut at the
98
+ # budget boundary (e.g. '&quo'). Complete entities end in ';' and don't match.
99
+ _SEVERED_ENTITY_TAIL_RE = re.compile(r"&[#0-9A-Za-z]{0,9}$")
100
+
101
+
102
+ @dataclasses.dataclass(frozen=True)
103
+ class MCPTestReport:
104
+ """What a successful one-shot connection test learned about a server.
105
+
106
+ Every string field is server-controlled: renderers must sanitize each
107
+ one against markup/structure injection before display. ``capabilities``
108
+ holds dotted feature names the server advertised (``tools``,
109
+ ``tools.listChanged``, …) flattened from ``ServerCapabilities``.
110
+ ``initial_tool_names`` is the progressive-disclosure starting surface,
111
+ or ``None`` when progressive disclosure is off.
112
+ """
113
+
114
+ server_name: str | None
115
+ server_title: str | None
116
+ server_version: str | None
117
+ protocol_version: str | None
118
+ capabilities: tuple[str, ...]
119
+ instructions: str | None
120
+ tools: tuple[tuple[str, str], ...]
121
+ prompts: tuple[tuple[str, str], ...]
122
+ initial_tool_names: tuple[str, ...] | None
123
+
124
+
125
+ # Standard MCP server capability groups (the spec's named feature sets).
126
+ # The report renderer always shows these; the flattener records them ahead
127
+ # of extras so the entry cap can never make a declared one look
128
+ # unadvertised.
129
+ STANDARD_CAPABILITY_GROUPS = ("completions", "logging", "prompts", "resources", "tools")
130
+
131
+ # Bounds for flattening server-controlled capability trees: a hostile
132
+ # server could otherwise inflate the report with unbounded extra keys or
133
+ # deep nesting (``ServerCapabilities`` accepts extras).
134
+ _MAX_CAPABILITY_ENTRIES = 128
135
+ _MAX_CAPABILITY_DEPTH = 4
136
+
137
+
138
+ def _top_capability_order(key: object) -> tuple[bool, str]:
139
+ return (str(key) not in STANDARD_CAPABILITY_GROUPS, str(key))
140
+
141
+
142
+ def _flatten_server_capabilities(capabilities: Any) -> tuple[str, ...]:
143
+ """Flatten advertised ``ServerCapabilities`` into sorted dotted feature names.
144
+
145
+ Walks nested capability objects (e.g. ``tasks.requests.tools``):
146
+ ``False`` leaves are omitted, any other advertised value marks presence.
147
+ Traversal is breadth-first with standard groups first at the top level,
148
+ so the entry cap only ever drops the deepest/extra branches — a declared
149
+ standard capability is always recorded.
150
+ """
151
+ if capabilities is None:
152
+ return ()
153
+ try:
154
+ dumped = capabilities.model_dump(exclude_none=True)
155
+ except Exception:
156
+ return ()
157
+ if not isinstance(dumped, dict):
158
+ return ()
159
+ flattened: list[str] = []
160
+ queue: deque[tuple[str, dict[Any, Any], int]] = deque([("", dumped, 1)])
161
+ while queue:
162
+ prefix, mapping, depth = queue.popleft()
163
+ for key in sorted(mapping, key=_top_capability_order if depth == 1 else str):
164
+ if len(flattened) >= _MAX_CAPABILITY_ENTRIES:
165
+ return tuple(sorted(flattened))
166
+ value = mapping[key]
167
+ if value is False or value is None:
168
+ continue
169
+ path = f"{prefix}.{key}" if prefix else str(key)
170
+ flattened.append(path)
171
+ if isinstance(value, dict) and depth < _MAX_CAPABILITY_DEPTH:
172
+ queue.append((path, value, depth + 1))
173
+ return tuple(sorted(flattened))
174
+
175
+
176
+ def _mcp_remote_name(tool: FunctionTool) -> str:
177
+ remote_name = (tool.additional_properties or {}).get(_MCP_REMOTE_NAME_KEY)
178
+ return remote_name if isinstance(remote_name, str) else tool.name
179
+
180
+
181
+ def _sole_config_name(tool: FunctionTool, catalog: list[FunctionTool]) -> str | None:
182
+ """Return the first ``allowed_tools``/``always_load`` name that selects *tool* and no other catalog tool."""
183
+ others = [other for other in catalog if other is not tool]
184
+ return next(
185
+ (
186
+ name
187
+ for name in _mcp_config_names_for(tool)
188
+ if all(name not in _mcp_config_names_for(other) for other in others)
189
+ ),
190
+ None,
191
+ )
192
+
193
+
194
+ class MCPAdapter:
195
+ """Manages MCP server connections and exposes their tools.
196
+
197
+ Supports stdio and HTTP (Streamable HTTP/SSE) transports as configured
198
+ in the agent profile YAML.
199
+
200
+ An ordinary per-server connection failure does not abort the agent build.
201
+ Deterministic tool-name collisions do abort it before any ambiguous tool
202
+ surface can reach the model. Ordinary failures are:
203
+
204
+ * published on the optional ``EventBus`` as ``Warning`` events
205
+ (``code="mcp.connect_failed"``) so the TUI can display them, and
206
+ * retained on :attr:`failures` for inspection by diagnostic UIs.
207
+ """
208
+
209
+ def __init__(
210
+ self,
211
+ bus: EventBus | None = None,
212
+ session_id: str | None = None,
213
+ *,
214
+ cache: MCPConnectionCache | None = None,
215
+ stdio_cwd: str | None = None,
216
+ session_dir: Path | None = None,
217
+ reserved_tool_names: Collection[str] = (),
218
+ ) -> None:
219
+ self._cache = cache or MCPConnectionCache()
220
+ self._owns_cache = cache is None
221
+ self._leases: dict[str, MCPConnectionLease] = {}
222
+ # Compatibility/debug view of this adapter's acquired underlying
223
+ # MCPTool objects. Ownership still flows through ``_leases``.
224
+ self._servers: dict[str, MCPTool] = {}
225
+ self._functions_by_server: dict[str, list[FunctionTool]] = {}
226
+ # Per-server ``expose_instructions`` config, captured at registration.
227
+ # Adapter-level (not on the shared MCPTool): profiles sharing a cached
228
+ # connection may disagree on exposure.
229
+ self._instructions_exposure: dict[str, bool] = {}
230
+ self._progressive_exposures: dict[str, _ProgressiveMCPExposure] = {}
231
+ self._tool_namespace_by_server: dict[str, set[str]] = {}
232
+ self._connecting: dict[str, asyncio.Task[list[Any]]] = {}
233
+ self._failures: dict[str, MCPConnectionError] = {}
234
+ self._tool_names_by_server: dict[str, list[str]] = {}
235
+ self._bus = bus
236
+ self._session_id = session_id
237
+ self._stdio_cwd = stdio_cwd
238
+ self._session_dir = session_dir
239
+ self._reserved_tool_names = frozenset(reserved_tool_names)
240
+ # Serializes the registration critical section shared by ``connect``
241
+ # and ``disconnect``/``disconnect_all``. Without this, a ``disconnect_all``
242
+ # that snapshots ``_servers`` while a concurrent ``connect`` is mid-
243
+ # ``__aenter__`` would miss the about-to-be-registered server, leaving
244
+ # an orphaned subprocess. Tool ``__aexit__`` calls are intentionally
245
+ # performed outside the lock so parallel disconnect is preserved.
246
+ self._lock = asyncio.Lock()
247
+
248
+ def set_reserved_tool_names(self, reserved_tool_names: Collection[str]) -> None:
249
+ """Replace names reserved from MCP namespace validation.
250
+
251
+ This is primarily used by configuration diagnostics whose surrounding
252
+ agent draft can change while the test panel remains mounted. Active
253
+ production adapters keep a stable namespace for their acquired leases.
254
+ """
255
+ if self._leases or self._connecting:
256
+ raise RuntimeError("Cannot replace reserved MCP tool names while connections are active.")
257
+ self._reserved_tool_names = frozenset(reserved_tool_names)
258
+
259
+ @staticmethod
260
+ def _create_tool_with_timeout_floor(config: MCPServerConfig, default_timeout: int) -> Any:
261
+ """Build the MCP tool, injecting the timeout floor when none is configured."""
262
+ return _connection._create_tool_with_timeout_floor(config, default_timeout)
263
+
264
+ @staticmethod
265
+ def _banner_lines_from(tool: Any) -> list[str]:
266
+ """Extract banner diagnostics from a stdio tool, empty list otherwise."""
267
+ return _connection._banner_lines_from(tool)
268
+
269
+ @staticmethod
270
+ def _stdio_failure_diagnostics_from(tool: Any) -> _StdioFailureDiagnostics:
271
+ """Snapshot bounded stdio subprocess diagnostics, empty otherwise."""
272
+ return _connection._stdio_failure_diagnostics_from(tool)
273
+
274
+ @staticmethod
275
+ def _is_timeout_cause(exc: BaseException) -> bool:
276
+ """True if the exception chain contains an MCP/anyio read-timeout signal."""
277
+ return _connection._is_timeout_cause(exc)
278
+
279
+ def create_resume_provider(self) -> ProgressiveMCPResumeProvider | None:
280
+ """Return the per-run refresh/resume provider when progressive servers exist."""
281
+ if not self._progressive_exposures:
282
+ return None
283
+ return ProgressiveMCPResumeProvider(self)
284
+
285
+ def get_server_instructions_map(self) -> dict[str, str]:
286
+ """Return {server_name: instructions} for connected servers with instructions.
287
+
288
+ Servers whose config disabled ``expose_instructions`` are excluded.
289
+ """
290
+ result: dict[str, str] = {}
291
+ for name, server in self._servers.items():
292
+ if not self._instructions_exposure.get(name, True):
293
+ continue
294
+ instructions = getattr(server, "_server_instructions", None)
295
+ if instructions:
296
+ result[name] = instructions
297
+ for name, lease in self._leases.items():
298
+ if name not in result and self._instructions_exposure.get(name, True):
299
+ instructions = getattr(lease.mcp_tool, "_server_instructions", None)
300
+ if instructions:
301
+ result[name] = instructions
302
+ return result
303
+
304
+ def render_instructions_reminder(self) -> str | None:
305
+ """Return an ``<mcp_instructions>`` block with all server instructions, or None.
306
+
307
+ Servers render in name order: ``connect_all`` registers concurrently,
308
+ so insertion order varies run to run and would leak that
309
+ nondeterminism into the prompt text. Per-server text is capped at
310
+ ``MCP_INSTRUCTIONS_CHAR_LIMIT``: instructions are server-controlled
311
+ and injected into every LLM call, so an unbounded value from one
312
+ misbehaving server could push every request past the context limit.
313
+ """
314
+ instructions_map = self.get_server_instructions_map()
315
+ if not instructions_map:
316
+ return None
317
+ lines = ["<mcp_instructions>"]
318
+ for name in sorted(instructions_map):
319
+ escaped_name = xml_escape(name, quote=True)
320
+ lines.append(f' <server name="{escaped_name}">')
321
+ budget = MCP_INSTRUCTIONS_CHAR_LIMIT
322
+ truncated = False
323
+ for line in instructions_map[name].strip().splitlines():
324
+ rendered = f" {xml_escape(line)}".rstrip()
325
+ # +1 charges the joining newline so a blank-line flood
326
+ # (zero-length rendered lines) cannot bypass the budget.
327
+ cost = len(rendered) + 1
328
+ if cost > budget:
329
+ if budget > 1:
330
+ lines.append(_SEVERED_ENTITY_TAIL_RE.sub("", rendered[: budget - 1]))
331
+ truncated = True
332
+ break
333
+ lines.append(rendered)
334
+ budget -= cost
335
+ if truncated:
336
+ lines.append(f" [instructions truncated: exceeded {MCP_INSTRUCTIONS_CHAR_LIMIT} characters]")
337
+ lines.append(" </server>")
338
+ lines.append("</mcp_instructions>")
339
+ return "\n".join(lines)
340
+
341
+ def _progressive_surface_tools(self) -> list[FunctionTool]:
342
+ """Return current controls and always-loaded tools for a fresh run."""
343
+ return [tool for exposure in self._progressive_exposures.values() for tool in exposure.current_surface_tools]
344
+
345
+ def _validate_server_namespace(
346
+ self,
347
+ config: MCPServerConfig,
348
+ catalog: list[FunctionTool],
349
+ control_names: set[str],
350
+ ) -> None:
351
+ """Reject invalid or colliding names before tools reach the model."""
352
+ violations: list[str] = []
353
+ for tool in catalog:
354
+ error = validate_mcp_exposed_tool_name(tool.name)
355
+ if error is None:
356
+ continue
357
+ additional = tool.additional_properties or {}
358
+ remote_name = additional.get(_MCP_REMOTE_NAME_KEY)
359
+ origin = f" Remote catalog name: {remote_name!r}." if isinstance(remote_name, str) else ""
360
+ violations.append(f"{error}{origin}")
361
+ violations.extend(
362
+ f"{error} Generated progressive-disclosure control."
363
+ for control_name in sorted(control_names)
364
+ if (error := validate_mcp_exposed_tool_name(control_name)) is not None
365
+ )
366
+ if violations:
367
+ raise MCPToolNameValidationError(
368
+ config.name,
369
+ config.transport,
370
+ violations=violations,
371
+ )
372
+
373
+ catalog_names = [tool.name for tool in catalog]
374
+ duplicate_catalog_names = {name for name, count in Counter(catalog_names).items() if count > 1}
375
+ if duplicate_catalog_names:
376
+ raise MCPToolNameCollisionError(
377
+ config.name,
378
+ config.transport,
379
+ conflicting_names=duplicate_catalog_names,
380
+ conflict_with=f"another permitted tool from MCP server {config.name!r}",
381
+ guidance=(
382
+ "Rename one of the remote entries, exclude the shared name from the Permitted Tool Set, "
383
+ "or disable 'Expose server prompts' when the duplicate is a prompt."
384
+ ),
385
+ )
386
+
387
+ # One configured name can be one tool's original name and another's
388
+ # prefixed name; both would pass the allowlist. Every tool a name
389
+ # selects survives that filter, so the permitted catalog shows it.
390
+ configured_names = dict.fromkeys([*(config.allowed_tools or ()), *config.always_load])
391
+ matches_by_name = {
392
+ name: [(_mcp_remote_name(tool), _sole_config_name(tool, catalog)) for tool in matches]
393
+ for name in configured_names
394
+ if len(matches := [tool for tool in catalog if name in _mcp_config_names_for(tool)]) > 1
395
+ }
396
+ if matches_by_name:
397
+ raise MCPToolNameAmbiguityError(config.name, config.transport, matches_by_name=matches_by_name)
398
+
399
+ catalog_name_set = set(catalog_names)
400
+ if control_collisions := catalog_name_set & control_names:
401
+ raise MCPToolNameCollisionError(
402
+ config.name,
403
+ config.transport,
404
+ conflicting_names=control_collisions,
405
+ conflict_with="a generated progressive-disclosure control",
406
+ guidance=(
407
+ "Exclude the remote tool, choose a non-conflicting Tool Name Prefix, "
408
+ "or disable progressive loading."
409
+ ),
410
+ )
411
+
412
+ namespace = catalog_name_set | control_names
413
+ if chrys_collisions := namespace & self._reserved_tool_names:
414
+ raise MCPToolNameCollisionError(
415
+ config.name,
416
+ config.transport,
417
+ conflicting_names=chrys_collisions,
418
+ conflict_with=f"a reserved {APP_DISPLAY_NAME} tool",
419
+ guidance="Exclude the remote tool from the Permitted Tool Set or configure a Tool Name Prefix.",
420
+ )
421
+
422
+ for other_server, other_namespace in self._tool_namespace_by_server.items():
423
+ if other_server == config.name:
424
+ continue
425
+ if cross_server_collisions := namespace & other_namespace:
426
+ raise MCPToolNameCollisionError(
427
+ config.name,
428
+ config.transport,
429
+ conflicting_names=cross_server_collisions,
430
+ conflict_with=f"MCP server {other_server!r}",
431
+ guidance="Configure distinct Tool Name Prefix values or exclude the duplicate tool.",
432
+ )
433
+
434
+ if config.name in self._leases:
435
+ self._tool_namespace_by_server[config.name] = namespace
436
+
437
+ def _current_functions_for_server(
438
+ self,
439
+ name: str,
440
+ lease: MCPConnectionLease,
441
+ ) -> list[FunctionTool]:
442
+ """Return a fresh progressive surface or the ordinary cached functions."""
443
+ exposure = self._progressive_exposures.get(name)
444
+ if exposure is not None:
445
+ return exposure.initial_tools
446
+ return list(self._functions_by_server.get(name, lease.functions))
447
+
448
+ def _context_tools_without_collisions(
449
+ self,
450
+ visible_tools: list[Any],
451
+ candidates: list[FunctionTool],
452
+ ) -> list[FunctionTool]:
453
+ """Return missing candidates, rejecting foreign same-name tools loudly."""
454
+ visible_by_name: dict[str, list[Any]] = {}
455
+ for tool in visible_tools:
456
+ if isinstance(tool, FunctionTool):
457
+ visible_by_name.setdefault(tool.name, []).append(tool)
458
+ continue
459
+ if isinstance(tool, Mapping):
460
+ function = tool.get("function")
461
+ if isinstance(function, Mapping) and isinstance(function.get("name"), str):
462
+ visible_by_name.setdefault(function["name"], []).append(tool)
463
+ additions: list[FunctionTool] = []
464
+ for candidate in candidates:
465
+ existing = visible_by_name.get(candidate.name, [])
466
+ if not existing:
467
+ additions.append(candidate)
468
+ visible_by_name[candidate.name] = [candidate]
469
+ continue
470
+
471
+ provenance = get_tool_context(candidate)
472
+ server_name = provenance.get("server_name") if provenance is not None else None
473
+ exposure = self._progressive_exposures.get(server_name) if isinstance(server_name, str) else None
474
+ if (
475
+ len(existing) == 1
476
+ and exposure is not None
477
+ and isinstance(existing[0], FunctionTool)
478
+ and exposure._same_surface_identity(existing[0], candidate)
479
+ ):
480
+ continue
481
+ config = exposure._config if exposure is not None else None
482
+ raise MCPToolNameCollisionError(
483
+ config.name if config is not None else server_name or "unknown",
484
+ config.transport if config is not None else "unknown",
485
+ conflicting_names={candidate.name},
486
+ conflict_with="another visible tool contributed to this run",
487
+ guidance="Configure a distinct Tool Name Prefix or remove the conflicting tool.",
488
+ )
489
+ return additions
490
+
491
+ def _resume_tools(self, calls: list[tuple[str, str | None]]) -> list[FunctionTool]:
492
+ """Clone uniquely resolved hidden MCP tools referenced by pending calls."""
493
+ tools: list[FunctionTool] = []
494
+ added_names: set[str] = set()
495
+ for name, server_name in calls:
496
+ if name in added_names:
497
+ continue
498
+ if server_name is not None:
499
+ exposure = self._progressive_exposures.get(server_name)
500
+ source = exposure.resume_source(name) if exposure is not None else None
501
+ matches = [source] if source is not None else []
502
+ else:
503
+ matches = [
504
+ source
505
+ for exposure in self._progressive_exposures.values()
506
+ if (source := exposure.resume_source(name)) is not None
507
+ ]
508
+ if len(matches) == 1:
509
+ match_context = get_tool_context(matches[0])
510
+ matched_server = (
511
+ server_name
512
+ if server_name is not None
513
+ else match_context.get("server_name")
514
+ if match_context is not None
515
+ else None
516
+ )
517
+ if (
518
+ isinstance(matched_server, str)
519
+ and (exposure := self._progressive_exposures.get(matched_server)) is not None
520
+ ):
521
+ tools.append(exposure.clone_source(matches[0]))
522
+ else:
523
+ tools.append(clone_mcp_function_tool(matches[0]))
524
+ added_names.add(name)
525
+ elif len(matches) > 1:
526
+ logger.warning(
527
+ "Cannot resume ambiguous progressive MCP tool %r; configure distinct tool_name_prefix values.",
528
+ name,
529
+ )
530
+ return tools
531
+
532
+ async def connect(self, config: MCPServerConfig) -> list[Any]:
533
+ """Connect to an MCP server and return its tools as FunctionTools.
534
+
535
+ Idempotent: calling with a previously-connected name returns the
536
+ cached tool list. Bounded by a hard timeout floor
537
+ (``DEFAULT_CONNECT_TIMEOUT_SECONDS``) injected into the SDK's
538
+ ``request_timeout`` when the user didn't configure one — so a
539
+ hung server cannot block the agent build.
540
+
541
+ Raises:
542
+ MCPConnectionError: If the server fails to connect, initialize,
543
+ or exceeds the connect timeout. ``asyncio.CancelledError``
544
+ is propagated unchanged after the partially-opened transport
545
+ is closed.
546
+ """
547
+ # Validate before cache lookup as well as connection creation: a cache
548
+ # hit must not let malformed wrapper-only progressive fields bypass the
549
+ # checks performed by ``_create_mcp_tool`` on a cold connection.
550
+ _connection._validate_config(config)
551
+ async with self._lock:
552
+ lease = self._leases.get(config.name)
553
+ if lease is not None:
554
+ return self._current_functions_for_server(config.name, lease)
555
+ task = self._connecting.get(config.name)
556
+ if task is None:
557
+ task = asyncio.create_task(self._acquire_new(config))
558
+ self._connecting[config.name] = task
559
+
560
+ def _discard_finished(done: asyncio.Task[list[Any]], name: str = config.name) -> None:
561
+ if self._connecting.get(name) is done:
562
+ self._connecting.pop(name, None)
563
+
564
+ task.add_done_callback(_discard_finished)
565
+
566
+ try:
567
+ return await task
568
+ finally:
569
+ if task.done():
570
+ async with self._lock:
571
+ if self._connecting.get(config.name) is task:
572
+ self._connecting.pop(config.name, None)
573
+
574
+ async def _connect_new(self, config: MCPServerConfig) -> list[Any]:
575
+ """Open or lease a new MCP server connection and register it on success."""
576
+ current_task = asyncio.current_task()
577
+ inserted = False
578
+ if current_task is not None:
579
+ async with self._lock:
580
+ if config.name not in self._connecting:
581
+ self._connecting[config.name] = current_task
582
+ inserted = True
583
+ try:
584
+ return await self._acquire_new(config)
585
+ finally:
586
+ if inserted:
587
+ async with self._lock:
588
+ if self._connecting.get(config.name) is current_task:
589
+ self._connecting.pop(config.name, None)
590
+
591
+ async def _acquire_new(self, config: MCPServerConfig) -> list[Any]:
592
+ """Acquire a cache lease and register it on this adapter."""
593
+ lease: MCPConnectionLease | None = None
594
+ duplicate_tools: list[Any] | None = None
595
+ release_duplicate = False
596
+ prune_private_cache = False
597
+ registered = False
598
+ exposure: _ProgressiveMCPExposure | None = None
599
+ delivered_tools: list[FunctionTool]
600
+ catalog: list[FunctionTool]
601
+ control_names: set[str] = set()
602
+ namespace: set[str]
603
+
604
+ try:
605
+ lease = await self._cache.acquire(config, stdio_cwd=self._stdio_cwd)
606
+ except MCPConnectionError as err:
607
+ self._failures[config.name] = err
608
+ raise
609
+ if lease is None:
610
+ raise RuntimeError("Acquiring an MCP connection did not return a lease.")
611
+
612
+ try:
613
+ catalog = [tool for tool in lease.mcp_tool.functions if isinstance(tool, FunctionTool)]
614
+ result_cap = resolve_mcp_result_cap(config.max_tool_result_tokens)
615
+ if config.use_progressive_disclosure:
616
+ exposure = _ProgressiveMCPExposure(
617
+ config,
618
+ lease.mcp_tool,
619
+ self._validate_server_namespace,
620
+ result_cap=result_cap,
621
+ spill_dir=self._session_dir,
622
+ )
623
+ control_names = exposure.control_names
624
+ delivered_tools = exposure.initial_tools
625
+ else:
626
+ self._validate_server_namespace(config, catalog, control_names)
627
+ delivered_tools = [
628
+ clone_mcp_function_tool(source, result_cap=result_cap, spill_dir=self._session_dir)
629
+ for source in catalog
630
+ ]
631
+ namespace = {tool.name for tool in catalog} | control_names
632
+ except Exception:
633
+ await self._release_unregistered_lease(config.name, lease)
634
+ raise
635
+
636
+ try:
637
+ async with self._lock:
638
+ existing = self._leases.get(config.name)
639
+ if existing is not None:
640
+ duplicate_tools = self._current_functions_for_server(config.name, existing)
641
+ release_duplicate = True
642
+ elif (existing_server := self._servers.get(config.name)) is not None:
643
+ # existing_server.functions belong to the owned MCP engine.
644
+ # Every outward path must deliver Chrys FunctionTool clones
645
+ # so each acquisition keeps its own tool state and result cap.
646
+ result_cap = resolve_mcp_result_cap(config.max_tool_result_tokens)
647
+ duplicate_tools = [
648
+ clone_mcp_function_tool(t, result_cap=result_cap, spill_dir=self._session_dir)
649
+ for t in existing_server.functions
650
+ ]
651
+ release_duplicate = True
652
+ prune_private_cache = self._owns_cache
653
+ else:
654
+ current_task = asyncio.current_task()
655
+ if self._connecting.get(config.name) is not current_task:
656
+ raise asyncio.CancelledError
657
+ # Re-check under the registration lock so parallel server
658
+ # connects cannot both pass against an empty namespace.
659
+ self._validate_server_namespace(config, catalog, control_names)
660
+ self._leases[config.name] = lease
661
+ self._servers[config.name] = lease.mcp_tool
662
+ self._functions_by_server[config.name] = delivered_tools
663
+ self._instructions_exposure[config.name] = config.expose_instructions
664
+ self._tool_namespace_by_server[config.name] = namespace
665
+ if exposure is not None:
666
+ self._progressive_exposures[config.name] = exposure
667
+ registered = True
668
+ except BaseException:
669
+ if lease is not None and not registered:
670
+ await self._release_unregistered_lease(config.name, lease)
671
+ raise
672
+
673
+ if release_duplicate:
674
+ await self._release_unregistered_lease(config.name, lease)
675
+ if prune_private_cache:
676
+ await self._cache.prune_idle(max_idle_seconds=0)
677
+ if duplicate_tools is None:
678
+ raise RuntimeError("A duplicate MCP registration has no registered tools.")
679
+ return duplicate_tools
680
+
681
+ self._failures.pop(config.name, None)
682
+ if exposure is not None and (missing := exposure.unmatched_always_load_names):
683
+ missing_names = ", ".join(missing)
684
+ await self._publish_warning(
685
+ code="mcp.always_load_missing",
686
+ message=(
687
+ f"MCP server '{config.name}' did not advertise configured initially visible "
688
+ f"tool(s): {missing_names}. They will be picked up automatically if advertised later."
689
+ ),
690
+ )
691
+ logger.debug(
692
+ "Connected to MCP server '%s' (%s): %d tools loaded",
693
+ config.name,
694
+ config.transport,
695
+ len(lease.functions),
696
+ )
697
+ return list(delivered_tools)
698
+
699
+ async def _release_unregistered_lease(self, name: str, lease: MCPConnectionLease) -> None:
700
+ try:
701
+ await self._finish_cleanup_on_cancel(self._cache.release(lease))
702
+ except Exception:
703
+ logger.exception("Error releasing duplicate MCP lease for '%s'", name)
704
+ raise
705
+
706
+ async def _finish_cleanup_on_cancel(self, awaitable: Awaitable[Any]) -> None:
707
+ """Let cleanup finish before propagating caller cancellation."""
708
+ cleanup = asyncio.ensure_future(awaitable)
709
+ try:
710
+ await asyncio.shield(cleanup)
711
+ except asyncio.CancelledError:
712
+ with contextlib.suppress(Exception):
713
+ await cleanup
714
+ raise
715
+
716
+ async def test_connection(self, config: MCPServerConfig) -> MCPTestReport:
717
+ """Attempt a one-shot connection test for an MCP server.
718
+
719
+ Establishes the server connection, snapshots what it advertised
720
+ (identity, capabilities, instructions, tool/prompt catalog) into an
721
+ :class:`MCPTestReport`, and immediately closes it. Bounded by
722
+ ``DEFAULT_TEST_TIMEOUT_SECONDS`` injected into the SDK's
723
+ ``request_timeout`` when none is configured — so the Test button
724
+ can never hang indefinitely.
725
+
726
+ Raises:
727
+ MCPConnectionError: If the server fails to connect, initialize,
728
+ or exceeds the test timeout. Carries any captured stdout
729
+ banner lines, bounded stderr tail, and child exit code so the
730
+ UI can show why a misbehaving server failed.
731
+ """
732
+ mcp_tool: Any | None = None
733
+ try:
734
+ try:
735
+ mcp_tool = self._create_tool_with_timeout_floor(config, DEFAULT_TEST_TIMEOUT_SECONDS)
736
+ await mcp_tool.__aenter__()
737
+ catalog = [tool for tool in mcp_tool.functions if isinstance(tool, FunctionTool)]
738
+ initial_tool_names: tuple[str, ...] | None = None
739
+ if config.use_progressive_disclosure:
740
+ exposure = _ProgressiveMCPExposure(config, mcp_tool, self._validate_server_namespace)
741
+ # Materializing the surface validates both catalog names
742
+ # and generated controls through the production path.
743
+ initial_tool_names = tuple(tool.name for tool in exposure.initial_tools)
744
+ else:
745
+ self._validate_server_namespace(config, catalog, set())
746
+ return self._build_test_report(mcp_tool, catalog, initial_tool_names)
747
+ except MCPConnectionError:
748
+ raise
749
+ except Exception as exc:
750
+ diagnostics = self._stdio_failure_diagnostics_from(mcp_tool)
751
+ cause: BaseException = exc
752
+ if self._is_timeout_cause(exc):
753
+ timeout = getattr(mcp_tool, "request_timeout", DEFAULT_TEST_TIMEOUT_SECONDS)
754
+ cause = TimeoutError(
755
+ f"connection did not complete within {timeout}s (server may not be speaking MCP)"
756
+ )
757
+ raise MCPConnectionError(
758
+ config.name,
759
+ config.transport,
760
+ cause,
761
+ banner_lines=self._banner_lines_from(mcp_tool),
762
+ stderr_tail=diagnostics.stderr_tail,
763
+ stderr_dropped_bytes=diagnostics.stderr_dropped_bytes,
764
+ process_exit_code=diagnostics.process_exit_code,
765
+ resolved_executable=diagnostics.resolved_executable,
766
+ effective_cwd=diagnostics.effective_cwd,
767
+ ) from exc
768
+ finally:
769
+ if mcp_tool is not None:
770
+ with contextlib.suppress(Exception):
771
+ await mcp_tool.__aexit__(None, None, None)
772
+
773
+ @staticmethod
774
+ def _build_test_report(
775
+ mcp_tool: Any,
776
+ catalog: list[FunctionTool],
777
+ initial_tool_names: tuple[str, ...] | None,
778
+ ) -> MCPTestReport:
779
+ """Snapshot server-advertised state before the test connection closes."""
780
+ server_info = getattr(mcp_tool, "_server_info", None)
781
+ prompt_remote_names = getattr(mcp_tool, "_loaded_prompt_remote_names", None) or set()
782
+ tools: list[tuple[str, str]] = []
783
+ prompts: list[tuple[str, str]] = []
784
+ for tool in catalog:
785
+ additional = tool.additional_properties or {}
786
+ remote_name = additional.get(_MCP_REMOTE_NAME_KEY)
787
+ entry = (tool.name, tool.description or "")
788
+ if isinstance(remote_name, str) and remote_name in prompt_remote_names:
789
+ prompts.append(entry)
790
+ else:
791
+ tools.append(entry)
792
+ return MCPTestReport(
793
+ server_name=getattr(server_info, "name", None),
794
+ server_title=getattr(server_info, "title", None),
795
+ server_version=getattr(server_info, "version", None),
796
+ protocol_version=getattr(mcp_tool, "_protocol_version", None),
797
+ capabilities=_flatten_server_capabilities(getattr(mcp_tool, "_server_capabilities", None)),
798
+ instructions=getattr(mcp_tool, "_server_instructions", None),
799
+ tools=tuple(tools),
800
+ prompts=tuple(prompts),
801
+ initial_tool_names=initial_tool_names,
802
+ )
803
+
804
+ async def connect_all(
805
+ self,
806
+ configs: list[MCPServerConfig],
807
+ *,
808
+ progress: Callable[[MCPServerConfig, str, int, int, int], Awaitable[None]] | None = None,
809
+ ) -> list[Any]:
810
+ """Connect to multiple enabled MCP servers and return all tools.
811
+
812
+ Per-server failures do not abort the sequence — they are recorded on
813
+ :attr:`failures` and published on the event bus as ``Warning`` events
814
+ so the TUI can display them to the user.
815
+
816
+ Connections run concurrently, so progress callbacks arrive in
817
+ completion order rather than profile declaration order. Callers
818
+ should treat ``current`` / ``failed`` / ``total`` as aggregate
819
+ counters and key any per-server UI by server name.
820
+
821
+ Any ordinary ``Exception`` is treated as a per-server failure and
822
+ dropped from the returned tool list. This includes the documented
823
+ ``MCPConnectionError`` path and also defensive handling of
824
+ unexpected errors (e.g. ``ValueError`` from a malformed config
825
+ caught by ``_validate_config``, owned MCP engine errors,
826
+ ``ExceptionGroup`` leaks from anyio task groups) — none of which
827
+ should be allowed to abort the agent build and leave the engine
828
+ half-initialised. ``MCPToolConfigurationError`` is the deliberate
829
+ exception: deterministic invalid/colliding tool names must abort the
830
+ build before a provider rejects the request or a last-wins tool map
831
+ selects the wrong function.
832
+ ``BaseException`` subclasses (``CancelledError``,
833
+ ``KeyboardInterrupt``, ``SystemExit``, and bare ``BaseExceptionGroup``)
834
+ propagate so structural cancellation / shutdown signals are never
835
+ swallowed.
836
+ """
837
+ enabled_configs: list[MCPServerConfig] = []
838
+ for config in configs:
839
+ if not config.enabled:
840
+ logger.debug("Skipping disabled MCP server '%s'", config.name)
841
+ continue
842
+ enabled_configs.append(config)
843
+ self._tool_names_by_server = {}
844
+
845
+ total = len(enabled_configs)
846
+ connected = 0
847
+ failed = 0
848
+
849
+ async def _connect_one(config: MCPServerConfig) -> list[Any]:
850
+ nonlocal connected, failed
851
+ if progress is not None:
852
+ await progress(config, "starting", connected, total, failed)
853
+ try:
854
+ tools = await self.connect(config)
855
+ except asyncio.CancelledError:
856
+ raise
857
+ except MCPToolConfigurationError:
858
+ raise
859
+ except MCPConnectionError as exc:
860
+ await self._publish_failure(exc)
861
+ failed += 1
862
+ if progress is not None:
863
+ await progress(config, "failed", connected, total, failed)
864
+ return []
865
+ except Exception as exc:
866
+ err = MCPConnectionError(config.name, config.transport, exc)
867
+ self._failures[config.name] = err
868
+ await self._publish_failure(err)
869
+ failed += 1
870
+ if progress is not None:
871
+ await progress(config, "failed", connected, total, failed)
872
+ return []
873
+ connected += 1
874
+ exposure = self._progressive_exposures.get(config.name)
875
+ self._tool_names_by_server[config.name] = (
876
+ exposure.catalog_tool_names
877
+ if exposure is not None
878
+ else [getattr(tool, "name", str(tool)) for tool in tools]
879
+ )
880
+ if progress is not None:
881
+ await progress(config, "connected", connected, total, failed)
882
+ return tools
883
+
884
+ results = await asyncio.gather(*(_connect_one(config) for config in enabled_configs))
885
+ self._tool_names_by_server = {
886
+ config.name: self._tool_names_by_server[config.name]
887
+ for config in enabled_configs
888
+ if config.name in self._tool_names_by_server
889
+ }
890
+ return [tool for tools in results for tool in tools]
891
+
892
+ async def disconnect(self, name: str) -> None:
893
+ """Disconnect from an MCP server.
894
+
895
+ The lock is held only for the pop so that a concurrent ``connect``
896
+ cannot stamp the server back into ``_servers`` after we snapshotted
897
+ its absence. ``__aexit__`` runs outside the lock.
898
+ """
899
+ async with self._lock:
900
+ task = self._connecting.pop(name, None)
901
+ lease = self._leases.pop(name, None)
902
+ server = self._servers.pop(name, None)
903
+ self._functions_by_server.pop(name, None)
904
+ self._instructions_exposure.pop(name, None)
905
+ self._progressive_exposures.pop(name, None)
906
+ self._tool_namespace_by_server.pop(name, None)
907
+ if task is not None:
908
+ task.cancel()
909
+ with contextlib.suppress(asyncio.CancelledError, Exception):
910
+ await task
911
+ if lease is None:
912
+ if server is not None:
913
+ try:
914
+ await server.__aexit__(None, None, None)
915
+ except Exception:
916
+ logger.exception("Error disconnecting MCP server '%s'", name)
917
+ return
918
+ try:
919
+ await self._cache.release(lease)
920
+ if self._owns_cache:
921
+ await self._cache.prune_idle(max_idle_seconds=0)
922
+ except Exception:
923
+ logger.exception("Error disconnecting MCP server '%s'", name)
924
+
925
+ async def disconnect_all(self) -> None:
926
+ """Disconnect from all connected MCP servers in parallel.
927
+
928
+ The snapshot+clear runs under ``_lock`` so connected servers are
929
+ disconnected and in-flight connects are cancelled before they can
930
+ register a server. The per-tool ``__aexit__`` calls run outside
931
+ the lock so they proceed concurrently and a slow shutdown does not
932
+ block new connects.
933
+ """
934
+ async with self._lock:
935
+ items = list(self._leases.items())
936
+ leased_server_names = set(self._leases)
937
+ fallback_items = [
938
+ (name, server) for name, server in self._servers.items() if name not in leased_server_names
939
+ ]
940
+ connecting = list(self._connecting.values())
941
+ self._leases.clear()
942
+ self._connecting.clear()
943
+ self._servers.clear()
944
+ self._functions_by_server.clear()
945
+ self._instructions_exposure.clear()
946
+ self._progressive_exposures.clear()
947
+ self._tool_namespace_by_server.clear()
948
+ for task in connecting:
949
+ task.cancel()
950
+ if connecting:
951
+ await self._finish_cleanup_on_cancel(asyncio.gather(*connecting, return_exceptions=True))
952
+
953
+ async def _release_one(name: str, lease: MCPConnectionLease) -> None:
954
+ try:
955
+ await self._cache.release(lease)
956
+ except Exception:
957
+ logger.exception("Error disconnecting MCP server '%s'", name)
958
+
959
+ if items:
960
+ await self._finish_cleanup_on_cancel(
961
+ asyncio.gather(*(_release_one(n, lease) for n, lease in items), return_exceptions=True)
962
+ )
963
+ if fallback_items:
964
+
965
+ async def _exit_one(name: str, tool: MCPTool) -> None:
966
+ try:
967
+ await tool.__aexit__(None, None, None)
968
+ except Exception:
969
+ logger.exception("Error disconnecting MCP server '%s'", name)
970
+
971
+ await self._finish_cleanup_on_cancel(
972
+ asyncio.gather(*(_exit_one(n, t) for n, t in fallback_items), return_exceptions=True)
973
+ )
974
+ if self._owns_cache:
975
+ try:
976
+ await self._finish_cleanup_on_cancel(self._cache.close_all())
977
+ finally:
978
+ self._cache = MCPConnectionCache()
979
+
980
+ async def _publish_warning(self, *, code: str, message: str) -> None:
981
+ """Log and best-effort publish one user-visible non-fatal warning."""
982
+ logger.warning("%s", message)
983
+ if self._bus is None:
984
+ return
985
+ from chrys.foundation.events.types import Warning as WarningEvent
986
+
987
+ with contextlib.suppress(Exception):
988
+ await self._bus.publish(
989
+ WarningEvent(
990
+ code=code,
991
+ message=message,
992
+ session_id=self._session_id,
993
+ )
994
+ )
995
+
996
+ async def _publish_failure(self, err: MCPConnectionError) -> None:
997
+ """Log and, if a bus is configured, publish a user-visible Warning."""
998
+ logger.error(
999
+ "MCP server '%s' (%s) failed to connect: %s",
1000
+ err.server_name,
1001
+ err.transport,
1002
+ err.cause,
1003
+ exc_info=err.cause,
1004
+ )
1005
+ if self._bus is None:
1006
+ return
1007
+ from chrys.foundation.events.types import Warning as WarningEvent
1008
+
1009
+ with contextlib.suppress(Exception):
1010
+ await self._bus.publish(
1011
+ WarningEvent(
1012
+ code="mcp.connect_failed",
1013
+ message=str(err),
1014
+ session_id=self._session_id,
1015
+ )
1016
+ )
1017
+
1018
+ @property
1019
+ def server_names(self) -> list[str]:
1020
+ """Names of currently connected MCP servers."""
1021
+ return list(dict.fromkeys([*self._leases.keys(), *self._servers.keys()]))
1022
+
1023
+ @property
1024
+ def tool_names_by_server(self) -> dict[str, list[str]]:
1025
+ """Snapshot of server-name -> loaded tool names from the last connect_all call."""
1026
+ return {name: list(tools) for name, tools in self._tool_names_by_server.items()}
1027
+
1028
+ @property
1029
+ def failures(self) -> dict[str, MCPConnectionError]:
1030
+ """Snapshot of server-name → last connect failure."""
1031
+ return dict(self._failures)