iCode-TUI 0.29.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1138) hide show
  1. chrys/__init__.py +16 -0
  2. chrys/app/__init__.py +3 -0
  3. chrys/app/acp/__init__.py +3 -0
  4. chrys/app/acp/bridge.py +664 -0
  5. chrys/app/acp/content.py +132 -0
  6. chrys/app/acp/doc/frontend-api.md +425 -0
  7. chrys/app/acp/history.py +585 -0
  8. chrys/app/acp/server.py +1796 -0
  9. chrys/app/acp/session_manager.py +1815 -0
  10. chrys/app/acp/tool_status.py +45 -0
  11. chrys/app/cli/__init__.py +3 -0
  12. chrys/app/cli/acp.py +178 -0
  13. chrys/app/cli/app.py +165 -0
  14. chrys/app/cli/approvals.py +101 -0
  15. chrys/app/cli/headless.py +223 -0
  16. chrys/app/cli/launch_cwd.py +42 -0
  17. chrys/app/cli/profiles.py +298 -0
  18. chrys/app/cli/progress.py +857 -0
  19. chrys/app/cli/run.py +425 -0
  20. chrys/app/cli/serve.py +1593 -0
  21. chrys/app/cli/tool_summary.py +225 -0
  22. chrys/app/cli/trajectory.py +201 -0
  23. chrys/app/cli/workflow.py +601 -0
  24. chrys/app/features/__init__.py +3 -0
  25. chrys/app/features/buddy/__init__.py +3 -0
  26. chrys/app/features/buddy/actions.py +68 -0
  27. chrys/app/features/buddy/animation.py +76 -0
  28. chrys/app/features/buddy/commands.py +196 -0
  29. chrys/app/features/buddy/docs/design.md +187 -0
  30. chrys/app/features/buddy/hatchery.py +80 -0
  31. chrys/app/features/buddy/lifecycle.py +41 -0
  32. chrys/app/features/buddy/model.py +265 -0
  33. chrys/app/features/buddy/pixel_renderer.py +113 -0
  34. chrys/app/features/buddy/pixel_sprites.py +279 -0
  35. chrys/app/features/buddy/portrait.py +76 -0
  36. chrys/app/features/buddy/progression.py +58 -0
  37. chrys/app/features/buddy/replies.py +191 -0
  38. chrys/app/features/buddy/sprites/alien.toml +70 -0
  39. chrys/app/features/buddy/sprites/axolotl.toml +71 -0
  40. chrys/app/features/buddy/sprites/bat.toml +69 -0
  41. chrys/app/features/buddy/sprites/bee.toml +69 -0
  42. chrys/app/features/buddy/sprites/cactus.toml +73 -0
  43. chrys/app/features/buddy/sprites/capybara.toml +70 -0
  44. chrys/app/features/buddy/sprites/cat.toml +69 -0
  45. chrys/app/features/buddy/sprites/crab.toml +70 -0
  46. chrys/app/features/buddy/sprites/dragon.toml +71 -0
  47. chrys/app/features/buddy/sprites/duck.toml +69 -0
  48. chrys/app/features/buddy/sprites/fox.toml +69 -0
  49. chrys/app/features/buddy/sprites/frog.toml +70 -0
  50. chrys/app/features/buddy/sprites/ghost.toml +70 -0
  51. chrys/app/features/buddy/sprites/goose.toml +69 -0
  52. chrys/app/features/buddy/sprites/jelly.toml +70 -0
  53. chrys/app/features/buddy/sprites/llama.toml +70 -0
  54. chrys/app/features/buddy/sprites/mushroom.toml +69 -0
  55. chrys/app/features/buddy/sprites/octopus.toml +70 -0
  56. chrys/app/features/buddy/sprites/owl.toml +70 -0
  57. chrys/app/features/buddy/sprites/panda.toml +71 -0
  58. chrys/app/features/buddy/sprites/penguin.toml +69 -0
  59. chrys/app/features/buddy/sprites/rabbit.toml +70 -0
  60. chrys/app/features/buddy/sprites/robot.toml +72 -0
  61. chrys/app/features/buddy/sprites/shark.toml +70 -0
  62. chrys/app/features/buddy/sprites/snail.toml +69 -0
  63. chrys/app/features/buddy/sprites/snake.toml +69 -0
  64. chrys/app/features/buddy/sprites/tabby.toml +69 -0
  65. chrys/app/features/buddy/sprites/turtle.toml +69 -0
  66. chrys/app/features/buddy/store.py +143 -0
  67. chrys/app/features/session_title/__init__.py +7 -0
  68. chrys/app/features/session_title/updater.py +582 -0
  69. chrys/app/installer.py +651 -0
  70. chrys/app/parsing.py +17 -0
  71. chrys/app/tui/__init__.py +3 -0
  72. chrys/app/tui/app.py +1287 -0
  73. chrys/app/tui/behaviors/__init__.py +3 -0
  74. chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
  75. chrys/app/tui/behaviors/insert_clipboard.py +73 -0
  76. chrys/app/tui/behaviors/right_click_copy.py +146 -0
  77. chrys/app/tui/binding_display.py +139 -0
  78. chrys/app/tui/buddy_messages.py +17 -0
  79. chrys/app/tui/buddy_reply.py +86 -0
  80. chrys/app/tui/chrys.tcss +549 -0
  81. chrys/app/tui/clipboard.py +109 -0
  82. chrys/app/tui/copy_messages.py +10 -0
  83. chrys/app/tui/i18n.py +230 -0
  84. chrys/app/tui/language.py +58 -0
  85. chrys/app/tui/notifications/__init__.py +12 -0
  86. chrys/app/tui/notifications/drivers.py +180 -0
  87. chrys/app/tui/notifications/service.py +126 -0
  88. chrys/app/tui/notifications/settings.py +141 -0
  89. chrys/app/tui/screens/__init__.py +3 -0
  90. chrys/app/tui/screens/agents/__init__.py +8 -0
  91. chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
  92. chrys/app/tui/screens/agents/config.py +2142 -0
  93. chrys/app/tui/screens/agents/config.tcss +169 -0
  94. chrys/app/tui/screens/agents/panels/__init__.py +3 -0
  95. chrys/app/tui/screens/agents/panels/acp.py +1176 -0
  96. chrys/app/tui/screens/agents/panels/basic.py +497 -0
  97. chrys/app/tui/screens/agents/panels/compaction.py +231 -0
  98. chrys/app/tui/screens/agents/panels/config_card.py +218 -0
  99. chrys/app/tui/screens/agents/panels/instructions.py +91 -0
  100. chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
  101. chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
  102. chrys/app/tui/screens/agents/panels/memory.py +684 -0
  103. chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
  104. chrys/app/tui/screens/agents/panels/skills.py +806 -0
  105. chrys/app/tui/screens/agents/panels/subagents.py +652 -0
  106. chrys/app/tui/screens/agents/panels/tools.py +205 -0
  107. chrys/app/tui/screens/agents/picker.py +103 -0
  108. chrys/app/tui/screens/agents/skill_paths.py +38 -0
  109. chrys/app/tui/screens/agents/validation_messages.py +452 -0
  110. chrys/app/tui/screens/dialogs/__init__.py +35 -0
  111. chrys/app/tui/screens/dialogs/agent_load.py +834 -0
  112. chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
  113. chrys/app/tui/screens/dialogs/app_mode.py +55 -0
  114. chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
  115. chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
  116. chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
  117. chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
  118. chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
  119. chrys/app/tui/screens/dialogs/approval/body.py +110 -0
  120. chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
  121. chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
  122. chrys/app/tui/screens/dialogs/ask_user.py +148 -0
  123. chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
  124. chrys/app/tui/screens/dialogs/base.py +84 -0
  125. chrys/app/tui/screens/dialogs/confirm.py +184 -0
  126. chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
  127. chrys/app/tui/screens/dialogs/connection_test.py +183 -0
  128. chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
  129. chrys/app/tui/screens/dialogs/diagram.py +102 -0
  130. chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
  131. chrys/app/tui/screens/dialogs/editor.py +378 -0
  132. chrys/app/tui/screens/dialogs/editor.tcss +132 -0
  133. chrys/app/tui/screens/dialogs/file_picker.py +582 -0
  134. chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
  135. chrys/app/tui/screens/dialogs/fork_session.py +219 -0
  136. chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
  137. chrys/app/tui/screens/dialogs/image_compression.py +102 -0
  138. chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
  139. chrys/app/tui/screens/dialogs/man_page.py +172 -0
  140. chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
  141. chrys/app/tui/screens/dialogs/new_folder.py +277 -0
  142. chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
  143. chrys/app/tui/screens/dialogs/notice.tcss +14 -0
  144. chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
  145. chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
  146. chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
  147. chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
  148. chrys/app/tui/screens/dialogs/session_title.py +110 -0
  149. chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
  150. chrys/app/tui/screens/dialogs/tool_view.py +207 -0
  151. chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
  152. chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
  153. chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
  154. chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
  155. chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
  156. chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
  157. chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
  158. chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
  159. chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
  160. chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
  161. chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
  162. chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
  163. chrys/app/tui/screens/diff/__init__.py +22 -0
  164. chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
  165. chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
  166. chrys/app/tui/screens/diff/screen.py +1532 -0
  167. chrys/app/tui/screens/diff/screen.tcss +104 -0
  168. chrys/app/tui/screens/guides/__init__.py +7 -0
  169. chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
  170. chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
  171. chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
  172. chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
  173. chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
  174. chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
  175. chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
  176. chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
  177. chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
  178. chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
  179. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
  180. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
  181. chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
  182. chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
  183. chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
  184. chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
  185. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
  186. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
  187. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
  188. chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
  189. chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
  190. chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
  191. chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
  192. chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
  193. chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
  194. chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
  195. chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
  196. chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
  197. chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
  198. chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
  199. chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
  200. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
  201. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
  202. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
  203. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
  204. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
  205. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
  206. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
  207. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
  208. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
  209. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
  210. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
  211. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
  212. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
  213. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
  214. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
  215. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
  216. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
  217. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
  218. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
  219. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
  220. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
  221. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
  222. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
  223. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
  224. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
  225. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
  226. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
  227. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
  228. chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
  229. chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
  230. chrys/app/tui/screens/guides/index.py +215 -0
  231. chrys/app/tui/screens/guides/reader.py +173 -0
  232. chrys/app/tui/screens/guides/screen.py +442 -0
  233. chrys/app/tui/screens/guides/screen.tcss +52 -0
  234. chrys/app/tui/screens/logs/__init__.py +7 -0
  235. chrys/app/tui/screens/logs/screen.py +504 -0
  236. chrys/app/tui/screens/logs/screen.tcss +45 -0
  237. chrys/app/tui/screens/main/__init__.py +7 -0
  238. chrys/app/tui/screens/main/action_availability.py +30 -0
  239. chrys/app/tui/screens/main/buddy_command.py +143 -0
  240. chrys/app/tui/screens/main/commands.py +1073 -0
  241. chrys/app/tui/screens/main/config_actions.py +464 -0
  242. chrys/app/tui/screens/main/copy_actions.py +117 -0
  243. chrys/app/tui/screens/main/dialog_controllers.py +826 -0
  244. chrys/app/tui/screens/main/dialog_gateway.py +166 -0
  245. chrys/app/tui/screens/main/diff_controller.py +478 -0
  246. chrys/app/tui/screens/main/engine_read_model.py +184 -0
  247. chrys/app/tui/screens/main/event_handlers.py +2014 -0
  248. chrys/app/tui/screens/main/input_flow.py +241 -0
  249. chrys/app/tui/screens/main/live_diff.py +244 -0
  250. chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
  251. chrys/app/tui/screens/main/model_indicator.py +217 -0
  252. chrys/app/tui/screens/main/navigation.py +343 -0
  253. chrys/app/tui/screens/main/ports.py +571 -0
  254. chrys/app/tui/screens/main/recent_dirs.py +123 -0
  255. chrys/app/tui/screens/main/rollback_controller.py +586 -0
  256. chrys/app/tui/screens/main/runtime_info.py +71 -0
  257. chrys/app/tui/screens/main/screen.py +2265 -0
  258. chrys/app/tui/screens/main/screen.tcss +21 -0
  259. chrys/app/tui/screens/main/session_handlers.py +1127 -0
  260. chrys/app/tui/screens/main/session_title.py +239 -0
  261. chrys/app/tui/screens/main/settings_coordinator.py +365 -0
  262. chrys/app/tui/screens/main/settings_persistence.py +184 -0
  263. chrys/app/tui/screens/main/shell_mode.py +112 -0
  264. chrys/app/tui/screens/main/state.py +289 -0
  265. chrys/app/tui/screens/main/submit_state.py +11 -0
  266. chrys/app/tui/screens/main/subscriptions.py +177 -0
  267. chrys/app/tui/screens/main/suggestions.py +982 -0
  268. chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
  269. chrys/app/tui/screens/main/view_adapter.py +1567 -0
  270. chrys/app/tui/screens/main/workflow_browser.py +747 -0
  271. chrys/app/tui/screens/main/workflow_content.py +248 -0
  272. chrys/app/tui/screens/main/workflow_controller.py +240 -0
  273. chrys/app/tui/screens/main/workflow_feedback.py +137 -0
  274. chrys/app/tui/screens/main/workflow_flow.py +49 -0
  275. chrys/app/tui/screens/main/workflow_run_control.py +456 -0
  276. chrys/app/tui/screens/main/workflow_session_view.py +316 -0
  277. chrys/app/tui/screens/main/workspace_actions.py +272 -0
  278. chrys/app/tui/screens/main/workspace_branch.py +194 -0
  279. chrys/app/tui/screens/menus/__init__.py +8 -0
  280. chrys/app/tui/screens/menus/languages.py +114 -0
  281. chrys/app/tui/screens/models/__init__.py +7 -0
  282. chrys/app/tui/screens/models/picker.py +147 -0
  283. chrys/app/tui/screens/models/screen.py +2043 -0
  284. chrys/app/tui/screens/models/screen.tcss +255 -0
  285. chrys/app/tui/screens/sessions/__init__.py +8 -0
  286. chrys/app/tui/screens/sessions/presenter.py +586 -0
  287. chrys/app/tui/screens/sessions/screen.py +779 -0
  288. chrys/app/tui/screens/sessions/screen.tcss +127 -0
  289. chrys/app/tui/screens/settings/__init__.py +29 -0
  290. chrys/app/tui/screens/settings/danger.py +26 -0
  291. chrys/app/tui/screens/settings/dialog.py +281 -0
  292. chrys/app/tui/screens/settings/layout.py +519 -0
  293. chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
  294. chrys/app/tui/screens/settings/panes/__init__.py +3 -0
  295. chrys/app/tui/screens/settings/panes/notifications.py +248 -0
  296. chrys/app/tui/screens/settings/panes/sessions.py +148 -0
  297. chrys/app/tui/screens/settings/ports.py +55 -0
  298. chrys/app/tui/screens/settings/provenance.py +102 -0
  299. chrys/app/tui/screens/settings/rows.py +546 -0
  300. chrys/app/tui/screens/settings/settings.tcss +406 -0
  301. chrys/app/tui/screens/themes/__init__.py +8 -0
  302. chrys/app/tui/screens/themes/dialogs.py +330 -0
  303. chrys/app/tui/screens/themes/editor.py +632 -0
  304. chrys/app/tui/screens/themes/editor.tcss +268 -0
  305. chrys/app/tui/screens/themes/messages.py +95 -0
  306. chrys/app/tui/screens/themes/palette.py +694 -0
  307. chrys/app/tui/screens/themes/panel.py +113 -0
  308. chrys/app/tui/screens/themes/picker.py +203 -0
  309. chrys/app/tui/screens/themes/save.py +191 -0
  310. chrys/app/tui/support/__init__.py +3 -0
  311. chrys/app/tui/support/file_manager.py +64 -0
  312. chrys/app/tui/support/gc_freeze.py +1114 -0
  313. chrys/app/tui/support/path_shortcuts.py +50 -0
  314. chrys/app/tui/support/prompt_history.py +298 -0
  315. chrys/app/tui/support/workspace_mru.py +476 -0
  316. chrys/app/tui/terminal/__init__.py +6 -0
  317. chrys/app/tui/terminal/_pty_backend.py +114 -0
  318. chrys/app/tui/terminal/emulator/__init__.py +24 -0
  319. chrys/app/tui/terminal/emulator/buffer.py +456 -0
  320. chrys/app/tui/terminal/emulator/charsets.py +113 -0
  321. chrys/app/tui/terminal/emulator/core.py +963 -0
  322. chrys/app/tui/terminal/emulator/input.py +342 -0
  323. chrys/app/tui/terminal/emulator/parser.py +227 -0
  324. chrys/app/tui/terminal/emulator/pen.py +158 -0
  325. chrys/app/tui/terminal/launcher.py +455 -0
  326. chrys/app/tui/terminal/leaked_reports.py +44 -0
  327. chrys/app/tui/terminal/panel.py +323 -0
  328. chrys/app/tui/terminal/pty_process.py +405 -0
  329. chrys/app/tui/terminal/rendering.py +151 -0
  330. chrys/app/tui/terminal/shell.py +273 -0
  331. chrys/app/tui/terminal/shell_integration.py +199 -0
  332. chrys/app/tui/terminal/title.py +224 -0
  333. chrys/app/tui/terminal/widget.py +654 -0
  334. chrys/app/tui/theme.py +422 -0
  335. chrys/app/tui/theme_loader.py +201 -0
  336. chrys/app/tui/theme_stylesheet.py +49 -0
  337. chrys/app/tui/themes/__init__.py +3 -0
  338. chrys/app/tui/themes/document.py +120 -0
  339. chrys/app/tui/themes/preview.py +170 -0
  340. chrys/app/tui/themes/store.py +176 -0
  341. chrys/app/tui/toasts.py +69 -0
  342. chrys/app/tui/util/__init__.py +3 -0
  343. chrys/app/tui/util/context_pressure.py +146 -0
  344. chrys/app/tui/util/debug_log.py +292 -0
  345. chrys/app/tui/util/diff_entries.py +64 -0
  346. chrys/app/tui/util/formatting.py +53 -0
  347. chrys/app/tui/util/git_branch.py +326 -0
  348. chrys/app/tui/util/invocation_progress.py +57 -0
  349. chrys/app/tui/util/logo.py +32 -0
  350. chrys/app/tui/util/message_gate.py +35 -0
  351. chrys/app/tui/util/mutation_sources.py +14 -0
  352. chrys/app/tui/util/picker_text.py +27 -0
  353. chrys/app/tui/util/removal.py +47 -0
  354. chrys/app/tui/util/rich_style.py +36 -0
  355. chrys/app/tui/util/source_text.py +12 -0
  356. chrys/app/tui/util/static_update.py +88 -0
  357. chrys/app/tui/util/todo_format.py +51 -0
  358. chrys/app/tui/util/visibility.py +214 -0
  359. chrys/app/tui/widgets/__init__.py +60 -0
  360. chrys/app/tui/widgets/ask_user_controls.py +652 -0
  361. chrys/app/tui/widgets/ask_user_markdown.py +17 -0
  362. chrys/app/tui/widgets/ask_user_prompt.py +606 -0
  363. chrys/app/tui/widgets/buttons.py +52 -0
  364. chrys/app/tui/widgets/chat/__init__.py +3 -0
  365. chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
  366. chrys/app/tui/widgets/chat/chrome.py +296 -0
  367. chrys/app/tui/widgets/chat/compaction_card.py +353 -0
  368. chrys/app/tui/widgets/chat/context_fold.py +77 -0
  369. chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
  370. chrys/app/tui/widgets/chat/image_preview.py +214 -0
  371. chrys/app/tui/widgets/chat/live_renderer.py +383 -0
  372. chrys/app/tui/widgets/chat/messages.py +1039 -0
  373. chrys/app/tui/widgets/chat/panel.py +1769 -0
  374. chrys/app/tui/widgets/chat/ports.py +245 -0
  375. chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
  376. chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
  377. chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
  378. chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
  379. chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
  380. chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
  381. chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
  382. chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
  383. chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
  384. chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
  385. chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
  386. chrys/app/tui/widgets/chat/renderers/search.py +348 -0
  387. chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
  388. chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
  389. chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
  390. chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
  391. chrys/app/tui/widgets/chat/renderers/web.py +69 -0
  392. chrys/app/tui/widgets/chat/replay.py +1605 -0
  393. chrys/app/tui/widgets/chat/replay_mount.py +322 -0
  394. chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
  395. chrys/app/tui/widgets/chat/selection_controller.py +435 -0
  396. chrys/app/tui/widgets/chat/session_json.py +508 -0
  397. chrys/app/tui/widgets/chat/toc_model.py +156 -0
  398. chrys/app/tui/widgets/chat/tool_call.py +2478 -0
  399. chrys/app/tui/widgets/chat/tool_registry.py +753 -0
  400. chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
  401. chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
  402. chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
  403. chrys/app/tui/widgets/checkbox.py +24 -0
  404. chrys/app/tui/widgets/chrome/__init__.py +3 -0
  405. chrys/app/tui/widgets/chrome/app_header.py +283 -0
  406. chrys/app/tui/widgets/chrome/commands.py +95 -0
  407. chrys/app/tui/widgets/chrome/file_index.py +68 -0
  408. chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
  409. chrys/app/tui/widgets/chrome/footer.py +284 -0
  410. chrys/app/tui/widgets/chrome/image_paste.py +351 -0
  411. chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
  412. chrys/app/tui/widgets/chrome/status_bar.py +911 -0
  413. chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
  414. chrys/app/tui/widgets/click_affordance.py +24 -0
  415. chrys/app/tui/widgets/color_picker/__init__.py +254 -0
  416. chrys/app/tui/widgets/color_picker/controls.py +171 -0
  417. chrys/app/tui/widgets/color_picker/model.py +89 -0
  418. chrys/app/tui/widgets/dialog_buttons.py +58 -0
  419. chrys/app/tui/widgets/dialog_scroll.py +24 -0
  420. chrys/app/tui/widgets/diff_view/__init__.py +20 -0
  421. chrys/app/tui/widgets/diff_view/cells.py +59 -0
  422. chrys/app/tui/widgets/diff_view/code.py +280 -0
  423. chrys/app/tui/widgets/diff_view/compute.py +286 -0
  424. chrys/app/tui/widgets/diff_view/gutter.py +161 -0
  425. chrys/app/tui/widgets/diff_view/inline.py +77 -0
  426. chrys/app/tui/widgets/diff_view/palette.py +87 -0
  427. chrys/app/tui/widgets/diff_view/protocols.py +24 -0
  428. chrys/app/tui/widgets/diff_view/rows.py +138 -0
  429. chrys/app/tui/widgets/diff_view/unified.py +267 -0
  430. chrys/app/tui/widgets/diff_view/widget.py +209 -0
  431. chrys/app/tui/widgets/editor/__init__.py +79 -0
  432. chrys/app/tui/widgets/editor/coordinates.py +30 -0
  433. chrys/app/tui/widgets/editor/highlighter.py +139 -0
  434. chrys/app/tui/widgets/editor/keymaps.py +357 -0
  435. chrys/app/tui/widgets/editor/types.py +216 -0
  436. chrys/app/tui/widgets/editor/vim.py +1195 -0
  437. chrys/app/tui/widgets/editor/widget.py +401 -0
  438. chrys/app/tui/widgets/file_preview.py +58 -0
  439. chrys/app/tui/widgets/hatch.py +138 -0
  440. chrys/app/tui/widgets/input.py +66 -0
  441. chrys/app/tui/widgets/loading.py +125 -0
  442. chrys/app/tui/widgets/markdown/__init__.py +25 -0
  443. chrys/app/tui/widgets/markdown/_utils.py +21 -0
  444. chrys/app/tui/widgets/markdown/anchors.py +71 -0
  445. chrys/app/tui/widgets/markdown/blocks.py +115 -0
  446. chrys/app/tui/widgets/markdown/copyable.py +163 -0
  447. chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
  448. chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
  449. chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
  450. chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
  451. chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
  452. chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
  453. chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
  454. chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
  455. chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
  456. chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
  457. chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
  458. chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
  459. chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
  460. chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
  461. chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
  462. chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
  463. chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
  464. chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
  465. chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
  466. chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
  467. chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
  468. chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
  469. chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
  470. chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
  471. chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
  472. chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
  473. chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
  474. chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
  475. chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
  476. chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
  477. chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
  478. chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
  479. chrys/app/tui/widgets/markdown/links.py +47 -0
  480. chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
  481. chrys/app/tui/widgets/markdown/math/layout.py +739 -0
  482. chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
  483. chrys/app/tui/widgets/markdown/math/parser.py +325 -0
  484. chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
  485. chrys/app/tui/widgets/markdown/navigator.py +80 -0
  486. chrys/app/tui/widgets/markdown/parser.py +1057 -0
  487. chrys/app/tui/widgets/markdown/stream.py +73 -0
  488. chrys/app/tui/widgets/markdown/toc.py +98 -0
  489. chrys/app/tui/widgets/markdown/viewer.py +146 -0
  490. chrys/app/tui/widgets/markdown/widget.py +2046 -0
  491. chrys/app/tui/widgets/marquee.py +101 -0
  492. chrys/app/tui/widgets/option_menu.py +82 -0
  493. chrys/app/tui/widgets/page_navigator.py +118 -0
  494. chrys/app/tui/widgets/select.py +196 -0
  495. chrys/app/tui/widgets/selection.py +59 -0
  496. chrys/app/tui/widgets/sidebar/__init__.py +3 -0
  497. chrys/app/tui/widgets/sidebar/buddy.py +367 -0
  498. chrys/app/tui/widgets/sidebar/context.py +482 -0
  499. chrys/app/tui/widgets/sidebar/debug.py +195 -0
  500. chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
  501. chrys/app/tui/widgets/sidebar/panel.py +198 -0
  502. chrys/app/tui/widgets/sidebar/tasks.py +198 -0
  503. chrys/app/tui/widgets/sidebar/toc.py +166 -0
  504. chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
  505. chrys/app/tui/widgets/syntax_theme.py +85 -0
  506. chrys/app/tui/widgets/text_area.py +258 -0
  507. chrys/app/tui/widgets/trajectory/__init__.py +8 -0
  508. chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
  509. chrys/app/tui/widgets/trajectory/insights.py +1266 -0
  510. chrys/app/tui/widgets/trajectory/overview.py +1101 -0
  511. chrys/app/tui/widgets/trajectory/panel.py +983 -0
  512. chrys/app/tui/widgets/trajectory/presentation.py +311 -0
  513. chrys/app/tui/widgets/trajectory/session_info.py +124 -0
  514. chrys/app/tui/widgets/trajectory/text_view.py +124 -0
  515. chrys/app/tui/widgets/trajectory/timeline.py +380 -0
  516. chrys/app/tui/widgets/welcome.py +126 -0
  517. chrys/app/tui/widgets/workflow/__init__.py +5 -0
  518. chrys/app/tui/widgets/workflow/edges.py +89 -0
  519. chrys/app/tui/widgets/workflow/graph.py +869 -0
  520. chrys/app/tui/widgets/workflow/info.py +310 -0
  521. chrys/app/tui/widgets/workflow/node_view.py +27 -0
  522. chrys/app/tui/widgets/workflow/output.py +225 -0
  523. chrys/app/tui/widgets/workflow/panel.py +658 -0
  524. chrys/app/tui/widgets/workflow/projector.py +324 -0
  525. chrys/app/tui/widgets/workflow/records.py +69 -0
  526. chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
  527. chrys/app/tui/widgets/workflow/selection.py +151 -0
  528. chrys/app/tui/widgets/workflow/source.py +65 -0
  529. chrys/app/tui/widgets/workflow/text.py +257 -0
  530. chrys/app/tui/widgets/workflow/values.py +317 -0
  531. chrys/foundation/__init__.py +0 -0
  532. chrys/foundation/branding.py +15 -0
  533. chrys/foundation/config/__init__.py +7 -0
  534. chrys/foundation/config/coercion.py +374 -0
  535. chrys/foundation/config/context.py +34 -0
  536. chrys/foundation/config/env_file.py +141 -0
  537. chrys/foundation/config/env_layers.py +370 -0
  538. chrys/foundation/config/migrations.py +521 -0
  539. chrys/foundation/config/notification_events.py +22 -0
  540. chrys/foundation/config/process_settings.py +332 -0
  541. chrys/foundation/config/runtime_pointer.py +133 -0
  542. chrys/foundation/config/settings.py +1952 -0
  543. chrys/foundation/config/settings_store.py +996 -0
  544. chrys/foundation/config/spec.py +396 -0
  545. chrys/foundation/config/user_settings.py +140 -0
  546. chrys/foundation/config/warnings.py +262 -0
  547. chrys/foundation/config/web_values.py +135 -0
  548. chrys/foundation/config/yaml_store.py +487 -0
  549. chrys/foundation/errors/__init__.py +68 -0
  550. chrys/foundation/errors/_legacy.py +117 -0
  551. chrys/foundation/errors/_text.py +13 -0
  552. chrys/foundation/errors/_walk.py +85 -0
  553. chrys/foundation/errors/classify.py +278 -0
  554. chrys/foundation/errors/display.py +274 -0
  555. chrys/foundation/errors/formatting.py +283 -0
  556. chrys/foundation/errors/kinds.py +67 -0
  557. chrys/foundation/errors/network.py +497 -0
  558. chrys/foundation/errors/provider.py +445 -0
  559. chrys/foundation/errors/route.py +85 -0
  560. chrys/foundation/events/__init__.py +8 -0
  561. chrys/foundation/events/bus.py +143 -0
  562. chrys/foundation/events/types.py +1629 -0
  563. chrys/foundation/events/workflow.py +38 -0
  564. chrys/foundation/hosted_tools.py +142 -0
  565. chrys/foundation/i18n/__init__.py +32 -0
  566. chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
  567. chrys/foundation/i18n/catalogs.py +42 -0
  568. chrys/foundation/i18n/formatting.py +202 -0
  569. chrys/foundation/i18n/locale.py +92 -0
  570. chrys/foundation/i18n/localizer.py +187 -0
  571. chrys/foundation/i18n/messages.py +268 -0
  572. chrys/foundation/io/__init__.py +3 -0
  573. chrys/foundation/io/result_content.py +237 -0
  574. chrys/foundation/models/__init__.py +3 -0
  575. chrys/foundation/models/approval_reuse.py +23 -0
  576. chrys/foundation/models/ask_user.py +468 -0
  577. chrys/foundation/models/execution.py +18 -0
  578. chrys/foundation/models/history_markers.py +115 -0
  579. chrys/foundation/models/invocations.py +63 -0
  580. chrys/foundation/models/mutation_scope.py +28 -0
  581. chrys/foundation/models/session_env.py +87 -0
  582. chrys/foundation/models/session_surface.py +29 -0
  583. chrys/foundation/models/todos.py +74 -0
  584. chrys/foundation/models/turns.py +125 -0
  585. chrys/foundation/models/workflow_session.py +89 -0
  586. chrys/foundation/models/workspace.py +59 -0
  587. chrys/foundation/net/__init__.py +3 -0
  588. chrys/foundation/net/address_class.py +57 -0
  589. chrys/foundation/net/route_probe.py +94 -0
  590. chrys/foundation/net/url.py +107 -0
  591. chrys/foundation/observability/__init__.py +3 -0
  592. chrys/foundation/observability/exporters.py +174 -0
  593. chrys/foundation/observability/gate.py +31 -0
  594. chrys/foundation/observability/setup.py +497 -0
  595. chrys/foundation/observability/sink.py +228 -0
  596. chrys/foundation/patches/__init__.py +19 -0
  597. chrys/foundation/patches/mdurl_cache.py +59 -0
  598. chrys/foundation/patches/patcher.py +287 -0
  599. chrys/foundation/patches/staged_members.py +205 -0
  600. chrys/foundation/patches/textual_block_border.py +105 -0
  601. chrys/foundation/patches/textual_button_ansi.py +65 -0
  602. chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
  603. chrys/foundation/patches/textual_callback_cache.py +248 -0
  604. chrys/foundation/patches/textual_callback_dispatch.py +109 -0
  605. chrys/foundation/patches/textual_compositor_cjk.py +566 -0
  606. chrys/foundation/patches/textual_dispatch_cache.py +150 -0
  607. chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
  608. chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
  609. chrys/foundation/patches/textual_lru_acyclic.py +241 -0
  610. chrys/foundation/patches/textual_message_pump.py +136 -0
  611. chrys/foundation/patches/textual_node_diet.py +875 -0
  612. chrys/foundation/patches/textual_one_shot_timer.py +48 -0
  613. chrys/foundation/patches/textual_option_list.py +90 -0
  614. chrys/foundation/patches/textual_precompose.py +89 -0
  615. chrys/foundation/patches/textual_pruned_tabs.py +69 -0
  616. chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
  617. chrys/foundation/patches/textual_removed_node_caches.py +86 -0
  618. chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
  619. chrys/foundation/patches/textual_selection_extract.py +48 -0
  620. chrys/foundation/patches/textual_strip_cycles.py +119 -0
  621. chrys/foundation/patches/textual_tab_selection.py +1611 -0
  622. chrys/foundation/patches/textual_timer_skip.py +70 -0
  623. chrys/foundation/patches/textual_utf8_decoder.py +106 -0
  624. chrys/foundation/patches/textual_win_sleep.py +167 -0
  625. chrys/foundation/patches/watchdog_fsevents.py +70 -0
  626. chrys/foundation/patches/watchdog_windows.py +52 -0
  627. chrys/foundation/platform/__init__.py +556 -0
  628. chrys/foundation/platform/c_api.py +32 -0
  629. chrys/foundation/platform/child_reap.py +107 -0
  630. chrys/foundation/platform/command_line.py +95 -0
  631. chrys/foundation/platform/files.py +1470 -0
  632. chrys/foundation/platform/output_capture.py +113 -0
  633. chrys/foundation/platform/paths.py +193 -0
  634. chrys/foundation/platform/process.py +1468 -0
  635. chrys/foundation/platform/pty_output.py +19 -0
  636. chrys/foundation/platform/runtime_paths.py +281 -0
  637. chrys/foundation/platform/win_console.py +129 -0
  638. chrys/foundation/reasoning_origin.py +67 -0
  639. chrys/foundation/recovery.py +16 -0
  640. chrys/foundation/retry.py +278 -0
  641. chrys/foundation/text/__init__.py +3 -0
  642. chrys/foundation/text/encoding.py +603 -0
  643. chrys/foundation/text/images.py +304 -0
  644. chrys/foundation/text/lines.py +24 -0
  645. chrys/foundation/text/mentions.py +90 -0
  646. chrys/foundation/text/model_json.py +25 -0
  647. chrys/foundation/text/reading_order.py +20 -0
  648. chrys/foundation/text/tokenizer.py +25 -0
  649. chrys/foundation/text/tool_output.py +287 -0
  650. chrys/foundation/text/yaml_io.py +62 -0
  651. chrys/foundation/tool_call_context.py +202 -0
  652. chrys/foundation/tool_execution_stamp.py +184 -0
  653. chrys/foundation/tool_invocation_order.py +48 -0
  654. chrys/foundation/tool_kinds.py +102 -0
  655. chrys/foundation/tool_result_metadata.py +287 -0
  656. chrys/foundation/trajectory/__init__.py +10 -0
  657. chrys/foundation/trajectory/context.py +481 -0
  658. chrys/foundation/trajectory/envelope.py +571 -0
  659. chrys/foundation/trajectory/event_types.py +294 -0
  660. chrys/foundation/trajectory/finalizers.py +46 -0
  661. chrys/foundation/trajectory/fingerprint.py +62 -0
  662. chrys/foundation/trajectory/ids.py +155 -0
  663. chrys/foundation/trajectory/keys.py +99 -0
  664. chrys/foundation/trajectory/lease.py +173 -0
  665. chrys/foundation/trajectory/metadata.py +72 -0
  666. chrys/foundation/trajectory/reader.py +271 -0
  667. chrys/foundation/trajectory/recovery.py +225 -0
  668. chrys/foundation/trajectory/revisions.py +152 -0
  669. chrys/foundation/trajectory/segments.py +225 -0
  670. chrys/foundation/trajectory/tools.py +117 -0
  671. chrys/foundation/trajectory/usage.py +151 -0
  672. chrys/foundation/trajectory/writer.py +972 -0
  673. chrys/foundation/trajectory_timing.py +97 -0
  674. chrys/foundation/util/__init__.py +3 -0
  675. chrys/foundation/util/async_tasks.py +29 -0
  676. chrys/foundation/util/chrys_headers.py +23 -0
  677. chrys/foundation/util/env_templates.py +104 -0
  678. chrys/foundation/util/filenames.py +96 -0
  679. chrys/foundation/util/header_charset.py +138 -0
  680. chrys/foundation/util/httpx_helpers.py +12 -0
  681. chrys/foundation/util/lock.py +236 -0
  682. chrys/foundation/util/once_close.py +46 -0
  683. chrys/foundation/util/session_ids.py +29 -0
  684. chrys/foundation/util/shell_tokens.py +88 -0
  685. chrys/foundation/util/sub_agent_context.py +38 -0
  686. chrys/foundation/util/time.py +35 -0
  687. chrys/foundation/util/unicode_scalars.py +36 -0
  688. chrys/foundation/vendor/__init__.py +43 -0
  689. chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
  690. chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
  691. chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
  692. chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
  693. chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
  694. chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
  695. chrys/kernel/__init__.py +256 -0
  696. chrys/kernel/_content.py +1866 -0
  697. chrys/kernel/_loop_recorder.py +631 -0
  698. chrys/kernel/_null_overlay.py +1026 -0
  699. chrys/kernel/_result_ceiling.py +216 -0
  700. chrys/kernel/_serialization.py +431 -0
  701. chrys/kernel/_tool_arg_errors.py +1629 -0
  702. chrys/kernel/_tool_execution.py +1172 -0
  703. chrys/kernel/_tool_expansion.py +26 -0
  704. chrys/kernel/_types.py +2598 -0
  705. chrys/kernel/agent.py +994 -0
  706. chrys/kernel/client.py +1084 -0
  707. chrys/kernel/compaction.py +1315 -0
  708. chrys/kernel/exceptions.py +211 -0
  709. chrys/kernel/exchanges.py +683 -0
  710. chrys/kernel/identity.py +95 -0
  711. chrys/kernel/images.py +36 -0
  712. chrys/kernel/instrumentation.py +1618 -0
  713. chrys/kernel/loop.py +2308 -0
  714. chrys/kernel/middleware.py +616 -0
  715. chrys/kernel/sessions.py +604 -0
  716. chrys/kernel/tools.py +1061 -0
  717. chrys/kernel/types.py +126 -0
  718. chrys/orchestration/__init__.py +3 -0
  719. chrys/orchestration/engine/__init__.py +3 -0
  720. chrys/orchestration/engine/assembly.py +236 -0
  721. chrys/orchestration/engine/build/__init__.py +3 -0
  722. chrys/orchestration/engine/build/builder.py +1061 -0
  723. chrys/orchestration/engine/build/construction.py +408 -0
  724. chrys/orchestration/engine/build/loaded.py +139 -0
  725. chrys/orchestration/engine/engine.py +789 -0
  726. chrys/orchestration/engine/execution.py +771 -0
  727. chrys/orchestration/engine/loader.py +865 -0
  728. chrys/orchestration/engine/rollback.py +918 -0
  729. chrys/orchestration/engine/run/__init__.py +3 -0
  730. chrys/orchestration/engine/run/active_injection.py +419 -0
  731. chrys/orchestration/engine/run/attachments.py +519 -0
  732. chrys/orchestration/engine/run/bindings.py +1095 -0
  733. chrys/orchestration/engine/run/coordinator.py +1062 -0
  734. chrys/orchestration/engine/run/finalizer.py +417 -0
  735. chrys/orchestration/engine/run/input_refs.py +60 -0
  736. chrys/orchestration/engine/run/prompt_content.py +457 -0
  737. chrys/orchestration/engine/run/resume.py +299 -0
  738. chrys/orchestration/engine/run/retry.py +948 -0
  739. chrys/orchestration/engine/run/runner.py +625 -0
  740. chrys/orchestration/engine/run/runtime_skills.py +146 -0
  741. chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
  742. chrys/orchestration/engine/run/turn_hooks.py +258 -0
  743. chrys/orchestration/engine/run/turn_state.py +149 -0
  744. chrys/orchestration/engine/run/working_dir.py +50 -0
  745. chrys/orchestration/engine/session_lifecycle.py +2013 -0
  746. chrys/orchestration/engine/state/__init__.py +3 -0
  747. chrys/orchestration/engine/state/active_session.py +183 -0
  748. chrys/orchestration/engine/state/controls.py +639 -0
  749. chrys/orchestration/engine/state/current_agent.py +26 -0
  750. chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
  751. chrys/orchestration/engine/state/machine.py +193 -0
  752. chrys/orchestration/engine/state/session_writer.py +434 -0
  753. chrys/orchestration/engine/trajectory.py +714 -0
  754. chrys/orchestration/engine/usage.py +118 -0
  755. chrys/orchestration/invoker/__init__.py +3 -0
  756. chrys/orchestration/invoker/acp.py +727 -0
  757. chrys/orchestration/invoker/acp_protocol.py +1115 -0
  758. chrys/orchestration/invoker/acp_spec.py +79 -0
  759. chrys/orchestration/invoker/attempts.py +994 -0
  760. chrys/orchestration/invoker/child_compaction.py +131 -0
  761. chrys/orchestration/invoker/child_history.py +99 -0
  762. chrys/orchestration/invoker/contracts.py +253 -0
  763. chrys/orchestration/invoker/evidence.py +94 -0
  764. chrys/orchestration/invoker/kernel.py +343 -0
  765. chrys/orchestration/invoker/origin.py +79 -0
  766. chrys/orchestration/invoker/resources.py +343 -0
  767. chrys/orchestration/invoker/runtime.py +299 -0
  768. chrys/orchestration/invoker/usage.py +80 -0
  769. chrys/orchestration/session_hooks.py +117 -0
  770. chrys/orchestration/session_host.py +1023 -0
  771. chrys/orchestration/session_resources.py +95 -0
  772. chrys/orchestration/session_usage.py +219 -0
  773. chrys/orchestration/startup.py +255 -0
  774. chrys/orchestration/sub_agents/__init__.py +3 -0
  775. chrys/orchestration/sub_agents/acp_policy.py +269 -0
  776. chrys/orchestration/sub_agents/kernel_policy.py +848 -0
  777. chrys/orchestration/sub_agents/registration.py +44 -0
  778. chrys/orchestration/sub_agents/shell.py +337 -0
  779. chrys/orchestration/sub_agents/tools.py +2228 -0
  780. chrys/orchestration/workflows/__init__.py +3 -0
  781. chrys/orchestration/workflows/agent_archive.py +156 -0
  782. chrys/orchestration/workflows/agent_node.py +894 -0
  783. chrys/orchestration/workflows/agent_node_build.py +687 -0
  784. chrys/orchestration/workflows/catalog.py +332 -0
  785. chrys/orchestration/workflows/coordinator.py +771 -0
  786. chrys/orchestration/workflows/hooks.py +176 -0
  787. chrys/orchestration/workflows/preview.py +322 -0
  788. chrys/orchestration/workflows/rollback.py +151 -0
  789. chrys/orchestration/workflows/runner.py +986 -0
  790. chrys/orchestration/workflows/session.py +390 -0
  791. chrys/orchestration/workflows/settings.py +97 -0
  792. chrys/orchestration/workflows/validation.py +674 -0
  793. chrys/orchestration/workflows/worker_client.py +1002 -0
  794. chrys/service/__init__.py +3 -0
  795. chrys/service/acp_client/__init__.py +65 -0
  796. chrys/service/acp_client/_transport.py +922 -0
  797. chrys/service/acp_client/client.py +1311 -0
  798. chrys/service/acp_client/errors.py +224 -0
  799. chrys/service/acp_client/protocol.py +220 -0
  800. chrys/service/acp_client/spawn.py +239 -0
  801. chrys/service/acp_client/spec.py +100 -0
  802. chrys/service/agent_middleware/__init__.py +60 -0
  803. chrys/service/agent_middleware/_metadata_keys.py +52 -0
  804. chrys/service/agent_middleware/control/__init__.py +3 -0
  805. chrys/service/agent_middleware/control/approval.py +769 -0
  806. chrys/service/agent_middleware/control/approval_hooks.py +62 -0
  807. chrys/service/agent_middleware/control/ask_user.py +207 -0
  808. chrys/service/agent_middleware/control/interrupt.py +50 -0
  809. chrys/service/agent_middleware/control/sleep.py +118 -0
  810. chrys/service/agent_middleware/control/todo.py +117 -0
  811. chrys/service/agent_middleware/events/__init__.py +3 -0
  812. chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
  813. chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
  814. chrys/service/agent_middleware/events/intermediate_text.py +128 -0
  815. chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
  816. chrys/service/agent_middleware/events/result_persistence.py +98 -0
  817. chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
  818. chrys/service/agent_middleware/events/tool_error_text.py +22 -0
  819. chrys/service/agent_middleware/events/tool_events.py +779 -0
  820. chrys/service/agent_middleware/injection.py +510 -0
  821. chrys/service/agent_middleware/reminders/__init__.py +91 -0
  822. chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
  823. chrys/service/agent_middleware/reminders/base.py +25 -0
  824. chrys/service/agent_middleware/reminders/context_usage.py +74 -0
  825. chrys/service/agent_middleware/reminders/file_change.py +63 -0
  826. chrys/service/agent_middleware/reminders/mcp.py +39 -0
  827. chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
  828. chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
  829. chrys/service/agent_middleware/reminders/skills.py +44 -0
  830. chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
  831. chrys/service/agent_middleware/reminders/todo.py +35 -0
  832. chrys/service/agent_middleware/reminders/turn_line.py +48 -0
  833. chrys/service/agent_middleware/response_validation.py +1380 -0
  834. chrys/service/agent_middleware/system_reminder.py +1107 -0
  835. chrys/service/agent_middleware/validators.py +396 -0
  836. chrys/service/analytics/__init__.py +124 -0
  837. chrys/service/analytics/_actions.py +896 -0
  838. chrys/service/analytics/_context_evidence.py +357 -0
  839. chrys/service/analytics/_critical_path.py +179 -0
  840. chrys/service/analytics/_facts.py +919 -0
  841. chrys/service/analytics/_insights.py +769 -0
  842. chrys/service/analytics/_metric_ops.py +83 -0
  843. chrys/service/analytics/_session_projection.py +460 -0
  844. chrys/service/analytics/_timeline.py +434 -0
  845. chrys/service/analytics/_turn_graph.py +1237 -0
  846. chrys/service/analytics/_turns.py +1377 -0
  847. chrys/service/analytics/_workflows.py +171 -0
  848. chrys/service/analytics/aggregation.py +1474 -0
  849. chrys/service/analytics/classification.py +128 -0
  850. chrys/service/analytics/export.py +803 -0
  851. chrys/service/analytics/findings.py +282 -0
  852. chrys/service/analytics/math.py +79 -0
  853. chrys/service/analytics/model.py +886 -0
  854. chrys/service/analytics/reader.py +321 -0
  855. chrys/service/approval/__init__.py +3 -0
  856. chrys/service/approval/arbitration.py +102 -0
  857. chrys/service/approval/command_identity.py +109 -0
  858. chrys/service/approval/correlation.py +57 -0
  859. chrys/service/approval/grant_store.py +108 -0
  860. chrys/service/approval/judge.py +589 -0
  861. chrys/service/approval/level.py +15 -0
  862. chrys/service/approval/policy.py +209 -0
  863. chrys/service/approval/reuse.py +223 -0
  864. chrys/service/approval/reuse_binding.py +133 -0
  865. chrys/service/approval/safety_classifier.py +536 -0
  866. chrys/service/approval/turn_context.py +49 -0
  867. chrys/service/context/__init__.py +3 -0
  868. chrys/service/context/compaction/__init__.py +38 -0
  869. chrys/service/context/compaction/breaker.py +156 -0
  870. chrys/service/context/compaction/budgets.py +93 -0
  871. chrys/service/context/compaction/calibration.py +77 -0
  872. chrys/service/context/compaction/compression.py +690 -0
  873. chrys/service/context/compaction/current_turn_drop.py +458 -0
  874. chrys/service/context/compaction/events.py +201 -0
  875. chrys/service/context/compaction/exclusions.py +261 -0
  876. chrys/service/context/compaction/groups.py +361 -0
  877. chrys/service/context/compaction/last_words.py +1996 -0
  878. chrys/service/context/compaction/last_words_state.py +752 -0
  879. chrys/service/context/compaction/scoped.py +472 -0
  880. chrys/service/context/compaction/spill.py +1947 -0
  881. chrys/service/context/compaction/strategy.py +1116 -0
  882. chrys/service/context/compaction/summaries.py +319 -0
  883. chrys/service/context/compaction/turns.py +375 -0
  884. chrys/service/context/manager.py +174 -0
  885. chrys/service/context/memory_loader.py +792 -0
  886. chrys/service/context/middleware/__init__.py +3 -0
  887. chrys/service/context/middleware/usage.py +332 -0
  888. chrys/service/context/providers/__init__.py +3 -0
  889. chrys/service/context/providers/context_management.py +432 -0
  890. chrys/service/context/providers/history.py +647 -0
  891. chrys/service/context/providers/memory.py +50 -0
  892. chrys/service/context/tool_content.py +22 -0
  893. chrys/service/hooks/__init__.py +37 -0
  894. chrys/service/hooks/detached_worker.py +118 -0
  895. chrys/service/hooks/docs/README.md +14 -0
  896. chrys/service/hooks/docs/authoring.md +684 -0
  897. chrys/service/hooks/docs/configuration.md +499 -0
  898. chrys/service/hooks/docs/design.md +359 -0
  899. chrys/service/hooks/docs/project-hooks.md +348 -0
  900. chrys/service/hooks/events.py +157 -0
  901. chrys/service/hooks/loader.py +665 -0
  902. chrys/service/hooks/manager.py +807 -0
  903. chrys/service/hooks/matcher.py +111 -0
  904. chrys/service/hooks/outbox.py +294 -0
  905. chrys/service/hooks/runner.py +572 -0
  906. chrys/service/hooks/schema.py +407 -0
  907. chrys/service/llm/__init__.py +7 -0
  908. chrys/service/llm/_structured_outputs.py +192 -0
  909. chrys/service/llm/anthropic_messages/__init__.py +9 -0
  910. chrys/service/llm/anthropic_messages/client.py +254 -0
  911. chrys/service/llm/anthropic_messages/decode.py +331 -0
  912. chrys/service/llm/anthropic_messages/history.py +279 -0
  913. chrys/service/llm/anthropic_messages/request.py +323 -0
  914. chrys/service/llm/anthropic_messages/server_tools.py +319 -0
  915. chrys/service/llm/anthropic_messages/stream.py +297 -0
  916. chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
  917. chrys/service/llm/chat_completions/__init__.py +9 -0
  918. chrys/service/llm/chat_completions/client.py +369 -0
  919. chrys/service/llm/chat_completions/decode.py +338 -0
  920. chrys/service/llm/chat_completions/history.py +657 -0
  921. chrys/service/llm/chat_completions/reasoning.py +244 -0
  922. chrys/service/llm/chat_completions/request.py +192 -0
  923. chrys/service/llm/chat_completions/stream.py +546 -0
  924. chrys/service/llm/chat_completions/validation.py +308 -0
  925. chrys/service/llm/clients.py +613 -0
  926. chrys/service/llm/images.py +65 -0
  927. chrys/service/llm/mock.py +413 -0
  928. chrys/service/llm/observer.py +446 -0
  929. chrys/service/llm/one_shot.py +106 -0
  930. chrys/service/llm/openai_exceptions.py +93 -0
  931. chrys/service/llm/openai_responses/__init__.py +10 -0
  932. chrys/service/llm/openai_responses/client.py +323 -0
  933. chrys/service/llm/openai_responses/decode.py +432 -0
  934. chrys/service/llm/openai_responses/history.py +519 -0
  935. chrys/service/llm/openai_responses/hosted.py +625 -0
  936. chrys/service/llm/openai_responses/replay.py +629 -0
  937. chrys/service/llm/openai_responses/request.py +342 -0
  938. chrys/service/llm/openai_responses/stream.py +847 -0
  939. chrys/service/llm/openai_timestamps.py +57 -0
  940. chrys/service/llm/providers.py +107 -0
  941. chrys/service/llm/proxy_route.py +54 -0
  942. chrys/service/llm/raw_http_log.py +309 -0
  943. chrys/service/llm/route_facts.py +63 -0
  944. chrys/service/llm/route_sessions.py +49 -0
  945. chrys/service/llm/sdk_anthropic.py +18 -0
  946. chrys/service/llm/sdk_headers.py +47 -0
  947. chrys/service/llm/sdk_openai.py +18 -0
  948. chrys/service/llm/sdk_retry.py +59 -0
  949. chrys/service/llm/wire_client.py +246 -0
  950. chrys/service/mcp/__init__.py +3 -0
  951. chrys/service/mcp/_connection.py +235 -0
  952. chrys/service/mcp/_http_transport.py +350 -0
  953. chrys/service/mcp/_progressive.py +459 -0
  954. chrys/service/mcp/_stdio_transport.py +614 -0
  955. chrys/service/mcp/_tool_mixins.py +96 -0
  956. chrys/service/mcp/adapter.py +1031 -0
  957. chrys/service/mcp/cache.py +476 -0
  958. chrys/service/mcp/content.py +32 -0
  959. chrys/service/mcp/errors.py +169 -0
  960. chrys/service/mcp/owned.py +1330 -0
  961. chrys/service/mcp/result_limits.py +168 -0
  962. chrys/service/mcp/thinking_warning.py +95 -0
  963. chrys/service/mcp/validation.py +74 -0
  964. chrys/service/mutations/__init__.py +84 -0
  965. chrys/service/mutations/coordination.py +894 -0
  966. chrys/service/mutations/detector.py +458 -0
  967. chrys/service/mutations/git_calibrator.py +397 -0
  968. chrys/service/mutations/git_state.py +647 -0
  969. chrys/service/mutations/pipeline.py +779 -0
  970. chrys/service/mutations/scanner.py +466 -0
  971. chrys/service/mutations/snapshot_files.py +233 -0
  972. chrys/service/mutations/store.py +625 -0
  973. chrys/service/mutations/tool_names.py +15 -0
  974. chrys/service/mutations/trace.py +246 -0
  975. chrys/service/mutations/tracker.py +1289 -0
  976. chrys/service/mutations/types.py +559 -0
  977. chrys/service/mutations/workspace_changes.py +1583 -0
  978. chrys/service/profiles/__init__.py +2 -0
  979. chrys/service/profiles/agents/__init__.py +3 -0
  980. chrys/service/profiles/agents/builtins/Code.yaml +199 -0
  981. chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
  982. chrys/service/profiles/agents/builtins/General.yaml +114 -0
  983. chrys/service/profiles/agents/builtins/QA.yaml +128 -0
  984. chrys/service/profiles/agents/loader.py +808 -0
  985. chrys/service/profiles/agents/registry.py +727 -0
  986. chrys/service/profiles/agents/schema.py +371 -0
  987. chrys/service/profiles/agents/serializer.py +196 -0
  988. chrys/service/profiles/coercion.py +22 -0
  989. chrys/service/profiles/models/__init__.py +3 -0
  990. chrys/service/profiles/models/env_bridge.py +157 -0
  991. chrys/service/profiles/models/loader.py +282 -0
  992. chrys/service/profiles/models/options.py +438 -0
  993. chrys/service/profiles/models/registry.py +104 -0
  994. chrys/service/profiles/models/resolver.py +286 -0
  995. chrys/service/profiles/models/schema.py +91 -0
  996. chrys/service/profiles/models/serializer.py +76 -0
  997. chrys/service/session/__init__.py +3 -0
  998. chrys/service/session/checkpoint.py +156 -0
  999. chrys/service/session/history.py +1616 -0
  1000. chrys/service/session/message_metadata.py +151 -0
  1001. chrys/service/session/persistence.py +435 -0
  1002. chrys/service/session/runtime_metadata.py +210 -0
  1003. chrys/service/session/sub_agent_logs.py +1069 -0
  1004. chrys/service/session/sub_agent_transcript.py +654 -0
  1005. chrys/service/skills/__init__.py +3 -0
  1006. chrys/service/skills/adapter.py +408 -0
  1007. chrys/service/skills/constants.py +8 -0
  1008. chrys/service/skills/loader.py +575 -0
  1009. chrys/service/skills/model.py +130 -0
  1010. chrys/service/skills/provider.py +841 -0
  1011. chrys/service/skills/runner.py +390 -0
  1012. chrys/service/state/__init__.py +3 -0
  1013. chrys/service/state/_fork.py +710 -0
  1014. chrys/service/state/_session_files.py +175 -0
  1015. chrys/service/state/_session_meta.py +552 -0
  1016. chrys/service/state/locks.py +154 -0
  1017. chrys/service/state/serializers.py +156 -0
  1018. chrys/service/state/session_catalog.py +361 -0
  1019. chrys/service/state/session_listing.py +84 -0
  1020. chrys/service/state/session_migration.py +446 -0
  1021. chrys/service/state/session_mru.py +315 -0
  1022. chrys/service/state/store.py +2132 -0
  1023. chrys/service/state/workflow.py +158 -0
  1024. chrys/service/text_blocks.py +47 -0
  1025. chrys/service/todos/__init__.py +3 -0
  1026. chrys/service/todos/reminder.py +31 -0
  1027. chrys/service/todos/tracker.py +58 -0
  1028. chrys/service/tools/__init__.py +3 -0
  1029. chrys/service/tools/approval_targets.py +92 -0
  1030. chrys/service/tools/builtins/__init__.py +3 -0
  1031. chrys/service/tools/builtins/ask_user.py +326 -0
  1032. chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
  1033. chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
  1034. chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
  1035. chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
  1036. chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
  1037. chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
  1038. chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
  1039. chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
  1040. chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
  1041. chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
  1042. chrys/service/tools/builtins/doc_converter/registry.py +55 -0
  1043. chrys/service/tools/builtins/filesystem.py +1071 -0
  1044. chrys/service/tools/builtins/search.py +1265 -0
  1045. chrys/service/tools/builtins/shell.py +967 -0
  1046. chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
  1047. chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
  1048. chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
  1049. chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
  1050. chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
  1051. chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
  1052. chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
  1053. chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
  1054. chrys/service/tools/builtins/shell_filter.py +24 -0
  1055. chrys/service/tools/builtins/sleep.py +91 -0
  1056. chrys/service/tools/builtins/todo.py +64 -0
  1057. chrys/service/tools/builtins/web/__init__.py +6 -0
  1058. chrys/service/tools/builtins/web/build.py +268 -0
  1059. chrys/service/tools/builtins/web/config.py +338 -0
  1060. chrys/service/tools/builtins/web/credentials.py +37 -0
  1061. chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
  1062. chrys/service/tools/builtins/web/fetch/cache.py +68 -0
  1063. chrys/service/tools/builtins/web/fetch/convert.py +56 -0
  1064. chrys/service/tools/builtins/web/fetch/tool.py +223 -0
  1065. chrys/service/tools/builtins/web/hosted.py +61 -0
  1066. chrys/service/tools/builtins/web/http.py +330 -0
  1067. chrys/service/tools/builtins/web/progress.py +18 -0
  1068. chrys/service/tools/builtins/web/search/__init__.py +3 -0
  1069. chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
  1070. chrys/service/tools/builtins/web/search/html.py +152 -0
  1071. chrys/service/tools/builtins/web/search/json_api.py +227 -0
  1072. chrys/service/tools/builtins/web/search/providers.py +77 -0
  1073. chrys/service/tools/builtins/web/search/tool.py +218 -0
  1074. chrys/service/tools/builtins/web/search/types.py +28 -0
  1075. chrys/service/tools/builtins/web/tls_identity.py +66 -0
  1076. chrys/service/tools/kinds.py +115 -0
  1077. chrys/service/tools/names.py +47 -0
  1078. chrys/service/tools/registry.py +242 -0
  1079. chrys/service/tools/result_metadata.py +103 -0
  1080. chrys/service/tools/session_artifacts.py +196 -0
  1081. chrys/service/tools/spill.py +218 -0
  1082. chrys/service/tools/workspace_paths.py +81 -0
  1083. chrys/service/trajectory/__init__.py +11 -0
  1084. chrys/service/trajectory/approvals.py +201 -0
  1085. chrys/service/trajectory/compaction.py +420 -0
  1086. chrys/service/trajectory/fork.py +75 -0
  1087. chrys/service/trajectory/hooks.py +217 -0
  1088. chrys/service/trajectory/items.py +41 -0
  1089. chrys/service/trajectory/preparation.py +374 -0
  1090. chrys/service/trajectory/retries.py +95 -0
  1091. chrys/service/trajectory/revisions.py +129 -0
  1092. chrys/service/trajectory/segmented.py +163 -0
  1093. chrys/service/trajectory/session.py +762 -0
  1094. chrys/service/trajectory/state.py +83 -0
  1095. chrys/service/trajectory/sub_agents.py +169 -0
  1096. chrys/service/trajectory/tombstone.py +262 -0
  1097. chrys/service/trajectory/tools.py +323 -0
  1098. chrys/service/trajectory/validation.py +130 -0
  1099. chrys/service/trajectory/waits.py +161 -0
  1100. chrys/service/trajectory/workflow.py +148 -0
  1101. chrys/service/vision.py +128 -0
  1102. chrys/service/workflows/__init__.py +3 -0
  1103. chrys/service/workflows/admission.py +151 -0
  1104. chrys/service/workflows/artifacts.py +109 -0
  1105. chrys/service/workflows/asks.py +130 -0
  1106. chrys/service/workflows/builtins/__init__.py +12 -0
  1107. chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
  1108. chrys/service/workflows/builtins/demo-workflow.py +756 -0
  1109. chrys/service/workflows/discovery.py +554 -0
  1110. chrys/service/workflows/environment.py +284 -0
  1111. chrys/service/workflows/graph.py +430 -0
  1112. chrys/service/workflows/history.py +82 -0
  1113. chrys/service/workflows/interpreter.py +107 -0
  1114. chrys/service/workflows/journal.py +334 -0
  1115. chrys/service/workflows/layout.py +56 -0
  1116. chrys/service/workflows/ledger.py +149 -0
  1117. chrys/service/workflows/model_selection.py +18 -0
  1118. chrys/service/workflows/orphans.py +209 -0
  1119. chrys/service/workflows/outcomes.py +46 -0
  1120. chrys/service/workflows/protocol.py +210 -0
  1121. chrys/service/workflows/records.py +142 -0
  1122. chrys/service/workflows/scheduler.py +770 -0
  1123. chrys/service/workflows/sdk/__init__.py +51 -0
  1124. chrys/service/workflows/sdk/_ask.py +167 -0
  1125. chrys/service/workflows/sdk/_builder.py +781 -0
  1126. chrys/service/workflows/sdk/_values.py +92 -0
  1127. chrys/service/workflows/sdk_artifact.py +74 -0
  1128. chrys/service/workflows/store.py +494 -0
  1129. chrys/service/workflows/transcript.py +102 -0
  1130. chrys/service/workflows/values.py +73 -0
  1131. chrys/service/workflows/worker_host.py +1820 -0
  1132. chrys/workflows.py +38 -0
  1133. icode_tui-0.29.1.dist-info/METADATA +104 -0
  1134. icode_tui-0.29.1.dist-info/RECORD +1138 -0
  1135. icode_tui-0.29.1.dist-info/WHEEL +4 -0
  1136. icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
  1137. icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
  1138. icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
