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,1002 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Owner of one workflow worker process and the RPC client that talks to it.
4
+
5
+ One reader task demultiplexes every frame the worker writes: responses go to
6
+ the future registered under their id, ``emit`` notifications go through an
7
+ ordered dispatcher, ``ask`` requests each get their own task so a human answer
8
+ never blocks the channel. Worker EOF, a malformed frame or the worker's exit
9
+ (noticed on its own, since a forked child of the worker can keep its pipes
10
+ open) rejects every pending future with :class:`WorkerLostError`, cancels the
11
+ helpers and terminates the process tree, in that order and only once.
12
+
13
+ Two contracts the runner relies on live here:
14
+
15
+ * **attempt fence** — once a ``run_python`` attempt has its terminal envelope,
16
+ a later ``emit`` for it is dropped and a later ``ask`` is answered
17
+ ``attempt_terminated``; a ``cancel`` names one exact attempt.
18
+ * **projection barrier** — ``run_python`` returns only after the emit
19
+ dispatcher has processed every ordinal up to the envelope's
20
+ ``last_emit_ordinal``; the cancel and worker-lost paths are exempt.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import contextlib
27
+ import hashlib
28
+ import logging
29
+ import os
30
+ from collections import defaultdict
31
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
32
+ from dataclasses import dataclass, field
33
+ from pathlib import Path
34
+ from typing import Any, Final, cast
35
+
36
+ from chrys.foundation.models.ask_user import AskUserAnswer, AskUserQuestion
37
+ from chrys.foundation.platform import get_platform
38
+ from chrys.foundation.platform.process import (
39
+ ManagedStdioProcess,
40
+ MissingWorkingDirectoryError,
41
+ spawn_managed_stdio_process,
42
+ )
43
+ from chrys.foundation.trajectory.keys import ensure_owner_only_directory
44
+ from chrys.service.workflows import protocol
45
+ from chrys.service.workflows.asks import answers_to_wire, questions_from_wire
46
+ from chrys.service.workflows.environment import PreparedEnvironment
47
+ from chrys.service.workflows.graph import ManifestError, manifest_warnings
48
+ from chrys.service.workflows.protocol import (
49
+ LIMITS,
50
+ PROTOCOL_VERSION,
51
+ ErrorCode,
52
+ Method,
53
+ ProtocolError,
54
+ decode_frame,
55
+ encode_frame,
56
+ ref_from_wire,
57
+ ref_key,
58
+ ref_to_wire,
59
+ )
60
+ from chrys.service.workflows.scheduler import AttemptRef
61
+ from chrys.service.workflows.sdk import SourceValue, WorkflowValue
62
+ from chrys.service.workflows.sdk_artifact import SdkArtifact
63
+ from chrys.service.workflows.values import canonical_json, source_to_wire, value_from_wire, value_to_wire
64
+
65
+ logger = logging.getLogger(__name__)
66
+
67
+ HOST_PATH = Path(protocol.__file__).resolve().parent / "worker_host.py"
68
+ BYTECODE_CACHE_ENV = "PYTHONPYCACHEPREFIX"
69
+ STDERR_TAIL_BYTES = 64 * 1024
70
+ MAX_IN_FLIGHT_REQUESTS = 64
71
+ _STDIO_LIMIT = LIMITS.max_frame_bytes + 2
72
+
73
+
74
+ def _bounded_diagnostic(text: str) -> str:
75
+ """Cap diagnostic text so its error frame always fits the wire limit; a dropped error reply hangs the worker."""
76
+ limit = LIMITS.captured_output_bytes
77
+ if len(text) <= limit:
78
+ return text
79
+ return text[:limit] + "... [" + str(len(text) - limit) + " chars dropped]"
80
+
81
+
82
+ _CONTROL_METHODS = frozenset({Method.CANCEL, Method.SHUTDOWN})
83
+ # Requests that run one attempt each; their terminal envelope carries the attempt's leak verdict.
84
+ _ATTEMPT_METHODS = frozenset({Method.RUN_PYTHON, Method.EVAL_OUTGOING, Method.EVAL_LOOP_UNTIL, Method.COMBINE})
85
+ _EVAL_METHODS = frozenset({Method.EVAL_OUTGOING, Method.EVAL_LOOP_UNTIL, Method.COMBINE})
86
+
87
+ AskHandler = Callable[[AttemptRef, tuple[AskUserQuestion, ...]], Awaitable[tuple[AskUserAnswer, ...]]]
88
+ EmitHandler = Callable[[AttemptRef, int, str], Awaitable[None]]
89
+
90
+
91
+ class WorkerError(Exception):
92
+ """Base of every failure the client reports."""
93
+
94
+
95
+ class WorkerStartError(WorkerError):
96
+ """The worker never became usable (probe, spawn, hello, SDK origin)."""
97
+
98
+
99
+ class WorkerLostError(WorkerError):
100
+ """The worker exited, broke the protocol, or was killed; the run is over."""
101
+
102
+
103
+ class WorkerRpcError(WorkerError):
104
+ """An ``error`` response; ``code`` is one of :class:`protocol.ErrorCode`."""
105
+
106
+ def __init__(self, code: str, message: str, data: Mapping[str, Any] | None = None) -> None:
107
+ super().__init__(f"{code}: {message}")
108
+ self.code = code
109
+ self.message = message
110
+ self.data: dict[str, Any] = dict(data or {})
111
+ self.stdout = _captured(self.data.get("stdout"))
112
+ self.traceback = _string(self.data.get("traceback", ""), "traceback")
113
+
114
+
115
+ class AttemptTimeout(WorkerError):
116
+ """A deadline passed; the attempt was cancelled (``leaked_thread`` says whether a thread stayed behind)."""
117
+
118
+ def __init__(self, ref: AttemptRef, *, leaked_thread: bool, data: Mapping[str, Any]) -> None:
119
+ super().__init__(f"{ref.activation_id} attempt {ref.attempt} timed out")
120
+ self.ref = ref
121
+ self.leaked_thread = leaked_thread
122
+ self.stdout = _captured(data.get("stdout"))
123
+ self.traceback = _string(data.get("traceback", ""), "traceback")
124
+
125
+
126
+ class AskUnavailable(Exception):
127
+ """Raised by an ask handler when nobody can answer (headless)."""
128
+
129
+
130
+ @dataclass(frozen=True, slots=True)
131
+ class CapturedOutput:
132
+ text: str
133
+ truncated: bool
134
+
135
+
136
+ @dataclass(frozen=True, slots=True)
137
+ class Hello:
138
+ protocol_version: int
139
+ python_version: str
140
+ implementation: str
141
+ platform: str
142
+ sdk_origin_ok: bool
143
+ sdk_origin_error: str | None
144
+
145
+
146
+ @dataclass(frozen=True, slots=True)
147
+ class LoadResult:
148
+ entry_digest: str
149
+ manifest_digest: str
150
+ manifest: dict[str, Any]
151
+ stdout: CapturedOutput
152
+ sites: dict[str, tuple[str | None, int | None]] = field(default_factory=dict)
153
+ """Where each node was declared, as ``(file, line)``; only a ``diagnose`` load reports them."""
154
+ sites_truncated: bool = False
155
+ """The sites were left out to keep the response within the frame limit."""
156
+
157
+
158
+ LOAD_TIMED_OUT: Final = "timeout"
159
+ """``WorkerRpcError.data["reason"]`` of a load that did not finish within ``LIMITS.load_timeout``."""
160
+
161
+
162
+ @dataclass(frozen=True, slots=True)
163
+ class LoadNote:
164
+ message: str
165
+ file: str | None
166
+ line: int | None
167
+
168
+
169
+ @dataclass(frozen=True, slots=True)
170
+ class LoadDiagnostic:
171
+ """A load failure as the worker placed it; lines and character columns count from 1.
172
+
173
+ ``end_column`` is the column just past the faulty range; ``node`` is the
174
+ node or edge an SDK validation error names.
175
+ """
176
+
177
+ code: str
178
+ message: str
179
+ file: str | None
180
+ line: int | None
181
+ column: int | None
182
+ end_line: int | None
183
+ end_column: int | None
184
+ node: str | None
185
+ source_line: str | None
186
+ notes: tuple[LoadNote, ...]
187
+ hint: str | None
188
+
189
+
190
+ def load_diagnostics(data: Mapping[str, Any]) -> tuple[tuple[LoadDiagnostic, ...], bool]:
191
+ """The diagnostics of a ``diagnose`` load's ``load_failed`` data, and whether any were left out."""
192
+ raw = data.get("diagnostics", [])
193
+ if not isinstance(raw, list) or not all(isinstance(item, dict) for item in raw):
194
+ raise ProtocolError("diagnostics must be a list of objects.")
195
+ diagnostics = tuple(
196
+ LoadDiagnostic(
197
+ code=_string(item.get("code"), "diagnostic code"),
198
+ message=_string(item.get("message"), "diagnostic message"),
199
+ file=_optional_string(item.get("file"), "diagnostic file"),
200
+ line=_optional_count(item.get("line"), "diagnostic line"),
201
+ column=_optional_count(item.get("column"), "diagnostic column"),
202
+ end_line=_optional_count(item.get("end_line"), "diagnostic end_line"),
203
+ end_column=_optional_count(item.get("end_column"), "diagnostic end_column"),
204
+ node=_optional_string(item.get("node"), "diagnostic node"),
205
+ source_line=_optional_string(item.get("source_line"), "diagnostic source_line"),
206
+ notes=_load_notes(item.get("notes", [])),
207
+ hint=_optional_string(item.get("hint"), "diagnostic hint"),
208
+ )
209
+ for item in raw
210
+ )
211
+ return diagnostics, _boolean(data.get("diagnostics_truncated", False), "diagnostics_truncated")
212
+
213
+
214
+ def _load_notes(raw: Any) -> tuple[LoadNote, ...]:
215
+ if not isinstance(raw, list) or not all(isinstance(item, dict) for item in raw):
216
+ raise ProtocolError("diagnostic notes must be a list of objects.")
217
+ return tuple(
218
+ LoadNote(
219
+ message=_string(item.get("message"), "note message"),
220
+ file=_optional_string(item.get("file"), "note file"),
221
+ line=_optional_count(item.get("line"), "note line"),
222
+ )
223
+ for item in raw
224
+ )
225
+
226
+
227
+ def _load_sites(raw: Any) -> dict[str, tuple[str | None, int | None]]:
228
+ """``{"files": [file, ...], "nodes": {node_id: [file_index, line]}}`` as ``{node_id: (file, line)}``."""
229
+ if raw is None:
230
+ return {}
231
+ files, nodes = (raw.get("files"), raw.get("nodes")) if isinstance(raw, dict) else (None, None)
232
+ if not isinstance(files, list) or not isinstance(nodes, dict):
233
+ raise ProtocolError("sites must hold a files list and a nodes object.")
234
+ paths = [_optional_string(file, "site file") for file in files]
235
+ sites: dict[str, tuple[str | None, int | None]] = {}
236
+ for node_id, site in nodes.items():
237
+ if not (isinstance(site, list) and len(site) == 2 and type(site[0]) is int and 0 <= site[0] < len(paths)):
238
+ raise ProtocolError("a node site must be [file_index, line].")
239
+ sites[node_id] = (paths[site[0]], _optional_count(site[1], "site line"))
240
+ return sites
241
+
242
+
243
+ @dataclass(frozen=True, slots=True)
244
+ class PythonResult:
245
+ value: WorkflowValue
246
+ stdout: CapturedOutput
247
+ last_emit_ordinal: int
248
+
249
+
250
+ @dataclass(frozen=True, slots=True)
251
+ class CancelResult:
252
+ cancelled: bool
253
+ leaked_threads_by_pool: dict[str, int]
254
+
255
+
256
+ @dataclass(frozen=True, slots=True)
257
+ class NativeOutput:
258
+ text: str
259
+ dropped_bytes: int
260
+
261
+
262
+ def _encode(frame: dict[str, Any]) -> bytes:
263
+ try:
264
+ return encode_frame(frame)
265
+ except ProtocolError as exc:
266
+ raise WorkerRpcError(ErrorCode.VALUE_TOO_LARGE, str(exc)) from exc
267
+ except ValueError as exc:
268
+ raise WorkerRpcError(ErrorCode.VALUE_NOT_SERIALIZABLE, str(exc)) from exc
269
+
270
+
271
+ def _string(value: Any, field: str) -> str:
272
+ if not isinstance(value, str):
273
+ raise ProtocolError(f"{field} must be a string.")
274
+ return value
275
+
276
+
277
+ def _boolean(value: Any, field: str) -> bool:
278
+ if not isinstance(value, bool):
279
+ raise ProtocolError(f"{field} must be a boolean.")
280
+ return value
281
+
282
+
283
+ def _optional_string(value: Any, field: str) -> str | None:
284
+ return None if value is None else _string(value, field)
285
+
286
+
287
+ def _optional_count(value: Any, field: str) -> int | None:
288
+ """A 1-based line or column, or ``None``."""
289
+ if value is not None and (type(value) is not int or value < 1):
290
+ raise ProtocolError(f"{field} must be a positive integer or null.")
291
+ return value
292
+
293
+
294
+ def _ordinal(value: Any) -> int:
295
+ if type(value) is not int or value < 0:
296
+ raise ProtocolError("last_emit_ordinal must be a non-negative integer.")
297
+ return value
298
+
299
+
300
+ def _leaks(payload: Mapping[str, Any]) -> dict[str, int]:
301
+ pools = payload.get("leaked_threads_by_pool")
302
+ if (
303
+ not isinstance(pools, dict)
304
+ or set(pools) != {"body", "eval"}
305
+ or any(type(count) is not int or count < 0 for count in pools.values())
306
+ ):
307
+ raise ProtocolError("invalid per-pool thread leak verdict.")
308
+ return pools
309
+
310
+
311
+ def _captured(payload: Any) -> CapturedOutput:
312
+ if payload is None:
313
+ return CapturedOutput(text="", truncated=False)
314
+ if not isinstance(payload, dict):
315
+ raise ProtocolError("stdout must be an object.")
316
+ return CapturedOutput(
317
+ _string(payload.get("text"), "stdout.text"), _boolean(payload.get("truncated"), "stdout.truncated")
318
+ )
319
+
320
+
321
+ @dataclass(frozen=True, slots=True)
322
+ class EvaluationResult[T]:
323
+ value: T
324
+ stdout: CapturedOutput
325
+
326
+
327
+ @dataclass(slots=True)
328
+ class _Pending:
329
+ method: str
330
+ key: str | None
331
+ future: asyncio.Future[dict[str, Any]]
332
+ lanes: tuple[str, ...]
333
+ sent: bool = False
334
+
335
+
336
+ class WorkflowWorkerClient:
337
+ """Async RPC client bound to one worker process; see the module docstring."""
338
+
339
+ def __init__(
340
+ self,
341
+ process: ManagedStdioProcess,
342
+ *,
343
+ bytecode_cache: Path,
344
+ ask_handler: AskHandler | None,
345
+ emit_handler: EmitHandler | None,
346
+ ) -> None:
347
+ self._process = process
348
+ self._bytecode_cache = bytecode_cache
349
+ self._ask_handler = ask_handler
350
+ self._emit_handler = emit_handler
351
+ self._loop = asyncio.get_running_loop()
352
+ self._next_id = 1
353
+ self._pending: dict[int, _Pending] = {}
354
+ self._in_flight = {"body": 0, "eval": 0, "sync": 0}
355
+ self._capacity = {
356
+ "body": MAX_IN_FLIGHT_REQUESTS,
357
+ "eval": LIMITS.eval_thread_pool_size,
358
+ "sync": LIMITS.worker_thread_pool_size,
359
+ }
360
+ self._capacity_changed = asyncio.Event()
361
+ self._hello: asyncio.Future[Hello] = self._loop.create_future()
362
+ self._terminal: set[str] = set()
363
+ self._projected: dict[str, int] = {}
364
+ self._projection = asyncio.Condition()
365
+ self._emits: asyncio.Queue[tuple[AttemptRef, int, str] | None] = asyncio.Queue()
366
+ self._ask_tasks: dict[str, set[asyncio.Task[None]]] = defaultdict(set)
367
+ self._asks: set[asyncio.Task[None]] = set() # every live ask task, whichever attempt it served
368
+ self._lost: WorkerLostError | None = None
369
+ self._lost_event = asyncio.Event()
370
+ self._closing: asyncio.Task[None] | None = None
371
+ self._frames = 0
372
+ self._bytes = 0
373
+ self._stderr_tail = bytearray()
374
+ self._leaked_threads_by_pool = {"body": 0, "eval": 0}
375
+ self._reader = asyncio.create_task(self._read_loop(), name="chrys.workflow.worker.reader")
376
+ self._tasks = [
377
+ self._reader,
378
+ asyncio.create_task(self._dispatch_emits(), name="chrys.workflow.worker.emits"),
379
+ asyncio.create_task(self._read_stderr(), name="chrys.workflow.worker.stderr"),
380
+ asyncio.create_task(self._watch_exit(), name="chrys.workflow.worker.exit"),
381
+ ]
382
+
383
+ # ----------------------------------------------------------------- lifecycle
384
+
385
+ @classmethod
386
+ async def launch(
387
+ cls,
388
+ *,
389
+ environment: PreparedEnvironment,
390
+ sdk: SdkArtifact,
391
+ workspace: Path,
392
+ bytecode_cache: Path,
393
+ env: Mapping[str, str] | None = None,
394
+ ask_handler: AskHandler | None = None,
395
+ emit_handler: EmitHandler | None = None,
396
+ host_path: Path = HOST_PATH,
397
+ hello_timeout: float = LIMITS.hello_timeout,
398
+ ) -> WorkflowWorkerClient:
399
+ """Spawn the host on the prepared interpreter, inject *sdk*, and wait for a hello that matches both.
400
+
401
+ The interpreter and every Python process it starts keep compiled
402
+ bytecode only under the private *bytecode_cache*, never next to a
403
+ source file, so a cache planted or left beside a workflow can't run in
404
+ place of the source that was confirmed.
405
+ """
406
+ if environment.sdk_digest != sdk.digest:
407
+ raise WorkerStartError("The environment was prepared against a different SDK build than the one injected.")
408
+ try:
409
+ await asyncio.to_thread(ensure_owner_only_directory, bytecode_cache)
410
+ except OSError as exc:
411
+ raise WorkerStartError(
412
+ f"Cannot prepare the workflow bytecode cache {str(bytecode_cache)!r}: {exc}"
413
+ ) from exc
414
+ child_env = dict(os.environ)
415
+ child_env.update(env or {})
416
+ child_env[BYTECODE_CACHE_ENV] = str(bytecode_cache)
417
+ try:
418
+ process = await spawn_managed_stdio_process(
419
+ environment.executable,
420
+ str(host_path),
421
+ str(sdk.path),
422
+ cwd=str(workspace),
423
+ env=child_env,
424
+ limit=_STDIO_LIMIT,
425
+ parent_env=dict(os.environ),
426
+ )
427
+ except MissingWorkingDirectoryError as exc:
428
+ raise WorkerStartError(f"Cannot start the workflow worker: {exc}") from exc
429
+ except OSError as exc:
430
+ raise WorkerStartError(f"Cannot start the workflow worker with {environment.executable!r}: {exc}") from exc
431
+ client = cls(process, bytecode_cache=bytecode_cache, ask_handler=ask_handler, emit_handler=emit_handler)
432
+ try:
433
+ await client._handshake(hello_timeout, environment)
434
+ except BaseException:
435
+ # Nobody else holds the client yet: a failed or cancelled launch owns its cleanup.
436
+ await client.close()
437
+ raise
438
+ return client
439
+
440
+ async def _handshake(self, hello_timeout: float, environment: PreparedEnvironment) -> None:
441
+ try:
442
+ hello = await asyncio.wait_for(asyncio.shield(self._hello), timeout=hello_timeout)
443
+ except TimeoutError:
444
+ raise WorkerStartError(f"The workflow worker sent no hello within {hello_timeout:g}s.") from None
445
+ except WorkerLostError as exc:
446
+ raise WorkerStartError(f"The workflow worker exited before hello: {exc}{self._stderr_suffix()}") from exc
447
+ if hello.protocol_version != PROTOCOL_VERSION:
448
+ raise WorkerStartError(f"Worker speaks protocol {hello.protocol_version}, expected {PROTOCOL_VERSION}.")
449
+ if not hello.sdk_origin_ok:
450
+ raise WorkerStartError(f"Workflow SDK injection failed: {hello.sdk_origin_error}")
451
+ facts = (hello.implementation, hello.python_version, hello.platform)
452
+ expected = (environment.implementation, environment.python_version, environment.platform)
453
+ if facts != expected:
454
+ raise WorkerStartError(
455
+ f"The worker is {' '.join(facts)}, but the environment was prepared as {' '.join(expected)}."
456
+ )
457
+
458
+ @property
459
+ def lost(self) -> WorkerLostError | None:
460
+ return self._lost
461
+
462
+ async def wait_lost(self) -> WorkerLostError:
463
+ """Return once the worker is lost, for whatever reason (a close counts): the runner's watch."""
464
+ await self._lost_event.wait()
465
+ return self._lost_error()
466
+
467
+ @property
468
+ def stderr_tail(self) -> str:
469
+ return bytes(self._stderr_tail).decode("utf-8", errors="replace")
470
+
471
+ async def close(self, *, grace: float = LIMITS.shutdown_grace) -> None:
472
+ """Shut the worker down: shutdown RPC, EOF, terminate, kill; concurrent callers share one close."""
473
+ if self._closing is None:
474
+ self._closing = asyncio.create_task(self._close(grace), name="chrys.workflow.worker.close")
475
+ await asyncio.shield(self._closing) # a cancelled waiter must not take the close down with it
476
+
477
+ async def _close(self, grace: float) -> None:
478
+ if self._lost is None and self._process.returncode is None:
479
+ with contextlib.suppress(WorkerError, TimeoutError, asyncio.CancelledError):
480
+ await asyncio.wait_for(self._request(Method.SHUTDOWN, {}, key=None), timeout=grace)
481
+ self._process.close_stdin()
482
+ if not await self._wait_exit(grace):
483
+ self._process.terminate_tree()
484
+ if not await self._wait_exit(grace):
485
+ self._process.kill_tree()
486
+ await self._wait_exit(grace)
487
+ self._mark_lost("worker closed")
488
+ for task in self._tasks:
489
+ task.cancel()
490
+ # Ask tasks were cancelled by settlement or by _mark_lost; their handlers may still be cleaning up.
491
+ for task in [*self._tasks, *self._asks]:
492
+ with contextlib.suppress(asyncio.CancelledError, Exception):
493
+ await task
494
+ self._process.close_transports()
495
+
496
+ async def _wait_exit(self, timeout: float) -> bool:
497
+ """True once the worker and everything left in its process group are gone: the tree is what we own."""
498
+ deadline = self._loop.time() + timeout
499
+ try:
500
+ await asyncio.wait_for(self._process.wait(), timeout=timeout)
501
+ except TimeoutError:
502
+ return False
503
+ while self._group_alive():
504
+ if self._loop.time() >= deadline:
505
+ return False
506
+ await asyncio.sleep(0.02)
507
+ return True
508
+
509
+ def _group_alive(self) -> bool:
510
+ """A body's child that ignores SIGTERM outlives the worker; it still sits in the worker's process group."""
511
+ group = self._process.process_group_id
512
+ if get_platform().is_windows or group is None: # the Windows job object takes the tree down on close
513
+ return False
514
+ try:
515
+ cast(Any, os).killpg(group, 0)
516
+ except ProcessLookupError:
517
+ return False
518
+ except PermissionError: # macOS: a member that is exiting but not yet reaped answers EPERM
519
+ pass
520
+ return True
521
+
522
+ # ----------------------------------------------------------------- RPC surface
523
+
524
+ async def load(
525
+ self,
526
+ source: bytes,
527
+ *,
528
+ filename: str,
529
+ workspace: Path,
530
+ package_dir: str | None = None,
531
+ diagnose: bool = False,
532
+ precompile: Sequence[str] = (),
533
+ ) -> LoadResult:
534
+ """Execute the workflow file from *source* bytes; ``filename`` is its original canonical path.
535
+
536
+ *package_dir* names a workflow folder: its modules' cached bytecode is
537
+ cleared first, so every file in it compiles from its current source.
538
+ With *diagnose*, the worker first compiles the *precompile* files (the
539
+ folder's other Python files) without running them, places a failure
540
+ in ``load_diagnostics(error.data)`` and reports where each node was
541
+ declared; a load that times out carries ``data["reason"] == LOAD_TIMED_OUT``.
542
+ """
543
+ try:
544
+ text = source.decode("utf-8")
545
+ except UnicodeDecodeError as exc:
546
+ raise WorkerRpcError(ErrorCode.LOAD_FAILED, f"workflow file is not UTF-8: {exc}") from exc
547
+ params: dict[str, Any] = {
548
+ "source": text,
549
+ "filename": filename,
550
+ "workspace": str(workspace),
551
+ "bytecode_cache": str(self._bytecode_cache),
552
+ }
553
+ if package_dir is not None:
554
+ params["package_dir"] = package_dir
555
+ if diagnose:
556
+ params["diagnose"] = True
557
+ params["precompile"] = list(precompile)
558
+ try:
559
+ result = await asyncio.wait_for(self._call(Method.LOAD, params, key=None), timeout=LIMITS.load_timeout)
560
+ except TimeoutError:
561
+ await self.close()
562
+ raise WorkerRpcError(
563
+ ErrorCode.LOAD_FAILED,
564
+ f"Workflow load did not finish within {LIMITS.load_timeout:g}s.",
565
+ {"reason": LOAD_TIMED_OUT},
566
+ ) from None
567
+ manifest = result.get("manifest")
568
+ if not isinstance(manifest, dict):
569
+ raise ProtocolError("load result must carry a manifest object.")
570
+ if type(manifest.get("schema_version")) is not int:
571
+ raise ProtocolError("manifest schema_version must be an integer.")
572
+ try:
573
+ manifest_warnings(manifest)
574
+ except ManifestError as exc:
575
+ raise ProtocolError(str(exc)) from exc
576
+ return LoadResult(
577
+ entry_digest=hashlib.sha256(source).hexdigest(),
578
+ manifest_digest=hashlib.sha256(canonical_json(manifest).encode("utf-8", "surrogatepass")).hexdigest(),
579
+ manifest=manifest,
580
+ stdout=_captured(result.get("stdout")),
581
+ sites=_load_sites(result.get("sites")),
582
+ sites_truncated=_boolean(result.get("sites_truncated", False), "sites_truncated"),
583
+ )
584
+
585
+ async def run_python(
586
+ self, ref: AttemptRef, value: WorkflowValue, *, blocking: bool, timeout: float | None = None
587
+ ) -> PythonResult:
588
+ """Run one python-node attempt; raises :class:`WorkerRpcError`, :class:`AttemptTimeout`, or lost."""
589
+ params = {"ref": ref_to_wire(ref), "value": value_to_wire(value)}
590
+ frame = await self._request(
591
+ Method.RUN_PYTHON, params, key=ref_key(ref), ref=ref, timeout=timeout, blocking=blocking
592
+ )
593
+ error = frame.get("error")
594
+ if error is not None:
595
+ data = error.get("data", {})
596
+ if error["code"] != ErrorCode.ATTEMPT_TERMINATED:
597
+ await self.wait_projected(ref, _ordinal(data.get("last_emit_ordinal", 0)))
598
+ raise WorkerRpcError(error["code"], error["message"], data)
599
+ result = frame["result"]
600
+ last_emit_ordinal = _ordinal(result.get("last_emit_ordinal", 0))
601
+ await self.wait_projected(ref, last_emit_ordinal)
602
+ return PythonResult(
603
+ value=value_from_wire(result.get("value"), where="python result"),
604
+ stdout=_captured(result.get("stdout")),
605
+ last_emit_ordinal=last_emit_ordinal,
606
+ )
607
+
608
+ async def eval_outgoing(
609
+ self, ref: AttemptRef, value: WorkflowValue, edge_ids: tuple[str, ...], *, timeout: float | None = None
610
+ ) -> EvaluationResult[dict[str, bool]]:
611
+ params = {"ref": ref_to_wire(ref), "value": value_to_wire(value), "edge_ids": list(edge_ids)}
612
+ result = await self._evaluate(Method.EVAL_OUTGOING, params, ref, timeout)
613
+ decisions = result.get("decisions")
614
+ if not isinstance(decisions, dict) or set(decisions) != set(edge_ids):
615
+ raise ProtocolError("eval_outgoing decisions do not cover the requested edges.")
616
+ return EvaluationResult(
617
+ {edge_id: _boolean(decisions[edge_id], "edge decision") for edge_id in edge_ids},
618
+ _captured(result.get("stdout")),
619
+ )
620
+
621
+ async def eval_loop_until(
622
+ self, ref: AttemptRef, iteration: int, value: WorkflowValue, *, timeout: float | None = None
623
+ ) -> EvaluationResult[bool]:
624
+ params = {"ref": ref_to_wire(ref), "iteration": iteration, "value": value_to_wire(value)}
625
+ result = await self._evaluate(Method.EVAL_LOOP_UNTIL, params, ref, timeout)
626
+ return EvaluationResult(_boolean(result.get("verdict"), "until verdict"), _captured(result.get("stdout")))
627
+
628
+ async def combine(
629
+ self, ref: AttemptRef, sources: tuple[SourceValue, ...], *, timeout: float | None = None
630
+ ) -> EvaluationResult[WorkflowValue]:
631
+ wire = [source_to_wire(source) for source in sources]
632
+ retained = len(canonical_json(wire).encode("utf-8", "surrogatepass"))
633
+ if retained > LIMITS.max_join_retained_bytes:
634
+ raise WorkerRpcError(ErrorCode.VALUE_TOO_LARGE, "join sources exceed max_join_retained_bytes.")
635
+ result = await self._evaluate(Method.COMBINE, {"ref": ref_to_wire(ref), "sources": wire}, ref, timeout)
636
+ return EvaluationResult(
637
+ value_from_wire(result.get("value"), where="combine result"), _captured(result.get("stdout"))
638
+ )
639
+
640
+ async def cancel(self, ref: AttemptRef) -> CancelResult:
641
+ """Cancel exactly this attempt; the ack sums the leak verdicts its terminal envelopes just carried."""
642
+ result = await self._call(Method.CANCEL, {"ref": ref_to_wire(ref)}, key=None)
643
+ outcome = CancelResult(
644
+ cancelled=_boolean(result.get("cancelled"), "cancelled"), leaked_threads_by_pool=_leaks(result)
645
+ )
646
+ if self._lost is not None:
647
+ raise self._lost_error()
648
+ return outcome
649
+
650
+ async def native_output(self) -> NativeOutput:
651
+ result = await self._call(Method.NATIVE_OUTPUT, {}, key=None)
652
+ return NativeOutput(text=str(result["text"]), dropped_bytes=int(result["dropped_bytes"]))
653
+
654
+ async def wait_projected(self, ref: AttemptRef, ordinal: int) -> None:
655
+ """Block until every emit of *ref* up to *ordinal* went through the emit handler."""
656
+ key = ref_key(ref)
657
+ async with self._projection:
658
+ await self._projection.wait_for(lambda: self._lost is not None or self._projected.get(key, 0) >= ordinal)
659
+ if self._projected.get(key, 0) < ordinal:
660
+ raise self._lost_error() # the emits can no longer be projected: converge to worker-lost
661
+
662
+ # ----------------------------------------------------------------- internals
663
+
664
+ async def _evaluate(
665
+ self, method: str, params: dict[str, Any], ref: AttemptRef, timeout: float | None
666
+ ) -> dict[str, Any]:
667
+ deadline = LIMITS.eval_deadline if timeout is None else timeout
668
+ frame = await self._request(method, params, key=None, ref=ref, timeout=deadline)
669
+ error = frame.get("error")
670
+ if error is not None:
671
+ raise WorkerRpcError(error["code"], error["message"], error.get("data"))
672
+ return frame["result"]
673
+
674
+ async def _deadline(
675
+ self, request: Awaitable[dict[str, Any]], pending: _Pending, ref: AttemptRef, timeout: float | None
676
+ ) -> dict[str, Any]:
677
+ if timeout is None:
678
+ return await request
679
+ task = asyncio.ensure_future(request)
680
+ try:
681
+ done, _ = await asyncio.wait({task}, timeout=timeout)
682
+ if done:
683
+ return task.result()
684
+ grace = LIMITS.shutdown_grace
685
+ try:
686
+ outcome = await asyncio.wait_for(self.cancel(ref), timeout=grace)
687
+ except TimeoutError:
688
+ self._mark_lost(f"cancel of {ref.activation_id} was not acknowledged within {grace:g}s")
689
+ raise self._lost_error() from None
690
+ # The worker sends the terminal envelope before acknowledging cancellation.
691
+ if not pending.future.done():
692
+ self._mark_lost(f"cancel of {ref.activation_id} was acknowledged without a terminal envelope")
693
+ frame = pending.future.result()
694
+ payload = frame["error"].get("data", {}) if "error" in frame else frame["result"]
695
+ raise AttemptTimeout(ref, leaked_thread=any(outcome.leaked_threads_by_pool.values()), data=payload)
696
+ finally:
697
+ if not task.done():
698
+ task.cancel()
699
+ await asyncio.gather(task, return_exceptions=True)
700
+
701
+ async def _call(self, method: str, params: dict[str, Any], *, key: str | None) -> dict[str, Any]:
702
+ frame = await self._request(method, params, key=key)
703
+ error = frame.get("error")
704
+ if error is not None:
705
+ raise WorkerRpcError(error["code"], error["message"], error.get("data"))
706
+ return frame["result"]
707
+
708
+ async def _request(
709
+ self,
710
+ method: str,
711
+ params: dict[str, Any],
712
+ *,
713
+ key: str | None,
714
+ ref: AttemptRef | None = None,
715
+ timeout: float | None = None,
716
+ blocking: bool = False,
717
+ ) -> dict[str, Any]:
718
+ # No deadline runs while waiting for capacity. The pending entry owns its slot, even when
719
+ # its caller stops waiting: only a terminal envelope, loss, or a never-sent frame releases it.
720
+ lanes = () if method in _CONTROL_METHODS else ("eval",) if method in _EVAL_METHODS else ("body",)
721
+ if blocking:
722
+ lanes += ("sync",)
723
+ while (
724
+ self._lost is None
725
+ and lanes
726
+ and (
727
+ any(self._in_flight[lane] >= self._capacity[lane] for lane in lanes)
728
+ or len(self._pending) >= LIMITS.max_pending_requests
729
+ )
730
+ ):
731
+ self._capacity_changed.clear()
732
+ await self._capacity_changed.wait()
733
+ if self._lost is not None:
734
+ raise self._lost_error()
735
+ request_id = self._next_id
736
+ self._next_id += 2
737
+ data = _encode({"id": request_id, "method": method, "params": params})
738
+ future: asyncio.Future[dict[str, Any]] = self._loop.create_future()
739
+ pending = _Pending(method, key, future, lanes)
740
+ self._pending[request_id] = pending
741
+ for lane in lanes:
742
+ self._in_flight[lane] += 1
743
+ try:
744
+ request = self._exchange(data, pending)
745
+ return await request if ref is None else await self._deadline(request, pending, ref, timeout)
746
+ finally:
747
+ if not pending.sent:
748
+ self._pop_pending(request_id)
749
+ if not future.done():
750
+ future.cancel()
751
+ elif not future.cancelled():
752
+ future.exception() # loss during transmission may settle it before _exchange awaits it
753
+
754
+ async def _exchange(self, data: bytes, pending: _Pending) -> dict[str, Any]:
755
+ await self._transmit(data, pending=pending)
756
+ return await pending.future
757
+
758
+ def _pop_pending(self, request_id: int) -> _Pending | None:
759
+ pending = self._pending.pop(request_id, None)
760
+ if pending is not None:
761
+ for lane in pending.lanes:
762
+ self._in_flight[lane] -= 1
763
+ self._capacity_changed.set()
764
+ return pending
765
+
766
+ async def _send(self, frame: dict[str, Any]) -> None:
767
+ await self._transmit(_encode(frame))
768
+
769
+ async def _transmit(self, data: bytes, *, pending: _Pending | None = None) -> None:
770
+ self._account(len(data))
771
+ if self._lost is not None:
772
+ raise self._lost_error()
773
+ try:
774
+ self._process.stdin.write(data)
775
+ if pending is not None:
776
+ pending.sent = True
777
+ await self._process.stdin.drain()
778
+ except (OSError, RuntimeError, asyncio.CancelledError) as exc:
779
+ if isinstance(exc, asyncio.CancelledError):
780
+ raise
781
+ self._mark_lost(f"write failed: {exc}")
782
+ raise self._lost_error() from exc
783
+
784
+ def _account(self, size: int) -> None:
785
+ self._frames += 1
786
+ self._bytes += size
787
+ if self._frames > LIMITS.max_run_frames or self._bytes > LIMITS.max_run_bytes:
788
+ self._mark_lost("run frame budget exhausted")
789
+
790
+ def _charge(self, frame: dict[str, Any]) -> None:
791
+ """Charge the threads an attempt left behind to the pool budget.
792
+
793
+ Called once per attempt, from its terminal envelope: each phase of a ref (body, then each
794
+ evaluation) is its own attempt with its own verdict, and a cancel ack only sums the verdicts the
795
+ envelopes it follows already carried.
796
+ """
797
+ payload = frame["error"].get("data", {}) if "error" in frame else frame["result"]
798
+ # Invalid requests may fail before an attempt was registered, without a pool verdict.
799
+ if "error" in frame and "leaked_threads_by_pool" not in payload:
800
+ return
801
+ pools = _leaks(payload)
802
+ for pool, budget in (("body", LIMITS.worker_leak_budget), ("eval", LIMITS.eval_leak_budget)):
803
+ self._leaked_threads_by_pool[pool] += pools[pool]
804
+ if self._leaked_threads_by_pool[pool] >= budget:
805
+ self._mark_lost(f"{pool} thread leak budget exhausted")
806
+ # Do not reuse capacity charged as leaked: its abandoned work may never finish. Keep
807
+ # admission matched to the remaining threads, rather than queueing behind abandoned work.
808
+ self._capacity["eval"] = max(0, LIMITS.eval_thread_pool_size - self._leaked_threads_by_pool["eval"])
809
+ self._capacity["sync"] = max(0, LIMITS.worker_thread_pool_size - self._leaked_threads_by_pool["body"])
810
+
811
+ def _lost_error(self) -> WorkerLostError:
812
+ if self._lost is None:
813
+ raise RuntimeError("The workflow worker has not been marked lost.")
814
+ return self._lost
815
+
816
+ def _stderr_suffix(self) -> str:
817
+ tail = self.stderr_tail.strip()
818
+ return f" (stderr: {tail[-500:]})" if tail else ""
819
+
820
+ def _mark_lost(self, reason: str) -> None:
821
+ if self._lost is not None:
822
+ return
823
+ self._lost = WorkerLostError(reason)
824
+ self._lost_event.set()
825
+ for request_id in tuple(self._pending):
826
+ pending = self._pop_pending(request_id)
827
+ if pending is None:
828
+ raise RuntimeError("A registered worker request disappeared during failure handling.")
829
+ if not pending.future.done():
830
+ pending.future.set_exception(self._lost)
831
+ self._capacity_changed.set() # also wake callers that never registered a request
832
+ if not self._hello.done():
833
+ self._hello.set_exception(self._lost)
834
+ for tasks in self._ask_tasks.values():
835
+ for task in tasks:
836
+ task.cancel()
837
+ self._emits.put_nowait(None)
838
+ self._loop.create_task(self._wake_projection(), name="chrys.workflow.worker.wake")
839
+ with contextlib.suppress(Exception):
840
+ self._process.terminate_tree()
841
+
842
+ async def _wake_projection(self) -> None:
843
+ async with self._projection:
844
+ self._projection.notify_all()
845
+
846
+ async def _read_loop(self) -> None:
847
+ stdout = self._process.stdout
848
+ reason = "worker exited"
849
+ try:
850
+ while True:
851
+ line = await stdout.readline()
852
+ if not line:
853
+ break
854
+ self._account(len(line))
855
+ self._on_frame(decode_frame(line.rstrip(b"\r\n")))
856
+ except asyncio.CancelledError:
857
+ raise
858
+ except (ProtocolError, ValueError) as exc:
859
+ reason = f"protocol error: {exc}"
860
+ except Exception as exc:
861
+ reason = f"reader failed: {exc!r}"
862
+ self._mark_lost(f"{reason}{self._stderr_suffix()}")
863
+
864
+ async def _watch_exit(self) -> None:
865
+ """The exit status settles a dead worker's debts even while a forked child of its holds the pipes open."""
866
+ code = await self._process.wait()
867
+ # Frames written before the exit are still in the pipe: the reader drains them and, at EOF, marks the loss.
868
+ await asyncio.wait({self._reader}, timeout=LIMITS.shutdown_grace)
869
+ self._mark_lost(f"worker exited with status {code}{self._stderr_suffix()}")
870
+
871
+ async def _read_stderr(self) -> None:
872
+ stderr = self._process.stderr
873
+ while chunk := await stderr.read(64 * 1024):
874
+ self._stderr_tail.extend(chunk)
875
+ if len(self._stderr_tail) > STDERR_TAIL_BYTES:
876
+ del self._stderr_tail[:-STDERR_TAIL_BYTES]
877
+
878
+ def _on_frame(self, frame: dict[str, Any]) -> None:
879
+ if self._lost is not None:
880
+ return # lost: its debts are settled already, and an ask would start a handler nobody cancels
881
+ if "method" not in frame:
882
+ pending = self._pop_pending(frame["id"])
883
+ if pending is None:
884
+ return
885
+ if pending.key is not None and pending.method == Method.RUN_PYTHON:
886
+ self._settle_attempt(pending.key)
887
+ if pending.method in _ATTEMPT_METHODS:
888
+ try:
889
+ self._charge(frame)
890
+ except (TypeError, ValueError) as exc:
891
+ self._mark_lost(str(exc))
892
+ if not pending.future.done():
893
+ pending.future.set_exception(self._lost_error())
894
+ return
895
+ if not pending.future.done():
896
+ pending.future.set_result(frame)
897
+ return
898
+ method, params = frame["method"], frame["params"]
899
+ if "id" in frame:
900
+ if method == Method.ASK:
901
+ self._on_ask(frame["id"], params)
902
+ else:
903
+ self._loop.create_task(
904
+ self._respond_error(frame["id"], ErrorCode.UNKNOWN_METHOD, f"unknown reverse request {method!r}.")
905
+ )
906
+ elif method == Method.HELLO:
907
+ if not self._hello.done():
908
+ self._hello.set_result(_hello_from_wire(params))
909
+ elif method == Method.EMIT:
910
+ self._on_emit(params)
911
+
912
+ def _settle_attempt(self, key: str) -> None:
913
+ self._terminal.add(key)
914
+ for task in self._ask_tasks.pop(key, ()):
915
+ task.cancel()
916
+
917
+ def _on_emit(self, params: dict[str, Any]) -> None:
918
+ ref = ref_from_wire(params.get("ref"))
919
+ ordinal, text = params.get("ordinal"), params.get("text")
920
+ if not isinstance(ordinal, int) or isinstance(ordinal, bool) or not isinstance(text, str):
921
+ raise ProtocolError("emit needs an int ordinal and a str text.")
922
+ if ref_key(ref) in self._terminal:
923
+ return
924
+ self._emits.put_nowait((ref, ordinal, text))
925
+
926
+ async def _dispatch_emits(self) -> None:
927
+ while (item := await self._emits.get()) is not None:
928
+ ref, ordinal, text = item
929
+ try:
930
+ if self._emit_handler is not None:
931
+ await self._emit_handler(ref, ordinal, text)
932
+ except asyncio.CancelledError:
933
+ raise
934
+ except Exception:
935
+ logger.warning("workflow emit handler failed for %s", ref.activation_id, exc_info=True)
936
+ finally:
937
+ async with self._projection:
938
+ key = ref_key(ref)
939
+ self._projected[key] = max(self._projected.get(key, 0), ordinal)
940
+ self._projection.notify_all()
941
+
942
+ def _on_ask(self, request_id: int, params: dict[str, Any]) -> None:
943
+ ref = ref_from_wire(params.get("ref"))
944
+ questions = questions_from_wire(params.get("questions"))
945
+ key = ref_key(ref)
946
+ if key in self._terminal:
947
+ self._loop.create_task(self._respond_error(request_id, ErrorCode.ATTEMPT_TERMINATED, "attempt is over."))
948
+ return
949
+ if self._ask_handler is None:
950
+ self._loop.create_task(
951
+ self._respond_error(request_id, ErrorCode.ASK_UNAVAILABLE, "nobody can answer in this run.")
952
+ )
953
+ return
954
+ task = self._loop.create_task(self._serve_ask(request_id, ref, questions), name="chrys.workflow.worker.ask")
955
+ self._ask_tasks[key].add(task)
956
+ self._asks.add(task)
957
+ task.add_done_callback(lambda done, key=key: self._forget_ask(key, done))
958
+
959
+ def _forget_ask(self, key: str, task: asyncio.Task[None]) -> None:
960
+ self._ask_tasks[key].discard(task)
961
+ self._asks.discard(task)
962
+
963
+ async def _serve_ask(self, request_id: int, ref: AttemptRef, questions: tuple[AskUserQuestion, ...]) -> None:
964
+ if self._ask_handler is None:
965
+ raise RuntimeError("Serving a workflow question requires an ask handler.")
966
+ try:
967
+ answers = answers_to_wire(questions, await self._ask_handler(ref, questions))
968
+ except asyncio.CancelledError:
969
+ with contextlib.suppress(WorkerError):
970
+ await self._respond_error(request_id, ErrorCode.ATTEMPT_TERMINATED, "attempt is over.")
971
+ raise
972
+ except AskUnavailable as exc:
973
+ await self._respond_error(request_id, ErrorCode.ASK_UNAVAILABLE, str(exc) or "nobody can answer.")
974
+ except Exception as exc:
975
+ logger.warning("workflow ask handler failed for %s", ref.activation_id, exc_info=True)
976
+ await self._respond_error(request_id, ErrorCode.ASK_UNAVAILABLE, f"ask handler failed: {exc!r}")
977
+ else:
978
+ try:
979
+ await self._send({"id": request_id, "result": {"answers": answers}})
980
+ except WorkerRpcError as exc: # the answer cannot be framed: the worker must not wait forever
981
+ await self._respond_error(request_id, ErrorCode.ASK_UNAVAILABLE, f"answer is not serializable: {exc}")
982
+ except WorkerLostError:
983
+ pass
984
+
985
+ async def _respond_error(self, request_id: int, code: str, message: str) -> None:
986
+ message = _bounded_diagnostic(message.encode("utf-8", "replace").decode("utf-8")) # must always frame
987
+ with contextlib.suppress(WorkerError):
988
+ await self._send({"id": request_id, "error": {"code": code, "message": message, "data": {}}})
989
+
990
+
991
+ def _hello_from_wire(params: dict[str, Any]) -> Hello:
992
+ try:
993
+ return Hello(
994
+ protocol_version=int(params["protocol_version"]),
995
+ python_version=str(params["python_version"]),
996
+ implementation=str(params["implementation"]),
997
+ platform=str(params["platform"]),
998
+ sdk_origin_ok=bool(params["sdk_origin_ok"]),
999
+ sdk_origin_error=None if params.get("sdk_origin_error") is None else str(params["sdk_origin_error"]),
1000
+ )
1001
+ except (KeyError, TypeError, ValueError) as exc:
1002
+ raise ProtocolError(f"hello frame is malformed: {exc!r}") from exc