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
chrys/kernel/tools.py ADDED
@@ -0,0 +1,1061 @@
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 FunctionTool and tool-normalization surface.
6
+
7
+ This module provides ``FunctionTool`` construction, schema generation,
8
+ serialization and invocation,
9
+ ``SKIP_PARSING``, ``tool()``, and ``normalize_tools()``. Tool-invocation
10
+ telemetry lives in ``_tool_execution.py``; ``FunctionTool.invoke``
11
+ intentionally stays free of observability and decorative logging.
12
+ Explicit-null restoration is classified and applied by the finite static
13
+ policy in ``_null_overlay.py``.
14
+
15
+ HARD RULE: kernel modules may import only the stdlib, intra-package modules,
16
+ allowed third-party packages, and downward ``chrys.foundation.*`` modules.
17
+ Sibling or upward ``chrys.*`` imports remain forbidden.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import contextvars
24
+ import copy
25
+ import inspect
26
+ import logging
27
+ import types
28
+ import typing
29
+ from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
30
+ from contextlib import suppress
31
+ from functools import reduce, wraps
32
+ from operator import or_
33
+ from typing import (
34
+ Annotated,
35
+ Any,
36
+ ClassVar,
37
+ Final,
38
+ Literal,
39
+ TypedDict,
40
+ TypeGuard,
41
+ get_args,
42
+ get_origin,
43
+ overload,
44
+ )
45
+
46
+ from pydantic import (
47
+ BaseModel,
48
+ BeforeValidator,
49
+ Field,
50
+ ValidationError,
51
+ create_model,
52
+ )
53
+
54
+ from chrys.foundation.text.model_json import model_json
55
+
56
+ from ._content import Content
57
+ from ._null_overlay import (
58
+ OverlayPolicy,
59
+ OverlayTier,
60
+ _classification_fallback,
61
+ classify,
62
+ restore_explicit_nulls,
63
+ )
64
+ from ._serialization import SerializationMixin
65
+ from .exceptions import ModelVisibleToolError, ToolException
66
+ from .middleware import FunctionInvocationContext
67
+
68
+ __all__ = [
69
+ "SKIP_PARSING",
70
+ "FunctionInvocationConfiguration",
71
+ "FunctionTool",
72
+ "SyncToolCancelledAfterCompletion",
73
+ "ToolException",
74
+ "ToolTypes",
75
+ "normalize_tools",
76
+ "tool",
77
+ ]
78
+
79
+ logger = logging.getLogger(__name__)
80
+
81
+
82
+ class SyncToolCancelledAfterCompletion(asyncio.CancelledError):
83
+ """Cancellation that landed after a threaded sync tool already returned.
84
+
85
+ The worker thread finished — side effects are done and the return value
86
+ exists — but the invoking task was cancelled before its ``await`` could
87
+ resume and deliver it. ``completed_result`` carries that value so the
88
+ tool loop can journal it before cancellation proceeds; plain
89
+ ``except asyncio.CancelledError`` handlers still treat this as a normal
90
+ cancellation.
91
+ """
92
+
93
+ def __init__(self, completed_result: Any) -> None:
94
+ super().__init__()
95
+ self.completed_result = completed_result
96
+
97
+
98
+ class _SkipParsingSentinel:
99
+ """Sentinel meaning ``FunctionTool.invoke`` should return raw values."""
100
+
101
+ _instance: ClassVar[_SkipParsingSentinel | None] = None
102
+
103
+ def __new__(cls) -> _SkipParsingSentinel:
104
+ if cls._instance is None:
105
+ cls._instance = super().__new__(cls)
106
+ return cls._instance
107
+
108
+ def __repr__(self) -> str:
109
+ return "SKIP_PARSING"
110
+
111
+
112
+ SKIP_PARSING: Final[_SkipParsingSentinel] = _SkipParsingSentinel()
113
+
114
+ type ResultParser = Callable[[Any], str | list[Content]]
115
+
116
+
117
+ def _is_result_parser(value: object) -> TypeGuard[ResultParser]:
118
+ """Narrow a configured parser after the runtime callable check."""
119
+ return callable(value)
120
+
121
+
122
+ def _is_skip_parsing_sentinel(value: object) -> bool:
123
+ """Accept SKIP_PARSING and foreign sentinels with the same class name and representation."""
124
+ return value is SKIP_PARSING or (type(value).__name__ == "_SkipParsingSentinel" and repr(value) == "SKIP_PARSING")
125
+
126
+
127
+ def _annotation_includes_function_invocation_context(annotation: Any) -> bool:
128
+ """Check whether an annotation resolves to FunctionInvocationContext."""
129
+ candidates = get_args(annotation) or (annotation,)
130
+ return any(
131
+ candidate is FunctionInvocationContext
132
+ or candidate == "FunctionInvocationContext"
133
+ or (
134
+ getattr(candidate, "__name__", None) == "FunctionInvocationContext"
135
+ and str(getattr(candidate, "__module__", "")).endswith("middleware")
136
+ )
137
+ for candidate in candidates
138
+ )
139
+
140
+
141
+ def _parse_annotation(annotation: Any) -> Any:
142
+ """Convert string annotation metadata into Pydantic field descriptions."""
143
+ origin = get_origin(annotation)
144
+ if origin is not None:
145
+ if origin is Literal:
146
+ return annotation
147
+
148
+ args = get_args(annotation)
149
+ if len(args) > 1 and isinstance(args[1], str):
150
+ args_list = list(args)
151
+ if len(args_list) == 2:
152
+ return Annotated.__getitem__((args_list[0], Field(description=args_list[1])))
153
+ return Annotated.__getitem__((args_list[0], Field(description=args_list[1]), tuple(args_list[2:])))
154
+ return annotation
155
+
156
+
157
+ def _reject_bool_as_int(value: Any) -> Any:
158
+ """Reject bool before Pydantic applies its otherwise useful int coercions."""
159
+ if isinstance(value, bool):
160
+ raise ValueError("boolean values are not valid integers")
161
+ return value
162
+
163
+
164
+ def _make_bool_int_literal_guard(values: tuple[Any, ...]) -> Callable[[Any], Any]:
165
+ """Require exact type+value literal matches for raw bool/int inputs.
166
+
167
+ Pydantic matches ``Literal`` members by equality, and ``True == 1`` /
168
+ ``False == 0`` in Python — so ``Literal[0, True]`` would otherwise accept
169
+ ``False`` (as ``0``) and ``1`` (as ``True``). Non-bool/int inputs pass
170
+ through so Pydantic keeps its own lax handling for them.
171
+ """
172
+
173
+ def _guard(value: Any) -> Any:
174
+ if type(value) in (bool, int) and not any(type(member) is type(value) and member == value for member in values):
175
+ raise ValueError("literal values require an exact type and value match")
176
+ return value
177
+
178
+ return _guard
179
+
180
+
181
+ def _prevent_bool_int_coercion(annotation: Any) -> Any:
182
+ """Recursively guard int annotations while preserving other Pydantic coercions."""
183
+ if annotation is int:
184
+ return Annotated[int, BeforeValidator(_reject_bool_as_int)]
185
+
186
+ origin = get_origin(annotation)
187
+ if origin is Literal:
188
+ values = get_args(annotation)
189
+ if any(type(value) in (bool, int) for value in values):
190
+ return Annotated[annotation, BeforeValidator(_make_bool_int_literal_guard(values))]
191
+ return annotation
192
+ if origin is None:
193
+ return annotation
194
+
195
+ args = get_args(annotation)
196
+ if origin is Annotated:
197
+ base, *metadata = args
198
+ transformed = _prevent_bool_int_coercion(base)
199
+ if get_origin(transformed) is Annotated:
200
+ transformed_base, *transformed_metadata = get_args(transformed)
201
+ return Annotated[transformed_base, *metadata, *transformed_metadata]
202
+ return Annotated[transformed, *metadata]
203
+
204
+ transformed_args = tuple(_prevent_bool_int_coercion(arg) for arg in args)
205
+ if transformed_args == args:
206
+ return annotation
207
+ if origin is types.UnionType:
208
+ return reduce(or_, transformed_args)
209
+ return origin[transformed_args]
210
+
211
+
212
+ def _matches_json_schema_type(value: Any, schema_type: str, *, tuples_as_arrays: bool = False) -> bool:
213
+ """Check a value against a JSON Schema primitive type."""
214
+ match schema_type:
215
+ case "string":
216
+ return isinstance(value, str)
217
+ case "integer":
218
+ return isinstance(value, int) and not isinstance(value, bool)
219
+ case "number":
220
+ return (isinstance(value, int | float)) and not isinstance(value, bool)
221
+ case "boolean":
222
+ return isinstance(value, bool)
223
+ case "array":
224
+ # Python-mode ``model_dump`` preserves tuple-typed fields, so the
225
+ # validated paths opt in via ``tuples_as_arrays``. Schema-supplied
226
+ # raw arguments keep the strict list-only contract.
227
+ if tuples_as_arrays and isinstance(value, tuple):
228
+ return True
229
+ return isinstance(value, list)
230
+ case "object":
231
+ return isinstance(value, dict)
232
+ case "null":
233
+ return value is None
234
+ case _:
235
+ return True
236
+
237
+
238
+ def _validate_arguments_against_schema(
239
+ *,
240
+ arguments: Mapping[str, Any],
241
+ schema: Mapping[str, Any],
242
+ tool_name: str,
243
+ tuples_as_arrays: bool = False,
244
+ values_prevalidated: bool = False,
245
+ ) -> dict[str, Any]:
246
+ """Apply lightweight schema checks at the callable argument boundary."""
247
+ parsed_arguments = dict(arguments)
248
+
249
+ required_fields = [field for field in schema.get("required", []) if isinstance(field, str)]
250
+ missing_fields = [field for field in required_fields if field not in parsed_arguments]
251
+ if missing_fields:
252
+ raise TypeError(f"Missing required argument(s) for '{tool_name}': {', '.join(sorted(missing_fields))}")
253
+
254
+ if values_prevalidated:
255
+ return parsed_arguments
256
+
257
+ properties: Mapping[str, Any] = schema.get("properties", {})
258
+ if schema.get("additionalProperties") is False:
259
+ unexpected_fields = sorted(field for field in parsed_arguments if field not in properties)
260
+ if unexpected_fields:
261
+ raise TypeError(f"Unexpected argument(s) for '{tool_name}': {', '.join(unexpected_fields)}")
262
+
263
+ for field_name, field_value in parsed_arguments.items():
264
+ if not isinstance(properties.get(field_name), dict):
265
+ continue
266
+
267
+ enum_values = properties.get(field_name, {}).get("enum")
268
+ if isinstance(enum_values, list) and enum_values and field_value not in enum_values:
269
+ raise TypeError(
270
+ f"Invalid value for '{field_name}' in '{tool_name}': {field_value!r} is not in {enum_values!r}"
271
+ )
272
+
273
+ schema_type = properties.get(field_name, {}).get("type")
274
+ if isinstance(schema_type, str):
275
+ if not _matches_json_schema_type(field_value, schema_type, tuples_as_arrays=tuples_as_arrays):
276
+ raise TypeError(
277
+ f"Invalid type for '{field_name}' in '{tool_name}': "
278
+ f"expected {schema_type}, got {type(field_value).__name__}"
279
+ )
280
+ continue
281
+
282
+ if isinstance(schema_type, list):
283
+ allowed_types: list[str] = [item for item in schema_type if isinstance(item, str)]
284
+ if allowed_types and not any(
285
+ _matches_json_schema_type(field_value, item, tuples_as_arrays=tuples_as_arrays)
286
+ for item in allowed_types
287
+ ):
288
+ raise TypeError(
289
+ f"Invalid type for '{field_name}' in '{tool_name}': expected one of "
290
+ f"{allowed_types}, got {type(field_value).__name__}"
291
+ )
292
+
293
+ return parsed_arguments
294
+
295
+
296
+ def _coerce_content(value: Any) -> Content | None:
297
+ """Convert compatible foreign content through its serialized shape into Chrys Content."""
298
+ if isinstance(value, Content):
299
+ return value
300
+ to_dict = getattr(value, "to_dict", None)
301
+ if not callable(to_dict) or not hasattr(value, "type"):
302
+ return None
303
+ try:
304
+ payload = to_dict(exclude_none=False)
305
+ except TypeError:
306
+ payload = to_dict()
307
+ if not isinstance(payload, Mapping) or not isinstance(payload.get("type"), str):
308
+ return None
309
+ try:
310
+ converted = Content.from_dict(payload)
311
+ except Exception:
312
+ return None
313
+ converted.raw_representation = getattr(value, "raw_representation", None)
314
+ return converted
315
+
316
+
317
+ class FunctionTool(SerializationMixin):
318
+ """A callable tool with argument validation, result parsing and serializable schema."""
319
+
320
+ # Inherited class identifier: serialized payloads always carry the
321
+ # instance ``type`` field ("function_tool"), so subclasses must validate
322
+ # against that same identifier on from_dict — the snake_case class-name
323
+ # fallback would reject every payload a subclass itself serialized.
324
+ TYPE: ClassVar[str] = "function_tool"
325
+ INJECTABLE: ClassVar[set[str]] = {"func"}
326
+ DEFAULT_EXCLUDE: ClassVar[set[str]] = {
327
+ "additional_properties",
328
+ "input_model",
329
+ "_invocation_duration_histogram",
330
+ "_cached_parameters",
331
+ "_input_schema",
332
+ "_serialization_schema",
333
+ "_serialization_schema_cached",
334
+ "_null_overlay_policy",
335
+ "_null_overlay_policy_cached",
336
+ "_schema_supplied",
337
+ "_invoke_sync_on_event_loop",
338
+ }
339
+
340
+ def __init__(
341
+ self,
342
+ *,
343
+ name: str,
344
+ description: str = "",
345
+ kind: str | None = None,
346
+ max_invocations: int | None = None,
347
+ max_invocation_exceptions: int | None = None,
348
+ additional_properties: dict[str, Any] | None = None,
349
+ func: Callable[..., Any] | None = None,
350
+ input_model: type[BaseModel] | Mapping[str, Any] | None = None,
351
+ result_parser: Callable[[Any], str | list[Content]] | _SkipParsingSentinel | object | None = None,
352
+ **kwargs: Any,
353
+ ) -> None:
354
+ self.name = name
355
+ self.description = description
356
+ self.kind = kind
357
+ self.additional_properties = additional_properties
358
+ self._invoke_sync_on_event_loop = False
359
+ for key, value in kwargs.items():
360
+ setattr(self, key, value)
361
+
362
+ self.func = func
363
+ self._instance = None
364
+ self._context_parameter_name: str | None = None
365
+ self._input_model_explicitly_provided = input_model is not None
366
+ if self.func:
367
+ self._discover_injected_parameters()
368
+
369
+ self._input_schema_cached: dict[str, Any] | None = None
370
+ self._serialization_schema_cached: dict[str, Any] | None = None
371
+ self._null_overlay_policy_cached: OverlayPolicy | None = None
372
+ if isinstance(input_model, Mapping):
373
+ self._schema_supplied = True
374
+ self._input_schema_cached = dict(input_model)
375
+ self.input_model: type[BaseModel] | None = None
376
+ else:
377
+ self._schema_supplied = False
378
+ self.input_model = self._resolve_input_model(input_model)
379
+ self._cached_parameters: dict[str, Any] | None = None
380
+ if max_invocations is not None and max_invocations < 1:
381
+ raise ValueError("max_invocations must be at least 1 or None.")
382
+ if max_invocation_exceptions is not None and max_invocation_exceptions < 1:
383
+ raise ValueError("max_invocation_exceptions must be at least 1 or None.")
384
+ self.max_invocations = max_invocations
385
+ self.invocation_count = 0
386
+ self.max_invocation_exceptions = max_invocation_exceptions
387
+ self.invocation_exception_count = 0
388
+ self._invocation_duration_histogram = None
389
+ self.type: Literal["function_tool"] = "function_tool"
390
+ self.result_parser = SKIP_PARSING if _is_skip_parsing_sentinel(result_parser) else result_parser
391
+
392
+ def _discover_injected_parameters(self) -> None:
393
+ """Inspect the wrapped function for runtime injection parameters."""
394
+ func = self.func.func if isinstance(self.func, FunctionTool) else self.func
395
+ if func is None:
396
+ return
397
+
398
+ signature = inspect.signature(func)
399
+ try:
400
+ type_hints = typing.get_type_hints(func)
401
+ except Exception:
402
+ type_hints = {name: param.annotation for name, param in signature.parameters.items()}
403
+
404
+ for name, param in signature.parameters.items():
405
+ if name in {"self", "cls"}:
406
+ continue
407
+ annotation = type_hints.get(name, param.annotation)
408
+ if self._is_context_parameter(name, annotation):
409
+ if self._context_parameter_name is not None:
410
+ raise ValueError(f"Function '{self.name}' defines multiple FunctionInvocationContext parameters.")
411
+ self._context_parameter_name = name
412
+
413
+ def _is_context_parameter(self, name: str, annotation: Any) -> bool:
414
+ """Check whether a callable parameter should receive FunctionInvocationContext injection."""
415
+ if _annotation_includes_function_invocation_context(annotation):
416
+ return True
417
+ return self._input_model_explicitly_provided and name == "ctx" and annotation is inspect.Parameter.empty
418
+
419
+ def __str__(self) -> str:
420
+ """Return a string representation of the tool."""
421
+ if self.description:
422
+ return f"{self.__class__.__name__}(name={self.name}, description={self.description})"
423
+ return f"{self.__class__.__name__}(name={self.name})"
424
+
425
+ @property
426
+ def bound_instance(self) -> object | None:
427
+ """Return the owner of a descriptor-bound tool, or None for an unbound tool."""
428
+ return self._instance
429
+
430
+ @property
431
+ def declaration_only(self) -> bool:
432
+ """Indicate whether the function is declaration only."""
433
+ declaration_flag = self.__dict__.get("_declaration_only", False)
434
+ if isinstance(declaration_flag, bool) and declaration_flag:
435
+ return True
436
+ return self.func is None
437
+
438
+ def __get__(self, obj: Any, objtype: type | None = None) -> FunctionTool:
439
+ """Bind descriptor tools to their owning instance."""
440
+ if obj is None:
441
+ return self
442
+
443
+ if self.func is not None:
444
+ sig = inspect.signature(self.func)
445
+ params = list(sig.parameters.keys())
446
+ if params and params[0] in {"self", "cls"}:
447
+ bound_func = copy.copy(self)
448
+ bound_func._instance = obj
449
+ return bound_func
450
+
451
+ return self
452
+
453
+ def _resolve_input_model(self, input_model: type[BaseModel] | None) -> type[BaseModel]:
454
+ """Resolve the input model for the function."""
455
+ if input_model is not None:
456
+ if inspect.isclass(input_model) and issubclass(input_model, BaseModel):
457
+ return input_model
458
+ raise TypeError("input_model must be a Pydantic BaseModel subclass or a JSON schema dict.")
459
+
460
+ if self.func is None:
461
+ return create_model(f"{self.name}_input")
462
+
463
+ func = self.func.func if isinstance(self.func, FunctionTool) else self.func
464
+ if func is None:
465
+ return create_model(f"{self.name}_input")
466
+ sig = inspect.signature(func)
467
+ try:
468
+ type_hints = typing.get_type_hints(func, include_extras=True)
469
+ except Exception:
470
+ type_hints = {}
471
+ fields: dict[str, Any] = {
472
+ pname: (
473
+ _prevent_bool_int_coercion(_parse_annotation(type_hints.get(pname, param.annotation)))
474
+ if type_hints.get(pname, param.annotation) is not inspect.Parameter.empty
475
+ else str,
476
+ param.default if param.default is not inspect.Parameter.empty else ...,
477
+ )
478
+ for pname, param in sig.parameters.items()
479
+ if pname not in {"self", "cls"}
480
+ and pname != self._context_parameter_name
481
+ and param.kind not in {inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD}
482
+ }
483
+ return create_model(f"{self.name}_input", **fields)
484
+
485
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
486
+ """Call the wrapped function with the provided arguments."""
487
+ if self.declaration_only:
488
+ raise ToolException(f"Function '{self.name}' is declaration only and cannot be invoked.")
489
+ if self.max_invocations is not None and self.invocation_count >= self.max_invocations:
490
+ raise ToolException(
491
+ f"Function '{self.name}' has reached its maximum invocation limit, you can no longer use this tool."
492
+ )
493
+ if (
494
+ self.max_invocation_exceptions is not None
495
+ and self.invocation_exception_count >= self.max_invocation_exceptions
496
+ ):
497
+ raise ToolException(
498
+ f"Function '{self.name}' has reached its maximum exception limit, "
499
+ f"you tried to use this tool too many times and it kept failing."
500
+ )
501
+ self.invocation_count += 1
502
+ try:
503
+ func = self.func
504
+ if func is None:
505
+ raise ToolException(f"Function '{self.name}' has no implementation.")
506
+ call_args = (self._instance, *args) if self._instance is not None else args
507
+ result = func(*call_args, **kwargs)
508
+ except Exception:
509
+ self.invocation_exception_count += 1
510
+ raise
511
+ if inspect.isawaitable(result):
512
+ return self._count_awaited_exception(result)
513
+ return result
514
+
515
+ async def _count_awaited_exception(self, awaitable: Awaitable[Any]) -> Any:
516
+ """Await an async tool's result; its failure surfaces only here, so count it here."""
517
+ try:
518
+ return await awaitable
519
+ except Exception:
520
+ self.invocation_exception_count += 1
521
+ raise
522
+
523
+ async def _invoke_function(self, call_kwargs: Mapping[str, Any]) -> Any:
524
+ """Run sync tools off the event loop during async invocation."""
525
+ func = self.func.func if isinstance(self.func, FunctionTool) else self.func
526
+ if inspect.iscoroutinefunction(func) or self._invoke_sync_on_event_loop:
527
+ res = self.__call__(**call_kwargs)
528
+ return await res if inspect.isawaitable(res) else res
529
+
530
+ # ``to_thread`` equivalent with a completion record the cancellation
531
+ # path can inspect: the worker may finish — side effects done, value
532
+ # produced — while this task is still suspended, and a cancellation
533
+ # arriving in that window would otherwise drop the completed result.
534
+ var_context = contextvars.copy_context()
535
+ completed: list[Any] = []
536
+
537
+ def _call_and_record() -> Any:
538
+ value = var_context.run(self.__call__, **call_kwargs)
539
+ completed.append(value)
540
+ return value
541
+
542
+ try:
543
+ res = await asyncio.get_running_loop().run_in_executor(None, _call_and_record)
544
+ except asyncio.CancelledError:
545
+ if completed and not inspect.isawaitable(completed[0]):
546
+ raise SyncToolCancelledAfterCompletion(completed[0]) from None
547
+ raise
548
+ return await res if inspect.isawaitable(res) else res
549
+
550
+ @property
551
+ def _null_overlay_policy(self) -> OverlayPolicy:
552
+ """Get the input model's one-time static null-restore policy."""
553
+ if self._null_overlay_policy_cached is None:
554
+ if self.input_model is None:
555
+ raise RuntimeError("Schema-supplied tools do not have a null-overlay policy.")
556
+ classification_failed = False
557
+ try:
558
+ policy = classify(self.input_model)
559
+ except Exception as exc:
560
+ classification_failed = True
561
+ reason = f"classification failed with {type(exc).__name__}"
562
+ policy = _classification_fallback(self.input_model, reason)
563
+ logger.debug(
564
+ "Tool %s null-overlay classification failed; using %s restoration: %s",
565
+ self.name,
566
+ policy.tier.value,
567
+ reason,
568
+ exc_info=True,
569
+ )
570
+ if not classification_failed and policy.tier is not OverlayTier.FULL:
571
+ logger.debug(
572
+ "Tool %s uses %s explicit-null restoration: %s",
573
+ self.name,
574
+ policy.tier.value,
575
+ policy.reason or "nested annotation graph is outside the static whitelist",
576
+ )
577
+ self._null_overlay_policy_cached = policy
578
+ return self._null_overlay_policy_cached
579
+
580
+ def _dump_arguments(self, validated: BaseModel) -> dict[str, Any]:
581
+ """Dump once, then apply the cached static explicit-null policy."""
582
+ if self.input_model is None:
583
+ raise RuntimeError("Schema-supplied tools do not use typed argument dumps.")
584
+ if type(validated) is self.input_model:
585
+ dumped = validated.model_dump(exclude_none=True)
586
+ else:
587
+ dumped = self.input_model.__pydantic_serializer__.to_python(validated, exclude_none=True)
588
+ if not isinstance(dumped, dict):
589
+ return dumped
590
+ return restore_explicit_nulls(validated, dumped, self._null_overlay_policy)
591
+
592
+ @overload
593
+ async def invoke(
594
+ self,
595
+ *,
596
+ arguments: BaseModel | Mapping[str, Any] | None = None,
597
+ context: FunctionInvocationContext | None = None,
598
+ tool_call_id: str | None = None,
599
+ skip_parsing: Literal[True],
600
+ **kwargs: Any,
601
+ ) -> Any: ...
602
+
603
+ @overload
604
+ async def invoke(
605
+ self,
606
+ *,
607
+ arguments: BaseModel | Mapping[str, Any] | None = None,
608
+ context: FunctionInvocationContext | None = None,
609
+ tool_call_id: str | None = None,
610
+ skip_parsing: Literal[False] = False,
611
+ **kwargs: Any,
612
+ ) -> list[Content]: ...
613
+
614
+ async def invoke(
615
+ self,
616
+ *,
617
+ arguments: BaseModel | Mapping[str, Any] | None = None,
618
+ context: FunctionInvocationContext | None = None,
619
+ tool_call_id: str | None = None,
620
+ skip_parsing: bool = False,
621
+ **kwargs: Any,
622
+ ) -> list[Content] | Any:
623
+ """Run the wrapped function and parse the result to Chrys Content."""
624
+ if self.declaration_only:
625
+ raise ToolException(f"Function '{self.name}' is declaration only and cannot be invoked.")
626
+
627
+ configured_parser = self.result_parser
628
+ skip_parsing = skip_parsing or _is_skip_parsing_sentinel(configured_parser)
629
+ custom_parser = configured_parser if _is_result_parser(configured_parser) else None
630
+ arguments_in_callable_keyspace = bool(kwargs.pop("_arguments_in_callable_keyspace", False))
631
+
632
+ parameter_names = set(self.parameters().get("properties", {}).keys())
633
+ direct_argument_kwargs = (
634
+ {key: value for key, value in kwargs.items() if key in parameter_names} if arguments is None else {}
635
+ )
636
+ runtime_kwargs = dict(context.kwargs) if context is not None else {}
637
+ unexpected_kwargs = {key: value for key, value in kwargs.items() if key not in direct_argument_kwargs}
638
+ if unexpected_kwargs:
639
+ unexpected_names = ", ".join(sorted(unexpected_kwargs))
640
+ raise TypeError(
641
+ f"Unexpected keyword argument(s) for tool '{self.name}': {unexpected_names}. "
642
+ "Pass runtime data via FunctionInvocationContext instead."
643
+ )
644
+ if arguments is None and direct_argument_kwargs:
645
+ arguments = direct_argument_kwargs
646
+ if arguments is None and context is not None:
647
+ arguments = context.arguments
648
+
649
+ if arguments is None:
650
+ validated_arguments: dict[str, Any] = {}
651
+ else:
652
+ # Tuple-typed fields only survive into the arguments via a
653
+ # python-mode ``model_dump``; raw schema-supplied arguments keep
654
+ # the strict list-only array contract.
655
+ typed_model_dump = False
656
+ try:
657
+ if arguments_in_callable_keyspace:
658
+ if self.input_model is None or self._schema_supplied or not isinstance(arguments, Mapping):
659
+ raise TypeError("Callable-keyspace arguments require a typed-model mapping.")
660
+ parsed_arguments = dict(arguments)
661
+ typed_model_dump = True
662
+ elif isinstance(arguments, Mapping):
663
+ parsed_arguments = dict(arguments)
664
+ if self.input_model is not None and not self._schema_supplied:
665
+ try:
666
+ validation_arguments = copy.deepcopy(parsed_arguments)
667
+ except Exception:
668
+ validation_arguments = parsed_arguments
669
+ validated = self.input_model.model_validate(validation_arguments)
670
+ parsed_arguments = self._dump_arguments(validated)
671
+ typed_model_dump = True
672
+ elif isinstance(arguments, BaseModel):
673
+ if (
674
+ self.input_model is not None
675
+ and not self._schema_supplied
676
+ and not isinstance(arguments, self.input_model)
677
+ ):
678
+ raise TypeError(f"Expected {self.input_model.__name__}, got {type(arguments).__name__}")
679
+ if self.input_model is not None and not self._schema_supplied:
680
+ parsed_arguments = self._dump_arguments(arguments)
681
+ else:
682
+ # Schema-supplied/no-model tools have no declared model
683
+ # policy; preserve their runtime-model behavior.
684
+ parsed_arguments = arguments.model_dump(exclude_none=True)
685
+ typed_model_dump = not self._schema_supplied
686
+ else:
687
+ raise TypeError(
688
+ f"Expected mapping-like arguments for tool '{self.name}', got {type(arguments).__name__}"
689
+ )
690
+ except ValidationError as exc:
691
+ raise TypeError(f"Invalid arguments for '{self.name}': {exc}") from exc
692
+
693
+ validated_arguments = _validate_arguments_against_schema(
694
+ arguments=parsed_arguments,
695
+ schema=self._serialization_schema
696
+ if typed_model_dump and not self._schema_supplied
697
+ else self.parameters(),
698
+ tool_name=self.name,
699
+ tuples_as_arrays=typed_model_dump,
700
+ values_prevalidated=typed_model_dump and not self._schema_supplied,
701
+ )
702
+
703
+ effective_context = context
704
+ if effective_context is None and self._context_parameter_name is not None:
705
+ effective_context = FunctionInvocationContext(
706
+ function=self,
707
+ arguments=validated_arguments,
708
+ kwargs=runtime_kwargs,
709
+ )
710
+ if effective_context is not None:
711
+ effective_context.function = self
712
+ effective_context.arguments = validated_arguments
713
+ effective_context.kwargs = dict(runtime_kwargs)
714
+
715
+ call_kwargs = dict(validated_arguments)
716
+ if self._context_parameter_name is not None and effective_context is not None:
717
+ call_kwargs[self._context_parameter_name] = effective_context
718
+
719
+ try:
720
+ result = await self._invoke_function(call_kwargs)
721
+ except SyncToolCancelledAfterCompletion as exc:
722
+ # Parse the drained value in place (parsers are synchronous) so
723
+ # the journaled result has the same shape a normal return would
724
+ # have produced.
725
+ if not skip_parsing:
726
+ try:
727
+ exc.completed_result = self._parse_invocation_result(exc.completed_result, custom_parser)
728
+ except ModelVisibleToolError as parse_error:
729
+ exc.completed_result = [Content.from_text(parse_error.result_text or "")]
730
+ raise
731
+ if skip_parsing:
732
+ return result
733
+ return self._parse_invocation_result(result, custom_parser)
734
+
735
+ def _parse_invocation_result(self, result: Any, custom_parser: ResultParser | None) -> Any:
736
+ """Convert a completed call's value to Content.
737
+
738
+ A failing custom parser fails the call without showing the raw value,
739
+ which can be large or internal; the default parser falls back to ``str()``.
740
+ """
741
+ if custom_parser is None:
742
+ try:
743
+ parsed: Any = FunctionTool.parse_result(result)
744
+ except Exception:
745
+ logger.warning("Function %s: result parser failed, falling back to str().", self.name)
746
+ parsed = [Content.from_text(str(result))]
747
+ return FunctionTool._normalize_parser_output(parsed)
748
+ try:
749
+ parsed = custom_parser(result)
750
+ except Exception as exc:
751
+ raise ModelVisibleToolError(
752
+ f"Function '{self.name}' completed, but its result parser failed.", inner_exception=exc
753
+ ) from exc
754
+ return FunctionTool._normalize_parser_output(parsed)
755
+
756
+ @property
757
+ def _input_schema(self) -> dict[str, Any]:
758
+ """Get the input schema, generating it lazily if needed."""
759
+ if self._input_schema_cached is None:
760
+ if self.input_model is not None:
761
+ with suppress(Exception):
762
+ self.input_model.model_rebuild(force=False, raise_errors=False)
763
+ self._input_schema_cached = self.input_model.model_json_schema()
764
+ else:
765
+ self._input_schema_cached = {}
766
+ return self._input_schema_cached
767
+
768
+ @property
769
+ def _serialization_schema(self) -> dict[str, Any]:
770
+ """Get the typed model's callable-facing serialization schema."""
771
+ if self._serialization_schema_cached is None:
772
+ if self.input_model is not None:
773
+ with suppress(Exception):
774
+ self.input_model.model_rebuild(force=False, raise_errors=False)
775
+ by_alias = bool(self.input_model.model_config.get("serialize_by_alias", False))
776
+ try:
777
+ schema = self.input_model.model_json_schema(mode="serialization", by_alias=by_alias)
778
+ except Exception:
779
+ schema = {}
780
+ else:
781
+ policy = self._null_overlay_policy
782
+ if policy.tier is OverlayTier.FULL:
783
+ restorable = set(self.input_model.model_fields) - set(
784
+ policy.never_restore.get(self.input_model, frozenset())
785
+ )
786
+ elif policy.tier is OverlayTier.TOP_LEVEL:
787
+ restorable = set(policy.top_level_restorable)
788
+ else:
789
+ restorable = set()
790
+ policy_keys = policy.dump_keys.get(self.input_model, {})
791
+ required: set[str] = set()
792
+ for field_name, field_info in self.input_model.model_fields.items():
793
+ if by_alias:
794
+ dump_key = (
795
+ field_info.serialization_alias
796
+ if field_info.serialization_alias is not None
797
+ else field_info.alias
798
+ if field_info.alias is not None
799
+ else field_name
800
+ )
801
+ else:
802
+ dump_key = field_name
803
+ dump_key = policy_keys.get(field_name, dump_key)
804
+ if field_info.is_required() and field_name in restorable:
805
+ required.add(dump_key)
806
+ schema["required"] = sorted(required)
807
+ self._serialization_schema_cached = schema
808
+ else:
809
+ self._serialization_schema_cached = {}
810
+ return self._serialization_schema_cached
811
+
812
+ def parameters(self) -> dict[str, Any]:
813
+ """Create and cache the JSON schema for the function parameters."""
814
+ if self._cached_parameters is None:
815
+ self._cached_parameters = self._input_schema
816
+ return self._cached_parameters
817
+
818
+ @staticmethod
819
+ def _make_dumpable(value: Any) -> Any:
820
+ """Recursively convert a value to a JSON-dumpable form."""
821
+ if isinstance(value, list):
822
+ return [FunctionTool._make_dumpable(item) for item in value]
823
+ if isinstance(value, dict):
824
+ return {key: FunctionTool._make_dumpable(item) for key, item in value.items()}
825
+ if content := _coerce_content(value):
826
+ return content.to_dict(exclude={"raw_representation", "additional_properties"})
827
+ if isinstance(value, BaseModel):
828
+ return value.model_dump()
829
+ to_dict = getattr(value, "to_dict", None)
830
+ if callable(to_dict):
831
+ return to_dict()
832
+ text = getattr(value, "text", None)
833
+ if isinstance(text, str):
834
+ return text
835
+ return value
836
+
837
+ @staticmethod
838
+ def parse_result(result: Any) -> list[Content]:
839
+ """Convert a raw function return value to Chrys ``Content`` items."""
840
+ if result is None:
841
+ return [Content.from_text("")]
842
+ if isinstance(result, str):
843
+ return [Content.from_text(result)]
844
+ if content := _coerce_content(result):
845
+ return [content]
846
+ if isinstance(result, list) and any(_coerce_content(item) is not None for item in result):
847
+ parsed_items: list[Content] = []
848
+ for item in result:
849
+ if content := _coerce_content(item):
850
+ parsed_items.append(content)
851
+ else:
852
+ dumpable = FunctionTool._make_dumpable(item)
853
+ text = dumpable if isinstance(dumpable, str) else model_json(dumpable, default=str)
854
+ parsed_items.append(Content.from_text(text))
855
+ return parsed_items
856
+ dumpable = FunctionTool._make_dumpable(result)
857
+ if isinstance(dumpable, str):
858
+ return [Content.from_text(dumpable)]
859
+ return [Content.from_text(model_json(dumpable, default=str))]
860
+
861
+ @staticmethod
862
+ def _normalize_parser_output(parsed: Any) -> Any:
863
+ """Normalize custom parser output into Chrys Content while retaining existing Content lists."""
864
+ if isinstance(parsed, str):
865
+ return [Content.from_text(parsed)]
866
+ if content := _coerce_content(parsed):
867
+ return [content]
868
+ if not isinstance(parsed, list) or not parsed:
869
+ return parsed
870
+ if all(isinstance(item, Content) for item in parsed):
871
+ return parsed
872
+ if not any(_coerce_content(item) is not None for item in parsed):
873
+ return parsed
874
+
875
+ normalized: list[Content] = []
876
+ for item in parsed:
877
+ if content := _coerce_content(item):
878
+ normalized.append(content)
879
+ else:
880
+ dumpable = FunctionTool._make_dumpable(item)
881
+ text = dumpable if isinstance(dumpable, str) else model_json(dumpable, default=str)
882
+ normalized.append(Content.from_text(text))
883
+ return normalized
884
+
885
+ def to_json_schema_spec(self) -> dict[str, Any]:
886
+ """Convert this tool to the JSON Schema function specification format."""
887
+ return {
888
+ "type": "function",
889
+ "function": {
890
+ "name": self.name,
891
+ "description": self.description,
892
+ "parameters": self.parameters(),
893
+ },
894
+ }
895
+
896
+ def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) -> dict[str, Any]:
897
+ """Serialize the tool with input_model represented as its parameter schema."""
898
+ as_dict = super().to_dict(exclude=exclude, exclude_none=exclude_none)
899
+ if (exclude and "input_model" in exclude) or not self.input_model:
900
+ return as_dict
901
+ as_dict["input_model"] = self.parameters()
902
+ return as_dict
903
+
904
+
905
+ type ToolTypes = FunctionTool | Mapping[str, Any] | object
906
+
907
+
908
+ class FunctionInvocationConfiguration(TypedDict, total=False):
909
+ """Configuration for function invocation in chat clients."""
910
+
911
+ enabled: bool
912
+ max_iterations: int
913
+ max_function_calls: int | None
914
+ max_consecutive_errors_per_request: int
915
+ terminate_on_unknown_calls: bool
916
+ additional_tools: Sequence[FunctionTool]
917
+ include_detailed_errors: bool
918
+
919
+
920
+ @overload
921
+ def tool(
922
+ func: Callable[..., Any],
923
+ *,
924
+ name: str | None = None,
925
+ description: str | None = None,
926
+ schema: type[BaseModel] | Mapping[str, Any] | None = None,
927
+ kind: str | None = None,
928
+ max_invocations: int | None = None,
929
+ max_invocation_exceptions: int | None = None,
930
+ additional_properties: dict[str, Any] | None = None,
931
+ result_parser: Callable[[Any], str | list[Content]] | _SkipParsingSentinel | object | None = None,
932
+ ) -> FunctionTool: ...
933
+
934
+
935
+ @overload
936
+ def tool(
937
+ func: None = None,
938
+ *,
939
+ name: str | None = None,
940
+ description: str | None = None,
941
+ schema: type[BaseModel] | Mapping[str, Any] | None = None,
942
+ kind: str | None = None,
943
+ max_invocations: int | None = None,
944
+ max_invocation_exceptions: int | None = None,
945
+ additional_properties: dict[str, Any] | None = None,
946
+ result_parser: Callable[[Any], str | list[Content]] | _SkipParsingSentinel | object | None = None,
947
+ ) -> Callable[[Callable[..., Any]], FunctionTool]: ...
948
+
949
+
950
+ def tool(
951
+ func: Callable[..., Any] | None = None,
952
+ *,
953
+ name: str | None = None,
954
+ description: str | None = None,
955
+ schema: type[BaseModel] | Mapping[str, Any] | None = None,
956
+ kind: str | None = None,
957
+ max_invocations: int | None = None,
958
+ max_invocation_exceptions: int | None = None,
959
+ additional_properties: dict[str, Any] | None = None,
960
+ result_parser: Callable[[Any], str | list[Content]] | _SkipParsingSentinel | object | None = None,
961
+ ) -> FunctionTool | Callable[[Callable[..., Any]], FunctionTool]:
962
+ """Decorate a function into a Chrys ``FunctionTool``."""
963
+
964
+ def decorator(func: Callable[..., Any]) -> FunctionTool:
965
+ @wraps(func)
966
+ def wrapper(f: Callable[..., Any]) -> FunctionTool:
967
+ tool_name: str = name or getattr(f, "__name__", "unknown_function")
968
+ tool_desc: str = description or (f.__doc__ or "")
969
+ return FunctionTool(
970
+ name=tool_name,
971
+ description=tool_desc,
972
+ kind=kind,
973
+ max_invocations=max_invocations,
974
+ max_invocation_exceptions=max_invocation_exceptions,
975
+ additional_properties=additional_properties or {},
976
+ func=f,
977
+ input_model=schema,
978
+ result_parser=result_parser,
979
+ )
980
+
981
+ return wrapper(func)
982
+
983
+ return decorator(func) if func else decorator
984
+
985
+
986
+ def normalize_tools(
987
+ tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
988
+ ) -> list[ToolTypes]:
989
+ """Normalize tool inputs while preserving non-callable tool objects."""
990
+ if not tools:
991
+ return []
992
+
993
+ if isinstance(tools, (str, bytes, bytearray, Mapping)) or not isinstance(tools, Sequence):
994
+ tools = [tools]
995
+
996
+ normalized: list[ToolTypes] = []
997
+ for tool_item in tools:
998
+ if isinstance(tool_item, FunctionTool):
999
+ normalized.append(tool_item)
1000
+ continue
1001
+ if isinstance(tool_item, dict):
1002
+ normalized.append(tool_item)
1003
+ continue
1004
+ if callable(tool_item):
1005
+ normalized.append(tool(tool_item))
1006
+ continue
1007
+ collection_tools = getattr(tool_item, "tools", None)
1008
+ if isinstance(collection_tools, Iterable) and not isinstance(
1009
+ collection_tools, (str, bytes, bytearray, Mapping)
1010
+ ):
1011
+ normalized.extend(normalize_tools(list(collection_tools)))
1012
+ continue
1013
+ if isinstance(tool_item, Iterable) and not isinstance(tool_item, (str, bytes, bytearray, Mapping, BaseModel)):
1014
+ normalized.extend(normalize_tools(list(tool_item)))
1015
+ continue
1016
+ normalized.append(tool_item)
1017
+ return normalized
1018
+
1019
+
1020
+ def declared_tool_name(tool_item: ToolTypes) -> str | None:
1021
+ """Return the declared name of an owned function tool or function-tool mapping."""
1022
+ if isinstance(tool_item, FunctionTool):
1023
+ return tool_item.name
1024
+ if isinstance(tool_item, Mapping):
1025
+ function = tool_item.get("function")
1026
+ if not isinstance(function, Mapping):
1027
+ return None
1028
+ name = function.get("name")
1029
+ return name if isinstance(name, str) else None
1030
+ return None
1031
+
1032
+
1033
+ def _append_unique_tools(existing_tools: list[ToolTypes], new_tools: Sequence[ToolTypes]) -> list[ToolTypes]:
1034
+ """Append named tools with atomic duplicate-name validation.
1035
+
1036
+ The caller supplies a throwaway ``existing_tools`` copy when it needs
1037
+ all-or-nothing mutation of a live list. Re-adding the exact same object is
1038
+ a no-op; a different object with the same declared name is rejected.
1039
+ Nameless provider tool objects remain append-only.
1040
+ """
1041
+ seen_by_name: dict[str, ToolTypes] = {}
1042
+ for tool_item in existing_tools:
1043
+ if tool_name := declared_tool_name(tool_item):
1044
+ seen_by_name[tool_name] = tool_item
1045
+
1046
+ for tool_item in new_tools:
1047
+ tool_name = declared_tool_name(tool_item)
1048
+ if tool_name is None:
1049
+ existing_tools.append(tool_item)
1050
+ continue
1051
+
1052
+ existing_tool = seen_by_name.get(tool_name)
1053
+ if existing_tool is None:
1054
+ seen_by_name[tool_name] = tool_item
1055
+ existing_tools.append(tool_item)
1056
+ continue
1057
+ if existing_tool is tool_item:
1058
+ continue
1059
+ raise ValueError(f"Duplicate tool name '{tool_name}'. Tool names must be unique.")
1060
+
1061
+ return existing_tools