@@ -0,0 +1,1762 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Provider-neutral planning and live presentation for hosted tools."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import inspect
8
+ import itertools
9
+ import json
10
+ import logging
11
+ from collections import Counter, defaultdict, deque
12
+ from collections.abc import Awaitable, Callable, Collection, Mapping, Sequence
13
+ from copy import copy
14
+ from dataclasses import dataclass, field, replace
15
+ from typing import Any
16
+
17
+ from chrys.foundation.hosted_tools import (
18
+ HOSTED_TOOL_DEFAULT_RETRY_SAFETY_BY_FAMILY,
19
+ HOSTED_TOOL_DEFAULT_TITLE_BY_FAMILY,
20
+ PRESENTATION_TEXT_SEGMENT_ID_KEY,
21
+ HostedRetrySafety,
22
+ HostedToolFamily,
23
+ HostedToolPhase,
24
+ HostedToolStatus,
25
+ normalize_hosted_tool_status,
26
+ )
27
+ from chrys.foundation.io.result_content import extract_result_images, extract_result_text
28
+ from chrys.foundation.models.history_markers import HistoryMarkerKind
29
+ from chrys.foundation.tool_result_metadata import (
30
+ TOOL_ERROR_CODE_METADATA_KEY,
31
+ TOOL_ERROR_MESSAGE_METADATA_KEY,
32
+ TOOL_ERRORED_METADATA_KEY,
33
+ TOOL_FAILURE_TEXT_SYNTHESIZED_METADATA_KEY,
34
+ )
35
+ from chrys.kernel import AgentResponse, Content, Message
36
+ from chrys.kernel.exchanges import (
37
+ TOOL_CALL_CONTENT_TYPES,
38
+ TOOL_RESULT_CONTENT_TYPES,
39
+ EmptyIdPolicy,
40
+ LiveAccessor,
41
+ NoneIdPolicy,
42
+ Occurrence,
43
+ PairingPolicy,
44
+ iter_exchanges,
45
+ pair_results,
46
+ )
47
+ from chrys.kernel.identity import WeakIdentityRegistry
48
+ from chrys.kernel.images import is_image_data_uri, is_image_media_type
49
+ from chrys.service.agent_middleware.events.intermediate_text import intermediate_text_contents
50
+
51
+ type ContentOrder = tuple[int, int, int]
52
+
53
+ _HOSTED_CALL_TYPES = TOOL_CALL_CONTENT_TYPES - {"function_call"}
54
+ _HOSTED_RESULT_TYPES = TOOL_RESULT_CONTENT_TYPES - {"function_result"}
55
+ _TERMINAL_STATUSES = frozenset({HostedToolStatus.COMPLETED, HostedToolStatus.FAILED, HostedToolStatus.INTERRUPTED})
56
+ _PRESENTATION_PAIRING_POLICY = PairingPolicy(
57
+ call_types=TOOL_CALL_CONTENT_TYPES,
58
+ include_informational_calls=True,
59
+ result_types=TOOL_RESULT_CONTENT_TYPES,
60
+ none_id=NoneIdPolicy.POSITIONAL,
61
+ empty_id=EmptyIdPolicy.POSITIONAL,
62
+ malformed_id="stringify",
63
+ )
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class HostedToolView:
68
+ """Safe, provider-neutral presentation fields for one hosted occurrence."""
69
+
70
+ family: str
71
+ provider: str
72
+ provider_item_type: str
73
+ provider_call_id: str
74
+ tool_name: str
75
+ display_title: str
76
+ arguments: dict[str, Any]
77
+ result_text: str
78
+ image_contents: list[Content]
79
+ artifacts: list[Content]
80
+ provider_status: str
81
+ status: str
82
+ phase: str
83
+ retry_safety: str
84
+ metadata: dict[str, Any]
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class IntermediateTextOp:
89
+ """Text that precedes or separates tool activity."""
90
+
91
+ text: str
92
+ order: ContentOrder
93
+ attempt_id: str = ""
94
+ segment_ids: tuple[str, ...] = ()
95
+ provisional: bool = False
96
+ source_contents: tuple[Content, ...] = field(default=(), compare=False, repr=False)
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class PresentationAttemptAcceptedOp:
101
+ """Commit the canonical subset of provisional text from one attempt."""
102
+
103
+ attempt_id: str
104
+ segments: tuple[IntermediateTextOp, ...]
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class PresentationAttemptRejectedOp:
109
+ """Retract every provisional text segment published by one attempt."""
110
+
111
+ attempt_id: str
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class HostedToolStartOp:
116
+ """Start one hosted presentation occurrence."""
117
+
118
+ occurrence_ordinal: int
119
+ presentation_id: str
120
+ view: HostedToolView
121
+ order: ContentOrder
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class HostedToolArgsOp:
126
+ """Publish a hosted occurrence's current normalized arguments."""
127
+
128
+ occurrence_ordinal: int
129
+ presentation_id: str
130
+ view: HostedToolView
131
+ order: ContentOrder
132
+
133
+
134
+ @dataclass(frozen=True)
135
+ class HostedToolProgressOp:
136
+ """Publish a non-terminal hosted progress snapshot."""
137
+
138
+ occurrence_ordinal: int
139
+ presentation_id: str
140
+ view: HostedToolView
141
+ order: ContentOrder
142
+
143
+
144
+ @dataclass(frozen=True)
145
+ class HostedToolStatusOp:
146
+ """Publish a hosted lifecycle status without fabricating a result."""
147
+
148
+ occurrence_ordinal: int
149
+ presentation_id: str
150
+ view: HostedToolView
151
+ order: ContentOrder
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class HostedToolResultOp:
156
+ """Publish a terminal hosted result."""
157
+
158
+ occurrence_ordinal: int
159
+ presentation_id: str
160
+ view: HostedToolView
161
+ order: ContentOrder
162
+
163
+
164
+ @dataclass(frozen=True)
165
+ class FinalTextOp:
166
+ """Sentinel for the final agent message; empty text remains meaningful."""
167
+
168
+ text: str
169
+ structured_output_completed: bool = False
170
+
171
+
172
+ type HostedToolOperation = (
173
+ HostedToolStartOp | HostedToolArgsOp | HostedToolProgressOp | HostedToolStatusOp | HostedToolResultOp
174
+ )
175
+ type PresentationOperation = IntermediateTextOp | HostedToolOperation
176
+ type PresentationSinkOperation = (
177
+ PresentationOperation | FinalTextOp | PresentationAttemptAcceptedOp | PresentationAttemptRejectedOp
178
+ )
179
+ type PresentationSink = Callable[[PresentationSinkOperation], Awaitable[None] | None]
180
+
181
+ logger = logging.getLogger(__name__)
182
+
183
+ _CONTEXT_ARGUMENTS_LIMIT = 1_200
184
+ _CONTEXT_RESULT_LIMIT = 2_000
185
+
186
+
187
+ def _family_for(content: Content | None) -> str:
188
+ if content is None:
189
+ return HostedToolFamily.GENERIC
190
+ if content.hosted_family:
191
+ return content.hosted_family
192
+ if content.type.startswith("search_"):
193
+ return HostedToolFamily.SEARCH
194
+ if content.type.startswith("mcp_server_"):
195
+ return HostedToolFamily.MCP
196
+ if content.type.startswith("code_interpreter_"):
197
+ return HostedToolFamily.CODE
198
+ if content.type.startswith("image_generation_"):
199
+ return HostedToolFamily.IMAGE
200
+ if content.type.startswith("shell_"):
201
+ return HostedToolFamily.SHELL
202
+ return HostedToolFamily.GENERIC
203
+
204
+
205
+ def _provider_call_id(content: Content | None) -> str:
206
+ if content is None:
207
+ return ""
208
+ raw_id = content.image_id if content.type.startswith("image_generation_") else content.call_id
209
+ return raw_id if isinstance(raw_id, str) else ""
210
+
211
+
212
+ def _presentation_text_segment_ids(content: Content) -> tuple[str, ...]:
213
+ raw_ids = content.additional_properties.get(PRESENTATION_TEXT_SEGMENT_ID_KEY)
214
+ if isinstance(raw_ids, str):
215
+ return (raw_ids,) if raw_ids else ()
216
+ if isinstance(raw_ids, Sequence):
217
+ return tuple(dict.fromkeys(segment_id for segment_id in raw_ids if isinstance(segment_id, str) and segment_id))
218
+ return ()
219
+
220
+
221
+ def _json_safe(value: Any) -> Any:
222
+ if value is None or isinstance(value, str | int | float | bool):
223
+ return value
224
+ if isinstance(value, Mapping):
225
+ return {str(key): _json_safe(item) for key, item in value.items()}
226
+ if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
227
+ return [_json_safe(item) for item in value]
228
+ if isinstance(value, Content):
229
+ return value.to_dict()
230
+ return str(value)
231
+
232
+
233
+ def _arguments_dict(value: Any) -> dict[str, Any]:
234
+ if value is None:
235
+ return {}
236
+ if isinstance(value, Mapping):
237
+ return {str(key): _json_safe(item) for key, item in value.items()}
238
+ if isinstance(value, str):
239
+ try:
240
+ decoded = json.loads(value)
241
+ except json.JSONDecodeError:
242
+ return {"input": value}
243
+ if isinstance(decoded, Mapping):
244
+ return {str(key): _json_safe(item) for key, item in decoded.items()}
245
+ return {"input": _json_safe(decoded)}
246
+ return {"input": _json_safe(value)}
247
+
248
+
249
+ def _content_values(content: Content | None) -> list[Content]:
250
+ if content is None:
251
+ return []
252
+ candidates: list[Any] = []
253
+ if content.items is not None:
254
+ candidates.extend(content.items)
255
+ if isinstance(content.outputs, Sequence) and not isinstance(content.outputs, str | bytes | bytearray):
256
+ candidates.extend(content.outputs)
257
+ if isinstance(content.output, Sequence) and not isinstance(content.output, str | bytes | bytearray):
258
+ candidates.extend(content.output)
259
+ if isinstance(content.result, Sequence) and not isinstance(content.result, str | bytes | bytearray):
260
+ candidates.extend(content.result)
261
+ return [item for item in candidates if isinstance(item, Content)]
262
+
263
+
264
+ def _is_image(content: Content) -> bool:
265
+ # Share the extraction-side predicate: OpenAI code outputs carry the bare
266
+ # "image" media type, which must not double as an artifact.
267
+ return is_image_media_type(content.media_type) or is_image_data_uri(content.uri)
268
+
269
+
270
+ def _is_artifact(content: Content) -> bool:
271
+ if content.type in {"hosted_file", "hosted_vector_store"}:
272
+ return True
273
+ return content.type in {"data", "uri"} and not _is_image(content)
274
+
275
+
276
+ def _result_payload(content: Content | None) -> Any:
277
+ if content is None:
278
+ return None
279
+ if content.type == "mcp_server_tool_result":
280
+ return content.output
281
+ if content.type in {"code_interpreter_tool_result", "image_generation_tool_result", "shell_tool_result"}:
282
+ return content.outputs
283
+ if content.result is not None:
284
+ return content.result
285
+ if content.items is not None:
286
+ return content.items
287
+ if content.message is not None:
288
+ return content.message
289
+ return None
290
+
291
+
292
+ def _error_code(content: Content | None) -> str | int | None:
293
+ if content is None:
294
+ return None
295
+ if content.error_code:
296
+ return content.error_code
297
+ error = content.additional_properties.get("error")
298
+ if isinstance(error, Mapping):
299
+ code = error.get("error_code", error.get("code"))
300
+ if isinstance(code, str | int) and not isinstance(code, bool):
301
+ return code
302
+ code = content.additional_properties.get("error_code")
303
+ if isinstance(code, str | int) and not isinstance(code, bool):
304
+ return code
305
+ return None
306
+
307
+
308
+ def _is_failed(content: Content | None, provider_status: str) -> bool:
309
+ if normalize_hosted_tool_status(provider_status) is HostedToolStatus.FAILED:
310
+ return True
311
+ return bool(content is not None and content.additional_properties.get("is_error") is True)
312
+
313
+
314
+ def _failure_text(text: str, provider_status: str) -> tuple[str, bool]:
315
+ message = text.strip() or provider_status.strip()
316
+ synthesized = not message
317
+ message = message or "Provider-hosted tool failed."
318
+ return (message if message.startswith("Error: ") else f"Error: {message}", synthesized)
319
+
320
+
321
+ class GenericHostedAdapter:
322
+ """Adapt any canonical hosted pair without provider-specific branches."""
323
+
324
+ def adapt(self, call: Content | None, result: Content | None = None) -> HostedToolView:
325
+ family = _family_for(call or result)
326
+ tool_name = ""
327
+ if call is not None:
328
+ tool_name = call.tool_name or call.name or ""
329
+ if not tool_name and result is not None:
330
+ tool_name = result.tool_name or result.name or ""
331
+ if not tool_name:
332
+ tool_name = family
333
+
334
+ arguments = self._arguments(call)
335
+ payload = _result_payload(result)
336
+ values = _content_values(result)
337
+ image_contents = extract_result_images(values)
338
+ artifacts = [content for content in values if _is_artifact(content)]
339
+ textual_values = [content for content in values if not _is_image(content) and not _is_artifact(content)]
340
+ if textual_values:
341
+ result_text = extract_result_text(textual_values)
342
+ elif values:
343
+ result_text = ""
344
+ else:
345
+ result_text = extract_result_text(payload) if payload is not None else ""
346
+ provider_status = ""
347
+ for candidate in (result, call):
348
+ if candidate is not None and (candidate.provider_status or candidate.status):
349
+ provider_status = candidate.provider_status or candidate.status or ""
350
+ break
351
+ status = normalize_hosted_tool_status(provider_status)
352
+ failed = _is_failed(result or call, provider_status)
353
+ failure_text_synthesized = False
354
+ if failed:
355
+ status = HostedToolStatus.FAILED
356
+ result_text, failure_text_synthesized = _failure_text(result_text, provider_status)
357
+
358
+ phase = ""
359
+ for candidate in (result, call):
360
+ if candidate is not None and candidate.provider_phase:
361
+ phase = candidate.provider_phase
362
+ break
363
+ if not phase:
364
+ phase = HostedToolPhase.TERMINAL if result is not None else HostedToolPhase.START
365
+
366
+ retry_safety = ""
367
+ for candidate in (result, call):
368
+ if candidate is not None and candidate.retry_safety:
369
+ retry_safety = candidate.retry_safety
370
+ break
371
+ retry_safety = retry_safety or HOSTED_TOOL_DEFAULT_RETRY_SAFETY_BY_FAMILY.get(family, HostedRetrySafety.UNKNOWN)
372
+
373
+ provider = ""
374
+ provider_item_type = ""
375
+ provider_item_id = ""
376
+ for candidate in (call, result):
377
+ if candidate is None:
378
+ continue
379
+ provider = provider or candidate.hosted_provider or ""
380
+ provider_item_type = provider_item_type or candidate.provider_item_type or ""
381
+ provider_item_id = provider_item_id or candidate.provider_item_id or ""
382
+
383
+ metadata: dict[str, Any] = {}
384
+ if provider_item_id:
385
+ metadata["provider_item_id"] = provider_item_id
386
+ if phase:
387
+ metadata["provider_phase"] = phase
388
+ if failed:
389
+ metadata[TOOL_ERRORED_METADATA_KEY] = True
390
+ metadata[TOOL_ERROR_MESSAGE_METADATA_KEY] = result_text.removeprefix("Error: ").strip()
391
+ if failure_text_synthesized:
392
+ metadata[TOOL_FAILURE_TEXT_SYNTHESIZED_METADATA_KEY] = True
393
+ metadata["provider_status"] = provider_status
394
+ error_code = _error_code(result or call)
395
+ if error_code is not None:
396
+ metadata[TOOL_ERROR_CODE_METADATA_KEY] = error_code
397
+
398
+ return HostedToolView(
399
+ family=family,
400
+ provider=provider,
401
+ provider_item_type=provider_item_type,
402
+ provider_call_id=_provider_call_id(call or result),
403
+ tool_name=tool_name,
404
+ display_title=HOSTED_TOOL_DEFAULT_TITLE_BY_FAMILY.get(family, tool_name),
405
+ arguments=arguments,
406
+ result_text=result_text,
407
+ image_contents=image_contents,
408
+ artifacts=artifacts,
409
+ provider_status=provider_status,
410
+ status=status,
411
+ phase=phase,
412
+ retry_safety=retry_safety,
413
+ metadata=metadata,
414
+ )
415
+
416
+ def _arguments(self, call: Content | None) -> dict[str, Any]:
417
+ if call is None:
418
+ return {}
419
+ return _arguments_dict(call.arguments)
420
+
421
+
422
+ class SearchHostedAdapter(GenericHostedAdapter):
423
+ """Adapt hosted search and fetch pairs."""
424
+
425
+
426
+ class McpHostedAdapter(GenericHostedAdapter):
427
+ """Adapt provider-managed MCP pairs."""
428
+
429
+ def _arguments(self, call: Content | None) -> dict[str, Any]:
430
+ arguments = super()._arguments(call)
431
+ if call is not None and call.server_name:
432
+ return {"server": call.server_name, **arguments}
433
+ return arguments
434
+
435
+
436
+ class CodeHostedAdapter(GenericHostedAdapter):
437
+ """Adapt code-interpreter calls, logs, images, and artifacts."""
438
+
439
+ def _arguments(self, call: Content | None) -> dict[str, Any]:
440
+ if call is None:
441
+ return {}
442
+ inputs = call.inputs or []
443
+ code = "".join(content.text or "" for content in inputs if content.type == "text")
444
+ # Anthropic's first canonical representation stored the complete
445
+ # ``{"code": ...}`` input object as text. Unwrap that representation
446
+ # so live and replayed cards expose one provider-neutral code string.
447
+ nested_code = _arguments_dict(code).get("code")
448
+ if isinstance(nested_code, str):
449
+ code = nested_code
450
+ return {"code": code} if code else super()._arguments(call)
451
+
452
+
453
+ class ImageHostedAdapter(GenericHostedAdapter):
454
+ """Adapt image generation snapshots and terminal outputs."""
455
+
456
+
457
+ class ShellHostedAdapter(GenericHostedAdapter):
458
+ """Adapt provider-hosted shell calls and command outputs."""
459
+
460
+ def adapt(self, call: Content | None, result: Content | None = None) -> HostedToolView:
461
+ if result is None or result.type != "shell_tool_result" or result.outputs is None:
462
+ return super().adapt(call, result)
463
+
464
+ normalized_outputs: list[Content] = []
465
+ command_details: list[dict[str, Any]] = []
466
+ for output in result.outputs:
467
+ if output.type != "shell_command_output":
468
+ normalized_outputs.append(output)
469
+ continue
470
+ details = {
471
+ key: value
472
+ for key, value in {
473
+ "stdout": output.stdout,
474
+ "stderr": output.stderr,
475
+ "exit_code": output.exit_code,
476
+ "timed_out": output.timed_out,
477
+ }.items()
478
+ if value is not None
479
+ }
480
+ command_details.append(details)
481
+ text = "\n".join(value for value in (output.stdout, output.stderr) if isinstance(value, str) and value)
482
+ if text:
483
+ normalized_outputs.append(Content.from_text(text))
484
+
485
+ normalized_result = Content.from_shell_tool_result(
486
+ call_id=result.call_id,
487
+ outputs=normalized_outputs,
488
+ max_output_length=result.max_output_length,
489
+ hosted_provider=result.hosted_provider,
490
+ provider_item_type=result.provider_item_type,
491
+ provider_item_id=result.provider_item_id,
492
+ provider_phase=result.provider_phase,
493
+ provider_status=result.provider_status,
494
+ retry_safety=result.retry_safety,
495
+ annotations=result.annotations,
496
+ additional_properties=dict(result.additional_properties),
497
+ raw_representation=result.raw_representation,
498
+ )
499
+ view = super().adapt(call, normalized_result)
500
+ if not command_details:
501
+ return view
502
+
503
+ metadata = dict(view.metadata)
504
+ metadata["outputs"] = command_details
505
+ stdout = "\n".join(
506
+ str(details["stdout"]) for details in command_details if details.get("stdout") not in (None, "")
507
+ )
508
+ stderr = "\n".join(
509
+ str(details["stderr"]) for details in command_details if details.get("stderr") not in (None, "")
510
+ )
511
+ if stdout:
512
+ metadata["stdout"] = stdout
513
+ if stderr:
514
+ metadata["stderr"] = stderr
515
+ exit_codes = [
516
+ details["exit_code"]
517
+ for details in command_details
518
+ if isinstance(details.get("exit_code"), int) and not isinstance(details.get("exit_code"), bool)
519
+ ]
520
+ if exit_codes:
521
+ metadata["exit_code"] = next((code for code in exit_codes if code != 0), exit_codes[-1])
522
+ if any(details.get("timed_out") is True for details in command_details):
523
+ metadata["timed_out"] = True
524
+ elif any("timed_out" in details for details in command_details):
525
+ metadata["timed_out"] = False
526
+ return replace(view, metadata=metadata)
527
+
528
+ def _arguments(self, call: Content | None) -> dict[str, Any]:
529
+ if call is None:
530
+ return {}
531
+ arguments: dict[str, Any] = {}
532
+ if call.commands is not None:
533
+ arguments["commands"] = list(call.commands)
534
+ if call.timeout_ms is not None:
535
+ arguments["timeout_ms"] = call.timeout_ms
536
+ if call.max_output_length is not None:
537
+ arguments["max_output_length"] = call.max_output_length
538
+ return arguments or super()._arguments(call)
539
+
540
+
541
+ _SEARCH_ADAPTER = SearchHostedAdapter()
542
+ _MCP_ADAPTER = McpHostedAdapter()
543
+ _CODE_ADAPTER = CodeHostedAdapter()
544
+ _IMAGE_ADAPTER = ImageHostedAdapter()
545
+ _SHELL_ADAPTER = ShellHostedAdapter()
546
+ _GENERIC_ADAPTER = GenericHostedAdapter()
547
+
548
+
549
+ def get_hosted_adapter(family: str) -> GenericHostedAdapter:
550
+ """Return the six-family adapter selected by canonical hosted family."""
551
+ if family in {HostedToolFamily.SEARCH, HostedToolFamily.FETCH}:
552
+ return _SEARCH_ADAPTER
553
+ if family == HostedToolFamily.MCP:
554
+ return _MCP_ADAPTER
555
+ if family == HostedToolFamily.CODE:
556
+ return _CODE_ADAPTER
557
+ if family == HostedToolFamily.IMAGE:
558
+ return _IMAGE_ADAPTER
559
+ if family == HostedToolFamily.SHELL:
560
+ return _SHELL_ADAPTER
561
+ return _GENERIC_ADAPTER
562
+
563
+
564
+ def adapt_hosted_tool(call: Content | None, result: Content | None = None) -> HostedToolView:
565
+ """Adapt a canonical hosted call and optional paired result."""
566
+ return get_hosted_adapter(_family_for(call or result)).adapt(call, result)
567
+
568
+
569
+ def hosted_replay_status(view: HostedToolView, *, has_result: bool) -> str:
570
+ """Return the terminal status used when restoring a persisted occurrence."""
571
+ status = normalize_hosted_tool_status(view.status)
572
+ if status in _TERMINAL_STATUSES:
573
+ return status.value
574
+ if view.phase == HostedToolPhase.TERMINAL:
575
+ return HostedToolStatus.COMPLETED
576
+ # Persisted history cannot keep a live card alive. A pending/running call,
577
+ # or a partial result snapshot, was interrupted when the session stopped.
578
+ del has_result
579
+ return HostedToolStatus.INTERRUPTED
580
+
581
+
582
+ def _is_successful_structured_output(view: HostedToolView, *, has_result: bool) -> bool:
583
+ """Return whether a hosted occurrence can stand as the turn's visible output."""
584
+ return hosted_replay_status(view, has_result=has_result) == HostedToolStatus.COMPLETED and view.family not in {
585
+ HostedToolFamily.SEARCH,
586
+ HostedToolFamily.FETCH,
587
+ HostedToolFamily.TOOL_DISCOVERY,
588
+ }
589
+
590
+
591
+ def _bounded_context_value(value: Any, limit: int) -> str:
592
+ try:
593
+ rendered = json.dumps(_json_safe(value), ensure_ascii=False, sort_keys=True)
594
+ except TypeError, ValueError:
595
+ rendered = str(value)
596
+ if len(rendered) <= limit:
597
+ return rendered
598
+ return f"{rendered[: limit - 1]}…"
599
+
600
+
601
+ def hosted_context_summary(call: Content | None, result: Content | None = None) -> str:
602
+ """Build a bounded provider-neutral assistant summary for one occurrence."""
603
+ view = adapt_hosted_tool(call, result)
604
+ status = hosted_replay_status(view, has_result=result is not None)
605
+ lines = [
606
+ "[Provider-hosted tool context]",
607
+ f"Tool: {view.tool_name or view.display_title}",
608
+ f"Family: {view.family}",
609
+ f"Status: {status}",
610
+ ]
611
+ if view.arguments:
612
+ lines.append(f"Arguments: {_bounded_context_value(view.arguments, _CONTEXT_ARGUMENTS_LIMIT)}")
613
+ if view.result_text:
614
+ lines.append(f"Result: {_bounded_context_value(view.result_text, _CONTEXT_RESULT_LIMIT)}")
615
+ if view.image_contents:
616
+ lines.append(f"Images: {len(view.image_contents)}")
617
+ if view.artifacts:
618
+ lines.append(f"Artifacts: {len(view.artifacts)}")
619
+ return "\n".join(lines)
620
+
621
+
622
+ def _is_hosted_call(content: Content) -> bool:
623
+ if content.type in _HOSTED_CALL_TYPES:
624
+ return True
625
+ return content.type == "function_call" and content.informational_only and content.provider_hosted
626
+
627
+
628
+ def _is_hosted_result(content: Content) -> bool:
629
+ return content.type in _HOSTED_RESULT_TYPES or (content.type == "function_result" and content.provider_hosted)
630
+
631
+
632
+ def is_legacy_server_executed_informational_call(content: Content | Mapping[str, Any]) -> bool:
633
+ """Recognize old OpenAI informational calls with explicit server execution proof."""
634
+ if isinstance(content, Content):
635
+ if content.type != "function_call" or not content.informational_only:
636
+ return False
637
+ extra: Mapping[str, Any] = content.additional_properties
638
+ else:
639
+ if content.get("type") != "function_call" or content.get("informational_only") is not True:
640
+ return False
641
+ raw_extra = content.get("additional_properties")
642
+ if not isinstance(raw_extra, Mapping):
643
+ return False
644
+ extra = raw_extra
645
+ item_type = extra.get("item_type")
646
+ if item_type not in {"tool_search_call", "custom_tool_call"}:
647
+ return False
648
+ return extra.get("execution") == "server" or (
649
+ "server_execution" in extra and extra.get("server_execution") is not None
650
+ )
651
+
652
+
653
+ def normalize_hosted_replay_content(content: Content) -> Content:
654
+ """Promote supported legacy server-executed calls to canonical hosted content."""
655
+ if not is_legacy_server_executed_informational_call(content):
656
+ return content
657
+ promoted = copy(content)
658
+ extra = promoted.additional_properties
659
+ item_type = str(extra.get("item_type") or "")
660
+ promoted.provider_hosted = True
661
+ promoted.hosted_family = (
662
+ HostedToolFamily.TOOL_DISCOVERY if item_type == "tool_search_call" else HostedToolFamily.GENERIC
663
+ )
664
+ promoted.hosted_provider = "openai"
665
+ promoted.provider_item_type = item_type
666
+ promoted.provider_item_id = str(extra.get("item_id") or "") or None
667
+ promoted.provider_status = str(extra.get("status") or "") or None
668
+ promoted.provider_phase = HostedToolPhase.TERMINAL
669
+ promoted.tool_name = promoted.name or "tool"
670
+ return promoted
671
+
672
+
673
+ def _coord(occurrence: Occurrence) -> tuple[int, int]:
674
+ return occurrence.message_index, occurrence.content_index
675
+
676
+
677
+ @dataclass(frozen=True)
678
+ class _Pair:
679
+ call: tuple[int, int] | None
680
+ result: tuple[int, int] | None
681
+
682
+
683
+ def _presentation_pairs(messages: Sequence[Message]) -> dict[tuple[int, int], _Pair]:
684
+ accessor = LiveAccessor()
685
+ pairs: dict[tuple[int, int], _Pair] = {}
686
+ for exchange in iter_exchanges(messages, accessor):
687
+ pairing = pair_results(messages, exchange, accessor, _PRESENTATION_PAIRING_POLICY)
688
+ assignments = [assignment for entries in pairing.truthy_assignments.values() for assignment in entries] + [
689
+ assignment for entries in pairing.falsy_assignments.values() for assignment in entries
690
+ ]
691
+ for call_occurrence, result_occurrence in assignments:
692
+ call_coord = _coord(call_occurrence)
693
+ result_coord = _coord(result_occurrence) if result_occurrence is not None else None
694
+ pair = _Pair(call_coord, result_coord)
695
+ pairs[call_coord] = pair
696
+ if result_coord is not None:
697
+ pairs[result_coord] = pair
698
+ unconsumed = [occurrence for entries in pairing.unconsumed_results.values() for occurrence in entries] + [
699
+ occurrence for entries in pairing.falsy_unconsumed_results.values() for occurrence in entries
700
+ ]
701
+ for occurrence in [*unconsumed, *pairing.unpairable_results]:
702
+ result_coord = _coord(occurrence)
703
+ pairs.setdefault(result_coord, _Pair(None, result_coord))
704
+ for occurrence in pairing.unpairable_calls:
705
+ call_coord = _coord(occurrence)
706
+ pairs.setdefault(call_coord, _Pair(call_coord, None))
707
+
708
+ # ``iter_exchanges`` starts at a call-bearing assistant run, so a fully
709
+ # standalone hosted result belongs to no exchange. Inventory those
710
+ # occurrences without attempting any global ID matching; each result is
711
+ # its own presentation occurrence and degradation anchor.
712
+ for message_index, message in enumerate(messages):
713
+ if message.role not in {"assistant", "tool"} or HistoryMarkerKind.KEY in message.additional_properties:
714
+ continue
715
+ for content_index, content in enumerate(message.contents):
716
+ coordinate = (message_index, content_index)
717
+ if coordinate not in pairs and _is_hosted_result(content):
718
+ pairs[coordinate] = _Pair(None, coordinate)
719
+ return pairs
720
+
721
+
722
+ def _content_at(messages: Sequence[Message], coordinate: tuple[int, int] | None) -> Content | None:
723
+ if coordinate is None:
724
+ return None
725
+ message_index, content_index = coordinate
726
+ return messages[message_index].contents[content_index]
727
+
728
+
729
+ def cross_provider_hosted_degradations(
730
+ messages: Sequence[Message],
731
+ *,
732
+ target_provider: str,
733
+ unsafe_same_provider_families: Collection[str] = (),
734
+ ) -> dict[int, str | None]:
735
+ """Map hosted contents that are unsafe to replay to one neutral summary each.
736
+
737
+ A provider switch always makes a hosted item unsafe. Callers may also
738
+ identify same-provider families whose server-issued item identities cannot
739
+ be replayed through a locally managed transcript.
740
+ """
741
+ pairs = _presentation_pairs(messages)
742
+ degradations: dict[int, str | None] = {}
743
+ handled_pairs: set[_Pair] = set()
744
+ for message_index, message in enumerate(messages):
745
+ for content_index, content in enumerate(message.contents):
746
+ # Hosted-tool persistence shipped together with this marker, so
747
+ # pre-marker histories cannot contain supported hosted records.
748
+ # Current producers must use the canonical factories, which always
749
+ # set it; marker-less hand-built payloads are outside compatibility.
750
+ if not content.provider_hosted:
751
+ continue
752
+ source_provider = content.hosted_provider or ""
753
+ family = _family_for(content)
754
+ # Chat Completions passes an empty target because that wire has no
755
+ # hosted item types: every provider-hosted item is unsafe there,
756
+ # including factory-created contents with unknown provenance. A
757
+ # hosted-capable target may still receive unstamped contents built
758
+ # for its current dialect, so only an explicit mismatch is foreign.
759
+ cross_provider = not target_provider or bool(source_provider and source_provider != target_provider)
760
+ unsafe_same_provider = source_provider == target_provider and family in unsafe_same_provider_families
761
+ if not cross_provider and not unsafe_same_provider:
762
+ continue
763
+ pair = pairs.get((message_index, content_index), _Pair((message_index, content_index), None))
764
+ if pair in handled_pairs:
765
+ continue
766
+ handled_pairs.add(pair)
767
+ call = _content_at(messages, pair.call)
768
+ result = _content_at(messages, pair.result)
769
+ members = [candidate for candidate in (call, result) if candidate is not None and candidate.provider_hosted]
770
+ if not members:
771
+ continue
772
+ anchor = call if call is not None and call.provider_hosted else members[0]
773
+ for member in members:
774
+ degradations[id(member)] = None
775
+ degradations[id(anchor)] = hosted_context_summary(call, result)
776
+ representative = call or result
777
+ if representative is None:
778
+ raise RuntimeError("A hosted exchange requires a call or a result.")
779
+ logger.debug(
780
+ "Degrading provider-hosted history to assistant context: source=%s target=%s family=%s item_type=%s",
781
+ source_provider,
782
+ target_provider,
783
+ _family_for(call or result),
784
+ representative.provider_item_type,
785
+ )
786
+ return degradations
787
+
788
+
789
+ def _hosted_ops(
790
+ *,
791
+ occurrence: int,
792
+ view: HostedToolView,
793
+ order: ContentOrder,
794
+ start: bool,
795
+ result_content: bool,
796
+ ) -> list[HostedToolOperation]:
797
+ operations: list[HostedToolOperation] = []
798
+ suborder = order[2]
799
+ if start:
800
+ operations.append(HostedToolStartOp(occurrence, "", view, (order[0], order[1], suborder)))
801
+ suborder += 1
802
+ if view.arguments and (start or result_content):
803
+ # Some providers attach arguments only to the terminal item (OpenAI
804
+ # web_search action), so result observations re-announce them; the
805
+ # publish-side signature dedupes unchanged payloads.
806
+ operations.append(HostedToolArgsOp(occurrence, "", view, (order[0], order[1], suborder)))
807
+ suborder += 1
808
+ status = normalize_hosted_tool_status(view.status)
809
+ nonterminal_snapshot = (
810
+ view.phase in {HostedToolPhase.DELTA, HostedToolPhase.SNAPSHOT} and status not in _TERMINAL_STATUSES
811
+ )
812
+ if result_content and nonterminal_snapshot:
813
+ operations.append(HostedToolProgressOp(occurrence, "", view, (order[0], order[1], suborder)))
814
+ elif result_content:
815
+ operations.append(HostedToolResultOp(occurrence, "", view, (order[0], order[1], suborder)))
816
+ elif view.phase in {HostedToolPhase.DELTA, HostedToolPhase.SNAPSHOT} and (view.result_text or view.image_contents):
817
+ operations.append(HostedToolProgressOp(occurrence, "", view, (order[0], order[1], suborder)))
818
+ elif status is not HostedToolStatus.UNKNOWN:
819
+ operations.append(HostedToolStatusOp(occurrence, "", view, (order[0], order[1], suborder)))
820
+ return operations
821
+
822
+
823
+ @dataclass(frozen=True)
824
+ class ResponsePresentationPlan:
825
+ """Pure, exchange-aware presentation plan for an assembled response."""
826
+
827
+ operations: list[PresentationOperation]
828
+ final_text: str
829
+ structured_output_completed: bool = False
830
+
831
+ @classmethod
832
+ def from_messages(cls, messages: Sequence[Message]) -> ResponsePresentationPlan:
833
+ """Plan text and hosted operations without rebuilding any content."""
834
+ has_hosted = any(
835
+ _is_hosted_call(content) or _is_hosted_result(content)
836
+ for message in messages
837
+ for content in message.contents
838
+ )
839
+ if not has_hosted:
840
+ final_text = ""
841
+ if messages:
842
+ final_text = "".join(
843
+ content.text or "" for content in messages[-1].contents if content.type == "text" and content.text
844
+ )
845
+ return cls([], final_text)
846
+
847
+ pairs = _presentation_pairs(messages)
848
+ operations: list[PresentationOperation] = []
849
+ occurrence_by_pair: dict[_Pair, int] = {}
850
+ emitted_starts: set[int] = set()
851
+ terminal_occurrences: set[int] = set()
852
+ successful_structured_occurrences: set[int] = set()
853
+ pending_text: list[Content] = []
854
+ trailing_text_final = False
855
+ next_occurrence = 0
856
+
857
+ def flush_intermediate(order: ContentOrder) -> None:
858
+ if not pending_text:
859
+ return
860
+ segment_ids = tuple(
861
+ dict.fromkeys(
862
+ segment_id for content in pending_text for segment_id in _presentation_text_segment_ids(content)
863
+ )
864
+ )
865
+ operations.append(
866
+ IntermediateTextOp(
867
+ "".join(content.text or "" for content in pending_text),
868
+ order,
869
+ segment_ids=segment_ids,
870
+ source_contents=tuple(pending_text),
871
+ )
872
+ )
873
+ pending_text.clear()
874
+
875
+ for message_index, message in enumerate(messages):
876
+ for content_index, content in enumerate(message.contents):
877
+ coordinate = (message_index, content_index)
878
+ order = (message_index, content_index, 0)
879
+ if content.type == "text":
880
+ if content.text:
881
+ pending_text.append(content)
882
+ continue
883
+ if content.type == "function_call" and not content.informational_only:
884
+ flush_intermediate((message_index, content_index, -1))
885
+ trailing_text_final = False
886
+ continue
887
+ if content.type == "function_result" and not _is_hosted_result(content):
888
+ # A full-run transcript interleaves local results between
889
+ # responses; the loop resumed past this call, so text that
890
+ # follows is the model's post-tool answer, not preamble.
891
+ flush_intermediate((message_index, content_index, -1))
892
+ trailing_text_final = True
893
+ continue
894
+
895
+ pair = pairs.get(coordinate)
896
+ if pair is None:
897
+ continue
898
+ call = _content_at(messages, pair.call)
899
+ result = _content_at(messages, pair.result)
900
+ if call is not None and not _is_hosted_call(call):
901
+ continue
902
+ if call is None and (result is None or not _is_hosted_result(result)):
903
+ continue
904
+ occurrence = occurrence_by_pair.get(pair)
905
+ if occurrence is None:
906
+ occurrence = next_occurrence
907
+ next_occurrence += 1
908
+ occurrence_by_pair[pair] = occurrence
909
+
910
+ if _is_hosted_result(content):
911
+ flush_intermediate((message_index, content_index, -1))
912
+ start = occurrence not in emitted_starts
913
+ view = adapt_hosted_tool(call, content)
914
+ operations.extend(
915
+ _hosted_ops(
916
+ occurrence=occurrence,
917
+ view=view,
918
+ order=order,
919
+ start=start,
920
+ result_content=True,
921
+ )
922
+ )
923
+ emitted_starts.add(occurrence)
924
+ status = normalize_hosted_tool_status(view.status)
925
+ if view.phase == HostedToolPhase.TERMINAL or status in _TERMINAL_STATUSES:
926
+ terminal_occurrences.add(occurrence)
927
+ if _is_successful_structured_output(view, has_result=True):
928
+ successful_structured_occurrences.add(occurrence)
929
+ trailing_text_final = True
930
+ else:
931
+ trailing_text_final = False
932
+ continue
933
+
934
+ if _is_hosted_call(content) and occurrence not in emitted_starts:
935
+ flush_intermediate((message_index, content_index, -1))
936
+ view = adapt_hosted_tool(content)
937
+ operations.extend(
938
+ _hosted_ops(
939
+ occurrence=occurrence,
940
+ view=view,
941
+ order=order,
942
+ start=True,
943
+ result_content=False,
944
+ )
945
+ )
946
+ emitted_starts.add(occurrence)
947
+ has_later_result = pair.result is not None and pair.result != coordinate
948
+ status = normalize_hosted_tool_status(view.status)
949
+ terminal_call = not has_later_result and (
950
+ view.phase == HostedToolPhase.TERMINAL or status in _TERMINAL_STATUSES
951
+ )
952
+ if terminal_call:
953
+ terminal_occurrences.add(occurrence)
954
+ if _is_successful_structured_output(view, has_result=False):
955
+ successful_structured_occurrences.add(occurrence)
956
+ trailing_text_final = True
957
+ else:
958
+ trailing_text_final = False
959
+
960
+ if pending_text and trailing_text_final:
961
+ final_text = "".join(content.text or "" for content in pending_text)
962
+ else:
963
+ flush_intermediate((len(messages), 0, 0))
964
+ final_text = ""
965
+ return cls(operations, final_text, bool(successful_structured_occurrences))
966
+
967
+
968
+ @dataclass(frozen=True, slots=True)
969
+ class FinalSegment:
970
+ """A delegated agent's answer: the caller's result and the transcript's last text.
971
+
972
+ The caller receives the response's final segment, as a main turn's answer
973
+ is its last message: text written between tool calls and hosted outputs
974
+ is published live as intermediate transcript text. A response whose final
975
+ segment is empty (it ends on a tool result or hosted output) gives the
976
+ caller everything it said instead; all of that is intermediate text
977
+ already, so ``transcript`` stays empty.
978
+ """
979
+
980
+ result: str
981
+ transcript: str
982
+
983
+ @classmethod
984
+ def of(cls, response: AgentResponse[Any]) -> FinalSegment:
985
+ final_text = ResponsePresentationPlan.from_messages(response.messages).final_text
986
+ return cls(final_text or response.text, final_text)
987
+
988
+
989
+ @dataclass
990
+ class _LiveRecord:
991
+ operation: PresentationOperation | None = None
992
+ local_call_id: str = ""
993
+ local_call_content: Content | None = None
994
+ text_segment_id: str = ""
995
+ released: bool = False
996
+
997
+
998
+ @dataclass
999
+ class _LiveTextSegment:
1000
+ record: _LiveRecord
1001
+ order: ContentOrder
1002
+ parts: list[str] = field(default_factory=list)
1003
+
1004
+
1005
+ @dataclass
1006
+ class _HostedOccurrence:
1007
+ ordinal: int
1008
+ presentation_id: str
1009
+ pairing_key: tuple[str, str]
1010
+ provider_item_id: str
1011
+ call_seen: bool = False
1012
+ result_seen: bool = False
1013
+ start_published: bool = False
1014
+ terminal: bool = False
1015
+ last_view: HostedToolView | None = None
1016
+
1017
+
1018
+ @dataclass
1019
+ class _PlanBarrier:
1020
+ call_id: str
1021
+ order: ContentOrder
1022
+ content: Content
1023
+ released: bool = False
1024
+
1025
+
1026
+ _hosted_run_generations = itertools.count(1)
1027
+
1028
+
1029
+ def next_hosted_run_generation() -> int:
1030
+ """Return a hosted pass generation that no other pass in this process has used.
1031
+
1032
+ Presentation ids embed it, and frontends key tool cards by those ids. A
1033
+ counter per bindings object restarts on every engine rebuild and would
1034
+ hand a new card the id of an earlier one.
1035
+ """
1036
+ return next(_hosted_run_generations)
1037
+
1038
+
1039
+ @dataclass
1040
+ class HostedPresentationBridge:
1041
+ """Stateful streaming/blocking bridge that emits presentation operations."""
1042
+
1043
+ publish: PresentationSink
1044
+ run_generation: int = 0
1045
+ response_index: int = 0
1046
+ batch_id: int = 0
1047
+ # Awaited as each logical response begins, before anything it produces
1048
+ # is presented; it publishes text an earlier response left buffered.
1049
+ before_response: Callable[[], Awaitable[None]] | None = None
1050
+ attempt_index: int = field(default=-1, init=False)
1051
+ next_occurrence_ordinal: int = field(default=0, init=False)
1052
+ published_terminal_signatures: set[tuple[Any, ...]] = field(default_factory=set, init=False)
1053
+ _published_signatures: set[tuple[Any, ...]] = field(default_factory=set, init=False, repr=False)
1054
+ _live_records: list[_LiveRecord] = field(default_factory=list, init=False, repr=False)
1055
+ _live_cursor: int = field(default=0, init=False, repr=False)
1056
+ _occurrences: list[_HostedOccurrence] = field(default_factory=list, init=False, repr=False)
1057
+ _accepted_occurrences: list[_HostedOccurrence] = field(default_factory=list, init=False, repr=False)
1058
+ _open_occurrences: dict[tuple[str, str], deque[_HostedOccurrence]] = field(
1059
+ default_factory=lambda: defaultdict(deque), init=False, repr=False
1060
+ )
1061
+ _provider_item_candidates: dict[str, list[_HostedOccurrence]] = field(
1062
+ default_factory=lambda: defaultdict(list), init=False, repr=False
1063
+ )
1064
+ _live_text_segments: dict[str, _LiveTextSegment] = field(default_factory=dict, init=False, repr=False)
1065
+ _published_provisional_segments: dict[str, IntermediateTextOp] = field(default_factory=dict, init=False, repr=False)
1066
+ _published_canonical_text_contents: WeakIdentityRegistry = field(
1067
+ default_factory=WeakIdentityRegistry, init=False, repr=False
1068
+ )
1069
+ _started_local_call_contents: WeakIdentityRegistry = field(
1070
+ default_factory=WeakIdentityRegistry, init=False, repr=False
1071
+ )
1072
+ _accepted_barriers: list[_PlanBarrier] = field(default_factory=list, init=False, repr=False)
1073
+ _unbound_local_start_counts: Counter[str] = field(default_factory=Counter, init=False, repr=False)
1074
+ _pending_reconciliation: deque[PresentationSinkOperation] = field(default_factory=deque, init=False, repr=False)
1075
+ _finalized_response_keys: set[tuple[int, int]] = field(default_factory=set, init=False, repr=False)
1076
+ _response_started: bool = field(default=False, init=False, repr=False)
1077
+ _saw_incremental_contents: bool = field(default=False, init=False, repr=False)
1078
+ _saw_hosted_contents: bool = field(default=False, init=False, repr=False)
1079
+
1080
+ async def begin_response(self, *, response_index: int | None = None, batch_id: int | None = None) -> None:
1081
+ """Begin another logical response without resetting run-monotonic ordinals."""
1082
+ if self.before_response is not None:
1083
+ await self.before_response()
1084
+ if response_index is not None:
1085
+ self.response_index = response_index
1086
+ elif self._response_started:
1087
+ self.response_index += 1
1088
+ self._response_started = True
1089
+ if batch_id is not None:
1090
+ self.batch_id = batch_id
1091
+ # Provider-controlled call ids may repeat in a later response. Only
1092
+ # signals that could not yet bind to a concrete Content are scoped to
1093
+ # the current response; identity-bound starts survive for cumulative
1094
+ # final reconciliation. Continuation attempts do not call this method.
1095
+ self._unbound_local_start_counts.clear()
1096
+ self.attempt_index = -1
1097
+ self._clear_attempt_state()
1098
+
1099
+ async def attempt_started(self, *, continuation: bool = False) -> None:
1100
+ """Open a validation attempt; continuation resumes the current occurrence set."""
1101
+ self._response_started = True
1102
+ if continuation and self.attempt_index >= 0:
1103
+ return
1104
+ self.attempt_index += 1
1105
+ self._clear_attempt_state()
1106
+
1107
+ async def observe_contents(self, contents: Sequence[Content], *, is_final: bool = False) -> None:
1108
+ """Observe one raw provider update or final content batch in content order."""
1109
+ if self.attempt_index < 0:
1110
+ await self.attempt_started()
1111
+ if not is_final and any(content.type != "usage" for content in contents):
1112
+ self._saw_incremental_contents = True
1113
+ blocking_final_snapshot = is_final and not self._saw_incremental_contents
1114
+ for content in contents:
1115
+ if content.type == "text":
1116
+ if not content.text:
1117
+ continue
1118
+ if blocking_final_snapshot:
1119
+ self._live_records.append(
1120
+ _LiveRecord(
1121
+ operation=IntermediateTextOp(
1122
+ content.text,
1123
+ (0, len(self._live_records), 0),
1124
+ source_contents=(content,),
1125
+ )
1126
+ )
1127
+ )
1128
+ continue
1129
+ if is_final:
1130
+ continue
1131
+ segment_ids = _presentation_text_segment_ids(content)
1132
+ if len(segment_ids) == 1:
1133
+ segment_id = segment_ids[0]
1134
+ segment = self._live_text_segments.get(segment_id)
1135
+ if segment is None:
1136
+ record = _LiveRecord(text_segment_id=segment_id)
1137
+ self._live_records.append(record)
1138
+ segment = _LiveTextSegment(record, (0, len(self._live_records) - 1, 0))
1139
+ self._live_text_segments[segment_id] = segment
1140
+ segment.parts.append(content.text)
1141
+ else:
1142
+ self._live_records.append(
1143
+ _LiveRecord(operation=IntermediateTextOp(content.text, (0, len(self._live_records), 0)))
1144
+ )
1145
+ continue
1146
+ if content.type == "function_call" and not content.informational_only:
1147
+ if not is_final and self._saw_hosted_contents:
1148
+ await self._seal_live_text_segments()
1149
+ call_id = _provider_call_id(content)
1150
+ record = _LiveRecord(local_call_id=call_id, local_call_content=content)
1151
+ if self._unbound_local_start_counts[call_id] > 0:
1152
+ record.released = True
1153
+ self._started_local_call_contents.register(content)
1154
+ self._unbound_local_start_counts[call_id] -= 1
1155
+ if self._unbound_local_start_counts[call_id] == 0:
1156
+ del self._unbound_local_start_counts[call_id]
1157
+ self._live_records.append(record)
1158
+ await self._drain_live_prefix()
1159
+ continue
1160
+ if _is_hosted_call(content):
1161
+ self._saw_hosted_contents = True
1162
+ if not is_final:
1163
+ await self._seal_live_text_segments()
1164
+ await self._observe_call(content)
1165
+ elif _is_hosted_result(content):
1166
+ self._saw_hosted_contents = True
1167
+ if not is_final:
1168
+ await self._seal_live_text_segments()
1169
+ await self._observe_result(content)
1170
+
1171
+ async def local_call_start_published(self, provider_call_id: str) -> None:
1172
+ """Release the next local-call ordering barrier for a provider call id."""
1173
+ bound = False
1174
+ for record in self._live_records:
1175
+ if record.local_call_id == provider_call_id and not record.released:
1176
+ record.released = True
1177
+ if record.local_call_content is not None:
1178
+ self._started_local_call_contents.register(record.local_call_content)
1179
+ bound = True
1180
+ break
1181
+ for barrier in self._accepted_barriers:
1182
+ if barrier.call_id == provider_call_id and not barrier.released:
1183
+ barrier.released = True
1184
+ self._started_local_call_contents.register(barrier.content)
1185
+ bound = True
1186
+ break
1187
+ if not bound:
1188
+ self._unbound_local_start_counts[provider_call_id] += 1
1189
+ await self._drain_live_prefix()
1190
+ await self._drain_reconciliation()
1191
+
1192
+ async def attempt_rejected(
1193
+ self,
1194
+ reason: str = "",
1195
+ *,
1196
+ status: str = HostedToolStatus.FAILED,
1197
+ preserve_provisional: bool = False,
1198
+ ) -> None:
1199
+ """Terminalize visible cards and conclude all rejected-attempt buffers.
1200
+
1201
+ Validation failures and retries retract presentation-only text. A
1202
+ user interrupt may instead preserve text the user has already read;
1203
+ publishing it as accepted also commits its reserved persistence batch.
1204
+ """
1205
+ if self._published_provisional_segments:
1206
+ if preserve_provisional:
1207
+ segments = tuple(dict.fromkeys(self._published_provisional_segments.values()))
1208
+ await self._publish_once(PresentationAttemptAcceptedOp(self._attempt_id(), segments))
1209
+ else:
1210
+ await self._publish_once(PresentationAttemptRejectedOp(self._attempt_id()))
1211
+ terminal_status = (
1212
+ HostedToolStatus.INTERRUPTED if status == HostedToolStatus.INTERRUPTED else HostedToolStatus.FAILED
1213
+ )
1214
+ for occurrence in self._occurrences:
1215
+ if not occurrence.start_published or occurrence.terminal or occurrence.last_view is None:
1216
+ continue
1217
+ metadata = dict(occurrence.last_view.metadata)
1218
+ result_text = occurrence.last_view.result_text
1219
+ if terminal_status is HostedToolStatus.FAILED:
1220
+ metadata[TOOL_ERRORED_METADATA_KEY] = True
1221
+ metadata[TOOL_ERROR_MESSAGE_METADATA_KEY] = reason or "Response attempt rejected."
1222
+ result_text, failure_text_synthesized = _failure_text(reason, terminal_status)
1223
+ if failure_text_synthesized:
1224
+ metadata[TOOL_FAILURE_TEXT_SYNTHESIZED_METADATA_KEY] = True
1225
+ terminal_view = replace(
1226
+ occurrence.last_view,
1227
+ result_text=result_text,
1228
+ status=terminal_status,
1229
+ provider_status=terminal_status,
1230
+ phase=HostedToolPhase.TERMINAL,
1231
+ metadata=metadata,
1232
+ )
1233
+ operation = HostedToolStatusOp(
1234
+ occurrence.ordinal,
1235
+ occurrence.presentation_id,
1236
+ terminal_view,
1237
+ (0, len(self._live_records), 0),
1238
+ )
1239
+ await self._publish_once(operation)
1240
+ occurrence.terminal = True
1241
+ self._clear_attempt_state()
1242
+
1243
+ async def attempt_accepted(self, messages: Sequence[Message] | None = None) -> None:
1244
+ """Mark validation accepted and reconcile its live-visible operations."""
1245
+ for occurrence in self._occurrences:
1246
+ if occurrence not in self._accepted_occurrences:
1247
+ self._accepted_occurrences.append(occurrence)
1248
+ if messages is not None:
1249
+ # The client's intermediate-text callback already owns this
1250
+ # response's narration, and a final reconcile over a run with
1251
+ # hosted output would publish it a second time.
1252
+ for content in intermediate_text_contents(messages):
1253
+ self._published_canonical_text_contents.register(content)
1254
+ # A logical response inside agent.run() is not necessarily the
1255
+ # turn's final response: local function calls can continue the
1256
+ # loop. Publish its canonical text/tool ordering now, but leave
1257
+ # the terminal FinalTextOp to the run-completion reconciliation.
1258
+ await self._reconcile_accepted(
1259
+ messages,
1260
+ final=False,
1261
+ publish_final=False,
1262
+ occurrence_candidates=tuple(self._occurrences),
1263
+ )
1264
+
1265
+ async def reconcile_accepted(
1266
+ self,
1267
+ messages: Sequence[Message],
1268
+ *,
1269
+ final: bool = False,
1270
+ publish_final: bool = True,
1271
+ ) -> None:
1272
+ """Reconcile live events against assembled content and emit final text last.
1273
+
1274
+ ``final`` marks the run-completion reconcile: no further local tool
1275
+ starts can arrive after it. ``publish_final`` is false while accepting
1276
+ one non-terminal response inside the tool loop.
1277
+ """
1278
+ accepted_ordinals = {occurrence.ordinal for occurrence in self._accepted_occurrences}
1279
+ occurrence_candidates = (
1280
+ *self._accepted_occurrences,
1281
+ *(occurrence for occurrence in self._occurrences if occurrence.ordinal not in accepted_ordinals),
1282
+ )
1283
+ await self._reconcile_accepted(
1284
+ messages,
1285
+ final=final,
1286
+ publish_final=publish_final,
1287
+ occurrence_candidates=occurrence_candidates,
1288
+ )
1289
+
1290
+ async def _reconcile_accepted(
1291
+ self,
1292
+ messages: Sequence[Message],
1293
+ *,
1294
+ final: bool,
1295
+ publish_final: bool,
1296
+ occurrence_candidates: Sequence[_HostedOccurrence],
1297
+ ) -> None:
1298
+ response_key = (self.attempt_index, self.response_index)
1299
+ if publish_final and response_key in self._finalized_response_keys:
1300
+ return
1301
+ plan = ResponsePresentationPlan.from_messages(messages)
1302
+ mapped_occurrences: dict[int, _HostedOccurrence] = {}
1303
+ used_ordinals: set[int] = set()
1304
+ reconciled: list[PresentationSinkOperation] = []
1305
+ for operation in plan.operations:
1306
+ if isinstance(operation, IntermediateTextOp):
1307
+ reconciled.append(replace(operation, attempt_id=self._attempt_id()))
1308
+ continue
1309
+ occurrence = mapped_occurrences.get(operation.occurrence_ordinal)
1310
+ if occurrence is None:
1311
+ occurrence = self._match_reconciled_occurrence(
1312
+ operation.view,
1313
+ used_ordinals,
1314
+ occurrence_candidates,
1315
+ )
1316
+ mapped_occurrences[operation.occurrence_ordinal] = occurrence
1317
+ used_ordinals.add(occurrence.ordinal)
1318
+ reconciled.append(
1319
+ replace(
1320
+ operation,
1321
+ occurrence_ordinal=occurrence.ordinal,
1322
+ presentation_id=occurrence.presentation_id,
1323
+ )
1324
+ )
1325
+
1326
+ accepted_segments, reconciled = self._accept_provisional_text(reconciled)
1327
+ reconciled = [
1328
+ operation
1329
+ for operation in reconciled
1330
+ if not (
1331
+ isinstance(operation, IntermediateTextOp)
1332
+ and operation.source_contents
1333
+ and all(content in self._published_canonical_text_contents for content in operation.source_contents)
1334
+ )
1335
+ ]
1336
+ if self._published_provisional_segments:
1337
+ await self._publish_once(
1338
+ PresentationAttemptAcceptedOp(
1339
+ self._attempt_id(),
1340
+ tuple(accepted_segments),
1341
+ )
1342
+ )
1343
+
1344
+ self._accepted_barriers = self._plan_barriers(messages)
1345
+ for barrier in self._accepted_barriers:
1346
+ if barrier.content in self._started_local_call_contents:
1347
+ barrier.released = True
1348
+ continue
1349
+ if self._unbound_local_start_counts[barrier.call_id] > 0:
1350
+ barrier.released = True
1351
+ self._started_local_call_contents.register(barrier.content)
1352
+ self._unbound_local_start_counts[barrier.call_id] -= 1
1353
+ if self._unbound_local_start_counts[barrier.call_id] == 0:
1354
+ del self._unbound_local_start_counts[barrier.call_id]
1355
+ if final:
1356
+ # Calls that never entered the tool pipeline (unknown tool,
1357
+ # pre-pipeline argument rejection) publish no start; once the run
1358
+ # is over their barriers can only gate FinalTextOp forever.
1359
+ for barrier in self._accepted_barriers:
1360
+ barrier.released = True
1361
+ pending: list[PresentationSinkOperation] = list(reconciled)
1362
+ if publish_final:
1363
+ pending.append(
1364
+ FinalTextOp(
1365
+ plan.final_text,
1366
+ structured_output_completed=not plan.final_text.strip() and plan.structured_output_completed,
1367
+ )
1368
+ )
1369
+ self._finalized_response_keys.add(response_key)
1370
+ self._pending_reconciliation = deque(pending)
1371
+ await self._drain_reconciliation()
1372
+
1373
+ async def publish_blocking(self, messages: Sequence[Message]) -> None:
1374
+ """Plan an assembled blocking response and publish its final sentinel last."""
1375
+ if self.attempt_index < 0:
1376
+ await self.attempt_started()
1377
+ plan = ResponsePresentationPlan.from_messages(messages)
1378
+ mapped: dict[int, _HostedOccurrence] = {}
1379
+ for operation in plan.operations:
1380
+ outbound: PresentationOperation = operation
1381
+ if not isinstance(operation, IntermediateTextOp):
1382
+ occurrence = mapped.get(operation.occurrence_ordinal)
1383
+ if occurrence is None:
1384
+ occurrence = self._allocate_occurrence(operation.view)
1385
+ mapped[operation.occurrence_ordinal] = occurrence
1386
+ outbound = replace(
1387
+ operation,
1388
+ occurrence_ordinal=occurrence.ordinal,
1389
+ presentation_id=occurrence.presentation_id,
1390
+ )
1391
+ await self._publish_once(outbound)
1392
+ await self._publish_sink(
1393
+ FinalTextOp(
1394
+ plan.final_text,
1395
+ structured_output_completed=not plan.final_text.strip() and plan.structured_output_completed,
1396
+ )
1397
+ )
1398
+
1399
+ async def _observe_call(self, content: Content) -> None:
1400
+ view = adapt_hosted_tool(content)
1401
+ occurrence = self._call_occurrence(content, view)
1402
+ if occurrence.terminal and not occurrence.call_seen:
1403
+ occurrence.call_seen = True
1404
+ if occurrence.last_view is not None:
1405
+ occurrence.last_view = replace(
1406
+ occurrence.last_view,
1407
+ tool_name=view.tool_name,
1408
+ display_title=view.display_title,
1409
+ arguments=view.arguments,
1410
+ )
1411
+ return
1412
+ occurrence.call_seen = True
1413
+ occurrence.last_view = view
1414
+ operations = _hosted_ops(
1415
+ occurrence=occurrence.ordinal,
1416
+ view=view,
1417
+ order=(0, len(self._live_records), 0),
1418
+ start=not self._has_start_record(occurrence),
1419
+ result_content=False,
1420
+ )
1421
+ for operation in operations:
1422
+ await self._append_live(replace(operation, presentation_id=occurrence.presentation_id))
1423
+ status = normalize_hosted_tool_status(view.status)
1424
+ if view.phase == HostedToolPhase.TERMINAL or status in _TERMINAL_STATUSES:
1425
+ occurrence.terminal = True
1426
+
1427
+ async def _observe_result(self, content: Content) -> None:
1428
+ key = self._pairing_key(content)
1429
+ provider_item_id = content.provider_item_id or ""
1430
+ occurrence = (
1431
+ self._provider_item_candidates[provider_item_id][-1]
1432
+ if provider_item_id and self._provider_item_candidates[provider_item_id]
1433
+ else next(
1434
+ (candidate for candidate in reversed(self._open_occurrences[key]) if not candidate.result_seen),
1435
+ None,
1436
+ )
1437
+ )
1438
+ if occurrence is None:
1439
+ view = adapt_hosted_tool(None, content)
1440
+ occurrence = self._allocate_occurrence(view, pairing_key=key)
1441
+ view = adapt_hosted_tool(None, content)
1442
+ if occurrence.call_seen:
1443
+ view = replace(
1444
+ view,
1445
+ tool_name=occurrence.last_view.tool_name if occurrence.last_view is not None else view.tool_name,
1446
+ display_title=(
1447
+ occurrence.last_view.display_title if occurrence.last_view is not None else view.display_title
1448
+ ),
1449
+ arguments=occurrence.last_view.arguments if occurrence.last_view is not None else view.arguments,
1450
+ )
1451
+ occurrence.result_seen = True
1452
+ occurrence.last_view = view
1453
+ operations = _hosted_ops(
1454
+ occurrence=occurrence.ordinal,
1455
+ view=view,
1456
+ order=(0, len(self._live_records), 0),
1457
+ start=not self._has_start_record(occurrence),
1458
+ result_content=True,
1459
+ )
1460
+ for operation in operations:
1461
+ await self._append_live(replace(operation, presentation_id=occurrence.presentation_id))
1462
+ status = normalize_hosted_tool_status(view.status)
1463
+ if view.phase == HostedToolPhase.TERMINAL or status in _TERMINAL_STATUSES:
1464
+ occurrence.terminal = True
1465
+
1466
+ def _call_occurrence(self, content: Content, view: HostedToolView) -> _HostedOccurrence:
1467
+ provider_item_id = content.provider_item_id or ""
1468
+ if provider_item_id and self._provider_item_candidates[provider_item_id]:
1469
+ return self._provider_item_candidates[provider_item_id][-1]
1470
+ key = self._pairing_key(content)
1471
+ for occurrence in reversed(self._open_occurrences[key]):
1472
+ if not occurrence.call_seen or not occurrence.result_seen:
1473
+ return occurrence
1474
+ return self._allocate_occurrence(view, pairing_key=key)
1475
+
1476
+ def _allocate_occurrence(
1477
+ self,
1478
+ view: HostedToolView,
1479
+ *,
1480
+ pairing_key: tuple[str, str] | None = None,
1481
+ ) -> _HostedOccurrence:
1482
+ ordinal = self.next_occurrence_ordinal
1483
+ self.next_occurrence_ordinal += 1
1484
+ occurrence = _HostedOccurrence(
1485
+ ordinal=ordinal,
1486
+ presentation_id=(
1487
+ f"hosted:{self.run_generation}:{max(self.attempt_index, 0)}:{self.response_index}:{ordinal}"
1488
+ ),
1489
+ pairing_key=pairing_key or ("call_id", view.provider_call_id),
1490
+ provider_item_id=str(view.metadata.get("provider_item_id", "")),
1491
+ last_view=view,
1492
+ )
1493
+ self._occurrences.append(occurrence)
1494
+ self._open_occurrences[occurrence.pairing_key].append(occurrence)
1495
+ if occurrence.provider_item_id:
1496
+ self._provider_item_candidates[occurrence.provider_item_id].append(occurrence)
1497
+ return occurrence
1498
+
1499
+ def _match_reconciled_occurrence(
1500
+ self,
1501
+ view: HostedToolView,
1502
+ used_ordinals: set[int],
1503
+ candidates: Sequence[_HostedOccurrence],
1504
+ ) -> _HostedOccurrence:
1505
+ provider_item_id = str(view.metadata.get("provider_item_id", ""))
1506
+ if provider_item_id:
1507
+ for occurrence in candidates:
1508
+ if occurrence.provider_item_id != provider_item_id:
1509
+ continue
1510
+ if occurrence.ordinal not in used_ordinals:
1511
+ return occurrence
1512
+ for occurrence in candidates:
1513
+ if occurrence.ordinal in used_ordinals:
1514
+ continue
1515
+ if occurrence.pairing_key == ("call_id", view.provider_call_id):
1516
+ return occurrence
1517
+ if occurrence.last_view is not None and (
1518
+ occurrence.last_view.family,
1519
+ occurrence.last_view.tool_name,
1520
+ ) == (view.family, view.tool_name):
1521
+ return occurrence
1522
+ return self._allocate_occurrence(view)
1523
+
1524
+ def _pairing_key(self, content: Content) -> tuple[str, str]:
1525
+ if content.type.startswith("image_generation_"):
1526
+ return "image_id", content.image_id or ""
1527
+ return "call_id", content.call_id or ""
1528
+
1529
+ def _has_start_record(self, occurrence: _HostedOccurrence) -> bool:
1530
+ return any(
1531
+ isinstance(record.operation, HostedToolStartOp)
1532
+ and record.operation.occurrence_ordinal == occurrence.ordinal
1533
+ for record in self._live_records
1534
+ )
1535
+
1536
+ async def _append_live(self, operation: HostedToolOperation) -> None:
1537
+ self._live_records.append(_LiveRecord(operation=operation))
1538
+ await self._drain_live_prefix()
1539
+
1540
+ async def _seal_live_text_segments(self) -> None:
1541
+ """Publish completed text only once later output proves it intermediate."""
1542
+ if not self._live_text_segments:
1543
+ return
1544
+ for segment_id, segment in self._live_text_segments.items():
1545
+ segment.record.operation = IntermediateTextOp(
1546
+ "".join(segment.parts),
1547
+ segment.order,
1548
+ attempt_id=self._attempt_id(),
1549
+ segment_ids=(segment_id,),
1550
+ provisional=True,
1551
+ )
1552
+ segment.record.text_segment_id = ""
1553
+ self._live_text_segments.clear()
1554
+ await self._drain_live_prefix()
1555
+
1556
+ async def _drain_live_prefix(self) -> None:
1557
+ while self._live_cursor < len(self._live_records):
1558
+ record = self._live_records[self._live_cursor]
1559
+ if record.local_call_id:
1560
+ if not record.released:
1561
+ return
1562
+ self._live_cursor += 1
1563
+ continue
1564
+ if record.text_segment_id:
1565
+ return
1566
+ if isinstance(record.operation, IntermediateTextOp):
1567
+ if not record.operation.provisional:
1568
+ return
1569
+ await self._publish_once(record.operation)
1570
+ for segment_id in record.operation.segment_ids:
1571
+ self._published_provisional_segments[segment_id] = record.operation
1572
+ self._live_cursor += 1
1573
+ continue
1574
+ if record.operation is None:
1575
+ self._live_cursor += 1
1576
+ continue
1577
+ await self._publish_once(record.operation)
1578
+ if isinstance(record.operation, HostedToolStartOp):
1579
+ occurrence = self._occurrence_by_ordinal(record.operation.occurrence_ordinal)
1580
+ occurrence.start_published = True
1581
+ self._live_cursor += 1
1582
+
1583
+ async def _drain_reconciliation(self) -> None:
1584
+ while self._pending_reconciliation:
1585
+ operation = self._pending_reconciliation[0]
1586
+ if isinstance(operation, PresentationAttemptAcceptedOp | PresentationAttemptRejectedOp):
1587
+ self._pending_reconciliation.popleft()
1588
+ await self._publish_once(operation)
1589
+ continue
1590
+ if isinstance(operation, FinalTextOp):
1591
+ if any(not barrier.released for barrier in self._accepted_barriers):
1592
+ return
1593
+ elif any(not barrier.released and barrier.order < operation.order for barrier in self._accepted_barriers):
1594
+ return
1595
+ self._pending_reconciliation.popleft()
1596
+ await self._publish_once(operation)
1597
+
1598
+ def _plan_barriers(self, messages: Sequence[Message]) -> list[_PlanBarrier]:
1599
+ barriers: list[_PlanBarrier] = []
1600
+ for message_index, message in enumerate(messages):
1601
+ for content_index, content in enumerate(message.contents):
1602
+ if content.type == "function_call" and not content.informational_only:
1603
+ barriers.append(
1604
+ _PlanBarrier(
1605
+ _provider_call_id(content),
1606
+ (message_index, content_index, 0),
1607
+ content,
1608
+ )
1609
+ )
1610
+ return barriers
1611
+
1612
+ def _occurrence_by_ordinal(self, ordinal: int) -> _HostedOccurrence:
1613
+ return next(occurrence for occurrence in self._occurrences if occurrence.ordinal == ordinal)
1614
+
1615
+ async def _publish_once(self, operation: PresentationSinkOperation) -> None:
1616
+ signature = self._signature(operation)
1617
+ if signature in self._published_signatures:
1618
+ return
1619
+ self._published_signatures.add(signature)
1620
+ if isinstance(operation, HostedToolResultOp) or (
1621
+ isinstance(operation, HostedToolStatusOp)
1622
+ and normalize_hosted_tool_status(operation.view.status) in _TERMINAL_STATUSES
1623
+ ):
1624
+ self.published_terminal_signatures.add(signature)
1625
+ if isinstance(operation, IntermediateTextOp) and not operation.provisional:
1626
+ for content in operation.source_contents:
1627
+ self._published_canonical_text_contents.register(content)
1628
+ await self._publish_sink(operation)
1629
+
1630
+ async def _publish_sink(self, operation: PresentationSinkOperation) -> None:
1631
+ outcome = self.publish(operation)
1632
+ if inspect.isawaitable(outcome):
1633
+ await outcome
1634
+
1635
+ def _signature(self, operation: PresentationSinkOperation) -> tuple[Any, ...]:
1636
+ if isinstance(operation, FinalTextOp):
1637
+ return (
1638
+ "final",
1639
+ self.attempt_index,
1640
+ self.response_index,
1641
+ operation.text,
1642
+ operation.structured_output_completed,
1643
+ )
1644
+ if isinstance(operation, PresentationAttemptAcceptedOp):
1645
+ return ("presentation_accepted", operation.attempt_id)
1646
+ if isinstance(operation, PresentationAttemptRejectedOp):
1647
+ return ("presentation_rejected", operation.attempt_id)
1648
+ if isinstance(operation, IntermediateTextOp):
1649
+ if operation.segment_ids:
1650
+ return ("text", operation.attempt_id, operation.segment_ids, operation.text)
1651
+ return ("text", self.attempt_index, self.response_index, operation.order, operation.text)
1652
+ view = operation.view
1653
+ base = (type(operation).__name__, operation.presentation_id)
1654
+ if isinstance(operation, HostedToolStartOp):
1655
+ return base
1656
+ if isinstance(operation, HostedToolArgsOp):
1657
+ return (*base, json.dumps(_json_safe(view.arguments), sort_keys=True, ensure_ascii=False))
1658
+ if isinstance(operation, HostedToolProgressOp):
1659
+ images = tuple((content.uri, content.media_type, content.file_id) for content in view.image_contents)
1660
+ return (*base, view.phase, view.provider_status, view.result_text, images)
1661
+ if isinstance(operation, HostedToolStatusOp):
1662
+ return (*base, view.status, view.provider_status)
1663
+ artifacts = tuple((content.file_id, content.uri, content.media_type) for content in view.artifacts)
1664
+ images = tuple((content.uri, content.media_type, content.file_id) for content in view.image_contents)
1665
+ return (*base, view.status, view.provider_status, view.result_text, images, artifacts)
1666
+
1667
+ def _clear_attempt_state(self) -> None:
1668
+ self._live_records.clear()
1669
+ self._live_cursor = 0
1670
+ self._occurrences.clear()
1671
+ self._open_occurrences.clear()
1672
+ self._provider_item_candidates.clear()
1673
+ self._live_text_segments.clear()
1674
+ self._published_provisional_segments.clear()
1675
+ self._accepted_barriers.clear()
1676
+ self._pending_reconciliation.clear()
1677
+ self._saw_incremental_contents = False
1678
+ self._saw_hosted_contents = False
1679
+
1680
+ def _attempt_id(self) -> str:
1681
+ return f"presentation:{self.run_generation}:{self.response_index}:{max(self.attempt_index, 0)}"
1682
+
1683
+ def _accept_provisional_text(
1684
+ self,
1685
+ operations: list[PresentationSinkOperation],
1686
+ ) -> tuple[list[IntermediateTextOp], list[PresentationSinkOperation]]:
1687
+ """Match live provisional text to canonical intermediate operations."""
1688
+ if not self._published_provisional_segments:
1689
+ return [], operations
1690
+ accepted: list[IntermediateTextOp] = []
1691
+ retained: list[PresentationSinkOperation] = []
1692
+ accepted_ids: set[str] = set()
1693
+ published_by_text: dict[str, list[IntermediateTextOp]] = defaultdict(list)
1694
+ for operation in dict.fromkeys(self._published_provisional_segments.values()):
1695
+ published_by_text[operation.text].append(operation)
1696
+
1697
+ for operation in operations:
1698
+ if not isinstance(operation, IntermediateTextOp):
1699
+ retained.append(operation)
1700
+ continue
1701
+ matched: list[IntermediateTextOp] = []
1702
+ if operation.segment_ids and all(
1703
+ segment_id in self._published_provisional_segments for segment_id in operation.segment_ids
1704
+ ):
1705
+ matched = list(
1706
+ dict.fromkeys(
1707
+ self._published_provisional_segments[segment_id] for segment_id in operation.segment_ids
1708
+ )
1709
+ )
1710
+ elif candidates := published_by_text.get(operation.text):
1711
+ matched = [
1712
+ candidate for candidate in candidates if not accepted_ids.intersection(candidate.segment_ids)
1713
+ ][:1]
1714
+ if not matched:
1715
+ retained.append(operation)
1716
+ continue
1717
+ # The streamed fragments are presentation-only objects, while
1718
+ # ``operation.source_contents`` belongs to the assembled response
1719
+ # that later becomes the final run transcript. Remember that
1720
+ # canonical identity now so run-completion reconciliation cannot
1721
+ # re-publish an already accepted provisional segment.
1722
+ for content in operation.source_contents:
1723
+ self._published_canonical_text_contents.register(content)
1724
+ for candidate in matched:
1725
+ if accepted_ids.intersection(candidate.segment_ids):
1726
+ continue
1727
+ accepted.append(candidate)
1728
+ accepted_ids.update(candidate.segment_ids)
1729
+ return accepted, retained
1730
+
1731
+
1732
+ __all__ = [
1733
+ "CodeHostedAdapter",
1734
+ "FinalSegment",
1735
+ "FinalTextOp",
1736
+ "GenericHostedAdapter",
1737
+ "HostedPresentationBridge",
1738
+ "HostedToolArgsOp",
1739
+ "HostedToolProgressOp",
1740
+ "HostedToolResultOp",
1741
+ "HostedToolStartOp",
1742
+ "HostedToolStatusOp",
1743
+ "HostedToolView",
1744
+ "ImageHostedAdapter",
1745
+ "IntermediateTextOp",
1746
+ "McpHostedAdapter",
1747
+ "PresentationAttemptAcceptedOp",
1748
+ "PresentationAttemptRejectedOp",
1749
+ "PresentationOperation",
1750
+ "PresentationSinkOperation",
1751
+ "ResponsePresentationPlan",
1752
+ "SearchHostedAdapter",
1753
+ "ShellHostedAdapter",
1754
+ "adapt_hosted_tool",
1755
+ "cross_provider_hosted_degradations",
1756
+ "get_hosted_adapter",
1757
+ "hosted_context_summary",
1758
+ "hosted_replay_status",
1759
+ "is_legacy_server_executed_informational_call",
1760
+ "next_hosted_run_generation",
1761
+ "normalize_hosted_replay_content",
1762
+ ]