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/_types.py ADDED
@@ -0,0 +1,2598 @@
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 chat/agent types."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import contextlib
11
+ import json
12
+ import logging
13
+ from asyncio import iscoroutine
14
+ from collections.abc import (
15
+ AsyncIterable,
16
+ AsyncIterator,
17
+ Awaitable,
18
+ Callable,
19
+ Generator,
20
+ Iterable,
21
+ Mapping,
22
+ MutableMapping,
23
+ Sequence,
24
+ )
25
+ from copy import deepcopy
26
+ from inspect import isawaitable
27
+ from typing import (
28
+ TYPE_CHECKING,
29
+ Any,
30
+ ClassVar,
31
+ Final,
32
+ Generic,
33
+ Literal,
34
+ NewType,
35
+ ReadOnly,
36
+ TypeVar,
37
+ cast,
38
+ final,
39
+ overload,
40
+ )
41
+
42
+ from pydantic import BaseModel
43
+ from typing_extensions import TypedDict
44
+
45
+ from ._content import (
46
+ Content,
47
+ UsageDetails,
48
+ _combine_annotations,
49
+ _combine_raw_representations,
50
+ _parse_content_list,
51
+ _restore_compaction_annotation_in_additional_properties,
52
+ _text_for_join,
53
+ add_usage_details,
54
+ normalize_stream_usage,
55
+ )
56
+ from ._serialization import SerializationMixin
57
+ from ._tool_expansion import _get_tool_expander
58
+ from .exceptions import AdditionItemMismatch, ContentError
59
+ from .identity import ContentList
60
+
61
+ if TYPE_CHECKING:
62
+ from .tools import FunctionTool as _OwnedFunctionTool
63
+ else:
64
+ _OwnedFunctionTool = object
65
+
66
+ type ToolTypes = _OwnedFunctionTool | Mapping[str, Any] | object
67
+
68
+ logger = logging.getLogger(__name__)
69
+
70
+ # Identity-only sentinel carried by synthetic stream-progress updates. It is
71
+ # deliberately opaque: rejected provider payloads must never ride heartbeat
72
+ # updates across response-validation boundaries.
73
+ # Keep this marker and the matching
74
+ # ``ChatResponseUpdate.transport_heartbeat`` / ``_process_update`` handling
75
+ # together when changing streaming types, so heartbeats remain distinguishable.
76
+ _STREAM_HEARTBEAT_MARKER = object()
77
+ RETRY_BOUNDARY_UPDATE_KEY = "_chrys_retry_boundary"
78
+
79
+
80
+ # region Content Parsing Utilities
81
+
82
+
83
+ # endregion
84
+
85
+ # region Constants and types
86
+
87
+
88
+ _UNSET_USAGE: Final[UsageDetails] = cast("UsageDetails", object())
89
+ """Sentinel distinguishing an omitted ``latest_usage_details`` from an explicit None.
90
+
91
+ An omitted argument means "single-call response" and inherits ``usage_details``.
92
+ An explicit ``None`` means the final call's usage is genuinely unavailable and
93
+ must NOT fall back to the aggregate — consumers such as after-run
94
+ force-compression would otherwise mistake turn-total billing usage for the
95
+ final call's context occupancy.
96
+ """
97
+
98
+
99
+ # region Content Class
100
+
101
+
102
+ # endregion
103
+
104
+
105
+ # region Chat Response constants
106
+
107
+ RoleLiteral = Literal["system", "user", "assistant", "tool"]
108
+ """Literal type for known role values. Accepts any string for extensibility."""
109
+
110
+ Role = NewType("Role", str)
111
+ """Type for chat message roles. Use string values directly (e.g., "user", "assistant").
112
+
113
+ Known values: "system", "user", "assistant", "tool"
114
+
115
+ Examples:
116
+ .. code-block:: python
117
+
118
+ from chrys.kernel._types import Message
119
+
120
+ # Use string values directly
121
+ user_msg = Message("user", ["Hello"])
122
+ assistant_msg = Message("assistant", ["Hi there!"])
123
+
124
+ # Custom roles are also supported
125
+ custom_msg = Message("custom", ["Custom role message"])
126
+
127
+ # Compare roles directly as strings
128
+ if user_msg.role == "user":
129
+ print("This is a user message")
130
+ """
131
+
132
+ FinishReasonLiteral = Literal["stop", "length", "tool_calls", "content_filter"]
133
+ """Literal type for known finish reason values. Accepts any string for extensibility."""
134
+
135
+ FinishReason = NewType("FinishReason", str)
136
+ """Type for chat response finish reasons. Use string values directly.
137
+
138
+ Known values:
139
+ - "stop": Normal completion
140
+ - "length": Max tokens reached
141
+ - "tool_calls": Tool calls triggered
142
+ - "content_filter": Content filter triggered
143
+
144
+ Examples:
145
+ .. code-block:: python
146
+
147
+ from chrys.kernel._types import ChatResponse
148
+
149
+ response = ChatResponse(messages=[...], finish_reason="stop")
150
+
151
+ # Check finish reason directly as string
152
+ if response.finish_reason == "stop":
153
+ print("Response completed normally")
154
+ elif response.finish_reason == "tool_calls":
155
+ print("Tool calls need to be processed")
156
+ """
157
+
158
+
159
+ # region Message
160
+
161
+
162
+ class Message(SerializationMixin):
163
+ """Represents a chat message.
164
+
165
+ Attributes:
166
+ role: The role of the author of the message.
167
+ contents: The chat message content items.
168
+ author_name: The name of the author of the message.
169
+ message_id: The ID of the chat message.
170
+ additional_properties: Any additional properties associated with the chat message.
171
+ Additional properties are used internally, they are not sent to services.
172
+ raw_representation: The raw representation of the chat message from an underlying implementation.
173
+
174
+ Examples:
175
+ .. code-block:: python
176
+
177
+ from chrys.kernel._content import Content
178
+ from chrys.kernel._types import Message
179
+
180
+ # Create a message with text content
181
+ user_msg = Message("user", ["What's the weather?"])
182
+ print(user_msg.text) # "What's the weather?"
183
+
184
+ # Create a system message
185
+ system_msg = Message("system", ["You are a helpful assistant."])
186
+
187
+ # Create a message with mixed content types
188
+ assistant_msg = Message(
189
+ "assistant",
190
+ ["The weather is sunny!", Content.from_image_uri("https://...")],
191
+ )
192
+ print(assistant_msg.text) # "The weather is sunny!"
193
+
194
+ # Serialization - to_dict and from_dict
195
+ msg_dict = user_msg.to_dict()
196
+ # {'type': 'chat_message', 'role': 'user',
197
+ # 'contents': [{'type': 'text', 'text': "What's the weather?"}], 'additional_properties': {}}
198
+ restored_msg = Message.from_dict(msg_dict)
199
+ print(restored_msg.text) # "What's the weather?"
200
+
201
+ # Serialization - to_json and from_json
202
+ msg_json = user_msg.to_json()
203
+ # '{"type": "chat_message", "role": "user", "contents": [...], ...}'
204
+ restored_from_json = Message.from_json(msg_json)
205
+ print(restored_from_json.role) # "user"
206
+
207
+ """
208
+
209
+ DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
210
+
211
+ def __init__(
212
+ self,
213
+ role: RoleLiteral | str,
214
+ contents: Sequence[Content | str | Mapping[str, Any]] | None = None,
215
+ *,
216
+ author_name: str | None = None,
217
+ message_id: str | None = None,
218
+ additional_properties: MutableMapping[str, Any] | None = None,
219
+ raw_representation: Any | None = None,
220
+ ) -> None:
221
+ """Initialize Message.
222
+
223
+ Args:
224
+ role: The role of the author of the message (e.g., "user", "assistant", "system", "tool").
225
+ contents: A sequence of content items. Can be Content objects, strings (auto-converted
226
+ to TextContent), or dicts (parsed via Content.from_dict). Defaults to empty list.
227
+
228
+ Keyword Args:
229
+ author_name: Optional name of the author of the message.
230
+ message_id: Optional ID of the chat message.
231
+ additional_properties: Optional additional properties associated with the chat message.
232
+ Additional properties are used internally, they are not sent to services.
233
+ raw_representation: Optional raw representation of the chat message.
234
+ """
235
+ parsed_contents = [] if contents is None else _parse_content_list(contents)
236
+
237
+ self.role: str = role
238
+ self.contents = parsed_contents
239
+ self.author_name = author_name
240
+ self.message_id = message_id
241
+ self.additional_properties = (
242
+ _restore_compaction_annotation_in_additional_properties(additional_properties) or {}
243
+ )
244
+ self.raw_representation = raw_representation
245
+ # Runtime-only assembly provenance. The tool loop sets this when an
246
+ # update for this logical message had echoed content removed before
247
+ # assembly; private attributes are excluded from serialization.
248
+ self._chrys_echo_content_stripped = False
249
+
250
+ @property
251
+ def contents(self) -> ContentList:
252
+ """The chat message content items, always a ``ContentList``.
253
+
254
+ The setter coerces any assigned sequence so captors can hold
255
+ ``weakref.ref(msg.contents)`` (built-in ``list`` is not
256
+ weakref-able). It backs onto ``__dict__["contents"]`` — the PUBLIC
257
+ key — because ``to_dict`` enumerates ``__dict__`` dropping
258
+ ``_``-prefixed keys and the generic ``__deepcopy__`` copies
259
+ ``__dict__`` entries via ``object.__setattr__`` (which follows the
260
+ descriptor protocol back into this setter). ``ContentList`` input is
261
+ stored as-is, preserving list identity for shared-list twins and
262
+ deepcopy re-coercion; a plain list is copied into a fresh
263
+ ``ContentList``, so a caller's plain-list alias no longer aliases
264
+ ``msg.contents`` after assignment.
265
+ """
266
+ return self.__dict__["contents"]
267
+
268
+ @contents.setter
269
+ def contents(self, value: Sequence[Content]) -> None:
270
+ if not isinstance(value, ContentList):
271
+ value = ContentList(value)
272
+ self.__dict__["contents"] = value
273
+
274
+ @property
275
+ def text(self) -> str:
276
+ """Returns the text content of the message.
277
+
278
+ Remarks:
279
+ This property concatenates the text of all TextContent objects in Content.
280
+ """
281
+ return " ".join(_text_for_join(content) for content in self.contents if content.type == "text")
282
+
283
+
284
+ AgentRunInputs = str | Content | Message | Sequence[str | Content | Message]
285
+
286
+
287
+ def normalize_messages(
288
+ messages: AgentRunInputs | None = None,
289
+ ) -> list[Message]:
290
+ """Normalize message inputs to a list of Message objects.
291
+
292
+ Args:
293
+ messages: The input messages in various supported formats. Can be:
294
+ - None (returns empty list)
295
+ - A string (converted to a user message)
296
+ - A Content object (wrapped in a user Message)
297
+ - A Message object
298
+ - A sequence containing any mix of the above
299
+
300
+ Returns:
301
+ A list of Message objects.
302
+ """
303
+ if messages is None:
304
+ return []
305
+
306
+ if isinstance(messages, str):
307
+ return [Message("user", [messages])]
308
+
309
+ if isinstance(messages, Content):
310
+ return [Message("user", [messages])]
311
+
312
+ if isinstance(messages, Message):
313
+ return [messages]
314
+
315
+ result: list[Message] = []
316
+ for msg in messages:
317
+ if isinstance(msg, (str, Content)):
318
+ result.append(Message("user", [msg]))
319
+ else:
320
+ result.append(msg)
321
+ return result
322
+
323
+
324
+ def prepend_instructions_to_messages(
325
+ messages: list[Message],
326
+ instructions: str | Sequence[str] | None,
327
+ role: RoleLiteral | str = "system",
328
+ ) -> list[Message]:
329
+ """Prepend instructions to a list of messages with a specified role.
330
+
331
+ This is a helper method for chat clients that need to add instructions
332
+ from options as messages. Different providers support different roles for
333
+ instructions (e.g., OpenAI uses "system", some providers might use "user").
334
+
335
+ Args:
336
+ messages: The existing list of Message objects.
337
+ instructions: The instructions to prepend. Can be a single string or a sequence of strings.
338
+ role: The role to use for the instruction messages. Defaults to "system".
339
+
340
+ Returns:
341
+ A new list with instruction messages prepended.
342
+
343
+ Examples:
344
+ .. code-block:: python
345
+
346
+ from chrys.kernel._types import prepend_instructions_to_messages, Message
347
+
348
+ messages = [Message("user", ["Hello"])]
349
+ instructions = "You are a helpful assistant"
350
+
351
+ # Prepend as system message (default)
352
+ messages_with_instructions = prepend_instructions_to_messages(messages, instructions)
353
+
354
+ # Or use a different role
355
+ messages_with_user_instructions = prepend_instructions_to_messages(messages, instructions, role="user")
356
+ """
357
+ if instructions is None:
358
+ return messages
359
+
360
+ if isinstance(instructions, str):
361
+ instructions = [instructions]
362
+
363
+ # Skip instructions that are already present as leading messages with the
364
+ # same role and text. This prevents duplicate system messages when
365
+ # instructions are injected by multiple layers (e.g. Agent + chat client).
366
+ deduplicated: list[str] = []
367
+ for idx, instr in enumerate(instructions):
368
+ if idx < len(messages) and messages[idx].role == role and messages[idx].text == instr:
369
+ continue
370
+ deduplicated.append(instr)
371
+
372
+ if not deduplicated:
373
+ return messages
374
+
375
+ instruction_messages = [Message(role, [instr]) for instr in deduplicated]
376
+ return [*instruction_messages, *messages]
377
+
378
+
379
+ # region ChatResponse
380
+
381
+
382
+ def _is_transport_heartbeat_update(update: ChatResponseUpdate | AgentResponseUpdate) -> bool:
383
+ """Return whether *update* reports stream progress without response semantics."""
384
+ if isinstance(update, ChatResponseUpdate):
385
+ return update.is_transport_heartbeat
386
+
387
+ raw_update = update.raw_representation
388
+ return (
389
+ isinstance(raw_update, ChatResponseUpdate)
390
+ and raw_update.is_transport_heartbeat
391
+ and not update.contents
392
+ and update.role is None
393
+ and update.response_id is None
394
+ and update.message_id is None
395
+ and update.created_at is None
396
+ and update.finish_reason is None
397
+ and update.continuation_token is None
398
+ and not update.additional_properties
399
+ )
400
+
401
+
402
+ def is_retry_boundary_update(update: ChatResponseUpdate | AgentResponseUpdate) -> bool:
403
+ """Return whether *update* marks a fresh wire-attempt boundary."""
404
+ return (
405
+ not update.contents
406
+ and update.additional_properties is not None
407
+ and update.additional_properties.get(RETRY_BOUNDARY_UPDATE_KEY) is True
408
+ )
409
+
410
+
411
+ def _process_update(
412
+ response: ChatResponse | AgentResponse,
413
+ update: ChatResponseUpdate | AgentResponseUpdate,
414
+ seen_contents: dict[int, Content],
415
+ ) -> None:
416
+ """Processes a single update and modifies the response in place.
417
+
418
+ ``seen_contents`` is one assembly pass's identity memo (each
419
+ ``from_updates``-style caller seeds an empty dict and threads it through
420
+ every update): a content OBJECT the pass already incorporated is a
421
+ duplicate emission, not a new fragment, and is skipped. Without this, a
422
+ repeated function-call object adjacent to itself would SELF-MERGE —
423
+ concatenating its own argument string into unparseable JSON — turning a
424
+ valid call into an argument-parsing failure; the blocking path already
425
+ collapses identity duplicates within one response to a single copy at
426
+ landing, and assembly-scoped dedup gives streamed responses the same
427
+ semantics (per attempt, so a validation retry re-emitting an object a
428
+ REJECTED attempt streamed is unaffected — each assembly seeds its own
429
+ memo). The memo maps id -> content precisely to RETAIN each object for
430
+ the pass: generator-fed assembly holds no other reference to consumed
431
+ fragments, and a merged-away fragment released mid-pass would let
432
+ CPython hand a later fresh fragment the same id — silently dropping it.
433
+ Usage contents are exempt: they never land in messages and their
434
+ accumulation is value-based, not object-based.
435
+ """
436
+ if _is_transport_heartbeat_update(update) or is_retry_boundary_update(update):
437
+ return
438
+ is_new_message = False
439
+ if (
440
+ not response.messages
441
+ or (
442
+ update.message_id
443
+ and response.messages[-1].message_id
444
+ and response.messages[-1].message_id != update.message_id
445
+ )
446
+ or (update.role and response.messages[-1].role != update.role)
447
+ ):
448
+ is_new_message = True
449
+
450
+ message = Message("assistant", []) if is_new_message else response.messages[-1]
451
+ if isinstance(update, ChatResponseUpdate) and update._chrys_echo_content_stripped:
452
+ # Preserve which logical message — not merely which model call —
453
+ # lost echo content. The loop can then remove only a shell that
454
+ # remained empty after its accepted stream was assembled.
455
+ message._chrys_echo_content_stripped = True
456
+ skipped_duplicate_content = False
457
+ # Incorporate the update's properties into the message.
458
+ if update.author_name is not None:
459
+ message.author_name = update.author_name
460
+ if update.role is not None:
461
+ message.role = update.role
462
+ if update.message_id:
463
+ message.message_id = update.message_id
464
+ for content in update.contents:
465
+ # Fast path: get type attribute (most content will have it)
466
+ content_type = getattr(content, "type", None)
467
+ # Slow path: only check for dict if type is None
468
+ if content_type is None and isinstance(content, (dict, MutableMapping)):
469
+ try:
470
+ content = Content.from_dict(dict(content.items()))
471
+ content_type = content.type
472
+ except ContentError as exc:
473
+ logger.warning(f"Skipping unknown content type or invalid content: {exc}")
474
+ continue
475
+ if content_type != "usage":
476
+ if id(content) in seen_contents:
477
+ skipped_duplicate_content = True
478
+ continue
479
+ seen_contents[id(content)] = content
480
+ match content_type:
481
+ # mypy doesn't narrow type based on match/case, but we know these are FunctionCallContents
482
+ case "function_call" if message.contents and message.contents[-1].type == "function_call":
483
+ try:
484
+ message.contents[-1] += content # type: ignore[operator]
485
+ except AdditionItemMismatch, ContentError:
486
+ message.contents.append(content)
487
+ case "usage":
488
+ if response.usage_details is None:
489
+ response.usage_details = UsageDetails()
490
+ # mypy doesn't narrow type based on match/case, but we know this is UsageContent
491
+ response.usage_details = add_usage_details(response.usage_details, content.usage_details) # type: ignore[arg-type]
492
+ usage_details = content.usage_details or {}
493
+ response.latest_usage_details = normalize_stream_usage(
494
+ [response.latest_usage_details or {}, usage_details]
495
+ )
496
+ case _:
497
+ message.contents.append(content)
498
+ if is_new_message and not (skipped_duplicate_content and not message.contents):
499
+ # A duplicate object emitted under a fresh message boundary belongs
500
+ # to the message that already incorporated it. Do not leave behind an
501
+ # empty shell whose message_id/role makes response validation reject
502
+ # an otherwise valid streamed response. Preserve the existing shape
503
+ # for genuinely empty and usage-only updates.
504
+ response.messages.append(message)
505
+ # Incorporate the update's properties into the response.
506
+ if update.response_id:
507
+ response.response_id = update.response_id
508
+ if update.created_at is not None:
509
+ response.created_at = update.created_at
510
+ if update.additional_properties is not None:
511
+ response.additional_properties.update(update.additional_properties)
512
+ if response.raw_representation is None:
513
+ response.raw_representation = []
514
+ if not isinstance(response.raw_representation, list):
515
+ response.raw_representation = [response.raw_representation]
516
+ raw_representation_value = cast(Any, response.raw_representation)
517
+ raw_representation_list = cast(list[Any], raw_representation_value)
518
+ raw_representation_list.append(update.raw_representation)
519
+ if isinstance(response, ChatResponse) and isinstance(update, ChatResponseUpdate):
520
+ if update.conversation_id is not None:
521
+ response.conversation_id = update.conversation_id
522
+ if update.finish_reason is not None:
523
+ response.finish_reason = update.finish_reason
524
+ if update.model is not None:
525
+ response.model = update.model
526
+ if (
527
+ isinstance(response, AgentResponse)
528
+ and isinstance(update, AgentResponseUpdate)
529
+ and update.finish_reason is not None
530
+ ):
531
+ response.finish_reason = update.finish_reason
532
+ response.continuation_token = update.continuation_token
533
+
534
+
535
+ def _coalesce_text_content(contents: list[Content], type_str: Literal["text", "text_reasoning"]) -> None:
536
+ """Take any subsequence Text or TextReasoningContent items and coalesce them into a single item."""
537
+ if not contents:
538
+ return
539
+ coalesced_contents: list[Content] = []
540
+ first_new_content: Any | None = None
541
+ for content in contents:
542
+ if content.type == type_str:
543
+ if first_new_content is None:
544
+ first_new_content = deepcopy(content)
545
+ else:
546
+ try:
547
+ first_new_content += content
548
+ except AdditionItemMismatch:
549
+ # Different IDs means a new logical segment; flush the current one
550
+ coalesced_contents.append(first_new_content)
551
+ first_new_content = deepcopy(content)
552
+ else:
553
+ # skip this content, it is not of the right type
554
+ # so write the existing one to the list and start a new one,
555
+ # once the right type is found again
556
+ if first_new_content:
557
+ coalesced_contents.append(first_new_content)
558
+ first_new_content = None
559
+ # but keep the other content in the new list
560
+ coalesced_contents.append(content)
561
+ if first_new_content:
562
+ coalesced_contents.append(first_new_content)
563
+ contents.clear()
564
+ contents.extend(coalesced_contents)
565
+
566
+
567
+ def _content_items_text(items: Any) -> str | None:
568
+ """Return concatenated text when a content item list only contains text."""
569
+ if not isinstance(items, list):
570
+ return None
571
+ text_parts: list[str] = []
572
+ content_items = cast(list[object], items)
573
+ for item in content_items:
574
+ if not isinstance(item, Content) or item.type != "text":
575
+ return None
576
+ text_parts.append(item.text or "")
577
+ return "".join(text_parts)
578
+
579
+
580
+ def _merge_content_item_lists(existing: Any, incoming: Any) -> Any:
581
+ """Merge streamed nested content lists, replacing deltas with a later full value when present."""
582
+ if incoming is None:
583
+ return existing
584
+ if existing is None:
585
+ return deepcopy(incoming)
586
+
587
+ existing_text = _content_items_text(existing)
588
+ incoming_text = _content_items_text(incoming)
589
+ if existing_text is not None and incoming_text is not None:
590
+ if incoming_text.startswith(existing_text):
591
+ return deepcopy(incoming)
592
+ if existing_text.startswith(incoming_text):
593
+ return existing
594
+
595
+ existing_items = cast(list[Content], existing)
596
+ merged = deepcopy(existing_items[0])
597
+ merged.text = existing_text + incoming_text
598
+ return [merged]
599
+
600
+ if isinstance(existing, list) and isinstance(incoming, list):
601
+ existing_list = cast(list[object], existing)
602
+ incoming_list = cast(list[object], incoming)
603
+ return [*existing_list, *deepcopy(incoming_list)]
604
+ return deepcopy(incoming)
605
+
606
+
607
+ def _merge_code_interpreter_content(existing: Content, incoming: Content) -> None:
608
+ """Merge two code interpreter content items for the same logical call."""
609
+ existing.inputs = _merge_content_item_lists(existing.inputs, incoming.inputs)
610
+ existing.outputs = _merge_content_item_lists(existing.outputs, incoming.outputs)
611
+ existing.annotations = _combine_annotations(existing.annotations, incoming.annotations)
612
+ existing.additional_properties = {**existing.additional_properties, **incoming.additional_properties}
613
+ existing.raw_representation = _combine_raw_representations(existing.raw_representation, incoming.raw_representation)
614
+
615
+
616
+ def _code_interpreter_key(content: Content) -> tuple[str, str] | None:
617
+ """Return the aggregation key for code interpreter call/result content."""
618
+ if content.type not in {"code_interpreter_tool_call", "code_interpreter_tool_result"}:
619
+ return None
620
+ call_id = content.call_id or content.additional_properties.get("item_id")
621
+ if not isinstance(call_id, str) or not call_id:
622
+ return None
623
+ return content.type, call_id
624
+
625
+
626
+ def _coalesce_code_interpreter_content(contents: list[Content]) -> None:
627
+ """Coalesce streaming code interpreter chunks by call id."""
628
+ if not contents:
629
+ return
630
+
631
+ coalesced_contents: list[Content] = []
632
+ seen: dict[tuple[str, str], Content] = {}
633
+ for content in contents:
634
+ key = _code_interpreter_key(content)
635
+ if key is None:
636
+ coalesced_contents.append(content)
637
+ continue
638
+
639
+ existing = seen.get(key)
640
+ if existing is None:
641
+ copied = deepcopy(content)
642
+ seen[key] = copied
643
+ coalesced_contents.append(copied)
644
+ continue
645
+
646
+ _merge_code_interpreter_content(existing, content)
647
+
648
+ contents.clear()
649
+ contents.extend(coalesced_contents)
650
+
651
+
652
+ def _finalize_response(response: ChatResponse | AgentResponse) -> None:
653
+ """Finalizes the response by performing any necessary post-processing."""
654
+ # A single response stream may carry cumulative usage snapshots. Keep
655
+ # only the latest per-key values; a surrounding tool loop explicitly
656
+ # restores the aggregate across its distinct model calls afterwards.
657
+ if response.latest_usage_details is not None:
658
+ response.usage_details = response.latest_usage_details
659
+ for msg in response.messages:
660
+ _coalesce_text_content(msg.contents, "text")
661
+ _coalesce_text_content(msg.contents, "text_reasoning")
662
+ _coalesce_code_interpreter_content(msg.contents)
663
+
664
+
665
+ # region ContinuationToken
666
+
667
+
668
+ class ContinuationToken(TypedDict):
669
+ """Opaque token for resuming long-running agent operations.
670
+
671
+ A JSON-serializable dict used to poll for completion or resume a
672
+ streaming response. Presence on a response indicates the operation
673
+ is still in progress; ``None`` means the operation is complete.
674
+
675
+ Each provider subclasses this with its own fields; consumers should
676
+ treat the token as opaque and simply pass it back to the same agent.
677
+
678
+ Examples:
679
+ .. code-block:: python
680
+
681
+ import json
682
+
683
+ # Persist token across restarts
684
+ token_json = json.dumps(response.continuation_token)
685
+
686
+ # Restore and resume
687
+ token = json.loads(token_json)
688
+ response = await agent.run(
689
+ session=session,
690
+ options={"continuation_token": token},
691
+ )
692
+ """
693
+
694
+
695
+ # endregion
696
+
697
+
698
+ def _parse_structured_response_value(text: str, response_format: Any | None) -> Any | None:
699
+ if response_format is None:
700
+ return None
701
+ if not text:
702
+ return None
703
+ if isinstance(response_format, type) and issubclass(response_format, BaseModel):
704
+ return response_format.model_validate_json(text)
705
+ if isinstance(response_format, Mapping):
706
+ try:
707
+ return json.loads(text)
708
+ except json.JSONDecodeError as exc:
709
+ raise ValueError(f"Response text is not valid JSON: {exc}") from exc
710
+ logger.warning(
711
+ "Unable to parse structured response value, use either a Pydantic model or a dict defining the schema, "
712
+ "received response_format type: %s",
713
+ type(response_format), # type: ignore[reportUnknownArgumentType]
714
+ )
715
+ return None
716
+
717
+
718
+ def _last_non_empty_assistant_message_text(messages: Sequence[Message]) -> str:
719
+ """Return structured-output text from the final substantive assistant message."""
720
+ for message in reversed(messages):
721
+ if message.role != "assistant":
722
+ continue
723
+ text = "".join((content.text or "") for content in message.contents if content.type == "text")
724
+ if text.strip():
725
+ return text
726
+ return ""
727
+
728
+
729
+ _T = TypeVar("_T")
730
+ ChatResponseT = TypeVar("ChatResponseT", bound="ChatResponse")
731
+ ToolModeT = TypeVar("ToolModeT", bound="ToolMode")
732
+ AgentResponseT = TypeVar("AgentResponseT", bound="AgentResponse")
733
+ ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel | None, default=None, covariant=True)
734
+ ResponseModelBoundT = TypeVar("ResponseModelBoundT", bound=BaseModel)
735
+ StructuredResponseFormat = type[BaseModel] | Mapping[str, Any] | None
736
+
737
+ CreatedAtT = str # Use a datetimeoffset type? Or a more specific type like datetime.datetime?
738
+
739
+
740
+ class ChatResponse(SerializationMixin, Generic[ResponseModelT]):
741
+ """Represents the response to a chat request.
742
+
743
+ Attributes:
744
+ messages: The list of chat messages in the response.
745
+ response_id: The ID of the chat response.
746
+ conversation_id: An identifier for the state of the conversation.
747
+ model: The model used in the creation of the chat response.
748
+ created_at: A timestamp for the chat response.
749
+ finish_reason: The reason for the chat response.
750
+ usage_details: Aggregate usage across all model calls represented by the response.
751
+ latest_usage_details: Usage for the most recent model call only.
752
+ structured_output: The structured output of the chat response, if applicable.
753
+ additional_properties: Any additional properties associated with the chat response.
754
+ raw_representation: The raw representation of the chat response from an underlying implementation.
755
+
756
+ Note:
757
+ The `author_name` attribute is available on the `Message` objects inside `messages`,
758
+ not on the `ChatResponse` itself. Use `response.messages[0].author_name` to access
759
+ the author name of individual messages.
760
+
761
+ Examples:
762
+ .. code-block:: python
763
+
764
+ from chrys.kernel._types import ChatResponse, Message
765
+
766
+ # Create a response with messages
767
+ msg = Message("assistant", ["The weather is sunny."])
768
+ response = ChatResponse(
769
+ messages=[msg],
770
+ finish_reason="stop",
771
+ model="gpt-4",
772
+ )
773
+ print(response.text) # "The weather is sunny."
774
+
775
+ # Combine streaming updates
776
+ updates = [...] # List of ChatResponseUpdate objects
777
+ response = ChatResponse.from_updates(updates)
778
+
779
+ # Serialization - to_dict and from_dict
780
+ response_dict = response.to_dict()
781
+ # {'type': 'chat_response', 'messages': [...], 'model': 'gpt-4', 'finish_reason': 'stop'}
782
+ restored_response = ChatResponse.from_dict(response_dict)
783
+ print(restored_response.model) # "gpt-4"
784
+
785
+ # Serialization - to_json and from_json
786
+ response_json = response.to_json()
787
+ # '{"type": "chat_response", "messages": [...], "model": "gpt-4", ...}'
788
+ restored_from_json = ChatResponse.from_json(response_json)
789
+ print(restored_from_json.text) # "The weather is sunny."
790
+ """
791
+
792
+ DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation", "additional_properties"}
793
+ _INTERNAL_CONVERSATION_ID_KEY: ClassVar[str] = "_chrys_internal_conversation_id"
794
+
795
+ def __init__(
796
+ self,
797
+ *,
798
+ messages: Message | Sequence[Message] | None = None,
799
+ response_id: str | None = None,
800
+ conversation_id: str | None = None,
801
+ model: str | None = None,
802
+ created_at: CreatedAtT | None = None,
803
+ finish_reason: FinishReasonLiteral | FinishReason | None = None,
804
+ usage_details: UsageDetails | None = None,
805
+ latest_usage_details: UsageDetails | None = _UNSET_USAGE,
806
+ value: ResponseModelT | None = None,
807
+ response_format: StructuredResponseFormat = None,
808
+ continuation_token: ContinuationToken | None = None,
809
+ additional_properties: dict[str, Any] | None = None,
810
+ raw_representation: Any | None = None,
811
+ ) -> None:
812
+ """Initializes a ChatResponse with the provided parameters.
813
+
814
+ Keyword Args:
815
+ messages: A single Message or sequence of Message objects to include in the response.
816
+ response_id: Optional ID of the chat response.
817
+ conversation_id: Optional identifier for the state of the conversation.
818
+ model: Optional model used in the creation of the chat response.
819
+ created_at: Optional timestamp for when the response was created.
820
+ finish_reason: Optional reason for the chat response (e.g., "stop", "length", "tool_calls").
821
+ usage_details: Optional aggregate usage for the chat response.
822
+ latest_usage_details: Optional usage for the most recent model call.
823
+ When omitted, defaults to ``usage_details`` (single-call response).
824
+ Pass ``None`` explicitly when the final call's usage is unknown;
825
+ it is preserved rather than falling back to the aggregate.
826
+ value: Optional value of the structured output.
827
+ response_format: Optional response format for the chat response.
828
+ continuation_token: Optional token for resuming a long-running background operation.
829
+ When present, indicates the operation is still in progress.
830
+ additional_properties: Optional additional properties associated with the chat response.
831
+ raw_representation: Optional raw representation of the chat response from an underlying implementation.
832
+ """
833
+ if messages is None:
834
+ self.messages: list[Message] = []
835
+ elif isinstance(messages, Message):
836
+ self.messages = [messages]
837
+ else:
838
+ # Handle both Message objects and dicts (for from_dict support)
839
+ processed_messages: list[Message] = []
840
+ for msg in messages:
841
+ if isinstance(msg, Message):
842
+ processed_messages.append(msg)
843
+ elif isinstance(msg, dict):
844
+ processed_messages.append(Message.from_dict(msg))
845
+ else:
846
+ processed_messages.append(msg)
847
+ self.messages = processed_messages
848
+ self.response_id = response_id
849
+ self.conversation_id = conversation_id
850
+ self.model = model
851
+ self.created_at = created_at
852
+ self.finish_reason = finish_reason
853
+ self.usage_details = usage_details
854
+ # ``usage_details`` may aggregate multiple tool-loop iterations for
855
+ # billing; this value represents the final request's context occupancy.
856
+ # Public attribute so serialization round-trips preserve both views.
857
+ self.latest_usage_details: UsageDetails | None = (
858
+ usage_details if latest_usage_details is _UNSET_USAGE else latest_usage_details
859
+ )
860
+ self._value: ResponseModelT | None = value
861
+ self._response_format: StructuredResponseFormat = response_format
862
+ self._value_parsed: bool = value is not None
863
+ self.additional_properties = (
864
+ _restore_compaction_annotation_in_additional_properties(additional_properties) or {}
865
+ )
866
+ self.continuation_token = continuation_token
867
+ self.raw_representation: Any | list[Any] | None = raw_representation
868
+ # Runtime-only marker set by the tool loop's exhaustion tail when it
869
+ # stripped content that a service-stored conversation still holds:
870
+ # continuation handles derived from this response must not be
871
+ # propagated or restored. Private attributes are excluded from
872
+ # serialization.
873
+ self._chrys_service_state_invalidated = False
874
+
875
+ def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) -> dict[str, Any]:
876
+ """Serialize, preserving an explicitly-unavailable latest usage.
877
+
878
+ ``exclude_none`` would drop ``latest_usage_details=None`` and the
879
+ constructor default would then substitute the billing aggregate on
880
+ deserialization. ``None`` is a meaningful value here (final call's
881
+ usage unknown), so it is kept as an explicit ``null`` whenever an
882
+ aggregate exists to be wrongly inherited.
883
+ """
884
+ result = super().to_dict(exclude=exclude, exclude_none=exclude_none)
885
+ if (
886
+ exclude_none
887
+ and self.latest_usage_details is None
888
+ and "latest_usage_details" not in (exclude or ())
889
+ and "usage_details" in result
890
+ ):
891
+ result["latest_usage_details"] = None
892
+ return result
893
+
894
+ def mark_internal_conversation_id(self) -> None:
895
+ """Mark the current conversation_id as internal control-flow state."""
896
+ self.additional_properties[self._INTERNAL_CONVERSATION_ID_KEY] = True
897
+
898
+ def clear_internal_conversation_id(self) -> None:
899
+ """Remove the internal conversation-id marker."""
900
+ self.additional_properties.pop(self._INTERNAL_CONVERSATION_ID_KEY, None)
901
+
902
+ def has_internal_conversation_id(self) -> bool:
903
+ """Return whether conversation_id is internal control-flow state."""
904
+ return bool(self.additional_properties.get(self._INTERNAL_CONVERSATION_ID_KEY, False))
905
+
906
+ @overload
907
+ @classmethod
908
+ def from_updates(
909
+ cls: type[ChatResponse[Any]],
910
+ updates: Sequence[ChatResponseUpdate],
911
+ *,
912
+ output_format_type: type[ResponseModelBoundT],
913
+ ) -> ChatResponse[ResponseModelBoundT]: ...
914
+
915
+ @overload
916
+ @classmethod
917
+ def from_updates(
918
+ cls: type[ChatResponse[Any]],
919
+ updates: Sequence[ChatResponseUpdate],
920
+ *,
921
+ output_format_type: Mapping[str, Any],
922
+ ) -> ChatResponse[Any]: ...
923
+
924
+ @overload
925
+ @classmethod
926
+ def from_updates(
927
+ cls: type[ChatResponse[Any]],
928
+ updates: Sequence[ChatResponseUpdate],
929
+ *,
930
+ output_format_type: None = None,
931
+ ) -> ChatResponse[Any]: ...
932
+
933
+ @classmethod
934
+ def from_updates(
935
+ cls: type[ChatResponseT],
936
+ updates: Sequence[ChatResponseUpdate],
937
+ *,
938
+ output_format_type: StructuredResponseFormat = None,
939
+ ) -> ChatResponseT:
940
+ """Joins multiple updates into a single ChatResponse.
941
+
942
+ Example:
943
+ .. code-block:: python
944
+
945
+ from chrys.kernel._types import ChatResponse, ChatResponseUpdate
946
+
947
+ # Create some response updates
948
+ updates = [
949
+ ChatResponseUpdate(contents=[Content.from_text(text="Hello")], role="assistant"),
950
+ ChatResponseUpdate(contents=[Content.from_text(text=" How can I help you?")]),
951
+ ]
952
+
953
+ # Combine updates into a single ChatResponse
954
+ response = ChatResponse.from_updates(updates)
955
+ print(response.text) # "Hello How can I help you?"
956
+
957
+ Args:
958
+ updates: A sequence of ChatResponseUpdate objects to combine.
959
+
960
+ Keyword Args:
961
+ output_format_type: Optional Pydantic model type or JSON schema mapping used to parse the
962
+ response text into structured data.
963
+ """
964
+ msg = cls(messages=[], response_format=output_format_type)
965
+ seen_contents: dict[int, Content] = {}
966
+ for update in updates:
967
+ _process_update(msg, update, seen_contents)
968
+ _finalize_response(msg)
969
+ return msg
970
+
971
+ @overload
972
+ @classmethod
973
+ async def from_update_generator(
974
+ cls: type[ChatResponse[Any]],
975
+ updates: AsyncIterable[ChatResponseUpdate],
976
+ *,
977
+ output_format_type: type[ResponseModelBoundT],
978
+ ) -> ChatResponse[ResponseModelBoundT]: ...
979
+
980
+ @overload
981
+ @classmethod
982
+ async def from_update_generator(
983
+ cls: type[ChatResponse[Any]],
984
+ updates: AsyncIterable[ChatResponseUpdate],
985
+ *,
986
+ output_format_type: Mapping[str, Any],
987
+ ) -> ChatResponse[Any]: ...
988
+
989
+ @overload
990
+ @classmethod
991
+ async def from_update_generator(
992
+ cls: type[ChatResponse[Any]],
993
+ updates: AsyncIterable[ChatResponseUpdate],
994
+ *,
995
+ output_format_type: None = None,
996
+ ) -> ChatResponse[Any]: ...
997
+
998
+ @classmethod
999
+ async def from_update_generator(
1000
+ cls: type[ChatResponseT],
1001
+ updates: AsyncIterable[ChatResponseUpdate],
1002
+ *,
1003
+ output_format_type: StructuredResponseFormat = None,
1004
+ ) -> ChatResponseT:
1005
+ """Joins multiple updates into a single ChatResponse.
1006
+
1007
+ Example:
1008
+ .. code-block:: python
1009
+
1010
+ from chrys.kernel._types import ChatResponse
1011
+
1012
+ response = await ChatResponse.from_update_generator(
1013
+ updates
1014
+ )
1015
+ print(response.text)
1016
+
1017
+ Args:
1018
+ updates: An async iterable of ChatResponseUpdate objects to combine.
1019
+
1020
+ Keyword Args:
1021
+ output_format_type: Optional Pydantic model type or JSON schema mapping used to parse the
1022
+ response text into structured data.
1023
+ """
1024
+ msg = cls(messages=[], response_format=output_format_type)
1025
+ seen_contents: dict[int, Content] = {}
1026
+ async for update in updates:
1027
+ _process_update(msg, update, seen_contents)
1028
+ _finalize_response(msg)
1029
+ return msg
1030
+
1031
+ @property
1032
+ def text(self) -> str:
1033
+ """Returns the concatenated text of all messages in the response."""
1034
+ return self.raw_text.strip()
1035
+
1036
+ @property
1037
+ def raw_text(self) -> str:
1038
+ """The concatenated message text without the outer-whitespace strip of ``text``.
1039
+
1040
+ Format-sensitive consumers need the provider text verbatim: the
1041
+ LAST_WORDS structured-note validator treats leading indentation as
1042
+ CommonMark-meaningful, so a stripped view would silently legitimize
1043
+ a code-block-indented heading.
1044
+ """
1045
+ return "\n".join(message.text for message in self.messages if isinstance(message, Message))
1046
+
1047
+ @property
1048
+ def value(self) -> ResponseModelT | None:
1049
+ """Get the parsed structured output value.
1050
+
1051
+ If a response_format was provided and parsing hasn't been attempted yet,
1052
+ this parses the final non-empty assistant message into the specified type.
1053
+
1054
+ Raises:
1055
+ ValidationError: If the response text doesn't match the expected schema.
1056
+ ValueError: If the response text is not valid JSON for a non-Pydantic structured format.
1057
+ """
1058
+ if self._value_parsed:
1059
+ return self._value
1060
+ if self._response_format is not None:
1061
+ self._value = cast(
1062
+ ResponseModelT,
1063
+ _parse_structured_response_value(
1064
+ _last_non_empty_assistant_message_text(self.messages),
1065
+ self._response_format,
1066
+ ),
1067
+ )
1068
+ self._value_parsed = True
1069
+ return self._value
1070
+
1071
+ def __str__(self) -> str:
1072
+ return self.text
1073
+
1074
+
1075
+ # region ChatResponseUpdate
1076
+
1077
+
1078
+ class ChatResponseUpdate(SerializationMixin):
1079
+ """Represents a single streaming response chunk from a `ChatClient`.
1080
+
1081
+ Attributes:
1082
+ contents: The chat response update content items.
1083
+ role: The role of the author of the response update.
1084
+ author_name: The name of the author of the response update. This is primarily used in
1085
+ multi-agent scenarios to identify which agent or participant generated the response.
1086
+ When updates are combined into a `ChatResponse`, the `author_name` is propagated
1087
+ to the resulting `Message` objects.
1088
+ response_id: The ID of the response of which this update is a part.
1089
+ message_id: The ID of the message of which this update is a part.
1090
+ conversation_id: An identifier for the state of the conversation of which this update is a part.
1091
+ model: The model associated with this response update.
1092
+ created_at: A timestamp for the chat response update.
1093
+ finish_reason: The finish reason for the operation.
1094
+ additional_properties: Any additional properties associated with the chat response update.
1095
+ raw_representation: The raw representation of the chat response update from an underlying implementation.
1096
+
1097
+ Examples:
1098
+ .. code-block:: python
1099
+
1100
+ from chrys.kernel._content import Content
1101
+ from chrys.kernel._types import ChatResponseUpdate
1102
+
1103
+ # Create a response update with text content
1104
+ update = ChatResponseUpdate(
1105
+ contents=[Content.from_text(text="Hello")],
1106
+ role="assistant",
1107
+ message_id="msg_123",
1108
+ )
1109
+ print(update.text) # "Hello"
1110
+
1111
+ # Serialization - to_dict and from_dict
1112
+ update_dict = update.to_dict()
1113
+ # {'type': 'chat_response_update', 'contents': [{'type': 'text', 'text': 'Hello'}],
1114
+ # 'role': 'assistant', 'message_id': 'msg_123'}
1115
+ restored_update = ChatResponseUpdate.from_dict(update_dict)
1116
+ print(restored_update.text) # "Hello"
1117
+
1118
+ # Serialization - to_json and from_json
1119
+ update_json = update.to_json()
1120
+ # '{"type": "chat_response_update", "contents": [{"type": "text", "text": "Hello"}], ...}'
1121
+ restored_from_json = ChatResponseUpdate.from_json(update_json)
1122
+ print(restored_from_json.message_id) # "msg_123"
1123
+
1124
+ """
1125
+
1126
+ DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
1127
+
1128
+ def __init__(
1129
+ self,
1130
+ *,
1131
+ contents: Sequence[Content] | None = None,
1132
+ role: RoleLiteral | Role | None = None,
1133
+ author_name: str | None = None,
1134
+ response_id: str | None = None,
1135
+ message_id: str | None = None,
1136
+ conversation_id: str | None = None,
1137
+ model: str | None = None,
1138
+ created_at: CreatedAtT | None = None,
1139
+ finish_reason: FinishReasonLiteral | FinishReason | None = None,
1140
+ continuation_token: ContinuationToken | None = None,
1141
+ additional_properties: dict[str, Any] | None = None,
1142
+ raw_representation: Any | None = None,
1143
+ ) -> None:
1144
+ """Initializes a ChatResponseUpdate with the provided parameters.
1145
+
1146
+ Keyword Args:
1147
+ contents: Optional list of Content items to include in the update.
1148
+ role: Optional role of the author of the response update (e.g., "user", "assistant").
1149
+ author_name: Optional name of the author of the response update.
1150
+ response_id: Optional ID of the response of which this update is a part.
1151
+ message_id: Optional ID of the message of which this update is a part.
1152
+ conversation_id: Optional identifier for the state of the conversation of which this update is a part
1153
+ model: Optional model associated with this response update.
1154
+ created_at: Optional timestamp for the chat response update.
1155
+ finish_reason: Optional finish reason for the operation.
1156
+ continuation_token: Optional token for resuming a long-running background operation.
1157
+ When present, indicates the operation is still in progress.
1158
+ additional_properties: Optional additional properties associated with the chat response update.
1159
+ raw_representation: Optional raw representation of the chat response update
1160
+ from an underlying implementation.
1161
+
1162
+ """
1163
+ # Handle contents - support dict conversion for from_dict
1164
+ if contents is None:
1165
+ self.contents: list[Content] = []
1166
+ else:
1167
+ processed_contents: list[Content] = []
1168
+ for c in contents:
1169
+ if isinstance(c, Content):
1170
+ processed_contents.append(c)
1171
+ elif isinstance(c, dict):
1172
+ processed_contents.append(Content.from_dict(c))
1173
+ else:
1174
+ processed_contents.append(c)
1175
+ self.contents = processed_contents
1176
+
1177
+ self.role = role
1178
+ self.author_name = author_name
1179
+ self.response_id = response_id
1180
+ self.message_id = message_id
1181
+ self.conversation_id = conversation_id
1182
+ self.model = model
1183
+ self.created_at = created_at
1184
+ self.finish_reason = finish_reason
1185
+ self.continuation_token = continuation_token
1186
+ self.additional_properties = _restore_compaction_annotation_in_additional_properties(
1187
+ additional_properties,
1188
+ allow_none=True,
1189
+ )
1190
+ self.raw_representation = raw_representation
1191
+ # Runtime-only provenance copied through stream proxies and folded
1192
+ # into the corresponding Message by _process_update.
1193
+ self._chrys_echo_content_stripped = False
1194
+
1195
+ @classmethod
1196
+ def transport_heartbeat(cls) -> ChatResponseUpdate:
1197
+ """Create an opaque progress update carrying no provider response data."""
1198
+ return cls(contents=[], raw_representation=_STREAM_HEARTBEAT_MARKER)
1199
+
1200
+ @classmethod
1201
+ def retry_boundary(cls) -> ChatResponseUpdate:
1202
+ """Create an in-band marker for a fresh logical-call attempt."""
1203
+ return cls(contents=[], additional_properties={RETRY_BOUNDARY_UPDATE_KEY: True})
1204
+
1205
+ @property
1206
+ def is_transport_heartbeat(self) -> bool:
1207
+ """Return whether this is the explicit opaque transport heartbeat."""
1208
+ return (
1209
+ self.raw_representation is _STREAM_HEARTBEAT_MARKER
1210
+ and not self.contents
1211
+ and self.role is None
1212
+ and self.author_name is None
1213
+ and self.response_id is None
1214
+ and self.message_id is None
1215
+ and self.conversation_id is None
1216
+ and self.model is None
1217
+ and self.created_at is None
1218
+ and self.finish_reason is None
1219
+ and self.continuation_token is None
1220
+ and not self.additional_properties
1221
+ )
1222
+
1223
+ @property
1224
+ def text(self) -> str:
1225
+ """Returns the concatenated text of all contents in the update."""
1226
+ return "".join(_text_for_join(content) for content in self.contents if content.type == "text")
1227
+
1228
+ def __str__(self) -> str:
1229
+ return self.text
1230
+
1231
+
1232
+ # region AgentResponse
1233
+
1234
+
1235
+ class AgentResponse(SerializationMixin, Generic[ResponseModelT]):
1236
+ """Represents the response to an Agent run request.
1237
+
1238
+ Provides one or more response messages and metadata about the response.
1239
+ A typical response will contain a single message, but may contain multiple
1240
+ messages in scenarios involving function calls, RAG retrievals, or complex logic.
1241
+
1242
+ Note:
1243
+ The `author_name` attribute is available on the `Message` objects inside `messages`,
1244
+ not on the `AgentResponse` itself. Use `response.messages[0].author_name` to access
1245
+ the author name of individual messages.
1246
+
1247
+ Examples:
1248
+ .. code-block:: python
1249
+
1250
+ from chrys.kernel._types import AgentResponse, Message
1251
+
1252
+ # Create agent response
1253
+ msg = Message("assistant", ["Task completed successfully."])
1254
+ response = AgentResponse(messages=[msg], response_id="run_123")
1255
+ print(response.text) # "Task completed successfully."
1256
+
1257
+ # Combine streaming updates
1258
+ updates = [...] # List of AgentResponseUpdate objects
1259
+ response = AgentResponse.from_updates(updates)
1260
+
1261
+ # Serialization - to_dict and from_dict
1262
+ response_dict = response.to_dict()
1263
+ # {'type': 'agent_response', 'messages': [...], 'response_id': 'run_123',
1264
+ # 'additional_properties': {}}
1265
+ restored_response = AgentResponse.from_dict(response_dict)
1266
+ print(restored_response.response_id) # "run_123"
1267
+
1268
+ # Serialization - to_json and from_json
1269
+ response_json = response.to_json()
1270
+ # '{"type": "agent_response", "messages": [...], "response_id": "run_123", ...}'
1271
+ restored_from_json = AgentResponse.from_json(response_json)
1272
+ print(restored_from_json.text) # "Task completed successfully."
1273
+ """
1274
+
1275
+ DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
1276
+
1277
+ def __init__(
1278
+ self,
1279
+ *,
1280
+ messages: Message | Sequence[Message] | None = None,
1281
+ response_id: str | None = None,
1282
+ agent_id: str | None = None,
1283
+ created_at: CreatedAtT | None = None,
1284
+ finish_reason: FinishReasonLiteral | FinishReason | None = None,
1285
+ usage_details: UsageDetails | None = None,
1286
+ latest_usage_details: UsageDetails | None = _UNSET_USAGE,
1287
+ value: ResponseModelT | None = None,
1288
+ response_format: StructuredResponseFormat = None,
1289
+ continuation_token: ContinuationToken | None = None,
1290
+ raw_representation: Any | None = None,
1291
+ additional_properties: dict[str, Any] | None = None,
1292
+ ) -> None:
1293
+ """Initialize an AgentResponse.
1294
+
1295
+ Keyword Args:
1296
+ messages: A single Message or sequence of Message objects to include in the response.
1297
+ response_id: The ID of the chat response.
1298
+ agent_id: The identifier of the agent that produced this response. Useful in multi-agent
1299
+ scenarios to track which agent generated the response.
1300
+ created_at: A timestamp for the chat response.
1301
+ finish_reason: The reason the model stopped generating. Common values include
1302
+ ``"stop"`` (natural completion), ``"length"`` (token limit), and
1303
+ ``"tool_calls"`` (the model invoked a tool).
1304
+ usage_details: Aggregate usage across all model calls in the agent run.
1305
+ latest_usage_details: Usage for the most recent model call.
1306
+ When omitted, defaults to ``usage_details`` (single-call response).
1307
+ Pass ``None`` explicitly when the final call's usage is unknown;
1308
+ it is preserved rather than falling back to the aggregate.
1309
+ value: The structured output of the agent run response, if applicable.
1310
+ response_format: Optional response format for the agent response.
1311
+ continuation_token: Optional token for resuming a long-running background operation.
1312
+ When present, indicates the operation is still in progress.
1313
+ additional_properties: Any additional properties associated with the chat response.
1314
+ raw_representation: The raw representation of the chat response from an underlying implementation.
1315
+ """
1316
+ if messages is None:
1317
+ self.messages: list[Message] = []
1318
+ elif isinstance(messages, Message):
1319
+ self.messages = [messages]
1320
+ else:
1321
+ # Handle both Message objects and dicts (for from_dict support)
1322
+ processed_messages: list[Message] = []
1323
+ for msg in messages:
1324
+ if isinstance(msg, Message):
1325
+ processed_messages.append(msg)
1326
+ elif isinstance(msg, dict):
1327
+ processed_messages.append(Message.from_dict(msg))
1328
+ else:
1329
+ processed_messages.append(msg)
1330
+ self.messages = processed_messages
1331
+ self.response_id = response_id
1332
+ self.agent_id = agent_id
1333
+ self.created_at = created_at
1334
+ self.finish_reason = finish_reason
1335
+ self.usage_details = usage_details
1336
+ # Public attribute so serialization round-trips preserve the
1337
+ # occupancy view alongside the billing aggregate.
1338
+ self.latest_usage_details: UsageDetails | None = (
1339
+ usage_details if latest_usage_details is _UNSET_USAGE else latest_usage_details
1340
+ )
1341
+ self._value: ResponseModelT | None = value
1342
+ self._response_format: type[BaseModel] | Mapping[str, Any] | None = response_format
1343
+ self._value_parsed: bool = value is not None
1344
+ self.additional_properties = (
1345
+ _restore_compaction_annotation_in_additional_properties(additional_properties) or {}
1346
+ )
1347
+ self.continuation_token = continuation_token
1348
+ self.raw_representation = raw_representation
1349
+ # Runtime-only mirror of the inner ChatResponse's invalidation marker
1350
+ # (see ChatResponse.__init__); the agent's streaming post-hook reads it
1351
+ # to skip restoring a service session handle the loop invalidated.
1352
+ self._chrys_service_state_invalidated = False
1353
+
1354
+ @property
1355
+ def text(self) -> str:
1356
+ """Get the concatenated text of all messages."""
1357
+ return "".join(msg.text for msg in self.messages) if self.messages else ""
1358
+
1359
+ @property
1360
+ def value(self) -> ResponseModelT | None:
1361
+ """Get the parsed structured output value.
1362
+
1363
+ If a response_format was provided and parsing hasn't been attempted yet,
1364
+ this parses the final non-empty assistant message into the specified type.
1365
+
1366
+ Raises:
1367
+ ValidationError: If the response text doesn't match the expected schema.
1368
+ ValueError: If the response text is not valid JSON for a non-Pydantic structured format.
1369
+ """
1370
+ if self._value_parsed:
1371
+ return self._value
1372
+ if self._response_format is not None:
1373
+ self._value = cast(
1374
+ ResponseModelT,
1375
+ _parse_structured_response_value(
1376
+ _last_non_empty_assistant_message_text(self.messages),
1377
+ self._response_format,
1378
+ ),
1379
+ )
1380
+ self._value_parsed = True
1381
+ return self._value
1382
+
1383
+ def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) -> dict[str, Any]:
1384
+ """Serialize, preserving an explicitly-unavailable latest usage.
1385
+
1386
+ ``exclude_none`` would drop ``latest_usage_details=None`` and the
1387
+ constructor default would then substitute the billing aggregate on
1388
+ deserialization. ``None`` is a meaningful value here (final call's
1389
+ usage unknown), so it is kept as an explicit ``null`` whenever an
1390
+ aggregate exists to be wrongly inherited.
1391
+ """
1392
+ result = super().to_dict(exclude=exclude, exclude_none=exclude_none)
1393
+ if (
1394
+ exclude_none
1395
+ and self.latest_usage_details is None
1396
+ and "latest_usage_details" not in (exclude or ())
1397
+ and "usage_details" in result
1398
+ ):
1399
+ result["latest_usage_details"] = None
1400
+ return result
1401
+
1402
+ @overload
1403
+ @classmethod
1404
+ def from_updates(
1405
+ cls: type[AgentResponse[Any]],
1406
+ updates: Sequence[AgentResponseUpdate],
1407
+ *,
1408
+ output_format_type: type[ResponseModelBoundT],
1409
+ value: Any | None = None,
1410
+ ) -> AgentResponse[ResponseModelBoundT]: ...
1411
+
1412
+ @overload
1413
+ @classmethod
1414
+ def from_updates(
1415
+ cls: type[AgentResponse[Any]],
1416
+ updates: Sequence[AgentResponseUpdate],
1417
+ *,
1418
+ output_format_type: Mapping[str, Any],
1419
+ value: Any | None = None,
1420
+ ) -> AgentResponse[Any]: ...
1421
+
1422
+ @overload
1423
+ @classmethod
1424
+ def from_updates(
1425
+ cls: type[AgentResponse[Any]],
1426
+ updates: Sequence[AgentResponseUpdate],
1427
+ *,
1428
+ output_format_type: None = None,
1429
+ value: Any | None = None,
1430
+ ) -> AgentResponse[Any]: ...
1431
+
1432
+ @classmethod
1433
+ def from_updates(
1434
+ cls: type[AgentResponseT],
1435
+ updates: Sequence[AgentResponseUpdate],
1436
+ *,
1437
+ output_format_type: StructuredResponseFormat = None,
1438
+ value: Any | None = None,
1439
+ ) -> AgentResponseT:
1440
+ """Joins multiple updates into a single AgentResponse.
1441
+
1442
+ Args:
1443
+ updates: A sequence of AgentResponseUpdate objects to combine.
1444
+
1445
+ Keyword Args:
1446
+ output_format_type: Optional Pydantic model type or JSON schema mapping used to parse the
1447
+ response text into structured data.
1448
+ value: Optional pre-parsed structured output value to set directly on the response.
1449
+ """
1450
+ msg = cls(messages=[], response_format=output_format_type, value=value)
1451
+ seen_contents: dict[int, Content] = {}
1452
+ for update in updates:
1453
+ _process_update(msg, update, seen_contents)
1454
+ _finalize_response(msg)
1455
+ return msg
1456
+
1457
+ @overload
1458
+ @classmethod
1459
+ async def from_update_generator(
1460
+ cls: type[AgentResponse[Any]],
1461
+ updates: AsyncIterable[AgentResponseUpdate],
1462
+ *,
1463
+ output_format_type: type[ResponseModelBoundT],
1464
+ ) -> AgentResponse[ResponseModelBoundT]: ...
1465
+
1466
+ @overload
1467
+ @classmethod
1468
+ async def from_update_generator(
1469
+ cls: type[AgentResponse[Any]],
1470
+ updates: AsyncIterable[AgentResponseUpdate],
1471
+ *,
1472
+ output_format_type: Mapping[str, Any],
1473
+ ) -> AgentResponse[Any]: ...
1474
+
1475
+ @overload
1476
+ @classmethod
1477
+ async def from_update_generator(
1478
+ cls: type[AgentResponse[Any]],
1479
+ updates: AsyncIterable[AgentResponseUpdate],
1480
+ *,
1481
+ output_format_type: None = None,
1482
+ ) -> AgentResponse[Any]: ...
1483
+
1484
+ @classmethod
1485
+ async def from_update_generator(
1486
+ cls: type[AgentResponseT],
1487
+ updates: AsyncIterable[AgentResponseUpdate],
1488
+ *,
1489
+ output_format_type: StructuredResponseFormat = None,
1490
+ ) -> AgentResponseT:
1491
+ """Joins multiple updates into a single AgentResponse.
1492
+
1493
+ Args:
1494
+ updates: An async iterable of AgentResponseUpdate objects to combine.
1495
+
1496
+ Keyword Args:
1497
+ output_format_type: Optional Pydantic model type or JSON schema mapping used to parse the
1498
+ response text into structured data.
1499
+ """
1500
+ msg = cls(messages=[], response_format=output_format_type)
1501
+ seen_contents: dict[int, Content] = {}
1502
+ async for update in updates:
1503
+ _process_update(msg, update, seen_contents)
1504
+ _finalize_response(msg)
1505
+ return msg
1506
+
1507
+ def __str__(self) -> str:
1508
+ return self.text
1509
+
1510
+
1511
+ def _build_agent_response_from_chat_response(
1512
+ response: ChatResponse[Any],
1513
+ *,
1514
+ response_format: StructuredResponseFormat = None,
1515
+ ) -> AgentResponse[Any]:
1516
+ """Wrap a completed chat response without eagerly parsing structured output."""
1517
+ agent_response = AgentResponse(
1518
+ messages=response.messages,
1519
+ response_id=response.response_id,
1520
+ created_at=response.created_at,
1521
+ finish_reason=response.finish_reason,
1522
+ usage_details=response.usage_details,
1523
+ latest_usage_details=response.latest_usage_details,
1524
+ response_format=response_format,
1525
+ continuation_token=response.continuation_token,
1526
+ raw_representation=response,
1527
+ additional_properties=response.additional_properties,
1528
+ )
1529
+ if response._value_parsed:
1530
+ agent_response._value = response._value
1531
+ agent_response._value_parsed = True
1532
+ if response._chrys_service_state_invalidated:
1533
+ # Mirror the ephemeral invalidation marker so blocking callers see
1534
+ # the same no-continuation verdict the streaming finalizer stamps.
1535
+ agent_response._chrys_service_state_invalidated = True
1536
+ return agent_response
1537
+
1538
+
1539
+ # region AgentResponseUpdate
1540
+
1541
+
1542
+ class AgentResponseUpdate(SerializationMixin):
1543
+ """Represents a single streaming response chunk from an Agent.
1544
+
1545
+ Attributes:
1546
+ contents: The content items in this update.
1547
+ role: The role of the author of the response update.
1548
+ author_name: The name of the author of the response update. In multi-agent scenarios,
1549
+ this identifies which agent generated this update. When updates are combined into
1550
+ an `AgentResponse`, the `author_name` is propagated to the resulting `Message` objects.
1551
+ agent_id: The identifier of the agent that produced this update. Useful in multi-agent
1552
+ scenarios to track which agent generated specific parts of the response.
1553
+ response_id: The ID of the response of which this update is a part.
1554
+ message_id: The ID of the message of which this update is a part.
1555
+ created_at: A timestamp for the response update.
1556
+ additional_properties: Any additional properties associated with the update.
1557
+ raw_representation: The raw representation from an underlying implementation.
1558
+
1559
+ Examples:
1560
+ .. code-block:: python
1561
+
1562
+ from chrys.kernel._content import Content
1563
+ from chrys.kernel._types import AgentResponseUpdate
1564
+
1565
+ # Create an agent run update
1566
+ update = AgentResponseUpdate(
1567
+ contents=[Content.from_text(text="Processing...")],
1568
+ role="assistant",
1569
+ response_id="run_123",
1570
+ )
1571
+ print(update.text) # "Processing..."
1572
+
1573
+ # Serialization - to_dict and from_dict
1574
+ update_dict = update.to_dict()
1575
+ # {'type': 'agent_response_update', 'contents': [{'type': 'text', 'text': 'Processing...'}],
1576
+ # 'role': 'assistant', 'response_id': 'run_123'}
1577
+ restored_update = AgentResponseUpdate.from_dict(update_dict)
1578
+ print(restored_update.response_id) # "run_123"
1579
+
1580
+ # Serialization - to_json and from_json
1581
+ update_json = update.to_json()
1582
+ # '{"type": "agent_response_update", "contents": [{"type": "text", "text": "Processing..."}], ...}'
1583
+ restored_from_json = AgentResponseUpdate.from_json(update_json)
1584
+ print(restored_from_json.text) # "Processing..."
1585
+ """
1586
+
1587
+ DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
1588
+
1589
+ def __init__(
1590
+ self,
1591
+ *,
1592
+ contents: Sequence[Content] | None = None,
1593
+ role: RoleLiteral | str | None = None,
1594
+ author_name: str | None = None,
1595
+ agent_id: str | None = None,
1596
+ response_id: str | None = None,
1597
+ message_id: str | None = None,
1598
+ created_at: CreatedAtT | None = None,
1599
+ finish_reason: FinishReasonLiteral | FinishReason | None = None,
1600
+ continuation_token: ContinuationToken | None = None,
1601
+ additional_properties: dict[str, Any] | None = None,
1602
+ raw_representation: Any | None = None,
1603
+ ) -> None:
1604
+ """Initialize an AgentResponseUpdate.
1605
+
1606
+ Keyword Args:
1607
+ contents: Optional list of Content items to include in the update.
1608
+ role: The role of the author of the response update (e.g., "user", "assistant").
1609
+ author_name: Optional name of the author of the response update. Used in multi-agent
1610
+ scenarios to identify which agent generated this update.
1611
+ agent_id: Optional identifier of the agent that produced this update.
1612
+ response_id: Optional ID of the response of which this update is a part.
1613
+ message_id: Optional ID of the message of which this update is a part.
1614
+ created_at: Optional timestamp for the chat response update.
1615
+ finish_reason: The reason the model stopped generating. Common values include
1616
+ ``"stop"`` (natural completion), ``"length"`` (token limit), and
1617
+ ``"tool_calls"`` (the model invoked a tool).
1618
+ continuation_token: Optional token for resuming a long-running background operation.
1619
+ When present, indicates the operation is still in progress.
1620
+ additional_properties: Optional additional properties associated with the chat response update.
1621
+ raw_representation: Optional raw representation of the chat response update.
1622
+
1623
+ """
1624
+ # Handle contents - support dict conversion for from_dict
1625
+ if contents is None:
1626
+ self.contents: list[Content] = []
1627
+ else:
1628
+ processed_contents: list[Content] = []
1629
+ for c in contents:
1630
+ if isinstance(c, Content):
1631
+ processed_contents.append(c)
1632
+ elif isinstance(c, dict):
1633
+ processed_contents.append(Content.from_dict(c))
1634
+ else:
1635
+ processed_contents.append(c)
1636
+ self.contents = processed_contents
1637
+
1638
+ self.role: str | None = role
1639
+ self.author_name = author_name
1640
+ self.agent_id = agent_id
1641
+ self.response_id = response_id
1642
+ self.message_id = message_id
1643
+ self.created_at = created_at
1644
+ self.finish_reason = finish_reason
1645
+ self.continuation_token = continuation_token
1646
+ self.additional_properties = _restore_compaction_annotation_in_additional_properties(
1647
+ additional_properties,
1648
+ allow_none=True,
1649
+ )
1650
+ self.raw_representation: Any | list[Any] | None = raw_representation
1651
+
1652
+ @property
1653
+ def text(self) -> str:
1654
+ """Get the concatenated text of all TextContent objects in contents."""
1655
+ return "".join(_text_for_join(content) for content in self.contents if content.type == "text")
1656
+
1657
+ def __str__(self) -> str:
1658
+ return self.text
1659
+
1660
+
1661
+ # region ResponseStream
1662
+
1663
+
1664
+ def map_chat_to_agent_update(update: ChatResponseUpdate, agent_name: str | None) -> AgentResponseUpdate:
1665
+ return AgentResponseUpdate(
1666
+ contents=update.contents,
1667
+ role=update.role,
1668
+ author_name=update.author_name or agent_name,
1669
+ response_id=update.response_id,
1670
+ message_id=update.message_id,
1671
+ created_at=update.created_at,
1672
+ finish_reason=update.finish_reason, # type: ignore[arg-type]
1673
+ continuation_token=update.continuation_token,
1674
+ additional_properties=update.additional_properties,
1675
+ raw_representation=update,
1676
+ )
1677
+
1678
+
1679
+ # Type variables for ResponseStream
1680
+ def _raise_stream_teardown_errors(errors: Sequence[BaseException]) -> None:
1681
+ """Raise a collected teardown failure, preserving cancellation semantics."""
1682
+ for error in errors:
1683
+ if isinstance(error, asyncio.CancelledError):
1684
+ raise error
1685
+ raise errors[0]
1686
+
1687
+
1688
+ async def _resolve_maybe_awaitable[T](value: T | Awaitable[T]) -> T:
1689
+ """Resolve a possibly-awaitable value without widening its result type."""
1690
+ if isawaitable(value):
1691
+ return cast("T", await value)
1692
+ return value
1693
+
1694
+
1695
+ async def _resolve_stream_source[T](
1696
+ source: AsyncIterable[T] | Awaitable[AsyncIterable[T]],
1697
+ ) -> AsyncIterable[T]:
1698
+ """Resolve the two supported stream-source shapes without losing ``T``."""
1699
+ if isinstance(source, AsyncIterable):
1700
+ return cast("AsyncIterable[T]", source)
1701
+ if not iscoroutine(source):
1702
+ return cast("AsyncIterable[T]", source)
1703
+ return await source
1704
+
1705
+
1706
+ @final
1707
+ class ResponseStream[UpdateT, FinalT](AsyncIterable[UpdateT]):
1708
+ """Async stream wrapper that supports iteration and deferred finalization."""
1709
+
1710
+ def __init__(
1711
+ self,
1712
+ stream: AsyncIterable[UpdateT] | Awaitable[AsyncIterable[UpdateT]],
1713
+ *,
1714
+ finalizer: Callable[[Sequence[UpdateT]], FinalT | Awaitable[FinalT]] | None = None,
1715
+ transform_hooks: list[Callable[[UpdateT], UpdateT | Awaitable[UpdateT | None] | None]] | None = None,
1716
+ cleanup_hooks: list[Callable[[], Awaitable[None] | None]] | None = None,
1717
+ result_hooks: list[Callable[[FinalT], FinalT | Awaitable[FinalT | None] | None]] | None = None,
1718
+ ) -> None:
1719
+ """A Async Iterable stream of updates.
1720
+
1721
+ Args:
1722
+ stream: An async iterable or awaitable that resolves to an async iterable of updates.
1723
+
1724
+ Keyword Args:
1725
+ finalizer: An optional callable that takes the list of all updates and produces a final result.
1726
+ transform_hooks: Optional list of callables that transform each update as it is yielded.
1727
+ cleanup_hooks: Optional list of callables that run after the stream is fully consumed (before finalizer).
1728
+ result_hooks: Optional list of callables that transform the final result (after finalizer).
1729
+
1730
+ """
1731
+ self._stream_source = stream
1732
+ self._finalizer = finalizer
1733
+ self._stream: AsyncIterable[UpdateT] | None = None
1734
+ self._iterator: AsyncIterator[UpdateT] | None = None
1735
+ self._updates: list[UpdateT] = []
1736
+ self._consumed: bool = False
1737
+ self._finalized: bool = False
1738
+ self._final_result: FinalT | list[UpdateT] | None = None
1739
+ self._transform_hooks: list[Callable[[UpdateT], UpdateT | Awaitable[UpdateT | None] | None]] = (
1740
+ transform_hooks if transform_hooks is not None else []
1741
+ )
1742
+ self._update_filters: list[Callable[[UpdateT], UpdateT]] = []
1743
+ self._result_hooks: list[Callable[[FinalT], FinalT | Awaitable[FinalT | None] | None]] = (
1744
+ result_hooks if result_hooks is not None else []
1745
+ )
1746
+ self._cleanup_hooks: list[Callable[[], Awaitable[None] | None]] = (
1747
+ cleanup_hooks if cleanup_hooks is not None else []
1748
+ )
1749
+ self._cleanup_run: bool = False
1750
+ self._closed: bool = False
1751
+ self._abandoned: bool = False
1752
+ self._stream_error: Exception | None = None
1753
+ self._inner_stream: ResponseStream[Any, Any] | None = None
1754
+ self._inner_stream_source: ResponseStream[Any, Any] | Awaitable[ResponseStream[Any, Any]] | None = None
1755
+ self._wrap_inner: bool = False
1756
+ self._map_update: Callable[[Any], UpdateT | Awaitable[UpdateT]] | None = None
1757
+ self._pull_context_manager_factories: list[Callable[[], contextlib.AbstractContextManager[Any]]] = []
1758
+
1759
+ @overload
1760
+ def map[OuterUpdateT, OuterFinalT](
1761
+ self,
1762
+ transform: Callable[[UpdateT], OuterUpdateT | Awaitable[OuterUpdateT]],
1763
+ finalizer: Callable[[Sequence[OuterUpdateT]], Awaitable[OuterFinalT]],
1764
+ ) -> ResponseStream[OuterUpdateT, OuterFinalT]: ...
1765
+
1766
+ @overload
1767
+ def map[OuterUpdateT, OuterFinalT](
1768
+ self,
1769
+ transform: Callable[[UpdateT], OuterUpdateT | Awaitable[OuterUpdateT]],
1770
+ finalizer: Callable[[Sequence[OuterUpdateT]], OuterFinalT],
1771
+ ) -> ResponseStream[OuterUpdateT, OuterFinalT]: ...
1772
+
1773
+ def map[OuterUpdateT, OuterFinalT](
1774
+ self,
1775
+ transform: Callable[[UpdateT], OuterUpdateT | Awaitable[OuterUpdateT]],
1776
+ finalizer: Callable[[Sequence[OuterUpdateT]], OuterFinalT | Awaitable[OuterFinalT]],
1777
+ ) -> ResponseStream[OuterUpdateT, OuterFinalT]:
1778
+ """Create a new stream that transforms each update.
1779
+
1780
+ The returned stream delegates iteration to this stream, ensuring single consumption.
1781
+ Each update is transformed by the provided function before being yielded.
1782
+
1783
+ Since the update type changes, a new finalizer MUST be provided that works with
1784
+ the transformed update type. The inner stream's finalizer cannot be used as it
1785
+ expects the original update type.
1786
+
1787
+ When ``get_final_response()`` is called on the mapped stream:
1788
+ 1. The inner stream's finalizer runs first (on the original updates)
1789
+ 2. The inner stream's result_hooks run (on the inner final result)
1790
+ 3. The outer stream's finalizer runs (on the transformed updates)
1791
+ 4. The outer stream's result_hooks run (on the outer final result)
1792
+
1793
+ This ensures that post-processing hooks registered on the inner stream (e.g.,
1794
+ context provider notifications, telemetry) are still executed.
1795
+
1796
+ Args:
1797
+ transform: Function to transform each update to a new type.
1798
+ finalizer: Function to convert collected (transformed) updates to the final type.
1799
+ This is required because the inner stream's finalizer won't work with
1800
+ the new update type.
1801
+
1802
+ Returns:
1803
+ A new ResponseStream with transformed update and final types.
1804
+
1805
+ Example:
1806
+ >>> chat_stream.map(
1807
+ ... lambda u: AgentResponseUpdate(...),
1808
+ ... AgentResponse.from_updates,
1809
+ ... )
1810
+ """
1811
+ stream = ResponseStream[OuterUpdateT, OuterFinalT](self, finalizer=finalizer)
1812
+ stream._inner_stream_source = self
1813
+ stream._wrap_inner = True
1814
+ stream._map_update = transform
1815
+ return stream
1816
+
1817
+ @overload
1818
+ def with_finalizer[OuterFinalT](
1819
+ self,
1820
+ finalizer: Callable[[Sequence[UpdateT]], Awaitable[OuterFinalT]],
1821
+ ) -> ResponseStream[UpdateT, OuterFinalT]: ...
1822
+
1823
+ @overload
1824
+ def with_finalizer[OuterFinalT](
1825
+ self,
1826
+ finalizer: Callable[[Sequence[UpdateT]], OuterFinalT],
1827
+ ) -> ResponseStream[UpdateT, OuterFinalT]: ...
1828
+
1829
+ def with_finalizer[OuterFinalT](
1830
+ self,
1831
+ finalizer: Callable[[Sequence[UpdateT]], OuterFinalT | Awaitable[OuterFinalT]],
1832
+ ) -> ResponseStream[UpdateT, OuterFinalT]:
1833
+ """Create a new stream with a different finalizer.
1834
+
1835
+ The returned stream delegates iteration to this stream, ensuring single consumption.
1836
+ When `get_final_response()` is called, the inner stream is finalized first,
1837
+ including its result hooks, and retains its cached final response. The new
1838
+ finalizer then receives the collected updates to produce the outer response.
1839
+ Each stream caches its own result; repeated retrieval does not repeat either
1840
+ finalizer or its result hooks.
1841
+
1842
+ Args:
1843
+ finalizer: Function to convert collected updates to the final response type.
1844
+
1845
+ Returns:
1846
+ A new ResponseStream with the new final type.
1847
+
1848
+ Example:
1849
+ >>> stream.with_finalizer(AgentResponse.from_updates)
1850
+ """
1851
+ stream = ResponseStream[UpdateT, OuterFinalT](self, finalizer=finalizer)
1852
+ stream._inner_stream_source = self
1853
+ stream._wrap_inner = True
1854
+ return stream
1855
+
1856
+ @classmethod
1857
+ def from_awaitable(
1858
+ cls,
1859
+ awaitable: Awaitable[ResponseStream[UpdateT, FinalT]],
1860
+ ) -> ResponseStream[UpdateT, FinalT]:
1861
+ """Create a ResponseStream from an awaitable that resolves to a ResponseStream.
1862
+
1863
+ This is useful when you have an async function that returns a ResponseStream
1864
+ and you want to wrap it to add hooks or use it in a pipeline.
1865
+
1866
+ The returned stream delegates to the inner stream once it resolves, using the
1867
+ inner stream's finalizer if no new finalizer is provided.
1868
+
1869
+ Args:
1870
+ awaitable: An awaitable that resolves to a ResponseStream.
1871
+
1872
+ Returns:
1873
+ A new ResponseStream that wraps the awaitable.
1874
+
1875
+ Example:
1876
+ >>> async def get_stream() -> ResponseStream[Update, Response]: ...
1877
+ >>> stream = ResponseStream.from_awaitable(get_stream())
1878
+ """
1879
+ stream: ResponseStream[UpdateT, FinalT] = cls(awaitable)
1880
+ stream._inner_stream_source = awaitable
1881
+ stream._wrap_inner = True
1882
+ return stream
1883
+
1884
+ async def _get_stream(self) -> AsyncIterable[UpdateT]:
1885
+ if self._stream is None:
1886
+ self._stream = await _resolve_stream_source(self._stream_source)
1887
+ if self._map_update is None and isinstance(self._stream, ResponseStream):
1888
+ # Push registered update filters down to the source stream so
1889
+ # they run before ITS accumulation — the innermost finalizer
1890
+ # is what produces the final response. Mapped streams keep
1891
+ # filters local: theirs expect the post-map update type.
1892
+ for update_filter in self._update_filters:
1893
+ self._stream.with_update_filter(update_filter)
1894
+ if isinstance(self._stream, ResponseStream) and self._wrap_inner:
1895
+ self._inner_stream = self._stream
1896
+ return self._inner_stream
1897
+ stream = self._stream
1898
+ if stream is None:
1899
+ raise RuntimeError("ResponseStream source did not resolve to an async iterable")
1900
+ return stream
1901
+
1902
+ def __aiter__(self) -> ResponseStream[UpdateT, FinalT]:
1903
+ return self
1904
+
1905
+ async def __anext__(self) -> UpdateT:
1906
+ if self._closed:
1907
+ raise StopAsyncIteration
1908
+ try:
1909
+ with contextlib.ExitStack() as stack:
1910
+ for factory in self._pull_context_manager_factories:
1911
+ stack.enter_context(factory())
1912
+ # Resolve the underlying stream inside the pull contexts so that any
1913
+ # spans/contexts created during stream resolution (e.g. inner chat
1914
+ # completion spans created on the first pull of a wrapped agent stream)
1915
+ # inherit the active context (e.g. an outer agent invoke span).
1916
+ if self._iterator is None:
1917
+ stream = await self._get_stream()
1918
+ self._iterator = stream.__aiter__()
1919
+ update: UpdateT = await self._iterator.__anext__()
1920
+ except StopAsyncIteration:
1921
+ self._consumed = True
1922
+ await self._run_cleanup_hooks()
1923
+ await self.get_final_response()
1924
+ raise
1925
+ except asyncio.CancelledError:
1926
+ try:
1927
+ await self.aclose()
1928
+ except Exception:
1929
+ logger.debug("ResponseStream close failed while cancelling a pull", exc_info=True)
1930
+ raise
1931
+ except Exception as exc:
1932
+ self._stream_error = exc
1933
+ try:
1934
+ await self._run_cleanup_hooks()
1935
+ finally:
1936
+ self._stream_error = None
1937
+ raise
1938
+ if self._map_update is not None:
1939
+ update = await _resolve_maybe_awaitable(self._map_update(update))
1940
+ if self._update_filters and (self._map_update is not None or not isinstance(self._stream, ResponseStream)):
1941
+ # Filters apply exactly once, at the level that pulls raw updates
1942
+ # (or locally after a map transform); a ResponseStream source
1943
+ # already applied them via push-down before accumulating.
1944
+ for update_filter in self._update_filters:
1945
+ update = update_filter(update)
1946
+ self._updates.append(update)
1947
+ for hook in self._transform_hooks:
1948
+ hooked = await _resolve_maybe_awaitable(hook(update))
1949
+ if hooked is not None:
1950
+ update = hooked
1951
+ return update
1952
+
1953
+ async def aclose(self) -> None:
1954
+ """Close this stream and every resolved inner stream without finalizing.
1955
+
1956
+ Calling this method repeatedly is safe. An unresolved coroutine source
1957
+ is closed rather than awaited, so abandoning a lazy stream cannot start
1958
+ a provider request.
1959
+ """
1960
+ if self._closed:
1961
+ return
1962
+ self._closed = True
1963
+ # A close before finalization abandons the partial updates: cleanup
1964
+ # hooks (and anything else) must never finalize them into a
1965
+ # successful-looking response or fire result hooks on them.
1966
+ if not self._finalized:
1967
+ self._abandoned = True
1968
+
1969
+ resolved: list[Any] = []
1970
+ if self._inner_stream is not None:
1971
+ resolved.append(self._inner_stream)
1972
+ if self._stream is not None:
1973
+ resolved.append(self._stream)
1974
+ if self._iterator is not None:
1975
+ resolved.append(self._iterator)
1976
+
1977
+ seen: set[int] = set()
1978
+ close_errors: list[BaseException] = []
1979
+ try:
1980
+ for item in resolved:
1981
+ if item is self or id(item) in seen:
1982
+ continue
1983
+ seen.add(id(item))
1984
+ try:
1985
+ if isinstance(item, ResponseStream):
1986
+ await item.aclose()
1987
+ continue
1988
+ close = getattr(item, "aclose", None)
1989
+ if close is not None:
1990
+ result = close()
1991
+ if isawaitable(result):
1992
+ await result
1993
+ except BaseException as exc:
1994
+ close_errors.append(exc)
1995
+
1996
+ if self._stream is None:
1997
+ sources = (self._stream_source, self._inner_stream_source)
1998
+ for source in sources:
1999
+ if source is None or id(source) in seen:
2000
+ continue
2001
+ seen.add(id(source))
2002
+ try:
2003
+ if iscoroutine(source):
2004
+ source.close()
2005
+ elif isinstance(source, asyncio.Future) and not source.done():
2006
+ source.cancel()
2007
+ else:
2008
+ close = getattr(source, "aclose", None)
2009
+ if close is not None:
2010
+ result = close()
2011
+ if isawaitable(result):
2012
+ await result
2013
+ except BaseException as exc:
2014
+ close_errors.append(exc)
2015
+ finally:
2016
+ try:
2017
+ await self._run_cleanup_hooks()
2018
+ except BaseException as exc:
2019
+ close_errors.append(exc)
2020
+ if close_errors:
2021
+ _raise_stream_teardown_errors(close_errors)
2022
+
2023
+ async def _resolve_stream_with_pull_contexts(self) -> AsyncIterable[UpdateT]:
2024
+ """Resolve the underlying stream while activating any registered pull context managers.
2025
+
2026
+ Used by ``__await__`` and ``get_final_response`` so that any spans/contexts created
2027
+ during stream resolution (e.g. when the source is an Awaitable that internally
2028
+ creates child telemetry spans) inherit the same active context as iterator pulls.
2029
+ ``__anext__`` resolves the stream inside its own ExitStack and so calls ``_get_stream``
2030
+ directly.
2031
+ """
2032
+ if self._stream is not None:
2033
+ return await self._get_stream()
2034
+ with contextlib.ExitStack() as stack:
2035
+ for factory in self._pull_context_manager_factories:
2036
+ stack.enter_context(factory())
2037
+ return await self._get_stream()
2038
+
2039
+ def __await__(self) -> Generator[Any, None, ResponseStream[UpdateT, FinalT]]:
2040
+ async def _wrap() -> ResponseStream[UpdateT, FinalT]:
2041
+ await self._resolve_stream_with_pull_contexts()
2042
+ return self
2043
+
2044
+ return _wrap().__await__()
2045
+
2046
+ async def get_final_response(self) -> FinalT:
2047
+ """Get the final response by applying the finalizer to all collected updates.
2048
+
2049
+ If a finalizer is configured, it receives the list of updates and returns the final type.
2050
+ Result hooks are then applied in order to transform the result.
2051
+
2052
+ If no finalizer is configured, returns the collected updates as Sequence[UpdateT].
2053
+
2054
+ For wrapped streams (created via .map() or .from_awaitable()):
2055
+ - The inner stream's finalizer is called first to produce the inner final result.
2056
+ - The inner stream's result_hooks are then applied to that inner result.
2057
+ - The outer stream's finalizer is called to convert the outer (mapped) updates to the final type.
2058
+ - The outer stream's result_hooks are then applied to transform the outer result.
2059
+
2060
+ This ensures that post-processing hooks registered on the inner stream (e.g., context
2061
+ provider notifications) are still executed even when the stream is wrapped/mapped.
2062
+ """
2063
+ if self._abandoned and not self._finalized:
2064
+ raise RuntimeError("ResponseStream was closed before completion; no final response is available.")
2065
+ if self._wrap_inner:
2066
+ if self._inner_stream is None:
2067
+ # Use _resolve_stream_with_pull_contexts() so that any spans/contexts
2068
+ # created while resolving the awaitable (e.g. inner telemetry spans)
2069
+ # inherit the same active context as iterator pulls. This also handles
2070
+ # the case where _stream_source and _inner_stream_source are the same
2071
+ # coroutine (e.g., from from_awaitable), avoiding double-await errors.
2072
+ await self._resolve_stream_with_pull_contexts()
2073
+ if self._inner_stream is None:
2074
+ raise RuntimeError("Inner stream not available")
2075
+ if not self._finalized and not self._consumed:
2076
+ # Consume outer stream (which delegates to inner) if not already consumed
2077
+ async for _ in self:
2078
+ pass
2079
+
2080
+ # Re-check: __anext__ auto-finalization may have already finalized this stream
2081
+ if not self._finalized:
2082
+ # This ensures inner post-processing (e.g., context provider notifications) runs
2083
+ # Skip if inner stream was already finalized (e.g., via auto-finalization on iteration)
2084
+ if not self._inner_stream._finalized:
2085
+ inner_stream = self._inner_stream
2086
+ inner_result: Any
2087
+ if inner_stream._finalizer is not None:
2088
+ inner_finalizer = inner_stream._finalizer
2089
+ inner_result = await _resolve_maybe_awaitable(inner_finalizer(inner_stream._updates))
2090
+ else:
2091
+ inner_result = list(inner_stream._updates)
2092
+
2093
+ # Run inner stream's result hooks
2094
+ inner_hooks = cast(list[Callable[[Any], Any | Awaitable[Any] | None]], inner_stream._result_hooks)
2095
+ for hook in inner_hooks:
2096
+ hooked_result = await _resolve_maybe_awaitable(hook(inner_result))
2097
+ if hooked_result is not None:
2098
+ inner_result = hooked_result
2099
+ inner_stream._final_result = inner_result
2100
+ inner_stream._finalized = True
2101
+ else:
2102
+ inner_result = self._inner_stream._final_result
2103
+
2104
+ # Now finalize the outer stream with its own finalizer
2105
+ # If outer has no finalizer, use inner's result (preserves from_awaitable behavior)
2106
+ outer_result: Any
2107
+ if self._finalizer is not None:
2108
+ outer_result = await _resolve_maybe_awaitable(self._finalizer(self._updates))
2109
+ else:
2110
+ # No outer finalizer - use inner's finalized result
2111
+ outer_result = inner_result
2112
+
2113
+ # Apply outer's result_hooks
2114
+ outer_hooks = cast(list[Callable[[Any], Any | Awaitable[Any] | None]], self._result_hooks)
2115
+ for hook in outer_hooks:
2116
+ outer_hook_result = await _resolve_maybe_awaitable(hook(outer_result))
2117
+ if outer_hook_result is not None:
2118
+ outer_result = outer_hook_result
2119
+ self._final_result = outer_result
2120
+ self._finalized = True
2121
+ return cast("FinalT", self._final_result)
2122
+
2123
+ if not self._finalized and not self._consumed:
2124
+ async for _ in self:
2125
+ pass
2126
+
2127
+ # Re-check: __anext__ auto-finalization may have already finalized this stream
2128
+ if not self._finalized:
2129
+ result: Any
2130
+ if self._finalizer is not None:
2131
+ result = await _resolve_maybe_awaitable(self._finalizer(self._updates))
2132
+ else:
2133
+ result = list(self._updates)
2134
+
2135
+ final_hooks = cast(list[Callable[[Any], Any | Awaitable[Any] | None]], self._result_hooks)
2136
+ for hook in final_hooks:
2137
+ final_hook_result = await _resolve_maybe_awaitable(hook(result))
2138
+ if final_hook_result is not None:
2139
+ result = final_hook_result
2140
+ self._final_result = result
2141
+ self._finalized = True
2142
+ return cast("FinalT", self._final_result)
2143
+
2144
+ def with_transform_hook(
2145
+ self,
2146
+ hook: Callable[[UpdateT], UpdateT | Awaitable[UpdateT | None] | None],
2147
+ ) -> ResponseStream[UpdateT, FinalT]:
2148
+ """Register a transform hook executed for each update during iteration."""
2149
+ self._transform_hooks.append(hook)
2150
+ return self
2151
+
2152
+ def with_update_filter(
2153
+ self,
2154
+ update_filter: Callable[[UpdateT], UpdateT],
2155
+ ) -> ResponseStream[UpdateT, FinalT]:
2156
+ """Register a synchronous filter applied to each update BEFORE accumulation.
2157
+
2158
+ Transform hooks run after an update joins ``updates`` and shape only
2159
+ what consumers iterate; an update filter runs before accumulation, so
2160
+ the finalizer's assembly, every result hook, transform hooks, and
2161
+ iteration all observe the same filtered sequence. Wrapped streams
2162
+ push filters down to the innermost stream — whose finalizer produces
2163
+ the final response — and apply them exactly once, at the level that
2164
+ pulls from the raw update source (mapped streams apply theirs
2165
+ locally, after the map transform, matching the mapped update type).
2166
+ The filter must return an update; return a modified copy rather than
2167
+ mutating the argument, so producer-held aliases keep their shape.
2168
+
2169
+ Push-down follows ``ResponseStream`` sources only. A stream whose
2170
+ source is a plain async generator that itself drains ANOTHER stream
2171
+ and replays its updates (a semantic proxy, e.g. response-validation
2172
+ middleware) is a boundary push-down cannot cross — a filter
2173
+ registered on the proxy shapes only the replayed updates, never the
2174
+ drained stream's assembly. To reach beneath such proxies, deliver
2175
+ the filter on the request path instead
2176
+ (``ChatContext.stream_update_filters`` — the middleware pipeline
2177
+ attaches it to the stream its final handler resolves).
2178
+ """
2179
+ self._update_filters.append(update_filter)
2180
+ if self._map_update is None and isinstance(self._stream, ResponseStream):
2181
+ self._stream.with_update_filter(update_filter)
2182
+ return self
2183
+
2184
+ def with_result_hook(
2185
+ self,
2186
+ hook: Callable[[FinalT], FinalT | Awaitable[FinalT | None] | None],
2187
+ ) -> ResponseStream[UpdateT, FinalT]:
2188
+ """Register a result hook executed after finalization."""
2189
+ self._result_hooks.append(hook)
2190
+ self._finalized = False
2191
+ self._final_result = None
2192
+ return self
2193
+
2194
+ def with_cleanup_hook(
2195
+ self,
2196
+ hook: Callable[[], Awaitable[None] | None],
2197
+ ) -> ResponseStream[UpdateT, FinalT]:
2198
+ """Register a cleanup hook executed after stream consumption (before finalizer)."""
2199
+ self._cleanup_hooks.append(hook)
2200
+ return self
2201
+
2202
+ def with_pull_context_manager(
2203
+ self,
2204
+ cm_factory: Callable[[], contextlib.AbstractContextManager[Any]],
2205
+ ) -> ResponseStream[UpdateT, FinalT]:
2206
+ """Register a context manager factory invoked around each underlying iterator pull.
2207
+
2208
+ The factory is called once per ``__anext__`` and the returned context manager wraps
2209
+ the await of the underlying iterator. This is useful for state that needs to be
2210
+ active while the inner async work runs - for example, attaching an OpenTelemetry
2211
+ span to the current context so child spans created by inner code (HTTP clients,
2212
+ tool execution) are correctly parented.
2213
+
2214
+ Because the context manager is entered and exited within the same ``__anext__``
2215
+ invocation, attach/detach style operations remain symmetric in the same async
2216
+ context regardless of where the stream is iterated.
2217
+ """
2218
+ self._pull_context_manager_factories.append(cm_factory)
2219
+ return self
2220
+
2221
+ async def _run_cleanup_hooks(self) -> None:
2222
+ if self._cleanup_run:
2223
+ return
2224
+ self._cleanup_run = True
2225
+ hook_errors: list[BaseException] = []
2226
+ for hook in self._cleanup_hooks:
2227
+ try:
2228
+ result = hook()
2229
+ if isawaitable(result):
2230
+ await result
2231
+ except BaseException as exc:
2232
+ hook_errors.append(exc)
2233
+ if hook_errors:
2234
+ _raise_stream_teardown_errors(hook_errors)
2235
+
2236
+ @property
2237
+ def updates(self) -> Sequence[UpdateT]:
2238
+ return self._updates
2239
+
2240
+
2241
+ # region ChatOptions
2242
+
2243
+
2244
+ class ToolMode(TypedDict, total=False):
2245
+ """Tool choice mode for the chat options.
2246
+
2247
+ Fields:
2248
+ mode: One of "auto", "required", or "none".
2249
+ required_function_name: Optional function name when `mode == "required"`.
2250
+ allowed_tools: Optional list of tool names when `mode` is `"auto"` or `"required"`.
2251
+ """
2252
+
2253
+ mode: Literal["auto", "required", "none"]
2254
+ required_function_name: str
2255
+ allowed_tools: list[str]
2256
+
2257
+
2258
+ # region TypedDict-based Chat Options
2259
+
2260
+
2261
+ class _ChatOptionsBase(TypedDict, total=False):
2262
+ """Common request settings for AI services as a TypedDict.
2263
+
2264
+ All fields are optional (total=False) to allow partial specification.
2265
+ Provider-specific TypedDicts extend this with additional options.
2266
+
2267
+ These options represent the common denominator across chat providers.
2268
+ Individual implementations may raise errors for unsupported options.
2269
+
2270
+ Examples:
2271
+ .. code-block:: python
2272
+
2273
+ from chrys.kernel._types import ChatOptions, ToolMode
2274
+
2275
+ # Type-safe options
2276
+ options: ChatOptions = {
2277
+ "temperature": 0.7,
2278
+ "max_tokens": 1000,
2279
+ "model": "gpt-4",
2280
+ }
2281
+
2282
+ # With tools
2283
+ options_with_tools: ChatOptions = {
2284
+ "model": "gpt-4",
2285
+ "tool_choice": "auto",
2286
+ "temperature": 0.7,
2287
+ }
2288
+
2289
+ # Used with Unpack for function signatures
2290
+ # async def get_response(self, **options: Unpack[ChatOptions]) -> ChatResponse:
2291
+ """
2292
+
2293
+ # Model selection
2294
+ model: str
2295
+ # Generation parameters
2296
+ temperature: float
2297
+ top_p: float
2298
+ max_tokens: int
2299
+ stop: str | Sequence[str]
2300
+ seed: ReadOnly[int | None]
2301
+ logit_bias: ReadOnly[dict[str | int, float] | None]
2302
+
2303
+ # Penalty parameters
2304
+ frequency_penalty: ReadOnly[float | None]
2305
+ presence_penalty: ReadOnly[float | None]
2306
+
2307
+ # Tool configuration (forward reference to avoid circular import)
2308
+ tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None
2309
+ tool_choice: ToolMode | Literal["auto", "required", "none"]
2310
+ allow_multiple_tool_calls: bool
2311
+
2312
+ # Metadata
2313
+ metadata: dict[str, Any]
2314
+ user: str
2315
+ store: ReadOnly[bool | None]
2316
+ conversation_id: ReadOnly[str | None]
2317
+
2318
+ # System/instructions
2319
+ instructions: str
2320
+
2321
+
2322
+ if TYPE_CHECKING:
2323
+
2324
+ class ChatOptions(_ChatOptionsBase, Generic[ResponseModelT], total=False):
2325
+ response_format: type[ResponseModelT] | Mapping[str, Any] | None
2326
+
2327
+ else:
2328
+ ChatOptions = _ChatOptionsBase
2329
+
2330
+
2331
+ # region Chat Options Utility Functions
2332
+
2333
+
2334
+ async def validate_chat_options(options: dict[str, Any]) -> dict[str, Any]:
2335
+ """Validate and normalize chat options dictionary.
2336
+
2337
+ Validates numeric constraints and converts types as needed.
2338
+
2339
+ Args:
2340
+ options: The options dictionary to validate.
2341
+
2342
+ Returns:
2343
+ The validated and normalized options dictionary.
2344
+
2345
+ Raises:
2346
+ ValueError: If any option value is invalid.
2347
+
2348
+ Examples:
2349
+ .. code-block:: python
2350
+
2351
+ from chrys.kernel._types import validate_chat_options
2352
+
2353
+ options = await validate_chat_options({
2354
+ "temperature": 0.7,
2355
+ "max_tokens": 1000,
2356
+ })
2357
+ """
2358
+ result = dict(options) # Make a copy
2359
+
2360
+ # Validate numeric constraints
2361
+ if (freq_pen := result.get("frequency_penalty")) is not None:
2362
+ if not (-2.0 <= freq_pen <= 2.0):
2363
+ raise ValueError("frequency_penalty must be between -2.0 and 2.0")
2364
+ result["frequency_penalty"] = float(freq_pen)
2365
+
2366
+ if (pres_pen := result.get("presence_penalty")) is not None:
2367
+ if not (-2.0 <= pres_pen <= 2.0):
2368
+ raise ValueError("presence_penalty must be between -2.0 and 2.0")
2369
+ result["presence_penalty"] = float(pres_pen)
2370
+
2371
+ if (temp := result.get("temperature")) is not None:
2372
+ if not (0.0 <= temp <= 2.0):
2373
+ raise ValueError("temperature must be between 0.0 and 2.0")
2374
+ result["temperature"] = float(temp)
2375
+
2376
+ if (top_p := result.get("top_p")) is not None:
2377
+ if not (0.0 <= top_p <= 1.0):
2378
+ raise ValueError("top_p must be between 0.0 and 1.0")
2379
+ result["top_p"] = float(top_p)
2380
+
2381
+ if (max_tokens := result.get("max_tokens")) is not None and max_tokens <= 0:
2382
+ raise ValueError("max_tokens must be greater than 0")
2383
+
2384
+ # Validate and normalize tools
2385
+ if "tools" in result:
2386
+ result["tools"] = await validate_tools(result["tools"])
2387
+
2388
+ return result
2389
+
2390
+
2391
+ def normalize_tools(
2392
+ tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
2393
+ ) -> list[ToolTypes]:
2394
+ """Normalize tools into a list.
2395
+
2396
+ Converts callables to FunctionTool objects and preserves existing tool objects.
2397
+
2398
+ Args:
2399
+ tools: Tools to normalize - can be a single tool, callable, or sequence.
2400
+
2401
+ Returns:
2402
+ Normalized list of tools.
2403
+
2404
+ Examples:
2405
+ .. code-block:: python
2406
+
2407
+ from chrys.kernel import normalize_tools, tool
2408
+
2409
+
2410
+ @tool
2411
+ def my_tool(x: int) -> int:
2412
+ return x * 2
2413
+
2414
+
2415
+ # Single tool
2416
+ tools = normalize_tools(my_tool)
2417
+
2418
+ # List of tools
2419
+ tools = normalize_tools([my_tool, another_tool])
2420
+ """
2421
+ from .tools import normalize_tools as _normalize_tools
2422
+
2423
+ return _normalize_tools(tools)
2424
+
2425
+
2426
+ async def validate_tools(
2427
+ tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
2428
+ ) -> list[ToolTypes]:
2429
+ """Validate and normalize tools into a list.
2430
+
2431
+ Converts callables to FunctionTool objects, expands MCP tools to their constituent
2432
+ functions (connecting them if needed), while preserving non-callable tool objects.
2433
+
2434
+ Args:
2435
+ tools: Tools to validate - can be a single tool, callable, or sequence.
2436
+
2437
+ Returns:
2438
+ Normalized list of tools, or None if no tools provided.
2439
+
2440
+ Examples:
2441
+ .. code-block:: python
2442
+
2443
+ from chrys.kernel import tool
2444
+ from chrys.kernel._types import validate_tools
2445
+
2446
+
2447
+ @tool
2448
+ def my_tool(x: int) -> int:
2449
+ return x * 2
2450
+
2451
+
2452
+ # Single tool
2453
+ tools = await validate_tools(my_tool)
2454
+
2455
+ # List of tools
2456
+ tools = await validate_tools([my_tool, another_tool])
2457
+ """
2458
+ # Use normalize_tools for common sync logic (converts callables to FunctionTool)
2459
+ normalized = normalize_tools(tools)
2460
+
2461
+ expander = _get_tool_expander()
2462
+ final_tools: list[ToolTypes] = []
2463
+ for tool_ in normalized:
2464
+ expanded = await expander(tool_) if expander is not None else None
2465
+ if expanded is None:
2466
+ final_tools.append(tool_)
2467
+ else:
2468
+ final_tools.extend(expanded)
2469
+
2470
+ return final_tools
2471
+
2472
+
2473
+ def validate_tool_mode(
2474
+ tool_choice: ToolMode | Literal["auto", "required", "none"] | None,
2475
+ ) -> ToolMode | None:
2476
+ """Validate and normalize tool_choice to a ToolMode dict.
2477
+
2478
+ Args:
2479
+ tool_choice: The tool choice value to validate.
2480
+
2481
+ Returns:
2482
+ A ToolMode dict (contains keys: "mode", and optionally
2483
+ "required_function_name" or "allowed_tools"), or ``None`` when not provided.
2484
+
2485
+ Raises:
2486
+ ContentError: If the tool_choice string is invalid.
2487
+ """
2488
+ if tool_choice is None:
2489
+ return None
2490
+ if isinstance(tool_choice, str):
2491
+ if tool_choice not in ("auto", "required", "none"):
2492
+ raise ContentError(f"Invalid tool choice: {tool_choice}")
2493
+ return {"mode": tool_choice}
2494
+ if "mode" not in tool_choice:
2495
+ raise ContentError("tool_choice dict must contain 'mode' key")
2496
+ if tool_choice["mode"] not in ("auto", "required", "none"):
2497
+ raise ContentError(f"Invalid tool choice: {tool_choice['mode']}")
2498
+ if tool_choice["mode"] != "required" and "required_function_name" in tool_choice:
2499
+ raise ContentError("tool_choice with mode other than 'required' cannot have 'required_function_name'")
2500
+ if tool_choice["mode"] not in ("auto", "required") and "allowed_tools" in tool_choice:
2501
+ raise ContentError("tool_choice 'allowed_tools' is only valid when mode is 'auto' or 'required'")
2502
+ if "allowed_tools" in tool_choice:
2503
+ allowed_tools = tool_choice["allowed_tools"]
2504
+ if isinstance(allowed_tools, str) or not isinstance(allowed_tools, Sequence):
2505
+ raise ContentError("tool_choice 'allowed_tools' must be a non-string sequence of strings")
2506
+ if not all(isinstance(tool_name, str) for tool_name in allowed_tools):
2507
+ raise ContentError("tool_choice 'allowed_tools' must contain only strings")
2508
+ normalized_tool_choice = dict(tool_choice)
2509
+ normalized_tool_choice["allowed_tools"] = list(allowed_tools)
2510
+ return cast(ToolMode, normalized_tool_choice)
2511
+ return tool_choice
2512
+
2513
+
2514
+ def merge_chat_options(
2515
+ base: dict[str, Any] | None,
2516
+ override: dict[str, Any] | None,
2517
+ ) -> dict[str, Any]:
2518
+ """Merge two chat options dictionaries.
2519
+
2520
+ Values from override take precedence over base.
2521
+ Lists and dicts are combined (not replaced).
2522
+ Instructions are concatenated with newlines.
2523
+
2524
+ Args:
2525
+ base: The base options dictionary.
2526
+ override: The override options dictionary.
2527
+
2528
+ Returns:
2529
+ A new merged options dictionary.
2530
+
2531
+ Examples:
2532
+ .. code-block:: python
2533
+
2534
+ from chrys.kernel._types import merge_chat_options
2535
+
2536
+ base = {"temperature": 0.5, "model": "gpt-4"}
2537
+ override = {"temperature": 0.7, "max_tokens": 1000}
2538
+ merged = merge_chat_options(base, override)
2539
+ # {"temperature": 0.7, "model": "gpt-4", "max_tokens": 1000}
2540
+ """
2541
+ if not base:
2542
+ return dict(override) if override else {}
2543
+ if not override:
2544
+ return dict(base)
2545
+
2546
+ result: dict[str, Any] = {}
2547
+
2548
+ # Copy base values (shallow copy for simple values, dict copy for dicts)
2549
+ for key, value in base.items():
2550
+ if isinstance(value, dict):
2551
+ result[key] = dict(value) # type: ignore[reportUnknownArgumentType]
2552
+ elif isinstance(value, list):
2553
+ result[key] = list(value) # type: ignore[reportUnknownArgumentType]
2554
+ else:
2555
+ result[key] = value
2556
+
2557
+ # Apply overrides
2558
+ for key, value in override.items():
2559
+ if value is None:
2560
+ continue
2561
+
2562
+ if key == "instructions":
2563
+ # Concatenate instructions
2564
+ base_instructions = result.get("instructions")
2565
+ if base_instructions:
2566
+ result["instructions"] = f"{base_instructions}\n{value}"
2567
+ else:
2568
+ result["instructions"] = value
2569
+ elif key == "tools":
2570
+ # Merge tools lists
2571
+ base_tools = result.get("tools")
2572
+ if base_tools and value:
2573
+ # Add tools that aren't already present
2574
+ merged_tools = list(base_tools)
2575
+ for tool in value if isinstance(value, Iterable) else [value]: # type: ignore[reportUnknownVariableType]
2576
+ if tool not in merged_tools:
2577
+ merged_tools.append(tool)
2578
+ result["tools"] = merged_tools
2579
+ elif value:
2580
+ result["tools"] = value if isinstance(value, list) else [value]
2581
+ elif key in ("logit_bias", "metadata", "additional_properties"):
2582
+ # Merge dicts
2583
+ base_dict = result.get(key)
2584
+ if base_dict and isinstance(base_dict, dict) and isinstance(value, dict):
2585
+ result[key] = {**base_dict, **value}
2586
+ elif value:
2587
+ result[key] = dict(cast(Mapping[Any, Any], value)) if isinstance(value, dict) else value
2588
+ elif key == "tool_choice":
2589
+ # tool_choice from override takes precedence
2590
+ result["tool_choice"] = value or result.get("tool_choice")
2591
+ elif key == "response_format":
2592
+ # response_format from override takes precedence if set
2593
+ result["response_format"] = value
2594
+ else:
2595
+ # Simple override
2596
+ result[key] = value
2597
+
2598
+ return result