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,1071 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Workspace tools — read_file, write_file, edit_file.
4
+
5
+ Approval for ``write_file`` and ``edit_file`` is handled by
6
+ ``ApprovalMiddleware`` (a ``FunctionMiddleware``); the tool layer itself never
7
+ pauses for approval.
8
+
9
+ Shell execution tools are in shell.py as instance tools.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import secrets
16
+ import stat
17
+ import tempfile
18
+ import threading
19
+ from dataclasses import dataclass
20
+ from typing import TYPE_CHECKING, Annotated
21
+
22
+ from chrys.foundation.platform import get_platform
23
+ from chrys.foundation.platform.files import replace_with_retry, surrogate_safe_text
24
+ from chrys.foundation.platform.paths import resolve_existing_path, resolve_workspace_path
25
+ from chrys.foundation.text.images import ImageProcessingError, load_image_file
26
+ from chrys.foundation.text.lines import normalize_line_endings, split_lines
27
+ from chrys.kernel import Content
28
+ from chrys.service.tools.approval_targets import ApprovedTargets, approved_write_path, file_write_target
29
+ from chrys.service.tools.kinds import KIND_FILESYSTEM_READ, KIND_FILESYSTEM_WRITE, tool
30
+ from chrys.service.tools.result_metadata import tool_error
31
+ from chrys.service.tools.session_artifacts import (
32
+ is_document_artifact_handle,
33
+ resolve_document_image_artifact_handle,
34
+ resolve_document_markdown_artifact_handle,
35
+ resolve_tool_session_dir,
36
+ )
37
+ from chrys.service.tools.workspace_paths import missing_base_cwd_error
38
+
39
+ if TYPE_CHECKING:
40
+ from pathlib import Path
41
+
42
+ from chrys.foundation.models.session_env import SessionEnvironment
43
+
44
+ _DEFAULT_MAX_TOKENS = 5000
45
+ _MAX_LINE_DISPLAY_CHARS = 2048
46
+ _NEW_FILE_MODE = 0o644
47
+ _NEW_EXECUTABLE_FILE_MODE = 0o755
48
+ _ATOMIC_TEMP_CREATE_ATTEMPTS = 100
49
+ _SHELL_SCRIPT_EXTENSIONS = frozenset({".sh", ".bash", ".zsh", ".fish", ".ksh", ".csh", ".tcsh", ".command"})
50
+
51
+
52
+ @dataclass(slots=True)
53
+ class FileToolPreviewError:
54
+ """A deterministic file-tool failure discovered before execution."""
55
+
56
+ kind: str
57
+ message: str
58
+ resolved_path: str = ""
59
+
60
+
61
+ @dataclass(slots=True)
62
+ class WriteFilePlan:
63
+ """Prepared write_file operation with final in-memory content."""
64
+
65
+ resolved_path: str
66
+ write_content: str
67
+ display_content: str
68
+ before_display_content: str = ""
69
+ overwrites_existing: bool = False
70
+ content_only_reason: str = ""
71
+
72
+
73
+ @dataclass(slots=True)
74
+ class EditFilePlan:
75
+ """Prepared edit_file operation with before/after preview and write data."""
76
+
77
+ resolved_path: str
78
+ encoding: str
79
+ decode_errors: str
80
+ before_text: str
81
+ after_text: str
82
+ write_content: str
83
+ replacements: int
84
+ snippet: str
85
+
86
+
87
+ def _new_posix_file_mode(path: str, content: str) -> int:
88
+ extension = os.path.splitext(path)[1].lower()
89
+ if content.startswith("#!") or extension in _SHELL_SCRIPT_EXTENSIONS:
90
+ return _NEW_EXECUTABLE_FILE_MODE
91
+ return _NEW_FILE_MODE
92
+
93
+
94
+ def _posix_write_mode(path: str, content: str) -> tuple[int, bool]:
95
+ """Return requested rwx mode and whether it must exactly match an existing file."""
96
+ try:
97
+ # Preserve existing user/group/other rwx bits. Special bits are
98
+ # deliberately cleared when replacing file contents.
99
+ return stat.S_IMODE(os.stat(path).st_mode) & 0o777, True
100
+ except OSError:
101
+ # A dangling, looping, or inaccessible symlink was replaceable before
102
+ # mode preservation was added. Treat every stat failure as a new-file
103
+ # replacement and let the actual temp creation/replace report any real
104
+ # parent-directory failure.
105
+ return _new_posix_file_mode(path, content), False
106
+
107
+
108
+ def _open_posix_atomic_temp(parent: str, mode: int) -> tuple[int, str]:
109
+ """Securely create a same-directory temp file with *mode* filtered by umask."""
110
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
111
+ for _ in range(_ATOMIC_TEMP_CREATE_ATTEMPTS):
112
+ tmp_path = os.path.join(parent, f".chrys-{secrets.token_hex(12)}.tmp")
113
+ try:
114
+ # Unlike tempfile.mkstemp's fixed 0600, os.open applies the
115
+ # process umask to the requested new-file mode without reading or
116
+ # mutating that process-global setting.
117
+ return os.open(tmp_path, flags, mode), tmp_path
118
+ except FileExistsError:
119
+ continue
120
+ raise FileExistsError("failed to allocate a unique atomic-write temporary file")
121
+
122
+
123
+ def _atomic_write(path: str, content: str, encoding: str = "utf-8", errors: str = "strict") -> None:
124
+ """Write *content* to *path* atomically via temp-file + rename.
125
+
126
+ Writes to a temporary file in the same directory (same filesystem) and
127
+ then calls ``os.replace()`` which is atomic on POSIX and Windows/NTFS.
128
+ If the write or flush fails, the temp file is removed and the original
129
+ file is left untouched.
130
+ """
131
+ path = approved_write_path(path)
132
+ parent = os.path.dirname(path) or "."
133
+ platform = get_platform()
134
+ target_mode: int | None = None
135
+ preserve_existing_mode = False
136
+ if not platform.is_windows:
137
+ target_mode, preserve_existing_mode = _posix_write_mode(path, content)
138
+ fd = None
139
+ tmp_path = ""
140
+ try:
141
+ if platform.is_windows:
142
+ fd, tmp_path = tempfile.mkstemp(dir=parent, suffix=".tmp")
143
+ else:
144
+ if target_mode is None:
145
+ raise RuntimeError("A POSIX atomic write requires a target file mode.")
146
+ fd, tmp_path = _open_posix_atomic_temp(parent, target_mode)
147
+ with os.fdopen(fd, "w", encoding=encoding, errors=errors, newline="") as f:
148
+ fd = None # os.fdopen takes ownership of the fd
149
+ f.write(content)
150
+ f.flush()
151
+ if preserve_existing_mode:
152
+ # Existing modes override umask and must be restored exactly.
153
+ # This must run while fdopen still owns an open descriptor.
154
+ if target_mode is None:
155
+ raise RuntimeError("A POSIX atomic write requires a target file mode.")
156
+ os.fchmod(f.fileno(), target_mode)
157
+ os.fsync(f.fileno())
158
+ replace_with_retry(tmp_path, path)
159
+ except BaseException:
160
+ # Clean up the temp file on any failure
161
+ if fd is not None:
162
+ os.close(fd)
163
+ if tmp_path and os.path.exists(tmp_path):
164
+ os.unlink(tmp_path)
165
+ raise
166
+
167
+
168
+ def _detect_line_ending(raw: bytes, path: str = "") -> str:
169
+ """Detect the predominant line ending in raw file bytes.
170
+
171
+ Falls back to ``_default_eol_for_new_file(path)`` when the file contains
172
+ no line endings (e.g. single-line or empty files).
173
+ """
174
+ crlf = raw.count(b"\r\n")
175
+ cr = raw.count(b"\r") - crlf # standalone \r only
176
+ lf = raw.count(b"\n") - crlf # standalone \n only
177
+ if crlf >= lf and crlf >= cr and crlf > 0:
178
+ return "\r\n"
179
+ if cr > lf and cr > 0:
180
+ return "\r"
181
+ if lf > 0:
182
+ return "\n"
183
+ # No line endings found — use the smart default for this file type / platform
184
+ return _default_eol_for_new_file(path)
185
+
186
+
187
+ # File extensions that MUST use a specific line ending regardless of platform.
188
+ _FORCE_LF_EXTENSIONS = _SHELL_SCRIPT_EXTENSIONS | frozenset(
189
+ {
190
+ ".py",
191
+ ".rb",
192
+ ".pl",
193
+ ".php", # interpreters with shebang
194
+ ".yml",
195
+ ".yaml", # YAML is LF-only by spec
196
+ }
197
+ )
198
+ _FORCE_CRLF_EXTENSIONS = frozenset(
199
+ {
200
+ ".bat",
201
+ ".cmd", # Windows batch
202
+ }
203
+ )
204
+
205
+
206
+ def _default_eol_for_new_file(path: str) -> str:
207
+ """Choose the line ending for a newly created file.
208
+
209
+ Priority:
210
+ 1. File extension overrides (e.g. .sh → LF, .bat → CRLF)
211
+ 2. Platform default (CRLF on Windows, LF elsewhere)
212
+ """
213
+ ext = os.path.splitext(path)[1].lower()
214
+ if ext in _FORCE_LF_EXTENSIONS:
215
+ return "\n"
216
+ if ext in _FORCE_CRLF_EXTENSIONS:
217
+ return "\r\n"
218
+ return os.linesep
219
+
220
+
221
+ def _decode_write_preview_before(raw: bytes) -> tuple[str, str]:
222
+ """Return display-safe existing content for overwrite approval previews."""
223
+ from chrys.foundation.text.encoding import EncodingDetector
224
+
225
+ if EncodingDetector.looks_binary(raw):
226
+ return "", f"content-only; existing binary file ({len(raw)} bytes) not shown"
227
+
228
+ detector = EncodingDetector()
229
+ detection = detector.detect_from_bytes(raw)
230
+ if detection.is_binary:
231
+ return "", f"content-only; existing binary file ({len(raw)} bytes) not shown"
232
+
233
+ encoding = detection.encoding or "utf-8"
234
+ normalized_encoding = encoding.lower().replace("_", "-")
235
+ decode_errors = "replace" if normalized_encoding.startswith(("utf-16", "utf-32")) else "surrogateescape"
236
+ try:
237
+ before_text = raw.decode(encoding, errors=decode_errors)
238
+ except LookupError, UnicodeDecodeError:
239
+ return "", "content-only; existing file could not be decoded"
240
+ return _replace_surrogate_escapes(normalize_line_endings(before_text)), ""
241
+
242
+
243
+ def plan_write_file(
244
+ path: str,
245
+ content: str,
246
+ overwrite: bool = False,
247
+ *,
248
+ base_cwd: str | None = None,
249
+ ) -> WriteFilePlan | FileToolPreviewError:
250
+ """Prepare a ``write_file`` operation without touching the destination."""
251
+ if is_document_artifact_handle(path):
252
+ return FileToolPreviewError(
253
+ kind="read_only_handle",
254
+ message=tool_error(
255
+ "session_artifact_handle_is_read_only",
256
+ "session document handles are read-only; use a regular filesystem path for write_file.",
257
+ details={"path": path},
258
+ ),
259
+ )
260
+ try:
261
+ resolved = resolve_workspace_path(path, base_cwd=base_cwd)
262
+ if os.path.isdir(resolved):
263
+ return FileToolPreviewError(
264
+ kind="directory",
265
+ message=tool_error(
266
+ "path_is_directory",
267
+ f"path is a directory, not a file — {resolved}",
268
+ details={"path": path, "resolved_path": resolved},
269
+ ),
270
+ resolved_path=resolved,
271
+ )
272
+ if os.path.exists(resolved) and not overwrite:
273
+ return FileToolPreviewError(
274
+ kind="exists",
275
+ message=tool_error(
276
+ "file_exists",
277
+ f"file already exists — {resolved}. Set overwrite=true to replace it.",
278
+ details={"path": path, "resolved_path": resolved},
279
+ ),
280
+ resolved_path=resolved,
281
+ )
282
+
283
+ before_display_content = ""
284
+ content_only_reason = ""
285
+ overwrites_existing = overwrite and os.path.isfile(resolved)
286
+ # Determine line ending: preserve existing file's style, or pick a smart default.
287
+ if overwrites_existing:
288
+ with open(resolved, "rb") as f:
289
+ raw = f.read()
290
+ eol = _detect_line_ending(raw, resolved)
291
+ before_display_content, content_only_reason = _decode_write_preview_before(raw)
292
+ else:
293
+ eol = _default_eol_for_new_file(resolved)
294
+
295
+ write_content = normalize_line_endings(content)
296
+ if eol != "\n":
297
+ write_content = write_content.replace("\n", eol)
298
+ return WriteFilePlan(
299
+ resolved_path=resolved,
300
+ write_content=write_content,
301
+ display_content=_replace_surrogate_escapes(write_content),
302
+ before_display_content=before_display_content,
303
+ overwrites_existing=overwrites_existing,
304
+ content_only_reason=content_only_reason,
305
+ )
306
+ except Exception as e:
307
+ return FileToolPreviewError(
308
+ kind="prepare_failed",
309
+ message=tool_error("write_failed", f"failed to write file — {e}", details={"path": path}),
310
+ )
311
+
312
+
313
+ def _read_file_impl(
314
+ path: str,
315
+ max_tokens: int = _DEFAULT_MAX_TOKENS,
316
+ line_range: list[int] | None = None,
317
+ *,
318
+ base_cwd: str | None = None,
319
+ session_dir: Path | None = None,
320
+ ) -> str:
321
+ from pathlib import Path
322
+
323
+ from chrys.foundation.text.encoding import EncodingDetector
324
+ from chrys.foundation.text.tokenizer import MixedLanguageTokenizer
325
+
326
+ missing_base = missing_base_cwd_error(path, base_cwd)
327
+ if missing_base is not None:
328
+ return missing_base
329
+
330
+ tokenizer = MixedLanguageTokenizer()
331
+ detector = EncodingDetector()
332
+ effective_path = path
333
+
334
+ try:
335
+ try:
336
+ artifact_path = resolve_document_markdown_artifact_handle(path, session_dir)
337
+ except ValueError as e:
338
+ return tool_error(
339
+ "invalid_session_artifact_handle",
340
+ f"invalid session artifact handle — {surrogate_safe_text(str(e))}",
341
+ details={"path": path},
342
+ )
343
+ if artifact_path is None:
344
+ resolved = resolve_existing_path(effective_path, base_cwd=base_cwd)
345
+ else:
346
+ effective_path = artifact_path
347
+ # Handles name one exact session artifact. Do not feed them back
348
+ # into the user-path convenience resolver: its Unicode/whitespace
349
+ # fallbacks may select a different sibling than the handle names.
350
+ resolved = artifact_path if os.path.exists(artifact_path) else None
351
+ if resolved is None:
352
+ resolved = resolve_workspace_path(effective_path, base_cwd=base_cwd)
353
+ return tool_error(
354
+ "file_not_found",
355
+ f"file not found — {surrogate_safe_text(resolved)}",
356
+ details={"path": path, "resolved_path": resolved},
357
+ )
358
+ display_resolved = surrogate_safe_text(resolved)
359
+ detection = detector.detect_file(Path(resolved))
360
+ if detection.is_binary:
361
+ return tool_error(
362
+ "binary_file",
363
+ f"file appears to be binary — {display_resolved}",
364
+ details={"path": path, "resolved_path": resolved},
365
+ )
366
+ encoding = detection.encoding or "utf-8"
367
+ with open(resolved, encoding=encoding, errors="replace") as f:
368
+ content = f.read()
369
+
370
+ lines = split_lines(content)
371
+ total_lines = len(lines)
372
+ total_chars = len(content)
373
+
374
+ # Validate and apply line_range
375
+ offset = 0 # 0-indexed offset into `lines`
376
+ if line_range is not None:
377
+ if len(line_range) != 2:
378
+ return tool_error(
379
+ "invalid_line_range",
380
+ "line_range must be a list of exactly 2 integers [start, end].",
381
+ details={"line_range": line_range},
382
+ )
383
+ start, end = line_range
384
+ if start < 1:
385
+ return tool_error(
386
+ "invalid_line_range",
387
+ "line_range start must be >= 1.",
388
+ details={"line_range": line_range},
389
+ )
390
+ if end != -1 and end < start:
391
+ return tool_error(
392
+ "invalid_line_range",
393
+ "line_range end must be >= start or -1 for EOF.",
394
+ details={"line_range": line_range},
395
+ )
396
+ if start > total_lines:
397
+ return tool_error(
398
+ "invalid_line_range",
399
+ f"line_range start ({start}) is beyond the end of the file ({total_lines} lines).",
400
+ details={"line_range": line_range, "total_lines": total_lines},
401
+ )
402
+ offset = start - 1
403
+ end_idx = total_lines if end == -1 else min(end, total_lines)
404
+ lines = lines[offset:end_idx]
405
+
406
+ header = f"File: {display_resolved} ({total_lines} lines, {total_chars} chars)\n"
407
+ header_tokens = tokenizer.count_tokens(header)
408
+ remaining = max_tokens - header_tokens
409
+
410
+ output_lines: list[str] = []
411
+ truncated = False
412
+ last_line_num = 0
413
+ long_lines: list[tuple[int, int]] = []
414
+
415
+ for i, line in enumerate(lines):
416
+ line_num = offset + i + 1
417
+ if line_range is None and len(line) > _MAX_LINE_DISPLAY_CHARS:
418
+ long_lines.append((line_num, len(line)))
419
+ line = line[:_MAX_LINE_DISPLAY_CHARS] + "... [truncated]"
420
+ formatted = f"{line_num}|{line}\n"
421
+ line_tokens = tokenizer.count_tokens(formatted)
422
+ if line_tokens > remaining:
423
+ # Include a partial line instead of dropping it entirely
424
+ if remaining > 0:
425
+ fraction = remaining / line_tokens
426
+ cut = max(int(len(line) * fraction), 1)
427
+ output_lines.append(f"{line_num}|{line[:cut]}... [truncated]\n")
428
+ # Record for targeted reads so the LLM knows the actual length.
429
+ # Without line_range the char-level truncation (above) or the
430
+ # standard "[Truncated at line N]" message already covers it.
431
+ if line_range is not None:
432
+ long_lines.append((line_num, len(line)))
433
+ last_line_num = line_num
434
+ truncated = True
435
+ if last_line_num == 0:
436
+ last_line_num = line_num - 1 if i > 0 else offset
437
+ break
438
+ remaining -= line_tokens
439
+ output_lines.append(formatted)
440
+ last_line_num = line_num
441
+
442
+ result = header + "".join(output_lines)
443
+
444
+ if long_lines:
445
+ details = ", ".join(f"line {ln} ({length} chars)" for ln, length in long_lines)
446
+ result += (
447
+ f"\n[Long lines truncated to {_MAX_LINE_DISPLAY_CHARS} chars: {details}."
448
+ " Use line_range to read specific lines in full.]"
449
+ )
450
+
451
+ if truncated:
452
+ next_line = last_line_num + 1
453
+ result += (
454
+ f"\n[Truncated at line {last_line_num}, file has {total_lines} lines. "
455
+ f"Content exceeds max_tokens={max_tokens}. "
456
+ f"Use line_range=[{next_line}, -1] to continue reading.]"
457
+ )
458
+
459
+ return result
460
+ except FileNotFoundError:
461
+ resolved = resolve_workspace_path(effective_path, base_cwd=base_cwd)
462
+ return tool_error(
463
+ "file_not_found",
464
+ f"file not found — {surrogate_safe_text(resolved)}",
465
+ details={"path": path, "resolved_path": resolved},
466
+ )
467
+ except IsADirectoryError:
468
+ resolved = resolve_workspace_path(effective_path, base_cwd=base_cwd)
469
+ return tool_error(
470
+ "path_is_directory",
471
+ f"path is a directory, not a file — {surrogate_safe_text(resolved)}",
472
+ details={"path": path, "resolved_path": resolved},
473
+ )
474
+ except Exception as e:
475
+ return tool_error("read_failed", f"failed to read file — {surrogate_safe_text(str(e))}", details={"path": path})
476
+
477
+
478
+ @tool(kind=KIND_FILESYSTEM_READ)
479
+ def read_file(
480
+ path: Annotated[str, "Absolute or relative path to the file to read."],
481
+ max_tokens: Annotated[
482
+ int,
483
+ "Token budget for the returned content. Large files are truncated to fit.",
484
+ ] = _DEFAULT_MAX_TOKENS,
485
+ line_range: Annotated[
486
+ list[int] | None,
487
+ "Optional [start, end] 1-indexed line range. Use -1 for end to read to EOF.",
488
+ ] = None,
489
+ ) -> str:
490
+ """Read the contents of a text file and return them with line numbers."""
491
+ return _read_file_impl(path, max_tokens=max_tokens, line_range=line_range)
492
+
493
+
494
+ def _view_image_impl(
495
+ path: str,
496
+ *,
497
+ base_cwd: str | None = None,
498
+ session_dir: Path | None = None,
499
+ ) -> list[Content]:
500
+ from pathlib import Path
501
+
502
+ missing_base = missing_base_cwd_error(path, base_cwd)
503
+ if missing_base is not None:
504
+ return [Content.from_text(missing_base)]
505
+ effective_path = path
506
+ try:
507
+ try:
508
+ artifact_path = resolve_document_image_artifact_handle(path, session_dir)
509
+ except ValueError as exc:
510
+ return [
511
+ Content.from_text(
512
+ tool_error(
513
+ "invalid_session_artifact_handle",
514
+ f"invalid session artifact handle — {surrogate_safe_text(str(exc))}",
515
+ details={"path": path},
516
+ )
517
+ )
518
+ ]
519
+ if artifact_path is None:
520
+ resolved = resolve_existing_path(effective_path, base_cwd=base_cwd)
521
+ else:
522
+ effective_path = artifact_path
523
+ resolved = artifact_path if os.path.exists(artifact_path) else None
524
+ if resolved is None:
525
+ resolved = resolve_workspace_path(effective_path, base_cwd=base_cwd)
526
+ return [
527
+ Content.from_text(
528
+ tool_error(
529
+ "file_not_found",
530
+ f"file not found — {surrogate_safe_text(resolved)}",
531
+ details={"path": path, "resolved_path": resolved},
532
+ )
533
+ )
534
+ ]
535
+ source_path = Path(resolved)
536
+ if source_path.is_dir():
537
+ return [
538
+ Content.from_text(
539
+ tool_error(
540
+ "path_is_directory",
541
+ f"path is a directory, not a file — {surrogate_safe_text(resolved)}",
542
+ details={"path": path, "resolved_path": resolved},
543
+ )
544
+ )
545
+ ]
546
+
547
+ loaded = load_image_file(source_path)
548
+ metadata = {
549
+ "width": loaded.width,
550
+ "height": loaded.height,
551
+ "media_type": loaded.media_type,
552
+ "size": loaded.size,
553
+ "source_path": surrogate_safe_text(resolved),
554
+ }
555
+ return [Content.from_data(data=loaded.data, media_type=loaded.media_type, additional_properties=metadata)]
556
+ except ImageProcessingError as exc:
557
+ return [
558
+ Content.from_text(tool_error("view_image_failed", f"failed to view image — {exc}", details={"path": path}))
559
+ ]
560
+ except Exception as exc:
561
+ return [
562
+ Content.from_text(tool_error("view_image_failed", f"failed to view image — {exc}", details={"path": path}))
563
+ ]
564
+
565
+
566
+ @tool(kind=KIND_FILESYSTEM_READ)
567
+ def view_image(
568
+ path: Annotated[str, "Absolute or relative path to the image file to inspect."],
569
+ ) -> list[Content]:
570
+ """Read an image file and return it as model-visible image content.
571
+
572
+ Images the user attached to a message are already visible to you, at the
573
+ same size this tool returns; call it for one only when it is not in the
574
+ conversation.
575
+ """
576
+ return _view_image_impl(path)
577
+
578
+
579
+ _FS_WRITE_LOCKS: dict[str, threading.Lock] = {}
580
+ _FS_WRITE_LOCKS_GUARD = threading.Lock()
581
+
582
+
583
+ def _fs_write_lock(path: str, base_cwd: str | None) -> threading.Lock:
584
+ """Return the lock that serializes write/edit operations on *path*.
585
+
586
+ Chrys runs sync ``FunctionTool``s through ``asyncio.to_thread``, so two
587
+ write/edit calls in one tool batch can run concurrently in worker threads.
588
+ ``edit_file`` (and overwrite-aware ``write_file``) is read-modify-write, so
589
+ concurrent calls on the same file would race and lose updates. Lock per
590
+ resolved path so same-file writes stay deterministic while distinct files
591
+ still proceed in parallel; reads remain lock-free.
592
+ """
593
+ try:
594
+ # realpath (not just normpath) so symlink aliases to the same physical
595
+ # file share one lock — matching chrys's path-identity convention
596
+ # (runtime_paths.paths_equal, approval middleware).
597
+ key = os.path.normcase(os.path.realpath(resolve_workspace_path(path, base_cwd=base_cwd)))
598
+ except Exception:
599
+ key = "\x00unresolved"
600
+ with _FS_WRITE_LOCKS_GUARD:
601
+ lock = _FS_WRITE_LOCKS.get(key)
602
+ if lock is None:
603
+ lock = threading.Lock()
604
+ _FS_WRITE_LOCKS[key] = lock
605
+ return lock
606
+
607
+
608
+ def _write_file_impl(
609
+ path: str,
610
+ content: str,
611
+ overwrite: bool = False,
612
+ *,
613
+ base_cwd: str | None = None,
614
+ ) -> str:
615
+ # Checked first: a relative write would otherwise recreate the deleted
616
+ # working directory through ``os.makedirs`` below.
617
+ missing_base = missing_base_cwd_error(path, base_cwd)
618
+ if missing_base is not None:
619
+ return missing_base
620
+ try:
621
+ path = approved_write_path(path, base_cwd=base_cwd)
622
+ with _fs_write_lock(path, base_cwd):
623
+ plan = plan_write_file(path, content, overwrite=overwrite, base_cwd=base_cwd)
624
+ if isinstance(plan, FileToolPreviewError):
625
+ return plan.message
626
+
627
+ # Ensure parent directory exists
628
+ parent = os.path.dirname(plan.resolved_path)
629
+ if parent:
630
+ os.makedirs(parent, exist_ok=True)
631
+
632
+ _atomic_write(plan.resolved_path, plan.write_content)
633
+ line_count = len(split_lines(normalize_line_endings(content)))
634
+ return f"Written {len(content)} chars ({line_count} lines) to {surrogate_safe_text(plan.resolved_path)}."
635
+ except Exception as e:
636
+ return tool_error("write_failed", f"failed to write file — {e}", details={"path": path})
637
+
638
+
639
+ @tool(kind=KIND_FILESYSTEM_WRITE)
640
+ def write_file(
641
+ path: Annotated[str, "Absolute or relative path to the file to write."],
642
+ content: Annotated[str, "The full file content to write."],
643
+ overwrite: Annotated[
644
+ bool,
645
+ "Set to true to overwrite an existing file. "
646
+ "If false (default) and the file already exists, "
647
+ "the write will be rejected.",
648
+ ] = False,
649
+ ) -> str:
650
+ """Write content to a file. Creates parent directories if needed.
651
+
652
+ Use ``\\n`` for line breaks — the tool automatically applies the correct
653
+ line ending for the platform and file type. When overwriting an existing
654
+ file, the original line ending style is preserved. For new files, the
655
+ line ending is chosen by file extension (e.g. ``.sh`` → LF, ``.bat`` →
656
+ CRLF) and platform default (CRLF on Windows, LF on macOS/Linux).
657
+
658
+ On POSIX, new regular files request mode 0644 and new shell/shebang scripts
659
+ request mode 0755, with both filtered by the process umask. Overwriting an
660
+ existing file preserves its user/group/other rwx bits.
661
+
662
+ By default, refuses to overwrite existing files unless ``overwrite=true``.
663
+ """
664
+ return _write_file_impl(path, content, overwrite=overwrite)
665
+
666
+
667
+ _CONTEXT_LINES = 2
668
+ # A side of the snippet longer than this shows only its first and last halves.
669
+ _MAX_SNIPPET_SIDE_LINES = 30
670
+
671
+
672
+ def _format_diff_context(content: str, match_start: int, old_string: str, new_string: str) -> str:
673
+ """Return a before/after snippet with line numbers and surrounding context."""
674
+ lines = split_lines(content)
675
+
676
+ # Find which lines the old_string spans by counting newlines before the match
677
+ prefix = content[:match_start]
678
+ old_start_line = prefix.count("\n")
679
+ old_end_line = old_start_line + old_string.count("\n")
680
+
681
+ ctx_start = max(0, old_start_line - _CONTEXT_LINES)
682
+ ctx_end = min(len(lines), old_end_line + 1 + _CONTEXT_LINES)
683
+
684
+ # "Before" — lines from original content
685
+ before = _snippet_side(lines, ctx_start, ctx_end)
686
+
687
+ # "After" — apply replacement and show the same region
688
+ after_content = content[:match_start] + new_string + content[match_start + len(old_string) :]
689
+ after_lines = split_lines(after_content)
690
+ new_end_line = old_start_line + new_string.count("\n")
691
+ after_ctx_end = min(len(after_lines), new_end_line + 1 + _CONTEXT_LINES)
692
+
693
+ after = _snippet_side(after_lines, ctx_start, after_ctx_end)
694
+
695
+ return f"Before:\n{before}\nAfter:\n{after}"
696
+
697
+
698
+ def _snippet_side(lines: list[str], start: int, end: int) -> str:
699
+ """Number ``lines[start:end]``, eliding the middle of a side too long to read at a glance."""
700
+ numbered = [f" {index + 1}|{lines[index]}" for index in range(start, end)]
701
+ # The marker takes a line's place, so hiding a single line would save nothing.
702
+ if len(numbered) <= _MAX_SNIPPET_SIDE_LINES + 1:
703
+ return "\n".join(numbered)
704
+ keep = _MAX_SNIPPET_SIDE_LINES // 2
705
+ first_omitted = start + keep + 1
706
+ last_omitted = end - keep
707
+ omitted = last_omitted - first_omitted + 1
708
+ marker = f" [... {omitted} lines omitted ({first_omitted}-{last_omitted}) ...]"
709
+ return "\n".join([*numbered[:keep], marker, *numbered[-keep:]])
710
+
711
+
712
+ def _replace_surrogate_escapes(text: str) -> str:
713
+ """Return display-safe text with one output character per input character.
714
+
715
+ This must remain length-preserving: ``plan_edit_file`` passes a raw-content
716
+ offset to ``_format_diff_context`` for indexing the display-safe copy.
717
+ """
718
+ return "".join("\ufffd" if "\ud800" <= char <= "\udfff" else char for char in text)
719
+
720
+
721
+ def plan_edit_file(
722
+ path: str,
723
+ old_string: str,
724
+ new_string: str,
725
+ replace_all: bool = False,
726
+ *,
727
+ base_cwd: str | None = None,
728
+ ) -> EditFilePlan | FileToolPreviewError:
729
+ """Prepare an ``edit_file`` operation in memory without writing the file."""
730
+ from pathlib import Path
731
+
732
+ from chrys.foundation.text.encoding import EncodingDetector
733
+
734
+ detector = EncodingDetector()
735
+
736
+ if is_document_artifact_handle(path):
737
+ return FileToolPreviewError(
738
+ kind="read_only_handle",
739
+ message=tool_error(
740
+ "session_artifact_handle_is_read_only",
741
+ "session document handles are read-only; use a regular filesystem path for edit_file.",
742
+ details={"path": path},
743
+ ),
744
+ )
745
+
746
+ # An empty old_string "matches" between every two characters: it can
747
+ # neither name one place nor create a file.
748
+ if not old_string:
749
+ return FileToolPreviewError(
750
+ kind="empty_old_string",
751
+ message=tool_error(
752
+ "empty_old_string",
753
+ "old_string is empty. To create a file or replace a file's whole content, use write_file "
754
+ "(overwrite=true for an existing file). To insert text, put an existing neighbouring line "
755
+ "in old_string and repeat it in new_string.",
756
+ details={"path": path},
757
+ ),
758
+ )
759
+
760
+ try:
761
+ resolved = resolve_workspace_path(path, base_cwd=base_cwd)
762
+ if not os.path.exists(resolved):
763
+ return FileToolPreviewError(
764
+ kind="missing",
765
+ message=tool_error(
766
+ "file_not_found",
767
+ f"file not found — {resolved}",
768
+ details={"path": path, "resolved_path": resolved},
769
+ ),
770
+ resolved_path=resolved,
771
+ )
772
+ if os.path.isdir(resolved):
773
+ return FileToolPreviewError(
774
+ kind="directory",
775
+ message=tool_error(
776
+ "path_is_directory",
777
+ f"path is a directory, not a file — {resolved}",
778
+ details={"path": path, "resolved_path": resolved},
779
+ ),
780
+ resolved_path=resolved,
781
+ )
782
+
783
+ detection = detector.detect_file(Path(resolved))
784
+ if detection.is_binary:
785
+ return FileToolPreviewError(
786
+ kind="binary",
787
+ message=tool_error(
788
+ "binary_file",
789
+ f"file appears to be binary — {resolved}",
790
+ details={"path": path, "resolved_path": resolved},
791
+ ),
792
+ resolved_path=resolved,
793
+ )
794
+ encoding = detection.encoding or "utf-8"
795
+
796
+ # Read once in binary — detect EOL from raw bytes, then decode.
797
+ with open(resolved, "rb") as f:
798
+ raw = f.read()
799
+ original_eol = _detect_line_ending(raw, resolved)
800
+ decode_errors = "replace" if encoding.startswith(("utf-16", "utf-32")) else "surrogateescape"
801
+ content = normalize_line_endings(raw.decode(encoding, errors=decode_errors))
802
+
803
+ normalized_old = normalize_line_endings(old_string)
804
+ normalized_new = normalize_line_endings(new_string)
805
+
806
+ if normalized_old == normalized_new:
807
+ return FileToolPreviewError(
808
+ kind="identical",
809
+ message=tool_error("no_change", "old_string and new_string are identical — nothing to change."),
810
+ resolved_path=resolved,
811
+ )
812
+
813
+ count = content.count(normalized_old)
814
+ if count == 0:
815
+ return FileToolPreviewError(
816
+ kind="not_found",
817
+ message=tool_error(
818
+ "old_string_not_found",
819
+ f"old_string not found in {resolved}. "
820
+ "Make sure the string matches exactly (including whitespace and indentation). "
821
+ "Do not include line number prefixes (e.g. '13|') from read_file output.",
822
+ details={"path": path, "resolved_path": resolved},
823
+ ),
824
+ resolved_path=resolved,
825
+ )
826
+
827
+ if not replace_all and count > 1:
828
+ return FileToolPreviewError(
829
+ kind="ambiguous",
830
+ message=tool_error(
831
+ "ambiguous_match",
832
+ f"old_string appears {count} times in {resolved}. "
833
+ "Include more surrounding context in old_string to make the match unique, "
834
+ "or set replace_all=true to replace every occurrence.",
835
+ details={"path": path, "resolved_path": resolved, "matches": count},
836
+ ),
837
+ resolved_path=resolved,
838
+ )
839
+
840
+ match_start = content.find(normalized_old)
841
+ display_content = _replace_surrogate_escapes(content)
842
+ display_old = _replace_surrogate_escapes(normalized_old)
843
+ display_new = _replace_surrogate_escapes(normalized_new)
844
+ snippet = _format_diff_context(display_content, match_start, display_old, display_new)
845
+
846
+ new_content = (
847
+ content.replace(normalized_old, normalized_new)
848
+ if replace_all
849
+ else content.replace(normalized_old, normalized_new, 1)
850
+ )
851
+ write_content = new_content.replace("\n", original_eol) if original_eol != "\n" else new_content
852
+ replacements = count if replace_all else 1
853
+ return EditFilePlan(
854
+ resolved_path=resolved,
855
+ encoding=encoding,
856
+ decode_errors=decode_errors,
857
+ before_text=display_content,
858
+ after_text=_replace_surrogate_escapes(new_content),
859
+ write_content=write_content,
860
+ replacements=replacements,
861
+ snippet=snippet,
862
+ )
863
+ except Exception as e:
864
+ return FileToolPreviewError(
865
+ kind="prepare_failed",
866
+ message=tool_error("edit_failed", f"failed to edit file — {e}", details={"path": path}),
867
+ )
868
+
869
+
870
+ def _edit_file_impl(
871
+ path: str,
872
+ old_string: str,
873
+ new_string: str,
874
+ replace_all: bool = False,
875
+ *,
876
+ base_cwd: str | None = None,
877
+ ) -> str:
878
+ missing_base = missing_base_cwd_error(path, base_cwd)
879
+ if missing_base is not None:
880
+ return missing_base
881
+ try:
882
+ path = approved_write_path(path, base_cwd=base_cwd)
883
+ with _fs_write_lock(path, base_cwd):
884
+ plan = plan_edit_file(path, old_string, new_string, replace_all=replace_all, base_cwd=base_cwd)
885
+ if isinstance(plan, FileToolPreviewError):
886
+ return plan.message
887
+
888
+ _atomic_write(plan.resolved_path, plan.write_content, encoding=plan.encoding, errors=plan.decode_errors)
889
+
890
+ plural = "s" if plan.replacements != 1 else ""
891
+ return (
892
+ f"Replaced {plan.replacements} occurrence{plural} in {surrogate_safe_text(plan.resolved_path)}.\n"
893
+ f"{plan.snippet}"
894
+ )
895
+ except Exception as e:
896
+ return tool_error("edit_failed", f"failed to edit file — {e}", details={"path": path})
897
+
898
+
899
+ @tool(kind=KIND_FILESYSTEM_WRITE)
900
+ def edit_file(
901
+ path: Annotated[str, "Absolute or relative path to the file to edit."],
902
+ old_string: Annotated[
903
+ str,
904
+ "The exact text to find and replace. "
905
+ "Match the file content exactly — preserve indentation (tabs/spaces) as shown "
906
+ "by read_file. The line number prefix (e.g. '13|') is NOT part of the file "
907
+ "content; never include it in old_string or new_string. "
908
+ "When the text contains unicode escape sequences like \\u2192, "
909
+ "use the actual Unicode character (e.g. →) instead of the escape "
910
+ "sequence to avoid JSON encoding ambiguity.",
911
+ ],
912
+ new_string: Annotated[
913
+ str,
914
+ "The replacement text. "
915
+ "Preserve the surrounding indentation style (tabs or spaces). "
916
+ "Use actual Unicode characters instead of \\uXXXX escape sequences.",
917
+ ],
918
+ replace_all: Annotated[
919
+ bool,
920
+ "If true, replace all occurrences. If false (default), replace only "
921
+ "one occurrence and require it to be unique.",
922
+ ] = False,
923
+ ) -> str:
924
+ """Replace exact string occurrences in a file.
925
+
926
+ Line endings are handled automatically — use ``\\n`` in old_string and
927
+ new_string regardless of the file's actual line ending style. The original
928
+ line ending style is detected and preserved on write. On POSIX, the file's
929
+ existing user/group/other rwx bits are also preserved.
930
+
931
+ When ``replace_all=false`` (default), the ``old_string`` must appear exactly
932
+ once. If it appears multiple times, the edit is rejected — include more
933
+ surrounding context in ``old_string`` to make the match unique.
934
+ """
935
+ return _edit_file_impl(path, old_string, new_string, replace_all=replace_all)
936
+
937
+
938
+ class FilesystemTools:
939
+ """Runtime-bound filesystem tools that resolve relative paths per session."""
940
+
941
+ def __init__(
942
+ self,
943
+ runtime: SessionEnvironment,
944
+ session_dir: Path | None = None,
945
+ session_id: str = "",
946
+ ) -> None:
947
+ self._runtime = runtime
948
+ self._session_dir = resolve_tool_session_dir(
949
+ runtime.platform.config_dir,
950
+ session_id=session_id,
951
+ session_dir=session_dir,
952
+ )
953
+
954
+ def tools(self) -> list:
955
+ """Return filesystem tools for this runtime context."""
956
+ return [self.read_file, self.view_image, self.write_file, self.edit_file]
957
+
958
+ def approval_targets(self, arguments: dict[str, object]) -> ApprovedTargets:
959
+ """The entries a write/edit replaces; an unresolved path authorizes no write."""
960
+ path = arguments.get("path")
961
+ if isinstance(path, str) and is_document_artifact_handle(path):
962
+ # The tool refuses to write a session document; nothing to pin.
963
+ return ApprovedTargets()
964
+ if not isinstance(path, str) or not path or "\0" in path:
965
+ return ApprovedTargets(files=())
966
+ try:
967
+ return ApprovedTargets(files=(file_write_target(path, base_cwd=self._runtime.cwd),))
968
+ except OSError, ValueError:
969
+ return ApprovedTargets(files=())
970
+
971
+ @tool(kind=KIND_FILESYSTEM_READ)
972
+ def read_file(
973
+ self,
974
+ path: Annotated[str, "Absolute or relative path to the file to read."],
975
+ max_tokens: Annotated[
976
+ int,
977
+ "Token budget for the returned content. Large files are truncated to fit.",
978
+ ] = _DEFAULT_MAX_TOKENS,
979
+ line_range: Annotated[
980
+ list[int] | None,
981
+ "Optional [start, end] 1-indexed line range. Use -1 for end to read to EOF.",
982
+ ] = None,
983
+ ) -> str:
984
+ """Read the contents of a text file and return them with line numbers."""
985
+ return _read_file_impl(
986
+ path,
987
+ max_tokens=max_tokens,
988
+ line_range=line_range,
989
+ base_cwd=self._runtime.cwd,
990
+ session_dir=self._session_dir,
991
+ )
992
+
993
+ @tool(kind=KIND_FILESYSTEM_READ)
994
+ def view_image(
995
+ self,
996
+ path: Annotated[str, "Absolute or relative path to the image file to inspect."],
997
+ ) -> list[Content]:
998
+ """Read an image file and return it as model-visible image content.
999
+
1000
+ Images the user attached to a message are already visible to you, at the
1001
+ same size this tool returns; call it for one only when it is not in the
1002
+ conversation.
1003
+ """
1004
+ return _view_image_impl(path, base_cwd=self._runtime.cwd, session_dir=self._session_dir)
1005
+
1006
+ @tool(kind=KIND_FILESYSTEM_WRITE)
1007
+ def write_file(
1008
+ self,
1009
+ path: Annotated[str, "Absolute or relative path to the file to write."],
1010
+ content: Annotated[str, "The full file content to write."],
1011
+ overwrite: Annotated[
1012
+ bool,
1013
+ "Set to true to overwrite an existing file. "
1014
+ "If false (default) and the file already exists, "
1015
+ "the write will be rejected.",
1016
+ ] = False,
1017
+ ) -> str:
1018
+ """Write content to a file. Creates parent directories if needed.
1019
+
1020
+ Use ``\\n`` for line breaks — the tool automatically applies the correct
1021
+ line ending for the platform and file type. When overwriting an existing
1022
+ file, the original line ending style is preserved. For new files, the
1023
+ line ending is chosen by file extension (e.g. ``.sh`` → LF, ``.bat`` →
1024
+ CRLF) and platform default (CRLF on Windows, LF on macOS/Linux).
1025
+
1026
+ On POSIX, new regular files request mode 0644 and new shell/shebang scripts
1027
+ request mode 0755, with both filtered by the process umask. Overwriting an
1028
+ existing file preserves its user/group/other rwx bits.
1029
+
1030
+ By default, refuses to overwrite existing files unless ``overwrite=true``.
1031
+ """
1032
+ return _write_file_impl(path, content, overwrite=overwrite, base_cwd=self._runtime.cwd)
1033
+
1034
+ @tool(kind=KIND_FILESYSTEM_WRITE)
1035
+ def edit_file(
1036
+ self,
1037
+ path: Annotated[str, "Absolute or relative path to the file to edit."],
1038
+ old_string: Annotated[
1039
+ str,
1040
+ "The exact text to find and replace. "
1041
+ "Match the file content exactly — preserve indentation (tabs/spaces) as shown "
1042
+ "by read_file. The line number prefix (e.g. '13|') is NOT part of the file "
1043
+ "content; never include it in old_string or new_string. "
1044
+ "When the text contains unicode escape sequences like \\u2192, "
1045
+ "use the actual Unicode character (e.g. →) instead of the escape "
1046
+ "sequence to avoid JSON encoding ambiguity.",
1047
+ ],
1048
+ new_string: Annotated[
1049
+ str,
1050
+ "The replacement text. "
1051
+ "Preserve the surrounding indentation style (tabs or spaces). "
1052
+ "Use actual Unicode characters instead of \\uXXXX escape sequences.",
1053
+ ],
1054
+ replace_all: Annotated[
1055
+ bool,
1056
+ "If true, replace all occurrences. If false (default), replace only "
1057
+ "one occurrence and require it to be unique.",
1058
+ ] = False,
1059
+ ) -> str:
1060
+ """Replace exact string occurrences in a file.
1061
+
1062
+ Line endings are handled automatically — use ``\\n`` in old_string and
1063
+ new_string regardless of the file's actual line ending style. The original
1064
+ line ending style is detected and preserved on write. On POSIX, the file's
1065
+ existing user/group/other rwx bits are also preserved.
1066
+
1067
+ When ``replace_all=false`` (default), the ``old_string`` must appear exactly
1068
+ once. If it appears multiple times, the edit is rejected — include more
1069
+ surrounding context in ``old_string`` to make the match unique.
1070
+ """
1071
+ return _edit_file_impl(path, old_string, new_string, replace_all=replace_all, base_cwd=self._runtime.cwd)