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,1605 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Replay planning for persisted chat transcript history."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ from collections.abc import Callable
9
+ from contextlib import suppress
10
+ from dataclasses import dataclass, field, replace
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from chrys.app.tui.widgets.chat.context_fold import ContextFoldWidget
14
+ from chrys.app.tui.widgets.chat.messages import (
15
+ INTERRUPTED_CONTINUE_ACTION_MESSAGE,
16
+ INTERRUPTED_RETRY_ACTION_MESSAGE,
17
+ AgentMessage,
18
+ InterruptedMessage,
19
+ UserMessage,
20
+ format_message_created_at,
21
+ process_think_tags,
22
+ resolve_interrupted_message_copy,
23
+ )
24
+ from chrys.app.tui.widgets.chat.ports import ChatMountPort, TranscriptLocalizationPort, TranscriptUiPort
25
+ from chrys.app.tui.widgets.chat.toc_model import TurnTocModel, turn_index_from_extra
26
+ from chrys.app.tui.widgets.chat.tool_call import ToolGroup
27
+ from chrys.foundation.hosted_tools import (
28
+ HOSTED_TOOL_DEFAULT_KIND_BY_FAMILY,
29
+ HostedToolFamily,
30
+ )
31
+ from chrys.foundation.i18n import MessageDef, MessageRef
32
+ from chrys.foundation.i18n.formatting import format_message, sanitize_legacy_block
33
+ from chrys.foundation.models.history_markers import (
34
+ AWAITING_SUB_AGENTS_MESSAGE,
35
+ EXECUTION_FAILED_MESSAGE,
36
+ EXECUTION_INTERRUPTED_MESSAGE,
37
+ SESSION_CLOSED_MESSAGE,
38
+ SUB_AGENT_STATE_DISCARDED_MESSAGE,
39
+ HistoryMarkerKind,
40
+ )
41
+ from chrys.foundation.text.images import is_image_media_type
42
+ from chrys.foundation.tool_result_metadata import TOOL_INTERRUPTED_METADATA_KEY
43
+ from chrys.foundation.trajectory_timing import trajectory_timing_from_metadata
44
+ from chrys.foundation.util.time import parse_created_at
45
+ from chrys.kernel import Content, Message
46
+ from chrys.kernel.exchanges import (
47
+ LEGACY_CALL_CONTENT_TYPE,
48
+ TOOL_CALL_CONTENT_TYPES,
49
+ TOOL_RESULT_CONTENT_TYPES,
50
+ DictAccessor,
51
+ EmptyIdPolicy,
52
+ NoneIdPolicy,
53
+ PairingPolicy,
54
+ iter_exchanges,
55
+ pair_results,
56
+ )
57
+ from chrys.service.agent_middleware.events.hosted_tools import (
58
+ HostedToolView,
59
+ ResponsePresentationPlan,
60
+ _is_hosted_call,
61
+ _is_hosted_result,
62
+ adapt_hosted_tool,
63
+ hosted_replay_status,
64
+ is_legacy_server_executed_informational_call,
65
+ normalize_hosted_replay_content,
66
+ )
67
+ from chrys.service.agent_middleware.events.result_persistence import PERSISTED_RESULT_METADATA_KEYS
68
+ from chrys.service.mutations.tool_names import _FILE_TOOLS
69
+ from chrys.service.session.message_metadata import (
70
+ MESSAGE_CREATED_AT_KEY,
71
+ TOOL_RESULT_METADATA_KEY,
72
+ is_compaction_tool_summary,
73
+ persisted_tool_call_kind,
74
+ )
75
+ from chrys.service.text_blocks import join_text_blocks, reconstruct_text_blocks, text_block_id
76
+
77
+ if TYPE_CHECKING:
78
+ from textual.widget import Widget
79
+
80
+ from chrys.service.context.providers.history import CompressedBlock
81
+
82
+ # Replay renders every function-style call it can display: legacy serialized calls
83
+ # count as calls, informational calls still get cards, and idless calls/results pair
84
+ # positionally within their exchange (None-id and empty-id queues kept separate).
85
+ # Non-string ids are stringified so lookups match the str-normalized `_call_id` view.
86
+ _REPLAY_PAIRING_POLICY = PairingPolicy(
87
+ call_types=TOOL_CALL_CONTENT_TYPES | {LEGACY_CALL_CONTENT_TYPE},
88
+ include_informational_calls=True,
89
+ result_types=TOOL_RESULT_CONTENT_TYPES,
90
+ none_id=NoneIdPolicy.POSITIONAL,
91
+ empty_id=EmptyIdPolicy.POSITIONAL,
92
+ malformed_id="stringify",
93
+ )
94
+ _REPLAY_ONLY_CONTENT_KEYS = {
95
+ "_file_snapshot",
96
+ "_replay_approval",
97
+ "_replay_artifacts",
98
+ "_replay_image_contents",
99
+ "_replay_metadata",
100
+ "_replay_result",
101
+ }
102
+ _STATUS_MESSAGE_DEFINITIONS: dict[str, MessageDef] = {
103
+ HistoryMarkerKind.STATUS_EXECUTION_INTERRUPTED: EXECUTION_INTERRUPTED_MESSAGE,
104
+ HistoryMarkerKind.STATUS_EXECUTION_FAILED: EXECUTION_FAILED_MESSAGE,
105
+ HistoryMarkerKind.STATUS_SESSION_CLOSED: SESSION_CLOSED_MESSAGE,
106
+ HistoryMarkerKind.STATUS_SUB_AGENT_STATE_DISCARDED: SUB_AGENT_STATE_DISCARDED_MESSAGE,
107
+ HistoryMarkerKind.STATUS_AWAITING_SUB_AGENTS: AWAITING_SUB_AGENTS_MESSAGE,
108
+ }
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class ReplayCompatibilityAnnotation:
113
+ """Legacy mutation that must still be applied to the caller's raw message dicts."""
114
+
115
+ target: dict[str, Any]
116
+ values: dict[str, Any]
117
+
118
+ def apply(self) -> None:
119
+ self.target.update(self.values)
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class ReplayToolCall:
124
+ """A planned tool invocation with locally paired replay data."""
125
+
126
+ source: dict[str, Any]
127
+ name: str
128
+ call_id: str
129
+ tool_kind: str = ""
130
+ arguments: Any = None
131
+ result: Any = ""
132
+ metadata: dict[str, Any] | None = None
133
+ image_contents: list[dict[str, Any]] | None = None
134
+ file_snapshot: Any = None
135
+ approval: dict[str, Any] | None = None
136
+ provider_hosted: bool = False
137
+ hosted_family: str = ""
138
+ provider: str = ""
139
+ provider_item_type: str = ""
140
+ provider_status: str = ""
141
+ provider_call_id: str = ""
142
+ canonical_status: str = "completed"
143
+ artifacts: list[dict[str, Any]] | None = None
144
+ duration_ms: int | None = None
145
+ started_at: str | None = None
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class ReplayTurnMarker:
150
+ turn_index: int | None
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class ReplayUserMessage:
155
+ text: str
156
+ contents: list[Any]
157
+ created_at: Any = None
158
+ is_injection: bool = False
159
+ profile_switch_to: str = ""
160
+ duration_ms: int | None = None
161
+
162
+
163
+ @dataclass(frozen=True)
164
+ class ReplayProfileSwitch:
165
+ profile_name: str
166
+
167
+
168
+ @dataclass(frozen=True)
169
+ class ReplayAgentMessage:
170
+ contents: list[Any]
171
+ created_at: Any = None
172
+ is_intermediate: bool = False
173
+ structured_output_completed: bool = False
174
+ intermediate_texts: list[str] = field(default_factory=list)
175
+ status_code: str | None = None
176
+ status_count: int | None = None
177
+ duration_ms: int | None = None
178
+
179
+
180
+ @dataclass(frozen=True)
181
+ class ReplayContextFold:
182
+ context_id: str
183
+ summary: str
184
+
185
+
186
+ @dataclass(frozen=True)
187
+ class ReplayInterrupted:
188
+ reason: str
189
+ source: str = "user"
190
+ action_label: MessageDef | None = None
191
+ status_code: str | None = None
192
+ status_count: int | None = None
193
+
194
+
195
+ ReplayEntry = (
196
+ ReplayTurnMarker
197
+ | ReplayUserMessage
198
+ | ReplayProfileSwitch
199
+ | ReplayAgentMessage
200
+ | ReplayContextFold
201
+ | ReplayInterrupted
202
+ )
203
+
204
+
205
+ @dataclass(frozen=True)
206
+ class ReplayPlan:
207
+ """Ordered replay operations plus compatibility annotations."""
208
+
209
+ entries: list[ReplayEntry]
210
+ annotations: list[ReplayCompatibilityAnnotation]
211
+
212
+ def apply_compatibility_annotations(self) -> None:
213
+ for annotation in self.annotations:
214
+ annotation.apply()
215
+
216
+
217
+ @dataclass
218
+ class _PairedToolData:
219
+ result: Any = ""
220
+ metadata: dict[str, Any] | None = None
221
+ image_contents: list[dict[str, Any]] | None = None
222
+ file_snapshot: Any = None
223
+ hosted_view: HostedToolView | None = None
224
+ artifacts: list[dict[str, Any]] | None = None
225
+ has_result: bool = False
226
+ duration_ms: int | None = None
227
+ started_at: str | None = None
228
+
229
+
230
+ @dataclass
231
+ class _MergedAssistantEntry:
232
+ contents: list[Any]
233
+ additional_properties: dict[str, Any]
234
+ batch_id: Any = None
235
+ is_intermediate: bool = False
236
+ intermediate_texts: list[str] = field(default_factory=list)
237
+ has_preceding_text: bool = False
238
+ merged_role: str = ""
239
+
240
+
241
+ @dataclass
242
+ class _RawEntry:
243
+ message: dict[str, Any]
244
+
245
+
246
+ MergedEntry = _MergedAssistantEntry | _RawEntry
247
+
248
+
249
+ class HistoryReplayPlanner:
250
+ """Transform persisted raw messages into replay operations without mounting widgets."""
251
+
252
+ def build_plan(
253
+ self,
254
+ messages: list[dict[str, Any]],
255
+ *,
256
+ file_snapshots: dict[str, list[Any]] | None = None,
257
+ suppress_marker_text: bool = False,
258
+ ) -> ReplayPlan:
259
+ # Compaction tool summaries are the model's context and never render.
260
+ # Drop them before pairing and merging, so a hidden summary cannot
261
+ # split or join the tool groups around it.
262
+ messages = [message for message in messages if not is_compaction_tool_summary(message)]
263
+ pairings, standalone_hosted_results, annotations = self._pair_tool_calls(
264
+ messages,
265
+ file_snapshots=file_snapshots,
266
+ )
267
+ merged = self._merge_assistant_entries(messages, pairings, standalone_hosted_results, annotations)
268
+ entries = self._build_entries(merged, suppress_marker_text=suppress_marker_text)
269
+ return ReplayPlan(entries=entries, annotations=annotations)
270
+
271
+ def _pair_tool_calls(
272
+ self,
273
+ messages: list[dict[str, Any]],
274
+ *,
275
+ file_snapshots: dict[str, list[Any]] | None,
276
+ ) -> tuple[dict[int, _PairedToolData], dict[int, ReplayToolCall], list[ReplayCompatibilityAnnotation]]:
277
+ pairings: dict[int, _PairedToolData] = {}
278
+ standalone_result_coordinates: set[tuple[int, int]] = set()
279
+ annotations: list[ReplayCompatibilityAnnotation] = []
280
+ snapshot_cursors: dict[str, int] = {}
281
+
282
+ # Exchange-scoped pairing over the serialized transcript: every
283
+ # response sibling shares the exchange's output block, embedded
284
+ # results answer their own message's calls, markers are hard
285
+ # boundaries, and duplicate ids consume one shared per-key cursor.
286
+ accessor = DictAccessor()
287
+ result_by_call: dict[tuple[int, int], dict[str, Any] | None] = {}
288
+ paired_result_coordinates: set[tuple[int, int]] = set()
289
+ for exchange in iter_exchanges(messages, accessor):
290
+ pairing = pair_results(messages, exchange, accessor, _REPLAY_PAIRING_POLICY)
291
+ for slots in (*pairing.truthy_assignments.values(), *pairing.falsy_assignments.values()):
292
+ for call, result in slots:
293
+ result_content: dict[str, Any] | None = None
294
+ if result is not None:
295
+ paired_result_coordinates.add((result.message_index, result.content_index))
296
+ candidate = _message_contents(messages[result.message_index])[result.content_index]
297
+ result_content = candidate if isinstance(candidate, dict) else None
298
+ result_by_call[(call.message_index, call.content_index)] = result_content
299
+ unconsumed_results = [
300
+ result
301
+ for results in (*pairing.unconsumed_results.values(), *pairing.falsy_unconsumed_results.values())
302
+ for result in results
303
+ ]
304
+ for result in [*unconsumed_results, *pairing.unpairable_results]:
305
+ coordinate = (result.message_index, result.content_index)
306
+ candidate = _message_contents(messages[result.message_index])[result.content_index]
307
+ if isinstance(candidate, dict) and _is_hosted_tool_result(candidate):
308
+ standalone_result_coordinates.add(coordinate)
309
+
310
+ # ``iter_exchanges`` deliberately starts only at a call-bearing
311
+ # assistant run, so a completely standalone output belongs to no
312
+ # exchange. Pairing remains exchange-scoped above; this pass only
313
+ # inventories canonical hosted result occurrences that the pairing
314
+ # authority did not consume, without attempting any id matching.
315
+ for message_index, message in enumerate(messages):
316
+ if message.get("role", "") not in {"assistant", "tool"}:
317
+ continue
318
+ if HistoryMarkerKind.KEY in _message_extra(message):
319
+ continue
320
+ for content_index, content in enumerate(_message_contents(message)):
321
+ coordinate = (message_index, content_index)
322
+ if (
323
+ coordinate not in paired_result_coordinates
324
+ and isinstance(content, dict)
325
+ and _is_hosted_tool_result(content)
326
+ ):
327
+ standalone_result_coordinates.add(coordinate)
328
+
329
+ for index, msg in enumerate(messages):
330
+ if msg.get("role", "") != "assistant":
331
+ continue
332
+ if HistoryMarkerKind.KEY in _message_extra(msg):
333
+ # Marker payloads are chrome: they never render as tools, so
334
+ # they must not consume snapshot cursors that belong to the
335
+ # visible calls around them.
336
+ continue
337
+ indexed_tool_calls = [
338
+ (content_index, content)
339
+ for content_index, content in enumerate(_message_contents(msg))
340
+ if _is_tool_call(content)
341
+ ]
342
+ if not indexed_tool_calls:
343
+ continue
344
+
345
+ for content_index, tool_call in indexed_tool_calls:
346
+ call_id = _call_id(tool_call)
347
+ result_content = result_by_call.get((index, content_index))
348
+
349
+ paired = _PairedToolData()
350
+ timing = _content_timing(result_content) or _content_timing(tool_call)
351
+ if timing is not None:
352
+ paired.duration_ms = timing["duration_ms"]
353
+ paired.started_at = timing["started_at"]
354
+ values: dict[str, Any] = {}
355
+ if _is_hosted_tool_call(tool_call):
356
+ call = _hosted_content_for_replay(tool_call)
357
+ result = _content_for_replay(result_content) if isinstance(result_content, dict) else None
358
+ paired.has_result = result is not None
359
+ paired.hosted_view = adapt_hosted_tool(call, result)
360
+ paired.result = paired.hosted_view.result_text
361
+ persisted_metadata = _replay_tool_metadata(result_content) if result_content is not None else None
362
+ paired.metadata = {
363
+ **(persisted_metadata or {}),
364
+ **paired.hosted_view.metadata,
365
+ } or None
366
+ paired.image_contents = [content.to_dict() for content in paired.hosted_view.image_contents] or None
367
+ paired.artifacts = _hosted_artifact_descriptors(paired.hosted_view) or None
368
+ elif isinstance(result_content, dict):
369
+ paired.result = result_content.get("result", "")
370
+ paired.metadata = _replay_tool_metadata(result_content)
371
+ images = _replay_tool_images(result_content)
372
+ paired.image_contents = images or None
373
+ values["_replay_result"] = paired.result
374
+ if paired.metadata is not None:
375
+ values["_replay_metadata"] = paired.metadata
376
+ if paired.image_contents:
377
+ values["_replay_image_contents"] = paired.image_contents
378
+ if paired.artifacts:
379
+ values["_replay_artifacts"] = paired.artifacts
380
+
381
+ if tool_call.get("name", "") in _FILE_TOOLS:
382
+ snapshots = file_snapshots.get(call_id, []) if file_snapshots else []
383
+ snapshot_index = snapshot_cursors.get(call_id, 0)
384
+ if snapshot_index < len(snapshots):
385
+ paired.file_snapshot = snapshots[snapshot_index]
386
+ values["_file_snapshot"] = paired.file_snapshot
387
+ snapshot_cursors[call_id] = snapshot_index + 1
388
+
389
+ pairings[id(tool_call)] = paired
390
+ annotations.append(ReplayCompatibilityAnnotation(tool_call, values))
391
+
392
+ standalone_hosted_results: dict[int, ReplayToolCall] = {}
393
+ for message_index, message in enumerate(messages):
394
+ for content_index, content in enumerate(_message_contents(message)):
395
+ if (message_index, content_index) not in standalone_result_coordinates:
396
+ continue
397
+ standalone_hosted_results[id(content)] = self._standalone_hosted_replay_tool(
398
+ content,
399
+ message_index=message_index,
400
+ content_index=content_index,
401
+ )
402
+
403
+ return pairings, standalone_hosted_results, annotations
404
+
405
+ def _merge_assistant_entries(
406
+ self,
407
+ messages: list[dict[str, Any]],
408
+ pairings: dict[int, _PairedToolData],
409
+ standalone_hosted_results: dict[int, ReplayToolCall],
410
+ annotations: list[ReplayCompatibilityAnnotation],
411
+ ) -> list[MergedEntry]:
412
+ merged: list[MergedEntry] = []
413
+ legacy_duplicate_sidecars = _legacy_duplicate_intermediate_sidecars(messages)
414
+ for message_index, msg in enumerate(messages):
415
+ role = msg.get("role", "")
416
+ if role not in {"assistant", "tool"}:
417
+ merged.append(_RawEntry(msg))
418
+ continue
419
+
420
+ contents = _message_contents(msg)
421
+ extra = _message_extra(msg)
422
+ if HistoryMarkerKind.KEY in extra:
423
+ # Key presence, matching the grammar's boundary rule: a
424
+ # falsy kind is still chrome, never tool activity.
425
+ merged.append(_RawEntry(msg))
426
+ continue
427
+
428
+ tool_part = [
429
+ content
430
+ for content in contents
431
+ if (role == "assistant" and _is_tool_call(content)) or id(content) in standalone_hosted_results
432
+ ]
433
+
434
+ if role == "tool" and not tool_part:
435
+ continue
436
+
437
+ has_text = _has_visible_text(contents)
438
+ embedded_itext = extra.get("_intermediate_text")
439
+ # _intermediate_text is a replay sidecar for intermediate text
440
+ # already shown before tool execution. Normally suppress it only
441
+ # when the same text is present in this message; the precomputed
442
+ # compatibility set also covers old cross-batch misattribution.
443
+ pending_embedded_itext = (
444
+ None
445
+ if message_index in legacy_duplicate_sidecars or _intermediate_text_is_visible(contents, embedded_itext)
446
+ else embedded_itext
447
+ )
448
+
449
+ if not tool_part:
450
+ if has_text:
451
+ merged.append(
452
+ _MergedAssistantEntry(
453
+ contents=list(contents),
454
+ additional_properties=extra,
455
+ )
456
+ )
457
+ else:
458
+ merged.append(_RawEntry(msg))
459
+ continue
460
+
461
+ call_replay_tools = iter(
462
+ self._build_replay_tool_calls(
463
+ [content for content in tool_part if id(content) not in standalone_hosted_results],
464
+ extra,
465
+ pairings,
466
+ annotations,
467
+ )
468
+ )
469
+ replay_tools = iter(
470
+ [
471
+ standalone_hosted_results[id(content)]
472
+ if id(content) in standalone_hosted_results
473
+ else next(call_replay_tools)
474
+ for content in tool_part
475
+ ]
476
+ )
477
+ pending_text: list[Any] = []
478
+ pending_tools: list[ReplayToolCall] = []
479
+ text_before_next_tool = False
480
+
481
+ for content in contents:
482
+ if _is_tool_call(content) or id(content) in standalone_hosted_results:
483
+ if self._append_text_segment(merged, pending_text, extra, is_intermediate=True):
484
+ text_before_next_tool = True
485
+ pending_text.clear()
486
+ pending_tools.append(next(replay_tools))
487
+ else:
488
+ if pending_tools:
489
+ self._append_replay_tool_group(
490
+ merged,
491
+ list(pending_tools),
492
+ extra,
493
+ pending_embedded_itext,
494
+ has_preceding_text=text_before_next_tool,
495
+ )
496
+ if pending_embedded_itext:
497
+ pending_embedded_itext = None
498
+ pending_tools.clear()
499
+ text_before_next_tool = False
500
+ pending_text.append(content)
501
+
502
+ if pending_tools:
503
+ self._append_replay_tool_group(
504
+ merged,
505
+ list(pending_tools),
506
+ extra,
507
+ pending_embedded_itext,
508
+ has_preceding_text=text_before_next_tool,
509
+ )
510
+ trailing_text_is_final = bool(pending_text) and _hosted_trailing_text_is_final(contents)
511
+ self._append_text_segment(merged, pending_text, extra, is_intermediate=not trailing_text_is_final)
512
+
513
+ return self._coalesce_tool_groups(merged)
514
+
515
+ @staticmethod
516
+ def _standalone_hosted_replay_tool(
517
+ content: dict[str, Any],
518
+ *,
519
+ message_index: int,
520
+ content_index: int,
521
+ ) -> ReplayToolCall:
522
+ """Build one synthetic card from an unpaired canonical hosted result."""
523
+ result = _content_for_replay(content)
524
+ view = adapt_hosted_tool(None, result)
525
+ canonical_status = hosted_replay_status(view, has_result=True)
526
+ persisted_metadata = _replay_tool_metadata(content)
527
+ metadata = {**(persisted_metadata or {}), **view.metadata} or None
528
+ call_id = _call_id(content) or f"hosted-result:{message_index}:{content_index}"
529
+ return ReplayToolCall(
530
+ source=content,
531
+ name=view.tool_name or view.display_title or "hosted_tool",
532
+ call_id=call_id,
533
+ tool_kind=HOSTED_TOOL_DEFAULT_KIND_BY_FAMILY.get(view.family, ""),
534
+ arguments=view.arguments,
535
+ result=view.result_text or ("interrupted" if canonical_status == "interrupted" else ""),
536
+ metadata=metadata,
537
+ image_contents=[item.to_dict() for item in view.image_contents] or None,
538
+ provider_hosted=True,
539
+ hosted_family=view.family,
540
+ provider=view.provider,
541
+ provider_item_type=view.provider_item_type,
542
+ provider_status=canonical_status if canonical_status == "interrupted" else view.provider_status,
543
+ provider_call_id=view.provider_call_id,
544
+ canonical_status=canonical_status,
545
+ artifacts=_hosted_artifact_descriptors(view) or None,
546
+ duration_ms=_content_duration_ms(content),
547
+ started_at=_content_started_at(content),
548
+ )
549
+
550
+ @staticmethod
551
+ def _append_text_segment(
552
+ merged: list[MergedEntry],
553
+ contents: list[Any],
554
+ extra: dict[str, Any],
555
+ *,
556
+ is_intermediate: bool,
557
+ ) -> bool:
558
+ if not _has_visible_text(contents):
559
+ return False
560
+ merged.append(
561
+ _MergedAssistantEntry(
562
+ contents=list(contents),
563
+ additional_properties=extra,
564
+ is_intermediate=is_intermediate,
565
+ )
566
+ )
567
+ return True
568
+
569
+ @staticmethod
570
+ def _append_replay_tool_group(
571
+ merged: list[MergedEntry],
572
+ replay_tools: list[ReplayToolCall],
573
+ extra: dict[str, Any],
574
+ embedded_itext: Any,
575
+ *,
576
+ has_preceding_text: bool,
577
+ ) -> None:
578
+ batch_id = extra.get("_batch_id")
579
+ merge_target = None
580
+ if not has_preceding_text and batch_id is not None:
581
+ for merge_index in range(len(merged) - 1, -1, -1):
582
+ previous = merged[merge_index]
583
+ if (
584
+ isinstance(previous, _MergedAssistantEntry)
585
+ and previous.merged_role == "assistant_tools"
586
+ and previous.batch_id == batch_id
587
+ ):
588
+ merge_target = merge_index
589
+ break
590
+ if not (isinstance(previous, _MergedAssistantEntry) and previous.merged_role == "assistant_tools"):
591
+ break
592
+ if merge_target is not None:
593
+ target = merged[merge_target]
594
+ if isinstance(target, _MergedAssistantEntry):
595
+ target.contents.extend(replay_tools)
596
+ if embedded_itext:
597
+ target.intermediate_texts.append(embedded_itext)
598
+ else:
599
+ intermediate_texts = [embedded_itext] if embedded_itext else []
600
+ merged.append(
601
+ _MergedAssistantEntry(
602
+ contents=list(replay_tools),
603
+ additional_properties=extra,
604
+ batch_id=batch_id,
605
+ intermediate_texts=intermediate_texts,
606
+ has_preceding_text=has_preceding_text,
607
+ merged_role="assistant_tools",
608
+ )
609
+ )
610
+
611
+ def _build_replay_tool_calls(
612
+ self,
613
+ tool_part: list[dict[str, Any]],
614
+ extra: dict[str, Any],
615
+ pairings: dict[int, _PairedToolData],
616
+ annotations: list[ReplayCompatibilityAnnotation],
617
+ ) -> list[ReplayToolCall]:
618
+ approvals: dict[int, dict[str, Any]] = {}
619
+ for tool_call in tool_part:
620
+ content_approval = _approval_from_content(tool_call)
621
+ if content_approval is not None:
622
+ approvals[id(tool_call)] = content_approval
623
+ annotations.append(ReplayCompatibilityAnnotation(tool_call, {"_replay_approval": content_approval}))
624
+
625
+ approval = extra.get("_approval")
626
+ if isinstance(approval, dict):
627
+ tool_name = approval.get("tool_name", "")
628
+ for tool_call in tool_part:
629
+ if (
630
+ tool_call.get("_replay_approval") is not None
631
+ or id(tool_call) in approvals
632
+ or _approval_from_content(tool_call) is not None
633
+ ):
634
+ continue
635
+ if not tool_name or tool_call.get("name", "") == tool_name:
636
+ approvals[id(tool_call)] = approval
637
+ annotations.append(ReplayCompatibilityAnnotation(tool_call, {"_replay_approval": approval}))
638
+ break
639
+
640
+ replay_tools: list[ReplayToolCall] = []
641
+ for tool_call in tool_part:
642
+ paired = pairings.get(id(tool_call), _PairedToolData())
643
+ hosted = paired.hosted_view
644
+ canonical_status = (
645
+ hosted_replay_status(hosted, has_result=paired.has_result)
646
+ if hosted is not None
647
+ else _local_replay_status(paired.metadata)
648
+ )
649
+ replay_result = paired.result or ("interrupted" if canonical_status == "interrupted" else "")
650
+ replay_tools.append(
651
+ ReplayToolCall(
652
+ source=tool_call,
653
+ name=hosted.tool_name if hosted is not None else str(tool_call.get("name", "tool")),
654
+ call_id=_call_id(tool_call),
655
+ tool_kind=(
656
+ HOSTED_TOOL_DEFAULT_KIND_BY_FAMILY.get(hosted.family, "")
657
+ if hosted is not None
658
+ else _persisted_tool_kind(tool_call)
659
+ ),
660
+ arguments=hosted.arguments if hosted is not None else tool_call.get("arguments"),
661
+ result=replay_result,
662
+ metadata=paired.metadata,
663
+ image_contents=paired.image_contents,
664
+ file_snapshot=paired.file_snapshot,
665
+ approval=approvals.get(id(tool_call)) or tool_call.get("_replay_approval"),
666
+ provider_hosted=hosted is not None,
667
+ hosted_family=hosted.family if hosted is not None else "",
668
+ provider=hosted.provider if hosted is not None else "",
669
+ provider_item_type=hosted.provider_item_type if hosted is not None else "",
670
+ provider_status=(
671
+ canonical_status
672
+ if hosted is not None and canonical_status == "interrupted"
673
+ else hosted.provider_status
674
+ if hosted is not None
675
+ else ""
676
+ ),
677
+ provider_call_id=hosted.provider_call_id if hosted is not None else "",
678
+ canonical_status=canonical_status,
679
+ artifacts=paired.artifacts,
680
+ duration_ms=paired.duration_ms,
681
+ started_at=paired.started_at,
682
+ )
683
+ )
684
+ return replay_tools
685
+
686
+ @staticmethod
687
+ def _coalesce_tool_groups(merged: list[MergedEntry]) -> list[MergedEntry]:
688
+ coalesced: list[MergedEntry] = []
689
+ for entry in merged:
690
+ can_merge = (
691
+ isinstance(entry, _MergedAssistantEntry)
692
+ and entry.merged_role == "assistant_tools"
693
+ and not entry.has_preceding_text
694
+ and not entry.intermediate_texts
695
+ and coalesced
696
+ and isinstance(coalesced[-1], _MergedAssistantEntry)
697
+ and coalesced[-1].merged_role == "assistant_tools"
698
+ )
699
+ if can_merge:
700
+ previous = coalesced[-1]
701
+ if isinstance(previous, _MergedAssistantEntry):
702
+ previous.contents.extend(entry.contents)
703
+ # The coalesced entry represents the complete sequence;
704
+ # message-level completion timing belongs to its latest
705
+ # assistant response, not the first tool cycle.
706
+ previous.additional_properties = entry.additional_properties
707
+ else:
708
+ coalesced.append(entry)
709
+ return coalesced
710
+
711
+ def _build_entries(
712
+ self,
713
+ merged: list[MergedEntry],
714
+ *,
715
+ suppress_marker_text: bool,
716
+ ) -> list[ReplayEntry]:
717
+ entries: list[ReplayEntry] = []
718
+ seen_user_in_turn = False
719
+ retry_action_marker_index = _trailing_retry_marker_index(merged)
720
+
721
+ for msg_index, entry in enumerate(merged):
722
+ if isinstance(entry, _MergedAssistantEntry):
723
+ entries.append(
724
+ ReplayAgentMessage(
725
+ contents=list(entry.contents),
726
+ created_at=_message_created_at(entry.additional_properties),
727
+ is_intermediate=entry.is_intermediate,
728
+ intermediate_texts=list(entry.intermediate_texts),
729
+ duration_ms=_trajectory_duration_ms(entry.additional_properties),
730
+ )
731
+ )
732
+ continue
733
+
734
+ msg = entry.message
735
+ role = msg.get("role", "")
736
+ contents = _message_contents(msg)
737
+ extra = _message_extra(msg)
738
+ marker = extra.get(HistoryMarkerKind.KEY)
739
+ status_code = _recognized_status_code(extra)
740
+
741
+ if marker == HistoryMarkerKind.TURN:
742
+ entries.append(ReplayTurnMarker(turn_index_from_extra(extra)))
743
+ seen_user_in_turn = False
744
+ continue
745
+
746
+ if marker == HistoryMarkerKind.SUMMARY:
747
+ entries.append(
748
+ ReplayContextFold(
749
+ context_id=str(extra.get("_block_id", "")),
750
+ summary=_fold_summary(contents),
751
+ )
752
+ )
753
+ seen_user_in_turn = False
754
+ continue
755
+
756
+ if marker == HistoryMarkerKind.INTERRUPTED:
757
+ source = str(extra.get("_interrupted_by", "user"))
758
+ action_label = None
759
+ if msg_index == retry_action_marker_index:
760
+ action_label = (
761
+ INTERRUPTED_RETRY_ACTION_MESSAGE if source == "error" else INTERRUPTED_CONTINUE_ACTION_MESSAGE
762
+ )
763
+ entries.append(
764
+ ReplayInterrupted(
765
+ reason=_first_text(contents) or "Execution interrupted",
766
+ source=source,
767
+ action_label=action_label,
768
+ status_code=status_code,
769
+ status_count=_status_count(extra),
770
+ )
771
+ )
772
+ continue
773
+
774
+ if HistoryMarkerKind.KEY in extra:
775
+ # Status and unrecognized marker kinds (falsy included) fall
776
+ # through as plain text only — chrome payloads never surface
777
+ # as conversation tool activity.
778
+ if suppress_marker_text:
779
+ continue
780
+ text_contents = [
781
+ {**content, "text": sanitize_legacy_block(str(content.get("text", "")))}
782
+ for content in contents
783
+ if isinstance(content, dict) and content.get("type") == "text" and content.get("text")
784
+ ]
785
+ if role == "assistant" and (text_contents or status_code is not None):
786
+ entries.append(
787
+ ReplayAgentMessage(
788
+ contents=text_contents,
789
+ created_at=_message_created_at(extra),
790
+ status_code=status_code,
791
+ status_count=_status_count(extra),
792
+ duration_ms=_trajectory_duration_ms(extra),
793
+ )
794
+ )
795
+ continue
796
+
797
+ if role == "user":
798
+ # A flagged synthetic ``continue`` nudge is a zero-content
799
+ # placeholder (the next resume deletes it) — don't render
800
+ # it, and don't flip ``seen_user_in_turn``. Legacy
801
+ # unflagged nudges are indistinguishable from a user-typed
802
+ # "continue" and keep rendering via the positional fallback.
803
+ if extra.get(HistoryMarkerKind.CONTINUATION_KEY):
804
+ continue
805
+ text = "\n".join(_text_parts(contents)).strip()
806
+ profile_switch_to = str(extra.get(HistoryMarkerKind.PROFILE_SWITCH_TO_KEY) or "")
807
+ if not text:
808
+ if profile_switch_to:
809
+ entries.append(ReplayProfileSwitch(profile_switch_to))
810
+ continue
811
+ is_injection = bool(extra.get(HistoryMarkerKind.INJECTED_KEY, False) or seen_user_in_turn)
812
+ entries.append(
813
+ ReplayUserMessage(
814
+ text=text,
815
+ contents=list(contents),
816
+ created_at=_message_created_at(extra),
817
+ is_injection=is_injection,
818
+ profile_switch_to=profile_switch_to,
819
+ duration_ms=_trajectory_duration_ms(extra),
820
+ )
821
+ )
822
+ if not is_injection:
823
+ seen_user_in_turn = True
824
+ continue
825
+
826
+ if role == "assistant":
827
+ entries.append(
828
+ ReplayAgentMessage(
829
+ contents=list(contents),
830
+ created_at=_message_created_at(extra),
831
+ duration_ms=_trajectory_duration_ms(extra),
832
+ )
833
+ )
834
+
835
+ return _mark_structured_output_completions(entries)
836
+
837
+
838
+ def _mark_structured_output_completions(entries: list[ReplayEntry]) -> list[ReplayEntry]:
839
+ """Mark a turn-ending successful hosted result that has no text response."""
840
+ candidate_index: int | None = None
841
+
842
+ def finalize_candidate() -> None:
843
+ nonlocal candidate_index
844
+ if candidate_index is None:
845
+ return
846
+ candidate = entries[candidate_index]
847
+ if isinstance(candidate, ReplayAgentMessage) and _has_successful_structured_tool(candidate.contents):
848
+ entries[candidate_index] = replace(candidate, structured_output_completed=True)
849
+ candidate_index = None
850
+
851
+ for index, entry in enumerate(entries):
852
+ if isinstance(entry, ReplayTurnMarker):
853
+ finalize_candidate()
854
+ continue
855
+ if isinstance(entry, ReplayUserMessage) and not entry.is_injection:
856
+ finalize_candidate()
857
+ continue
858
+ if isinstance(entry, ReplayInterrupted):
859
+ candidate_index = None
860
+ continue
861
+ if not isinstance(entry, ReplayAgentMessage) or entry.is_intermediate:
862
+ continue
863
+ if _has_visible_text(entry.contents) or any(
864
+ isinstance(content, ReplayToolCall) or (isinstance(content, dict) and _is_tool_call(content))
865
+ for content in entry.contents
866
+ ):
867
+ candidate_index = index
868
+
869
+ finalize_candidate()
870
+ return entries
871
+
872
+
873
+ def _has_successful_structured_tool(contents: list[Any]) -> bool:
874
+ """Return whether replay contents carry a successful non-search hosted call."""
875
+ return any(
876
+ isinstance(content, ReplayToolCall)
877
+ and content.provider_hosted
878
+ and content.canonical_status == "completed"
879
+ and content.hosted_family
880
+ not in {HostedToolFamily.SEARCH, HostedToolFamily.FETCH, HostedToolFamily.TOOL_DISCOVERY}
881
+ for content in contents
882
+ )
883
+
884
+
885
+ class HistoryReplayRenderer:
886
+ """Render planned persisted history into transcript widgets."""
887
+
888
+ def __init__(
889
+ self,
890
+ mount: ChatMountPort,
891
+ ui: TranscriptUiPort,
892
+ toc: TurnTocModel,
893
+ tool_kind_resolver: Callable[[str], str],
894
+ *,
895
+ default_profile: Callable[[], str],
896
+ localization: TranscriptLocalizationPort | None = None,
897
+ tool_widget_configurator: Callable[[Widget], None] | None = None,
898
+ ) -> None:
899
+ self._mount = mount
900
+ self._ui = ui
901
+ self._toc = toc
902
+ self._tool_kind_resolver = tool_kind_resolver
903
+ self._default_profile = default_profile
904
+ self._tool_widget_configurator = tool_widget_configurator
905
+ self._resolve_message = localization.render if localization is not None else format_message
906
+ self._planner = HistoryReplayPlanner()
907
+
908
+ async def replay_history(
909
+ self,
910
+ messages: list[dict[str, Any]],
911
+ *,
912
+ initial_profile: str = "",
913
+ file_snapshots: dict[str, list[Any]] | None = None,
914
+ compressed_blocks: dict[str, CompressedBlock] | None = None,
915
+ ) -> None:
916
+ """Replay saved session messages into the chat transcript."""
917
+ await self._ui.dismiss_welcome_for_content()
918
+ self._ui.on_replay_started()
919
+ plan = self._planner.build_plan(messages, file_snapshots=file_snapshots)
920
+ plan.apply_compatibility_annotations()
921
+ replay_profile = initial_profile or self._default_profile()
922
+ seen_user_in_turn = False
923
+ widgets: list[Widget] = []
924
+
925
+ for entry in plan.entries:
926
+ if isinstance(entry, ReplayTurnMarker):
927
+ self._toc.apply_turn_marker(entry.turn_index)
928
+ seen_user_in_turn = False
929
+ continue
930
+
931
+ if isinstance(entry, ReplayContextFold):
932
+ block = compressed_blocks.get(entry.context_id) if compressed_blocks else None
933
+ if block:
934
+ replay_profile = await self._replay_compressed_block(block, replay_profile, widgets=widgets)
935
+ freed_messages = len(block.messages) if block else 0
936
+ turn_range = block.turn_range if block else (0, 0)
937
+ widgets.append(
938
+ ContextFoldWidget(
939
+ entry.context_id,
940
+ entry.summary,
941
+ freed_messages,
942
+ turn_range,
943
+ resolve_message=self._resolve_message,
944
+ )
945
+ )
946
+ seen_user_in_turn = False
947
+ continue
948
+
949
+ if isinstance(entry, ReplayInterrupted):
950
+ reason = self._resolve_status(entry.status_code, entry.status_count)
951
+ if reason is None:
952
+ reason = sanitize_legacy_block(entry.reason)
953
+ copy = resolve_interrupted_message_copy(reason, entry.source, self._resolve_message)
954
+ action_label = None
955
+ if entry.action_label is not None:
956
+ action_label = self._resolve_message(entry.action_label.bind())
957
+ widgets.append(
958
+ InterruptedMessage(copy.reason, entry.source, action_label=action_label, header=copy.header)
959
+ )
960
+ continue
961
+
962
+ if isinstance(entry, ReplayUserMessage):
963
+ if entry.profile_switch_to:
964
+ replay_profile = entry.profile_switch_to
965
+ is_injection = entry.is_injection or seen_user_in_turn
966
+ if is_injection and self._toc.has_items():
967
+ turn_id, _item = self._toc.add_injection(entry.text)
968
+ widget = UserMessage(
969
+ entry.text,
970
+ timestamp=format_message_created_at(entry.created_at),
971
+ is_injection=True,
972
+ contents=entry.contents,
973
+ )
974
+ else:
975
+ turn_id, _item = self._toc.begin_user_turn(entry.text)
976
+ widget = UserMessage(
977
+ entry.text,
978
+ timestamp=format_message_created_at(entry.created_at),
979
+ contents=entry.contents,
980
+ )
981
+ seen_user_in_turn = True
982
+ widget.id = turn_id
983
+ widgets.append(widget)
984
+ continue
985
+
986
+ if isinstance(entry, ReplayProfileSwitch):
987
+ replay_profile = entry.profile_name
988
+ continue
989
+
990
+ if isinstance(entry, ReplayAgentMessage):
991
+ widgets.extend(
992
+ AgentMessage(
993
+ itext,
994
+ is_final=True,
995
+ profile_name=replay_profile,
996
+ is_intermediate=True,
997
+ )
998
+ for itext in entry.intermediate_texts
999
+ if process_think_tags(itext, intermediate=True)
1000
+ )
1001
+ contents = entry.contents
1002
+ status_text = self._resolve_status(entry.status_code, entry.status_count)
1003
+ if status_text is not None:
1004
+ contents = [{"type": "text", "text": status_text}]
1005
+ await self._replay_assistant(
1006
+ contents,
1007
+ profile_name=replay_profile,
1008
+ is_intermediate=entry.is_intermediate,
1009
+ structured_output_completed=entry.structured_output_completed,
1010
+ created_at=entry.created_at,
1011
+ duration_ms=entry.duration_ms,
1012
+ widgets=widgets,
1013
+ )
1014
+
1015
+ if widgets:
1016
+ await self._mount.mount_transcript_widgets(widgets)
1017
+ self._ui.after_replay_history_mounted()
1018
+
1019
+ def _resolve_status(self, status_code: str | None, count: int | None) -> str | None:
1020
+ if status_code is None:
1021
+ return None
1022
+ # A recognized code the display map does not cover, or a dynamic
1023
+ # semantic marker missing its structured fields, fails closed to the
1024
+ # persisted literal — same contract as an unrecognized code.
1025
+ definition = _STATUS_MESSAGE_DEFINITIONS.get(status_code)
1026
+ if definition is None:
1027
+ return None
1028
+ reference: MessageRef
1029
+ if status_code == HistoryMarkerKind.STATUS_AWAITING_SUB_AGENTS:
1030
+ if not count:
1031
+ return None
1032
+ reference = definition.bind(count=count)
1033
+ else:
1034
+ reference = definition.bind()
1035
+ return self._resolve_message(reference)
1036
+
1037
+ async def _replay_compressed_block(
1038
+ self,
1039
+ block: CompressedBlock,
1040
+ replay_profile: str,
1041
+ *,
1042
+ widgets: list[Widget] | None = None,
1043
+ ) -> str:
1044
+ """Render a compressed block through the canonical replay planner."""
1045
+ replay_widgets = widgets if widgets is not None else []
1046
+ serialized_messages = [message.to_dict() for message in block.messages]
1047
+ plan = self._planner.build_plan(serialized_messages, suppress_marker_text=True)
1048
+ plan.apply_compatibility_annotations()
1049
+ seen_user_in_turn = False
1050
+ for entry in plan.entries:
1051
+ if isinstance(entry, ReplayTurnMarker):
1052
+ self._toc.apply_turn_marker(entry.turn_index)
1053
+ seen_user_in_turn = False
1054
+ continue
1055
+
1056
+ if isinstance(entry, ReplayProfileSwitch):
1057
+ replay_profile = entry.profile_name
1058
+ continue
1059
+
1060
+ if isinstance(entry, ReplayUserMessage):
1061
+ if entry.profile_switch_to:
1062
+ replay_profile = entry.profile_switch_to
1063
+ is_injection = entry.is_injection or seen_user_in_turn
1064
+ if is_injection and self._toc.has_items():
1065
+ turn_id, _item = self._toc.add_injection(entry.text, compressed=True)
1066
+ widget = UserMessage(
1067
+ entry.text,
1068
+ timestamp=format_message_created_at(entry.created_at),
1069
+ compressed=True,
1070
+ is_injection=True,
1071
+ contents=entry.contents,
1072
+ )
1073
+ else:
1074
+ turn_id, _item = self._toc.begin_user_turn(entry.text, compressed=True)
1075
+ widget = UserMessage(
1076
+ entry.text,
1077
+ timestamp=format_message_created_at(entry.created_at),
1078
+ compressed=True,
1079
+ contents=entry.contents,
1080
+ )
1081
+ seen_user_in_turn = True
1082
+ widget.id = turn_id
1083
+ replay_widgets.append(widget)
1084
+ continue
1085
+
1086
+ if isinstance(entry, ReplayAgentMessage):
1087
+ replay_widgets.extend(
1088
+ AgentMessage(
1089
+ text,
1090
+ is_final=True,
1091
+ profile_name=replay_profile,
1092
+ is_intermediate=True,
1093
+ is_compressed=True,
1094
+ )
1095
+ for text in entry.intermediate_texts
1096
+ if process_think_tags(text, intermediate=True)
1097
+ )
1098
+ visible_contents = [
1099
+ content
1100
+ for content in entry.contents
1101
+ if not isinstance(content, ReplayToolCall)
1102
+ or content.provider_hosted
1103
+ or (
1104
+ content.source.get("type") == "function_call"
1105
+ and content.source.get("informational_only") is True
1106
+ )
1107
+ ]
1108
+ await self._replay_assistant(
1109
+ visible_contents,
1110
+ profile_name=replay_profile,
1111
+ is_intermediate=entry.is_intermediate,
1112
+ structured_output_completed=entry.structured_output_completed,
1113
+ created_at=entry.created_at,
1114
+ duration_ms=entry.duration_ms,
1115
+ is_compressed=True,
1116
+ widgets=replay_widgets,
1117
+ )
1118
+
1119
+ if widgets is None:
1120
+ await self._mount.mount_transcript_widgets(replay_widgets)
1121
+ return replay_profile
1122
+
1123
+ async def _replay_assistant(
1124
+ self,
1125
+ contents: list[Any],
1126
+ profile_name: str = "",
1127
+ is_intermediate: bool = False,
1128
+ structured_output_completed: bool = False,
1129
+ created_at: Any = None,
1130
+ duration_ms: int | None = None,
1131
+ is_compressed: bool = False,
1132
+ widgets: list[Widget] | None = None,
1133
+ ) -> None:
1134
+ """Replay a single assistant message, preserving text/tool-call order."""
1135
+ replay_widgets = widgets if widgets is not None else []
1136
+ pending_text: list[tuple[str, str | None]] = []
1137
+ pending_tools: list[ReplayToolCall | dict[str, Any]] = []
1138
+ timestamp = "" if is_intermediate else format_message_created_at(created_at)
1139
+
1140
+ async def flush_text() -> None:
1141
+ text = join_text_blocks(reconstruct_text_blocks(pending_text)).strip()
1142
+ pending_text.clear()
1143
+ if text and process_think_tags(text, intermediate=is_intermediate):
1144
+ replay_widgets.append(
1145
+ AgentMessage(
1146
+ text,
1147
+ is_final=True,
1148
+ profile_name=profile_name,
1149
+ is_intermediate=is_intermediate,
1150
+ is_compressed=is_compressed,
1151
+ timestamp=timestamp,
1152
+ duration_ms=None if is_intermediate else duration_ms,
1153
+ )
1154
+ )
1155
+
1156
+ async def flush_tools() -> None:
1157
+ if not pending_tools:
1158
+ return
1159
+ group = ToolGroup(live=False, tool_widget_configurator=self._tool_widget_configurator)
1160
+ for idx, tc in enumerate(pending_tools):
1161
+ if isinstance(tc, ReplayToolCall):
1162
+ name = tc.name
1163
+ call_id = tc.call_id
1164
+ args = tc.arguments
1165
+ result = tc.result or ""
1166
+ snapshot = tc.file_snapshot
1167
+ replay_approval = tc.approval
1168
+ replay_metadata = tc.metadata
1169
+ replay_image_contents = tc.image_contents
1170
+ replay_artifacts = tc.artifacts
1171
+ provider_hosted = tc.provider_hosted
1172
+ hosted_family = tc.hosted_family
1173
+ provider = tc.provider
1174
+ provider_item_type = tc.provider_item_type
1175
+ provider_status = tc.provider_status
1176
+ provider_call_id = tc.provider_call_id
1177
+ canonical_status = tc.canonical_status
1178
+ replay_duration_ms = tc.duration_ms
1179
+ replay_started_at = tc.started_at
1180
+ else:
1181
+ call_id = _call_id(tc)
1182
+ result = tc.get("_replay_result", "") or ""
1183
+ snapshot = tc.get("_file_snapshot")
1184
+ replay_approval = tc.get("_replay_approval")
1185
+ replay_metadata = tc.get("_replay_metadata")
1186
+ replay_image_contents = tc.get("_replay_image_contents")
1187
+ replay_artifacts = tc.get("_replay_artifacts")
1188
+ replay_duration_ms = _content_duration_ms(tc)
1189
+ replay_started_at = _content_started_at(tc)
1190
+ if _is_hosted_tool_call(tc):
1191
+ hosted = adapt_hosted_tool(_hosted_content_for_replay(tc))
1192
+ name = hosted.tool_name
1193
+ args = hosted.arguments
1194
+ provider_hosted = True
1195
+ hosted_family = hosted.family
1196
+ provider = hosted.provider
1197
+ provider_item_type = hosted.provider_item_type
1198
+ provider_status = hosted.provider_status
1199
+ provider_call_id = hosted.provider_call_id
1200
+ canonical_status = hosted_replay_status(hosted, has_result=False)
1201
+ else:
1202
+ name = tc.get("name", "tool")
1203
+ args = tc.get("arguments")
1204
+ provider_hosted = False
1205
+ hosted_family = ""
1206
+ provider = ""
1207
+ provider_item_type = ""
1208
+ provider_status = ""
1209
+ provider_call_id = ""
1210
+ canonical_status = _local_replay_status(replay_metadata)
1211
+ if isinstance(args, str):
1212
+ with suppress(json.JSONDecodeError, ValueError):
1213
+ args = json.loads(args)
1214
+ args_str = ""
1215
+ if isinstance(args, dict):
1216
+ args_str = json.dumps(args, ensure_ascii=False)
1217
+ elif args:
1218
+ args_str = str(args)
1219
+ tool_args = args if isinstance(args, dict) else None
1220
+ if isinstance(tc, ReplayToolCall):
1221
+ tool_kind = tc.tool_kind or self._tool_kind_resolver(name)
1222
+ elif provider_hosted:
1223
+ tool_kind = HOSTED_TOOL_DEFAULT_KIND_BY_FAMILY.get(hosted_family, "")
1224
+ else:
1225
+ tool_kind = _persisted_tool_kind(tc) or self._tool_kind_resolver(name)
1226
+ unique_key = f"{call_id or name}#{idx}"
1227
+ tool_approval = replay_approval.get("status") if isinstance(replay_approval, dict) else None
1228
+ if not isinstance(replay_metadata, dict):
1229
+ replay_metadata = None
1230
+ if not isinstance(replay_image_contents, list):
1231
+ replay_image_contents = None
1232
+ if not isinstance(replay_artifacts, list):
1233
+ replay_artifacts = None
1234
+ await group.add_collapsed_replay_tool(
1235
+ unique_key,
1236
+ name,
1237
+ tool_kind,
1238
+ args_str,
1239
+ args=tool_args,
1240
+ result=result,
1241
+ file_snapshot=snapshot,
1242
+ approval=tool_approval,
1243
+ metadata=replay_metadata,
1244
+ image_contents=replay_image_contents,
1245
+ artifacts=replay_artifacts,
1246
+ provider_hosted=provider_hosted,
1247
+ hosted_family=hosted_family,
1248
+ provider=provider,
1249
+ provider_item_type=provider_item_type,
1250
+ provider_status=provider_status,
1251
+ provider_call_id=provider_call_id,
1252
+ canonical_status=canonical_status,
1253
+ duration_ms=replay_duration_ms or 0,
1254
+ duration_known=replay_duration_ms is not None,
1255
+ timestamp=format_message_created_at(replay_started_at),
1256
+ lazy=True,
1257
+ )
1258
+ replay_widgets.append(group)
1259
+ pending_tools.clear()
1260
+
1261
+ for content in contents:
1262
+ is_tool = isinstance(content, ReplayToolCall) or _is_tool_call(content)
1263
+ if is_tool:
1264
+ if pending_text:
1265
+ await flush_text()
1266
+ pending_tools.append(content)
1267
+ else:
1268
+ if pending_tools:
1269
+ await flush_tools()
1270
+ if isinstance(content, dict) and content.get("type") == "text":
1271
+ text = content.get("text", "")
1272
+ block_id = text_block_id(content.get("additional_properties"))
1273
+ # Even an empty fragment can separate different item IDs.
1274
+ # Suppress empty blocks only after reconstruction.
1275
+ pending_text.append((text or "", block_id))
1276
+ elif isinstance(content, str):
1277
+ pending_text.append((content, None))
1278
+
1279
+ await flush_tools()
1280
+ await flush_text()
1281
+ if structured_output_completed and not is_intermediate:
1282
+ replay_widgets.append(
1283
+ AgentMessage(
1284
+ "✓",
1285
+ is_final=True,
1286
+ profile_name=profile_name,
1287
+ is_compressed=is_compressed,
1288
+ is_structured_completion=True,
1289
+ timestamp=timestamp,
1290
+ duration_ms=duration_ms,
1291
+ )
1292
+ )
1293
+ if widgets is None:
1294
+ await self._mount.mount_transcript_widgets(replay_widgets)
1295
+
1296
+
1297
+ def _message_contents(message: dict[str, Any]) -> list[Any]:
1298
+ contents = message.get("contents", [])
1299
+ return contents if isinstance(contents, list) else []
1300
+
1301
+
1302
+ def _message_extra(message: dict[str, Any]) -> dict[str, Any]:
1303
+ extra = message.get("additional_properties") or {}
1304
+ return extra if isinstance(extra, dict) else {}
1305
+
1306
+
1307
+ def _trajectory_duration_ms(metadata: dict[str, Any]) -> int | None:
1308
+ timing = trajectory_timing_from_metadata(metadata)
1309
+ return timing["duration_ms"] if timing is not None else None
1310
+
1311
+
1312
+ def _message_created_at(metadata: dict[str, Any]) -> Any:
1313
+ created_at = metadata.get(MESSAGE_CREATED_AT_KEY)
1314
+ if parse_created_at(created_at) is not None:
1315
+ return created_at
1316
+ timing = trajectory_timing_from_metadata(metadata)
1317
+ return timing["finished_at"] if timing is not None else None
1318
+
1319
+
1320
+ def _content_timing(content: dict[str, Any] | None) -> dict[str, Any] | None:
1321
+ if content is None:
1322
+ return None
1323
+ metadata = content.get("additional_properties")
1324
+ return trajectory_timing_from_metadata(metadata) if isinstance(metadata, dict) else None
1325
+
1326
+
1327
+ def _content_duration_ms(content: dict[str, Any] | None) -> int | None:
1328
+ timing = _content_timing(content)
1329
+ return timing["duration_ms"] if timing is not None else None
1330
+
1331
+
1332
+ def _content_started_at(content: dict[str, Any] | None) -> str | None:
1333
+ timing = _content_timing(content)
1334
+ return timing["started_at"] if timing is not None else None
1335
+
1336
+
1337
+ def _recognized_status_code(extra: dict[str, Any]) -> str | None:
1338
+ status_code = extra.get(HistoryMarkerKind.STATUS_CODE_KEY)
1339
+ if isinstance(status_code, str) and status_code in HistoryMarkerKind.STATUS_CODES:
1340
+ return status_code
1341
+ return None
1342
+
1343
+
1344
+ def _status_count(extra: dict[str, Any]) -> int:
1345
+ invocation_ids = extra.get("_invocation_ids")
1346
+ return len(invocation_ids) if isinstance(invocation_ids, list) else 0
1347
+
1348
+
1349
+ def _message_timestamp(extra: dict[str, Any] | None) -> str:
1350
+ if not extra:
1351
+ return ""
1352
+ return format_message_created_at(extra.get(MESSAGE_CREATED_AT_KEY))
1353
+
1354
+
1355
+ def _is_tool_call(content: Any) -> bool:
1356
+ return isinstance(content, dict) and content.get("type") in (TOOL_CALL_CONTENT_TYPES | {LEGACY_CALL_CONTENT_TYPE})
1357
+
1358
+
1359
+ def _is_hosted_tool_call(content: dict[str, Any]) -> bool:
1360
+ return (
1361
+ bool(content.get("provider_hosted"))
1362
+ or content.get("type") in TOOL_CALL_CONTENT_TYPES - {"function_call"}
1363
+ or is_legacy_server_executed_informational_call(content)
1364
+ )
1365
+
1366
+
1367
+ def _is_hosted_tool_result(content: dict[str, Any]) -> bool:
1368
+ """Return whether persisted result content belongs to a hosted occurrence."""
1369
+ content_type = content.get("type")
1370
+ return content_type in TOOL_RESULT_CONTENT_TYPES - {"function_result"} or (
1371
+ content_type == "function_result" and bool(content.get("provider_hosted"))
1372
+ )
1373
+
1374
+
1375
+ def _hosted_content_for_replay(content: dict[str, Any]) -> Content:
1376
+ """Return a canonical in-memory hosted view without rewriting persisted data."""
1377
+ return normalize_hosted_replay_content(_content_for_replay(content))
1378
+
1379
+
1380
+ def _content_for_replay(content: dict[str, Any]) -> Content:
1381
+ """Decode persisted content without planner-only compatibility fields."""
1382
+ canonical = {key: value for key, value in content.items() if key not in _REPLAY_ONLY_CONTENT_KEYS}
1383
+ return Content.from_dict(canonical)
1384
+
1385
+
1386
+ def _local_replay_status(metadata: dict[str, Any] | None) -> str:
1387
+ """Derive the non-hosted canonical state from persisted result metadata."""
1388
+ if metadata is not None and metadata.get(TOOL_INTERRUPTED_METADATA_KEY) is True:
1389
+ return "interrupted"
1390
+ return "completed"
1391
+
1392
+
1393
+ def _hosted_trailing_text_is_final(contents: list[Any]) -> bool:
1394
+ """Mirror live planner semantics: text after terminal hosted work is final."""
1395
+ typed: list[Content] = []
1396
+ for content in contents:
1397
+ if not isinstance(content, dict):
1398
+ return False
1399
+ typed.append(_hosted_content_for_replay(content) if _is_tool_call(content) else _content_for_replay(content))
1400
+ if not any(_is_hosted_call(content) or _is_hosted_result(content) for content in typed):
1401
+ # Without hosted occurrences the planner reports any trailing text as final.
1402
+ return False
1403
+ return bool(ResponsePresentationPlan.from_messages([Message("assistant", typed)]).final_text)
1404
+
1405
+
1406
+ def _call_id(content: dict[str, Any]) -> str:
1407
+ if str(content.get("type", "")).startswith("image_generation_"):
1408
+ return str(content.get("image_id") or "")
1409
+ return str(content.get("call_id") or "")
1410
+
1411
+
1412
+ def _hosted_artifact_descriptors(view: HostedToolView) -> list[dict[str, Any]]:
1413
+ descriptors: list[dict[str, Any]] = []
1414
+ for artifact in view.artifacts:
1415
+ descriptor: dict[str, Any] = {
1416
+ "id": artifact.file_id or artifact.vector_store_id or artifact.id or "",
1417
+ "path": artifact.uri or artifact.name or "",
1418
+ "mime": artifact.media_type or "",
1419
+ }
1420
+ size = artifact.additional_properties.get("size")
1421
+ if isinstance(size, int) and not isinstance(size, bool):
1422
+ descriptor["size"] = size
1423
+ descriptors.append({key: value for key, value in descriptor.items() if value != ""})
1424
+ return descriptors
1425
+
1426
+
1427
+ def _approval_from_content(content: dict[str, Any]) -> dict[str, Any] | None:
1428
+ extra = content.get("additional_properties")
1429
+ if not isinstance(extra, dict):
1430
+ return None
1431
+ approval = extra.get("_approval")
1432
+ return approval if isinstance(approval, dict) else None
1433
+
1434
+
1435
+ def _replay_tool_metadata(content: dict[str, Any]) -> dict[str, Any] | None:
1436
+ extra = content.get("additional_properties")
1437
+ if not isinstance(extra, dict):
1438
+ return None
1439
+ metadata: dict[str, Any] = {}
1440
+ nested = extra.get(TOOL_RESULT_METADATA_KEY)
1441
+ if isinstance(nested, dict):
1442
+ metadata.update(nested)
1443
+ metadata.update({key: extra[key] for key in PERSISTED_RESULT_METADATA_KEYS if key in extra})
1444
+ return metadata or None
1445
+
1446
+
1447
+ def _persisted_tool_kind(content: dict[str, Any]) -> str:
1448
+ return persisted_tool_call_kind(content.get("additional_properties"))
1449
+
1450
+
1451
+ def _replay_tool_images(content: dict[str, Any]) -> list[dict[str, Any]]:
1452
+ items = content.get("items")
1453
+ if not isinstance(items, list):
1454
+ return []
1455
+
1456
+ image_items: list[dict[str, Any]] = []
1457
+ for item in items:
1458
+ if not isinstance(item, dict):
1459
+ continue
1460
+ media_type = item.get("media_type")
1461
+ if media_type is None:
1462
+ extra = item.get("additional_properties")
1463
+ media_type = extra.get("media_type") if isinstance(extra, dict) else None
1464
+ if is_image_media_type(media_type):
1465
+ image_items.append(item)
1466
+ return image_items
1467
+
1468
+
1469
+ def _fold_summary(contents: list[Any]) -> str:
1470
+ fallback = ""
1471
+ for content in contents:
1472
+ text = (
1473
+ content.get("text", "") if isinstance(content, dict) else str(content) if isinstance(content, str) else ""
1474
+ )
1475
+ if text and "Summary:" in text:
1476
+ return text.split("Summary:", 1)[1].strip()
1477
+ if text:
1478
+ fallback = text
1479
+ return fallback
1480
+
1481
+
1482
+ def _first_text(contents: list[Any]) -> str:
1483
+ for content in contents:
1484
+ if isinstance(content, dict) and content.get("type") == "text":
1485
+ return str(content.get("text", ""))
1486
+ if isinstance(content, str):
1487
+ return content
1488
+ return ""
1489
+
1490
+
1491
+ def _has_visible_text(contents: list[Any]) -> bool:
1492
+ return any(
1493
+ (isinstance(content, dict) and content.get("type") == "text" and content.get("text"))
1494
+ or isinstance(content, str)
1495
+ for content in contents
1496
+ )
1497
+
1498
+
1499
+ def _intermediate_text_is_visible(contents: list[Any], intermediate_text: Any) -> bool:
1500
+ if not isinstance(intermediate_text, str) or not intermediate_text.strip():
1501
+ return False
1502
+ normalized = intermediate_text.strip()
1503
+ return any(text == normalized for text in _visible_text_variants(contents))
1504
+
1505
+
1506
+ def _legacy_duplicate_intermediate_sidecars(messages: list[dict[str, Any]]) -> set[int]:
1507
+ """Find sidecars misattributed to an adjacent persisted tool batch.
1508
+
1509
+ Older Chrys versions paired captured intermediate text to assistant
1510
+ messages positionally. Retry, resume, and mid-pass compaction could shift
1511
+ that pairing by one batch, leaving the real text in one assistant message
1512
+ and an identical ``_intermediate_text`` sidecar on the adjacent batch.
1513
+
1514
+ This compatibility logic should be deprecated once sessions written by
1515
+ those buggy versions no longer need replay support. Keep it deliberately
1516
+ narrow in the meantime: both messages must be batch-tagged tool calls and
1517
+ separated only by one or more tool-result messages.
1518
+ """
1519
+ duplicates: set[int] = set()
1520
+ for index, message in enumerate(messages):
1521
+ if message.get("role") != "assistant":
1522
+ continue
1523
+ contents = _message_contents(message)
1524
+ if not any(_is_tool_call(content) for content in contents):
1525
+ continue
1526
+ extra = _message_extra(message)
1527
+ intermediate_text = extra.get("_intermediate_text")
1528
+ if "_batch_id" not in extra or _intermediate_text_is_visible(contents, intermediate_text):
1529
+ continue
1530
+
1531
+ for direction in (-1, 1):
1532
+ cursor = index + direction
1533
+ crossed_tool_result = False
1534
+ while 0 <= cursor < len(messages) and messages[cursor].get("role") == "tool":
1535
+ crossed_tool_result = True
1536
+ cursor += direction
1537
+ if not crossed_tool_result or not 0 <= cursor < len(messages):
1538
+ continue
1539
+
1540
+ adjacent = messages[cursor]
1541
+ if adjacent.get("role") != "assistant":
1542
+ continue
1543
+ adjacent_contents = _message_contents(adjacent)
1544
+ adjacent_extra = _message_extra(adjacent)
1545
+ if (
1546
+ "_batch_id" in adjacent_extra
1547
+ and any(_is_tool_call(content) for content in adjacent_contents)
1548
+ and _intermediate_text_is_visible(adjacent_contents, intermediate_text)
1549
+ ):
1550
+ duplicates.add(index)
1551
+ break
1552
+ return duplicates
1553
+
1554
+
1555
+ def _visible_text_variants(contents: list[Any]) -> list[str]:
1556
+ variants: list[str] = []
1557
+ all_parts: list[str] = []
1558
+ segment_parts: list[str] = []
1559
+ # Legacy replay inserted newlines between every fragment, while live text
1560
+ # and session sidecar suppression concatenated without separators. Recognize
1561
+ # both historical forms for backward-compatible _intermediate_text matching.
1562
+ for content in contents:
1563
+ if isinstance(content, dict) and content.get("type") == "text":
1564
+ text = str(content.get("text", ""))
1565
+ elif isinstance(content, str):
1566
+ text = content
1567
+ else:
1568
+ if segment_parts:
1569
+ variants.append("\n".join(segment_parts).strip())
1570
+ variants.append("".join(segment_parts).strip())
1571
+ segment_parts.clear()
1572
+ continue
1573
+ if text:
1574
+ all_parts.append(text)
1575
+ segment_parts.append(text)
1576
+ if segment_parts:
1577
+ variants.append("\n".join(segment_parts).strip())
1578
+ variants.append("".join(segment_parts).strip())
1579
+ if all_parts:
1580
+ variants.append("\n".join(all_parts).strip())
1581
+ variants.append("".join(all_parts).strip())
1582
+ return [text for text in variants if text]
1583
+
1584
+
1585
+ def _text_parts(contents: list[Any]) -> list[str]:
1586
+ parts: list[str] = []
1587
+ for content in contents:
1588
+ if isinstance(content, dict) and content.get("type") == "text":
1589
+ parts.append(str(content.get("text", "")))
1590
+ elif isinstance(content, str):
1591
+ parts.append(content)
1592
+ return parts
1593
+
1594
+
1595
+ def _trailing_retry_marker_index(messages: list[MergedEntry]) -> int | None:
1596
+ for index in range(len(messages) - 1, -1, -1):
1597
+ entry = messages[index]
1598
+ if not isinstance(entry, _RawEntry):
1599
+ return None
1600
+ extra = _message_extra(entry.message)
1601
+ marker = extra.get(HistoryMarkerKind.KEY)
1602
+ if marker == HistoryMarkerKind.TURN:
1603
+ continue
1604
+ return index if marker == HistoryMarkerKind.INTERRUPTED else None
1605
+ return None