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,2228 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """SubAgentTools — manages sub-agent instances and concurrency control.
4
+
5
+ Creates FunctionTool wrappers around sub-agents so the parent agent can
6
+ delegate tasks via tool calls. Enforces two levels of concurrency limits:
7
+
8
+ - **Global**: ``max_total_concurrency`` caps total active sub-agent calls.
9
+ - **Per-agent**: ``SubAgentRef.max_concurrency`` caps calls to a single sub-agent.
10
+
11
+ When either limit is exceeded, the tool returns an error string immediately
12
+ rather than blocking.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import contextlib
19
+ import logging
20
+ import tempfile
21
+ from contextvars import Token
22
+ from pathlib import Path
23
+ from typing import TYPE_CHECKING, Any
24
+ from uuid import uuid4
25
+
26
+ from chrys.foundation.config.settings import DEFAULT_ASK_USER_TIMEOUT_SECONDS
27
+ from chrys.foundation.events.types import (
28
+ InvocationCompactionCommitted,
29
+ InvocationCompactionFinished,
30
+ InvocationCompactionStarted,
31
+ InvocationContextPressure,
32
+ InvocationMessage,
33
+ InvocationRetryAttempt,
34
+ InvocationStarted,
35
+ Warning,
36
+ )
37
+ from chrys.foundation.models.invocations import InvocationOrigin
38
+ from chrys.foundation.platform.files import (
39
+ secure_unlink_owner_verified,
40
+ )
41
+ from chrys.foundation.retry import RetryAttemptInfo
42
+ from chrys.foundation.tool_kinds import (
43
+ KIND_ASK_USER,
44
+ KIND_FILESYSTEM_READ,
45
+ KIND_SHELL,
46
+ KIND_SUB_AGENT,
47
+ get_tool_kind,
48
+ set_tool_kind,
49
+ )
50
+ from chrys.foundation.tool_result_metadata import tool_result_metadata_failure_state
51
+ from chrys.foundation.trajectory.context import TRAJECTORY_CONTEXT_KWARG, trajectory_scope
52
+ from chrys.foundation.trajectory.event_types import TraceCoverage
53
+ from chrys.foundation.util.filenames import safe_fs_name
54
+ from chrys.foundation.util.sub_agent_context import (
55
+ SubAgentParentResultMetadata,
56
+ sub_agent_parent_call_id,
57
+ sub_agent_parent_result_metadata,
58
+ )
59
+ from chrys.kernel import (
60
+ Agent,
61
+ FunctionTool,
62
+ LoopRecorder,
63
+ Message,
64
+ StallExhaustedAction,
65
+ )
66
+ from chrys.orchestration.invoker.acp_protocol import AcpPermissionBroker
67
+ from chrys.orchestration.invoker.acp_spec import resolve_acp_spec
68
+ from chrys.orchestration.invoker.attempts import AgentRunKwargs, WireRecipe
69
+ from chrys.orchestration.invoker.child_history import service_storage_side
70
+ from chrys.orchestration.invoker.contracts import SubAgentFailureReason, SubAgentStatus, Unbind
71
+ from chrys.orchestration.invoker.origin import BoundEmitter, current_invocation_origin, invocation_routing_key
72
+ from chrys.orchestration.invoker.resources import Conversation, OperationLifetime, PreparedAgent, rollback_resources
73
+ from chrys.orchestration.invoker.runtime import (
74
+ ApprovalInputs,
75
+ AskUserInputs,
76
+ ChildRecipe,
77
+ ContextInputs,
78
+ KernelRuntime,
79
+ LastWordsInputs,
80
+ ReminderInputs,
81
+ SharedRuntime,
82
+ create_approval,
83
+ create_ask_user,
84
+ create_runtime,
85
+ create_validation,
86
+ )
87
+ from chrys.orchestration.sub_agents.acp_policy import AcpSubAgentPolicy
88
+ from chrys.orchestration.sub_agents.kernel_policy import KernelSubAgentPolicy
89
+ from chrys.service.acp_client import AcpConfigError
90
+ from chrys.service.agent_middleware import (
91
+ ApprovalMiddleware,
92
+ IntermediateTextBuffer,
93
+ SleepMiddleware,
94
+ SubAgentEventMiddleware,
95
+ SubAgentStatsMiddleware,
96
+ )
97
+ from chrys.service.approval.policy import ApprovalMode, ApprovalPolicy
98
+ from chrys.service.approval.turn_context import TurnContextHolder
99
+ from chrys.service.context.compaction.last_words_state import DropRoundBreakerState
100
+ from chrys.service.context.compaction.spill import COMPACTIONS_DIR_NAME, sub_agent_dropped_turn_relative_path
101
+ from chrys.service.context.manager import ContextManager
102
+ from chrys.service.context.middleware.usage import UsageTrackingMiddleware
103
+ from chrys.service.hooks.events import HookEvent
104
+ from chrys.service.llm.clients import create_client, effective_model_base_url
105
+ from chrys.service.llm.route_sessions import derive_llm_route_session_id, llm_parent_session_id, llm_route_session_id
106
+ from chrys.service.mcp.adapter import MCPAdapter
107
+ from chrys.service.mcp.thinking_warning import warn_if_tool_loading_unbinds_thinking
108
+ from chrys.service.profiles.agents.schema import DEFAULT_SUB_AGENT_CONCURRENCY
109
+ from chrys.service.profiles.models.options import effective_chat_options, uses_responses_compact_continuation
110
+ from chrys.service.profiles.models.schema import API_STYLE_RESPONSES
111
+ from chrys.service.session.message_metadata import TOOL_RESULT_METADATA_KEY
112
+ from chrys.service.session.sub_agent_logs import (
113
+ SubAgentLogStats,
114
+ SubAgentModelProvenance,
115
+ SubAgentSessionLogWriter,
116
+ pending_dir,
117
+ sanitize_base_url_origin,
118
+ sessions_dir,
119
+ )
120
+ from chrys.service.state.serializers import serialized_message_payload
121
+ from chrys.service.tools.builtins.web.build import assemble_web_tools
122
+ from chrys.service.tools.names import chrys_reserved_tool_names
123
+ from chrys.service.tools.registry import ToolRegistry
124
+ from chrys.service.tools.result_metadata import tool_error, tool_result_metadata
125
+ from chrys.service.trajectory.sub_agents import SubAgentStatus as TrajectorySubAgentStatus
126
+ from chrys.service.trajectory.sub_agents import SubAgentTrace
127
+ from chrys.service.vision import filter_image_tools
128
+
129
+ if TYPE_CHECKING:
130
+ from collections.abc import Awaitable, Callable, Coroutine, Mapping
131
+
132
+ from chrys.foundation.config.settings import Settings
133
+ from chrys.foundation.events.bus import EventBus
134
+ from chrys.foundation.models.session_env import SessionEnvironment
135
+ from chrys.service.approval.judge import ApprovalJudge
136
+ from chrys.service.context.compaction import PreCompactInfo
137
+ from chrys.service.context.compaction.last_words import CompactionStatus
138
+ from chrys.service.context.compaction.spill import SpillQuota
139
+ from chrys.service.hooks.manager import HookManager
140
+ from chrys.service.mcp.cache import MCPConnectionCache
141
+ from chrys.service.mutations.coordination import MutationCoordinator
142
+ from chrys.service.mutations.tracker import MutationTracker
143
+ from chrys.service.profiles.agents.schema import AgentProfile, ApprovalConfig, SubAgentRef
144
+ from chrys.service.profiles.models.registry import ModelProfileRegistry
145
+ from chrys.service.profiles.models.schema import ModelProfile
146
+ from chrys.service.skills.provider import ChrysSkillsProvider
147
+
148
+ from .shell import SubAgentToolShell
149
+
150
+ logger = logging.getLogger(__name__)
151
+
152
+
153
+ # Item cap for the cross-attempt ACP audit-update trail — matches the
154
+ # write-time envelope ring (``normalize_acp_state``'s ``_ACP_RING_ITEMS``),
155
+ # which also applies the authoritative byte bound on every write.
156
+ _ACP_AUDIT_TRAIL_ITEMS = 500
157
+ # Mirrors the write-time attempt ring in ``normalize_acp_state``
158
+ # (``_ACP_ATTEMPT_ITEMS``): manual retries are unlimited, so the in-memory
159
+ # attempt history must be bounded the same newest-wins way.
160
+ _ACP_AUDIT_ATTEMPT_ITEMS = 50
161
+
162
+
163
+ def _execution_note(per_agent_max: int, max_total: int) -> str:
164
+ """Describe execution and concurrency limits for either sub-agent backend."""
165
+ return (
166
+ f"\n(Concurrency limits: max {per_agent_max} concurrent calls to this tool, "
167
+ f"max {max_total} concurrent sub-agent calls total. "
168
+ "Exceeding either limit returns an error.)\n"
169
+ "This call waits for the sub-agent's result. Calls in the same response are dispatched "
170
+ "together with no guaranteed execution order. The calling agent resumes only after the entire "
171
+ "batch returns. Calls that depend on this result or could conflict through shared state must go "
172
+ "in a later response. No asynchronous task handle is provided."
173
+ )
174
+
175
+
176
+ def _close_sub_agent_trace(
177
+ trace: SubAgentTrace | None,
178
+ *,
179
+ hook_status: str,
180
+ failure_code: str | None,
181
+ controller: SubAgentToolShell | None = None,
182
+ failure_reason: SubAgentFailureReason | None = None,
183
+ ) -> None:
184
+ """Close the boundary from the invocation's terminal status (queued, never awaited: cleanup path).
185
+
186
+ ``hook_status`` is read off the result text, so a child that reports its
187
+ failure in prose records as a success, and one whose answer merely opens
188
+ like an error records as a failure. The controller knows which it was;
189
+ where it has run, its verdict decides — with the structured result
190
+ metadata deciding what its terminal status leaves open.
191
+ """
192
+ if trace is None:
193
+ return
194
+ if hook_status == "cancelled":
195
+ trace.finished_soon(status=TrajectorySubAgentStatus.CANCELLED)
196
+ return
197
+ succeeded = hook_status == "ok"
198
+ reason_code = failure_code
199
+ if controller is not None:
200
+ if controller.status == SubAgentStatus.COMPLETED:
201
+ # COMPLETED says the invocation reached its end, not that the work
202
+ # succeeded: a controller that gives up (spawn, auth, refusal)
203
+ # still completes, and records the failure structurally on its way
204
+ # out. That verdict decides where it exists; the prose never does.
205
+ succeeded = tool_result_metadata_failure_state(tool_result_metadata.get(None)) is not True
206
+ elif controller.status in (SubAgentStatus.ABORTED, SubAgentStatus.CASCADE_ABORTED):
207
+ succeeded = False
208
+ reason_code = failure_code or controller.status.value
209
+ if failure_reason is not None:
210
+ reason_code = failure_reason.value
211
+ if succeeded:
212
+ trace.finished_soon(status=TrajectorySubAgentStatus.OK)
213
+ return
214
+ trace.finished_soon(
215
+ status=TrajectorySubAgentStatus.SETUP_FAILED
216
+ if failure_code == "setup_failed"
217
+ else TrajectorySubAgentStatus.FAILED,
218
+ failure_reason_code=reason_code,
219
+ )
220
+
221
+
222
+ def _parent_result_replay_metadata(metadata: SubAgentParentResultMetadata) -> dict[str, Any]:
223
+ result_metadata: dict[str, Any] = {}
224
+ if metadata.sub_agent_invocation_id:
225
+ result_metadata["sub_agent_invocation_id"] = metadata.sub_agent_invocation_id
226
+ if metadata.sub_agent_log_file:
227
+ result_metadata["sub_agent_log_file"] = metadata.sub_agent_log_file
228
+ if metadata.sub_agent_audit_complete:
229
+ result_metadata["sub_agent_audit_complete"] = True
230
+ return result_metadata
231
+
232
+
233
+ def _commit_interrupted_parent_result(metadata: SubAgentParentResultMetadata | None) -> None:
234
+ if metadata is None or metadata.commit_interrupted_result is None or metadata.interrupted_result_committed:
235
+ return
236
+ metadata.commit_interrupted_result(_parent_result_replay_metadata(metadata))
237
+ metadata.interrupted_result_committed = True
238
+
239
+
240
+ def _upgrade_interrupted_parent_result(metadata: SubAgentParentResultMetadata | None) -> None:
241
+ """Merge late audit references into an already committed interrupted result."""
242
+ if metadata is None or metadata.commit_interrupted_result is None or not metadata.interrupted_result_committed:
243
+ return
244
+ metadata.commit_interrupted_result(_parent_result_replay_metadata(metadata))
245
+
246
+
247
+ async def _finish_cancellation_cleanup(awaitable: Coroutine[Any, Any, None]) -> None:
248
+ task = asyncio.create_task(awaitable)
249
+ while not task.done():
250
+ try:
251
+ await asyncio.shield(task)
252
+ except asyncio.CancelledError:
253
+ continue
254
+ try:
255
+ task.result()
256
+ except Exception:
257
+ logger.debug("sub-agent cancellation finalization failed", exc_info=True)
258
+
259
+
260
+ def _acp_stderr_path(
261
+ parent_session_dir: Path | None,
262
+ writer: SubAgentSessionLogWriter | None,
263
+ invocation_id: str,
264
+ ) -> Path:
265
+ if parent_session_dir is not None and writer is not None:
266
+ return sessions_dir(parent_session_dir) / f"{writer.log_file_name}.stderr.log"
267
+ return Path(tempfile.gettempdir()) / f"chrys-acp-{invocation_id}.stderr.log"
268
+
269
+
270
+ class SubAgentTools:
271
+ """Manages sub-agent lifecycle and exposes them as FunctionTools.
272
+
273
+ Each sub-agent runs with an independent session (``propagate_session=False``).
274
+ Concurrency is enforced at two levels:
275
+
276
+ - ``_total_active``: shared counter across all sub-agents.
277
+ - ``_agent_active[name]``: per-agent counter.
278
+
279
+ Both are checked before execution; exceeding either returns an error
280
+ string to the model.
281
+ """
282
+
283
+ def __init__(
284
+ self,
285
+ max_total_concurrency: int = DEFAULT_SUB_AGENT_CONCURRENCY,
286
+ event_bus: EventBus | None = None,
287
+ session_id: str | None = None,
288
+ session_dir: Path | None = None,
289
+ on_sub_agent_usage: Callable[..., None] | None = None,
290
+ on_side_call_usage: Callable[[Mapping[str, Any]], None] | None = None,
291
+ drain_parent_usage_publishes: Callable[[], Awaitable[None]] | None = None,
292
+ parent_approval: ApprovalConfig | None = None,
293
+ mutation_tracker: MutationTracker | None = None,
294
+ mutation_coordinator: MutationCoordinator | None = None,
295
+ approval_mode: ApprovalMode = ApprovalMode.MANUAL,
296
+ approval_judge: ApprovalJudge | None = None,
297
+ workspace_roots: list[str] | None = None,
298
+ approval_log_dir: Path | None = None,
299
+ mcp_cache: MCPConnectionCache | None = None,
300
+ hook_manager: HookManager | None = None,
301
+ workspace_cwd: str = "",
302
+ serialize_implicit_windows: bool = False,
303
+ spill_quota: SpillQuota | None = None,
304
+ ask_user_timeout_seconds: int | None = DEFAULT_ASK_USER_TIMEOUT_SECONDS,
305
+ turn_context: TurnContextHolder | None = None,
306
+ max_transient_retries: int | None = None,
307
+ tool_result_ceiling_tokens: int | None = None,
308
+ allow_user_interaction: bool = True,
309
+ human_failure_decisions: bool = True,
310
+ ) -> None:
311
+ self._max_total = max_total_concurrency
312
+ self._bus = event_bus
313
+ self._session_id = session_id
314
+ self._session_dir = session_dir
315
+ self._on_sub_agent_usage = on_sub_agent_usage
316
+ self._on_side_call_usage = on_side_call_usage
317
+ # Awaited before ``_invoke`` returns so the sub-agent's final
318
+ # ``UsageUpdate`` — queued on the parent engine's
319
+ # ``_usage_publish_tail`` via ``_track_usage_publish`` — is delivered
320
+ # before the parent's ``InvocationToolCallResult``. Without this, a slow
321
+ # subscriber can let the parent tool result overtake the trailing
322
+ # sub-agent usage event.
323
+ self._drain_parent_usage_publishes = drain_parent_usage_publishes
324
+ self._parent_approval = parent_approval
325
+ self._mutation_tracker = mutation_tracker
326
+ self._mutation_coordinator = mutation_coordinator
327
+ self._approval_mode = approval_mode
328
+ self._approval_judge = approval_judge
329
+ self._workspace_roots = list(workspace_roots or [])
330
+ self._approval_log_dir = approval_log_dir
331
+ self._mcp_cache = mcp_cache
332
+ self._hook_manager = hook_manager
333
+ self._workspace_cwd = workspace_cwd
334
+ self._serialize_implicit_windows = serialize_implicit_windows
335
+ self._spill_quota = spill_quota
336
+ # Per-tool profile name for hook payloads. Filled in
337
+ # :meth:`register`; reads on missing keys fall back to the bare
338
+ # tool name in :meth:`_make_tool._invoke`.
339
+ self._hook_profile_names: dict[str, str] = {}
340
+ self._total_active = 0
341
+ self._runtime: SessionEnvironment | None = None
342
+ self._ask_user_timeout_seconds = ask_user_timeout_seconds
343
+ self._max_transient_retries = max_transient_retries
344
+ self._tool_result_ceiling_tokens = tool_result_ceiling_tokens
345
+ self._allow_user_interaction = allow_user_interaction
346
+ # Whether a failed child pauses for the user's Retry/Abort on its card; a caller whose
347
+ # surface shows no such card ends the child instead, its error becoming the tool result.
348
+ self._human_failure_decisions = human_failure_decisions
349
+ self._turn_context = turn_context or TurnContextHolder()
350
+ self._agent_active: dict[str, int] = {} # tool_name → active count
351
+ self._agent_max: dict[str, int] = {} # tool_name → per-agent limit
352
+ self._prepared_by_tool: dict[str, PreparedAgent] = {}
353
+ self._agents: dict[str, Agent] = {} # tool_name → Agent
354
+ self._context_managers: dict[str, ContextManager] = {} # tool_name → ContextManager
355
+ self._mcp_adapters: dict[str, MCPAdapter] = {} # tool_name → MCPAdapter
356
+ self._chat_options_by_tool: dict[str, dict[str, Any] | None] = {} # tool_name → agent.run(options=...)
357
+ self._model_provenance_by_tool: dict[str, SubAgentModelProvenance] = {}
358
+ self._approval_policies: dict[str, ApprovalPolicy] = {} # tool_name → ApprovalPolicy
359
+ self._tool_kinds: dict[str, dict[str, str]] = {} # tool_name → {fn_name → kind}
360
+ self._display_names: dict[str, str] = {} # tool_name → human-readable agent name
361
+ self._acp_profiles: dict[str, AgentProfile] = {}
362
+ self._tools: list[FunctionTool] = []
363
+ # Live ApprovalMiddleware instances for in-flight sub-agent runs.
364
+ # Mid-run mode switches must propagate into these so subsequent tool
365
+ # calls within the same run pick up the new mode.
366
+ self._live_approvals: list[ApprovalMiddleware] = []
367
+ # Live controllers keyed by invocation_id. The engine routes
368
+ # InvocationRetryRequested / InvocationAbortRequested events through
369
+ # this registry so the right sub-agent receives the decision.
370
+ self._controllers: dict[str, SubAgentToolShell] = {}
371
+ self._pending_cleanup_paths: set[Path] = set()
372
+ self._intermediate_buffers: dict[str, IntermediateTextBuffer] = {}
373
+ self._invocation_emitters: dict[str, BoundEmitter] = {}
374
+
375
+ def _emitter_for(self, invocation_id: str) -> BoundEmitter:
376
+ """Resolve a registered callback key; missing keys never borrow identity."""
377
+ try:
378
+ return self._invocation_emitters[invocation_id]
379
+ except KeyError as exc:
380
+ raise ValueError("Cannot route an unbound invocation callback") from exc
381
+
382
+ async def register(
383
+ self,
384
+ ref: SubAgentRef,
385
+ profile: AgentProfile,
386
+ runtime: SessionEnvironment,
387
+ *,
388
+ settings: Settings,
389
+ fallback_profile: ModelProfile,
390
+ model_registry: ModelProfileRegistry | None = None,
391
+ ) -> None:
392
+ """Build a sub-agent from a profile and register it as a tool.
393
+
394
+ ``fallback_profile`` is the parent agent's resolved
395
+ ``ModelProfile`` — used when this sub-agent's
396
+ ``profile.model.profile_id`` is empty or references a deleted
397
+ profile. Sub-agents with their own model binding use that profile;
398
+ unbound sub-agents inherit the parent's resolved profile. The
399
+ command-line ``--model`` flag changes only the active fallback model,
400
+ so it does not replace sub-agent model bindings.
401
+ """
402
+ prepared = PreparedAgent()
403
+
404
+ async def _acquire(conversation: Conversation) -> None:
405
+ from chrys.service.profiles.models.resolver import resolve_for_agent
406
+
407
+ sub_active_profile = resolve_for_agent(model_registry, settings, profile, fallback=fallback_profile)
408
+
409
+ self._runtime = runtime
410
+ self._ask_user_timeout_seconds = settings.ask_user_timeout_seconds
411
+ runtime_cwd = runtime.cwd if isinstance(runtime.cwd, str) else None
412
+ if not self._workspace_cwd and runtime_cwd is not None:
413
+ self._workspace_cwd = runtime_cwd
414
+ chat_options = effective_chat_options(sub_active_profile)
415
+ capture_service_loop_messages = uses_responses_compact_continuation(sub_active_profile, chat_options)
416
+ tool_name = ref.tool_name or profile.name
417
+ sub_agent_session_id = derive_llm_route_session_id(
418
+ self._session_id,
419
+ route_kind="sub-agent",
420
+ route_parts=(tool_name, profile.name),
421
+ model_profile=sub_active_profile,
422
+ )
423
+ last_words_session_id = derive_llm_route_session_id(
424
+ self._session_id,
425
+ route_kind="last-words",
426
+ route_parts=(tool_name, profile.name),
427
+ model_profile=sub_active_profile,
428
+ )
429
+ agent_display_name = profile.display_name or profile.name
430
+
431
+ async def _on_intermediate_async(text: str) -> None:
432
+ invocation_id = invocation_routing_key.get()
433
+ buffer = self._intermediate_buffers.get(invocation_id)
434
+ if buffer is None:
435
+ return
436
+ emitter = self._emitter_for(invocation_id)
437
+ buffer.new_batch()
438
+ if self._bus is not None:
439
+ await emitter.publish(
440
+ InvocationMessage(
441
+ is_final=False,
442
+ is_intermediate=True,
443
+ origin=emitter.origin,
444
+ agent_name=agent_display_name,
445
+ text=text,
446
+ session_id=self._session_id,
447
+ )
448
+ )
449
+
450
+ def _on_intermediate_sync(text: str) -> None:
451
+ invocation_id = invocation_routing_key.get()
452
+ buffer = self._intermediate_buffers.get(invocation_id)
453
+ if buffer is not None:
454
+ buffer.store(text)
455
+
456
+ client = await create_client(
457
+ sub_active_profile,
458
+ on_intermediate_text_async=_on_intermediate_async,
459
+ on_intermediate_text_sync=_on_intermediate_sync,
460
+ session_id=sub_agent_session_id,
461
+ parent_session_id=self._session_id,
462
+ use_route_session_context=True,
463
+ session_dir=self._session_dir,
464
+ tool_result_ceiling_tokens=self._tool_result_ceiling_tokens,
465
+ )
466
+ await prepared.own_or_release(client.aclose)
467
+ model_provenance = SubAgentModelProvenance(
468
+ provider=sub_active_profile.provider,
469
+ api_style=sub_active_profile.api_style,
470
+ model_id=sub_active_profile.model_id,
471
+ base_url_origin=sanitize_base_url_origin(effective_model_base_url(sub_active_profile)),
472
+ )
473
+
474
+ # Build sub-agent's own tools from its profile
475
+ tool_registry = ToolRegistry(vision_enabled=sub_active_profile.vision)
476
+ builtin_categories = profile.tools.builtins or []
477
+ web_tools = assemble_web_tools(
478
+ builtin_categories,
479
+ settings,
480
+ profile.tools.web_search,
481
+ profile.tools.web_fetch,
482
+ chat_options=chat_options,
483
+ agent=profile.name,
484
+ model_profile_id=sub_active_profile.id,
485
+ session_id=self._session_id,
486
+ )
487
+ tool_registry.load_builtins(
488
+ builtin_categories,
489
+ runtime=runtime,
490
+ settings=settings,
491
+ shell_filter_config=profile.tools.shell_filter,
492
+ session_id=self._session_id,
493
+ session_dir=self._session_dir,
494
+ web=web_tools,
495
+ )
496
+ if self._bus is not None:
497
+ for warning in web_tools.warnings:
498
+ await self._bus.publish(warning)
499
+
500
+ tools = filter_image_tools(tool_registry.get_all(), vision_enabled=sub_active_profile.vision)
501
+ if not self._allow_user_interaction:
502
+ tools = [tool for tool in tools if get_tool_kind(tool) != KIND_ASK_USER]
503
+
504
+ # Load MCP tools from the sub-agent's profile
505
+ mcp_adapter: MCPAdapter | None = None
506
+ if profile.tools.mcp:
507
+ reserved_tool_names = chrys_reserved_tool_names()
508
+ reserved_tool_names.update(tool.name for tool in tools)
509
+ # A provider-hosted web tool owns its name too, so a clashing MCP tool fails
510
+ # the build with the guidance to rename or exclude it.
511
+ reserved_tool_names.update(web_tools.hosted)
512
+ mcp_adapter = MCPAdapter(
513
+ cache=self._mcp_cache,
514
+ stdio_cwd=runtime_cwd,
515
+ session_dir=self._session_dir,
516
+ reserved_tool_names=reserved_tool_names,
517
+ )
518
+ prepared.own(mcp_adapter.disconnect_all)
519
+ mcp_tools = await mcp_adapter.connect_all(profile.tools.mcp)
520
+ self._mcp_adapters[tool_name] = mcp_adapter
521
+ tools.extend(mcp_tools)
522
+ warn_if_tool_loading_unbinds_thinking(
523
+ profile, sub_active_profile, chat_options, mcp_adapter.tool_names_by_server
524
+ )
525
+
526
+ # Wire compaction + usage tracking for sub-agents. Compression
527
+ # tools (compress_context, recall_context, list_compressed_contexts)
528
+ # are disabled — sub-agents are single-turn and don't need cross-turn
529
+ # context management.
530
+ async def _on_pre_compact(info: PreCompactInfo) -> None:
531
+ await self._fire_pre_compact_hook(tool_name, info)
532
+
533
+ def _publish_context_pressure(
534
+ reason: str,
535
+ breaker: DropRoundBreakerState,
536
+ budget: int,
537
+ ) -> Awaitable[None] | None:
538
+ invocation_id = invocation_routing_key.get()
539
+ if self._bus is None:
540
+ return None
541
+ emitter = self._emitter_for(invocation_id)
542
+ return emitter.publish(
543
+ InvocationContextPressure(
544
+ origin=emitter.origin,
545
+ reason=reason,
546
+ attempts=breaker.attempts,
547
+ side_call_tokens=breaker.side_call_tokens,
548
+ side_call_token_budget=budget,
549
+ source="sub_agent",
550
+ session_id=self._session_id,
551
+ )
552
+ )
553
+
554
+ sub_log_dir: Path | None = None
555
+ if self._session_dir:
556
+ sub_log_dir = self._session_dir / "compactions" / "debug" / "sub_agents" / safe_fs_name(tool_name)
557
+
558
+ context_inputs = ContextInputs(
559
+ profile=sub_active_profile,
560
+ compaction_config=profile.compaction,
561
+ on_pre_compact=_on_pre_compact,
562
+ on_context_pressure=_publish_context_pressure,
563
+ spill_quota=self._spill_quota,
564
+ spill_record_dir=sub_agent_dropped_turn_relative_path(tool_name, "registered", 0).parent,
565
+ debug_log_dir=sub_log_dir,
566
+ include_context_management=False,
567
+ parent_session_id=self._session_id,
568
+ session_dir=self._session_dir,
569
+ skip_local_history_for_service_context=(
570
+ sub_active_profile.provider == "openai" and sub_active_profile.api_style == API_STYLE_RESPONSES
571
+ ),
572
+ service_context_default_options=chat_options,
573
+ )
574
+
575
+ # Load skills from the sub-agent's profile
576
+ shared_providers = []
577
+ progressive_resume_provider = mcp_adapter.create_resume_provider() if mcp_adapter is not None else None
578
+ from chrys.service.skills.adapter import create_skills_provider
579
+
580
+ skills_provider, skill_warnings = await create_skills_provider(
581
+ profile.skills,
582
+ runtime=runtime,
583
+ session_dir=self._session_dir,
584
+ project_skills_enabled=settings.project_skills_enabled,
585
+ )
586
+ if skills_provider is not None:
587
+ shared_providers.append(skills_provider)
588
+ if progressive_resume_provider is not None:
589
+ shared_providers.append(progressive_resume_provider)
590
+ if self._bus is not None:
591
+ for warn in skill_warnings:
592
+ await self._bus.publish(Warning(code=warn.code, message=warn.message))
593
+
594
+ # Sub-agents get their own SystemReminderMiddleware for runtime
595
+ # context injection (no usage/switch — single-turn agents).
596
+ shell_tool_enabled = any(get_tool_kind(t) == KIND_SHELL for t in tools)
597
+ reminder_inputs = ReminderInputs(
598
+ runtime=runtime,
599
+ shell_tool_enabled=shell_tool_enabled,
600
+ session_root=self._session_dir,
601
+ file_read_available=any(get_tool_kind(tool) == KIND_FILESYSTEM_READ for tool in tools),
602
+ spill_quota=self._spill_quota,
603
+ catalog_pointer_enabled=False,
604
+ skill_catalog_provider=skills_provider.render_catalog_reminder if skills_provider else None,
605
+ mcp_instructions_provider=mcp_adapter.render_instructions_reminder if mcp_adapter is not None else None,
606
+ )
607
+
608
+ # Wire Phase 4 LAST_WORDS for the sub-agent. Sub-agents run long
609
+ # tool loops (e.g. Explore searching many files) that absolutely can
610
+ # trigger Phase 4. Without this wiring the compaction strategy's
611
+ # `have_note` guard is always False and Phase 4 silently no-ops —
612
+ # leaving the sub-agent to blow past the API context limit.
613
+ #
614
+ # NOTE: LAST_WORDS generation retries transient failures internally
615
+ # against the same compaction input. If that local retry budget is
616
+ # exhausted, the controller pauses and the user decides (Retry / Abort)
617
+ # without first replaying the whole sub-agent attempt.
618
+ async def _publish_last_words_retry(info: RetryAttemptInfo, *, _tool_name=tool_name) -> None:
619
+ invocation_id = invocation_routing_key.get()
620
+ if self._bus is None or not invocation_id:
621
+ return
622
+ emitter = self._emitter_for(invocation_id)
623
+ await emitter.publish(
624
+ InvocationRetryAttempt(
625
+ scope="compaction",
626
+ origin=emitter.origin,
627
+ agent_name=self._display_names.get(_tool_name, _tool_name),
628
+ message=f"LAST_WORDS compaction: {info.reason}",
629
+ attempt=info.attempt,
630
+ max_attempts=info.max_attempts,
631
+ delay_seconds=int(info.delay_seconds),
632
+ session_id=self._session_id,
633
+ )
634
+ )
635
+
636
+ async def _publish_compaction_status(status: CompactionStatus, *, _tool_name=tool_name) -> None:
637
+ invocation_id = invocation_routing_key.get()
638
+ if self._bus is None or not invocation_id:
639
+ return
640
+ await self._publish_sub_agent_compaction_status(status, _tool_name, self._emitter_for(invocation_id))
641
+
642
+ last_words_inputs = LastWordsInputs(
643
+ profile=sub_active_profile,
644
+ template=profile.compaction.last_words_template,
645
+ max_output_tokens=profile.compaction.last_words_max_output_tokens,
646
+ log_dir=sub_log_dir,
647
+ session_id=last_words_session_id,
648
+ parent_session_id=self._session_id,
649
+ session_dir=self._session_dir,
650
+ max_transient_retries=self._max_transient_retries,
651
+ publish_retry=_publish_last_words_retry,
652
+ publish_status=_publish_compaction_status,
653
+ report_usage=self._on_side_call_usage,
654
+ )
655
+ registered = create_runtime(
656
+ conversation,
657
+ SharedRuntime(
658
+ client=client,
659
+ tools=tools,
660
+ providers=shared_providers,
661
+ name=profile.display_name or profile.name,
662
+ instructions=profile.instructions,
663
+ vision=sub_active_profile.vision,
664
+ ),
665
+ ChildRecipe(
666
+ context=context_inputs, reminder=reminder_inputs, last_words=last_words_inputs, registration=True
667
+ ),
668
+ )
669
+ ctx = registered.context
670
+ agent = registered.agent
671
+ await agent.__aenter__()
672
+ await prepared.own_or_release(lambda: agent.__aexit__(None, None, None))
673
+
674
+ per_agent_max = ref.max_concurrency
675
+ description = (
676
+ ref.tool_description
677
+ or profile.description
678
+ or f"Delegate task to {profile.display_name or profile.name}"
679
+ )
680
+
681
+ full_description = description + _execution_note(per_agent_max, self._max_total)
682
+
683
+ self._agents[tool_name] = agent
684
+ self._context_managers[tool_name] = ctx
685
+ self._chat_options_by_tool[tool_name] = chat_options
686
+ self._model_provenance_by_tool[tool_name] = model_provenance
687
+ # Use the parent's approval config so sub-agents inherit the same
688
+ # approval rules (e.g. shell: require) as the main agent.
689
+ approval_config = self._parent_approval if self._parent_approval is not None else profile.approval
690
+ self._approval_policies[tool_name] = ApprovalPolicy(approval_config, tools=tools)
691
+ self._tool_kinds[tool_name] = {t.name: k for t in tools if (k := get_tool_kind(t))}
692
+ self._display_names[tool_name] = profile.display_name or profile.name
693
+ self._hook_profile_names[tool_name] = profile.name
694
+ self._agent_active[tool_name] = 0
695
+ self._agent_max[tool_name] = per_agent_max
696
+
697
+ def _make_invocation_runtime(
698
+ owner: Conversation,
699
+ invocation_id: str,
700
+ ) -> KernelRuntime:
701
+ """Create per-invocation runtime state for registered sub-agents.
702
+
703
+ ``Agent`` instances are cheap wrappers around the shared client and
704
+ tools. Building a fresh context manager/reminder per invocation
705
+ keeps same-tool concurrent runs from sharing compaction calibration,
706
+ LAST_WORDS, or history-provider state.
707
+ """
708
+ emitter = self._emitter_for(invocation_id)
709
+ invocation_last_words_session_id = derive_llm_route_session_id(
710
+ self._session_id,
711
+ route_kind="last-words",
712
+ route_parts=(tool_name, profile.name, invocation_id),
713
+ model_profile=sub_active_profile,
714
+ )
715
+
716
+ async def _on_pre_compact(info: PreCompactInfo, *, _tool_name: str = tool_name) -> None:
717
+ await self._fire_pre_compact_hook(_tool_name, info)
718
+
719
+ def _publish_invocation_context_pressure(
720
+ reason: str,
721
+ breaker: DropRoundBreakerState,
722
+ budget: int,
723
+ ) -> Awaitable[None] | None:
724
+ if self._bus is None:
725
+ return None
726
+ return emitter.publish(
727
+ InvocationContextPressure(
728
+ origin=emitter.origin,
729
+ reason=reason,
730
+ attempts=breaker.attempts,
731
+ side_call_tokens=breaker.side_call_tokens,
732
+ side_call_token_budget=budget,
733
+ source="sub_agent",
734
+ session_id=self._session_id,
735
+ )
736
+ )
737
+
738
+ context_inputs = ContextInputs(
739
+ profile=sub_active_profile,
740
+ compaction_config=profile.compaction,
741
+ on_pre_compact=_on_pre_compact,
742
+ on_context_pressure=_publish_invocation_context_pressure,
743
+ spill_quota=self._spill_quota,
744
+ spill_record_dir=sub_agent_dropped_turn_relative_path(tool_name, invocation_id, 0).parent,
745
+ debug_log_dir=sub_log_dir,
746
+ include_context_management=False,
747
+ parent_session_id=self._session_id,
748
+ session_dir=self._session_dir,
749
+ skip_local_history_for_service_context=(
750
+ sub_active_profile.provider == "openai" and sub_active_profile.api_style == API_STYLE_RESPONSES
751
+ ),
752
+ service_context_default_options=chat_options,
753
+ )
754
+ reminder_inputs = ReminderInputs(
755
+ runtime=runtime,
756
+ shell_tool_enabled=shell_tool_enabled,
757
+ session_root=self._session_dir,
758
+ file_read_available=any(get_tool_kind(tool) == KIND_FILESYSTEM_READ for tool in tools),
759
+ spill_quota=self._spill_quota,
760
+ catalog_pointer_enabled=False,
761
+ skill_catalog_provider=skills_provider.render_catalog_reminder if skills_provider else None,
762
+ mcp_instructions_provider=sub_mcp.render_instructions_reminder
763
+ if (sub_mcp := self._mcp_adapters.get(tool_name)) is not None
764
+ else None,
765
+ )
766
+
767
+ async def _publish_last_words_retry(
768
+ info: RetryAttemptInfo,
769
+ *,
770
+ _tool_name: str = tool_name,
771
+ _invocation_id: str = invocation_id,
772
+ ) -> None:
773
+ if self._bus is None:
774
+ return
775
+ await emitter.publish(
776
+ InvocationRetryAttempt(
777
+ scope="compaction",
778
+ origin=emitter.origin,
779
+ agent_name=self._display_names.get(_tool_name, _tool_name),
780
+ message=f"LAST_WORDS compaction: {info.reason}",
781
+ attempt=info.attempt,
782
+ max_attempts=info.max_attempts,
783
+ delay_seconds=int(info.delay_seconds),
784
+ session_id=self._session_id,
785
+ )
786
+ )
787
+
788
+ async def _publish_compaction_status(
789
+ status: CompactionStatus,
790
+ *,
791
+ _tool_name: str = tool_name,
792
+ _invocation_id: str = invocation_id,
793
+ ) -> None:
794
+ if self._bus is None:
795
+ return
796
+ await self._publish_sub_agent_compaction_status(status, _tool_name, emitter)
797
+
798
+ last_words_inputs = LastWordsInputs(
799
+ profile=sub_active_profile,
800
+ template=profile.compaction.last_words_template,
801
+ max_output_tokens=profile.compaction.last_words_max_output_tokens,
802
+ log_dir=sub_log_dir,
803
+ session_id=invocation_last_words_session_id,
804
+ parent_session_id=self._session_id,
805
+ session_dir=self._session_dir,
806
+ max_transient_retries=self._max_transient_retries,
807
+ publish_retry=_publish_last_words_retry,
808
+ publish_status=_publish_compaction_status,
809
+ report_usage=self._on_side_call_usage,
810
+ )
811
+ return create_runtime(
812
+ owner,
813
+ SharedRuntime(
814
+ client=client,
815
+ tools=tools,
816
+ providers=shared_providers,
817
+ name=profile.display_name or profile.name,
818
+ instructions=profile.instructions,
819
+ vision=sub_active_profile.vision,
820
+ ),
821
+ ChildRecipe(
822
+ context=context_inputs,
823
+ reminder=reminder_inputs,
824
+ last_words=last_words_inputs,
825
+ registration=False,
826
+ ),
827
+ )
828
+
829
+ web_tools.check_names(tools)
830
+
831
+ fn_tool = self._make_tool(
832
+ tool_name,
833
+ full_description,
834
+ prepared=prepared,
835
+ runtime_factory=_make_invocation_runtime,
836
+ begin_web_pass=web_tools.begin_pass,
837
+ stream=sub_active_profile.stream,
838
+ stream_attempt_timeout=sub_active_profile.http_read_timeout,
839
+ skills_provider=skills_provider,
840
+ capture_service_loop_messages=capture_service_loop_messages,
841
+ sub_agent_session_id=sub_agent_session_id,
842
+ profile=profile,
843
+ active_profile=sub_active_profile,
844
+ model_provenance=model_provenance,
845
+ )
846
+ self._tools.append(fn_tool)
847
+
848
+ logger.debug("Registered sub-agent tool: %s (profile=%s, max=%d)", tool_name, profile.name, per_agent_max)
849
+
850
+ try:
851
+ await prepared.open(_acquire)
852
+ except BaseException:
853
+ await rollback_resources(prepared)
854
+ raise
855
+ self._prepared_by_tool[ref.tool_name or profile.name] = prepared
856
+
857
+ async def _refresh_skill_catalog(self, provider: ChrysSkillsProvider, tool_name: str) -> None:
858
+ """Refresh a sub-agent's runtime skills and publish non-fatal warnings."""
859
+ try:
860
+ skill_warnings = await provider.refresh_context()
861
+ except Exception:
862
+ logger.exception("Failed to refresh runtime skills for sub-agent '%s'", tool_name)
863
+ return
864
+ if self._bus is None:
865
+ return
866
+ for warn in skill_warnings:
867
+ await self._bus.publish(Warning(code=warn.code, message=warn.message, session_id=self._session_id))
868
+
869
+ async def register_acp(
870
+ self,
871
+ ref: SubAgentRef,
872
+ profile: AgentProfile,
873
+ runtime: SessionEnvironment,
874
+ ) -> None:
875
+ """Register an external ACP profile without constructing kernel machinery."""
876
+ if profile.acp is None:
877
+ raise ValueError("register_acp requires an ACP agent profile")
878
+ self._runtime = runtime
879
+ if not self._workspace_cwd:
880
+ self._workspace_cwd = runtime.cwd
881
+ tool_name = ref.tool_name or profile.name
882
+ per_agent_max = ref.max_concurrency
883
+ description = (
884
+ ref.tool_description or profile.description or f"Delegate task to {profile.display_name or profile.name}"
885
+ )
886
+ description += _execution_note(per_agent_max, self._max_total)
887
+ self._acp_profiles[tool_name] = profile
888
+ self._display_names[tool_name] = profile.display_name or profile.name
889
+ self._hook_profile_names[tool_name] = profile.name
890
+ self._agent_active[tool_name] = 0
891
+ self._agent_max[tool_name] = per_agent_max
892
+ self._prepared_by_tool[tool_name] = PreparedAgent()
893
+ self._tools.append(self._make_acp_tool(tool_name, description, profile, runtime))
894
+ logger.debug("Registered ACP sub-agent tool: %s (profile=%s, max=%d)", tool_name, profile.name, per_agent_max)
895
+
896
+ def _make_acp_tool(
897
+ self,
898
+ tool_name: str,
899
+ description: str,
900
+ profile: AgentProfile,
901
+ runtime: SessionEnvironment,
902
+ ) -> FunctionTool:
903
+ async def _invoke_acp(prompt: str) -> str:
904
+ if self._total_active >= self._max_total:
905
+ return tool_error(
906
+ "sub_agent_concurrency_limit",
907
+ (
908
+ f"total sub-agent concurrency limit reached ({self._total_active}/{self._max_total} active). "
909
+ "Wait for a running sub-agent to complete before starting another."
910
+ ),
911
+ retryable=True,
912
+ details={"active": self._total_active, "limit": self._max_total},
913
+ )
914
+ per_max = self._agent_max.get(tool_name, DEFAULT_SUB_AGENT_CONCURRENCY)
915
+ per_active = self._agent_active.get(tool_name, 0)
916
+ if per_active >= per_max:
917
+ return tool_error(
918
+ "sub_agent_concurrency_limit",
919
+ (
920
+ f"concurrency limit for '{tool_name}' reached ({per_active}/{per_max} active). "
921
+ "Wait for a running call to complete before starting another."
922
+ ),
923
+ retryable=True,
924
+ details={"tool_name": tool_name, "active": per_active, "limit": per_max},
925
+ )
926
+
927
+ invocation_id = uuid4().hex[:12]
928
+ origin = InvocationOrigin(
929
+ "sub_agent", self._session_id or "", invocation_id, current_invocation_origin.get()
930
+ )
931
+ emitter = BoundEmitter(self._bus, origin)
932
+ self._invocation_emitters[invocation_id] = emitter
933
+ invocation_token = invocation_routing_key.set(invocation_id)
934
+ parent_metadata = sub_agent_parent_result_metadata.get()
935
+ parent_event_call_id = ""
936
+ parent_provider_call_id = ""
937
+ if parent_metadata is not None:
938
+ parent_event_call_id = parent_metadata.parent_event_call_id
939
+ parent_provider_call_id = parent_metadata.parent_provider_call_id
940
+ parent_metadata.sub_agent_invocation_id = invocation_id
941
+ else:
942
+ parent_event_call_id = sub_agent_parent_call_id.get()
943
+ hook_status = "failed"
944
+ hook_summary = ""
945
+ # ``boundary_only`` excludes the external ACP process's own model
946
+ # runs and tools. Parent-recorded approvals, input waits, and
947
+ # connection retries still appear under the child actor.
948
+ sub_agent_trace = SubAgentTrace.open(
949
+ invocation_id=invocation_id, trace_coverage=TraceCoverage.BOUNDARY_ONLY
950
+ )
951
+ sub_agent_failure_code: str | None = None
952
+ hook_started = False
953
+ controller: SubAgentToolShell | None = None
954
+ policy: AcpSubAgentPolicy | None = None
955
+ broker: AcpPermissionBroker | None = None
956
+ log_stats = SubAgentLogStats()
957
+ log_writer: SubAgentSessionLogWriter | None = None
958
+ log_finalized = False
959
+ attempts: list[dict[str, Any]] = []
960
+ # Bounded audit-update trail instead of retaining every attempt's
961
+ # translator: each translator holds up to ~5 MiB of assembled content
962
+ # and thousands of call records, and manual retries are unlimited.
963
+ # Only the newest entries survive the write-time envelope ring
964
+ # (``normalize_acp_state``), so folding finished attempts down to
965
+ # their ring-bounded updates loses nothing the audit would keep.
966
+ audit_updates: list[Any] = []
967
+ active_translator: Any = None
968
+ last_agent_info: Any = None
969
+ stderr_path: Path | None = None
970
+ operation = OperationLifetime()
971
+ conversation: Conversation | None = None
972
+ unbind: Unbind | None = None
973
+ controller = SubAgentToolShell(
974
+ origin=origin,
975
+ tool_name=tool_name,
976
+ agent_name=self._display_names[tool_name],
977
+ event_bus=self._bus,
978
+ operation=operation,
979
+ human_failure_decisions=self._human_failure_decisions,
980
+ )
981
+
982
+ def _audit_update_trail() -> list[Any]:
983
+ trail = list(audit_updates)
984
+ if active_translator is not None:
985
+ trail.extend(active_translator.translated_updates)
986
+ # Same item cap as the write-time envelope ring; the byte
987
+ # bound is applied by ``normalize_acp_state`` on write.
988
+ return trail[-_ACP_AUDIT_TRAIL_ITEMS:]
989
+
990
+ def _record_terminal_audit(written: bool) -> None:
991
+ nonlocal log_finalized
992
+ log_finalized = written
993
+ if written and parent_metadata is not None and log_writer is not None:
994
+ parent_metadata.sub_agent_log_file = log_writer.log_file_name
995
+ parent_metadata.sub_agent_audit_complete = True
996
+ _upgrade_interrupted_parent_result(parent_metadata)
997
+
998
+ try:
999
+ # Inside the block whose ``finally`` releases them: the started
1000
+ # record awaits a write ack, and an interrupt there must not
1001
+ # leave an unfinished boundary or a counter nobody gives back.
1002
+ self._total_active += 1
1003
+ self._agent_active[tool_name] = per_active + 1
1004
+
1005
+ async def _open_acp(owner: Conversation) -> Conversation:
1006
+ return owner
1007
+
1008
+ conversation = await self._prepared_by_tool[tool_name].open(_open_acp)
1009
+ unbind = conversation.bind_operation(controller)
1010
+ if sub_agent_trace is not None:
1011
+ await sub_agent_trace.started(tool_name=tool_name, agent_profile=profile.name)
1012
+ config = profile.acp
1013
+ if config is None:
1014
+ raise RuntimeError("An ACP sub-agent requires ACP configuration.")
1015
+ if self._session_dir is not None:
1016
+ log_writer = SubAgentSessionLogWriter(
1017
+ parent_session_dir=self._session_dir,
1018
+ parent_session_id=self._session_id,
1019
+ invocation_id=invocation_id,
1020
+ tool_name=tool_name,
1021
+ agent_profile=profile.name,
1022
+ agent_display_name=self._display_names[tool_name],
1023
+ prompt=prompt,
1024
+ parent_provider_call_id=parent_provider_call_id,
1025
+ parent_event_call_id=parent_event_call_id,
1026
+ primary_cwd=self._workspace_cwd,
1027
+ working_dirs=list(
1028
+ self._workspace_roots or ([self._workspace_cwd] if self._workspace_cwd else [])
1029
+ ),
1030
+ stats=log_stats,
1031
+ runner="acp",
1032
+ acp_command=config.command,
1033
+ acp_args=config.args,
1034
+ )
1035
+ stderr_path = _acp_stderr_path(self._session_dir, log_writer, invocation_id)
1036
+ try:
1037
+ initial_spec = resolve_acp_spec(
1038
+ config, runtime, stderr_path, workspace_roots=self._workspace_roots
1039
+ )
1040
+ except AcpConfigError:
1041
+ pass
1042
+ else:
1043
+ log_writer.set_acp_launch(initial_spec.command, list(initial_spec.args))
1044
+ initial_log_written = await log_writer.write(
1045
+ status="running",
1046
+ acp_state={
1047
+ "agent_info": None,
1048
+ "attempts": [],
1049
+ "stop_reason": "",
1050
+ "translated_updates": [],
1051
+ },
1052
+ )
1053
+ if initial_log_written and parent_metadata is not None:
1054
+ parent_metadata.sub_agent_log_file = log_writer.log_file_name
1055
+ if stderr_path is None:
1056
+ stderr_path = _acp_stderr_path(self._session_dir, log_writer, invocation_id)
1057
+ if self._bus is not None:
1058
+ await emitter.publish(
1059
+ InvocationStarted(
1060
+ origin=emitter.origin,
1061
+ agent_name=self._display_names[tool_name],
1062
+ tool_name=tool_name,
1063
+ parent_call_id=parent_event_call_id,
1064
+ sub_agent_log_file=log_writer.log_file_name if log_writer is not None else "",
1065
+ session_id=self._session_id,
1066
+ )
1067
+ )
1068
+ await self._fire_sub_agent_hook(
1069
+ HookEvent.SUB_AGENT_START,
1070
+ tool_name=tool_name,
1071
+ invocation_id=invocation_id,
1072
+ parent_call_id=parent_event_call_id,
1073
+ parent_provider_call_id=parent_provider_call_id,
1074
+ target_operation_id=sub_agent_trace.operation_id if sub_agent_trace is not None else None,
1075
+ )
1076
+ hook_started = True
1077
+ if self._bus is None:
1078
+ raise RuntimeError("ACP sub-agents require an EventBus")
1079
+ broker = AcpPermissionBroker(
1080
+ event_bus=self._bus,
1081
+ session_id=self._session_id,
1082
+ caller_name=self._display_names[tool_name],
1083
+ mode_getter=lambda: self._approval_mode,
1084
+ turn_context=self._turn_context,
1085
+ workspace_roots=self._workspace_roots,
1086
+ workspace_cwd=self._workspace_cwd,
1087
+ approval_judge=self._approval_judge,
1088
+ ask_user_timeout_seconds=self._ask_user_timeout_seconds,
1089
+ allow_user_interaction=self._allow_user_interaction,
1090
+ trajectory_context=sub_agent_trace.child_context if sub_agent_trace is not None else None,
1091
+ trajectory_boundary_operation_id=(
1092
+ sub_agent_trace.operation_id if sub_agent_trace is not None else None
1093
+ ),
1094
+ )
1095
+
1096
+ async def _adopt_translator(translator: Any) -> None:
1097
+ # Fires from the controller BEFORE the attempt's transport
1098
+ # exists: a permission decision recorded during
1099
+ # open_session (ACP allows requests the moment session/new
1100
+ # is announced) must already ride _audit_update_trail()
1101
+ # when a pre-handshake failure writes the pause audit.
1102
+ nonlocal active_translator
1103
+ if active_translator is not None and active_translator is not translator:
1104
+ audit_updates.extend(active_translator.translated_updates)
1105
+ del audit_updates[:-_ACP_AUDIT_TRAIL_ITEMS]
1106
+ active_translator = translator
1107
+
1108
+ async def _record_attempt(attempt: int, handshake: Any, translator: Any) -> None:
1109
+ nonlocal last_agent_info
1110
+ attempts.append({"attempt": attempt, "acp_session_id": handshake.session_id})
1111
+ del attempts[:-_ACP_AUDIT_ATTEMPT_ITEMS]
1112
+ await _adopt_translator(translator)
1113
+ if log_writer is not None:
1114
+ if policy is not None and policy.backend.last_spec is not None:
1115
+ log_writer.set_acp_launch(
1116
+ policy.backend.last_spec.command,
1117
+ list(policy.backend.last_spec.args),
1118
+ )
1119
+ last_agent_info = (
1120
+ handshake.agent_info.model_dump(mode="json", by_alias=True)
1121
+ if handshake.agent_info is not None
1122
+ else None
1123
+ )
1124
+ await log_writer.write(
1125
+ status="running",
1126
+ acp_state={
1127
+ "agent_info": last_agent_info,
1128
+ "attempts": attempts,
1129
+ "stop_reason": "",
1130
+ "translated_updates": _audit_update_trail(),
1131
+ "stderr_log_path": str(stderr_path),
1132
+ },
1133
+ )
1134
+
1135
+ async def _record_pause() -> None:
1136
+ if log_writer is None or not log_writer.active:
1137
+ return
1138
+ ctrl = policy
1139
+ # The pause fires mid-run(): sync spend/tool counters from
1140
+ # the live controller exactly like the terminal writes do —
1141
+ # a crash while paused must not durably record zero
1142
+ # counters for attempts the parent was already charged for.
1143
+ if ctrl is not None:
1144
+ log_stats.tool_call_count = ctrl.backend.completed_tool_calls
1145
+ log_stats.total_tokens = ctrl.backend.latest_context_tokens
1146
+ log_stats.total_usage_tokens = ctrl.backend.total_usage_tokens
1147
+ log_stats.usage_unreported_attempts = ctrl.backend.usage_unreported_attempts
1148
+ if ctrl.backend.last_spec is not None:
1149
+ log_writer.set_acp_launch(ctrl.backend.last_spec.command, list(ctrl.backend.last_spec.args))
1150
+ await log_writer.write(
1151
+ status="paused",
1152
+ failure_reason=SubAgentFailureReason.ACP_TRANSPORT.value,
1153
+ last_error=ctrl.backend.last_error if ctrl is not None else "",
1154
+ acp_state={
1155
+ "agent_info": last_agent_info,
1156
+ "attempts": attempts,
1157
+ "stop_reason": ctrl.backend.stop_reason if ctrl is not None else "",
1158
+ "translated_updates": _audit_update_trail(),
1159
+ "stderr_log_path": str(stderr_path),
1160
+ "usage_unreported_attempts": log_stats.usage_unreported_attempts,
1161
+ },
1162
+ )
1163
+
1164
+ def _commit_parent_interruption() -> None:
1165
+ _commit_interrupted_parent_result(parent_metadata)
1166
+
1167
+ async def _finalize_cancellation() -> None:
1168
+ nonlocal log_finalized
1169
+ ctrl = policy
1170
+ if ctrl is not None:
1171
+ log_stats.tool_call_count = ctrl.backend.completed_tool_calls
1172
+ log_stats.total_tokens = ctrl.backend.latest_context_tokens
1173
+ log_stats.total_usage_tokens = ctrl.backend.total_usage_tokens
1174
+ log_stats.usage_unreported_attempts = ctrl.backend.usage_unreported_attempts
1175
+ if log_writer is None:
1176
+ return
1177
+ if not log_finalized:
1178
+ terminal_written = await log_writer.write(
1179
+ status="cancelled",
1180
+ result="cancelled",
1181
+ last_error="cancelled",
1182
+ ended=True,
1183
+ acp_state={
1184
+ "agent_info": last_agent_info,
1185
+ "attempts": attempts,
1186
+ "stop_reason": ctrl.backend.stop_reason if ctrl is not None else "",
1187
+ "translated_updates": _audit_update_trail(),
1188
+ "stderr_log_path": str(stderr_path),
1189
+ "usage_unreported_attempts": log_stats.usage_unreported_attempts,
1190
+ },
1191
+ )
1192
+ _record_terminal_audit(terminal_written)
1193
+
1194
+ if controller is None:
1195
+ raise RuntimeError("The sub-agent invocation has no controller.")
1196
+ controller.bind_parent_interrupt_commit(_commit_parent_interruption)
1197
+ policy = AcpSubAgentPolicy(
1198
+ shell=controller,
1199
+ prompt=prompt,
1200
+ spec_factory=lambda _attempt: resolve_acp_spec(
1201
+ config, runtime, stderr_path, workspace_roots=self._workspace_roots
1202
+ ),
1203
+ broker=broker,
1204
+ result_mode=config.result_mode,
1205
+ usage_callback=self._on_sub_agent_usage,
1206
+ attempt_callback=_record_attempt,
1207
+ translator_callback=_adopt_translator,
1208
+ pause_callback=_record_pause,
1209
+ persist_dir=self._pending_dir(),
1210
+ parent_provider_call_id=parent_provider_call_id,
1211
+ parent_event_call_id=parent_event_call_id,
1212
+ sub_agent_log_file=(
1213
+ log_writer.log_file_name if log_writer is not None and log_writer.active else ""
1214
+ ),
1215
+ pending_record_finalizer=self.queue_pending_cleanup,
1216
+ cancellation_finalizer=_finalize_cancellation,
1217
+ trajectory_context=sub_agent_trace.child_context if sub_agent_trace is not None else None,
1218
+ trajectory_boundary_operation_id=(
1219
+ sub_agent_trace.operation_id if sub_agent_trace is not None else None
1220
+ ),
1221
+ )
1222
+ controller.attach_policy(policy)
1223
+ self._controllers[invocation_id] = controller
1224
+ result = await controller.run()
1225
+ if parent_metadata is not None and policy.backend.transcript_final_text is not None:
1226
+ parent_metadata.transcript_final_text = policy.backend.transcript_final_text
1227
+ log_stats.tool_call_count = policy.backend.completed_tool_calls
1228
+ log_stats.total_tokens = policy.backend.latest_context_tokens
1229
+ log_stats.total_usage_tokens = policy.backend.total_usage_tokens
1230
+ log_stats.usage_unreported_attempts = policy.backend.usage_unreported_attempts
1231
+ hook_status = "failed" if result.startswith("Error:") else "ok"
1232
+ hook_summary = result[:500]
1233
+ if hook_status == "failed":
1234
+ sub_agent_failure_code = "tool_error"
1235
+ if log_writer is not None:
1236
+ if policy.backend.last_spec is not None:
1237
+ log_writer.set_acp_launch(
1238
+ policy.backend.last_spec.command,
1239
+ list(policy.backend.last_spec.args),
1240
+ )
1241
+ terminal_written = await log_writer.write(
1242
+ status="completed" if hook_status == "ok" else "failed",
1243
+ result=result,
1244
+ ended=True,
1245
+ acp_state={
1246
+ "agent_info": last_agent_info,
1247
+ "attempts": attempts,
1248
+ "successful_attempt": policy.backend.transport_ordinal if hook_status == "ok" else None,
1249
+ "stop_reason": policy.backend.stop_reason,
1250
+ "translated_updates": _audit_update_trail(),
1251
+ "stderr_log_path": str(stderr_path),
1252
+ "usage_unreported_attempts": log_stats.usage_unreported_attempts,
1253
+ },
1254
+ )
1255
+ _record_terminal_audit(terminal_written)
1256
+ return result
1257
+ except asyncio.CancelledError as exc:
1258
+ hook_status = "cancelled"
1259
+ hook_summary = "cancelled"
1260
+ _commit_interrupted_parent_result(parent_metadata)
1261
+ if policy is not None:
1262
+ if controller is None:
1263
+ raise RuntimeError("The sub-agent invocation has no controller.") from exc
1264
+ await _finish_cancellation_cleanup(controller.finalize_cancellation())
1265
+ raise
1266
+ except BaseException as exc:
1267
+ hook_status = "failed"
1268
+ hook_summary = str(exc)[:500]
1269
+ sub_agent_failure_code = "setup_failed" if policy is None else "exception"
1270
+ raise
1271
+ finally:
1272
+ operation.begin_cleanup()
1273
+ cleanup_cancelled = False
1274
+ cleanup_error: Exception | None = None
1275
+ _close_sub_agent_trace(
1276
+ sub_agent_trace,
1277
+ hook_status=hook_status,
1278
+ failure_code=sub_agent_failure_code,
1279
+ controller=controller if policy is not None else None,
1280
+ )
1281
+ if broker is not None:
1282
+ try:
1283
+ if policy is not None:
1284
+ await policy.backend.aclose()
1285
+ else:
1286
+ await broker.close()
1287
+ except asyncio.CancelledError:
1288
+ cleanup_cancelled = True
1289
+ except Exception as exc:
1290
+ cleanup_error = exc
1291
+ if policy is not None:
1292
+ log_stats.tool_call_count = policy.backend.completed_tool_calls
1293
+ log_stats.total_tokens = policy.backend.latest_context_tokens
1294
+ log_stats.total_usage_tokens = policy.backend.total_usage_tokens
1295
+ log_stats.usage_unreported_attempts = policy.backend.usage_unreported_attempts
1296
+ if log_writer is not None and not log_finalized:
1297
+ try:
1298
+ if policy is not None and policy.backend.last_spec is not None:
1299
+ log_writer.set_acp_launch(
1300
+ policy.backend.last_spec.command,
1301
+ list(policy.backend.last_spec.args),
1302
+ )
1303
+ terminal_written = await log_writer.write(
1304
+ status="cancelled" if hook_status == "cancelled" else "failed",
1305
+ result=hook_summary,
1306
+ ended=True,
1307
+ acp_state={
1308
+ "agent_info": last_agent_info,
1309
+ "attempts": attempts,
1310
+ "stop_reason": policy.backend.stop_reason if policy is not None else "",
1311
+ "translated_updates": _audit_update_trail(),
1312
+ "stderr_log_path": str(stderr_path) if stderr_path is not None else "",
1313
+ "usage_unreported_attempts": log_stats.usage_unreported_attempts,
1314
+ },
1315
+ )
1316
+ _record_terminal_audit(terminal_written)
1317
+ except asyncio.CancelledError:
1318
+ cleanup_cancelled = True
1319
+ except Exception as exc:
1320
+ cleanup_error = cleanup_error or exc
1321
+ if self._drain_parent_usage_publishes is not None:
1322
+ try:
1323
+ await self._drain_parent_usage_publishes()
1324
+ except asyncio.CancelledError:
1325
+ cleanup_cancelled = True
1326
+ except Exception as exc:
1327
+ cleanup_error = cleanup_error or exc
1328
+ if hook_started:
1329
+ try:
1330
+ await self._fire_sub_agent_hook(
1331
+ HookEvent.SUB_AGENT_END,
1332
+ tool_name=tool_name,
1333
+ invocation_id=invocation_id,
1334
+ parent_call_id=parent_event_call_id,
1335
+ parent_provider_call_id=parent_provider_call_id,
1336
+ status=hook_status,
1337
+ result_summary=hook_summary,
1338
+ target_operation_id=(sub_agent_trace.operation_id if sub_agent_trace is not None else None),
1339
+ )
1340
+ except asyncio.CancelledError:
1341
+ cleanup_cancelled = True
1342
+ except Exception:
1343
+ logger.exception("Failed to fire ACP sub-agent end hook")
1344
+ self._controllers.pop(invocation_id, None)
1345
+ self._invocation_emitters.pop(invocation_id, None)
1346
+ self._total_active -= 1
1347
+ self._agent_active[tool_name] -= 1
1348
+ invocation_routing_key.reset(invocation_token)
1349
+ if unbind is not None:
1350
+ unbind()
1351
+ operation.finish()
1352
+ if conversation is not None:
1353
+ try:
1354
+ await conversation.aclose()
1355
+ except asyncio.CancelledError:
1356
+ cleanup_cancelled = True
1357
+ if cleanup_cancelled:
1358
+ raise asyncio.CancelledError
1359
+ if cleanup_error is not None:
1360
+ raise cleanup_error
1361
+
1362
+ tool = FunctionTool(func=_invoke_acp, name=tool_name, description=description)
1363
+ set_tool_kind(tool, KIND_SUB_AGENT)
1364
+ return tool
1365
+
1366
+ def _make_tool(
1367
+ self,
1368
+ tool_name: str,
1369
+ description: str,
1370
+ *,
1371
+ prepared: PreparedAgent,
1372
+ runtime_factory: Callable[[Conversation, str], KernelRuntime],
1373
+ stream: bool,
1374
+ stream_attempt_timeout: float,
1375
+ skills_provider: ChrysSkillsProvider | None = None,
1376
+ capture_service_loop_messages: bool = False,
1377
+ profile: AgentProfile | None = None,
1378
+ active_profile: ModelProfile | None = None,
1379
+ sub_agent_session_id: str | None = None,
1380
+ model_provenance: SubAgentModelProvenance | None = None,
1381
+ begin_web_pass: Callable[[], None] | None = None,
1382
+ ) -> FunctionTool:
1383
+ """Create a FunctionTool that invokes the sub-agent with concurrency control."""
1384
+
1385
+ def _route_profile_name() -> str:
1386
+ return profile.name if profile is not None else self._hook_profile_names.get(tool_name, tool_name)
1387
+
1388
+ def _derive_invocation_session_id(invocation_id: str) -> str | None:
1389
+ if active_profile is None:
1390
+ return sub_agent_session_id
1391
+ return derive_llm_route_session_id(
1392
+ self._session_id,
1393
+ route_kind="sub-agent",
1394
+ route_parts=(tool_name, _route_profile_name(), invocation_id),
1395
+ model_profile=active_profile,
1396
+ )
1397
+
1398
+ async def _invoke(prompt: str) -> str:
1399
+ # Global concurrency guard
1400
+ if self._total_active >= self._max_total:
1401
+ return tool_error(
1402
+ "sub_agent_concurrency_limit",
1403
+ (
1404
+ f"total sub-agent concurrency limit reached ({self._total_active}/{self._max_total} active). "
1405
+ "Wait for a running sub-agent to complete before starting another."
1406
+ ),
1407
+ retryable=True,
1408
+ details={"active": self._total_active, "limit": self._max_total},
1409
+ )
1410
+
1411
+ # Per-agent concurrency guard
1412
+ per_max = self._agent_max.get(tool_name, DEFAULT_SUB_AGENT_CONCURRENCY)
1413
+ per_active = self._agent_active.get(tool_name, 0)
1414
+ if per_active >= per_max:
1415
+ return tool_error(
1416
+ "sub_agent_concurrency_limit",
1417
+ (
1418
+ f"concurrency limit for '{tool_name}' reached ({per_active}/{per_max} active). "
1419
+ "Wait for a running call to complete before starting another."
1420
+ ),
1421
+ retryable=True,
1422
+ details={"tool_name": tool_name, "active": per_active, "limit": per_max},
1423
+ )
1424
+
1425
+ invocation_id = ""
1426
+ invocation_token: Token[str] | None = None
1427
+ route_session_token: Token[str] | None = None
1428
+ parent_session_token: Token[str] | None = None
1429
+ total_counter_incremented = False
1430
+ agent_counter_incremented = False
1431
+ parent_event_call_id = ""
1432
+ parent_provider_call_id = ""
1433
+ parent_call_id = ""
1434
+ hook_status = "failed"
1435
+ hook_result_summary = ""
1436
+ sub_agent_hook_started = False
1437
+ approval_mw: ApprovalMiddleware | None = None
1438
+ controller: SubAgentToolShell | None = None
1439
+ policy: KernelSubAgentPolicy | None = None
1440
+ sub_event_mw: SubAgentEventMiddleware | None = None
1441
+ sleep_mw: SleepMiddleware | None = None
1442
+ log_stats = SubAgentLogStats()
1443
+ log_writer: SubAgentSessionLogWriter | None = None
1444
+ parent_result_metadata = None
1445
+ sub_agent_trace: SubAgentTrace | None = None
1446
+ sub_agent_failure_code: str | None = None
1447
+ conversation: Conversation | None = None
1448
+ operation = OperationLifetime()
1449
+ unbind: Unbind | None = None
1450
+ try:
1451
+ self._total_active += 1
1452
+ total_counter_incremented = True
1453
+ self._agent_active[tool_name] = per_active + 1
1454
+ agent_counter_incremented = True
1455
+ invocation_id = uuid4().hex[:12]
1456
+ origin = InvocationOrigin(
1457
+ "sub_agent", self._session_id or "", invocation_id, current_invocation_origin.get()
1458
+ )
1459
+ emitter = BoundEmitter(self._bus, origin)
1460
+ self._invocation_emitters[invocation_id] = emitter
1461
+ intermediate_buffer = IntermediateTextBuffer()
1462
+ self._intermediate_buffers[invocation_id] = intermediate_buffer
1463
+ invocation_session_id = _derive_invocation_session_id(invocation_id)
1464
+ invocation_token = invocation_routing_key.set(invocation_id)
1465
+ # In-process: the child's events inline into this writer under
1466
+ # the child's actor, hung off this boundary.
1467
+ sub_agent_trace = SubAgentTrace.open(invocation_id=invocation_id, trace_coverage=TraceCoverage.FULL)
1468
+
1469
+ async def _open_operation(owner: Conversation) -> Conversation:
1470
+ return owner
1471
+
1472
+ controller = SubAgentToolShell(
1473
+ origin=origin,
1474
+ tool_name=tool_name,
1475
+ agent_name=self._display_names.get(tool_name, tool_name),
1476
+ event_bus=self._bus,
1477
+ operation=operation,
1478
+ human_failure_decisions=self._human_failure_decisions,
1479
+ )
1480
+ conversation = await prepared.open(_open_operation)
1481
+ unbind = conversation.bind_operation(controller)
1482
+ if sub_agent_trace is not None:
1483
+ await sub_agent_trace.started(
1484
+ tool_name=tool_name, agent_profile=self._hook_profile_names.get(tool_name, tool_name)
1485
+ )
1486
+ route_session_token = (
1487
+ llm_route_session_id.set(invocation_session_id) if invocation_session_id is not None else None
1488
+ )
1489
+ parent_session_token = llm_parent_session_id.set(self._session_id or "")
1490
+ parent_result_metadata = sub_agent_parent_result_metadata.get()
1491
+ if parent_result_metadata is not None:
1492
+ parent_event_call_id = parent_result_metadata.parent_event_call_id
1493
+ parent_provider_call_id = parent_result_metadata.parent_provider_call_id
1494
+ parent_result_metadata.sub_agent_invocation_id = invocation_id
1495
+ else:
1496
+ parent_event_call_id = sub_agent_parent_call_id.get()
1497
+ parent_call_id = parent_event_call_id
1498
+ if self._session_dir is not None:
1499
+ log_writer = SubAgentSessionLogWriter(
1500
+ parent_session_dir=self._session_dir,
1501
+ parent_session_id=self._session_id,
1502
+ invocation_id=invocation_id,
1503
+ tool_name=tool_name,
1504
+ agent_profile=self._hook_profile_names.get(tool_name, tool_name),
1505
+ agent_display_name=self._display_names.get(tool_name, tool_name),
1506
+ prompt=prompt,
1507
+ parent_provider_call_id=parent_provider_call_id,
1508
+ parent_event_call_id=parent_event_call_id,
1509
+ model=model_provenance,
1510
+ primary_cwd=self._workspace_cwd,
1511
+ working_dirs=list(
1512
+ self._workspace_roots or ([self._workspace_cwd] if self._workspace_cwd else [])
1513
+ ),
1514
+ stats=log_stats,
1515
+ )
1516
+ if log_writer is not None:
1517
+ initial_log_written = await log_writer.write(status="running")
1518
+ if initial_log_written and parent_result_metadata is not None:
1519
+ parent_result_metadata.sub_agent_log_file = log_writer.log_file_name
1520
+ # Sub-agents are single-turn and do not carry parent usage
1521
+ # or profile-switch context; this snapshots runtime only.
1522
+ if skills_provider is not None:
1523
+ await self._refresh_skill_catalog(skills_provider, tool_name)
1524
+
1525
+ invocation_runtime = runtime_factory(conversation, invocation_id)
1526
+ invocation_agent = invocation_runtime.agent
1527
+ invocation_ctx = invocation_runtime.context
1528
+ invocation_reminder = invocation_runtime.reminder
1529
+ invocation_reminder.prepare_turn()
1530
+ # Publish the invocation-start event BEFORE any LLM call so
1531
+ # the TUI links this invocation_id to the parent tool call's
1532
+ # widget deterministically — even if the first LLM call
1533
+ # fails immediately (e.g. 529 overloaded) and no inner tool
1534
+ # call arrives for a while. See
1535
+ # :class:`InvocationStarted` docstring.
1536
+ if self._bus is not None:
1537
+ await emitter.publish(
1538
+ InvocationStarted(
1539
+ origin=emitter.origin,
1540
+ agent_name=self._display_names.get(tool_name, tool_name),
1541
+ tool_name=tool_name,
1542
+ parent_call_id=parent_call_id,
1543
+ sub_agent_log_file=log_writer.log_file_name if log_writer is not None else "",
1544
+ session_id=self._session_id,
1545
+ )
1546
+ )
1547
+ await self._fire_sub_agent_hook(
1548
+ HookEvent.SUB_AGENT_START,
1549
+ tool_name=tool_name,
1550
+ invocation_id=invocation_id,
1551
+ parent_call_id=parent_call_id,
1552
+ parent_provider_call_id=parent_provider_call_id,
1553
+ target_operation_id=sub_agent_trace.operation_id if sub_agent_trace is not None else None,
1554
+ )
1555
+ sub_agent_hook_started = True
1556
+
1557
+ middleware = []
1558
+ loop_recorder = LoopRecorder(
1559
+ capture_service_loop_messages=capture_service_loop_messages,
1560
+ message_hasher=serialized_message_payload,
1561
+ )
1562
+ if self._bus is not None:
1563
+ sub_event_mw = SubAgentEventMiddleware(
1564
+ self._bus,
1565
+ self._display_names.get(tool_name, tool_name),
1566
+ invocation_id,
1567
+ origin=origin,
1568
+ mutation_tracker=self._mutation_tracker,
1569
+ mutation_coordinator=self._mutation_coordinator,
1570
+ hook_manager=self._hook_manager,
1571
+ profile_name=self._hook_profile_names.get(tool_name, tool_name),
1572
+ session_id=self._session_id,
1573
+ workspace_cwd=self._workspace_cwd,
1574
+ serialize_implicit_windows=self._serialize_implicit_windows,
1575
+ stats=log_stats,
1576
+ tool_result_ceiling_tokens=self._tool_result_ceiling_tokens,
1577
+ intermediate_buffer=intermediate_buffer,
1578
+ )
1579
+ middleware.append(sub_event_mw)
1580
+ # AskUser middleware — sub-agent ask_user calls show the
1581
+ # same dialog as the main agent via the shared EventBus.
1582
+ middleware.append(
1583
+ create_ask_user(
1584
+ conversation,
1585
+ AskUserInputs(
1586
+ event_bus=self._bus,
1587
+ session_id=self._session_id,
1588
+ caller_name=self._display_names.get(tool_name, tool_name),
1589
+ timeout_seconds=self._ask_user_timeout_seconds,
1590
+ ),
1591
+ )
1592
+ )
1593
+ # Approval middleware — gates sub-agent tool calls with
1594
+ # user approval via the same EventBus flow as the main agent.
1595
+ # Reads current approval_mode/judge off self so mid-session
1596
+ # mode switches (MANUAL ↔ AUTO ↔ BYPASS) take effect on the
1597
+ # next sub-agent invocation.
1598
+ approval_policy = self._approval_policies.get(tool_name)
1599
+ if approval_policy is not None:
1600
+ approval_mw = await create_approval(
1601
+ conversation,
1602
+ ApprovalInputs(
1603
+ approval_policy=approval_policy,
1604
+ event_bus=self._bus,
1605
+ session_id=self._session_id,
1606
+ tool_kinds=self._tool_kinds.get(tool_name),
1607
+ caller_name=self._display_names.get(tool_name, tool_name),
1608
+ workspace_roots=self._workspace_roots,
1609
+ approval_mode=self._approval_mode,
1610
+ approval_judge=self._approval_judge,
1611
+ approval_log_dir=self._approval_log_dir,
1612
+ workspace_cwd=self._workspace_cwd,
1613
+ # Developer-mode handoff review is scoped to main-agent → sub-agent delegation.
1614
+ dev_mode=False,
1615
+ hook_manager=self._hook_manager,
1616
+ profile_name=self._hook_profile_names.get(tool_name, tool_name),
1617
+ session_archive_read_roots=(
1618
+ [self._session_dir / COMPACTIONS_DIR_NAME] if self._session_dir else None
1619
+ ),
1620
+ turn_context=self._turn_context,
1621
+ ),
1622
+ )
1623
+ approval_mw.bind_publisher(emitter)
1624
+ middleware.append(approval_mw)
1625
+ self._live_approvals.append(approval_mw)
1626
+ sleep_mw = SleepMiddleware(self._bus, session_id=self._session_id)
1627
+ middleware.append(sleep_mw)
1628
+ if sub_event_mw is None:
1629
+ middleware.append(SubAgentStatsMiddleware(log_stats))
1630
+ # Wire usage callback so token counts flow to TUI via
1631
+ # InvocationProgress. Per-invocation UsageTrackingMiddleware
1632
+ # — adding it to run-scoped middleware gives each concurrent
1633
+ # invocation its own callback state, so parallel sub-agent
1634
+ # runs no longer clobber each other's totals through the
1635
+ # registered tool's shared ``ctx.usage_middleware``. For
1636
+ # registered sub-agents, ``ctx`` is also fresh per invocation,
1637
+ # so calibration/overhead/LAST_WORDS state cannot leak between
1638
+ # same-tool concurrent runs.
1639
+ ctx = invocation_ctx
1640
+ if ctx:
1641
+ parent_cb = self._on_sub_agent_usage
1642
+ total_usage_tokens = 0
1643
+ max_context_tokens = ctx.usage_middleware.max_context_tokens
1644
+ agent_profile_name = self._hook_profile_names.get(tool_name, tool_name)
1645
+
1646
+ # Positional args mirror ``UsageTrackingMiddleware._fire_callback``;
1647
+ # if that callback grows a new arg, add it here too or the
1648
+ # parent accumulator won't see it (``*_rest`` will swallow it).
1649
+ def _on_usage(
1650
+ total,
1651
+ inp=0,
1652
+ out=0,
1653
+ local=0,
1654
+ ratio=1.0,
1655
+ overhead=0,
1656
+ cache_hit=None,
1657
+ calibration_initialized=False,
1658
+ use_local_context_estimate=False,
1659
+ *_rest,
1660
+ _mw=sub_event_mw,
1661
+ _cb=parent_cb,
1662
+ _max_context_tokens=max_context_tokens,
1663
+ _agent_profile=agent_profile_name,
1664
+ _usage_source_id=invocation_id,
1665
+ ):
1666
+ # ``normalize_stream_usage`` collapses provider deltas
1667
+ # into a single per-call snapshot; ``total`` is the
1668
+ # per-call cost, so we
1669
+ # can accumulate it directly. An ``(inp + out) or total``
1670
+ # fallback would double-count on any future provider
1671
+ # that reports ``total`` without an input/output split.
1672
+ nonlocal total_usage_tokens
1673
+ total_usage_tokens += total
1674
+ log_stats.record_usage(total, total_usage_tokens)
1675
+ if _mw is not None:
1676
+ _mw.record_usage(total, total_usage_tokens)
1677
+ if _cb is not None:
1678
+ _cb(
1679
+ total,
1680
+ inp,
1681
+ out,
1682
+ local,
1683
+ ratio,
1684
+ overhead,
1685
+ cache_hit,
1686
+ _max_context_tokens,
1687
+ _agent_profile,
1688
+ _usage_source_id,
1689
+ None,
1690
+ use_local_context_estimate,
1691
+ )
1692
+
1693
+ per_invocation_usage = UsageTrackingMiddleware(
1694
+ max_context_tokens=max_context_tokens,
1695
+ warn_threshold_pct=ctx.usage_middleware.warn_threshold_pct,
1696
+ on_usage=_on_usage,
1697
+ compaction_strategy=ctx.compaction_strategy,
1698
+ use_local_context_estimate_for_hosted_usage=(ctx.use_local_context_estimate_for_hosted_usage),
1699
+ )
1700
+ middleware.append(per_invocation_usage)
1701
+
1702
+ # Response-validation middleware — appended AFTER all other
1703
+ # per-invocation middleware so it ends up innermost in the
1704
+ # chat pipeline (closest to ``_inner_get_response``). Run-
1705
+ # scoped middleware is concatenated AFTER the agent's own
1706
+ # chat middleware, so listing it last here means it sits
1707
+ # last overall. Retries are therefore invisible to outer
1708
+ # instrumentation — the per-invocation usage middleware
1709
+ # above, and the agent-level system_reminder / ctx.middleware
1710
+ # — which each fire exactly once per turn.
1711
+ # ``publish_retry`` surfaces client-mode validation retries
1712
+ # as ``InvocationRetryAttempt`` keyed to this invocation;
1713
+ # service-side failures raise to the whole-run retry loop,
1714
+ # which publishes its own retry events.
1715
+ if self._bus is not None:
1716
+
1717
+ async def _publish_validation_retry(info: RetryAttemptInfo) -> None:
1718
+ await emitter.publish(
1719
+ InvocationRetryAttempt(
1720
+ scope="wire",
1721
+ origin=emitter.origin,
1722
+ agent_name=self._display_names.get(tool_name, tool_name),
1723
+ message=f"Invalid response: {info.reason}",
1724
+ attempt=info.attempt,
1725
+ max_attempts=info.max_attempts,
1726
+ delay_seconds=int(info.delay_seconds),
1727
+ session_id=self._session_id,
1728
+ )
1729
+ )
1730
+
1731
+ validation_middleware = create_validation(conversation, publish_retry=_publish_validation_retry)
1732
+ else:
1733
+ validation_middleware = create_validation(conversation, publish_retry=None)
1734
+ middleware.append(validation_middleware)
1735
+
1736
+ run_kwargs: AgentRunKwargs = {}
1737
+ sub_session = invocation_agent.create_session()
1738
+ run_kwargs["session"] = sub_session
1739
+ # Per-invocation channel for the chrys ToolLoopLayer — parallel
1740
+ # sub-agents share one client stack, so the recorder must ride
1741
+ # the run, never the client.
1742
+ run_client_kwargs: dict[str, Any] = {"loop_recorder": loop_recorder}
1743
+ if sub_agent_trace is not None:
1744
+ run_client_kwargs[TRAJECTORY_CONTEXT_KWARG] = sub_agent_trace.child_context
1745
+ run_kwargs["client_kwargs"] = run_client_kwargs
1746
+ if middleware:
1747
+ run_kwargs["middleware"] = middleware
1748
+ invocation_chat_options = self._chat_options_by_tool.get(tool_name)
1749
+ if invocation_chat_options:
1750
+ options_copy = dict(invocation_chat_options)
1751
+ if isinstance(extra_body := options_copy.get("extra_body"), dict):
1752
+ options_copy["extra_body"] = dict(extra_body)
1753
+ run_kwargs["options"] = options_copy
1754
+ if ctx is not None:
1755
+ run_kwargs["compaction_strategy"] = ctx.compaction_strategy
1756
+ # Same wiring as TurnBindings._build_run_kwargs: the raw client
1757
+ # estimates live tool definitions with the strategy's own
1758
+ # tokenizer.
1759
+ run_kwargs["tokenizer"] = ctx.compaction_strategy.tokenizer
1760
+
1761
+ # Controller owns the run → retry → pause → decide loop.
1762
+ # Registered before run() so an early cascade_abort (e.g.
1763
+ # global interrupt fired during concurrent startup) can
1764
+ # reach it even before it enters RUNNING.
1765
+ def _commit_parent_interruption() -> None:
1766
+ _commit_interrupted_parent_result(parent_result_metadata)
1767
+
1768
+ def _record_terminal_audit() -> None:
1769
+ if parent_result_metadata is not None and log_writer is not None:
1770
+ parent_result_metadata.sub_agent_log_file = log_writer.log_file_name
1771
+ parent_result_metadata.sub_agent_audit_complete = True
1772
+ _upgrade_interrupted_parent_result(parent_result_metadata)
1773
+
1774
+ def _begin_hosted_pass() -> None:
1775
+ if sub_event_mw is None:
1776
+ validation_middleware.set_observation_hook(None)
1777
+ return
1778
+ validation_middleware.set_observation_hook(sub_event_mw.begin_hosted_pass())
1779
+
1780
+ retry_kwargs: dict[str, Any] = {}
1781
+ if self._max_transient_retries is not None:
1782
+ retry_kwargs["max_retries"] = self._max_transient_retries
1783
+ if controller is None:
1784
+ raise RuntimeError("The sub-agent invocation has no controller.")
1785
+ controller.bind_parent_interrupt_commit(_commit_parent_interruption)
1786
+ policy = KernelSubAgentPolicy(
1787
+ shell=controller,
1788
+ conversation=invocation_runtime.owner,
1789
+ agent=invocation_agent,
1790
+ session=sub_session,
1791
+ loop_recorder=loop_recorder,
1792
+ prompt=prompt,
1793
+ run_kwargs=run_kwargs,
1794
+ # Persist the parent assistant function_call's
1795
+ # call_id so session-reload recovery can re-pair a
1796
+ # paused record to its dangling function_call by id
1797
+ # instead of by tool_name + appearance order.
1798
+ parent_call_id=parent_provider_call_id,
1799
+ parent_event_call_id=parent_event_call_id,
1800
+ sub_agent_log_file=log_writer.log_file_name if log_writer is not None and log_writer.active else "",
1801
+ persist_dir=self._pending_dir(),
1802
+ log_writer=log_writer,
1803
+ log_stats=log_stats,
1804
+ terminal_audit_callback=_record_terminal_audit,
1805
+ pending_record_finalizer=self.queue_pending_cleanup,
1806
+ tool_event_middleware=sub_event_mw,
1807
+ stream=stream,
1808
+ stream_attempt_timeout=stream_attempt_timeout,
1809
+ sleep_middleware=sleep_mw,
1810
+ pass_start_hooks=(
1811
+ *((begin_web_pass,) if begin_web_pass is not None else ()),
1812
+ _begin_hosted_pass,
1813
+ validation_middleware.reset_service_retry_state,
1814
+ ),
1815
+ hosted_commits_probe=validation_middleware.hosted_commits_observed,
1816
+ begin_hosted_baseline=validation_middleware.begin_pass_hosted_baseline,
1817
+ trajectory_context=sub_agent_trace.child_context if sub_agent_trace is not None else None,
1818
+ trajectory_boundary_operation_id=(
1819
+ sub_agent_trace.operation_id if sub_agent_trace is not None else None
1820
+ ),
1821
+ **retry_kwargs,
1822
+ )
1823
+ controller.attach_policy(policy)
1824
+ if not service_storage_side(invocation_agent.client, run_kwargs.get("options")):
1825
+ # Look up callbacks at retry time, not while constructing
1826
+ # the adapter.
1827
+ def _wire_sleep(seconds: int) -> Awaitable[bool]:
1828
+ return policy.sleep_for_wire_retry(seconds)
1829
+
1830
+ def _wire_notice(
1831
+ message: str, attempt: int, maximum: int, delay: int, exc: BaseException
1832
+ ) -> Awaitable[None]:
1833
+ return policy.publish_wire_retry_attempt(message, attempt, maximum, delay, exc)
1834
+
1835
+ run_client_kwargs["wire_retry_policy"] = WireRecipe(
1836
+ max_retries=lambda: policy.max_retries,
1837
+ stall_timeout_seconds=stream_attempt_timeout,
1838
+ stall_max_retries=lambda: policy.max_retries,
1839
+ stall_exhausted_action=StallExhaustedAction.RAISE,
1840
+ backoff_schedule=lambda: policy.backoff_schedule,
1841
+ interrupted=lambda: controller.cascade_requested,
1842
+ interruptible_sleep=_wire_sleep,
1843
+ publish_retry=_wire_notice,
1844
+ prepare_retry=None,
1845
+ hosted_commits_in_flight=validation_middleware.hosted_commits_in_flight,
1846
+ ).build()
1847
+ run_client_kwargs["continuation_token_observer"] = policy.observe_continuation_token
1848
+ self._controllers[invocation_id] = controller
1849
+ with (
1850
+ trajectory_scope(sub_agent_trace.child_context)
1851
+ if sub_agent_trace is not None
1852
+ else contextlib.nullcontext()
1853
+ ):
1854
+ result = await controller.run()
1855
+ segment = policy.final_segment
1856
+ if parent_result_metadata is not None and segment is not None and segment.transcript != segment.result:
1857
+ parent_result_metadata.transcript_final_text = segment.transcript
1858
+ hook_status = "failed" if result.startswith("Error:") else "ok"
1859
+ hook_result_summary = result[:500]
1860
+ if hook_status == "failed":
1861
+ sub_agent_failure_code = "tool_error"
1862
+ return result
1863
+ except asyncio.CancelledError as exc:
1864
+ hook_status = "cancelled"
1865
+ hook_result_summary = "cancelled"
1866
+ _commit_interrupted_parent_result(parent_result_metadata)
1867
+ if policy is not None:
1868
+ if controller is None:
1869
+ raise RuntimeError("The sub-agent invocation has no controller.") from exc
1870
+ await _finish_cancellation_cleanup(controller.finalize_cancellation())
1871
+ elif log_writer is not None:
1872
+ with contextlib.suppress(Exception):
1873
+ terminal_written = await log_writer.write(
1874
+ status="cancelled", result="cancelled", last_error="cancelled", ended=True
1875
+ )
1876
+ if terminal_written and parent_result_metadata is not None:
1877
+ parent_result_metadata.sub_agent_log_file = log_writer.log_file_name
1878
+ parent_result_metadata.sub_agent_audit_complete = True
1879
+ _upgrade_interrupted_parent_result(parent_result_metadata)
1880
+ raise
1881
+ except BaseException as exc:
1882
+ hook_status = "failed"
1883
+ hook_result_summary = str(exc)[:500]
1884
+ sub_agent_failure_code = "setup_failed" if policy is None else "exception"
1885
+ if policy is None and log_writer is not None:
1886
+ with contextlib.suppress(Exception):
1887
+ terminal_written = await log_writer.write(
1888
+ status="setup_failed",
1889
+ result=hook_result_summary,
1890
+ failure_reason="setup_failed",
1891
+ last_error=hook_result_summary,
1892
+ ended=True,
1893
+ )
1894
+ if terminal_written and parent_result_metadata is not None:
1895
+ parent_result_metadata.sub_agent_log_file = log_writer.log_file_name
1896
+ parent_result_metadata.sub_agent_audit_complete = True
1897
+ _upgrade_interrupted_parent_result(parent_result_metadata)
1898
+ raise
1899
+ finally:
1900
+ operation.begin_cleanup()
1901
+ cleanup_cancelled = False
1902
+ _close_sub_agent_trace(
1903
+ sub_agent_trace,
1904
+ hook_status=hook_status,
1905
+ failure_code=sub_agent_failure_code,
1906
+ controller=controller if policy is not None else None,
1907
+ failure_reason=policy.failure_reason if policy is not None else None,
1908
+ )
1909
+ if sub_event_mw is not None:
1910
+ try:
1911
+ await sub_event_mw.flush_progress()
1912
+ except asyncio.CancelledError:
1913
+ cleanup_cancelled = True
1914
+ except Exception:
1915
+ logger.debug("sub-agent progress flush failed for invocation %s", invocation_id, exc_info=True)
1916
+ if self._drain_parent_usage_publishes is not None:
1917
+ try:
1918
+ await self._drain_parent_usage_publishes()
1919
+ except asyncio.CancelledError:
1920
+ cleanup_cancelled = True
1921
+ except Exception:
1922
+ logger.debug(
1923
+ "parent usage publish drain failed for invocation %s", invocation_id, exc_info=True
1924
+ )
1925
+ if sub_agent_hook_started:
1926
+ try:
1927
+ await self._fire_sub_agent_hook(
1928
+ HookEvent.SUB_AGENT_END,
1929
+ tool_name=tool_name,
1930
+ invocation_id=invocation_id,
1931
+ parent_call_id=parent_call_id,
1932
+ parent_provider_call_id=parent_provider_call_id,
1933
+ status=hook_status,
1934
+ result_summary=hook_result_summary,
1935
+ target_operation_id=(sub_agent_trace.operation_id if sub_agent_trace is not None else None),
1936
+ )
1937
+ except asyncio.CancelledError:
1938
+ cleanup_cancelled = True
1939
+ logger.debug("sub-agent end hook cancelled for invocation %s", invocation_id)
1940
+ except Exception:
1941
+ logger.warning("sub-agent end hook failed for invocation %s", invocation_id, exc_info=True)
1942
+ if invocation_id:
1943
+ self._controllers.pop(invocation_id, None)
1944
+ self._intermediate_buffers.pop(invocation_id, None)
1945
+ self._invocation_emitters.pop(invocation_id, None)
1946
+ if total_counter_incremented:
1947
+ self._total_active -= 1
1948
+ if agent_counter_incremented:
1949
+ # ``register()`` seeds ``_agent_active[tool_name] = 0`` and
1950
+ # the enclosing ``try`` already bumped it by 1, so direct
1951
+ # subscript access is correct here. A KeyError would be a
1952
+ # genuine bug (tool invoked without registration) — surface
1953
+ # it rather than silently masking.
1954
+ try:
1955
+ self._agent_active[tool_name] -= 1
1956
+ except KeyError:
1957
+ logger.error(
1958
+ "sub-agent counter underflow: tool %r not in _agent_active (invocation %s)",
1959
+ tool_name,
1960
+ invocation_id,
1961
+ )
1962
+ if route_session_token is not None:
1963
+ llm_route_session_id.reset(route_session_token)
1964
+ if parent_session_token is not None:
1965
+ llm_parent_session_id.reset(parent_session_token)
1966
+ if invocation_token is not None:
1967
+ invocation_routing_key.reset(invocation_token)
1968
+ if unbind is not None:
1969
+ unbind()
1970
+ if approval_mw is not None:
1971
+ try:
1972
+ if conversation is None:
1973
+ raise RuntimeError("Closing sub-agent approvals requires their owning conversation.")
1974
+ await conversation.release(approval_mw.close)
1975
+ except asyncio.CancelledError:
1976
+ cleanup_cancelled = True
1977
+ except Exception:
1978
+ logger.debug("approval middleware close failed for invocation %s", invocation_id, exc_info=True)
1979
+ with contextlib.suppress(ValueError):
1980
+ self._live_approvals.remove(approval_mw)
1981
+ operation.finish()
1982
+ if conversation is not None:
1983
+ try:
1984
+ await conversation.aclose()
1985
+ except asyncio.CancelledError:
1986
+ cleanup_cancelled = True
1987
+ if cleanup_cancelled:
1988
+ raise asyncio.CancelledError
1989
+
1990
+ sub_agent_tool = FunctionTool(
1991
+ func=_invoke,
1992
+ name=tool_name,
1993
+ description=description,
1994
+ )
1995
+ set_tool_kind(sub_agent_tool, KIND_SUB_AGENT)
1996
+ return sub_agent_tool
1997
+
1998
+ async def _fire_sub_agent_hook(
1999
+ self,
2000
+ event: HookEvent,
2001
+ *,
2002
+ tool_name: str,
2003
+ invocation_id: str,
2004
+ parent_call_id: str,
2005
+ parent_provider_call_id: str = "",
2006
+ status: str = "",
2007
+ result_summary: str = "",
2008
+ target_operation_id: str | None = None,
2009
+ ) -> None:
2010
+ if self._hook_manager is None or not self._hook_manager.has_hooks_for(event):
2011
+ return
2012
+ payload: dict[str, Any] = {
2013
+ "session_id": self._session_id,
2014
+ "profile": self._hook_profile_names.get(tool_name, tool_name),
2015
+ "cwd": self._workspace_cwd,
2016
+ "sub_agent": {
2017
+ "name": self._display_names.get(tool_name, tool_name),
2018
+ "tool_name": tool_name,
2019
+ "invocation_id": invocation_id,
2020
+ "parent_call_id": parent_call_id,
2021
+ "parent_provider_call_id": parent_provider_call_id,
2022
+ },
2023
+ }
2024
+ if event == HookEvent.SUB_AGENT_END:
2025
+ payload["status"] = status
2026
+ payload["result_summary"] = result_summary
2027
+ await self._hook_manager.fire(event, payload, target_operation_id=target_operation_id)
2028
+
2029
+ async def _fire_pre_compact_hook(self, tool_name: str, info: PreCompactInfo) -> None:
2030
+ if self._hook_manager is None:
2031
+ return
2032
+ if not self._hook_manager.has_hooks_for(HookEvent.PRE_COMPACT):
2033
+ return
2034
+ await self._hook_manager.fire(
2035
+ HookEvent.PRE_COMPACT,
2036
+ {
2037
+ "session_id": self._session_id,
2038
+ "profile": self._hook_profile_names.get(tool_name, tool_name),
2039
+ "cwd": self._workspace_cwd,
2040
+ "trigger": info.trigger,
2041
+ "usage_pct": info.usage_pct,
2042
+ "tokens_before": info.tokens_before,
2043
+ "sub_agent": {
2044
+ "name": self._display_names.get(tool_name, tool_name),
2045
+ "tool_name": tool_name,
2046
+ },
2047
+ },
2048
+ target_operation_id=info.trajectory_operation_id,
2049
+ )
2050
+
2051
+ async def _publish_sub_agent_compaction_status(
2052
+ self,
2053
+ status: CompactionStatus,
2054
+ tool_name: str,
2055
+ emitter: BoundEmitter,
2056
+ ) -> None:
2057
+ """Publish a Phase-4 compaction UX signal scoped to one invocation.
2058
+
2059
+ Callers guard bus/invocation-id availability; this only translates
2060
+ the generator's :class:`CompactionStatus` into the sub-agent
2061
+ started/finished bus events (note text deliberately not forwarded —
2062
+ sub-agent cards only flip a status line).
2063
+ """
2064
+ bus = self._bus
2065
+ if bus is None:
2066
+ return
2067
+ agent_name = self._display_names.get(tool_name, tool_name)
2068
+ if status.stage == "started":
2069
+ await emitter.publish(
2070
+ InvocationCompactionStarted(
2071
+ origin=emitter.origin,
2072
+ agent_name=agent_name,
2073
+ compaction_id=status.compaction_id,
2074
+ session_id=self._session_id,
2075
+ )
2076
+ )
2077
+ return
2078
+ if status.stage == "committed":
2079
+ # Post-commit confirmation — a distinct event, NOT a second
2080
+ # finished signal: the card's compaction line already flipped on
2081
+ # finished-ok, this only drives the "Compactions: N" counter.
2082
+ await emitter.publish(
2083
+ InvocationCompactionCommitted(
2084
+ origin=emitter.origin,
2085
+ agent_name=agent_name,
2086
+ compaction_id=status.compaction_id,
2087
+ session_id=self._session_id,
2088
+ )
2089
+ )
2090
+ return
2091
+ await emitter.publish(
2092
+ InvocationCompactionFinished(
2093
+ origin=emitter.origin,
2094
+ agent_name=agent_name,
2095
+ compaction_id=status.compaction_id,
2096
+ outcome=status.outcome,
2097
+ duration_ms=status.duration_ms,
2098
+ format_violation=status.format_violation,
2099
+ failure_reason=status.failure_reason,
2100
+ session_id=self._session_id,
2101
+ )
2102
+ )
2103
+
2104
+ def get_tools(self) -> list[FunctionTool]:
2105
+ """Return all registered sub-agent tools."""
2106
+ return list(self._tools)
2107
+
2108
+ def tool_names(self) -> list[str]:
2109
+ """Return the tool names of all registered sub-agents."""
2110
+ return [tool.name for tool in self._tools]
2111
+
2112
+ # -- Per-invocation dispatch (used by engine event handlers) ------
2113
+
2114
+ def _pending_dir(self) -> Path | None:
2115
+ if not self._session_dir:
2116
+ return None
2117
+ return pending_dir(self._session_dir)
2118
+
2119
+ def request_retry(self, invocation_id: str) -> bool:
2120
+ """Deliver a user-triggered retry to the controller for ``invocation_id``.
2121
+
2122
+ Returns ``True`` if the controller existed and was paused;
2123
+ ``False`` if there's no such controller or it wasn't paused
2124
+ (e.g. finished or cascade-aborted between the click and delivery).
2125
+ """
2126
+ controller = self._controllers.get(invocation_id)
2127
+ if controller is None:
2128
+ logger.info("Sub-agent retry: no controller for invocation_id=%s", invocation_id)
2129
+ return False
2130
+ return controller.request_retry()
2131
+
2132
+ def request_abort(self, invocation_id: str) -> bool:
2133
+ """Deliver a user-triggered abort to the controller for ``invocation_id``."""
2134
+ controller = self._controllers.get(invocation_id)
2135
+ if controller is None:
2136
+ logger.info("Sub-agent abort: no controller for invocation_id=%s", invocation_id)
2137
+ return False
2138
+ return controller.request_abort()
2139
+
2140
+ def queue_pending_cleanup(self, path: Path | None) -> None:
2141
+ """Queue a pending control record for post-save cleanup."""
2142
+ if path is not None:
2143
+ self._pending_cleanup_paths.add(path)
2144
+
2145
+ def finalize_pending_cleanups(self) -> None:
2146
+ """Delete queued pending records after a successful parent session save."""
2147
+ remaining: set[Path] = set()
2148
+ for path in self._pending_cleanup_paths:
2149
+ if secure_unlink_owner_verified(path):
2150
+ continue
2151
+ try:
2152
+ path.lstat()
2153
+ except FileNotFoundError:
2154
+ continue
2155
+ except OSError:
2156
+ pass
2157
+ logger.warning("Failed to delete finalized sub-agent pending record %s", path)
2158
+ remaining.add(path)
2159
+ self._pending_cleanup_paths = remaining
2160
+
2161
+ @staticmethod
2162
+ def _sub_agent_backlinks(parent_messages: list[Message]) -> tuple[set[str], set[str]]:
2163
+ invocation_ids: set[str] = set()
2164
+ log_files: set[str] = set()
2165
+ for message in parent_messages:
2166
+ for content in message.contents:
2167
+ metadata = content.additional_properties.get(TOOL_RESULT_METADATA_KEY)
2168
+ if not isinstance(metadata, dict):
2169
+ continue
2170
+ invocation_id = metadata.get("sub_agent_invocation_id")
2171
+ if isinstance(invocation_id, str) and invocation_id:
2172
+ invocation_ids.add(invocation_id)
2173
+ log_file = metadata.get("sub_agent_log_file")
2174
+ if isinstance(log_file, str) and log_file:
2175
+ log_files.add(log_file)
2176
+ return invocation_ids, log_files
2177
+
2178
+ async def cascade_abort_all(self) -> None:
2179
+ """Tear down every live sub-agent — used by global :class:`UserInterrupt`.
2180
+
2181
+ Paused controllers resolve via the cascade decision path; running
2182
+ controllers cancel their active attempt directly so long-running
2183
+ inner tool calls stop before the parent task is cancelled.
2184
+ """
2185
+ controllers = list(self._controllers.values())
2186
+ results = await asyncio.gather(
2187
+ *(controller.cascade_abort() for controller in controllers),
2188
+ return_exceptions=True,
2189
+ )
2190
+ for result in results:
2191
+ if isinstance(result, BaseException):
2192
+ logger.debug("sub-agent cascade abort failed: %s", result)
2193
+
2194
+ @property
2195
+ def approval_mode(self) -> ApprovalMode:
2196
+ """The mode a fresh invocation constructs its approval middleware with."""
2197
+ return self._approval_mode
2198
+
2199
+ def set_approval_mode(self, mode: ApprovalMode) -> None:
2200
+ """Update the active approval mode for sub-agent invocations.
2201
+
2202
+ Propagates to **live** ``ApprovalMiddleware`` instances so in-flight
2203
+ sub-agent runs pick up the new mode on their next tool call. New
2204
+ invocations read the stored value at construction time.
2205
+ """
2206
+ self._approval_mode = mode
2207
+ for mw in list(self._live_approvals):
2208
+ mw.set_approval_mode(mode)
2209
+
2210
+ async def cleanup(self) -> None:
2211
+ """Shut down all sub-agent instances and disconnect MCP servers."""
2212
+ cancelled: asyncio.CancelledError | None = None
2213
+ for prepared in reversed(tuple(self._prepared_by_tool.values())):
2214
+ try:
2215
+ await prepared.aclose()
2216
+ except asyncio.CancelledError as exc:
2217
+ cancelled = exc
2218
+ self._prepared_by_tool.clear()
2219
+ self._mcp_adapters.clear()
2220
+ self._agents.clear()
2221
+ self._acp_profiles.clear()
2222
+ self._chat_options_by_tool.clear()
2223
+ self._tools.clear()
2224
+ self._agent_active.clear()
2225
+ self._agent_max.clear()
2226
+ self._total_active = 0
2227
+ if cancelled is not None:
2228
+ raise cancelled