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,1237 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Causal dependency graph and time attribution for one physical turn attempt.
4
+
5
+ Hook ownership, typed dependency edges, time slices, the wall-time partition,
6
+ critical paths and tool/MCP critical-path contributions all rely on one
7
+ definition of which operation displaces, waits on, or merely overlaps
8
+ another, so they live together. ``_critical_path`` holds the interval-path
9
+ search itself.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from array import array
15
+ from collections import Counter, defaultdict
16
+ from collections.abc import Callable
17
+ from dataclasses import dataclass
18
+ from itertools import pairwise
19
+ from threading import Event
20
+ from typing import Final
21
+
22
+ from chrys.foundation.trajectory.envelope import LinkRelation
23
+ from chrys.foundation.trajectory.event_types import RetryMode, ToolOutcome, WaitCategory
24
+ from chrys.service.analytics._context_evidence import _CompactionConsumptionResolution, _RevisionResolution
25
+ from chrys.service.analytics._critical_path import _longest_interval_path, _PathResolution
26
+ from chrys.service.analytics._facts import _HOOK_EXECUTION_MODES, _Endpoint, _Intermediate, _payload_bool, _payload_str
27
+ from chrys.service.analytics._timeline import _hook_drain_scope, _ResolvedNode, _tool_context_for_start
28
+ from chrys.service.analytics.classification import tool_failed
29
+ from chrys.service.analytics.math import clip_interval, interval_union, subtract_intervals
30
+ from chrys.service.analytics.model import (
31
+ FLOW_TERMINAL_INDEX,
32
+ HookOwnership,
33
+ TimelineOperation,
34
+ TimeSlice,
35
+ TurnFlow,
36
+ WallBucket,
37
+ )
38
+ from chrys.service.analytics.reader import raise_if_cancelled as _check_cancelled
39
+ from chrys.service.hooks.events import HookEvent
40
+
41
+ _WALL_PRIORITY: Final = {
42
+ WallBucket.IDLE: 0,
43
+ WallBucket.WAIT: 1,
44
+ WallBucket.TOOLS: 2,
45
+ WallBucket.MODEL: 3,
46
+ }
47
+ # Calls closed before dispatch have no tool preamble and no caused-by edge.
48
+ _NEVER_DISPATCHED_TOOL_OUTCOMES: Final = frozenset(
49
+ {
50
+ ToolOutcome.INVALID_ARGUMENTS,
51
+ ToolOutcome.UNKNOWN_TOOL,
52
+ ToolOutcome.FILTERED,
53
+ }
54
+ )
55
+ _MODEL_OPERATION_FAMILIES: Final = frozenset({"model.run", "model.cycle", "model.exchange"})
56
+ _RETRY_EXCHANGE_MODES: Final = frozenset(
57
+ {RetryMode.WIRE, RetryMode.STALL_FALLBACK, RetryMode.VALIDATION, RetryMode.CONTEXT_OVERFLOW}
58
+ )
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class _DependencyProof:
63
+ edges: dict[str, set[str]]
64
+ turn_root_id: str | None
65
+ response_terminal_id: str | None
66
+ # Displacing child -> parent pairs; an edge absent here is a causal pointer.
67
+ parents: dict[str, str]
68
+ # Edges that fork a concurrent hook off its target; the hook only depends
69
+ # on the target's triggering event, never on the target completing.
70
+ fork_edges: frozenset[tuple[str, str]] = frozenset()
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class _TurnProjection:
75
+ slices: tuple[TimeSlice, ...]
76
+ dependency: _DependencyProof
77
+ timeline_exact: bool
78
+ dag_exact: bool
79
+ response_dependency_exact: bool
80
+
81
+
82
+ @dataclass(frozen=True, slots=True)
83
+ class _CriticalPaths:
84
+ compute_ns: int | None
85
+ response_ns: int | None
86
+ acyclic: bool
87
+ response_reachable: bool
88
+ compute_bounded: bool
89
+ response_bounded: bool
90
+
91
+
92
+ def _project_turn(
93
+ nodes: list[_ResolvedNode],
94
+ turn_bounds: tuple[int, int],
95
+ *,
96
+ start: _Endpoint,
97
+ finish: _Endpoint | None,
98
+ tail_end: int | None,
99
+ waited_hook_ids: frozenset[str] | None,
100
+ suspension_deductions: dict[str, list[tuple[int, int]]],
101
+ revisions: _RevisionResolution,
102
+ compaction_consumption: _CompactionConsumptionResolution,
103
+ mapped_carrier: Callable[[str], str | None],
104
+ cancel_event: Event | None,
105
+ diagnostics: list[str],
106
+ ) -> _TurnProjection:
107
+ by_operation: dict[str, list[_ResolvedNode]] = defaultdict(list)
108
+ for node in nodes:
109
+ by_operation[node.operation_id].append(node)
110
+ preparation_for_tool = {
111
+ target: node
112
+ for node in nodes
113
+ if node.family == "preparation"
114
+ and _payload_str(node.start.payload, "scope") == "tool_preamble"
115
+ and (target := _payload_str(node.start.payload, "target_operation_id")) is not None
116
+ }
117
+ parents: dict[str, str] = {}
118
+ graph_edges: dict[str, set[str]] = defaultdict(set)
119
+ unsafe_work_nodes: set[str] = set()
120
+ timeline_exact = True
121
+ dag_exact = True
122
+ for node in nodes:
123
+ _check_cancelled(cancel_event)
124
+ parent = _displacing_parent(node, by_operation, preparation_for_tool, diagnostics)
125
+ if parent is False:
126
+ timeline_exact = False
127
+ dag_exact = False
128
+ if node.family == "hook.operation":
129
+ unsafe_work_nodes.add(node.node_id)
130
+ elif isinstance(parent, str):
131
+ parents[node.node_id] = parent
132
+ graph_edges[parent].add(node.node_id)
133
+ for relation, target_operation_id in node.start.links:
134
+ if relation != LinkRelation.CAUSED_BY:
135
+ continue
136
+ # A retry lifecycle deliberately shares the next operation's id.
137
+ # caused_by names the work operation, never that retry marker.
138
+ targets = [
139
+ target
140
+ for target in by_operation.get(target_operation_id, [])
141
+ if target.family != "retry" and target.node_id != node.node_id
142
+ ]
143
+ if len(targets) == 1:
144
+ graph_edges[targets[0].node_id].add(node.node_id)
145
+ else:
146
+ diagnostics.append("caused_by target cannot be resolved uniquely")
147
+ dag_exact = False
148
+ response_dependency_exact = _validate_async_hook_fences(nodes, waited_hook_ids, diagnostics)
149
+ children: dict[str, set[str]] = defaultdict(set)
150
+ for child, parent in parents.items():
151
+ children[parent].add(child)
152
+ node_by_id = {node.node_id: node for node in nodes}
153
+ if _has_cycle(children):
154
+ diagnostics.append("displacing-edge graph contains a cycle")
155
+ return _TurnProjection(
156
+ slices=(),
157
+ dependency=_DependencyProof(graph_edges, None, None, parents),
158
+ timeline_exact=False,
159
+ dag_exact=False,
160
+ response_dependency_exact=response_dependency_exact,
161
+ )
162
+ slices: list[TimeSlice] = []
163
+ for node in nodes:
164
+ _check_cancelled(cancel_event)
165
+ if _skip_node(node):
166
+ continue
167
+ descendants = _descendants(node.node_id, children)
168
+ removed = [node_by_id[descendant].interval for descendant in descendants]
169
+ removed.extend(suspension_deductions.get(node.node_id, ()))
170
+ residuals = subtract_intervals(node.interval, removed)
171
+ attributes = _slice_attributes(node)
172
+ if attributes is None:
173
+ continue
174
+ bucket, counts_as_work, compute_weight, response_weight, owner = attributes
175
+ if node.family == "hook.operation":
176
+ mode = _payload_str(node.start.payload, "execution_mode")
177
+ hook_event = _payload_str(node.start.payload, "hook_event")
178
+ if hook_event == "user_interrupt" or mode == "fire_and_forget":
179
+ response_weight = False
180
+ elif mode == "async":
181
+ response_weight = waited_hook_ids is not None and node.operation_id in waited_hook_ids
182
+ if node.node_id in unsafe_work_nodes:
183
+ counts_as_work = False
184
+ compute_weight = False
185
+ response_weight = False
186
+ for index, residual in enumerate(residuals):
187
+ clipped = clip_interval(residual, turn_bounds)
188
+ if clipped is None:
189
+ continue
190
+ slices.append(
191
+ TimeSlice(
192
+ family=node.family,
193
+ slice_index=index,
194
+ turn_id=start.turn_id or "",
195
+ runtime_id=node.start.runtime_id,
196
+ operation_id=node.operation_id,
197
+ owner=owner,
198
+ start_ns=clipped[0],
199
+ end_ns=clipped[1],
200
+ wall_bucket=bucket,
201
+ counts_as_work=counts_as_work,
202
+ compute_weight=compute_weight,
203
+ response_weight=response_weight,
204
+ outcome=_payload_str(node.finish.payload, "outcome"),
205
+ tool_name=_payload_str(node.start.payload, "tool_name"),
206
+ tool_kind=_payload_str(node.start.payload, "tool_kind"),
207
+ )
208
+ )
209
+ typed_exact, response_source_id = _connect_typed_dependencies(
210
+ nodes,
211
+ graph_edges,
212
+ main_actor_id=start.actor_id,
213
+ revisions=revisions,
214
+ compaction_consumption=compaction_consumption,
215
+ mapped_carrier=mapped_carrier,
216
+ cancel_event=cancel_event,
217
+ diagnostics=diagnostics,
218
+ )
219
+ dag_exact = dag_exact and typed_exact
220
+ roots = [
221
+ node.node_id
222
+ for node in nodes
223
+ if node.family == "preparation" and _payload_str(node.start.payload, "scope") == "turn_preamble"
224
+ ]
225
+ turn_root_id = roots[0] if len(roots) == 1 else None
226
+ response_terminal_id = (
227
+ f"turn.response:{start.turn_id}"
228
+ if finish is not None and tail_end is not None and response_source_id is not None
229
+ else None
230
+ )
231
+ if response_terminal_id is not None and response_source_id is not None:
232
+ graph_edges[response_source_id].add(response_terminal_id)
233
+ fork_edges: set[tuple[str, str]] = set()
234
+ hook_dag_exact, hook_response_exact = _connect_hook_dependencies(
235
+ nodes,
236
+ graph_edges,
237
+ waited_hook_ids=waited_hook_ids,
238
+ turn_root_id=turn_root_id,
239
+ response_source_id=response_source_id,
240
+ response_terminal_id=response_terminal_id,
241
+ fork_edges=fork_edges,
242
+ diagnostics=diagnostics,
243
+ )
244
+ dag_exact = dag_exact and hook_dag_exact
245
+ response_dependency_exact = response_dependency_exact and hook_response_exact
246
+ if response_source_id is not None and response_terminal_id is not None:
247
+ response_descendants = _descendants(response_source_id, children)
248
+ for node_id in response_descendants:
249
+ if not children.get(node_id, set()) & response_descendants:
250
+ graph_edges[node_id].add(response_terminal_id)
251
+ return _TurnProjection(
252
+ slices=tuple(slices),
253
+ dependency=_DependencyProof(graph_edges, turn_root_id, response_terminal_id, parents, frozenset(fork_edges)),
254
+ timeline_exact=timeline_exact,
255
+ dag_exact=dag_exact,
256
+ response_dependency_exact=response_dependency_exact,
257
+ )
258
+
259
+
260
+ def _turn_flow(
261
+ turn_id: str,
262
+ dependency: _DependencyProof,
263
+ operations: tuple[TimelineOperation, ...],
264
+ *,
265
+ acyclic: bool,
266
+ ) -> TurnFlow:
267
+ """Index-encode the typed per-turn dependency graph against the operations tuple."""
268
+ index_by_node: dict[str, int] = {}
269
+ for index, operation in enumerate(operations):
270
+ index_by_node.setdefault(f"{operation.family}:{operation.operation_id}", index)
271
+ terminal_id = dependency.response_terminal_id
272
+ parent_pairs = array("I")
273
+ causal_pairs = array("I")
274
+ for source, targets in sorted(dependency.edges.items()):
275
+ source_index = index_by_node.get(source)
276
+ if source_index is None:
277
+ continue
278
+ for target in sorted(targets):
279
+ if target == terminal_id:
280
+ target_index = FLOW_TERMINAL_INDEX
281
+ else:
282
+ found = index_by_node.get(target)
283
+ if found is None:
284
+ continue
285
+ target_index = found
286
+ pairs = parent_pairs if dependency.parents.get(target) == source else causal_pairs
287
+ pairs.append(source_index)
288
+ pairs.append(target_index)
289
+ root_id = dependency.turn_root_id
290
+ return TurnFlow(
291
+ turn_id=turn_id,
292
+ root_index=index_by_node.get(root_id) if root_id is not None else None,
293
+ has_terminal=terminal_id is not None,
294
+ parent_pairs=parent_pairs.tobytes(),
295
+ causal_pairs=causal_pairs.tobytes(),
296
+ acyclic=acyclic,
297
+ )
298
+
299
+
300
+ def _displacing_parent(
301
+ node: _ResolvedNode,
302
+ by_operation: dict[str, list[_ResolvedNode]],
303
+ preparation_for_tool: dict[str, _ResolvedNode],
304
+ diagnostics: list[str],
305
+ ) -> str | bool | None:
306
+ if node.family == "preparation":
307
+ scope = _payload_str(node.start.payload, "scope")
308
+ if scope == "turn_preamble":
309
+ return None
310
+ if scope == "pre_turn":
311
+ return None
312
+ if node.family == "hook.operation":
313
+ hook_event = _payload_str(node.start.payload, "hook_event")
314
+ mode = _payload_str(node.start.payload, "execution_mode")
315
+ ownership = classify_hook_ownership(hook_event, mode)
316
+ if ownership is HookOwnership.CONCURRENT:
317
+ return None
318
+ target = _payload_str(node.start.payload, "target_operation_id")
319
+ if ownership is HookOwnership.TOOL_PREAMBLE and target is not None:
320
+ preamble = preparation_for_tool.get(target)
321
+ if preamble is not None:
322
+ return preamble.node_id
323
+ elif ownership in {
324
+ HookOwnership.TOOL_TAIL,
325
+ HookOwnership.APPROVAL,
326
+ HookOwnership.TURN_PREAMBLE,
327
+ HookOwnership.SUB_AGENT,
328
+ HookOwnership.COMPACTION,
329
+ }:
330
+ families = _hook_target_families(hook_event)
331
+ resolved = [candidate for candidate in by_operation.get(target or "", []) if candidate.family in families]
332
+ if len(resolved) == 1:
333
+ return resolved[0].node_id
334
+ elif ownership in {HookOwnership.TURN_TAIL, HookOwnership.SESSION_ROOT, HookOwnership.WORKFLOW_ROOT}:
335
+ return None
336
+ diagnostics.append(f"blocking hook {hook_event or 'unknown'} has no placeable displacing edge")
337
+ return False
338
+ if node.family == "tool.operation":
339
+ parent = _payload_str(node.start.payload, "parent_model_operation_id")
340
+ if parent is None:
341
+ diagnostics.append("tool operation lacks parent_model_operation_id")
342
+ return False
343
+ candidates = [
344
+ candidate for candidate in by_operation.get(parent, []) if candidate.family in _MODEL_OPERATION_FAMILIES
345
+ ]
346
+ if len(candidates) == 1:
347
+ return candidates[0].node_id
348
+ diagnostics.append("tool parent model operation cannot be resolved uniquely")
349
+ return False
350
+ if node.family == "turn.suspension":
351
+ return None
352
+ target = None
353
+ families: frozenset[str] | None = None
354
+ if node.family == "approval":
355
+ generic_target = _payload_str(node.start.payload, "target_operation_id")
356
+ target = generic_target or _payload_str(node.start.payload, "target_tool_operation_id")
357
+ if generic_target is None:
358
+ families = frozenset({"tool.operation"})
359
+ elif node.family in {"preparation", "compaction"}:
360
+ target = node.start.parent_operation_id
361
+ families = _MODEL_OPERATION_FAMILIES
362
+ elif node.family == "compaction.phase":
363
+ target = node.start.parent_operation_id
364
+ families = frozenset({"compaction"})
365
+ elif node.family == "model.cycle":
366
+ target = node.start.parent_operation_id
367
+ families = frozenset({"model.run", "sub_agent"})
368
+ elif node.family == "model.exchange":
369
+ target = node.start.parent_operation_id
370
+ families = frozenset({"model.run", "model.cycle"})
371
+ elif node.family == "sub_agent":
372
+ target = node.start.parent_operation_id
373
+ families = frozenset({"tool.operation", *_MODEL_OPERATION_FAMILIES})
374
+ elif node.family == "wait":
375
+ target = node.start.parent_operation_id
376
+ category = _payload_str(node.start.payload, "category")
377
+ families = _wait_parent_families(category)
378
+ elif node.family == "retry":
379
+ target = node.start.parent_operation_id
380
+ families = _retry_parent_families(_payload_str(node.start.payload, "retry_mode"))
381
+ else:
382
+ target = node.start.parent_operation_id
383
+ if target is None:
384
+ return None
385
+ candidates = [
386
+ candidate
387
+ for candidate in by_operation.get(target, [])
388
+ if candidate.node_id != node.node_id
389
+ and candidate.family != "retry"
390
+ and (families is None or candidate.family in families)
391
+ ]
392
+ if len(candidates) == 1:
393
+ return candidates[0].node_id
394
+ if candidates:
395
+ diagnostics.append(f"{node.family} parent operation is ambiguous")
396
+ return False
397
+ diagnostics.append(f"{node.family} parent operation is missing")
398
+ return False
399
+
400
+
401
+ def _wait_parent_families(category: str | None) -> frozenset[str] | None:
402
+ if category == WaitCategory.USER_INPUT:
403
+ return frozenset({"tool.operation", "sub_agent"})
404
+ if category == WaitCategory.MCP_CONNECT:
405
+ return frozenset({"tool.operation"})
406
+ if category in {WaitCategory.INPUT_ADMISSION, WaitCategory.TOOL_ADMISSION}:
407
+ return frozenset({"preparation"})
408
+ if category == WaitCategory.RATE_LIMIT:
409
+ return _MODEL_OPERATION_FAMILIES
410
+ if category == WaitCategory.SUB_AGENT_CONCURRENCY:
411
+ return frozenset({"sub_agent"})
412
+ return None
413
+
414
+
415
+ def _retry_parent_families(retry_mode: str | None) -> frozenset[str] | None:
416
+ if retry_mode in _RETRY_EXCHANGE_MODES:
417
+ return frozenset({"model.run", "model.cycle"})
418
+ if retry_mode == RetryMode.RUN:
419
+ return frozenset({"sub_agent"})
420
+ if retry_mode == RetryMode.COMPACTION:
421
+ return frozenset({"compaction"})
422
+ if retry_mode == RetryMode.CONNECTION:
423
+ return frozenset({"sub_agent"})
424
+ return None
425
+
426
+
427
+ def _hook_target_families(hook_event: str | None) -> frozenset[str]:
428
+ if hook_event in {HookEvent.APPROVAL_REQUESTED, HookEvent.APPROVAL_RESOLVED}:
429
+ return frozenset({"approval"})
430
+ if hook_event in {HookEvent.BEFORE_TOOL_CALL, HookEvent.AFTER_TOOL_CALL, HookEvent.TOOL_ERROR}:
431
+ return frozenset({"tool.operation"})
432
+ if hook_event == HookEvent.BEFORE_TURN:
433
+ return frozenset({"preparation"})
434
+ if hook_event in {HookEvent.SUB_AGENT_START, HookEvent.SUB_AGENT_END}:
435
+ return frozenset({"sub_agent"})
436
+ if hook_event == HookEvent.PRE_COMPACT:
437
+ return frozenset({"compaction"})
438
+ return frozenset()
439
+
440
+
441
+ def classify_hook_ownership(hook_event: str | None, execution_mode: str | None) -> HookOwnership:
442
+ """Apply the ordered, exhaustive hook ownership matrix without timing guesses."""
443
+ if execution_mode not in _HOOK_EXECUTION_MODES:
444
+ return HookOwnership.UNSAFE
445
+ if hook_event == HookEvent.USER_INTERRUPT or execution_mode != "blocking":
446
+ return HookOwnership.CONCURRENT
447
+ if hook_event == HookEvent.BEFORE_TOOL_CALL:
448
+ return HookOwnership.TOOL_PREAMBLE
449
+ if hook_event in {HookEvent.AFTER_TOOL_CALL, HookEvent.TOOL_ERROR}:
450
+ return HookOwnership.TOOL_TAIL
451
+ if hook_event in {HookEvent.APPROVAL_REQUESTED, HookEvent.APPROVAL_RESOLVED}:
452
+ return HookOwnership.APPROVAL
453
+ if hook_event == HookEvent.BEFORE_TURN:
454
+ return HookOwnership.TURN_PREAMBLE
455
+ if hook_event == HookEvent.AFTER_TURN:
456
+ return HookOwnership.TURN_TAIL
457
+ if hook_event == HookEvent.USER_PROMPT_SUBMIT:
458
+ return HookOwnership.PRE_TURN
459
+ if hook_event in {HookEvent.SUB_AGENT_START, HookEvent.SUB_AGENT_END}:
460
+ return HookOwnership.SUB_AGENT
461
+ if hook_event == HookEvent.PRE_COMPACT:
462
+ return HookOwnership.COMPACTION
463
+ if hook_event in {HookEvent.WORKFLOW_RUN_START, HookEvent.WORKFLOW_RUN_END}:
464
+ return HookOwnership.WORKFLOW_ROOT
465
+ if hook_event in {HookEvent.SESSION_START, HookEvent.SESSION_RESTORED, HookEvent.SESSION_END}:
466
+ return HookOwnership.SESSION_ROOT
467
+ return HookOwnership.UNSAFE
468
+
469
+
470
+ def _slice_attributes(
471
+ node: _ResolvedNode,
472
+ ) -> tuple[WallBucket, bool, bool, bool, str] | None:
473
+ family = node.family
474
+ if family in {"model.run", "model.cycle", "model.exchange"}:
475
+ return WallBucket.MODEL, True, True, True, "model"
476
+ if family in {"compaction", "compaction.phase"}:
477
+ return WallBucket.MODEL, True, True, True, "compaction"
478
+ if family == "tool.operation":
479
+ if _payload_str(node.start.payload, "tool_kind") == "sleep":
480
+ return WallBucket.WAIT, False, False, True, "tool sleep"
481
+ return WallBucket.TOOLS, True, True, True, "tool"
482
+ if family == "sub_agent":
483
+ return WallBucket.TOOLS, True, True, True, "sub-agent"
484
+ if family == "continuation.poll":
485
+ return WallBucket.WAIT, False, False, True, "continuation poll"
486
+ if family == "turn.suspension":
487
+ return WallBucket.WAIT, False, False, True, "sub-agent suspension"
488
+ if family in {"wait", "approval", "retry"}:
489
+ if family == "wait" and _payload_str(node.start.payload, "category") == "input_admission":
490
+ return None
491
+ owner = (
492
+ "approval" if family == "approval" or _payload_str(node.start.payload, "category") == "approval" else family
493
+ )
494
+ return WallBucket.WAIT, False, False, True, owner
495
+ if family == "preparation":
496
+ scope = _payload_str(node.start.payload, "scope")
497
+ if scope == "pre_turn":
498
+ return None
499
+ return WallBucket.TOOLS, True, True, True, "preparation"
500
+ if family == "hook.operation":
501
+ outcome = _payload_str(node.finish.payload, "outcome")
502
+ if outcome == "detached":
503
+ return None
504
+ mode = _payload_str(node.start.payload, "execution_mode")
505
+ hook_event = _payload_str(node.start.payload, "hook_event")
506
+ if mode == "fire_and_forget" or hook_event == "user_interrupt":
507
+ return WallBucket.TOOLS, True, True, False, "hook"
508
+ return WallBucket.TOOLS, True, True, True, "hook"
509
+ return None
510
+
511
+
512
+ def _skip_node(node: _ResolvedNode) -> bool:
513
+ return node.family == "tool.operation" and _payload_bool(node.finish.payload, "abandoned")
514
+
515
+
516
+ def _never_dispatched(node: _ResolvedNode) -> bool:
517
+ return (
518
+ node.family == "tool.operation"
519
+ and _payload_str(node.finish.payload, "outcome") in _NEVER_DISPATCHED_TOOL_OUTCOMES
520
+ )
521
+
522
+
523
+ def _required_causes_present(nodes: list[_ResolvedNode], diagnostics: list[str]) -> bool:
524
+ preambles = [
525
+ node
526
+ for node in nodes
527
+ if node.family == "preparation" and _payload_str(node.start.payload, "scope") == "turn_preamble"
528
+ ]
529
+ runs = sorted((node for node in nodes if node.family == "model.run"), key=lambda node: node.start.sequence)
530
+ exact = True
531
+ if len(preambles) != 1 or not runs:
532
+ diagnostics.append("turn_preamble or first model.run is missing")
533
+ exact = False
534
+ elif (LinkRelation.CAUSED_BY, preambles[0].operation_id) not in runs[0].start.links:
535
+ diagnostics.append("first model.run lacks caused_by link to turn_preamble")
536
+ exact = False
537
+ tool_preambles = {
538
+ target: node
539
+ for node in nodes
540
+ if node.family == "preparation"
541
+ and _payload_str(node.start.payload, "scope") == "tool_preamble"
542
+ and (target := _payload_str(node.start.payload, "target_operation_id")) is not None
543
+ }
544
+ tools = [node for node in nodes if node.family == "tool.operation" and not _never_dispatched(node)]
545
+ if len(tool_preambles) != len(tools):
546
+ diagnostics.append("tool_preamble to tool pairing is not one-to-one")
547
+ exact = False
548
+ for tool in tools:
549
+ preamble = tool_preambles.get(tool.operation_id)
550
+ if preamble is None or (LinkRelation.CAUSED_BY, preamble.operation_id) not in tool.start.links:
551
+ diagnostics.append("tool operation lacks caused_by link to its tool_preamble")
552
+ exact = False
553
+ return exact
554
+
555
+
556
+ def _validate_async_hook_fences(
557
+ nodes: list[_ResolvedNode],
558
+ waited_hook_ids: frozenset[str] | None,
559
+ diagnostics: list[str],
560
+ ) -> bool:
561
+ async_turn_hooks = {
562
+ node.operation_id
563
+ for node in nodes
564
+ if node.family == "hook.operation"
565
+ and _payload_str(node.start.payload, "execution_mode") == "async"
566
+ and _hook_drain_scope(node.start) == "turn"
567
+ }
568
+ if not async_turn_hooks:
569
+ return True
570
+ if waited_hook_ids is None:
571
+ diagnostics.append("async hook response dependencies lack a complete turn fence")
572
+ return False
573
+ known_hooks = {node.operation_id: node for node in nodes if node.family == "hook.operation"}
574
+ invalid_members = [
575
+ operation_id
576
+ for operation_id in waited_hook_ids
577
+ if operation_id not in known_hooks
578
+ or _payload_str(known_hooks[operation_id].start.payload, "execution_mode") != "async"
579
+ or _hook_drain_scope(known_hooks[operation_id].start) != "turn"
580
+ ]
581
+ if invalid_members:
582
+ diagnostics.append("turn fence contains a hook that is not an async turn-scope dependency")
583
+ return False
584
+ return True
585
+
586
+
587
+ def _connect_typed_dependencies(
588
+ nodes: list[_ResolvedNode],
589
+ edges: dict[str, set[str]],
590
+ *,
591
+ main_actor_id: str | None,
592
+ revisions: _RevisionResolution,
593
+ compaction_consumption: _CompactionConsumptionResolution,
594
+ mapped_carrier: Callable[[str], str | None],
595
+ cancel_event: Event | None,
596
+ diagnostics: list[str],
597
+ ) -> tuple[bool, str | None]:
598
+ """Connect only producer-declared causal pointers; never infer adjacency."""
599
+ by_operation: dict[str, list[_ResolvedNode]] = defaultdict(list)
600
+ for node in nodes:
601
+ by_operation[node.operation_id].append(node)
602
+ exact = compaction_consumption.exact
603
+
604
+ compaction_bridges: dict[str, str] = {}
605
+ for run_id, consumption in compaction_consumption.by_run.items():
606
+ _check_cancelled(cancel_event)
607
+ compactions = [candidate for candidate in by_operation.get(run_id, ()) if candidate.family == "compaction"]
608
+ if len(compactions) != 1:
609
+ diagnostics.append("Phase-4 consumed items cannot resolve their compaction run uniquely")
610
+ exact = False
611
+ continue
612
+ compaction = compactions[0]
613
+ candidates = [
614
+ candidate
615
+ for candidate in by_operation.get(consumption.next_exchange_operation_id, ())
616
+ if candidate.family == "model.exchange"
617
+ and candidate.start.scope == compaction.start.scope
618
+ and candidate.start.sequence > compaction.finish.sequence
619
+ and candidate.start.monotonic_ns >= compaction.finish.monotonic_ns
620
+ ]
621
+ if len(candidates) != 1:
622
+ diagnostics.append("Phase-4 compaction successor exchange cannot be resolved uniquely")
623
+ exact = False
624
+ continue
625
+ edges[compaction.node_id].add(candidates[0].node_id)
626
+ compaction_bridges[compaction.operation_id] = compaction.node_id
627
+
628
+ for run in (node for node in nodes if node.family == "model.run"):
629
+ _check_cancelled(cancel_event)
630
+ previous = _payload_str(run.start.payload, "previous_run_operation_id")
631
+ if previous is not None:
632
+ exact = (
633
+ _connect_unique(
634
+ by_operation,
635
+ previous,
636
+ run.node_id,
637
+ edges,
638
+ diagnostics,
639
+ "previous model run",
640
+ families=frozenset({"model.run"}),
641
+ )
642
+ and exact
643
+ )
644
+
645
+ for retry in (node for node in nodes if node.family == "retry"):
646
+ retry_mode = _payload_str(retry.start.payload, "retry_mode")
647
+ neighbor_families = _retry_neighbor_families(retry_mode)
648
+ previous = _payload_str(retry.start.payload, "previous_operation_id")
649
+ following = _payload_str(retry.start.payload, "next_operation_id") or _payload_str(
650
+ retry.finish.payload, "next_operation_id"
651
+ )
652
+ if previous is not None:
653
+ exact = (
654
+ _connect_unique(
655
+ by_operation,
656
+ previous,
657
+ retry.node_id,
658
+ edges,
659
+ diagnostics,
660
+ "retry predecessor",
661
+ families=neighbor_families,
662
+ )
663
+ and exact
664
+ )
665
+ if following is not None:
666
+ exact = (
667
+ _connect_unique(
668
+ by_operation,
669
+ following,
670
+ None,
671
+ edges,
672
+ diagnostics,
673
+ "retry successor",
674
+ source=retry.node_id,
675
+ families=neighbor_families,
676
+ )
677
+ and exact
678
+ )
679
+
680
+ for poll in (node for node in nodes if node.family == "continuation.poll"):
681
+ _check_cancelled(cancel_event)
682
+ previous = _payload_str(poll.start.payload, "previous_exchange_operation_id")
683
+ following = _payload_str(poll.start.payload, "next_exchange_operation_id")
684
+ if previous is None or following is None:
685
+ diagnostics.append("continuation poll lacks typed neighboring exchanges")
686
+ exact = False
687
+ continue
688
+ exact = (
689
+ _connect_unique(
690
+ by_operation,
691
+ previous,
692
+ poll.node_id,
693
+ edges,
694
+ diagnostics,
695
+ "continuation poll predecessor",
696
+ families=frozenset({"model.exchange"}),
697
+ )
698
+ and exact
699
+ )
700
+ exact = (
701
+ _connect_unique(
702
+ by_operation,
703
+ following,
704
+ None,
705
+ edges,
706
+ diagnostics,
707
+ "continuation poll successor",
708
+ source=poll.node_id,
709
+ families=frozenset({"model.exchange"}),
710
+ )
711
+ and exact
712
+ )
713
+
714
+ response_source_id: str | None = None
715
+ response_source_operation_id: str | None = None
716
+ # Sub-agent cycles are inlined in the turn; only the turn's own actor
717
+ # answers the user, so the response source is its last final exchange.
718
+ cycles_with_final = [
719
+ (node, final_exchange)
720
+ for node in nodes
721
+ if node.family == "model.cycle"
722
+ and node.start.actor_id == main_actor_id
723
+ and (final_exchange := _payload_str(node.finish.payload, "final_exchange_operation_id")) is not None
724
+ ]
725
+ if not cycles_with_final:
726
+ diagnostics.append("response linkage lacks final_exchange_operation_id")
727
+ else:
728
+ _, final_exchange_id = max(cycles_with_final, key=lambda item: item[0].finish.sequence)
729
+ candidates = [node for node in by_operation.get(final_exchange_id, ()) if node.family == "model.exchange"]
730
+ if len(candidates) != 1:
731
+ diagnostics.append("final exchange operation cannot be resolved uniquely")
732
+ exact = False
733
+ else:
734
+ response_source_id = candidates[0].node_id
735
+ response_source_operation_id = candidates[0].operation_id
736
+
737
+ exchanges_by_revision: dict[str, list[_ResolvedNode]] = defaultdict(list)
738
+ for exchange in (node for node in nodes if node.family == "model.exchange"):
739
+ _check_cancelled(cancel_event)
740
+ revision_id = _payload_str(exchange.start.payload, "context_revision_id")
741
+ if revision_id is not None:
742
+ exchanges_by_revision[revision_id].append(exchange)
743
+
744
+ valid_exchange_memberships: dict[str, tuple[str, ...]] = {}
745
+ invalid_exchange_membership_diagnostics: list[str] = []
746
+ for revision_id, exchanges in exchanges_by_revision.items():
747
+ _check_cancelled(cancel_event)
748
+ revision_errors = revisions.errors.get(revision_id, ())
749
+ if revision_errors:
750
+ invalid_exchange_membership_diagnostics.extend(revision_errors)
751
+ continue
752
+ membership = revisions.memberships.get(revision_id)
753
+ revision = revisions.endpoints.get(revision_id)
754
+ if membership is None or revision is None:
755
+ invalid_exchange_membership_diagnostics.append(f"context revision {revision_id} cannot be resolved")
756
+ continue
757
+ if len(exchanges) != 1:
758
+ invalid_exchange_membership_diagnostics.append(
759
+ f"context revision {revision_id} is claimed by multiple exchanges"
760
+ )
761
+ continue
762
+ exchange = exchanges[0]
763
+ if (
764
+ revision.parent_operation_id != exchange.operation_id
765
+ or revision.runtime_id != exchange.start.runtime_id
766
+ or revision.branch_id != exchange.start.branch_id
767
+ or revision.coverage_id != exchange.start.coverage_id
768
+ or revision.actor_id != exchange.start.actor_id
769
+ or revision.sequence >= exchange.start.sequence
770
+ ):
771
+ invalid_exchange_membership_diagnostics.append(
772
+ f"context revision {revision_id} does not belong to its claiming exchange"
773
+ )
774
+ continue
775
+ valid_exchange_memberships[revision_id] = membership
776
+ diagnostics.extend(invalid_exchange_membership_diagnostics)
777
+
778
+ for tool in (node for node in nodes if node.family == "tool.operation" and not _skip_node(node)):
779
+ _check_cancelled(cancel_event)
780
+ parent = _payload_str(tool.start.payload, "parent_model_operation_id")
781
+ if parent is None:
782
+ diagnostics.append("tool call producer lacks parent_model_operation_id")
783
+ exact = False
784
+ else:
785
+ exact = (
786
+ _connect_unique(
787
+ by_operation,
788
+ parent,
789
+ tool.node_id,
790
+ edges,
791
+ diagnostics,
792
+ "tool call producer",
793
+ families=frozenset({"model.run", "model.cycle", "model.exchange"}),
794
+ )
795
+ and exact
796
+ )
797
+ call_item_id = _payload_str(tool.start.payload, "call_item_id")
798
+ result_item_id = _payload_str(tool.finish.payload, "result_item_id")
799
+ if result_item_id is None and _never_dispatched(tool):
800
+ # A call the kernel closed without dispatching may own no result
801
+ # item at all (a filtered call never gets one), so there is no
802
+ # fan-in edge to demand. One that does carry a result (invalid
803
+ # arguments, unknown tool) is consumed like any other and falls
804
+ # through to the ordinary requirement.
805
+ continue
806
+ if call_item_id is None or result_item_id is None:
807
+ diagnostics.append("tool result fan-in lacks call_item_id or result_item_id")
808
+ exact = False
809
+ continue
810
+ result_carrier_item_id = _payload_str(tool.finish.payload, "result_carrier_item_id")
811
+ fan_in_item_ids = {result_item_id}
812
+ if result_carrier_item_id is not None:
813
+ fan_in_item_ids.add(result_carrier_item_id)
814
+ consumers = {
815
+ exchange.node_id
816
+ for revision_id, memberships in valid_exchange_memberships.items()
817
+ if fan_in_item_ids.intersection(memberships)
818
+ for exchange in exchanges_by_revision.get(revision_id, ())
819
+ }
820
+ if not consumers:
821
+ needs_consumer = response_source_operation_id is None or parent != response_source_operation_id
822
+ carrier_mapping_missing = False
823
+ if needs_consumer and result_carrier_item_id is None:
824
+ result_carrier_item_id = mapped_carrier(result_item_id)
825
+ if result_carrier_item_id is None:
826
+ carrier_mapping_missing = True
827
+ else:
828
+ fan_in_item_ids.add(result_carrier_item_id)
829
+ consumers = {
830
+ exchange.node_id
831
+ for revision_id, memberships in valid_exchange_memberships.items()
832
+ if result_carrier_item_id in memberships
833
+ for exchange in exchanges_by_revision.get(revision_id, ())
834
+ }
835
+ if needs_consumer and not consumers:
836
+ compaction_runs = [
837
+ run_id
838
+ for run_id, consumption in compaction_consumption.by_run.items()
839
+ if fan_in_item_ids.intersection(consumption.item_ids)
840
+ ]
841
+ if len(compaction_runs) == 1:
842
+ compaction_node_id = compaction_bridges.get(compaction_runs[0])
843
+ if compaction_node_id is not None:
844
+ consumers = {compaction_node_id}
845
+ elif len(compaction_runs) > 1:
846
+ diagnostics.append("tool result fan-in matches multiple Phase-4 compaction runs")
847
+ exact = False
848
+ if needs_consumer and not consumers:
849
+ if carrier_mapping_missing:
850
+ diagnostics.append("carrier mapping unavailable")
851
+ diagnostics.append("tool result fan-in cannot resolve a consuming context revision")
852
+ exact = False
853
+ if not consumers:
854
+ continue
855
+ edges[tool.node_id].update(consumers)
856
+ return exact, response_source_id
857
+
858
+
859
+ def _retry_neighbor_families(retry_mode: str | None) -> frozenset[str] | None:
860
+ if retry_mode in _RETRY_EXCHANGE_MODES:
861
+ return frozenset({"model.exchange"})
862
+ if retry_mode == RetryMode.RUN:
863
+ return frozenset({"model.run"})
864
+ return None
865
+
866
+
867
+ def _connect_unique(
868
+ by_operation: dict[str, list[_ResolvedNode]],
869
+ operation_id: str,
870
+ target_node_id: str | None,
871
+ edges: dict[str, set[str]],
872
+ diagnostics: list[str],
873
+ label: str,
874
+ *,
875
+ source: str | None = None,
876
+ families: frozenset[str] | None = None,
877
+ ) -> bool:
878
+ candidates = [
879
+ candidate
880
+ for candidate in by_operation.get(operation_id, ())
881
+ if candidate.node_id != source
882
+ and candidate.node_id != target_node_id
883
+ and (families is None or candidate.family in families)
884
+ ]
885
+ if len(candidates) != 1:
886
+ diagnostics.append(f"{label} cannot be resolved uniquely")
887
+ return False
888
+ if source is None and target_node_id is not None:
889
+ edges[candidates[0].node_id].add(target_node_id)
890
+ elif source is not None:
891
+ edges[source].add(candidates[0].node_id)
892
+ return True
893
+
894
+
895
+ def _connect_hook_dependencies(
896
+ nodes: list[_ResolvedNode],
897
+ edges: dict[str, set[str]],
898
+ *,
899
+ waited_hook_ids: frozenset[str] | None,
900
+ turn_root_id: str | None,
901
+ response_source_id: str | None,
902
+ response_terminal_id: str | None,
903
+ fork_edges: set[tuple[str, str]],
904
+ diagnostics: list[str],
905
+ ) -> tuple[bool, bool]:
906
+ by_operation: dict[str, list[_ResolvedNode]] = defaultdict(list)
907
+ for node in nodes:
908
+ by_operation[node.operation_id].append(node)
909
+ runs = sorted((node for node in nodes if node.family == "model.run"), key=lambda node: node.start.sequence)
910
+ dag_exact = True
911
+ response_exact = True
912
+ after_tool_hooks: list[tuple[_ResolvedNode, _ResolvedNode]] = []
913
+ blocking_dispatches: dict[tuple[str, str | None, str], list[_ResolvedNode]] = defaultdict(list)
914
+ for hook in (node for node in nodes if node.family == "hook.operation"):
915
+ mode = _payload_str(hook.start.payload, "execution_mode")
916
+ event = _payload_str(hook.start.payload, "hook_event")
917
+ if mode not in _HOOK_EXECUTION_MODES:
918
+ continue
919
+ target = _payload_str(hook.start.payload, "target_operation_id")
920
+ if mode == "blocking" and event != HookEvent.USER_INTERRUPT:
921
+ scope = _hook_drain_scope(hook.start)
922
+ if event is None or scope is None:
923
+ diagnostics.append("blocking hook dispatch lacks field-based ordering evidence")
924
+ dag_exact = False
925
+ else:
926
+ blocking_dispatches[(event, target, scope)].append(hook)
927
+ target_families = _hook_target_families(event)
928
+ targets = [
929
+ candidate
930
+ for candidate in by_operation.get(target or "", ())
931
+ if candidate.family != "retry" and (not target_families or candidate.family in target_families)
932
+ ]
933
+ if mode != "blocking" or event == HookEvent.USER_INTERRUPT:
934
+ if target is not None and len(targets) == 1:
935
+ edges[targets[0].node_id].add(hook.node_id)
936
+ fork_edges.add((targets[0].node_id, hook.node_id))
937
+ elif event == HookEvent.BEFORE_TOOL_CALL and len(targets) == 1:
938
+ edges[hook.node_id].add(targets[0].node_id)
939
+ elif event == HookEvent.BEFORE_TURN and runs:
940
+ edges[hook.node_id].add(runs[0].node_id)
941
+ elif event in {HookEvent.AFTER_TOOL_CALL, HookEvent.TOOL_ERROR} and len(targets) == 1:
942
+ after_tool_hooks.append((hook, targets[0]))
943
+ elif event == HookEvent.AFTER_TURN and response_source_id is not None:
944
+ edges[response_source_id].add(hook.node_id)
945
+ if response_terminal_id is not None:
946
+ edges[hook.node_id].add(response_terminal_id)
947
+ after_tool_hook_node_ids = {hook.node_id for hook, _ in after_tool_hooks}
948
+ for hook, target in after_tool_hooks:
949
+ for successor in tuple(edges.get(target.node_id, ())):
950
+ if successor not in after_tool_hook_node_ids:
951
+ edges[hook.node_id].add(successor)
952
+ for dispatch_hooks in blocking_dispatches.values():
953
+ ordered = sorted(dispatch_hooks, key=lambda hook: hook.start.sequence)
954
+ for previous, current in pairwise(ordered):
955
+ edges[previous.node_id].add(current.node_id)
956
+ if (
957
+ previous.start.runtime_id != current.start.runtime_id
958
+ or previous.start.branch_id != current.start.branch_id
959
+ or previous.finish.sequence >= current.start.sequence
960
+ or previous.finish.monotonic_ns > current.start.monotonic_ns
961
+ ):
962
+ diagnostics.append("blocking hook dispatch has inconsistent serial intervals")
963
+ dag_exact = False
964
+ if waited_hook_ids is not None and response_terminal_id is not None:
965
+ hooks = {node.operation_id: node for node in nodes if node.family == "hook.operation"}
966
+ for operation_id in waited_hook_ids:
967
+ hook = hooks.get(operation_id)
968
+ if hook is None:
969
+ diagnostics.append("turn response fence names an unknown hook operation")
970
+ response_exact = False
971
+ else:
972
+ edges[hook.node_id].add(response_terminal_id)
973
+ if turn_root_id is None or not _reachable(edges, turn_root_id, hook.node_id):
974
+ diagnostics.append("waited async hook lacks a typed fork path from the turn root")
975
+ response_exact = False
976
+ return dag_exact, response_exact
977
+
978
+
979
+ def _reachable(edges: dict[str, set[str]], source: str, target: str) -> bool:
980
+ pending = [source]
981
+ seen: set[str] = set()
982
+ while pending:
983
+ node_id = pending.pop()
984
+ if node_id == target:
985
+ return True
986
+ if node_id in seen:
987
+ continue
988
+ seen.add(node_id)
989
+ pending.extend(edges.get(node_id, ()))
990
+ return False
991
+
992
+
993
+ def _critical_paths(
994
+ nodes: list[_ResolvedNode],
995
+ slices: tuple[TimeSlice, ...],
996
+ dependency: _DependencyProof,
997
+ *,
998
+ cancel_event: Event | None,
999
+ ) -> _CriticalPaths:
1000
+ compute_intervals, response_intervals = _critical_path_interval_maps(nodes, slices, cancel_event=cancel_event)
1001
+ terminal_id = dependency.response_terminal_id
1002
+ if terminal_id is not None:
1003
+ compute_intervals[terminal_id] = []
1004
+ response_intervals[terminal_id] = [
1005
+ (item.start_ns, item.end_ns) for item in slices if item.operation_id is None and item.response_weight
1006
+ ]
1007
+ compute = _longest_interval_path(
1008
+ compute_intervals,
1009
+ dependency.edges,
1010
+ parents=dependency.parents,
1011
+ fork_edges=dependency.fork_edges,
1012
+ cancel_event=cancel_event,
1013
+ )
1014
+ if dependency.turn_root_id is None or terminal_id is None:
1015
+ response = _PathResolution(None, compute.acyclic, True)
1016
+ response_reachable = False
1017
+ else:
1018
+ response = _longest_interval_path(
1019
+ response_intervals,
1020
+ dependency.edges,
1021
+ parents=dependency.parents,
1022
+ fork_edges=dependency.fork_edges,
1023
+ root_id=dependency.turn_root_id,
1024
+ terminal_id=terminal_id,
1025
+ cancel_event=cancel_event,
1026
+ )
1027
+ response_reachable = _reachable(dependency.edges, dependency.turn_root_id, terminal_id)
1028
+ return _CriticalPaths(
1029
+ compute_ns=compute.value,
1030
+ response_ns=response.value,
1031
+ acyclic=compute.acyclic and response.acyclic,
1032
+ response_reachable=response_reachable,
1033
+ compute_bounded=compute.bounded,
1034
+ response_bounded=response.bounded,
1035
+ )
1036
+
1037
+
1038
+ def _critical_path_interval_maps(
1039
+ nodes: list[_ResolvedNode],
1040
+ slices: tuple[TimeSlice, ...],
1041
+ *,
1042
+ cancel_event: Event | None,
1043
+ ) -> tuple[dict[str, list[tuple[int, int]]], dict[str, list[tuple[int, int]]]]:
1044
+ compute_intervals = {node.node_id: [] for node in nodes}
1045
+ response_intervals = {node.node_id: [] for node in nodes}
1046
+ for item in slices:
1047
+ _check_cancelled(cancel_event)
1048
+ if item.operation_id is None:
1049
+ continue
1050
+ candidates = [node for node in nodes if node.operation_id == item.operation_id]
1051
+ if len(candidates) != 1:
1052
+ candidates = [
1053
+ node
1054
+ for node in candidates
1055
+ if (attributes := _slice_attributes(node)) is not None and attributes[4] == item.owner
1056
+ ]
1057
+ if len(candidates) != 1:
1058
+ continue
1059
+ node_id = candidates[0].node_id
1060
+ if item.compute_weight:
1061
+ compute_intervals[node_id].append((item.start_ns, item.end_ns))
1062
+ if item.response_weight:
1063
+ response_intervals[node_id].append((item.start_ns, item.end_ns))
1064
+ return compute_intervals, response_intervals
1065
+
1066
+
1067
+ def _failed_tool_cp_contributions(
1068
+ nodes: list[_ResolvedNode],
1069
+ slices: tuple[TimeSlice, ...],
1070
+ dependency: _DependencyProof,
1071
+ *,
1072
+ response_cp: int,
1073
+ cancel_event: Event | None,
1074
+ ) -> dict[str, int]:
1075
+ groups = {
1076
+ node.operation_id: frozenset((node.node_id,))
1077
+ for node in nodes
1078
+ if node.family == "tool.operation" and tool_failed(_payload_str(node.finish.payload, "outcome"))
1079
+ }
1080
+ return {
1081
+ operation_id: contribution
1082
+ for operation_id, contribution in _grouped_cp_contributions(
1083
+ nodes,
1084
+ slices,
1085
+ dependency,
1086
+ groups=groups,
1087
+ response_cp=response_cp,
1088
+ cancel_event=cancel_event,
1089
+ ).items()
1090
+ if contribution
1091
+ }
1092
+
1093
+
1094
+ def _server_tool_cp_contributions(
1095
+ intermediate: _Intermediate,
1096
+ nodes: list[_ResolvedNode],
1097
+ slices: tuple[TimeSlice, ...],
1098
+ dependency: _DependencyProof,
1099
+ *,
1100
+ response_cp: int,
1101
+ cancel_event: Event | None,
1102
+ ) -> dict[str, int]:
1103
+ grouped: dict[str, set[str]] = defaultdict(set)
1104
+ tool_nodes = intermediate.nodes.get("tool.operation", {})
1105
+ for node in nodes:
1106
+ _check_cancelled(cancel_event)
1107
+ if node.family != "tool.operation":
1108
+ continue
1109
+ raw_node = tool_nodes.get(node.operation_id)
1110
+ context = _tool_context_for_start(raw_node, node.start) if raw_node is not None else None
1111
+ if context is not None and context.server_name is not None:
1112
+ grouped[context.server_name].add(node.node_id)
1113
+ return _grouped_cp_contributions(
1114
+ nodes,
1115
+ slices,
1116
+ dependency,
1117
+ groups={name: frozenset(node_ids) for name, node_ids in grouped.items()},
1118
+ response_cp=response_cp,
1119
+ cancel_event=cancel_event,
1120
+ )
1121
+
1122
+
1123
+ def _grouped_cp_contributions(
1124
+ nodes: list[_ResolvedNode],
1125
+ slices: tuple[TimeSlice, ...],
1126
+ dependency: _DependencyProof,
1127
+ *,
1128
+ groups: dict[str, frozenset[str]],
1129
+ response_cp: int,
1130
+ cancel_event: Event | None,
1131
+ ) -> dict[str, int]:
1132
+ """Resolve one leave-one-group-out response path per supplied group."""
1133
+ terminal_id = dependency.response_terminal_id
1134
+ root_id = dependency.turn_root_id
1135
+ if not groups or root_id is None or terminal_id is None or response_cp <= 0:
1136
+ return {}
1137
+ _, response_intervals = _critical_path_interval_maps(nodes, slices, cancel_event=cancel_event)
1138
+ response_intervals[terminal_id] = [
1139
+ (item.start_ns, item.end_ns) for item in slices if item.operation_id is None and item.response_weight
1140
+ ]
1141
+ children: dict[str, set[str]] = defaultdict(set)
1142
+ for child, parent in dependency.parents.items():
1143
+ children[parent].add(child)
1144
+ contributions: dict[str, int] = {}
1145
+ for group, node_ids in groups.items():
1146
+ _check_cancelled(cancel_event)
1147
+ without = dict(response_intervals)
1148
+ # A member's contribution spans everything it displaced: the approval
1149
+ # and sub-agent inside a tool left with it.
1150
+ for node_id in node_ids:
1151
+ without[node_id] = []
1152
+ for descendant in _descendants(node_id, children):
1153
+ without[descendant] = []
1154
+ resolved = _longest_interval_path(
1155
+ without,
1156
+ dependency.edges,
1157
+ parents=dependency.parents,
1158
+ fork_edges=dependency.fork_edges,
1159
+ root_id=root_id,
1160
+ terminal_id=terminal_id,
1161
+ cancel_event=cancel_event,
1162
+ )
1163
+ if resolved.value is not None and resolved.acyclic and resolved.bounded:
1164
+ contributions[group] = max(0, response_cp - resolved.value)
1165
+ return contributions
1166
+
1167
+
1168
+ def _wall_partition(
1169
+ bounds: tuple[int, int],
1170
+ slices: tuple[TimeSlice, ...],
1171
+ *,
1172
+ cancel_event: Event | None,
1173
+ ) -> tuple[dict[WallBucket, int], tuple[tuple[int, int], ...]]:
1174
+ points = {bounds[0], bounds[1]}
1175
+ changes: dict[int, Counter[WallBucket]] = defaultdict(Counter)
1176
+ for item in slices:
1177
+ _check_cancelled(cancel_event)
1178
+ if item.wall_bucket is None:
1179
+ continue
1180
+ start = max(bounds[0], item.start_ns)
1181
+ end = min(bounds[1], item.end_ns)
1182
+ if end <= start:
1183
+ continue
1184
+ points.add(start)
1185
+ points.add(end)
1186
+ changes[start][item.wall_bucket] += 1
1187
+ changes[end][item.wall_bucket] -= 1
1188
+ ordered = sorted(points)
1189
+ durations = dict.fromkeys(WallBucket, 0)
1190
+ idle: list[tuple[int, int]] = []
1191
+ active_counts: Counter[WallBucket] = Counter()
1192
+ for left, right in pairwise(ordered):
1193
+ _check_cancelled(cancel_event)
1194
+ for bucket, delta in changes.get(left, {}).items():
1195
+ count = active_counts[bucket] + delta
1196
+ if count > 0:
1197
+ active_counts[bucket] = count
1198
+ else:
1199
+ active_counts.pop(bucket, None)
1200
+ if right <= left:
1201
+ continue
1202
+ bucket = max(active_counts, key=lambda value: _WALL_PRIORITY[value]) if active_counts else WallBucket.IDLE
1203
+ durations[bucket] += right - left
1204
+ if bucket is WallBucket.IDLE:
1205
+ idle.append((left, right))
1206
+ return durations, interval_union(idle)
1207
+
1208
+
1209
+ def _descendants(node_id: str, children: dict[str, set[str]]) -> set[str]:
1210
+ result: set[str] = set()
1211
+ pending = list(children.get(node_id, set()))
1212
+ while pending:
1213
+ child = pending.pop()
1214
+ if child in result:
1215
+ continue
1216
+ result.add(child)
1217
+ pending.extend(children.get(child, set()))
1218
+ return result
1219
+
1220
+
1221
+ def _has_cycle(children: dict[str, set[str]]) -> bool:
1222
+ visiting: set[str] = set()
1223
+ visited: set[str] = set()
1224
+
1225
+ def visit(node_id: str) -> bool:
1226
+ if node_id in visiting:
1227
+ return True
1228
+ if node_id in visited:
1229
+ return False
1230
+ visiting.add(node_id)
1231
+ if any(visit(child) for child in children.get(node_id, set())):
1232
+ return True
1233
+ visiting.remove(node_id)
1234
+ visited.add(node_id)
1235
+ return False
1236
+
1237
+ return any(visit(node_id) for node_id in children)