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,1107 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """SystemReminderMiddleware — injects ``<system-reminder>`` tags per LLM call.
4
+
5
+ A ``ChatMiddleware`` that fires before each LLM call in the tool loop.
6
+ It replaces user messages in ``context.messages`` (a per-call shallow copy)
7
+ with enriched versions whose original user contents come first, followed by
8
+ ``<system-reminder>`` tags. What each reminder says, and the state behind
9
+ it, lives in its source (``reminders/``); this module decides when each one
10
+ is sent, records it and fixes the send order.
11
+
12
+ Reminders stay where they were first sent. Once a request carrying them is
13
+ established, the reminders a user message received are recorded in its
14
+ ``additional_properties`` (``HistoryMarkerKind.SYSTEM_REMINDERS_KEY``), and
15
+ every later call re-renders that record byte-identically — in later turns
16
+ too — so the conversation prefix, and the provider's prompt cache over it,
17
+ survives from one turn to the next. The message's own contents never hold a
18
+ reminder: ``ChatContext.messages`` is a **shallow copy** of the tool loop's
19
+ ``prepped_messages``, so enrichment replaces list elements, never the history
20
+ message objects.
21
+
22
+ Only reminders not already visible are added to the last user message:
23
+
24
+ - the turn line (turn-start time, previous-turn context usage) once per
25
+ turn;
26
+ - event reminders (hooks, workspace file changes, the context-usage warning,
27
+ which ``reminders.context_usage`` arms) once per turn by text;
28
+ - the profile-switch notice while the latest switch notice the turn shows
29
+ differs — a switch made before a retry reaches the retried request, and
30
+ Code → QA → Code → QA across retries sends the last notice again;
31
+ - catalogs — standing context: runtime environment, sub-agent tip, todo
32
+ list, skill catalog, MCP server instructions, archive-catalog pointer —
33
+ whenever the latest version of that catalog in view differs: a changed
34
+ catalog (changed back included), or one whose carrier a context fold took
35
+ out of view, is sent again, and one no longer offered is replaced by a
36
+ notice that it no longer applies where it has one.
37
+
38
+ A call that continues a service-side conversation (``conversation_id``)
39
+ sends only the new messages, so what that conversation holds is out of view:
40
+ the catalogs the last answered call left in it count as in view when the
41
+ next call continues exactly from the handle that answer returned. Any other
42
+ handle, a failed call, or a fresh middleware sends them again.
43
+
44
+ A continuation poll adds and records nothing: the provider ignores its
45
+ messages.
46
+
47
+ Turn reminders are snapshotted at ``prepare_turn()`` and reused for every LLM
48
+ call in that turn. User-initiated injection/retry may refresh the skill
49
+ catalog before the next model call because a newly added skill is a
50
+ meaningful runtime capability change. LAST_WORDS is different: it is
51
+ generated by mid-turn compaction, remains dynamic and is never recorded. Do
52
+ not reintroduce per-call timestamps or other volatile reminder text.
53
+
54
+ Escaping scope: this middleware defangs ``<system-reminder>`` tags in
55
+ user-role message content only. Tool-result content (web fetches, file
56
+ reads, sub-agent output) is passed through verbatim — treat reminder-shaped
57
+ text in tool results as untrusted in downstream prompts and rely on the
58
+ model's own judgement, not on this middleware, to discount it.
59
+ """
60
+
61
+ from __future__ import annotations
62
+
63
+ from collections.abc import Mapping
64
+ from contextvars import ContextVar
65
+ from dataclasses import dataclass, replace
66
+ from pathlib import Path
67
+ from typing import TYPE_CHECKING, Any, Literal, Protocol, cast, get_args
68
+
69
+ from chrys.foundation.models.history_markers import HistoryMarkerKind
70
+ from chrys.foundation.models.turns import opens_turn
71
+ from chrys.kernel import EXCLUDED_KEY, ChatResponse, Message, is_local_history_conversation_id
72
+ from chrys.kernel.middleware import ChatMiddleware
73
+ from chrys.service.profiles.models.schema import DEFAULT_MAX_CONTEXT_TOKENS
74
+
75
+ from .reminders import ReminderSources, build_sources
76
+
77
+ if TYPE_CHECKING:
78
+ from collections.abc import Awaitable, Callable, Iterable, Sequence
79
+
80
+ from chrys.foundation.models.session_env import SessionEnvironment
81
+ from chrys.kernel.middleware import ChatContext
82
+ from chrys.service.context.compaction.last_words_state import LastWordsTurn
83
+ from chrys.service.context.compaction.spill import SpillQuota
84
+
85
+ from .reminders.base import CatalogSource
86
+ from .reminders.file_change import FileChangeTurn
87
+ from .reminders.profile_switch import SwitchSnapshot
88
+
89
+ REMINDER_TAG_OPEN = "<system-reminder>"
90
+ REMINDER_TAG_CLOSE = "</system-reminder>"
91
+ ESCAPED_REMINDER_TAG_OPEN = "&lt;system-reminder&gt;"
92
+ ESCAPED_REMINDER_TAG_CLOSE = "&lt;/system-reminder&gt;"
93
+
94
+ LAST_WORDS_PREFIX = "[LAST_WORDS] "
95
+ """Opens every LAST_WORDS block (rendered by ``compaction.last_words_state``).
96
+
97
+ Load-bearing: ``refresh_last_words_reminder`` finds the blocks to replace by
98
+ it, so it must stay byte-identical."""
99
+
100
+
101
+ def escape_system_reminder_tags(text: str) -> str:
102
+ """Defang user-authored reminder tags before text reaches the model."""
103
+ return text.replace(REMINDER_TAG_OPEN, ESCAPED_REMINDER_TAG_OPEN).replace(
104
+ REMINDER_TAG_CLOSE,
105
+ ESCAPED_REMINDER_TAG_CLOSE,
106
+ )
107
+
108
+
109
+ def wrap_system_reminder(content: str) -> str:
110
+ """Wrap *content* in ``<system-reminder>`` tags."""
111
+ return f"{REMINDER_TAG_OPEN}\n{escape_system_reminder_tags(content)}\n{REMINDER_TAG_CLOSE}"
112
+
113
+
114
+ _ReminderKind = Literal["turn", "event", "switch", "catalog"]
115
+ _REMINDER_KINDS: frozenset[str] = frozenset(get_args(_ReminderKind))
116
+
117
+
118
+ @dataclass(frozen=True, slots=True)
119
+ class _ReminderItem:
120
+ """One reminder text and the rule that decides whether it is sent again.
121
+
122
+ ``turn``: turn-start snapshot, sent once per turn as a group. ``event``:
123
+ sent once per turn per text. ``switch``: the profile-switch notice, sent
124
+ while the turn's latest switch notice differs. ``catalog``: standing
125
+ context, sent while the latest version of the catalog *name* in view
126
+ differs. Records written before the runtime environment, todo list and
127
+ archive pointer became catalogs hold them as ``turn`` items.
128
+ """
129
+
130
+ kind: _ReminderKind
131
+ text: str
132
+ name: str | None = None
133
+ """Which catalog a ``catalog`` item is a version of; None for other kinds."""
134
+
135
+ def to_record(self) -> dict[str, str]:
136
+ record = {"kind": self.kind, "text": self.text}
137
+ if self.name is not None:
138
+ record["name"] = self.name
139
+ return record
140
+
141
+
142
+ def _recorded_reminders(properties: Mapping[str, Any]) -> list[_ReminderItem]:
143
+ """Return the reminders recorded on a user message, skipping malformed entries."""
144
+ raw = properties.get(HistoryMarkerKind.SYSTEM_REMINDERS_KEY)
145
+ if not isinstance(raw, list):
146
+ return []
147
+ items: list[_ReminderItem] = []
148
+ for entry in raw:
149
+ if not isinstance(entry, Mapping):
150
+ continue
151
+ kind = entry.get("kind")
152
+ text = entry.get("text")
153
+ name = entry.get("name")
154
+ # Type checks first: a persisted list or object ``kind`` is unhashable.
155
+ if isinstance(kind, str) and kind in _REMINDER_KINDS and isinstance(text, str) and text:
156
+ items.append(_ReminderItem(cast("_ReminderKind", kind), text, name if isinstance(name, str) else None))
157
+ return items
158
+
159
+
160
+ def _records_of(messages: Sequence[Message]) -> list[list[_ReminderItem]]:
161
+ """Each message's recorded reminders, parsed once per call (none for non-user roles)."""
162
+ return [
163
+ _recorded_reminders(message.additional_properties) if message.role == "user" else [] for message in messages
164
+ ]
165
+
166
+
167
+ def _record_reminders(properties: dict[str, Any], items: Sequence[_ReminderItem]) -> None:
168
+ """Record *items* on a message, always as a new list.
169
+
170
+ Retry rollback restores a props dict from a one-level copy that shares
171
+ its list values, so the stored list itself is never mutated.
172
+ """
173
+ properties[HistoryMarkerKind.SYSTEM_REMINDERS_KEY] = [item.to_record() for item in items]
174
+
175
+
176
+ def _is_visible(message: Message) -> bool:
177
+ return not message.additional_properties.get(EXCLUDED_KEY, False)
178
+
179
+
180
+ def _catalog_key(item: _ReminderItem) -> str:
181
+ """The catalog *item* is a version of; an unnamed entry is its own catalog."""
182
+ return item.name if item.name is not None else item.text
183
+
184
+
185
+ def _latest_switch_notice(items: Iterable[_ReminderItem]) -> str | None:
186
+ """The last profile-switch notice among *items*, if any."""
187
+ latest: str | None = None
188
+ for item in items:
189
+ if item.kind == "switch":
190
+ latest = item.text
191
+ return latest
192
+
193
+
194
+ def _latest_visible_catalogs(
195
+ messages: Sequence[Message],
196
+ records: Sequence[Sequence[_ReminderItem]],
197
+ ) -> dict[str, str]:
198
+ """The latest version of each catalog recorded on user messages still in view.
199
+
200
+ An older version stays in view after a newer one was sent, so a catalog
201
+ compares with its latest version only: A → B → A must send A again.
202
+ """
203
+ latest: dict[str, str] = {}
204
+ for message, items in zip(messages, records, strict=True):
205
+ if message.role != "user" or not _is_visible(message):
206
+ continue
207
+ for item in items:
208
+ if item.kind == "catalog":
209
+ latest[_catalog_key(item)] = item.text
210
+ return latest
211
+
212
+
213
+ def _withdrawn_notices(sources: Iterable[CatalogSource]) -> dict[str, str]:
214
+ """Each catalog's withdrawal notice by record name, for the catalogs that have one.
215
+
216
+ A catalog that disappeared (its provider went away or has nothing to say)
217
+ still shows its last version on an earlier message, so the model is told
218
+ it no longer applies.
219
+ """
220
+ return {source.name: source.withdrawn for source in sources if source.withdrawn is not None}
221
+
222
+
223
+ def _catalog_updates(
224
+ latest: Mapping[str, str],
225
+ current: Sequence[_ReminderItem],
226
+ withdrawn_notices: Mapping[str, str],
227
+ ) -> list[_ReminderItem]:
228
+ """The catalogs to send so that the latest version of each in view is current.
229
+
230
+ A changed catalog (changed back included) goes out again; one no longer
231
+ offered is replaced by its notice in *withdrawn_notices* while an older
232
+ version of it is still in view.
233
+ """
234
+ updates = [item for item in current if latest.get(_catalog_key(item)) != item.text]
235
+ offered = {_catalog_key(item) for item in current}
236
+ for name, text in latest.items():
237
+ withdrawn = withdrawn_notices.get(name)
238
+ if name not in offered and withdrawn is not None and text != withdrawn:
239
+ updates.append(_ReminderItem("catalog", withdrawn, name))
240
+ return updates
241
+
242
+
243
+ def _is_continuation_poll(context: ChatContext) -> bool:
244
+ """A poll retrieves an already-created response; the provider ignores its messages."""
245
+ options = context.options
246
+ return options is not None and options.get("continuation_token") is not None
247
+
248
+
249
+ def _service_handle(conversation_id: object) -> str | None:
250
+ """*conversation_id* when it names a service-side conversation."""
251
+ if isinstance(conversation_id, str) and conversation_id and not is_local_history_conversation_id(conversation_id):
252
+ return conversation_id
253
+ return None
254
+
255
+
256
+ @dataclass
257
+ class TurnReminderState:
258
+ """Intentionally mutable reminder state shared across one logical turn.
259
+
260
+ ContextVar values are copied into child tasks, so the middleware stores one
261
+ state object and mutates it in place. That lets Phase 4 LAST_WORDS updates
262
+ made inside an agent child task remain visible to sibling retry/resume tasks
263
+ that continue the same logical turn.
264
+ """
265
+
266
+ turn_reminders: tuple[str, ...] = ()
267
+ """Turn-start snapshot sent once per turn: time and previous-turn usage."""
268
+ context_warning: str | None = None
269
+ """The context-usage warning, when this turn crosses the threshold."""
270
+ event_reminders: tuple[str, ...] = ()
271
+ """Hook and skill-reference reminders queued for this turn."""
272
+ runtime_hint: str | None = None
273
+ sub_agent_tip: str | None = None
274
+ todo: str | None = None
275
+ """Turn-start todo list (None when empty)."""
276
+ skill_catalog: str | None = None
277
+ mcp_instructions: str | None = None
278
+ switch: SwitchSnapshot | None = None
279
+ """The switch this turn announces, with its notice: a retry keeps the
280
+ opener's turn group, and the notice must still reach the retried request."""
281
+ file_change: FileChangeTurn | None = None
282
+ """The workspace file-change notice drained for this turn."""
283
+ last_words: LastWordsTurn | None = None
284
+ """The Phase 4 slot ``compaction.last_words_state.LastWordsState`` owns; None until it writes one."""
285
+
286
+
287
+ class LastWordsPort(Protocol):
288
+ """The LAST_WORDS state this middleware renders (``compaction.last_words_state.LastWordsState``)."""
289
+
290
+ @property
291
+ def turns(self) -> ReminderTurns:
292
+ """The turn holder the state reads and writes; the middleware keeps its turns there too."""
293
+ ...
294
+
295
+ def begin_turn(self, previous: TurnReminderState | None, *, preserve: bool) -> LastWordsTurn:
296
+ """The new turn's LAST_WORDS slot, given the previous turn's container."""
297
+ ...
298
+
299
+ def discard_restored(self) -> None:
300
+ """Drop the restored stash: the turn ``begin_turn`` just built took it or discarded it."""
301
+ ...
302
+
303
+ def record_paths(self, turn: LastWordsTurn) -> set[str]:
304
+ """The record paths *turn*'s manifest lists."""
305
+ ...
306
+
307
+ def render(self) -> list[str]:
308
+ """The LAST_WORDS blocks the next call appends, unwrapped."""
309
+ ...
310
+
311
+
312
+ class ReminderTurns:
313
+ """Where one middleware keeps its per-turn reminder container.
314
+
315
+ The container lives in a ContextVar, so a task sees the turn its caller
316
+ prepared, and in an active-turn fallback: the engine runs one main turn
317
+ per middleware at a time, and a child or sibling task started under an
318
+ empty context recovers that turn's container from it.
319
+ """
320
+
321
+ def __init__(self) -> None:
322
+ self._local: ContextVar[TurnReminderState | None] = ContextVar(
323
+ f"chrys_reminder_turns_{id(self)}",
324
+ default=None,
325
+ )
326
+ self._active: TurnReminderState | None = None
327
+
328
+ @property
329
+ def active(self) -> TurnReminderState | None:
330
+ """The prepared turn's container, in any task."""
331
+ return self._active
332
+
333
+ def local(self) -> TurnReminderState | None:
334
+ """This task's container, without the active-turn fallback."""
335
+ return self._local.get()
336
+
337
+ def current(self) -> TurnReminderState | None:
338
+ """This task's container, else the active turn's."""
339
+ return self._local.get() or self._active
340
+
341
+ def ensure(self) -> TurnReminderState:
342
+ """The current container, or a new empty one for this task only."""
343
+ state = self.current()
344
+ if state is None:
345
+ state = TurnReminderState()
346
+ self._local.set(state)
347
+ return state
348
+
349
+ def install(self, state: TurnReminderState) -> None:
350
+ """Make *state* this task's container and the active turn's."""
351
+ self._local.set(state)
352
+ self._active = state
353
+
354
+ def install_local(self, state: TurnReminderState) -> None:
355
+ """Make *state* this task's container only."""
356
+ self._local.set(state)
357
+
358
+ def clear(self) -> None:
359
+ """Forget the active turn and this task's container."""
360
+ self._local.set(None)
361
+ self._active = None
362
+
363
+
364
+ @dataclass(frozen=True, eq=False)
365
+ class CurrentRunReminderScope:
366
+ """Opaque service-owned token for one logical main-agent run."""
367
+
368
+ scope_id: int
369
+
370
+
371
+ @dataclass(frozen=True, eq=False)
372
+ class CurrentRunReminderTarget:
373
+ """Opaque service-owned current-run reminder target captured by orchestration."""
374
+
375
+ scope: CurrentRunReminderScope
376
+ phase: Literal["pre_prepare", "prepared"]
377
+ generation: int
378
+ state: TurnReminderState | None = None
379
+
380
+
381
+ class SystemReminderMiddleware(ChatMiddleware):
382
+ """Appends trailing ``<system-reminder>`` content before each LLM call.
383
+
384
+ Each LLM call in the tool loop sees fresh enriched user messages whose
385
+ original user contents come first, followed by the reminders each one
386
+ carried when first sent; the last user message also gets the reminders
387
+ not yet in view. Exact reserved reminder tags in user-role text are
388
+ escaped in the per-call ``ChatContext.messages`` copy; tool results are
389
+ passed through unchanged. Message contents in ``prepped_messages`` and
390
+ session state are never modified — only the reminder record in a user
391
+ message's ``additional_properties``.
392
+ """
393
+
394
+ def __init__(
395
+ self,
396
+ runtime: SessionEnvironment | None = None,
397
+ *,
398
+ max_context_tokens: int = DEFAULT_MAX_CONTEXT_TOKENS,
399
+ warn_threshold_pct: float = 0.50,
400
+ sub_agent_names: list[str] | None = None,
401
+ shell_tool_enabled: bool = False,
402
+ tool_names: list[str] | None = None,
403
+ session_root: Path | None = None,
404
+ file_read_available: bool = False,
405
+ spill_quota: SpillQuota | None = None,
406
+ catalog_pointer_enabled: bool = True,
407
+ skill_catalog_provider: Callable[[], str | None] | None = None,
408
+ todo_state_provider: Callable[[], str | None] | None = None,
409
+ mcp_instructions_provider: Callable[[], str | None] | None = None,
410
+ file_change_provider: Callable[[], str | None] | None = None,
411
+ last_words: LastWordsPort | None = None,
412
+ ) -> None:
413
+ """*last_words* comes from ``create_reminder``; the middleware keeps its turns on the state's holder.
414
+
415
+ Without *last_words* the middleware renders no LAST_WORDS block and
416
+ cannot be bound to a compaction strategy.
417
+ """
418
+ self._sources = build_sources(
419
+ runtime=runtime,
420
+ max_context_tokens=max_context_tokens,
421
+ warn_threshold_pct=warn_threshold_pct,
422
+ shell_tool_enabled=shell_tool_enabled,
423
+ sub_agent_names=sub_agent_names,
424
+ todo_state_provider=todo_state_provider,
425
+ skill_catalog_provider=skill_catalog_provider,
426
+ mcp_instructions_provider=mcp_instructions_provider,
427
+ file_change_provider=file_change_provider,
428
+ tool_names=tool_names,
429
+ session_root=session_root,
430
+ file_read_available=file_read_available,
431
+ spill_quota=spill_quota,
432
+ catalog_pointer_enabled=catalog_pointer_enabled,
433
+ )
434
+ sources = self._sources
435
+ self._withdrawn_notices = _withdrawn_notices(
436
+ (sources.runtime_env, sources.sub_agents, sources.todo, sources.skills, sources.mcp)
437
+ )
438
+ # The catalogs a service-side conversation holds, keyed by the handle
439
+ # its last answered call returned (see the module docstring).
440
+ self._held_catalogs: tuple[str, dict[str, str]] | None = None
441
+ self._pending_hook_reminders: list[str] = []
442
+ self._next_current_run_scope_id = 1
443
+ self._current_run_scopes: set[CurrentRunReminderScope] = set()
444
+ self._pending_current_run_hook_reminders: dict[CurrentRunReminderScope, list[str]] = {}
445
+ self._active_turn_scope: CurrentRunReminderScope | None = None
446
+ self._current_run_reminder_generation = 0
447
+ # One holder for both, so the note Phase 4 writes is in the container
448
+ # prepare_turn installs.
449
+ self._turns = last_words.turns if last_words is not None else ReminderTurns()
450
+ self._last_words = last_words
451
+
452
+ @property
453
+ def sources(self) -> ReminderSources:
454
+ """The content sources this middleware composes; callers reach the switch through it."""
455
+ return self._sources
456
+
457
+ def prepare_turn(
458
+ self,
459
+ *,
460
+ reminder_scope: CurrentRunReminderScope | None = None,
461
+ usage: Mapping[str, Any] | None = None,
462
+ preserve_last_words: bool = False,
463
+ preserve_turn_reminders: bool = False,
464
+ ) -> None:
465
+ """Prepare reminder state before ``executor.run()`` or retry/resume.
466
+
467
+ ``preserve_last_words`` carries Phase 4 progress across retries.
468
+ ``preserve_turn_reminders`` keeps runtime/usage/switch text
469
+ byte-identical for KV-cache continuity on retry.
470
+ """
471
+ previous = self._turns.current()
472
+ snapshot_catalog_pointer = False
473
+ if preserve_turn_reminders and previous is not None:
474
+ snapshot = TurnReminderState(
475
+ turn_reminders=previous.turn_reminders,
476
+ context_warning=previous.context_warning,
477
+ event_reminders=previous.event_reminders,
478
+ runtime_hint=previous.runtime_hint,
479
+ sub_agent_tip=previous.sub_agent_tip,
480
+ todo=previous.todo,
481
+ skill_catalog=previous.skill_catalog,
482
+ mcp_instructions=previous.mcp_instructions,
483
+ switch=previous.switch,
484
+ )
485
+ else:
486
+ snapshot = self._snapshot_turn_reminders(usage=usage or {})
487
+ snapshot.skill_catalog = self._sources.skills.snapshot()
488
+ snapshot.mcp_instructions = self._sources.mcp.snapshot()
489
+ snapshot_catalog_pointer = True
490
+ event_reminders = snapshot.event_reminders
491
+ self._current_run_reminder_generation += 1
492
+ if reminder_scope is not None:
493
+ scoped_reminders = self._pending_current_run_hook_reminders.pop(reminder_scope, [])
494
+ if scoped_reminders:
495
+ event_reminders = (*event_reminders, *scoped_reminders)
496
+ if self._pending_hook_reminders:
497
+ event_reminders = (*event_reminders, *self._pending_hook_reminders)
498
+ self._pending_hook_reminders.clear()
499
+ last_words = (
500
+ self._last_words.begin_turn(previous, preserve=preserve_last_words)
501
+ if self._last_words is not None
502
+ else None
503
+ )
504
+ pointer = self._sources.archive_pointer
505
+ if snapshot_catalog_pointer:
506
+ restored_pointer_count = pointer.restored_record_count if preserve_last_words and previous is None else None
507
+ if restored_pointer_count is not None:
508
+ pointer.capture(restored_pointer_count)
509
+ else:
510
+ # Legacy restored sessions did not persist the turn-start
511
+ # snapshot. Excluding the bounded active manifest is the best
512
+ # compatible approximation, but new saves never rely on it.
513
+ active_manifest_paths = (
514
+ self._last_words.record_paths(last_words)
515
+ if self._last_words is not None and last_words is not None
516
+ else set()
517
+ )
518
+ pointer.capture_live(excluded_relative_paths=active_manifest_paths)
519
+ # The restored stashes are single-shot either way: a preserving turn
520
+ # consumed them; a fresh turn discards them (matching the in-process
521
+ # new-turn semantics).
522
+ pointer.discard_restored()
523
+ if self._last_words is not None:
524
+ self._last_words.discard_restored()
525
+ state = replace(
526
+ snapshot,
527
+ event_reminders=event_reminders,
528
+ file_change=self._sources.file_change.drain(),
529
+ last_words=last_words,
530
+ )
531
+ self._turns.install(state)
532
+ self._active_turn_scope = reminder_scope
533
+ self._sources.profile_switch.begin_turn()
534
+
535
+ def take_undelivered_file_change(self) -> str | None:
536
+ """Pop the prepared notice unless a model-bound request already carried it."""
537
+ state = self._turns.current()
538
+ if state is None:
539
+ return None
540
+ notice = state.file_change
541
+ state.file_change = None
542
+ if notice is None or notice.delivered:
543
+ return None
544
+ return notice.text
545
+
546
+ def create_current_run_scope(self) -> CurrentRunReminderScope:
547
+ """Allocate a service-owned reminder scope for a logical main-agent run."""
548
+ scope = CurrentRunReminderScope(self._next_current_run_scope_id)
549
+ self._next_current_run_scope_id += 1
550
+ self._current_run_scopes.add(scope)
551
+ return scope
552
+
553
+ def capture_current_run_target(
554
+ self,
555
+ reminder_scope: CurrentRunReminderScope,
556
+ ) -> CurrentRunReminderTarget | None:
557
+ """Capture a current-run reminder target if *reminder_scope* is still valid."""
558
+ if reminder_scope not in self._current_run_scopes:
559
+ return None
560
+ active = self._turns.active
561
+ if self._active_turn_scope is reminder_scope and active is not None:
562
+ return CurrentRunReminderTarget(
563
+ scope=reminder_scope,
564
+ phase="prepared",
565
+ generation=self._current_run_reminder_generation,
566
+ state=active,
567
+ )
568
+ return CurrentRunReminderTarget(
569
+ scope=reminder_scope,
570
+ phase="pre_prepare",
571
+ generation=self._current_run_reminder_generation,
572
+ )
573
+
574
+ def queue_hook_reminders_for_current_run(
575
+ self,
576
+ target: CurrentRunReminderTarget,
577
+ reminders: list[str],
578
+ ) -> bool:
579
+ """Queue hook reminders only if the captured current-run target is still valid."""
580
+ if not self._is_current_run_target_valid(target):
581
+ return False
582
+ clean = [reminder for reminder in reminders if reminder]
583
+ if not clean:
584
+ return True
585
+ if target.phase == "prepared" and target.state is not None:
586
+ target.state.event_reminders = (*target.state.event_reminders, *clean)
587
+ return True
588
+ active = self._turns.active
589
+ if self._active_turn_scope is target.scope and active is not None:
590
+ active.event_reminders = (*active.event_reminders, *clean)
591
+ return True
592
+ self._pending_current_run_hook_reminders.setdefault(target.scope, []).extend(clean)
593
+ return True
594
+
595
+ def update_skill_catalog_for_current_run(self, target: CurrentRunReminderTarget) -> bool:
596
+ """Refresh the skill catalog only if the captured current-run target is current."""
597
+ if not self._is_current_run_target_valid(target):
598
+ return False
599
+ active = self._turns.active
600
+ if target.phase == "prepared" and target.state is not None:
601
+ target.state.skill_catalog = self._sources.skills.snapshot()
602
+ elif self._active_turn_scope is target.scope and active is not None:
603
+ active.skill_catalog = self._sources.skills.snapshot()
604
+ return True
605
+
606
+ def set_skill_catalog_for_current_run(
607
+ self,
608
+ target: CurrentRunReminderTarget,
609
+ skill_catalog: str | None,
610
+ ) -> bool:
611
+ """Set the skill catalog only if the captured current-run target is current."""
612
+ if not self._is_current_run_target_valid(target):
613
+ return False
614
+ active = self._turns.active
615
+ if target.phase == "prepared" and target.state is not None:
616
+ target.state.skill_catalog = skill_catalog
617
+ elif self._active_turn_scope is target.scope and active is not None:
618
+ active.skill_catalog = skill_catalog
619
+ return True
620
+
621
+ def is_current_run_target_valid(self, target: CurrentRunReminderTarget) -> bool:
622
+ """Return whether a captured current-run reminder target is still valid."""
623
+ return self._is_current_run_target_valid(target)
624
+
625
+ def expire_current_run_scope(self, reminder_scope: CurrentRunReminderScope) -> None:
626
+ """Expire a current-run scope and discard stale reminders for it."""
627
+ self._current_run_scopes.discard(reminder_scope)
628
+ self._pending_current_run_hook_reminders.pop(reminder_scope, None)
629
+ if self._active_turn_scope is reminder_scope:
630
+ self._active_turn_scope = None
631
+ self._turns.clear()
632
+
633
+ def renders_last_words(self, state: object) -> bool:
634
+ """Whether *state* is the LAST_WORDS state this middleware renders."""
635
+ return self._last_words is not None and self._last_words is state
636
+
637
+ def refresh_last_words_reminder(self, messages: list[Any]) -> int | None:
638
+ """Rewrite the ``[LAST_WORDS]`` reminder on a per-call message list.
639
+
640
+ Phase 4 compaction runs *below* this middleware (inside the client's
641
+ message prep), so the LLM call that triggered it was enriched before
642
+ the new note existed — without this refresh, that call would go out
643
+ with its tool-call history dropped but only a stale note (or none)
644
+ in its place, and the model would restart the task from scratch.
645
+
646
+ Rebuilds the last user message with the current note rendered
647
+ exactly as the next call's enrichment will render it, keeping the
648
+ request prefix byte-stable across calls. The list entry is
649
+ *replaced* with a fresh ``Message`` — the existing object is never
650
+ mutated, because when enrichment was skipped it is the live history
651
+ message. ``additional_properties`` is shared by reference so
652
+ exclusion flags keep writing through to the history object; the
653
+ caller must recompute any token annotation riding in it, since the
654
+ shared count still describes the pre-refresh contents. Returns the
655
+ index of the replaced entry, or ``None`` when the list was left
656
+ unmodified.
657
+ """
658
+ message, idx = self._find_user_message(messages)
659
+ if message is None:
660
+ return None
661
+ reminders = self._last_words_reminders()
662
+ kept = [c for c in message.contents if not self._is_last_words_reminder_content(c)]
663
+ if len(kept) == len(message.contents) and not reminders:
664
+ return None
665
+ messages[idx] = self._rebuild(message, [*kept, *(wrap_system_reminder(r) for r in reminders)])
666
+ return idx
667
+
668
+ def restore_folded_reminders(self, messages: list[Message]) -> int | None:
669
+ """Re-send catalogs (standing context) a context fold just took out of view on this call.
670
+
671
+ A catalog is sent once and then only re-rendered from the user
672
+ message that recorded it. Folds (``compress_context``, Phase 3) run
673
+ *below* this middleware, inside the client's message prep, and
674
+ exclude whole earlier turns: when the carrier goes, the call that
675
+ triggered the fold would otherwise go out without the catalog. The
676
+ missing catalogs are appended to the last user message, before any
677
+ LAST_WORDS block, and recorded on it, so later calls and turns render
678
+ them there.
679
+
680
+ The list entry is replaced with a fresh ``Message`` sharing the
681
+ original's ``additional_properties``, as ``refresh_last_words_reminder``
682
+ does; the caller must recompute token annotations from the returned
683
+ index. Returns ``None`` when nothing was missing.
684
+ """
685
+ message, idx = self._find_user_message(messages)
686
+ state = self._turns.current()
687
+ if message is None or state is None or not _is_visible(message):
688
+ return None
689
+ missing = _catalog_updates(
690
+ _latest_visible_catalogs(messages, self._rendered_records(messages)),
691
+ self._catalog_items(state),
692
+ self._withdrawn_notices,
693
+ )
694
+ if not missing:
695
+ return None
696
+ kept = [c for c in message.contents if not self._is_last_words_reminder_content(c)]
697
+ last_words = [c for c in message.contents if self._is_last_words_reminder_content(c)]
698
+ messages[idx] = self._rebuild(
699
+ message, [*kept, *(wrap_system_reminder(item.text) for item in missing), *last_words]
700
+ )
701
+ _record_reminders(
702
+ message.additional_properties,
703
+ [*_recorded_reminders(message.additional_properties), *missing],
704
+ )
705
+ return idx
706
+
707
+ @staticmethod
708
+ def _is_last_words_reminder_content(content: Any) -> bool:
709
+ """Whether a content item is a middleware-authored LAST_WORDS reminder.
710
+
711
+ User-authored reminder tags are escaped during enrichment, so a raw
712
+ wrapped ``[LAST_WORDS]`` block can only come from the LAST_WORDS state.
713
+ """
714
+ return (
715
+ content.type == "text"
716
+ and content.text is not None
717
+ and content.text.startswith(f"{REMINDER_TAG_OPEN}\n{LAST_WORDS_PREFIX}")
718
+ )
719
+
720
+ def queue_hook_reminders(self, reminders: list[str], *, for_next_turn: bool = False) -> None:
721
+ """Queue hook-provided reminders for the current or next user turn."""
722
+ clean = [r for r in reminders if r]
723
+ if not clean:
724
+ return
725
+ state = None if for_next_turn else self._turns.current()
726
+ if state is not None:
727
+ state.event_reminders = (*state.event_reminders, *clean)
728
+ return
729
+ self._pending_hook_reminders.extend(clean)
730
+
731
+ def queue_drained_injection_reminders(self, reminders: list[str]) -> None:
732
+ """Queue a drained injection's hook/skill reminders for the call that sends it.
733
+
734
+ The injection middleware runs before this one in the same call, so
735
+ they land on the injected message; an injection that never reaches a
736
+ call (cancelled, or returned to the input box) never queues them.
737
+ """
738
+ clean = [reminder for reminder in reminders if reminder]
739
+ if clean:
740
+ state = self._turns.ensure()
741
+ state.event_reminders = (*state.event_reminders, *clean)
742
+
743
+ def update_skill_catalog_for_active_turn(self) -> None:
744
+ """Refresh the active turn's skill-catalog reminder from the provider."""
745
+ state = self._turns.current()
746
+ if state is None:
747
+ return
748
+ state.skill_catalog = self._sources.skills.snapshot()
749
+
750
+ # ------------------------------------------------------------------
751
+ # ChatMiddleware interface
752
+ # ------------------------------------------------------------------
753
+
754
+ async def process(
755
+ self,
756
+ context: ChatContext,
757
+ call_next: Callable[[], Awaitable[None]],
758
+ ) -> None:
759
+ reminder_items = self._build_reminder_items()
760
+ last_words_reminders = self._last_words_reminders()
761
+ applied_switch: SwitchSnapshot | None = None
762
+ # Replace user messages in the per-call shallow copy only. This
763
+ # renders recorded reminders and defangs user-authored reminder tags
764
+ # without modifying the tool loop's prepped_messages or
765
+ # SessionContext.input_messages. Tool-result messages are
766
+ # intentionally not escaped here.
767
+ # ``ChatMiddlewareLayer`` always gives ``ChatContext`` a fresh mutable
768
+ # list; the kernel constructor's public annotation is the broader
769
+ # ``Sequence`` accepted at its external boundary.
770
+ messages = cast("list[Message]", context.messages)
771
+ target, target_idx = self._find_user_message(messages)
772
+ if target is not None and (not _is_visible(target) or _is_continuation_poll(context)):
773
+ # An excluded message never reaches the wire, and a poll's
774
+ # messages are ignored: recording on either would claim a
775
+ # delivery that never happened.
776
+ target, target_idx = None, -1
777
+ records = self._rendered_records(messages)
778
+ handle = _service_handle(context.options.get("conversation_id") if context.options is not None else None)
779
+ held = self._held_catalogs[1] if self._held_catalogs is not None and self._held_catalogs[0] == handle else {}
780
+ in_view = {**held, **_latest_visible_catalogs(messages, records)}
781
+ turn_recorded = self._turn_recorded(messages, records, target_idx) if target is not None else []
782
+ new_items = self._unsent_reminders(in_view, turn_recorded, reminder_items) if target is not None else []
783
+ for msg_idx, message in enumerate(messages):
784
+ if message.role != "user" or not _is_visible(message):
785
+ # Excluded messages never reach the wire; nothing to render.
786
+ continue
787
+ reminders = [item.text for item in records[msg_idx]]
788
+ message_last_words_reminders: list[str] = []
789
+ if msg_idx == target_idx:
790
+ reminders.extend(item.text for item in new_items)
791
+ message_last_words_reminders = last_words_reminders
792
+ if reminders or message_last_words_reminders or self._has_system_reminder_tags(message):
793
+ messages[msg_idx] = self._create_enriched(message, reminders, message_last_words_reminders)
794
+ if target is not None:
795
+ if new_items:
796
+ context.request_message_observers.append(
797
+ self._record_on_delivery(target.additional_properties, tuple(new_items))
798
+ )
799
+ state = self._turns.current()
800
+ if state is not None and any(
801
+ item.kind == "event" and item.text == state.context_warning for item in new_items
802
+ ):
803
+ context.request_message_observers.append(self._sources.context_warning.delivered)
804
+ if state is not None and state.file_change is not None:
805
+ # Delivered means an established provider request, not
806
+ # a completed middleware chain: lazy streaming inner
807
+ # middleware returns a proxy from call_next() and only
808
+ # reaches the final handler at first stream
809
+ # consumption. Observers run immediately before every
810
+ # final-handler invocation, so a run cancelled before
811
+ # consumption keeps the notice requeueable.
812
+ context.request_message_observers.append(state.file_change.mark_delivered)
813
+ if (
814
+ state is not None
815
+ and state.switch is not None
816
+ and _latest_switch_notice((*turn_recorded, *new_items)) == state.switch.notice
817
+ ):
818
+ # Only a switch whose notice is the latest this request
819
+ # carries (sent now or earlier in the turn) is consumed.
820
+ applied_switch = state.switch
821
+
822
+ polled = _is_continuation_poll(context)
823
+ if not polled:
824
+ # Unknown until this call is answered: a failed call may or may
825
+ # not have left its catalogs in the conversation.
826
+ self._held_catalogs = None
827
+ await call_next()
828
+ if not polled:
829
+ carried = in_view | {_catalog_key(item): item.text for item in new_items if item.kind == "catalog"}
830
+ self._hold_catalogs_on_answer(context, carried)
831
+ if applied_switch is not None:
832
+ self._sources.profile_switch.carried(applied_switch)
833
+
834
+ # ------------------------------------------------------------------
835
+ # Internal helpers
836
+ # ------------------------------------------------------------------
837
+
838
+ def _build_reminders(self) -> list[str]:
839
+ """Every reminder text this turn offers, whether or not already sent."""
840
+ return [item.text for item in self._build_reminder_items()]
841
+
842
+ def _build_reminder_items(self) -> list[_ReminderItem]:
843
+ state = self._turns.local()
844
+ if state is None:
845
+ # Fallback for direct middleware use without ``prepare_turn()``.
846
+ # It deliberately avoids consuming pending profile switches:
847
+ # switch reminders are turn-scoped and must be snapshotted by
848
+ # the caller that starts the turn.
849
+ state = self._snapshot_turn_reminders(usage={}, include_pending_switch=False)
850
+ state.skill_catalog = self._sources.skills.snapshot()
851
+ state.mcp_instructions = self._sources.mcp.snapshot()
852
+ self._turns.install_local(state)
853
+ return self._compose_items(state)
854
+
855
+ def _compose_items(self, state: TurnReminderState) -> list[_ReminderItem]:
856
+ """Every reminder *state* offers, in send order, each with the kind that decides its re-send.
857
+
858
+ The one place the send order is written: the turn line, the
859
+ context-usage warning, the profile-switch notice, the event inbox in
860
+ queue order, the workspace file change, then the catalogs in their
861
+ fixed order. ``_unsent_reminders`` keeps this order and appends the
862
+ withdrawals; LAST_WORDS blocks go after everything, unrecorded.
863
+ """
864
+ items = [_ReminderItem("turn", text) for text in state.turn_reminders]
865
+ if state.context_warning:
866
+ items.append(_ReminderItem("event", state.context_warning))
867
+ if state.switch is not None:
868
+ items.append(_ReminderItem("switch", state.switch.notice))
869
+ items.extend(_ReminderItem("event", text) for text in state.event_reminders)
870
+ if state.file_change is not None:
871
+ items.append(_ReminderItem("event", state.file_change.text))
872
+ items.extend(self._catalog_items(state))
873
+ return items
874
+
875
+ def _catalog_items(self, state: TurnReminderState) -> list[_ReminderItem]:
876
+ """The standing context this turn offers, one item per catalog name."""
877
+ return [
878
+ _ReminderItem("catalog", text, name)
879
+ for name, text in (
880
+ (self._sources.runtime_env.name, state.runtime_hint),
881
+ (self._sources.sub_agents.name, state.sub_agent_tip),
882
+ (self._sources.todo.name, state.todo),
883
+ (self._sources.skills.name, state.skill_catalog),
884
+ (self._sources.mcp.name, state.mcp_instructions),
885
+ (self._sources.archive_pointer.name, self._sources.archive_pointer.current_text()),
886
+ )
887
+ if text
888
+ ]
889
+
890
+ @staticmethod
891
+ def _turn_recorded(
892
+ messages: list[Message],
893
+ records: list[list[_ReminderItem]],
894
+ target_idx: int,
895
+ ) -> list[_ReminderItem]:
896
+ """Reminders recorded on the current turn's user messages still in view.
897
+
898
+ Messages a fold excluded (or left out of the list entirely) no longer
899
+ show theirs. The current turn runs from its opener to the last user
900
+ message, injected and resumed messages included; with no opener in
901
+ the list, from its start.
902
+ """
903
+ turn_start = next((i for i in range(target_idx, -1, -1) if opens_turn(messages[i])), 0)
904
+ return [
905
+ item
906
+ for idx in range(turn_start, target_idx + 1)
907
+ if messages[idx].role == "user" and _is_visible(messages[idx])
908
+ for item in records[idx]
909
+ ]
910
+
911
+ def _unsent_reminders(
912
+ self,
913
+ catalogs_in_view: Mapping[str, str],
914
+ turn_recorded: list[_ReminderItem],
915
+ items: list[_ReminderItem],
916
+ ) -> list[_ReminderItem]:
917
+ """Return the reminders the last user message still has to carry.
918
+
919
+ Only reminders in view count as sent: *turn_recorded* for turn, event
920
+ and switch reminders, *catalogs_in_view* (the latest version of each)
921
+ for catalogs.
922
+ """
923
+ turn_started = any(item.kind == "turn" for item in turn_recorded)
924
+ turn_texts = {item.text for item in turn_recorded}
925
+ latest_switch = _latest_switch_notice(turn_recorded)
926
+ catalogs = _catalog_updates(
927
+ catalogs_in_view,
928
+ [item for item in items if item.kind == "catalog"],
929
+ self._withdrawn_notices,
930
+ )
931
+ unsent: list[_ReminderItem] = []
932
+ for item in items:
933
+ if item.kind == "turn":
934
+ sent = turn_started
935
+ elif item.kind == "switch":
936
+ # Switches compare with the turn's latest one only: switching
937
+ # Code → QA → Code → QA across retries repeats an earlier text.
938
+ sent = latest_switch == item.text
939
+ latest_switch = item.text
940
+ elif item.kind == "event":
941
+ # Two hooks can queue the same text; it still goes once.
942
+ sent = item.text in turn_texts
943
+ turn_texts.add(item.text)
944
+ else:
945
+ sent = item not in catalogs
946
+ if not sent:
947
+ unsent.append(item)
948
+ # Withdrawal notices for catalogs no longer offered go last.
949
+ unsent.extend(item for item in catalogs if item not in items)
950
+ return unsent
951
+
952
+ def _rendered_records(self, messages: Sequence[Message]) -> list[list[_ReminderItem]]:
953
+ """Each message's recorded reminders as this session renders them, parsed once per call."""
954
+ return [[self._rendered(item) for item in items] for items in _records_of(messages)]
955
+
956
+ def _rendered(self, item: _ReminderItem) -> _ReminderItem:
957
+ """A recorded reminder as this session renders it.
958
+
959
+ A recorded archive pointer names this session's catalog
960
+ (``ArchivePointerSource.retarget``). Comparisons read the rendered
961
+ text too, so a forked or moved session does not send it again.
962
+ """
963
+ pointer = self._sources.archive_pointer
964
+ is_pointer_kind = item.kind == "turn" or (item.kind == "catalog" and item.name == pointer.name)
965
+ retargeted = pointer.retarget(item.text) if is_pointer_kind else None
966
+ if retargeted is None:
967
+ return item
968
+ return replace(item, text=retargeted)
969
+
970
+ def _hold_catalogs_on_answer(self, context: ChatContext, carried: dict[str, str]) -> None:
971
+ """Remember *carried* under the handle the answer to this call returns."""
972
+
973
+ def _hold(response: ChatResponse) -> ChatResponse:
974
+ handle = _service_handle(response.conversation_id)
975
+ self._held_catalogs = (handle, carried) if handle is not None else None
976
+ return response
977
+
978
+ result = context.result
979
+ if isinstance(result, ChatResponse):
980
+ _hold(result)
981
+ elif result is not None:
982
+ # A stream is answered once it finalizes.
983
+ context.stream_result_hooks.append(_hold)
984
+
985
+ @staticmethod
986
+ def _record_on_delivery(
987
+ properties: dict[str, Any],
988
+ items: tuple[_ReminderItem, ...],
989
+ ) -> Callable[[Sequence[Message]], None]:
990
+ """Return a request observer that records *items* once a request carries them.
991
+
992
+ Observers run before every final-handler invocation, and again with
993
+ the prepared provider request; a validation retry re-sends the same
994
+ enriched list. The record is appended once per enrichment.
995
+ """
996
+ recorded = False
997
+
998
+ def _observe(_messages: Sequence[Message]) -> None:
999
+ nonlocal recorded
1000
+ if recorded:
1001
+ return
1002
+ recorded = True
1003
+ _record_reminders(properties, [*_recorded_reminders(properties), *items])
1004
+
1005
+ return _observe
1006
+
1007
+ def _is_current_run_target_valid(self, target: CurrentRunReminderTarget) -> bool:
1008
+ if target.scope not in self._current_run_scopes:
1009
+ return False
1010
+ if target.phase == "prepared":
1011
+ return self._active_turn_scope is target.scope and self._turns.active is target.state
1012
+ if self._active_turn_scope is target.scope and self._turns.active is not None:
1013
+ return True
1014
+ return self._active_turn_scope is None and target.generation == self._current_run_reminder_generation
1015
+
1016
+ def _snapshot_turn_reminders(
1017
+ self,
1018
+ *,
1019
+ usage: Mapping[str, Any],
1020
+ include_pending_switch: bool = True,
1021
+ ) -> TurnReminderState:
1022
+ """Snapshot the turn-level reminders, with the pending switch and its notice if included.
1023
+
1024
+ The returned state holds the turn line, the context-usage warning,
1025
+ the standing runtime/sub-agent/todo context and the switch; the
1026
+ caller adds the rest.
1027
+ """
1028
+ state = TurnReminderState(
1029
+ runtime_hint=self._sources.runtime_env.snapshot(),
1030
+ sub_agent_tip=self._sources.sub_agents.snapshot(),
1031
+ todo=self._sources.todo.snapshot(),
1032
+ )
1033
+
1034
+ # The turn line: time at turn start and the previous turn's usage.
1035
+ turn_line = self._sources.turn_line.snapshot(usage)
1036
+ state.context_warning = self._sources.context_warning.snapshot(usage)
1037
+ if turn_line is not None:
1038
+ state.turn_reminders = (turn_line,)
1039
+
1040
+ # Profile switch (stable for this turn; sent as an event)
1041
+ if include_pending_switch:
1042
+ state.switch = self._sources.profile_switch.snapshot()
1043
+
1044
+ return state
1045
+
1046
+ def _last_words_reminders(self) -> list[str]:
1047
+ """The LAST_WORDS blocks this call appends after every other reminder."""
1048
+ return self._last_words.render() if self._last_words is not None else []
1049
+
1050
+ @staticmethod
1051
+ def _find_user_message(messages: list[Any]) -> tuple[Message | None, int]:
1052
+ """Return the last user ``Message`` and its index, or ``(None, -1)``."""
1053
+ for i in range(len(messages) - 1, -1, -1):
1054
+ if messages[i].role == "user":
1055
+ return messages[i], i
1056
+ return None, -1
1057
+
1058
+ @staticmethod
1059
+ def _has_system_reminder_tags(message: Message) -> bool:
1060
+ """Return true if a message contains raw reserved reminder tags."""
1061
+ for c in message.contents:
1062
+ if (
1063
+ c.type == "text"
1064
+ and c.text is not None
1065
+ and (REMINDER_TAG_OPEN in c.text or REMINDER_TAG_CLOSE in c.text)
1066
+ ):
1067
+ return True
1068
+ return False
1069
+
1070
+ @classmethod
1071
+ def _create_enriched(
1072
+ cls,
1073
+ original: Message,
1074
+ turn_reminders: list[str],
1075
+ last_words_reminders: list[str],
1076
+ ) -> Message:
1077
+ """Create a new ``Message`` with user contents first and reminders last.
1078
+
1079
+ This keeps the user's message as the first content item while leaving
1080
+ platform context close to the end of the message.
1081
+ """
1082
+ reminder_contents = [wrap_system_reminder(r) for r in [*turn_reminders, *last_words_reminders]]
1083
+ # Extract text from Content objects so Message() can rebuild them.
1084
+ # Non-text Content (images, etc.) is passed through as-is.
1085
+ original_items: list[Any] = []
1086
+ for c in original.contents:
1087
+ if c.type == "text" and c.text is not None:
1088
+ original_items.append(escape_system_reminder_tags(c.text))
1089
+ else:
1090
+ original_items.append(c)
1091
+ return cls._rebuild(original, [*original_items, *reminder_contents])
1092
+
1093
+ @staticmethod
1094
+ def _rebuild(original: Message, contents: list[Any]) -> Message:
1095
+ """A fresh wrapper for *contents* that keeps the original's identity fields.
1096
+
1097
+ ``additional_properties`` is shared by reference — the sanctioned
1098
+ write-through channel for exclusion flags and the reminder record.
1099
+ """
1100
+ rebuilt = Message(
1101
+ original.role,
1102
+ contents,
1103
+ author_name=original.author_name,
1104
+ message_id=original.message_id,
1105
+ )
1106
+ rebuilt.additional_properties = original.additional_properties
1107
+ return rebuilt