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,1377 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Resolve one physical turn attempt into a ``TurnAnalysis``.
4
+
5
+ This module pairs lifecycles, checks coverage and runtime bounds, resolves the
6
+ turn tail, continuation polls and suspensions, computes token usage, and
7
+ assembles the attempt's metrics and their precision; counter-axis checks only
8
+ add diagnostics. The causal graph and time attribution live in
9
+ ``_turn_graph``; context membership evidence lives in ``_context_evidence``.
10
+ Folding retry attempts into one logical turn is ``aggregation``'s job.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections import defaultdict
16
+ from collections.abc import Callable
17
+ from dataclasses import dataclass, field
18
+ from functools import lru_cache
19
+ from itertools import pairwise
20
+ from threading import Event
21
+ from typing import Final, cast
22
+
23
+ from chrys.foundation.trajectory.event_types import TurnEndReason
24
+ from chrys.service.analytics._context_evidence import _compaction_consumption_resolution, _RevisionResolution
25
+ from chrys.service.analytics._facts import (
26
+ _START_FAMILIES,
27
+ _active,
28
+ _Endpoint,
29
+ _ExchangeUsage,
30
+ _Intermediate,
31
+ _lifecycle_cut,
32
+ _LifecycleCut,
33
+ _Node,
34
+ _payload_bool,
35
+ _payload_int,
36
+ _payload_str,
37
+ _payload_value,
38
+ _Segment,
39
+ _Turn,
40
+ )
41
+ from chrys.service.analytics._timeline import (
42
+ _endpoint_in_coverage_runtime,
43
+ _hook_drain_scope,
44
+ _infrastructure_branch_reaches,
45
+ _materialize_timeline,
46
+ _resolved_timeline_operation,
47
+ _ResolvedNode,
48
+ _timeline_hook_id,
49
+ _timeline_identity,
50
+ _timeline_parent_operation,
51
+ _TimelineProjection,
52
+ )
53
+ from chrys.service.analytics._turn_graph import (
54
+ _critical_paths,
55
+ _failed_tool_cp_contributions,
56
+ _project_turn,
57
+ _required_causes_present,
58
+ _server_tool_cp_contributions,
59
+ _turn_flow,
60
+ _wall_partition,
61
+ )
62
+ from chrys.service.analytics.math import interval_length
63
+ from chrys.service.analytics.model import (
64
+ ContextSample,
65
+ Metric,
66
+ Precision,
67
+ TimelineDiagnosticCode,
68
+ TimelineOperation,
69
+ TimeSlice,
70
+ TokenUsage,
71
+ TokenUsageSample,
72
+ TurnAnalysis,
73
+ TurnAttemptRef,
74
+ UsageBucket,
75
+ WallBucket,
76
+ )
77
+ from chrys.service.analytics.reader import raise_if_cancelled as _check_cancelled
78
+
79
+ _WEIGHTED_FAMILIES: Final = frozenset(_START_FAMILIES.values()) | {"approval", "compaction.phase"}
80
+ _OPTIONAL_USAGE_BUCKETS: Final = (UsageBucket.REASONING, UsageBucket.CACHE_READ, UsageBucket.CACHE_CREATION)
81
+
82
+
83
+ @dataclass(slots=True)
84
+ class _ResolutionCache:
85
+ """Resolve-derived state retained for exactly one live fact index."""
86
+
87
+ counter_axis_diagnostics_by_turn: dict[str, tuple[str, ...]] = field(default_factory=dict)
88
+
89
+
90
+ def _turn_token_usage(
91
+ intermediate: _Intermediate,
92
+ turn_id: str,
93
+ inactive_ranges: tuple[tuple[int, int], ...],
94
+ verdict: Metric,
95
+ ) -> TokenUsage:
96
+ exchanges = [
97
+ item for item in intermediate.usage_by_turn.get(turn_id, ()) if _active(item.sequence, inactive_ranges)
98
+ ]
99
+ return _interned_token_usage(tuple(_turn_bucket_usage(exchanges, bucket, verdict) for bucket in UsageBucket))
100
+
101
+
102
+ def _turn_uses_session_carrier_fallback(
103
+ intermediate: _Intermediate,
104
+ turn_id: str,
105
+ inactive_ranges: tuple[tuple[int, int], ...],
106
+ *,
107
+ cancel_event: Event | None,
108
+ ) -> bool:
109
+ for node in intermediate.nodes_by_turn.get(turn_id, ()):
110
+ _check_cancelled(cancel_event)
111
+ if node.family != "tool.operation":
112
+ continue
113
+ for finish in node.finishes:
114
+ if not _active(finish.sequence, inactive_ranges):
115
+ continue
116
+ if (
117
+ _payload_str(finish.payload, "result_item_id") is not None
118
+ and _payload_str(finish.payload, "result_carrier_item_id") is None
119
+ ):
120
+ return True
121
+ return False
122
+
123
+
124
+ def _resolve_turn(
125
+ intermediate: _Intermediate,
126
+ turn: _Turn,
127
+ inactive_ranges: tuple[tuple[int, int], ...],
128
+ *,
129
+ resolution_cache: _ResolutionCache,
130
+ revisions: _RevisionResolution,
131
+ mapped_carrier: Callable[[str], str | None],
132
+ rollback_projection_unresolved: bool,
133
+ refresh_counter_axis: bool,
134
+ cancel_event: Event | None,
135
+ ) -> TurnAnalysis:
136
+ _check_cancelled(cancel_event)
137
+ diagnostics: list[str] = []
138
+ starts = [event for event in turn.starts if _active(event.sequence, inactive_ranges)]
139
+ finishes = [event for event in turn.finishes if _active(event.sequence, inactive_ranges)]
140
+ if len(starts) != 1:
141
+ return _empty_turn(
142
+ intermediate,
143
+ turn.turn_id,
144
+ starts,
145
+ "turn lifecycle is not uniquely opened",
146
+ inactive_ranges=inactive_ranges,
147
+ resolution_cache=resolution_cache,
148
+ revisions=revisions,
149
+ refresh=refresh_counter_axis,
150
+ cancel_event=cancel_event,
151
+ )
152
+ start = starts[0]
153
+ finish = finishes[0] if len(finishes) == 1 else None
154
+ turn_lifecycle_exact = True
155
+ if finish is None:
156
+ diagnostics.append("turn lifecycle has no unique terminal")
157
+ turn_lifecycle_exact = False
158
+ elif finish.scope != start.scope or finish.sequence <= start.sequence or finish.monotonic_ns < start.monotonic_ns:
159
+ diagnostics.append("turn interval has invalid monotonic endpoints")
160
+ turn_lifecycle_exact = False
161
+ tail_end, tail_exact, end_sequence, waited_hook_ids = _turn_tail(
162
+ intermediate, turn, start, finish, inactive_ranges, diagnostics
163
+ )
164
+ visible_end = tail_end or (finish.monotonic_ns if finish is not None else start.monotonic_ns)
165
+ turn_bounds = (start.monotonic_ns, max(start.monotonic_ns, visible_end))
166
+ counter_axis_diagnostics = _counter_axis_diagnostics(
167
+ intermediate,
168
+ turn.turn_id,
169
+ inactive_ranges,
170
+ resolution_cache=resolution_cache,
171
+ axis_start_ns=turn_bounds[0],
172
+ axis_end_ns=turn_bounds[1],
173
+ revisions=revisions,
174
+ refresh=refresh_counter_axis,
175
+ cancel_event=cancel_event,
176
+ )
177
+ integrity_diagnostics: list[str] = []
178
+ coverage_exact = _turn_coverage_exact(
179
+ intermediate,
180
+ start,
181
+ end_sequence,
182
+ inactive_ranges,
183
+ integrity_diagnostics,
184
+ rollback_projection_unresolved=rollback_projection_unresolved,
185
+ )
186
+ diagnostics.extend(integrity_diagnostics)
187
+ resolved_nodes, lifecycle_exact = _resolved_turn_nodes(intermediate, start, inactive_ranges, diagnostics)
188
+ closed_exchange_terminal_sequences = frozenset(
189
+ node.finish.sequence for node in resolved_nodes if node.family == "model.exchange"
190
+ )
191
+ resolved_nodes = [node for node in resolved_nodes if not node.start.side_call]
192
+ compaction_consumption = _compaction_consumption_resolution(
193
+ intermediate,
194
+ resolved_nodes,
195
+ inactive_ranges,
196
+ cancel_event=cancel_event,
197
+ diagnostics=diagnostics,
198
+ )
199
+ _check_cancelled(cancel_event)
200
+ resolved_nodes.extend(_continuation_poll_nodes(resolved_nodes))
201
+ suspension_nodes, suspension_deductions, suspension_exact = _suspension_nodes(
202
+ turn, resolved_nodes, start, finish, inactive_ranges, diagnostics
203
+ )
204
+ resolved_nodes.extend(suspension_nodes)
205
+ projection = _project_turn(
206
+ resolved_nodes,
207
+ turn_bounds,
208
+ start=start,
209
+ finish=finish,
210
+ tail_end=tail_end,
211
+ waited_hook_ids=waited_hook_ids,
212
+ suspension_deductions=suspension_deductions,
213
+ revisions=revisions,
214
+ compaction_consumption=compaction_consumption,
215
+ mapped_carrier=mapped_carrier,
216
+ cancel_event=cancel_event,
217
+ diagnostics=diagnostics,
218
+ )
219
+ wall_durations, idle_intervals = _wall_partition(turn_bounds, projection.slices, cancel_event=cancel_event)
220
+ idle_slices = tuple(
221
+ TimeSlice(
222
+ family="idle",
223
+ slice_index=index,
224
+ turn_id=turn.turn_id,
225
+ runtime_id=start.runtime_id,
226
+ operation_id=None,
227
+ owner="idle(unattributed)",
228
+ start_ns=idle_start,
229
+ end_ns=idle_end,
230
+ wall_bucket=WallBucket.IDLE,
231
+ counts_as_work=False,
232
+ compute_weight=False,
233
+ response_weight=True,
234
+ )
235
+ for index, (idle_start, idle_end) in enumerate(idle_intervals)
236
+ )
237
+ all_slices = tuple(
238
+ sorted((*projection.slices, *idle_slices), key=lambda item: (item.start_ns, item.end_ns, item.slice_id))
239
+ )
240
+ work_slices = [item for item in all_slices if item.counts_as_work]
241
+ exclusive_work = sum(item.duration_ns for item in work_slices)
242
+ work_union = interval_length((item.start_ns, item.end_ns) for item in work_slices)
243
+ overlap_gain = exclusive_work - work_union
244
+ elapsed = turn_bounds[1] - turn_bounds[0]
245
+ precision = (
246
+ Precision.EXACT
247
+ if coverage_exact
248
+ and turn_lifecycle_exact
249
+ and lifecycle_exact
250
+ and tail_exact
251
+ and projection.timeline_exact
252
+ and suspension_exact
253
+ else Precision.UNRESOLVED
254
+ )
255
+ causes_exact = _required_causes_present(resolved_nodes, diagnostics)
256
+ compute_cp_exact = (
257
+ coverage_exact
258
+ and turn_lifecycle_exact
259
+ and lifecycle_exact
260
+ and projection.timeline_exact
261
+ and projection.dag_exact
262
+ and causes_exact
263
+ )
264
+ response_cp_exact = compute_cp_exact and tail_exact and suspension_exact and projection.response_dependency_exact
265
+ critical_paths = _critical_paths(
266
+ resolved_nodes,
267
+ all_slices,
268
+ projection.dependency,
269
+ cancel_event=cancel_event,
270
+ )
271
+ if not critical_paths.compute_bounded:
272
+ diagnostics.append("compute critical-path candidate cap exceeded")
273
+ compute_cp_exact = False
274
+ if not critical_paths.response_bounded:
275
+ diagnostics.append("response critical-path candidate cap exceeded")
276
+ response_cp_exact = False
277
+ if not critical_paths.acyclic:
278
+ diagnostics.append("operation dependency graph contains a cycle")
279
+ compute_cp_exact = False
280
+ response_cp_exact = False
281
+ if not critical_paths.response_reachable:
282
+ diagnostics.append("terminal response is not reachable from the turn root through typed edges")
283
+ response_cp_exact = False
284
+ critical_tool_contributions = (
285
+ _failed_tool_cp_contributions(
286
+ resolved_nodes,
287
+ all_slices,
288
+ projection.dependency,
289
+ response_cp=critical_paths.response_ns,
290
+ cancel_event=cancel_event,
291
+ )
292
+ if response_cp_exact and critical_paths.response_ns is not None
293
+ else {}
294
+ )
295
+ server_critical_contributions = (
296
+ _server_tool_cp_contributions(
297
+ intermediate,
298
+ resolved_nodes,
299
+ all_slices,
300
+ projection.dependency,
301
+ response_cp=critical_paths.response_ns,
302
+ cancel_event=cancel_event,
303
+ )
304
+ if response_cp_exact and critical_paths.response_ns is not None
305
+ else {}
306
+ )
307
+ metric_reason = None if precision is Precision.EXACT else "; ".join(dict.fromkeys(diagnostics))
308
+ unresolved_reason = "; ".join(dict.fromkeys(diagnostics))
309
+ compute_cp_reason = None if compute_cp_exact else unresolved_reason
310
+ response_cp_reason = None if response_cp_exact else unresolved_reason
311
+ wall_metrics = {
312
+ bucket: Metric(value=value, precision=precision, reason=metric_reason)
313
+ for bucket, value in wall_durations.items()
314
+ }
315
+ utilization = {
316
+ bucket: Metric(
317
+ value=(
318
+ sum(item.duration_ns for item in work_slices if item.wall_bucket is bucket) / elapsed
319
+ if elapsed
320
+ else 0.0
321
+ ),
322
+ precision=precision,
323
+ reason=metric_reason,
324
+ )
325
+ for bucket in (WallBucket.MODEL, WallBucket.TOOLS)
326
+ }
327
+ usage = _turn_usage(
328
+ intermediate,
329
+ turn.turn_id,
330
+ inactive_ranges,
331
+ integrity_exact=coverage_exact,
332
+ integrity_reason="; ".join(dict.fromkeys(integrity_diagnostics)),
333
+ turn_lifecycle_exact=turn_lifecycle_exact,
334
+ closed_exchange_terminal_sequences=closed_exchange_terminal_sequences,
335
+ cancel_event=cancel_event,
336
+ )
337
+ token_usage = _turn_token_usage(intermediate, turn.turn_id, inactive_ranges, usage)
338
+ turn_number = _payload_int(start.payload, "turn_number")
339
+ action_projection_diagnostics: list[str] = []
340
+ action_projection_exact = (
341
+ coverage_exact
342
+ and turn_lifecycle_exact
343
+ and _tool_action_projection_exact(
344
+ intermediate,
345
+ start,
346
+ finish.sequence if finish is not None else None,
347
+ inactive_ranges,
348
+ action_projection_diagnostics,
349
+ )
350
+ )
351
+ action_projection_reason = "; ".join(dict.fromkeys((*integrity_diagnostics, *action_projection_diagnostics)))
352
+ operations = _timeline_operations(
353
+ intermediate,
354
+ turn_id=turn.turn_id,
355
+ inactive_ranges=inactive_ranges,
356
+ resolved_nodes=resolved_nodes,
357
+ cancel_event=cancel_event,
358
+ )
359
+ flow = _turn_flow(turn.turn_id, projection.dependency, operations, acyclic=critical_paths.acyclic)
360
+ is_retry = _payload_bool(start.payload, "is_retry")
361
+ return TurnAnalysis(
362
+ turn_id=turn.turn_id,
363
+ turn_number=turn_number,
364
+ runtime_id=start.runtime_id,
365
+ start_sequence=start.sequence,
366
+ end_sequence=end_sequence,
367
+ elapsed_ns=Metric(elapsed, precision, metric_reason),
368
+ compute_cp_ns=Metric(
369
+ critical_paths.compute_ns if compute_cp_exact else None,
370
+ Precision.EXACT if compute_cp_exact else Precision.UNRESOLVED,
371
+ compute_cp_reason,
372
+ ),
373
+ response_cp_ns=Metric(
374
+ critical_paths.response_ns if response_cp_exact else None,
375
+ Precision.EXACT if response_cp_exact else Precision.UNRESOLVED,
376
+ response_cp_reason,
377
+ ),
378
+ exclusive_work_ns=Metric(exclusive_work, precision, metric_reason),
379
+ parallelism=Metric(exclusive_work / elapsed if elapsed else 0.0, precision, metric_reason),
380
+ overlap_gain_ns=Metric(overlap_gain, precision, metric_reason),
381
+ wall_time_ns=wall_metrics,
382
+ utilization=utilization,
383
+ usage_tokens=usage,
384
+ attempts=(
385
+ TurnAttemptRef(
386
+ turn_id=turn.turn_id,
387
+ runtime_id=start.runtime_id,
388
+ is_retry=is_retry,
389
+ physical_axis_start_ns=turn_bounds[0],
390
+ physical_axis_end_ns=turn_bounds[1],
391
+ logical_axis_start_ns=turn_bounds[0],
392
+ operation_start_index=0,
393
+ operation_end_index=len(operations),
394
+ slice_start_index=0,
395
+ slice_end_index=len(all_slices),
396
+ ),
397
+ ),
398
+ axis_start_ns=turn_bounds[0],
399
+ axis_end_ns=turn_bounds[1],
400
+ operations=operations,
401
+ slices=all_slices,
402
+ diagnostics=tuple(dict.fromkeys((*diagnostics, *counter_axis_diagnostics))),
403
+ critical_tool_contributions_ns=critical_tool_contributions,
404
+ server_critical_contributions_ns=server_critical_contributions,
405
+ action_projection_precision=Precision.EXACT if action_projection_exact else Precision.UNRESOLVED,
406
+ action_projection_reason=None if action_projection_exact else action_projection_reason,
407
+ token_usage=token_usage,
408
+ flow=flow,
409
+ )
410
+
411
+
412
+ def _turn_tail(
413
+ intermediate: _Intermediate,
414
+ turn: _Turn,
415
+ start: _Endpoint,
416
+ finish: _Endpoint | None,
417
+ inactive_ranges: tuple[tuple[int, int], ...],
418
+ diagnostics: list[str],
419
+ ) -> tuple[int | None, bool, int | None, frozenset[str] | None]:
420
+ if finish is None:
421
+ return None, False, None, None
422
+ if finish.scope != start.scope or finish.sequence <= start.sequence or finish.monotonic_ns < start.monotonic_ns:
423
+ return finish.monotonic_ns, False, finish.sequence, None
424
+ end_reason = _payload_str(finish.payload, "end_reason")
425
+ if end_reason in {TurnEndReason.PROCESS_EXIT, TurnEndReason.CANCELLED}:
426
+ return finish.monotonic_ns, True, finish.sequence, frozenset()
427
+ markers = [marker for marker in turn.response_markers if _active(marker.sequence, inactive_ranges)]
428
+ if len(markers) != 1:
429
+ diagnostics.append("turn response fence is missing or duplicated")
430
+ return finish.monotonic_ns, False, finish.sequence, None
431
+ marker = markers[0]
432
+ segment_end_sequence = max(
433
+ (
434
+ segment.sequence
435
+ for segment in intermediate.segments.get(marker.event_id or "", ())
436
+ if _active(segment.sequence, inactive_ranges)
437
+ ),
438
+ default=marker.sequence,
439
+ )
440
+ if marker.scope != start.scope or marker.sequence <= finish.sequence or marker.monotonic_ns < finish.monotonic_ns:
441
+ diagnostics.append("turn response fence is outside the turn runtime or precedes turn.finished")
442
+ return finish.monotonic_ns, False, segment_end_sequence, None
443
+ waited_ids = _reassemble_waited_hook_ids(intermediate, marker, inactive_ranges)
444
+ expected = _payload_int(marker.payload, "waited_hook_operation_count")
445
+ if waited_ids is None or expected is None or len(waited_ids) != expected or len(set(waited_ids)) != expected:
446
+ diagnostics.append("turn response fence hook membership is incomplete")
447
+ return marker.monotonic_ns, False, segment_end_sequence, None
448
+ drained_scopes = _payload_value(marker.payload, "drained_scopes")
449
+ outcome = _payload_str(marker.payload, "outcome")
450
+ if (
451
+ not isinstance(drained_scopes, tuple)
452
+ or not all(isinstance(scope, str) for scope in drained_scopes)
453
+ or len(drained_scopes) != len(set(drained_scopes))
454
+ or any(scope != "turn" for scope in drained_scopes)
455
+ or outcome not in {"settled", "cancelled", "partial"}
456
+ or (outcome == "settled" and drained_scopes not in {(), ("turn",)})
457
+ or (waited_ids and "turn" not in drained_scopes)
458
+ ):
459
+ diagnostics.append("turn response fence drained_scopes are inconsistent with its outcome or membership")
460
+ return marker.monotonic_ns, False, segment_end_sequence, None
461
+ hook_nodes = {
462
+ node.operation_id: node
463
+ for node in intermediate.nodes_by_turn.get(turn.turn_id, ())
464
+ if node.family == "hook.operation"
465
+ }
466
+ for operation_id in waited_ids:
467
+ node = hook_nodes.get(operation_id)
468
+ starts = [] if node is None else [event for event in node.starts if _active(event.sequence, inactive_ranges)]
469
+ terminals = (
470
+ [] if node is None else [event for event in node.finishes if _active(event.sequence, inactive_ranges)]
471
+ )
472
+ if (
473
+ len(starts) != 1
474
+ or len(terminals) != 1
475
+ or _payload_str(starts[0].payload, "execution_mode") != "async"
476
+ or _hook_drain_scope(starts[0]) != "turn"
477
+ or starts[0].scope != start.scope
478
+ or terminals[0].scope != start.scope
479
+ or starts[0].sequence <= start.sequence
480
+ or terminals[0].sequence <= starts[0].sequence
481
+ or terminals[0].sequence >= marker.sequence
482
+ or terminals[0].monotonic_ns < starts[0].monotonic_ns
483
+ or not _endpoint_in_coverage_runtime(intermediate, starts[0], inactive_ranges)
484
+ or not _endpoint_in_coverage_runtime(intermediate, terminals[0], inactive_ranges)
485
+ ):
486
+ diagnostics.append("a waited hook is not a uniquely closed async turn-scope hook before the response fence")
487
+ return marker.monotonic_ns, False, segment_end_sequence, None
488
+ return marker.monotonic_ns, True, segment_end_sequence, frozenset(waited_ids)
489
+
490
+
491
+ def _reassemble_waited_hook_ids(
492
+ intermediate: _Intermediate,
493
+ marker: _Endpoint,
494
+ inactive_ranges: tuple[tuple[int, int], ...],
495
+ ) -> list[str] | None:
496
+ declarations = [
497
+ declaration
498
+ for declaration in marker.segmented_fields
499
+ if declaration.field_pointer == "/payload/waited_hook_operation_ids"
500
+ ]
501
+ if len(declarations) != 1:
502
+ return None
503
+ declaration = declarations[0]
504
+ segments = [
505
+ segment
506
+ for segment in intermediate.segments.get(marker.event_id, [])
507
+ if _active(segment.sequence, inactive_ranges) and segment.field_pointer == declaration.field_pointer
508
+ ]
509
+ if len(segments) != declaration.segment_count:
510
+ return None
511
+ indexed_segments: list[tuple[int, _Segment]] = []
512
+ for segment in segments:
513
+ segment_index = segment.segment_index
514
+ if segment_index is None:
515
+ return None
516
+ indexed_segments.append((segment_index, segment))
517
+ indices = [segment_index for segment_index, _ in indexed_segments]
518
+ if sorted(indices) != list(range(declaration.segment_count)):
519
+ return None
520
+ values: list[str] = []
521
+ for _, segment in sorted(indexed_segments):
522
+ if (
523
+ segment.segment_count != declaration.segment_count
524
+ or segment.segment_group_id != declaration.segment_group_id
525
+ or segment.encoding != "array_slice"
526
+ or segment.entry_oversized
527
+ or segment.runtime_id != marker.runtime_id
528
+ or segment.branch_id != marker.branch_id
529
+ or segment.coverage_id != marker.coverage_id
530
+ or segment.turn_id != marker.turn_id
531
+ or segment.sequence <= marker.sequence
532
+ ):
533
+ return None
534
+ entries = segment.entries
535
+ if entries is None or not all(isinstance(item, str) for item in entries):
536
+ return None
537
+ values.extend(item for item in entries if isinstance(item, str))
538
+ return values
539
+
540
+
541
+ def _turn_coverage_exact(
542
+ intermediate: _Intermediate,
543
+ start: _Endpoint,
544
+ end_sequence: int | None,
545
+ inactive_ranges: tuple[tuple[int, int], ...],
546
+ diagnostics: list[str],
547
+ *,
548
+ rollback_projection_unresolved: bool,
549
+ ) -> bool:
550
+ if end_sequence is None:
551
+ return False
552
+ exact = True
553
+ coverage_starts = [
554
+ event
555
+ for event in intermediate.coverage_starts.get(start.coverage_id, [])
556
+ if event.sequence <= start.sequence
557
+ and event.runtime_id == start.runtime_id
558
+ and _infrastructure_branch_reaches(intermediate, event, start, inactive_ranges)
559
+ ]
560
+ if len(coverage_starts) != 1:
561
+ diagnostics.append("turn is not covered by trajectory.coverage.started")
562
+ exact = False
563
+ coverage_ends = [
564
+ event
565
+ for event in intermediate.coverage_ends.get(start.coverage_id, ())
566
+ if event.runtime_id == start.runtime_id
567
+ and _infrastructure_branch_reaches(intermediate, start, event, inactive_ranges)
568
+ ]
569
+ if len(coverage_ends) > 1:
570
+ diagnostics.append("trajectory coverage has duplicate terminal markers")
571
+ exact = False
572
+ elif coverage_ends:
573
+ coverage_end = coverage_ends[0]
574
+ last_sequence = _payload_int(coverage_end.payload, "last_sequence")
575
+ if last_sequence != coverage_end.sequence - 1:
576
+ diagnostics.append("trajectory coverage terminal has an invalid last_sequence")
577
+ exact = False
578
+ elif start.sequence > last_sequence or end_sequence > last_sequence:
579
+ diagnostics.append("turn falls outside its trajectory coverage window")
580
+ exact = False
581
+ runtime_starts = [
582
+ event
583
+ for event in intermediate.runtime_starts.get(start.runtime_id, ())
584
+ if _infrastructure_branch_reaches(intermediate, event, start, inactive_ranges)
585
+ ]
586
+ if len(runtime_starts) > 1:
587
+ diagnostics.append("trajectory runtime has duplicate start endpoints")
588
+ exact = False
589
+ elif runtime_starts and runtime_starts[0].sequence >= start.sequence:
590
+ diagnostics.append("turn begins before its trajectory runtime endpoint")
591
+ exact = False
592
+ runtime_recoveries = [
593
+ event
594
+ for event in intermediate.runtime_recoveries.get(start.runtime_id, ())
595
+ if _infrastructure_branch_reaches(intermediate, event, start, inactive_ranges)
596
+ ]
597
+ if len(runtime_recoveries) > 1:
598
+ diagnostics.append("trajectory runtime has duplicate recovery endpoints")
599
+ exact = False
600
+ elif runtime_recoveries and runtime_recoveries[0].sequence >= start.sequence:
601
+ diagnostics.append("turn begins before its trajectory runtime recovery endpoint")
602
+ exact = False
603
+ runtime_finishes = [
604
+ event
605
+ for event in intermediate.runtime_finishes.get(start.runtime_id, ())
606
+ if _infrastructure_branch_reaches(intermediate, start, event, inactive_ranges)
607
+ ]
608
+ if len(runtime_finishes) > 1:
609
+ diagnostics.append("trajectory runtime has duplicate terminal markers")
610
+ exact = False
611
+ elif runtime_finishes:
612
+ runtime_finish = runtime_finishes[0]
613
+ if start.sequence >= runtime_finish.sequence or end_sequence >= runtime_finish.sequence:
614
+ diagnostics.append("turn occurs after trajectory.runtime.finished")
615
+ exact = False
616
+ matching_coverage_end = coverage_ends[0] if len(coverage_ends) == 1 else None
617
+ if matching_coverage_end is None or matching_coverage_end.sequence >= runtime_finish.sequence:
618
+ diagnostics.append("trajectory runtime closure lacks an ordered coverage terminal")
619
+ exact = False
620
+ for first, last in intermediate.explicit_gaps:
621
+ if first <= end_sequence and last >= start.sequence:
622
+ diagnostics.append("trajectory gap intersects the turn")
623
+ exact = False
624
+ break
625
+ if any(start.sequence <= sequence <= end_sequence for sequence in intermediate.unsupported_sequences):
626
+ diagnostics.append("unsupported trajectory event intersects the turn")
627
+ exact = False
628
+ if any(start.sequence <= sequence + 1 <= end_sequence for sequence in intermediate.corrupt_after_sequences):
629
+ diagnostics.append("corrupt trajectory line intersects the turn")
630
+ exact = False
631
+ if any(violation.first_sequence <= end_sequence for violation in intermediate.prefix_violations):
632
+ diagnostics.append("trajectory accounted-prefix invariant failed")
633
+ exact = False
634
+ if rollback_projection_unresolved:
635
+ diagnostics.append("rollback live-history range is unresolved")
636
+ exact = False
637
+ return exact
638
+
639
+
640
+ def _resolved_turn_nodes(
641
+ intermediate: _Intermediate,
642
+ turn_start: _Endpoint,
643
+ inactive_ranges: tuple[tuple[int, int], ...],
644
+ diagnostics: list[str],
645
+ ) -> tuple[list[_ResolvedNode], bool]:
646
+ resolved: list[_ResolvedNode] = []
647
+ exact = True
648
+ turn_id = turn_start.turn_id or ""
649
+ for node in intermediate.nodes_by_turn.get(turn_id, ()):
650
+ starts = [
651
+ event for event in node.starts if event.turn_id == turn_id and _active(event.sequence, inactive_ranges)
652
+ ]
653
+ finishes = [
654
+ event for event in node.finishes if event.turn_id == turn_id and _active(event.sequence, inactive_ranges)
655
+ ]
656
+ if not starts and not finishes:
657
+ continue
658
+ endpoints = (*starts, *finishes)
659
+ if endpoints and all(event.side_call for event in endpoints):
660
+ # Side-call lifecycles stay out of the turn's arithmetic; the
661
+ # closed ones are kept only so their exchange terminals can vouch
662
+ # for the usage they contributed, and are dropped right after.
663
+ side_call_node = _resolved_side_call_node(intermediate, node, turn_start, starts, finishes, inactive_ranges)
664
+ if side_call_node is not None:
665
+ resolved.append(side_call_node)
666
+ continue
667
+ if any(event.side_call for event in endpoints):
668
+ diagnostics.append(f"{node.family} lifecycle mixes main and non-main actors")
669
+ exact = False
670
+ continue
671
+ cut = _lifecycle_cut(node, inactive_ranges)
672
+ if cut is not _LifecycleCut.NONE:
673
+ diagnostics.append(_lifecycle_cut_reason(node.family, cut))
674
+ exact = False
675
+ continue
676
+ if node.family == "retry" and len(starts) == 1 and not finishes:
677
+ # A lone scheduled marker is a cancelled backoff, not an open span.
678
+ continue
679
+ if len(starts) != 1 or len(finishes) != 1:
680
+ diagnostics.append(f"{node.family} lifecycle is not uniquely closed")
681
+ exact = False
682
+ continue
683
+ start, finish = starts[0], finishes[0]
684
+ if (
685
+ start.runtime_id != turn_start.runtime_id
686
+ or finish.runtime_id != turn_start.runtime_id
687
+ or start.branch_id != turn_start.branch_id
688
+ or finish.branch_id != turn_start.branch_id
689
+ or start.coverage_id != turn_start.coverage_id
690
+ or finish.coverage_id != turn_start.coverage_id
691
+ or start.sequence <= turn_start.sequence
692
+ or finish.sequence <= turn_start.sequence
693
+ ):
694
+ diagnostics.append(f"{node.family} lifecycle does not belong to the owning turn runtime and branch")
695
+ exact = False
696
+ continue
697
+ if (
698
+ start.scope != finish.scope
699
+ or (node.family != "compaction.phase" and finish.sequence <= start.sequence)
700
+ or finish.monotonic_ns < start.monotonic_ns
701
+ ):
702
+ diagnostics.append(f"{node.family} interval has invalid monotonic endpoints")
703
+ exact = False
704
+ continue
705
+ if not _endpoint_in_coverage_runtime(intermediate, start, inactive_ranges) or not _endpoint_in_coverage_runtime(
706
+ intermediate, finish, inactive_ranges
707
+ ):
708
+ diagnostics.append(f"{node.family} lifecycle falls outside coverage or after runtime closure")
709
+ exact = False
710
+ continue
711
+ if node.family in _WEIGHTED_FAMILIES and not finish.monotonic_measurement:
712
+ diagnostics.append(f"{node.family} duration lacks monotonic provenance")
713
+ exact = False
714
+ resolved.append(
715
+ _ResolvedNode(
716
+ node_id=f"{node.family}:{node.operation_id}",
717
+ family=node.family,
718
+ operation_id=node.operation_id,
719
+ start=start,
720
+ finish=finish,
721
+ )
722
+ )
723
+ return resolved, exact
724
+
725
+
726
+ def _resolved_side_call_node(
727
+ intermediate: _Intermediate,
728
+ node: _Node,
729
+ turn_start: _Endpoint,
730
+ starts: list[_Endpoint],
731
+ finishes: list[_Endpoint],
732
+ inactive_ranges: tuple[tuple[int, int], ...],
733
+ ) -> _ResolvedNode | None:
734
+ """Validate a side-call lifecycle in its own actor domain without entering main-turn metrics."""
735
+ if len(starts) != 1 or len(finishes) != 1:
736
+ return None
737
+ start, finish = starts[0], finishes[0]
738
+ if (
739
+ start.scope != finish.scope
740
+ or start.runtime_id != turn_start.runtime_id
741
+ or start.branch_id != turn_start.branch_id
742
+ or start.coverage_id != turn_start.coverage_id
743
+ or start.sequence <= turn_start.sequence
744
+ or finish.sequence <= start.sequence
745
+ or finish.monotonic_ns < start.monotonic_ns
746
+ or not _endpoint_in_coverage_runtime(intermediate, start, inactive_ranges)
747
+ or not _endpoint_in_coverage_runtime(intermediate, finish, inactive_ranges)
748
+ ):
749
+ return None
750
+ return _ResolvedNode(
751
+ node_id=f"{node.family}:{node.operation_id}",
752
+ family=node.family,
753
+ operation_id=node.operation_id,
754
+ start=start,
755
+ finish=finish,
756
+ )
757
+
758
+
759
+ def _tool_action_projection_exact(
760
+ intermediate: _Intermediate,
761
+ turn_start: _Endpoint,
762
+ terminal_sequence: int | None,
763
+ inactive_ranges: tuple[tuple[int, int], ...],
764
+ diagnostics: list[str],
765
+ ) -> bool:
766
+ exact = True
767
+ turn_id = turn_start.turn_id or ""
768
+ for node in intermediate.nodes_by_turn.get(turn_id, ()):
769
+ if node.family != "tool.operation":
770
+ continue
771
+ starts = [event for event in node.starts if _active(event.sequence, inactive_ranges)]
772
+ finishes = [event for event in node.finishes if _active(event.sequence, inactive_ranges)]
773
+ if not starts and not finishes:
774
+ continue
775
+ endpoints = (*starts, *finishes)
776
+ if endpoints and all(event.side_call for event in endpoints):
777
+ continue
778
+ if any(event.side_call for event in endpoints):
779
+ diagnostics.append("tool action projection mixes main and non-main actors")
780
+ exact = False
781
+ continue
782
+ cut = _lifecycle_cut(node, inactive_ranges)
783
+ if cut is not _LifecycleCut.NONE:
784
+ diagnostics.append(_lifecycle_cut_reason("tool action", cut))
785
+ exact = False
786
+ continue
787
+ if len(starts) != 1:
788
+ diagnostics.append("tool action projection lacks a unique start event")
789
+ exact = False
790
+ continue
791
+ start = starts[0]
792
+ if (
793
+ start.runtime_id != turn_start.runtime_id
794
+ or start.branch_id != turn_start.branch_id
795
+ or start.coverage_id != turn_start.coverage_id
796
+ or start.turn_id != turn_start.turn_id
797
+ or start.sequence <= turn_start.sequence
798
+ ):
799
+ diagnostics.append("tool action start does not belong to the owning turn scope")
800
+ exact = False
801
+ continue
802
+ # The scope check above bounds the start from below; tool activity
803
+ # past the turn terminal is just as unprovable as activity before the
804
+ # turn opened. The bound is ``turn.finished`` itself, not the tail's
805
+ # response fence — the fence settles after the turn closes and no
806
+ # tool may run in between. Terminals count too: an outcome read from
807
+ # a finish beyond the turn is no better founded than a stray start.
808
+ # An open turn has no terminal yet, and is already inexact through
809
+ # its lifecycle.
810
+ if terminal_sequence is not None and any(event.sequence > terminal_sequence for event in (start, *finishes)):
811
+ diagnostics.append("tool action endpoint lies beyond the turn terminal")
812
+ exact = False
813
+ return exact
814
+
815
+
816
+ def _timeline_operations(
817
+ intermediate: _Intermediate,
818
+ *,
819
+ turn_id: str,
820
+ inactive_ranges: tuple[tuple[int, int], ...],
821
+ resolved_nodes: list[_ResolvedNode],
822
+ cancel_event: Event | None,
823
+ ) -> tuple[TimelineOperation, ...]:
824
+ """Project lifecycle nodes for display without deriving metric values from them."""
825
+ rows = [_resolved_timeline_operation(node) for node in resolved_nodes]
826
+ resolved_ids = {node.node_id for node in resolved_nodes}
827
+ for node in intermediate.nodes_by_turn.get(turn_id, ()):
828
+ _check_cancelled(cancel_event)
829
+ node_id = f"{node.family}:{node.operation_id}"
830
+ if node_id in resolved_ids:
831
+ continue
832
+ starts = [
833
+ event for event in node.starts if event.turn_id == turn_id and _active(event.sequence, inactive_ranges)
834
+ ]
835
+ finishes = [
836
+ event for event in node.finishes if event.turn_id == turn_id and _active(event.sequence, inactive_ranges)
837
+ ]
838
+ endpoints = sorted((*starts, *finishes), key=lambda endpoint: endpoint.sequence)
839
+ if not endpoints:
840
+ continue
841
+ if all(endpoint.side_call for endpoint in endpoints):
842
+ continue
843
+ cut = _lifecycle_cut(node, inactive_ranges)
844
+ identity_endpoint = starts[0] if starts else endpoints[0]
845
+ identity_source = (
846
+ node.starts[0] if cut is _LifecycleCut.FINISH_SURVIVES and len(node.starts) == 1 else identity_endpoint
847
+ )
848
+ if cut is not _LifecycleCut.NONE:
849
+ reason = _lifecycle_cut_reason(node.family, cut)
850
+ diagnostic_code = _lifecycle_cut_diagnostic_code(cut)
851
+ else:
852
+ reason, diagnostic_code = _unresolved_operation_reason(node.family, starts, finishes)
853
+ rows.append(
854
+ _TimelineProjection(
855
+ operation_id=node.operation_id,
856
+ parent_operation_id=_timeline_parent_operation(node.family, identity_endpoint),
857
+ start_sequence=endpoints[0].sequence,
858
+ family=node.family,
859
+ start_ns=None,
860
+ end_ns=None,
861
+ precision=Precision.UNRESOLVED,
862
+ reason=reason,
863
+ diagnostic_code=diagnostic_code,
864
+ identity=_timeline_identity(node.family, identity_source),
865
+ hook_id=_timeline_hook_id(node.family, identity_source),
866
+ )
867
+ )
868
+ return _materialize_timeline(rows)
869
+
870
+
871
+ def _unresolved_operation_reason(
872
+ family: str,
873
+ starts: list[_Endpoint],
874
+ finishes: list[_Endpoint],
875
+ ) -> tuple[str, TimelineDiagnosticCode]:
876
+ if not starts:
877
+ return f"{family} lifecycle has no start endpoint", TimelineDiagnosticCode.MISSING_START
878
+ if not finishes:
879
+ return f"{family} lifecycle has no terminal endpoint", TimelineDiagnosticCode.MISSING_TERMINAL
880
+ if len(starts) != 1 or len(finishes) != 1:
881
+ return f"{family} lifecycle is not uniquely closed", TimelineDiagnosticCode.NONUNIQUE_LIFECYCLE
882
+ if finishes[0].sequence <= starts[0].sequence or finishes[0].monotonic_ns < starts[0].monotonic_ns:
883
+ return f"{family} interval has invalid monotonic endpoints", TimelineDiagnosticCode.INVALID_ENDPOINTS
884
+ return f"{family} lifecycle falls outside the owning turn coverage", TimelineDiagnosticCode.OUTSIDE_TURN_COVERAGE
885
+
886
+
887
+ def _lifecycle_cut_reason(family: str, cut: _LifecycleCut) -> str:
888
+ """Describe which endpoint of a raw lifecycle remains active."""
889
+ surviving = "start" if cut is _LifecycleCut.START_SURVIVES else "terminal"
890
+ return f"{family} lifecycle crosses rollback projection; only its {surviving} endpoint remains active"
891
+
892
+
893
+ def _lifecycle_cut_diagnostic_code(cut: _LifecycleCut) -> TimelineDiagnosticCode:
894
+ return (
895
+ TimelineDiagnosticCode.ROLLBACK_START_SURVIVES
896
+ if cut is _LifecycleCut.START_SURVIVES
897
+ else TimelineDiagnosticCode.ROLLBACK_TERMINAL_SURVIVES
898
+ )
899
+
900
+
901
+ def _continuation_poll_nodes(nodes: list[_ResolvedNode]) -> list[_ResolvedNode]:
902
+ """Derive the uninstrumented fixed continuation-poll pauses."""
903
+ exchanges = [node for node in nodes if node.family == "model.exchange"]
904
+ retries = [node for node in nodes if node.family == "retry"]
905
+ by_cycle: dict[str, list[_ResolvedNode]] = defaultdict(list)
906
+ for exchange in exchanges:
907
+ parent = exchange.start.parent_operation_id
908
+ if parent is not None:
909
+ by_cycle[parent].append(exchange)
910
+ derived: list[_ResolvedNode] = []
911
+ for cycle_operation_id, cycle_exchanges in by_cycle.items():
912
+ ordered = sorted(cycle_exchanges, key=lambda node: (node.start.monotonic_ns, node.start.sequence))
913
+ for previous, current in pairwise(ordered):
914
+ if (
915
+ _payload_str(previous.finish.payload, "outcome") != "success"
916
+ or _payload_str(current.start.payload, "continuation_mode") != "poll"
917
+ or previous.finish.monotonic_ns >= current.start.monotonic_ns
918
+ ):
919
+ continue
920
+ if any(
921
+ retry.start.monotonic_ns < current.start.monotonic_ns
922
+ and retry.finish.monotonic_ns > previous.finish.monotonic_ns
923
+ for retry in retries
924
+ ):
925
+ continue
926
+ operation_id = f"continuation_poll:{previous.operation_id}:{current.operation_id}"
927
+ start = _synthetic_endpoint(
928
+ previous.finish,
929
+ monotonic_ns=previous.finish.monotonic_ns,
930
+ parent_operation_id=cycle_operation_id,
931
+ payload={
932
+ "category": "continuation_poll",
933
+ "previous_exchange_operation_id": previous.operation_id,
934
+ "next_exchange_operation_id": current.operation_id,
935
+ },
936
+ )
937
+ finish = _synthetic_endpoint(
938
+ current.start,
939
+ monotonic_ns=current.start.monotonic_ns,
940
+ parent_operation_id=cycle_operation_id,
941
+ payload={
942
+ "category": "continuation_poll",
943
+ "outcome": "completed",
944
+ "previous_exchange_operation_id": previous.operation_id,
945
+ "next_exchange_operation_id": current.operation_id,
946
+ },
947
+ )
948
+ derived.append(
949
+ _ResolvedNode(
950
+ node_id=f"continuation.poll:{operation_id}",
951
+ family="continuation.poll",
952
+ operation_id=operation_id,
953
+ start=start,
954
+ finish=finish,
955
+ )
956
+ )
957
+ return derived
958
+
959
+
960
+ def _suspension_nodes(
961
+ turn: _Turn,
962
+ nodes: list[_ResolvedNode],
963
+ turn_start: _Endpoint,
964
+ finish: _Endpoint | None,
965
+ inactive_ranges: tuple[tuple[int, int], ...],
966
+ diagnostics: list[str],
967
+ ) -> tuple[list[_ResolvedNode], dict[str, list[tuple[int, int]]], bool]:
968
+ """Pair turn suspension markers and identify the sole deductible boundary."""
969
+ suspended = sorted(
970
+ (event for event in turn.suspended if _active(event.sequence, inactive_ranges)),
971
+ key=lambda event: event.sequence,
972
+ )
973
+ resumed = sorted(
974
+ (event for event in turn.resumed if _active(event.sequence, inactive_ranges)),
975
+ key=lambda event: event.sequence,
976
+ )
977
+ resume_index = 0
978
+ exact = True
979
+ derived: list[_ResolvedNode] = []
980
+ deductions: dict[str, list[tuple[int, int]]] = defaultdict(list)
981
+ boundaries = [node for node in nodes if node.family == "sub_agent"]
982
+ for index, start in enumerate(suspended):
983
+ while resume_index < len(resumed) and resumed[resume_index].sequence <= start.sequence:
984
+ diagnostics.append("turn.resumed has no preceding suspension")
985
+ exact = False
986
+ resume_index += 1
987
+ if resume_index < len(resumed):
988
+ terminal = resumed[resume_index]
989
+ resume_index += 1
990
+ elif finish is not None:
991
+ terminal = finish
992
+ diagnostics.append("turn suspension reaches the turn terminal without resume")
993
+ exact = False
994
+ else:
995
+ diagnostics.append("turn suspension has no observable terminal")
996
+ exact = False
997
+ continue
998
+ if (
999
+ start.runtime_id != turn_start.runtime_id
1000
+ or terminal.runtime_id != turn_start.runtime_id
1001
+ or start.branch_id != turn_start.branch_id
1002
+ or terminal.branch_id != turn_start.branch_id
1003
+ or terminal.sequence <= start.sequence
1004
+ or terminal.monotonic_ns < start.monotonic_ns
1005
+ ):
1006
+ diagnostics.append("turn suspension has invalid monotonic endpoints")
1007
+ exact = False
1008
+ continue
1009
+ operation_id = f"turn_suspension:{turn.turn_id}:{index}"
1010
+ derived.append(
1011
+ _ResolvedNode(
1012
+ node_id=f"turn.suspension:{operation_id}",
1013
+ family="turn.suspension",
1014
+ operation_id=operation_id,
1015
+ start=_synthetic_endpoint(
1016
+ start,
1017
+ monotonic_ns=start.monotonic_ns,
1018
+ parent_operation_id=None,
1019
+ payload={"category": "sub_agent_suspension"},
1020
+ ),
1021
+ finish=_synthetic_endpoint(
1022
+ terminal,
1023
+ monotonic_ns=terminal.monotonic_ns,
1024
+ parent_operation_id=None,
1025
+ payload={"category": "sub_agent_suspension", "outcome": "completed"},
1026
+ ),
1027
+ )
1028
+ )
1029
+ candidates = [
1030
+ boundary
1031
+ for boundary in boundaries
1032
+ if boundary.start.monotonic_ns <= start.monotonic_ns
1033
+ and boundary.finish.monotonic_ns >= terminal.monotonic_ns
1034
+ ]
1035
+ if len(candidates) == 1:
1036
+ deductions[candidates[0].node_id].append((start.monotonic_ns, terminal.monotonic_ns))
1037
+ elif len(candidates) > 1:
1038
+ diagnostics.append("turn suspension overlaps multiple open sub-agent boundaries")
1039
+ exact = False
1040
+ if resume_index < len(resumed):
1041
+ diagnostics.append("turn.resumed has no preceding suspension")
1042
+ exact = False
1043
+ return derived, deductions, exact
1044
+
1045
+
1046
+ def _synthetic_endpoint(
1047
+ source: _Endpoint,
1048
+ *,
1049
+ monotonic_ns: int,
1050
+ parent_operation_id: str | None,
1051
+ payload: dict[str, object],
1052
+ ) -> _Endpoint:
1053
+ return _Endpoint(
1054
+ event_id=source.event_id,
1055
+ sequence=source.sequence,
1056
+ scope=source.scope,
1057
+ monotonic_ns=monotonic_ns,
1058
+ parent_operation_id=parent_operation_id,
1059
+ side_call=source.side_call,
1060
+ payload=tuple(item for pair in payload.items() for item in pair),
1061
+ links=(),
1062
+ segmented_fields=(),
1063
+ monotonic_measurement=True,
1064
+ )
1065
+
1066
+
1067
+ def _usage_samples_by_turn(
1068
+ intermediate: _Intermediate,
1069
+ inactive_ranges: tuple[tuple[int, int], ...],
1070
+ ) -> dict[str, tuple[TokenUsageSample, ...]]:
1071
+ """Project timestamped per-exchange usage counters on demand for export."""
1072
+ finish_ns = {
1073
+ endpoint.sequence: endpoint.monotonic_ns
1074
+ for node in intermediate.nodes.get("model.exchange", {}).values()
1075
+ for endpoint in node.finishes
1076
+ }
1077
+ samples: dict[str, tuple[TokenUsageSample, ...]] = {}
1078
+ for turn_id, items in intermediate.usage_by_turn.items():
1079
+ rows = tuple(
1080
+ TokenUsageSample(
1081
+ sequence=item.sequence,
1082
+ end_ns=finish_ns.get(item.sequence),
1083
+ input_tokens=item.input_total,
1084
+ output_tokens=item.output_total,
1085
+ reasoning_tokens=item.extras.reasoning if item.extras is not None else None,
1086
+ cache_read_tokens=item.extras.cache_read if item.extras is not None else None,
1087
+ cache_creation_tokens=item.extras.cache_creation if item.extras is not None else None,
1088
+ )
1089
+ for item in items
1090
+ if _active(item.sequence, inactive_ranges)
1091
+ and (item.input_total is not None or item.output_total is not None or item.extras is not None)
1092
+ )
1093
+ if rows:
1094
+ samples[turn_id] = rows
1095
+ return samples
1096
+
1097
+
1098
+ def _context_samples_by_turn(
1099
+ intermediate: _Intermediate,
1100
+ inactive_ranges: tuple[tuple[int, int], ...],
1101
+ ) -> dict[str, tuple[ContextSample, ...]]:
1102
+ """Project timestamped context-size counters on demand for export.
1103
+
1104
+ The declared item_count is read straight off each uniquely defined revision
1105
+ endpoint, so no membership replay is needed; declaration inconsistencies
1106
+ already surface through the diagnostics pipeline.
1107
+ """
1108
+ by_turn: dict[str, list[ContextSample]] = defaultdict(list)
1109
+ for candidates in intermediate.context_revisions.values():
1110
+ active = [endpoint for endpoint in candidates if _active(endpoint.sequence, inactive_ranges)]
1111
+ if len(active) != 1:
1112
+ continue
1113
+ endpoint = active[0]
1114
+ item_count = _payload_int(endpoint.payload, "item_count")
1115
+ if endpoint.turn_id is None or item_count is None or item_count < 0 or endpoint.side_call:
1116
+ continue
1117
+ by_turn[endpoint.turn_id].append(
1118
+ ContextSample(
1119
+ sequence=endpoint.sequence,
1120
+ ns=endpoint.monotonic_ns,
1121
+ item_count=item_count,
1122
+ )
1123
+ )
1124
+ return {
1125
+ turn_id: tuple(sorted(rows, key=lambda sample: sample.sequence)) for turn_id, rows in sorted(by_turn.items())
1126
+ }
1127
+
1128
+
1129
+ def _turn_usage(
1130
+ intermediate: _Intermediate,
1131
+ turn_id: str,
1132
+ inactive_ranges: tuple[tuple[int, int], ...],
1133
+ *,
1134
+ integrity_exact: bool,
1135
+ integrity_reason: str,
1136
+ turn_lifecycle_exact: bool,
1137
+ closed_exchange_terminal_sequences: frozenset[int],
1138
+ cancel_event: Event | None,
1139
+ ) -> Metric:
1140
+ unclosed_start = False
1141
+ exchange_projection_exact = True
1142
+ for node in intermediate.nodes_by_turn.get(turn_id, ()):
1143
+ _check_cancelled(cancel_event)
1144
+ if node.family != "model.exchange":
1145
+ continue
1146
+ starts = [
1147
+ event for event in node.starts if event.turn_id == turn_id and _active(event.sequence, inactive_ranges)
1148
+ ]
1149
+ finishes = [
1150
+ event for event in node.finishes if event.turn_id == turn_id and _active(event.sequence, inactive_ranges)
1151
+ ]
1152
+ endpoints = (*starts, *finishes)
1153
+ if not endpoints:
1154
+ continue
1155
+ if _lifecycle_cut(node, inactive_ranges) is not _LifecycleCut.NONE:
1156
+ exchange_projection_exact = False
1157
+ continue
1158
+ if len(starts) == 1 and not finishes:
1159
+ unclosed_start = True
1160
+ continue
1161
+ if len(starts) != 1 or len(finishes) != 1 or finishes[0].sequence not in closed_exchange_terminal_sequences:
1162
+ exchange_projection_exact = False
1163
+ if unclosed_start:
1164
+ return Metric(None, Precision.MISSING, "one or more model exchanges have no terminal usage")
1165
+ exchanges = [
1166
+ item for item in intermediate.usage_by_turn.get(turn_id, ()) if _active(item.sequence, inactive_ranges)
1167
+ ]
1168
+ if any(
1169
+ item.normalization_unavailable or item.input_total is None or item.output_total is None for item in exchanges
1170
+ ):
1171
+ return Metric(None, Precision.MISSING, "one or more normalized usage totals are missing")
1172
+ value = sum(cast("int", item.input_total) + cast("int", item.output_total) for item in exchanges)
1173
+ if not integrity_exact:
1174
+ return Metric(value, Precision.UNRESOLVED, integrity_reason or "turn sequence integrity is unresolved")
1175
+ usage_terminals = [item.sequence for item in exchanges]
1176
+ if (
1177
+ not turn_lifecycle_exact
1178
+ or not exchange_projection_exact
1179
+ or any(sequence not in closed_exchange_terminal_sequences for sequence in usage_terminals)
1180
+ ):
1181
+ return Metric(value, Precision.UNRESOLVED, "usage requires a uniquely closed exchange and exact turn lifecycle")
1182
+ if not all(
1183
+ item.bucket_has_provider_provenance(UsageBucket.INPUT)
1184
+ and item.bucket_has_provider_provenance(UsageBucket.OUTPUT)
1185
+ for item in exchanges
1186
+ ):
1187
+ return Metric(value, Precision.UNRESOLVED, "normalized usage provenance is incomplete")
1188
+ return Metric(value, Precision.EXACT)
1189
+
1190
+
1191
+ def _turn_bucket_usage(exchanges: list[_ExchangeUsage], bucket: UsageBucket, verdict: Metric) -> Metric:
1192
+ """Split one turn's usage verdict into a per-bucket display metric.
1193
+
1194
+ The turn-level verdict caps every bucket: a missing or unresolved turn can
1195
+ never yield an exact bucket. Optional buckets additionally degrade on
1196
+ partial reporting because providers normalize them inconsistently.
1197
+ """
1198
+ if verdict.value is None:
1199
+ return Metric(None, verdict.precision, verdict.reason)
1200
+ reported = [value for item in exchanges if (value := item.bucket_value(bucket)) is not None]
1201
+ if bucket in _OPTIONAL_USAGE_BUCKETS and exchanges and not reported:
1202
+ return Metric(None, Precision.MISSING, "no exchange reported this bucket")
1203
+ value = sum(reported)
1204
+ if verdict.precision is not Precision.EXACT:
1205
+ return Metric(value, Precision.UNRESOLVED, verdict.reason)
1206
+ if bucket is UsageBucket.REASONING and any(
1207
+ item.extras is not None
1208
+ and item.extras.reasoning is not None
1209
+ and item.output_total is not None
1210
+ and item.extras.reasoning > item.output_total
1211
+ for item in exchanges
1212
+ ):
1213
+ return Metric(value, Precision.UNRESOLVED, "reasoning tokens exceed normalized output tokens")
1214
+ if len(reported) < len(exchanges):
1215
+ return Metric(value, Precision.ESTIMATED, "not every exchange reported this bucket")
1216
+ if not all(item.bucket_has_provider_provenance(bucket) for item in exchanges):
1217
+ return Metric(value, Precision.UNRESOLVED, "normalized usage provenance is incomplete")
1218
+ return Metric(value, Precision.EXACT)
1219
+
1220
+
1221
+ def _counter_axis_diagnostics(
1222
+ intermediate: _Intermediate,
1223
+ turn_id: str,
1224
+ inactive_ranges: tuple[tuple[int, int], ...],
1225
+ *,
1226
+ resolution_cache: _ResolutionCache,
1227
+ axis_start_ns: int,
1228
+ axis_end_ns: int,
1229
+ revisions: _RevisionResolution,
1230
+ refresh: bool,
1231
+ cancel_event: Event | None,
1232
+ ) -> tuple[str, ...]:
1233
+ """Validate one dirty physical turn's counter timestamps without a session scan."""
1234
+ if not refresh:
1235
+ return resolution_cache.counter_axis_diagnostics_by_turn.get(turn_id, ())
1236
+ diagnostics: list[str] = []
1237
+ usage_sequences: set[int] = set()
1238
+ for item in intermediate.usage_by_turn.get(turn_id, ()):
1239
+ _check_cancelled(cancel_event)
1240
+ if _active(item.sequence, inactive_ranges) and (
1241
+ item.input_total is not None or item.output_total is not None or item.extras is not None
1242
+ ):
1243
+ usage_sequences.add(item.sequence)
1244
+ usage_outside = False
1245
+ if usage_sequences:
1246
+ for node in intermediate.nodes_by_turn.get(turn_id, ()):
1247
+ _check_cancelled(cancel_event)
1248
+ if node.family != "model.exchange":
1249
+ continue
1250
+ for endpoint in node.finishes:
1251
+ _check_cancelled(cancel_event)
1252
+ if (
1253
+ endpoint.turn_id == turn_id
1254
+ and endpoint.sequence in usage_sequences
1255
+ and _active(endpoint.sequence, inactive_ranges)
1256
+ and not axis_start_ns <= endpoint.monotonic_ns <= axis_end_ns
1257
+ ):
1258
+ usage_outside = True
1259
+ break
1260
+ if usage_outside:
1261
+ break
1262
+ if usage_outside:
1263
+ diagnostics.append("usage sample lies outside its owning attempt axis")
1264
+
1265
+ for revision_id in intermediate.context_revision_ids_by_turn.get(turn_id, ()):
1266
+ _check_cancelled(cancel_event)
1267
+ endpoint = revisions.endpoints.get(revision_id)
1268
+ if endpoint is None:
1269
+ continue
1270
+ item_count = _payload_int(endpoint.payload, "item_count")
1271
+ if (
1272
+ item_count is not None
1273
+ and item_count >= 0
1274
+ and not endpoint.side_call
1275
+ and not axis_start_ns <= endpoint.monotonic_ns <= axis_end_ns
1276
+ ):
1277
+ diagnostics.append("context sample lies outside its owning attempt axis")
1278
+ break
1279
+ result = tuple(diagnostics)
1280
+ resolution_cache.counter_axis_diagnostics_by_turn[turn_id] = result
1281
+ return result
1282
+
1283
+
1284
+ @lru_cache(maxsize=256)
1285
+ def _interned_token_usage(metrics: tuple[Metric, ...]) -> TokenUsage:
1286
+ """Share value-identical per-turn usage; every retained turn carries one.
1287
+
1288
+ Long sessions repeat the same bucket shapes turn after turn, and a private
1289
+ five-metric dict per turn is what the residency ceiling notices first.
1290
+ """
1291
+ return TokenUsage(buckets=dict(zip(UsageBucket, metrics, strict=True)))
1292
+
1293
+
1294
+ def _empty_turn(
1295
+ intermediate: _Intermediate,
1296
+ turn_id: str,
1297
+ starts: list[_Endpoint],
1298
+ reason: str,
1299
+ *,
1300
+ inactive_ranges: tuple[tuple[int, int], ...],
1301
+ resolution_cache: _ResolutionCache,
1302
+ revisions: _RevisionResolution,
1303
+ refresh: bool,
1304
+ cancel_event: Event | None,
1305
+ ) -> TurnAnalysis:
1306
+ """Build an unresolved physical attempt without discarding stable opener identity.
1307
+
1308
+ Duplicate starts may still prove one ``turn_number``/``is_retry`` pair and
1309
+ therefore participate in logical retry folding. With no surviving start
1310
+ (for example, an opener hidden by an unaccounted gap), no such association
1311
+ is provable and the attempt deliberately remains an unnumbered turn.
1312
+ """
1313
+ start = starts[0] if starts else None
1314
+ turn_numbers = {_payload_int(candidate.payload, "turn_number") for candidate in starts}
1315
+ retry_flags = {_payload_bool(candidate.payload, "is_retry") for candidate in starts}
1316
+ runtime_ids = {candidate.runtime_id for candidate in starts}
1317
+ turn_number = next(iter(turn_numbers)) if len(turn_numbers) == 1 else None
1318
+ is_retry = next(iter(retry_flags)) if len(retry_flags) == 1 else False
1319
+ diagnostics = [reason]
1320
+ if starts and (len(turn_numbers) != 1 or len(retry_flags) != 1):
1321
+ diagnostics.append("turn starts disagree on logical retry identity")
1322
+ if len(runtime_ids) > 1:
1323
+ diagnostics.append("turn starts disagree on runtime ownership")
1324
+ unresolved = Metric(None, Precision.UNRESOLVED, reason)
1325
+ runtime_id = start.runtime_id if start is not None else ""
1326
+ sequence = start.sequence if start is not None else 0
1327
+ axis_ns = start.monotonic_ns if start is not None else 0
1328
+ diagnostics.extend(
1329
+ _counter_axis_diagnostics(
1330
+ intermediate,
1331
+ turn_id,
1332
+ inactive_ranges,
1333
+ resolution_cache=resolution_cache,
1334
+ axis_start_ns=axis_ns,
1335
+ axis_end_ns=axis_ns,
1336
+ revisions=revisions,
1337
+ refresh=refresh,
1338
+ cancel_event=cancel_event,
1339
+ )
1340
+ )
1341
+ wall = dict.fromkeys(WallBucket, unresolved)
1342
+ utilization = dict.fromkeys((WallBucket.MODEL, WallBucket.TOOLS), unresolved)
1343
+ return TurnAnalysis(
1344
+ turn_id=turn_id,
1345
+ turn_number=turn_number,
1346
+ runtime_id=runtime_id,
1347
+ start_sequence=sequence,
1348
+ end_sequence=None,
1349
+ elapsed_ns=unresolved,
1350
+ compute_cp_ns=unresolved,
1351
+ response_cp_ns=unresolved,
1352
+ exclusive_work_ns=unresolved,
1353
+ parallelism=unresolved,
1354
+ overlap_gain_ns=unresolved,
1355
+ wall_time_ns=wall,
1356
+ utilization=utilization,
1357
+ usage_tokens=Metric(None, Precision.MISSING, reason),
1358
+ attempts=(
1359
+ TurnAttemptRef(
1360
+ turn_id=turn_id,
1361
+ runtime_id=runtime_id,
1362
+ is_retry=is_retry,
1363
+ physical_axis_start_ns=axis_ns,
1364
+ physical_axis_end_ns=axis_ns,
1365
+ logical_axis_start_ns=axis_ns,
1366
+ operation_start_index=0,
1367
+ operation_end_index=0,
1368
+ slice_start_index=0,
1369
+ slice_end_index=0,
1370
+ ),
1371
+ ),
1372
+ axis_start_ns=axis_ns,
1373
+ axis_end_ns=axis_ns,
1374
+ diagnostics=tuple(diagnostics),
1375
+ action_projection_precision=Precision.UNRESOLVED,
1376
+ action_projection_reason=reason,
1377
+ )