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,1330 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
3
+ # Contains code adapted from Microsoft Agent Framework (MIT License; see NOTICE).
4
+
5
+ """Chrys-owned MCP client tool engine.
6
+
7
+ This module owns the MCP surface Chrys consumes:
8
+ stdio and streamable-HTTP transports, connection lifecycle, tool/prompt
9
+ loading, tool/prompt calls, result parsing, and MCP client spans. WebSocket
10
+ and SEP-2663 long-running task driving are intentionally out of scope.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import contextlib
17
+ import json
18
+ import logging
19
+ import re
20
+ from abc import abstractmethod
21
+ from collections.abc import Awaitable, Callable, Collection, Mapping, Sequence
22
+ from contextlib import AsyncExitStack
23
+ from datetime import timedelta
24
+ from functools import partial
25
+ from typing import TYPE_CHECKING, Any, Self, cast
26
+
27
+ from opentelemetry import propagate
28
+ from opentelemetry import trace as otel_trace
29
+
30
+ from chrys.foundation.branding import APP_DISPLAY_NAME
31
+ from chrys.foundation.text.model_json import model_json
32
+ from chrys.foundation.tool_call_context import set_tool_context
33
+ from chrys.foundation.trajectory.context import current_tool_operation_id
34
+ from chrys.foundation.trajectory.event_types import WaitCategory
35
+ from chrys.kernel import Content, normalize_tools
36
+ from chrys.kernel._tool_expansion import _set_tool_expander
37
+ from chrys.kernel.exceptions import ModelVisibleToolError, ToolException, ToolExecutionException
38
+ from chrys.kernel.instrumentation import OtelAttr, create_mcp_client_span, set_mcp_span_error
39
+ from chrys.kernel.middleware import FunctionInvocationContext
40
+ from chrys.kernel.tools import FunctionTool
41
+ from chrys.service.mcp.content import INVALID_AUDIO_TEXT, INVALID_IMAGE_TEXT, INVALID_RESOURCE_TEXT, decode_media_base64
42
+ from chrys.service.mcp.result_limits import (
43
+ _MCP_PROMPT_TOOL_KEY,
44
+ DEFAULT_MCP_TOOL_RESULT_MAX_TOKENS,
45
+ sanitize_mcp_result_binary,
46
+ truncate_mcp_error,
47
+ )
48
+ from chrys.service.trajectory.waits import WaitOutcome, WaitTrace
49
+
50
+ if TYPE_CHECKING:
51
+ from contextlib import _AsyncGeneratorContextManager # type: ignore[attr-defined]
52
+
53
+ from httpx import AsyncClient
54
+ from mcp import types
55
+ from mcp.client.session import ClientSession
56
+ from mcp.shared.session import RequestResponder
57
+
58
+ from chrys.kernel.types import ToolTypes
59
+
60
+ logger = logging.getLogger(__name__)
61
+
62
+
63
+ _MCP_REMOTE_NAME_KEY = "_mcp_remote_name"
64
+ _MCP_NORMALIZED_NAME_KEY = "_mcp_normalized_name"
65
+ _MCP_GLOBAL_EXTRA_ARGS_KEY = "*"
66
+ # Runtime argument names retained for diagnostics and regression coverage.
67
+ # Never use this catalog to reject an argument declared by the remote tool:
68
+ # provenance, not spelling, is the security boundary.
69
+ _MCP_FRAMEWORK_DENYLIST: frozenset[str] = frozenset(
70
+ {
71
+ "chat_options",
72
+ "tools",
73
+ "tool_choice",
74
+ "session",
75
+ "thread",
76
+ "conversation_id",
77
+ "options",
78
+ "response_format",
79
+ "_meta",
80
+ }
81
+ )
82
+ # ``ErrorData.data`` of the JSON-RPC errors the HTTP transport makes up when its
83
+ # own request fails. Their message is local httpx detail — the configured URL,
84
+ # which can carry credentials, included — not an answer from the server.
85
+ LOCAL_HTTP_FAILURE_ERROR_DATA = "chrys.local_http_failure"
86
+
87
+ LOG_LEVEL_MAPPING: dict[types.LoggingLevel, int] = {
88
+ "debug": logging.DEBUG,
89
+ "info": logging.INFO,
90
+ "notice": logging.INFO,
91
+ "warning": logging.WARNING,
92
+ "error": logging.ERROR,
93
+ "critical": logging.CRITICAL,
94
+ "alert": logging.CRITICAL,
95
+ "emergency": logging.CRITICAL,
96
+ }
97
+
98
+
99
+ def _get_input_model_from_mcp_prompt(prompt: types.Prompt) -> dict[str, Any]:
100
+ if not prompt.arguments:
101
+ return {"type": "object", "properties": {}}
102
+
103
+ properties: dict[str, Any] = {}
104
+ required: list[str] = []
105
+ for prompt_argument in prompt.arguments:
106
+ properties[prompt_argument.name] = {
107
+ "type": "string",
108
+ "description": prompt_argument.description if hasattr(prompt_argument, "description") else "",
109
+ }
110
+ if prompt_argument.required:
111
+ required.append(prompt_argument.name)
112
+
113
+ schema: dict[str, Any] = {"type": "object", "properties": properties}
114
+ if required:
115
+ schema["required"] = required
116
+ return schema
117
+
118
+
119
+ def _normalize_mcp_name(name: str) -> str:
120
+ # Keep the exposed name portable across model providers. MCP servers may
121
+ # advertise a broader character set, but OpenAI-compatible function tools
122
+ # reject periods and other punctuation before the first model response.
123
+ return re.sub(r"[^A-Za-z0-9_-]", "-", name)
124
+
125
+
126
+ def _build_prefixed_mcp_name(normalized_name: str, tool_name_prefix: str | None) -> str:
127
+ if not tool_name_prefix:
128
+ return normalized_name
129
+ normalized_prefix = _normalize_mcp_name(tool_name_prefix).rstrip("_.-")
130
+ if not normalized_prefix:
131
+ return normalized_name
132
+ trimmed_name = normalized_name.lstrip("_.-")
133
+ return f"{normalized_prefix}_{trimmed_name}" if trimmed_name else normalized_prefix
134
+
135
+
136
+ def _normalize_additional_tool_argument_names(
137
+ additional_tool_argument_names: Sequence[str] | Mapping[str, str | Sequence[str]] | None,
138
+ ) -> tuple[set[str], dict[str, set[str]]]:
139
+ if additional_tool_argument_names is None:
140
+ return set(), {}
141
+ if isinstance(additional_tool_argument_names, str):
142
+ return {additional_tool_argument_names}, {}
143
+ if isinstance(additional_tool_argument_names, Mapping):
144
+ global_extras: set[str] = set()
145
+ per_tool_extras: dict[str, set[str]] = {}
146
+ for tool_name, names in additional_tool_argument_names.items():
147
+ # The input contract leaves only a string sequence after the string case.
148
+ names_set = {names} if isinstance(names, str) else set(cast(Sequence[str], names))
149
+ if tool_name == _MCP_GLOBAL_EXTRA_ARGS_KEY:
150
+ global_extras.update(names_set)
151
+ else:
152
+ # Mapping keys are declared strings; the literal comparison only refines their value.
153
+ per_tool_extras[cast(str, tool_name)] = names_set
154
+ return global_extras, per_tool_extras
155
+ return set(additional_tool_argument_names), {}
156
+
157
+
158
+ def _mcp_config_candidate_names(*, local_name: str, normalized_name: str, remote_name: str) -> tuple[str, ...]:
159
+ """Return safe configuration names for MCP allow/approval matching."""
160
+ names = [remote_name]
161
+ if normalized_name == remote_name and local_name != remote_name:
162
+ names.append(local_name)
163
+ return tuple(names)
164
+
165
+
166
+ def _mcp_config_names_for(function: FunctionTool) -> tuple[str, ...]:
167
+ """Return the ``allowed_tools``/``always_load`` names that select *function*; none without MCP name stamps."""
168
+ additional = function.additional_properties or {}
169
+ normalized_name = additional.get(_MCP_NORMALIZED_NAME_KEY)
170
+ remote_name = additional.get(_MCP_REMOTE_NAME_KEY)
171
+ if not isinstance(normalized_name, str) or not isinstance(remote_name, str):
172
+ return ()
173
+ return _mcp_config_candidate_names(
174
+ local_name=function.name,
175
+ normalized_name=normalized_name,
176
+ remote_name=remote_name,
177
+ )
178
+
179
+
180
+ def _inject_otel_into_mcp_meta(
181
+ meta: dict[str, Any] | None = None,
182
+ *,
183
+ overwrite: bool = False,
184
+ ) -> dict[str, Any] | None:
185
+ carrier: dict[str, str] = {}
186
+ propagate.inject(carrier)
187
+ if not carrier:
188
+ return meta
189
+
190
+ if meta is None:
191
+ meta = {}
192
+ for key, value in carrier.items():
193
+ if overwrite or key not in meta:
194
+ meta[key] = value
195
+ return meta
196
+
197
+
198
+ def _server_error(message: str, subject: str, *, inner_exception: Exception | None = None) -> ModelVisibleToolError:
199
+ """Return the error the model reads for the server's answer to a call: *message*, already bounded.
200
+
201
+ A blank message (or one that is only ``Error:``) says so, naming *subject*
202
+ (``MCP tool 'x'``), rather than leave the model the generic fixed line.
203
+ """
204
+ error = ModelVisibleToolError(message, inner_exception=inner_exception)
205
+ if error.result_text is None:
206
+ error = ModelVisibleToolError(
207
+ f"{subject} reported an error without an error message.", inner_exception=inner_exception
208
+ )
209
+ return error
210
+
211
+
212
+ def _mcp_error_exception(error: Exception, message: str, subject: str) -> ToolExecutionException:
213
+ """Choose what to raise for a request that ended in *error*, whose bounded text is *message*.
214
+
215
+ A JSON-RPC error the server sent back (``-32602 Invalid params``, say) is
216
+ its answer to this call, and the MCP SDK's own errors (timeout, connection
217
+ closed) are fixed text, so the model reads either one. The HTTP
218
+ transport's made-up errors carry local detail and reach only the user.
219
+ """
220
+ from mcp.shared.exceptions import McpError
221
+
222
+ if isinstance(error, McpError) and error.error.data != LOCAL_HTTP_FAILURE_ERROR_DATA:
223
+ return _server_error(message, subject, inner_exception=error)
224
+ return ToolExecutionException(message, inner_exception=error)
225
+
226
+
227
+ def _should_propagate_cancelled_error(ex: BaseException) -> bool:
228
+ if not isinstance(ex, asyncio.CancelledError):
229
+ return False
230
+ task = asyncio.current_task()
231
+ return task is not None and task.cancelling() > 0
232
+
233
+
234
+ def _describe_error(ex: BaseException) -> str:
235
+ """Unwrap task-group and cancellation wrappers to name the actual failure."""
236
+ current = ex
237
+ seen: set[int] = set()
238
+ for _ in range(20):
239
+ seen.add(id(current))
240
+ if isinstance(current, BaseExceptionGroup) and len(current.exceptions) == 1:
241
+ cause = current.exceptions[0]
242
+ else:
243
+ cause = current.__cause__ or current.__context__
244
+ if cause is None or id(cause) in seen:
245
+ break
246
+ if isinstance(current, asyncio.CancelledError) and isinstance(cause, asyncio.CancelledError):
247
+ break
248
+ current = cause
249
+ return str(current) or repr(current)
250
+
251
+
252
+ def _describe_with_cleanup(ex: BaseException, cleanup_error: BaseException | None) -> str:
253
+ """A cancelled initialize may reveal its real error only during teardown."""
254
+ if isinstance(ex, asyncio.CancelledError) and cleanup_error is not None:
255
+ return _describe_error(cleanup_error)
256
+ return _describe_error(ex)
257
+
258
+
259
+ def _media_content(item: types.ImageContent | types.AudioContent) -> Content:
260
+ """An image or audio item as data; a placeholder text when its data is not non-empty base64."""
261
+ if data := decode_media_base64(item.data):
262
+ return Content.from_data(data=data, media_type=item.mimeType)
263
+ return Content.from_text(INVALID_IMAGE_TEXT if item.type == "image" else INVALID_AUDIO_TEXT)
264
+
265
+
266
+ def _blob_content(resource: types.BlobResourceContents) -> Content:
267
+ """A blob resource as data, empty allowed; a placeholder text when its blob is not base64."""
268
+ if (data := decode_media_base64(resource.blob)) is None:
269
+ return Content.from_text(INVALID_RESOURCE_TEXT)
270
+ return Content.from_data(data=data, media_type=resource.mimeType or "application/octet-stream")
271
+
272
+
273
+ def _resource_link_text(item: types.ResourceLink) -> Content:
274
+ """A link as text the model reads, never as the linked item itself.
275
+
276
+ Model APIs would fetch a linked item on every request, and one they can't
277
+ reach (a local file, a localhost or expired URL) would fail every later
278
+ request of the session.
279
+ """
280
+ lines = [f"Resource link: {item.name}", f"URI: {item.uri}"]
281
+ if item.mimeType:
282
+ lines.append(f"MIME type: {item.mimeType}")
283
+ if item.description:
284
+ lines.append(f"Description: {item.description}")
285
+ return Content.from_text("\n".join(lines))
286
+
287
+
288
+ class MCPTool:
289
+ """Base class for Chrys-owned MCP stdio and streamable-HTTP tools."""
290
+
291
+ def __init__(
292
+ self,
293
+ name: str,
294
+ description: str | None = None,
295
+ allowed_tools: Collection[str] | None = None,
296
+ tool_name_prefix: str | None = None,
297
+ load_tools: bool = True,
298
+ parse_tool_results: Callable[[types.CallToolResult], str | list[Content]] | None = None,
299
+ load_prompts: bool = True,
300
+ parse_prompt_results: Callable[[types.GetPromptResult], str] | None = None,
301
+ session: ClientSession | None = None,
302
+ request_timeout: int | None = None,
303
+ additional_properties: dict[str, Any] | None = None,
304
+ additional_tool_argument_names: Sequence[str] | Mapping[str, str | Sequence[str]] | None = None,
305
+ ) -> None:
306
+ self.name = name
307
+ self.description = description or ""
308
+ self.allowed_tools = allowed_tools
309
+ self.tool_name_prefix = _normalize_mcp_name(tool_name_prefix).rstrip("_.-") if tool_name_prefix else None
310
+ self.additional_properties = additional_properties
311
+ self.load_tools_flag = load_tools
312
+ self.parse_tool_results = parse_tool_results
313
+ self.load_prompts_flag = load_prompts
314
+ self.parse_prompt_results = parse_prompt_results
315
+ self._exit_stack = AsyncExitStack()
316
+ self._lifecycle_lock = asyncio.Lock()
317
+ self._lifecycle_request_lock = asyncio.Lock()
318
+ self._function_load_lock = asyncio.Lock()
319
+ self._lifecycle_queue: asyncio.Queue[tuple[str, bool, bool, asyncio.Future[None]]] | None = None
320
+ self._lifecycle_owner_task: asyncio.Task[None] | None = None
321
+ self.session = session
322
+ self.request_timeout = request_timeout
323
+ self._functions: list[FunctionTool] = []
324
+ # Reloads must skip an already wrapped remote declaration without
325
+ # collapsing *different* remote names that normalize to the same local
326
+ # name. The adapter validates those duplicates before exposure.
327
+ self._loaded_tool_remote_names: set[str] = set()
328
+ self._loaded_prompt_remote_names: set[str] = set()
329
+ self._tool_call_meta_by_name: dict[str, dict[str, Any]] = {}
330
+ self._tool_task_support_by_name: dict[str, str] = {}
331
+ self._tool_param_names_by_name: dict[str, set[str]] = {}
332
+ self._global_extra_arg_names, self._tool_extra_arg_names = _normalize_additional_tool_argument_names(
333
+ additional_tool_argument_names
334
+ )
335
+ self.is_connected = False
336
+ self._tools_loaded = False
337
+ self._prompts_loaded = False
338
+ self._server_capabilities: types.ServerCapabilities | None = None
339
+ self._server_instructions: str | None = None
340
+ self._server_info: types.Implementation | None = None
341
+ self._protocol_version: str | None = None
342
+ self._supports_tools = True
343
+ self._supports_prompts = True
344
+ self._supports_logging: bool | None = None
345
+ self._ping_available = True
346
+ self._pending_reload_tasks: set[asyncio.Task[None]] = set()
347
+
348
+ def __str__(self) -> str:
349
+ return f"MCPTool(name={self.name}, description={self.description})"
350
+
351
+ def _mcp_base_span_attributes(self) -> dict[str, Any]:
352
+ return {}
353
+
354
+ @property
355
+ def functions(self) -> list[FunctionTool]:
356
+ """Return exposed functions; ``None`` allows all tools, while ``[]`` allows none."""
357
+ if self.allowed_tools is None:
358
+ return self._functions
359
+ allowed_names = set(self.allowed_tools)
360
+ return [func for func in self._functions if not allowed_names.isdisjoint(_mcp_config_names_for(func))]
361
+
362
+ async def _safe_close_exit_stack(self) -> BaseException | None:
363
+ try:
364
+ await self._exit_stack.aclose()
365
+ except RuntimeError as err:
366
+ if "cancel scope" in str(err).lower():
367
+ logger.warning("Could not cleanly close MCP exit stack due to cancel scope error: %s", err)
368
+ return err
369
+ raise
370
+ except asyncio.CancelledError as err:
371
+ logger.warning("Could not cleanly close MCP exit stack because the task was cancelled.")
372
+ return err
373
+ except Exception as err:
374
+ if type(err).__name__ == "ExceptionGroup":
375
+ logger.warning("Could not cleanly close MCP exit stack due to cleanup error group: %s", err)
376
+ return err
377
+ raise
378
+ return None
379
+
380
+ async def _close_and_check_cancelled(self, ex: BaseException) -> tuple[bool, BaseException | None]:
381
+ cleanup_error = await self._safe_close_exit_stack()
382
+ return _should_propagate_cancelled_error(ex), cleanup_error
383
+
384
+ def _reset_session_state(self) -> None:
385
+ self._server_capabilities = None
386
+ self._server_instructions = None
387
+ self._server_info = None
388
+ self._protocol_version = None
389
+ self._supports_tools = True
390
+ self._supports_prompts = True
391
+ self._supports_logging = None
392
+ self._ping_available = True
393
+
394
+ def _set_server_capabilities(self, capabilities: types.ServerCapabilities | None) -> None:
395
+ self._server_capabilities = capabilities
396
+ if capabilities is None:
397
+ self._supports_tools = False
398
+ self._supports_prompts = False
399
+ self._supports_logging = False
400
+ return
401
+
402
+ self._supports_tools = getattr(capabilities, "tools", None) is not None
403
+ self._supports_prompts = getattr(capabilities, "prompts", None) is not None
404
+ self._supports_logging = getattr(capabilities, "logging", None) is not None
405
+
406
+ def _set_server_instructions(self, instructions: object) -> None:
407
+ self._server_instructions = (
408
+ instructions.strip() if isinstance(instructions, str) and instructions.strip() else None
409
+ )
410
+
411
+ def _set_server_identity(self, initialize_result: object) -> None:
412
+ # Duck-typed: ``mcp.types`` is a deferred import, so don't isinstance
413
+ # against Implementation here — a serverInfo with a name is enough.
414
+ server_info = getattr(initialize_result, "serverInfo", None)
415
+ self._server_info = server_info if getattr(server_info, "name", None) else None
416
+ protocol_version = getattr(initialize_result, "protocolVersion", None)
417
+ self._protocol_version = str(protocol_version) if protocol_version is not None else None
418
+
419
+ async def _ensure_lifecycle_owner(self) -> None:
420
+ async with self._lifecycle_lock:
421
+ if self._lifecycle_owner_task is not None and not self._lifecycle_owner_task.done():
422
+ return
423
+
424
+ self._lifecycle_queue = asyncio.Queue()
425
+ self._lifecycle_owner_task = asyncio.create_task(
426
+ self._run_lifecycle_owner(),
427
+ name=f"mcp-lifecycle:{self.name}",
428
+ )
429
+
430
+ async def _run_lifecycle_owner(self) -> None:
431
+ queue = self._lifecycle_queue
432
+ if queue is None:
433
+ return
434
+
435
+ stop_error: BaseException | None = None
436
+ try:
437
+ while True:
438
+ action, reset, load_configured, future = await queue.get()
439
+ try:
440
+ if action == "connect":
441
+ await self._connect_on_owner(reset=reset, load_configured=load_configured)
442
+ elif action == "close":
443
+ await self._close_on_owner()
444
+ else:
445
+ raise RuntimeError(f"Unknown MCP lifecycle action: {action}")
446
+ except asyncio.CancelledError as ex:
447
+ stop_error = ex
448
+ if not future.done():
449
+ future.set_exception(ex)
450
+ raise
451
+ except Exception as ex:
452
+ if not future.done():
453
+ future.set_exception(ex)
454
+ else:
455
+ if not future.done():
456
+ future.set_result(None)
457
+
458
+ if action == "close":
459
+ return
460
+ except asyncio.CancelledError as ex:
461
+ stop_error = ex
462
+ raise
463
+ finally:
464
+ while True:
465
+ try:
466
+ _, _, _, future = queue.get_nowait()
467
+ except asyncio.QueueEmpty:
468
+ break
469
+ if not future.done():
470
+ future.set_exception(stop_error or RuntimeError("MCP lifecycle owner stopped unexpectedly."))
471
+ self._lifecycle_queue = None
472
+ self._lifecycle_owner_task = None
473
+
474
+ def _is_lifecycle_owner_task(self) -> bool:
475
+ owner_task = self._lifecycle_owner_task
476
+ return owner_task is not None and asyncio.current_task() is owner_task
477
+
478
+ async def _run_on_lifecycle_owner(
479
+ self,
480
+ action: str,
481
+ *,
482
+ reset: bool = False,
483
+ load_configured: bool = True,
484
+ ) -> None:
485
+ await self._ensure_lifecycle_owner()
486
+
487
+ if self._is_lifecycle_owner_task():
488
+ if action == "connect":
489
+ await self._connect_on_owner(reset=reset, load_configured=load_configured)
490
+ elif action == "close":
491
+ await self._close_on_owner()
492
+ else:
493
+ raise RuntimeError(f"Unknown MCP lifecycle action: {action}")
494
+ return
495
+
496
+ queue = self._lifecycle_queue
497
+ if queue is None:
498
+ raise RuntimeError("MCP lifecycle owner is not available.")
499
+
500
+ future = asyncio.get_running_loop().create_future()
501
+ await queue.put((action, reset, load_configured, future))
502
+ await future
503
+
504
+ async def connect(self, *, reset: bool = False) -> None:
505
+ # A connect inside a model run (lazy first use, reconnect after a
506
+ # dropped session) is a wait the turn timeline must account for.
507
+ wait = WaitTrace.open(
508
+ WaitCategory.MCP_CONNECT,
509
+ target_operation_id=current_tool_operation_id(),
510
+ server_name=self.name,
511
+ )
512
+ try:
513
+ # Inside the block that closes the wait: the start marker awaits
514
+ # its write ack, and an interrupt landing there would otherwise
515
+ # leave the wait open forever.
516
+ if wait is not None:
517
+ await wait.started()
518
+ await self._connect_unrecorded(reset=reset)
519
+ except asyncio.CancelledError:
520
+ if wait is not None:
521
+ wait.finished_soon(outcome=WaitOutcome.CANCELLED)
522
+ raise
523
+ except BaseException:
524
+ if wait is not None:
525
+ await wait.finished(outcome=WaitOutcome.FAILED)
526
+ raise
527
+ if wait is not None:
528
+ await wait.finished()
529
+
530
+ async def _connect_unrecorded(self, *, reset: bool) -> None:
531
+ if self._is_lifecycle_owner_task():
532
+ await self._connect_on_owner(reset=reset)
533
+ return
534
+
535
+ async with self._lifecycle_request_lock:
536
+ await self._run_on_lifecycle_owner("connect", reset=reset)
537
+
538
+ async def _connect_on_owner(self, *, reset: bool = False, load_configured: bool = True) -> None:
539
+ """Connect, initialize, and load configured MCP functions."""
540
+ if reset:
541
+ await self._safe_close_exit_stack()
542
+ self.session = None
543
+ self.is_connected = False
544
+ self._reset_session_state()
545
+ self._exit_stack = AsyncExitStack()
546
+
547
+ if not self.session:
548
+ try:
549
+ transport = await self._exit_stack.enter_async_context(self.get_mcp_client())
550
+ except (Exception, asyncio.CancelledError) as ex:
551
+ cancelled, cleanup_error = await self._close_and_check_cancelled(ex)
552
+ if cancelled:
553
+ raise
554
+ command = getattr(self, "command", None)
555
+ if command:
556
+ message = f"Failed to start MCP server '{command}': {_describe_with_cleanup(ex, cleanup_error)}"
557
+ else:
558
+ message = f"Failed to connect to MCP server: {_describe_with_cleanup(ex, cleanup_error)}"
559
+ raise ToolException(message, inner_exception=ex if isinstance(ex, Exception) else None) from ex
560
+
561
+ try:
562
+ from mcp.client.session import ClientSession
563
+ except ModuleNotFoundError as ex:
564
+ await self._safe_close_exit_stack()
565
+ raise ToolException("MCP support requires `mcp`. Please install `mcp`.", inner_exception=ex) from ex
566
+
567
+ try:
568
+ session = await self._exit_stack.enter_async_context(
569
+ ClientSession(
570
+ read_stream=transport[0],
571
+ write_stream=transport[1],
572
+ read_timeout_seconds=timedelta(seconds=self.request_timeout) if self.request_timeout else None,
573
+ message_handler=self.message_handler,
574
+ logging_callback=self.logging_callback,
575
+ )
576
+ )
577
+ except (Exception, asyncio.CancelledError) as ex:
578
+ cancelled, cleanup_error = await self._close_and_check_cancelled(ex)
579
+ if cancelled:
580
+ raise
581
+ message = f"Failed to create MCP session: {_describe_with_cleanup(ex, cleanup_error)}"
582
+ raise ToolException(message, inner_exception=ex if isinstance(ex, Exception) else None) from ex
583
+
584
+ try:
585
+ with create_mcp_client_span("initialize", attributes=self._mcp_base_span_attributes()) as init_span:
586
+ initialize_result = await session.initialize()
587
+ init_span.set_attribute(OtelAttr.MCP_PROTOCOL_VERSION, initialize_result.protocolVersion)
588
+ self._set_server_capabilities(getattr(initialize_result, "capabilities", None))
589
+ self._set_server_instructions(getattr(initialize_result, "instructions", None))
590
+ self._set_server_identity(initialize_result)
591
+ except (Exception, asyncio.CancelledError) as ex:
592
+ cancelled, cleanup_error = await self._close_and_check_cancelled(ex)
593
+ if cancelled:
594
+ raise
595
+ command = getattr(self, "command", None)
596
+ if command:
597
+ args_str = " ".join(getattr(self, "args", []))
598
+ full_command = f"{command} {args_str}".strip()
599
+ message = (
600
+ f"MCP server '{full_command}' failed to initialize: {_describe_with_cleanup(ex, cleanup_error)}"
601
+ )
602
+ else:
603
+ message = f"MCP server failed to initialize: {_describe_with_cleanup(ex, cleanup_error)}"
604
+ raise ToolException(message, inner_exception=ex if isinstance(ex, Exception) else None) from ex
605
+ self.session = session
606
+
607
+ elif getattr(self.session, "_request_id", 0) == 0:
608
+ with create_mcp_client_span("initialize", attributes=self._mcp_base_span_attributes()) as init_span:
609
+ initialize_result = await self.session.initialize()
610
+ init_span.set_attribute(OtelAttr.MCP_PROTOCOL_VERSION, initialize_result.protocolVersion)
611
+ self._set_server_capabilities(getattr(initialize_result, "capabilities", None))
612
+ self._set_server_instructions(getattr(initialize_result, "instructions", None))
613
+ self._set_server_identity(initialize_result)
614
+ elif self._server_capabilities is None:
615
+ self._set_server_capabilities(getattr(self.session, "_server_capabilities", None))
616
+
617
+ self.is_connected = True
618
+ if load_configured and self.load_tools_flag:
619
+ if self._supports_tools:
620
+ await self.load_tools()
621
+ self._tools_loaded = True
622
+ if load_configured and self.load_prompts_flag:
623
+ if self._supports_prompts:
624
+ await self.load_prompts()
625
+ self._prompts_loaded = True
626
+
627
+ if logger.level != logging.NOTSET and self._supports_logging is not False:
628
+ with contextlib.suppress(Exception):
629
+ level_name = next(level for level, value in LOG_LEVEL_MAPPING.items() if value == logger.level)
630
+ await self.session.set_logging_level(level_name)
631
+
632
+ async def _reconnect_without_loading(self) -> None:
633
+ if self._is_lifecycle_owner_task():
634
+ await self._connect_on_owner(reset=True, load_configured=False)
635
+ return
636
+
637
+ await self._run_on_lifecycle_owner("connect", reset=True, load_configured=False)
638
+
639
+ async def _close_on_owner(self) -> None:
640
+ tasks = list(self._pending_reload_tasks)
641
+ for task in tasks:
642
+ task.cancel()
643
+ self._pending_reload_tasks.clear()
644
+ if tasks:
645
+ await asyncio.gather(*tasks, return_exceptions=True)
646
+
647
+ await self._safe_close_exit_stack()
648
+ self._exit_stack = AsyncExitStack()
649
+ self.session = None
650
+ self.is_connected = False
651
+ self._reset_session_state()
652
+
653
+ async def close(self) -> None:
654
+ if self._is_lifecycle_owner_task():
655
+ await self._close_on_owner()
656
+ return
657
+
658
+ async with self._lifecycle_request_lock:
659
+ await self._run_on_lifecycle_owner("close")
660
+
661
+ @abstractmethod
662
+ def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
663
+ """Return the MCP SDK transport context manager."""
664
+
665
+ async def _ensure_connected(self) -> None:
666
+ from mcp.shared.exceptions import McpError
667
+
668
+ if not self._ping_available:
669
+ return
670
+ # Connected operations run only after the lifecycle installs a session.
671
+ session = cast("ClientSession", self.session)
672
+ try:
673
+ await session.send_ping()
674
+ except McpError as mcp_exc:
675
+ if mcp_exc.error.code == -32601:
676
+ self._ping_available = False
677
+ logger.debug("Skipping future MCP pings because the server does not support ping.")
678
+ return
679
+ await self._reconnect_or_raise(mcp_exc)
680
+ except Exception as ex:
681
+ await self._reconnect_or_raise(ex)
682
+
683
+ async def _reconnect_or_raise(self, ex: BaseException) -> None:
684
+ logger.info("MCP connection invalid or closed. Reconnecting...")
685
+ try:
686
+ await self._reconnect_without_loading()
687
+ except Exception as reconn_ex:
688
+ raise ToolExecutionException("Failed to establish MCP connection.", inner_exception=reconn_ex) from ex
689
+
690
+ async def logging_callback(self, params: types.LoggingMessageNotificationParams) -> None:
691
+ logger.log(LOG_LEVEL_MAPPING[params.level], params.data)
692
+
693
+ async def message_handler(
694
+ self,
695
+ message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
696
+ ) -> None:
697
+ from mcp import types
698
+
699
+ if isinstance(message, Exception):
700
+ logger.error("Error from MCP server: %s", message, exc_info=message)
701
+ return
702
+ if isinstance(message, types.ServerNotification):
703
+ match message.root.method:
704
+ case "notifications/tools/list_changed":
705
+ self._schedule_reload(self.load_tools())
706
+ case "notifications/prompts/list_changed":
707
+ self._schedule_reload(self.load_prompts())
708
+ case _:
709
+ logger.debug("Unhandled notification: %s", message.root.method)
710
+
711
+ def _schedule_reload(self, coro: Any) -> None:
712
+ reload_name = f"mcp-reload:{self.name}:{coro.__qualname__}"
713
+ for existing in list(self._pending_reload_tasks):
714
+ if existing.get_name() == reload_name and not existing.done():
715
+ existing.cancel()
716
+
717
+ async def _safe_reload() -> None:
718
+ try:
719
+ await coro
720
+ except asyncio.CancelledError:
721
+ raise
722
+ except Exception:
723
+ logger.warning("Background MCP reload failed", exc_info=True)
724
+
725
+ task = asyncio.create_task(_safe_reload(), name=reload_name)
726
+ self._pending_reload_tasks.add(task)
727
+ task.add_done_callback(self._pending_reload_tasks.discard)
728
+
729
+ async def load_prompts(self) -> None:
730
+ async with self._function_load_lock:
731
+ await self._load_prompts_locked()
732
+
733
+ async def _load_prompts_locked(self) -> None:
734
+ from anyio import ClosedResourceError
735
+ from mcp import types
736
+
737
+ if not self._supports_prompts:
738
+ return
739
+
740
+ params: types.PaginatedRequestParams | None = None
741
+ while True:
742
+ prompt_list: types.ListPromptsResult | None = None
743
+ for attempt in range(2):
744
+ try:
745
+ await self._ensure_connected()
746
+ # Prompt loading is entered only for a connected lifecycle session.
747
+ session = cast("ClientSession", self.session)
748
+ if not self._supports_prompts:
749
+ return
750
+ with create_mcp_client_span("prompts/list", attributes=self._mcp_base_span_attributes()):
751
+ prompt_list = await session.list_prompts(params=params)
752
+ break
753
+ except ClosedResourceError as cl_ex:
754
+ if attempt == 0:
755
+ try:
756
+ await self._reconnect_without_loading()
757
+ except Exception as reconn_ex:
758
+ raise ToolExecutionException(
759
+ "Failed to reconnect to MCP server.",
760
+ inner_exception=reconn_ex,
761
+ ) from reconn_ex
762
+ continue
763
+ raise ToolExecutionException(
764
+ "Failed to load prompts - connection lost.", inner_exception=cl_ex
765
+ ) from cl_ex
766
+ if prompt_list is None:
767
+ raise ToolExecutionException("Failed to load prompts.")
768
+
769
+ for prompt in prompt_list.prompts:
770
+ normalized_name = _normalize_mcp_name(prompt.name)
771
+ local_name = _build_prefixed_mcp_name(normalized_name, self.tool_name_prefix)
772
+ if prompt.name in self._loaded_prompt_remote_names:
773
+ continue
774
+ func = FunctionTool(
775
+ func=partial(self.get_prompt, prompt.name),
776
+ name=local_name,
777
+ description=prompt.description or "",
778
+ input_model=_get_input_model_from_mcp_prompt(prompt),
779
+ additional_properties={
780
+ _MCP_REMOTE_NAME_KEY: prompt.name,
781
+ _MCP_NORMALIZED_NAME_KEY: normalized_name,
782
+ _MCP_PROMPT_TOOL_KEY: True,
783
+ },
784
+ )
785
+ # Static provenance persisted on the function_call content;
786
+ # identity only — never URLs, headers, env, or command lines.
787
+ set_tool_context(
788
+ func,
789
+ {
790
+ "server_name": self.name,
791
+ "remote_name": prompt.name,
792
+ "normalized_name": normalized_name,
793
+ },
794
+ )
795
+ self._functions.append(func)
796
+ self._loaded_prompt_remote_names.add(prompt.name)
797
+
798
+ if not prompt_list.nextCursor:
799
+ break
800
+ params = types.PaginatedRequestParams(cursor=prompt_list.nextCursor)
801
+
802
+ async def load_tools(self) -> None:
803
+ async with self._function_load_lock:
804
+ await self._load_tools_locked()
805
+
806
+ def _make_remote_tool_call(self, remote_tool_name: str) -> Callable[..., Awaitable[str | list[Content]]]:
807
+ """Bind one trusted remote name outside the model-callable signature."""
808
+
809
+ async def _call_tool_with_runtime_kwargs(
810
+ ctx: FunctionInvocationContext,
811
+ **kwargs: Any,
812
+ ) -> str | list[Content]:
813
+ trusted_meta = ctx.kwargs.get("_meta")
814
+ extras = self._resolved_extra_args(remote_tool_name)
815
+ runtime_arguments = {key: value for key, value in ctx.kwargs.items() if key != "_meta" and key in extras}
816
+ tool_arguments = dict(ctx.arguments)
817
+
818
+ # ``_meta`` is an MCP request-metadata channel, not a tool
819
+ # argument. Model arguments are untrusted even when the
820
+ # remote schema accepts additional properties; only the
821
+ # runtime context may populate request metadata.
822
+ tool_arguments.pop("_meta", None)
823
+
824
+ # Explicit runtime extras are trusted injections and therefore
825
+ # win over a same-named value supplied by the model.
826
+ call_kwargs = {**tool_arguments, **runtime_arguments}
827
+ if trusted_meta is not None:
828
+ call_kwargs["_meta"] = trusted_meta
829
+ return await self.call_tool(remote_tool_name, **call_kwargs)
830
+
831
+ return _call_tool_with_runtime_kwargs
832
+
833
+ async def _load_tools_locked(self) -> None:
834
+ from anyio import ClosedResourceError
835
+ from mcp import types
836
+
837
+ if not self._supports_tools:
838
+ return
839
+
840
+ tool_call_meta_by_name: dict[str, dict[str, Any]] = {}
841
+ tool_task_support_by_name: dict[str, str] = {}
842
+ tool_param_names_by_name: dict[str, set[str]] = {}
843
+
844
+ params: types.PaginatedRequestParams | None = None
845
+ while True:
846
+ tool_list: types.ListToolsResult | None = None
847
+ for attempt in range(2):
848
+ try:
849
+ await self._ensure_connected()
850
+ # Tool loading is entered only for a connected lifecycle session.
851
+ session = cast("ClientSession", self.session)
852
+ if not self._supports_tools:
853
+ return
854
+ with create_mcp_client_span("tools/list", attributes=self._mcp_base_span_attributes()):
855
+ tool_list = await session.list_tools(params=params)
856
+ break
857
+ except ClosedResourceError as cl_ex:
858
+ if attempt == 0:
859
+ try:
860
+ await self._reconnect_without_loading()
861
+ except Exception as reconn_ex:
862
+ raise ToolExecutionException(
863
+ "Failed to reconnect to MCP server.",
864
+ inner_exception=reconn_ex,
865
+ ) from reconn_ex
866
+ continue
867
+ raise ToolExecutionException(
868
+ "Failed to load tools - connection lost.", inner_exception=cl_ex
869
+ ) from cl_ex
870
+ if tool_list is None:
871
+ raise ToolExecutionException("Failed to load tools.")
872
+
873
+ for tool in tool_list.tools:
874
+ if tool.meta is not None:
875
+ tool_call_meta_by_name[tool.name] = dict(tool.meta)
876
+ task_support = getattr(getattr(tool, "execution", None), "taskSupport", None)
877
+ if task_support is not None:
878
+ tool_task_support_by_name[tool.name] = task_support
879
+
880
+ input_schema = dict(tool.inputSchema or {})
881
+ if input_schema.get("type") == "object" and "properties" not in input_schema:
882
+ input_schema["properties"] = {}
883
+ properties = input_schema.get("properties")
884
+ tool_param_names_by_name[tool.name] = (
885
+ set(cast(dict[str, Any], properties)) if isinstance(properties, dict) else set()
886
+ )
887
+
888
+ normalized_name = _normalize_mcp_name(tool.name)
889
+ local_name = _build_prefixed_mcp_name(normalized_name, self.tool_name_prefix)
890
+ if tool.name in self._loaded_tool_remote_names:
891
+ continue
892
+
893
+ func = FunctionTool(
894
+ func=self._make_remote_tool_call(tool.name),
895
+ name=local_name,
896
+ description=tool.description or "",
897
+ input_model=input_schema,
898
+ additional_properties={
899
+ _MCP_REMOTE_NAME_KEY: tool.name,
900
+ _MCP_NORMALIZED_NAME_KEY: normalized_name,
901
+ },
902
+ )
903
+ # Static provenance persisted on the function_call content;
904
+ # identity only — never URLs, headers, env, or command lines.
905
+ set_tool_context(
906
+ func,
907
+ {
908
+ "server_name": self.name,
909
+ "remote_name": tool.name,
910
+ "normalized_name": normalized_name,
911
+ },
912
+ )
913
+ self._functions.append(func)
914
+ self._loaded_tool_remote_names.add(tool.name)
915
+
916
+ if not tool_list.nextCursor:
917
+ break
918
+ params = types.PaginatedRequestParams(cursor=tool_list.nextCursor)
919
+
920
+ self._tool_call_meta_by_name = tool_call_meta_by_name
921
+ self._tool_task_support_by_name = tool_task_support_by_name
922
+ self._tool_param_names_by_name = tool_param_names_by_name
923
+
924
+ def _parse_prompt_result_from_mcp(self, mcp_type: types.GetPromptResult) -> str:
925
+ from mcp import types
926
+
927
+ parts: list[str] = []
928
+ for message in mcp_type.messages:
929
+ content = message.content
930
+ if isinstance(content, types.TextContent):
931
+ parts.append(content.text)
932
+ elif isinstance(content, (types.ImageContent, types.AudioContent)):
933
+ parts.append(
934
+ json.dumps(
935
+ {
936
+ "type": "image" if isinstance(content, types.ImageContent) else "audio",
937
+ "data": content.data,
938
+ "mimeType": content.mimeType,
939
+ },
940
+ default=str,
941
+ )
942
+ )
943
+ elif isinstance(content, types.EmbeddedResource):
944
+ match content.resource:
945
+ case types.TextResourceContents():
946
+ parts.append(content.resource.text)
947
+ case types.BlobResourceContents():
948
+ parts.append(
949
+ json.dumps(
950
+ {"type": "blob", "data": content.resource.blob, "mimeType": content.resource.mimeType},
951
+ default=str,
952
+ )
953
+ )
954
+ else:
955
+ parts.append(str(content))
956
+ if not parts:
957
+ return ""
958
+ if len(parts) == 1:
959
+ return parts[0]
960
+ return model_json(parts, default=str)
961
+
962
+ def _parse_tool_result_from_mcp(self, mcp_type: types.CallToolResult) -> list[Content]:
963
+ from mcp import types
964
+
965
+ result: list[Content] = []
966
+ for item in mcp_type.content:
967
+ match item:
968
+ case types.TextContent():
969
+ result.append(Content.from_text(item.text))
970
+ case types.ImageContent() | types.AudioContent():
971
+ result.append(_media_content(item))
972
+ case types.ResourceLink():
973
+ result.append(_resource_link_text(item))
974
+ case types.EmbeddedResource():
975
+ match item.resource:
976
+ case types.TextResourceContents():
977
+ result.append(Content.from_text(item.resource.text))
978
+ case types.BlobResourceContents():
979
+ result.append(_blob_content(item.resource))
980
+ case _:
981
+ result.append(Content.from_text(str(item)))
982
+ if not result:
983
+ result.append(Content.from_text("null"))
984
+ return result
985
+
986
+ def _resolved_extra_args(self, tool_name: str) -> set[str]:
987
+ return self._global_extra_arg_names | self._tool_extra_arg_names.get(tool_name, set())
988
+
989
+ def _prepare_call_kwargs(
990
+ self, tool_name: str, kwargs: dict[str, Any]
991
+ ) -> tuple[dict[str, Any], dict[str, Any] | None]:
992
+ """Build wire arguments from source-separated or directly trusted kwargs.
993
+
994
+ Generated MCP functions isolate Chrys runtime kwargs before this
995
+ method. Direct Python callers of :meth:`call_tool` are trusted, while
996
+ undeclared names still require an explicit extra-argument opt-in.
997
+ """
998
+ raw_user_meta = kwargs.get("_meta")
999
+ user_meta: dict[str, Any] | None = None
1000
+ if raw_user_meta is not None and not isinstance(raw_user_meta, dict):
1001
+ raise ToolExecutionException("MCP tool metadata provided via _meta must be a dict.")
1002
+ if isinstance(raw_user_meta, dict):
1003
+ user_meta = {}
1004
+ for key, value in raw_user_meta.items():
1005
+ if not isinstance(key, str):
1006
+ raise ToolExecutionException("MCP tool metadata provided via _meta must use string keys.")
1007
+ user_meta[key] = value
1008
+
1009
+ declared = self._tool_param_names_by_name.get(tool_name, set())
1010
+ extras = self._resolved_extra_args(tool_name)
1011
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k != "_meta" and (k in declared or k in extras)}
1012
+ request_meta = dict(user_meta) if user_meta is not None else None
1013
+ request_meta = _inject_otel_into_mcp_meta(request_meta, overwrite=True)
1014
+ tool_meta = self._tool_call_meta_by_name.get(tool_name)
1015
+ if tool_meta is not None:
1016
+ request_meta = {**(request_meta or {}), **tool_meta}
1017
+ return filtered_kwargs, request_meta
1018
+
1019
+ async def call_tool(self, tool_name: str, **kwargs: Any) -> str | list[Content]:
1020
+ if not self.load_tools_flag:
1021
+ raise ToolExecutionException(
1022
+ "Tools are not loaded for this server, please set load_tools=True in the constructor."
1023
+ )
1024
+
1025
+ if self._tool_task_support_by_name.get(tool_name) == "required":
1026
+ raise ModelVisibleToolError(
1027
+ f"MCP tool '{tool_name}' requires long-running task support, which {APP_DISPLAY_NAME} does not implement yet."
1028
+ )
1029
+
1030
+ filtered_kwargs, meta = self._prepare_call_kwargs(tool_name, kwargs)
1031
+ parser = self.parse_tool_results or self._parse_tool_result_from_mcp
1032
+ attrs = self._mcp_base_span_attributes()
1033
+ attrs.update({OtelAttr.TOOL_NAME: tool_name, OtelAttr.OPERATION: OtelAttr.TOOL_EXECUTION_OPERATION})
1034
+ with create_mcp_client_span("tools/call", target=tool_name, attributes=attrs) as span:
1035
+ return await self._call_tool_with_retries(tool_name, filtered_kwargs, meta, parser, span)
1036
+
1037
+ async def _call_tool_with_retries(
1038
+ self,
1039
+ tool_name: str,
1040
+ filtered_kwargs: dict[str, Any],
1041
+ meta: dict[str, Any] | None,
1042
+ parser: Callable[..., str | list[Content]],
1043
+ span: otel_trace.Span,
1044
+ ) -> str | list[Content]:
1045
+ from anyio import ClosedResourceError
1046
+ from mcp.shared.exceptions import McpError
1047
+
1048
+ for attempt in range(2):
1049
+ # Tool calls are exposed only after the lifecycle installs a session.
1050
+ session = cast("ClientSession", self.session)
1051
+ try:
1052
+ result = sanitize_mcp_result_binary(
1053
+ await session.call_tool(tool_name, arguments=filtered_kwargs, meta=meta)
1054
+ )
1055
+ if result.isError:
1056
+ # With neither content nor structuredContent, skip the parser and its "null"
1057
+ # placeholder. A structured-only error still parses: the transports' parsers
1058
+ # turn structuredContent into text when content is empty.
1059
+ has_payload = bool(result.content) or result.structuredContent is not None
1060
+ parsed = parser(result) if has_payload else ""
1061
+ text = (
1062
+ "\n".join(c.text for c in parsed if c.type == "text" and c.text)
1063
+ if isinstance(parsed, list)
1064
+ else parsed
1065
+ )
1066
+ # The server's execution error is written for the model to correct its call. With no
1067
+ # text (only media, links and binary resources, or nothing at all) it says so.
1068
+ error = _server_error(truncate_mcp_error(text), f"MCP tool '{tool_name}'")
1069
+ if span.is_recording():
1070
+ set_mcp_span_error(span, "tool_error", error.model_message)
1071
+ raise error
1072
+ return parser(result)
1073
+ except ToolExecutionException:
1074
+ raise
1075
+ except (ClosedResourceError, McpError) as call_ex:
1076
+ is_session_terminated = (
1077
+ isinstance(call_ex, McpError) and "session terminated" in call_ex.error.message.lower()
1078
+ )
1079
+ is_connection_lost = isinstance(call_ex, ClosedResourceError) or is_session_terminated
1080
+ if not is_connection_lost:
1081
+ message = truncate_mcp_error(
1082
+ call_ex.error.message if isinstance(call_ex, McpError) else str(call_ex)
1083
+ )
1084
+ if span.is_recording():
1085
+ set_mcp_span_error(span, type(call_ex).__name__, message)
1086
+ raise _mcp_error_exception(call_ex, message, f"MCP tool '{tool_name}'") from call_ex
1087
+
1088
+ if attempt == 0:
1089
+ try:
1090
+ await self.connect(reset=True)
1091
+ continue
1092
+ except Exception as reconn_ex:
1093
+ raise ToolExecutionException(
1094
+ "Failed to reconnect to MCP server.", inner_exception=reconn_ex
1095
+ ) from reconn_ex
1096
+ if span.is_recording():
1097
+ set_mcp_span_error(span, type(call_ex).__name__, truncate_mcp_error(str(call_ex)))
1098
+ raise ToolExecutionException(
1099
+ f"Failed to call tool '{tool_name}' - connection lost.",
1100
+ inner_exception=call_ex,
1101
+ ) from call_ex
1102
+ except Exception as ex:
1103
+ if span.is_recording():
1104
+ set_mcp_span_error(span, type(ex).__name__, str(ex))
1105
+ raise ToolExecutionException(f"Failed to call tool '{tool_name}'.", inner_exception=ex) from ex
1106
+ raise ToolExecutionException(f"Failed to call tool '{tool_name}' after retries.")
1107
+
1108
+ async def get_prompt(self, prompt_name: str, **kwargs: Any) -> str:
1109
+ from anyio import ClosedResourceError
1110
+ from mcp.shared.exceptions import McpError
1111
+
1112
+ if not self.load_prompts_flag:
1113
+ raise ToolExecutionException(
1114
+ "Prompts are not loaded for this server, please set load_prompts=True in the constructor."
1115
+ )
1116
+ parser = self.parse_prompt_results or self._parse_prompt_result_from_mcp
1117
+ attrs = self._mcp_base_span_attributes()
1118
+ attrs.update({OtelAttr.PROMPT_NAME: prompt_name})
1119
+ with create_mcp_client_span("prompts/get", target=prompt_name, attributes=attrs) as span:
1120
+ for attempt in range(2):
1121
+ # Prompt functions are exposed only after the lifecycle installs a session.
1122
+ session = cast("ClientSession", self.session)
1123
+ try:
1124
+ return parser(await session.get_prompt(prompt_name, arguments=kwargs))
1125
+ except ClosedResourceError as cl_ex:
1126
+ if attempt == 0:
1127
+ try:
1128
+ await self.connect(reset=True)
1129
+ except Exception as reconn_ex:
1130
+ raise ToolExecutionException(
1131
+ "Failed to reconnect to MCP server.",
1132
+ inner_exception=reconn_ex,
1133
+ ) from reconn_ex
1134
+ continue
1135
+ set_mcp_span_error(span, type(cl_ex).__name__, truncate_mcp_error(str(cl_ex)))
1136
+ raise ToolExecutionException(
1137
+ f"Failed to call prompt '{prompt_name}' - connection lost.", inner_exception=cl_ex
1138
+ ) from cl_ex
1139
+ except McpError as mcp_exc:
1140
+ message = truncate_mcp_error(mcp_exc.error.message)
1141
+ set_mcp_span_error(span, type(mcp_exc).__name__, message)
1142
+ raise _mcp_error_exception(mcp_exc, message, f"MCP prompt '{prompt_name}'") from mcp_exc
1143
+ except Exception as ex:
1144
+ set_mcp_span_error(span, type(ex).__name__, str(ex))
1145
+ raise ToolExecutionException(f"Failed to call prompt '{prompt_name}'.", inner_exception=ex) from ex
1146
+ raise ToolExecutionException(f"Failed to get prompt '{prompt_name}' after retries.")
1147
+
1148
+ async def __aenter__(self) -> Self:
1149
+ try:
1150
+ await self.connect()
1151
+ return self
1152
+ except ToolException:
1153
+ raise
1154
+ except Exception as ex:
1155
+ await self.close()
1156
+ raise ToolExecutionException("Failed to enter context manager.", inner_exception=ex) from ex
1157
+
1158
+ async def __aexit__(
1159
+ self,
1160
+ exc_type: type[BaseException] | None,
1161
+ exc_value: BaseException | None,
1162
+ traceback: Any,
1163
+ ) -> None:
1164
+ await self.close()
1165
+
1166
+
1167
+ async def _expand_mcp_tool(tool_: ToolTypes) -> list[ToolTypes] | None:
1168
+ if not isinstance(tool_, MCPTool):
1169
+ return None
1170
+ if not tool_.is_connected:
1171
+ await tool_.connect()
1172
+ from chrys.service.mcp.cache import clone_mcp_function_tool
1173
+
1174
+ return normalize_tools(
1175
+ clone_mcp_function_tool(function, result_cap=DEFAULT_MCP_TOOL_RESULT_MAX_TOKENS) for function in tool_.functions
1176
+ )
1177
+
1178
+
1179
+ _set_tool_expander(_expand_mcp_tool)
1180
+
1181
+
1182
+ class MCPStdioTool(MCPTool):
1183
+ """MCP tool for stdio-based servers."""
1184
+
1185
+ def __init__(
1186
+ self,
1187
+ name: str,
1188
+ command: str,
1189
+ *,
1190
+ tool_name_prefix: str | None = None,
1191
+ load_tools: bool = True,
1192
+ parse_tool_results: Callable[[types.CallToolResult], str | list[Content]] | None = None,
1193
+ load_prompts: bool = True,
1194
+ parse_prompt_results: Callable[[types.GetPromptResult], str] | None = None,
1195
+ request_timeout: int | None = None,
1196
+ session: ClientSession | None = None,
1197
+ description: str | None = None,
1198
+ allowed_tools: Collection[str] | None = None,
1199
+ args: list[str] | None = None,
1200
+ env: dict[str, str] | None = None,
1201
+ encoding: str | None = None,
1202
+ additional_properties: dict[str, Any] | None = None,
1203
+ additional_tool_argument_names: Sequence[str] | Mapping[str, str | Sequence[str]] | None = None,
1204
+ **kwargs: Any,
1205
+ ) -> None:
1206
+ super().__init__(
1207
+ name=name,
1208
+ description=description,
1209
+ allowed_tools=allowed_tools,
1210
+ tool_name_prefix=tool_name_prefix,
1211
+ additional_properties=additional_properties,
1212
+ session=session,
1213
+ load_tools=load_tools,
1214
+ parse_tool_results=parse_tool_results,
1215
+ load_prompts=load_prompts,
1216
+ parse_prompt_results=parse_prompt_results,
1217
+ request_timeout=request_timeout,
1218
+ additional_tool_argument_names=additional_tool_argument_names,
1219
+ )
1220
+ self.command = command
1221
+ self.args = args or []
1222
+ self.env = env
1223
+ self.encoding = encoding
1224
+ self._client_kwargs = kwargs
1225
+
1226
+ def _mcp_base_span_attributes(self) -> dict[str, Any]:
1227
+ attrs = super()._mcp_base_span_attributes()
1228
+ attrs[OtelAttr.NETWORK_TRANSPORT] = "pipe"
1229
+ return attrs
1230
+
1231
+ def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
1232
+ args: dict[str, Any] = {"command": self.command, "args": self.args, "env": self.env}
1233
+ if self.encoding:
1234
+ args["encoding"] = self.encoding
1235
+ if self._client_kwargs:
1236
+ args.update(self._client_kwargs)
1237
+ try:
1238
+ from mcp.client.stdio import StdioServerParameters, stdio_client
1239
+ except ModuleNotFoundError as ex:
1240
+ raise ModuleNotFoundError("`mcp` is required to use `MCPStdioTool`. Please install `mcp`.") from ex
1241
+ return stdio_client(server=StdioServerParameters(**args))
1242
+
1243
+
1244
+ class MCPStreamableHTTPTool(MCPTool):
1245
+ """MCP tool for streamable HTTP/SSE servers."""
1246
+
1247
+ def __init__(
1248
+ self,
1249
+ name: str,
1250
+ url: str,
1251
+ *,
1252
+ tool_name_prefix: str | None = None,
1253
+ load_tools: bool = True,
1254
+ parse_tool_results: Callable[[types.CallToolResult], str | list[Content]] | None = None,
1255
+ load_prompts: bool = True,
1256
+ parse_prompt_results: Callable[[types.GetPromptResult], str] | None = None,
1257
+ request_timeout: int | None = None,
1258
+ session: ClientSession | None = None,
1259
+ description: str | None = None,
1260
+ allowed_tools: Collection[str] | None = None,
1261
+ terminate_on_close: bool | None = None,
1262
+ additional_properties: dict[str, Any] | None = None,
1263
+ http_client: AsyncClient | None = None,
1264
+ additional_tool_argument_names: Sequence[str] | Mapping[str, str | Sequence[str]] | None = None,
1265
+ **_kwargs: Any,
1266
+ ) -> None:
1267
+ """Initialize a streamable HTTP MCP tool.
1268
+
1269
+ Keyword Args:
1270
+ http_client: Optional caller-managed HTTP client. Sensitive credentials
1271
+ configured directly on this client, such as with
1272
+ ``AsyncClient(headers=...)``, bypass Chrys's origin-scoped header
1273
+ hooks. If the client follows cross-origin redirects, those
1274
+ credentials can be sent to another origin. The caller is responsible
1275
+ for scoping them to the configured URL's origin.
1276
+ """
1277
+ super().__init__(
1278
+ name=name,
1279
+ description=description,
1280
+ allowed_tools=allowed_tools,
1281
+ tool_name_prefix=tool_name_prefix,
1282
+ additional_properties=additional_properties,
1283
+ session=session,
1284
+ load_tools=load_tools,
1285
+ parse_tool_results=parse_tool_results,
1286
+ load_prompts=load_prompts,
1287
+ parse_prompt_results=parse_prompt_results,
1288
+ request_timeout=request_timeout,
1289
+ additional_tool_argument_names=additional_tool_argument_names,
1290
+ )
1291
+ self.url = url
1292
+ self.terminate_on_close = terminate_on_close
1293
+ self._httpx_client: AsyncClient | None = http_client
1294
+
1295
+ def _mcp_base_span_attributes(self) -> dict[str, Any]:
1296
+ attrs = super()._mcp_base_span_attributes()
1297
+ attrs[OtelAttr.NETWORK_TRANSPORT] = "tcp"
1298
+ attrs[OtelAttr.NETWORK_PROTOCOL_NAME] = "http"
1299
+ try:
1300
+ from httpx import URL
1301
+
1302
+ parsed = URL(self.url)
1303
+ if parsed.host:
1304
+ attrs[OtelAttr.ADDRESS] = parsed.host
1305
+ attrs[OtelAttr.PORT] = parsed.port or (443 if parsed.scheme == "https" else 80)
1306
+ except Exception:
1307
+ logger.debug("Failed to parse URL for MCP span transport attributes", exc_info=True)
1308
+ return attrs
1309
+
1310
+ def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
1311
+ return streamable_http_client(
1312
+ url=self.url,
1313
+ http_client=self._httpx_client,
1314
+ terminate_on_close=self.terminate_on_close if self.terminate_on_close is not None else True,
1315
+ )
1316
+
1317
+
1318
+ def streamable_http_client(*args: Any, **kwargs: Any) -> _AsyncGeneratorContextManager[Any, None]:
1319
+ try:
1320
+ from mcp.client.streamable_http import streamable_http_client as _streamable_http_client
1321
+ except ModuleNotFoundError as ex:
1322
+ missing_name = ex.name or str(ex)
1323
+ if missing_name == "mcp" or missing_name.startswith("mcp.") or "mcp" in missing_name:
1324
+ raise ModuleNotFoundError("`MCPStreamableHTTPTool` requires `mcp`. Please install `mcp`.") from ex
1325
+ raise ModuleNotFoundError(
1326
+ "`MCPStreamableHTTPTool` requires streamable HTTP transport support. "
1327
+ f"The optional dependency `{missing_name}` is not installed. Please update your dependencies."
1328
+ ) from ex
1329
+
1330
+ return _streamable_http_client(*args, **kwargs)