octo-agent 0.11.2

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 (319) hide show
  1. checksums.yaml +7 -0
  2. data/.clacky/skills/commit/SKILL.md +423 -0
  3. data/.clacky/skills/gem-release/SKILL.md +199 -0
  4. data/.clacky/skills/gem-release/scripts/release.sh +304 -0
  5. data/.clacky/skills/oss-upload/SKILL.md +47 -0
  6. data/.octorules +106 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +8 -0
  9. data/CHANGELOG.md +76 -0
  10. data/CODE_OF_CONDUCT.md +132 -0
  11. data/CONTRIBUTING.md +92 -0
  12. data/Dockerfile +28 -0
  13. data/LICENSE.txt +22 -0
  14. data/POSITIONING.md +46 -0
  15. data/README.md +134 -0
  16. data/README_CN.md +134 -0
  17. data/Rakefile +34 -0
  18. data/benchmark/fixtures/sample_project/Gemfile +3 -0
  19. data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
  20. data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
  21. data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
  22. data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
  23. data/benchmark/results/EVALUATION_REPORT.md +165 -0
  24. data/benchmark/results/baseline_20260511_174424.json +128 -0
  25. data/benchmark/results/report_20260511_175256.json +271 -0
  26. data/benchmark/results/report_20260511_175444.json +271 -0
  27. data/benchmark/results/treatment_20260511_175103.json +130 -0
  28. data/benchmark/runner.rb +441 -0
  29. data/bin/octo +7 -0
  30. data/docs/agent-first-ui-design.md +77 -0
  31. data/docs/billing-system.md +318 -0
  32. data/docs/channel-architecture.md +235 -0
  33. data/docs/engineering-article.md +343 -0
  34. data/docs/session-skill-invocation.md +69 -0
  35. data/docs/time_machine_design.md +247 -0
  36. data/docs/ui2-architecture.md +124 -0
  37. data/homebrew/README.md +96 -0
  38. data/homebrew/openocto.rb +24 -0
  39. data/lib/octo/agent/hook_manager.rb +61 -0
  40. data/lib/octo/agent/llm_caller.rb +800 -0
  41. data/lib/octo/agent/memory_updater.rb +246 -0
  42. data/lib/octo/agent/message_compressor.rb +225 -0
  43. data/lib/octo/agent/message_compressor_helper.rb +869 -0
  44. data/lib/octo/agent/next_message_suggester.rb +215 -0
  45. data/lib/octo/agent/session_serializer.rb +685 -0
  46. data/lib/octo/agent/skill_auto_creator.rb +114 -0
  47. data/lib/octo/agent/skill_evolution.rb +61 -0
  48. data/lib/octo/agent/skill_manager.rb +466 -0
  49. data/lib/octo/agent/skill_reflector.rb +89 -0
  50. data/lib/octo/agent/system_prompt_builder.rb +101 -0
  51. data/lib/octo/agent/time_machine.rb +214 -0
  52. data/lib/octo/agent/tool_executor.rb +454 -0
  53. data/lib/octo/agent/tool_registry.rb +150 -0
  54. data/lib/octo/agent.rb +2180 -0
  55. data/lib/octo/agent_config.rb +989 -0
  56. data/lib/octo/agent_profile.rb +112 -0
  57. data/lib/octo/anthropic_stream_aggregator.rb +137 -0
  58. data/lib/octo/background_task_registry.rb +324 -0
  59. data/lib/octo/banner.rb +34 -0
  60. data/lib/octo/bedrock_stream_aggregator.rb +137 -0
  61. data/lib/octo/block_font.rb +331 -0
  62. data/lib/octo/cli.rb +968 -0
  63. data/lib/octo/client.rb +623 -0
  64. data/lib/octo/default_agents/SOUL.md +3 -0
  65. data/lib/octo/default_agents/USER.md +1 -0
  66. data/lib/octo/default_agents/base_prompt.md +66 -0
  67. data/lib/octo/default_agents/coding/profile.yml +2 -0
  68. data/lib/octo/default_agents/coding/system_prompt.md +67 -0
  69. data/lib/octo/default_agents/general/profile.yml +2 -0
  70. data/lib/octo/default_agents/general/system_prompt.md +16 -0
  71. data/lib/octo/default_parsers/doc_parser.rb +69 -0
  72. data/lib/octo/default_parsers/docx_parser.rb +188 -0
  73. data/lib/octo/default_parsers/pdf_parser.rb +120 -0
  74. data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
  75. data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
  76. data/lib/octo/default_parsers/pptx_parser.rb +140 -0
  77. data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
  78. data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
  79. data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
  80. data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
  81. data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
  82. data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
  83. data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
  84. data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
  85. data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
  86. data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
  87. data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
  88. data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
  89. data/lib/octo/default_skills/onboard/SKILL.md +578 -0
  90. data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
  91. data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
  92. data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
  93. data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
  94. data/lib/octo/default_skills/personal-website/publish.rb +235 -0
  95. data/lib/octo/default_skills/product-help/SKILL.md +123 -0
  96. data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
  97. data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
  98. data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
  99. data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
  100. data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
  101. data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
  102. data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
  103. data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
  104. data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
  105. data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
  106. data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
  107. data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
  108. data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
  109. data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
  110. data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
  111. data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
  112. data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
  113. data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
  114. data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
  115. data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
  116. data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
  117. data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
  118. data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
  119. data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
  120. data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
  121. data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
  122. data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
  123. data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
  124. data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
  125. data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  126. data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
  127. data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
  128. data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
  129. data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
  130. data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
  131. data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
  132. data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
  133. data/lib/octo/idle_compression_timer.rb +115 -0
  134. data/lib/octo/json_ui_controller.rb +204 -0
  135. data/lib/octo/message_format/anthropic.rb +409 -0
  136. data/lib/octo/message_format/bedrock.rb +361 -0
  137. data/lib/octo/message_format/open_ai.rb +222 -0
  138. data/lib/octo/message_history.rb +373 -0
  139. data/lib/octo/openai_stream_aggregator.rb +130 -0
  140. data/lib/octo/plain_ui_controller.rb +166 -0
  141. data/lib/octo/providers.rb +534 -0
  142. data/lib/octo/server/browser_manager.rb +397 -0
  143. data/lib/octo/server/channel/adapters/base.rb +82 -0
  144. data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
  145. data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
  146. data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
  147. data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
  148. data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
  149. data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
  150. data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
  151. data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
  152. data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
  153. data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
  154. data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
  155. data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
  156. data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
  157. data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
  158. data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
  159. data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
  160. data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
  161. data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
  162. data/lib/octo/server/channel/channel_config.rb +178 -0
  163. data/lib/octo/server/channel/channel_manager.rb +468 -0
  164. data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
  165. data/lib/octo/server/channel.rb +33 -0
  166. data/lib/octo/server/discover.rb +77 -0
  167. data/lib/octo/server/epipe_safe_io.rb +105 -0
  168. data/lib/octo/server/http_server.rb +3554 -0
  169. data/lib/octo/server/scheduler.rb +317 -0
  170. data/lib/octo/server/server_master.rb +325 -0
  171. data/lib/octo/server/session_registry.rb +431 -0
  172. data/lib/octo/server/web_ui_controller.rb +487 -0
  173. data/lib/octo/session_manager.rb +385 -0
  174. data/lib/octo/skill.rb +466 -0
  175. data/lib/octo/skill_loader.rb +328 -0
  176. data/lib/octo/tools/base.rb +118 -0
  177. data/lib/octo/tools/browser.rb +625 -0
  178. data/lib/octo/tools/edit.rb +165 -0
  179. data/lib/octo/tools/file_reader.rb +549 -0
  180. data/lib/octo/tools/glob.rb +162 -0
  181. data/lib/octo/tools/grep.rb +356 -0
  182. data/lib/octo/tools/invoke_skill.rb +96 -0
  183. data/lib/octo/tools/list_tasks.rb +54 -0
  184. data/lib/octo/tools/redo_task.rb +41 -0
  185. data/lib/octo/tools/request_user_feedback.rb +84 -0
  186. data/lib/octo/tools/security.rb +333 -0
  187. data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
  188. data/lib/octo/tools/terminal/persistent_session.rb +268 -0
  189. data/lib/octo/tools/terminal/safe_rm.sh +106 -0
  190. data/lib/octo/tools/terminal/session_manager.rb +213 -0
  191. data/lib/octo/tools/terminal.rb +1828 -0
  192. data/lib/octo/tools/todo_manager.rb +374 -0
  193. data/lib/octo/tools/trash_manager.rb +388 -0
  194. data/lib/octo/tools/undo_task.rb +35 -0
  195. data/lib/octo/tools/web_fetch.rb +242 -0
  196. data/lib/octo/tools/web_search.rb +260 -0
  197. data/lib/octo/tools/write.rb +77 -0
  198. data/lib/octo/ui2/block_font.rb +10 -0
  199. data/lib/octo/ui2/components/base_component.rb +163 -0
  200. data/lib/octo/ui2/components/command_suggestions.rb +290 -0
  201. data/lib/octo/ui2/components/common_component.rb +96 -0
  202. data/lib/octo/ui2/components/inline_input.rb +226 -0
  203. data/lib/octo/ui2/components/input_area.rb +1338 -0
  204. data/lib/octo/ui2/components/message_component.rb +99 -0
  205. data/lib/octo/ui2/components/modal_component.rb +419 -0
  206. data/lib/octo/ui2/components/todo_area.rb +149 -0
  207. data/lib/octo/ui2/components/tool_component.rb +107 -0
  208. data/lib/octo/ui2/components/welcome_banner.rb +139 -0
  209. data/lib/octo/ui2/layout_manager.rb +807 -0
  210. data/lib/octo/ui2/line_editor.rb +363 -0
  211. data/lib/octo/ui2/markdown_renderer.rb +100 -0
  212. data/lib/octo/ui2/output_buffer.rb +370 -0
  213. data/lib/octo/ui2/progress_handle.rb +362 -0
  214. data/lib/octo/ui2/progress_indicator.rb +55 -0
  215. data/lib/octo/ui2/screen_buffer.rb +273 -0
  216. data/lib/octo/ui2/terminal_detector.rb +119 -0
  217. data/lib/octo/ui2/theme_manager.rb +85 -0
  218. data/lib/octo/ui2/themes/base_theme.rb +105 -0
  219. data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
  220. data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
  221. data/lib/octo/ui2/thinking_verbs.rb +26 -0
  222. data/lib/octo/ui2/ui_controller.rb +1625 -0
  223. data/lib/octo/ui2/view_renderer.rb +177 -0
  224. data/lib/octo/ui2.rb +40 -0
  225. data/lib/octo/ui_interface.rb +154 -0
  226. data/lib/octo/utils/arguments_parser.rb +191 -0
  227. data/lib/octo/utils/browser_detector.rb +195 -0
  228. data/lib/octo/utils/encoding.rb +92 -0
  229. data/lib/octo/utils/environment_detector.rb +140 -0
  230. data/lib/octo/utils/file_ignore_helper.rb +170 -0
  231. data/lib/octo/utils/file_processor.rb +601 -0
  232. data/lib/octo/utils/gitignore_parser.rb +154 -0
  233. data/lib/octo/utils/limit_stack.rb +152 -0
  234. data/lib/octo/utils/logger.rb +124 -0
  235. data/lib/octo/utils/login_shell.rb +72 -0
  236. data/lib/octo/utils/model_pricing.rb +646 -0
  237. data/lib/octo/utils/parser_manager.rb +165 -0
  238. data/lib/octo/utils/path_helper.rb +15 -0
  239. data/lib/octo/utils/scripts_manager.rb +59 -0
  240. data/lib/octo/utils/string_matcher.rb +158 -0
  241. data/lib/octo/utils/trash_directory.rb +112 -0
  242. data/lib/octo/utils/workspace_rules.rb +46 -0
  243. data/lib/octo/version.rb +5 -0
  244. data/lib/octo/web/app.css +7141 -0
  245. data/lib/octo/web/app.js +543 -0
  246. data/lib/octo/web/apple-touch-icon.png +0 -0
  247. data/lib/octo/web/auth.js +150 -0
  248. data/lib/octo/web/channels.js +276 -0
  249. data/lib/octo/web/datepicker.js +205 -0
  250. data/lib/octo/web/favicon.png +0 -0
  251. data/lib/octo/web/i18n.js +1073 -0
  252. data/lib/octo/web/icon-512.png +0 -0
  253. data/lib/octo/web/icon-dark.svg +25 -0
  254. data/lib/octo/web/icon.svg +29 -0
  255. data/lib/octo/web/index.html +871 -0
  256. data/lib/octo/web/marked.min.js +69 -0
  257. data/lib/octo/web/onboard.js +491 -0
  258. data/lib/octo/web/profile.js +442 -0
  259. data/lib/octo/web/sessions.js +4421 -0
  260. data/lib/octo/web/settings.js +913 -0
  261. data/lib/octo/web/sidebar.js +32 -0
  262. data/lib/octo/web/skills.js +885 -0
  263. data/lib/octo/web/tasks.js +297 -0
  264. data/lib/octo/web/theme.js +105 -0
  265. data/lib/octo/web/trash.js +343 -0
  266. data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
  267. data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
  268. data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
  269. data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  270. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  271. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  272. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  273. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  274. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  275. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  276. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  277. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  278. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  279. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  280. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  281. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  282. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  283. data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  284. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  285. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  286. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  287. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  288. data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  289. data/lib/octo/web/vendor/katex/katex.min.css +1 -0
  290. data/lib/octo/web/vendor/katex/katex.min.js +1 -0
  291. data/lib/octo/web/version.js +449 -0
  292. data/lib/octo/web/weixin-qr.html +209 -0
  293. data/lib/octo/web/ws-dispatcher.js +357 -0
  294. data/lib/octo/web/ws.js +128 -0
  295. data/lib/octo.rb +145 -0
  296. data/scripts/build/build.sh +329 -0
  297. data/scripts/build/lib/apt.sh +56 -0
  298. data/scripts/build/lib/brew.sh +89 -0
  299. data/scripts/build/lib/colors.sh +17 -0
  300. data/scripts/build/lib/gem.sh +95 -0
  301. data/scripts/build/lib/mise.sh +125 -0
  302. data/scripts/build/lib/network.sh +157 -0
  303. data/scripts/build/lib/os.sh +57 -0
  304. data/scripts/build/lib/shell.sh +37 -0
  305. data/scripts/build/src/install.sh.cc +174 -0
  306. data/scripts/build/src/install_browser.sh.cc +101 -0
  307. data/scripts/build/src/install_full.sh.cc +290 -0
  308. data/scripts/build/src/install_rails_deps.sh.cc +145 -0
  309. data/scripts/build/src/install_system_deps.sh.cc +123 -0
  310. data/scripts/build/src/uninstall.sh.cc +101 -0
  311. data/scripts/install.ps1 +532 -0
  312. data/scripts/install.sh +567 -0
  313. data/scripts/install_browser.sh +479 -0
  314. data/scripts/install_full.sh +838 -0
  315. data/scripts/install_rails_deps.sh +746 -0
  316. data/scripts/install_system_deps.sh +518 -0
  317. data/scripts/uninstall.sh +287 -0
  318. data/sig/octo.rbs +4 -0
  319. metadata +614 -0
@@ -0,0 +1,357 @@
1
+ // ── WS event dispatcher ───────────────────────────────────────────────────
2
+ //
3
+ // Consumes events emitted by WS (ws.js) and dispatches them to the right
4
+ // business module (Sessions, Tasks, Skills, Channels, Settings, Brand, ...).
5
+ //
6
+ // Kept as a separate file from ws.js on purpose:
7
+ // - ws.js is a pure transport layer (connect / send / subscribe / reconnect)
8
+ // - this file is the application-level router that knows about every
9
+ // business module. Mixing the two would force ws.js to depend on every
10
+ // other module, breaking layering.
11
+ //
12
+ // Depends on: WS (ws.js), Sessions, Tasks, Skills, Channels, Settings, Brand,
13
+ // Router, I18n, global $ / escapeHtml / showConfirmModal helpers.
14
+ // ─────────────────────────────────────────────────────────────────────────
15
+ (function() {
16
+ // Guard: restore hash routing only once after initial session_list arrives.
17
+ let _initialRestoreDone = false;
18
+
19
+
20
+ WS.onEvent(ev => {
21
+ console.log("[DEBUG] WS event received:", ev.type, ev);
22
+ switch (ev.type) {
23
+
24
+ // ── Internal WS lifecycle ──────────────────────────────────────────
25
+ case "_ws_connected": {
26
+ const banner = document.getElementById("offline-banner");
27
+ if (banner) banner.style.display = "none";
28
+ const hint = $("ws-disconnect-hint");
29
+ if (hint) hint.style.display = "none";
30
+ break;
31
+ }
32
+
33
+ case "_ws_disconnected": {
34
+ const banner = document.getElementById("offline-banner");
35
+ if (banner) {
36
+ banner.textContent = I18n.t("offline.banner");
37
+ banner.style.display = "block";
38
+ }
39
+ // Do NOT force status bar to "idle" here — on a brief WS hiccup the
40
+ // agent may still be running, and reconnect will deliver a fresh
41
+ // session snapshot that patches the real status. Forcing idle here
42
+ // caused stuck UI after reconnect when the snapshot logic wasn't
43
+ // re-asserting status on every reconnect.
44
+ Sessions.clearAllProgress();
45
+ break;
46
+ }
47
+
48
+ // ── Session list ───────────────────────────────────────────────────
49
+ case "session_list": {
50
+ Sessions.setAll(ev.sessions || [], !!ev.has_more, ev.cron_count || 0);
51
+ Sessions.renderList();
52
+
53
+ // Restore URL hash once on initial connect; ignore subsequent session_list events.
54
+ // Skip if we are already on a session view (e.g. onboard flow navigated there
55
+ // before WS connected) — restoreFromHash would wrongly redirect to "welcome"
56
+ // because there is no hash set during onboarding.
57
+ if (!_initialRestoreDone) {
58
+ _initialRestoreDone = true;
59
+ if (Router.current !== "session") {
60
+ Router.restoreFromHash();
61
+ }
62
+ } else {
63
+ // If active session was deleted, go to welcome
64
+ if (Sessions.activeId && !Sessions.find(Sessions.activeId)) {
65
+ Router.navigate("welcome");
66
+ }
67
+ }
68
+ break;
69
+ }
70
+
71
+ // ── Session lifecycle ──────────────────────────────────────────────
72
+ case "subscribed": {
73
+ // Re-enable send button now that the server has confirmed the subscription.
74
+ $("btn-send").disabled = false;
75
+ $("user-input").focus();
76
+ // If this session was created by Tasks.run(), fire the agent now that
77
+ // we're guaranteed to receive its broadcasts.
78
+ const pendingId = Sessions.takePendingRunTask();
79
+ if (pendingId && pendingId === ev.session_id) {
80
+ WS.send({ type: "run_task", session_id: pendingId });
81
+ }
82
+ // If a slash-command was queued (e.g. /onboard from first-boot flow),
83
+ // send it now — after restoreFromHash has settled — so appendMsg won't be wiped.
84
+ const pendingMsg = Sessions.takePendingMessage();
85
+ if (pendingMsg && pendingMsg.session_id === ev.session_id) {
86
+ Sessions.appendMsg("user", escapeHtml(pendingMsg.content), { time: new Date() });
87
+ WS.send({ type: "message", session_id: pendingMsg.session_id, content: pendingMsg.content });
88
+ }
89
+ break;
90
+ }
91
+
92
+ case "session_update": {
93
+ // Two shapes arrive under this type:
94
+ // (1) Full session object from http_server broadcast_session_update:
95
+ // { type, session: { id, name, status, total_tasks, ... } }
96
+ // (2) Partial real-time update from web_ui_controller (tasks/status):
97
+ // { type, session_id, tasks?, status? }
98
+ let sid, patch;
99
+ if (ev.session) {
100
+ // Shape (1): full session — use as-is
101
+ sid = ev.session.id;
102
+ patch = ev.session;
103
+ } else {
104
+ // Shape (2): partial update — build patch from top-level fields
105
+ sid = ev.session_id;
106
+ patch = {};
107
+ if (ev.tasks !== undefined) patch.total_tasks = ev.tasks;
108
+ if (ev.status !== undefined) patch.status = ev.status;
109
+ // Latency pushed by Agent after each LLM call (see update_sessionbar).
110
+ // Stored under latest_latency — same field name the HTTP /api/sessions
111
+ // list returns, so updateInfoBar doesn't need to branch on the source.
112
+ if (ev.latency !== undefined) patch.latest_latency = ev.latency;
113
+ }
114
+ if (!sid) break;
115
+ Sessions.patch(sid, patch);
116
+ Sessions.renderList();
117
+ if (sid === Sessions.activeId) {
118
+ const current = Sessions.find(sid);
119
+ if (patch.status !== undefined) Sessions.updateStatusBar(patch.status);
120
+ Sessions.updateInfoBar(current);
121
+ // Update chat title/subtitle in case session was renamed or working_dir changed
122
+ Sessions.updateChatHeader(current);
123
+ }
124
+ // When a session finishes, refresh tasks and skills, and clear any progress state
125
+ if (patch.status === "idle") {
126
+ Tasks.load();
127
+ Skills.load();
128
+ // Clear progress state for this session (even if not currently active)
129
+ Sessions.clearProgress(sid);
130
+ }
131
+ break;
132
+ }
133
+
134
+ case "session_renamed": {
135
+ Sessions.patch(ev.session_id, { name: ev.name });
136
+ Sessions.renderList();
137
+ // Title is now shown only in the sidebar; chat-header element was removed.
138
+ break;
139
+ }
140
+
141
+ case "session_deleted":
142
+ Sessions.remove(ev.session_id);
143
+ if (ev.session_id === Sessions.activeId) Router.navigate("welcome");
144
+ Sessions.renderList();
145
+ break;
146
+
147
+ // ── Chat messages ──────────────────────────────────────────────────
148
+ case "history_user_message":
149
+ // During history replay, HistoryCollector sends events via HTTP — they
150
+ // never hit the WS dispatcher. Events arriving here are always live
151
+ // (agent just drained a queued message from the inbox). Remove the
152
+ // ghost bubble and render the real user message.
153
+ // Only remove the first pending ghost — subsequent ghosts belong
154
+ // to other queued messages that haven't been drained yet.
155
+ if (ev.session_id !== Sessions.activeId) break;
156
+ const firstGhost = document.querySelector(".msg-pending");
157
+ if (firstGhost) firstGhost.remove();
158
+ let userHtml = "";
159
+ if (Array.isArray(ev.images) && ev.images.length > 0) {
160
+ userHtml += ev.images.map(src => {
161
+ if (typeof src !== "string" || !src) return "";
162
+ if (src.startsWith("pdf:")) {
163
+ const fname = src.slice(4);
164
+ const lower = fname.toLowerCase();
165
+ const ext = (fname.split(".").pop() || "file").toUpperCase();
166
+ const displayExt = lower.endsWith(".tar.gz") ? "TAR.GZ" : ext;
167
+ const icon = ext === "PDF" ? "📄" :
168
+ (ext === "ZIP" || ext === "GZ" || ext === "TGZ" || ext === "TAR" ||
169
+ ext === "RAR" || ext === "7Z" || lower.endsWith(".tar.gz")) ? "🗜️" :
170
+ (ext === "DOC" || ext === "DOCX") ? "📝" :
171
+ (ext === "XLS" || ext === "XLSX" || ext === "CSV") ? "📊" :
172
+ (ext === "PPT" || ext === "PPTX") ? "📋" :
173
+ (ext === "MD" || ext === "MARKDOWN") ? "📝" :
174
+ (ext === "TXT" || ext === "LOG") ? "📄" : "📎";
175
+ return `<span class="msg-pdf-badge">` +
176
+ `<span class="msg-pdf-badge-icon">${icon}</span>` +
177
+ `<span class="msg-pdf-badge-info">` +
178
+ `<span class="msg-pdf-badge-name">${escapeHtml(fname)}</span>` +
179
+ `<span class="msg-pdf-badge-type">${escapeHtml(displayExt)}</span>` +
180
+ `</span>` +
181
+ `</span>`;
182
+ }
183
+ if (src.startsWith("expired:")) {
184
+ const fname = src.slice(8);
185
+ return `<span class="msg-pdf-badge msg-image-expired">` +
186
+ `<span class="msg-pdf-badge-icon">🖼️</span>` +
187
+ `<span class="msg-pdf-badge-info">` +
188
+ `<span class="msg-pdf-badge-name">${escapeHtml(fname || "image")}</span>` +
189
+ `<span class="msg-pdf-badge-type">${I18n.t("chat.image_expired") || "Expired"}</span>` +
190
+ `</span>` +
191
+ `</span>`;
192
+ }
193
+ return `<img src="${escapeHtml(src)}" alt="image" class="msg-image-thumb">`;
194
+ }).join("");
195
+ if (ev.content) userHtml += "<br>";
196
+ }
197
+ userHtml += escapeHtml(ev.content || "");
198
+ Sessions.appendMsg("user", userHtml, { time: ev.created_at });
199
+ break;
200
+
201
+ case "pending_user_messages":
202
+ // Replayed on WebSocket (re)subscribe — messages still in the
203
+ // inbox queue. Render them as pending ghost bubbles that the
204
+ // regular history_user_message handler will replace when the
205
+ // agent drains each one.
206
+ if (ev.session_id !== Sessions.activeId) break;
207
+ Sessions.renderPendingMessages(ev.messages || []);
208
+ break;
209
+
210
+ case "assistant_message":
211
+ if (ev.session_id !== Sessions.activeId) break;
212
+ Sessions.clearProgress();
213
+ Sessions.appendMsg("assistant", ev.content);
214
+ break;
215
+
216
+ case "tool_call":
217
+ if (ev.session_id !== Sessions.activeId) break;
218
+ Sessions.clearProgress();
219
+ Sessions.appendToolCall(ev.name, ev.args, ev.summary);
220
+ break;
221
+
222
+ case "tool_result":
223
+ if (ev.session_id !== Sessions.activeId) break;
224
+ Sessions.appendToolResult(ev.result, ev.ui_payload);
225
+ break;
226
+
227
+ case "tool_stdout":
228
+ if (ev.session_id !== Sessions.activeId) break;
229
+ Sessions.appendToolStdout(ev.lines);
230
+ break;
231
+
232
+ case "diff":
233
+ if (ev.session_id !== Sessions.activeId) break;
234
+ Sessions.appendDiff(ev.diff, ev.truncated);
235
+ break;
236
+
237
+ case "tool_error":
238
+ if (ev.session_id !== Sessions.activeId) break;
239
+ Sessions.appendMsg("info", `⚠ Tool error: ${escapeHtml(ev.error)}`);
240
+ break;
241
+
242
+ case "token_usage":
243
+ if (ev.session_id !== Sessions.activeId) break;
244
+ Sessions.appendTokenUsage(ev);
245
+ break;
246
+
247
+ case "progress":
248
+ if (ev.session_id !== Sessions.activeId) break;
249
+ if (ev.phase === "active" || ev.status === "start") {
250
+ const progress_type = ev.progress_type || "thinking";
251
+ const metadata = ev.metadata || {};
252
+ Sessions.showProgress(ev.message, progress_type, metadata, ev.started_at || null);
253
+ // A new turn started — any cached suggestion from the previous turn
254
+ // is stale.
255
+ Sessions.clearInputSuggestion();
256
+ } else {
257
+ Sessions.clearProgress(ev.message);
258
+ }
259
+ break;
260
+
261
+ case "complete":
262
+ if (ev.session_id !== Sessions.activeId) break;
263
+ Sessions.clearProgress();
264
+ Sessions.collapseToolGroup();
265
+ {
266
+ let mainLine = I18n.t("chat.done", { n: ev.iterations });
267
+ if (typeof ev.duration === "number" && ev.duration > 0) {
268
+ mainLine += I18n.t("chat.done.duration", { duration: ev.duration.toFixed(1) });
269
+ }
270
+ let cacheLine = null;
271
+ const cs = ev.cache_stats;
272
+ const total = cs && (cs.total_requests || cs["total_requests"]);
273
+ const hits = cs && (cs.cache_hit_requests || cs["cache_hit_requests"]);
274
+ const cachedTokens = cs && (cs.cache_read_input_tokens || cs["cache_read_input_tokens"]);
275
+ if (total && total > 0 && cachedTokens && cachedTokens > 0) {
276
+ const rate = ((hits / total) * 100).toFixed(1);
277
+ const tokensFmt = cachedTokens >= 1000
278
+ ? `${(cachedTokens / 1000).toFixed(1)}k`
279
+ : `${cachedTokens}`;
280
+ cacheLine = I18n.t("chat.done.cache", {
281
+ rate, hits, total: total, tokens: tokensFmt
282
+ });
283
+ }
284
+ Sessions.appendInfo(`✓ ${mainLine}`, cacheLine);
285
+ }
286
+ break;
287
+
288
+ case "next_message_suggestion":
289
+ // Backend predicted the user's next message — render as ghost text
290
+ // in the composer. Only apply when the suggestion is for the session
291
+ // the user is currently looking at; suggestions for background
292
+ // sessions are dropped (they'd be stale by the time the user switches).
293
+ if (ev.session_id !== Sessions.activeId) break;
294
+ Sessions.setInputSuggestion(ev.text);
295
+ break;
296
+
297
+ case "request_feedback":
298
+ if (ev.session_id !== Sessions.activeId) break;
299
+ Sessions.showFeedbackRequest(ev.question, ev.context, ev.options);
300
+ break;
301
+
302
+ case "request_confirmation":
303
+ if (ev.session_id !== Sessions.activeId) break;
304
+ showConfirmModal(ev.id, ev.message);
305
+ break;
306
+
307
+ case "interrupted":
308
+ if (ev.session_id !== Sessions.activeId) break;
309
+ Sessions.clearProgress();
310
+ Sessions.collapseToolGroup();
311
+ Sessions.appendInfo(I18n.t("chat.interrupted"));
312
+ break;
313
+
314
+ // ── Info / errors ──────────────────────────────────────────────────
315
+ case "info":
316
+ Sessions.appendInfo(ev.message);
317
+ break;
318
+
319
+ case "background_tasks_update":
320
+ if (ev.session_id !== Sessions.activeId) break;
321
+ Sessions.updateBgTasksBadge(ev.running || 0, ev.tasks || []);
322
+ break;
323
+
324
+ case "background_task_notice":
325
+ if (ev.session_id !== Sessions.activeId) break;
326
+ Sessions.appendBgTaskNotice(ev.command, ev.handle_id, ev.status);
327
+ break;
328
+
329
+ case "user_message_queue_status":
330
+ // Agent broadcast: N user messages are sitting in the inbox waiting
331
+ // to be drained at the next iteration boundary. Show/hide the
332
+ // "{{n}} messages waiting" hint above the input bar.
333
+ if (ev.session_id !== Sessions.activeId) break;
334
+ Sessions.setInputQueueHint(ev.pending || 0);
335
+ break;
336
+
337
+ case "warning":
338
+ // Optimize retry messages for better UX
339
+ const friendlyWarning = _transformRetryWarning(ev.message);
340
+ if (friendlyWarning) {
341
+ Sessions.appendInfo(friendlyWarning);
342
+ }
343
+ break;
344
+
345
+ case "success":
346
+ Sessions.appendMsg("success", "✓ " + escapeHtml(ev.message));
347
+ break;
348
+
349
+ case "error":
350
+ if (!ev.session_id || ev.session_id === Sessions.activeId)
351
+ Sessions.appendMsg("error", escapeHtml(ev.message));
352
+ break;
353
+ }
354
+ });
355
+
356
+
357
+ })();
@@ -0,0 +1,128 @@
1
+ // ── WS — WebSocket connection manager ─────────────────────────────────────
2
+ //
3
+ // Responsibilities:
4
+ // - Connect / reconnect with exponential backoff
5
+ // - Queue outbound messages while disconnected, flush on reconnect
6
+ // - Track active session subscription; restore it after reconnect
7
+ // - Dispatch inbound server events to registered handlers
8
+ //
9
+ // Usage:
10
+ // WS.onEvent(handler) // register an event handler
11
+ // WS.send({ type: "..." }) // send (queued if not yet connected)
12
+ // WS.connect() // start connection
13
+ // ─────────────────────────────────────────────────────────────────────────
14
+
15
+ const WS = (() => {
16
+ // ── Private state ──────────────────────────────────────────────────────
17
+ let _socket = null;
18
+ let _ready = false;
19
+ let _retryDelay = 1000; // ms; doubles on each failure, cap 30s
20
+ let _retryTimer = null;
21
+ let _queue = []; // messages queued while disconnected
22
+ let _handlers = []; // event handler callbacks
23
+ let _subscribedId = null; // currently subscribed session id
24
+
25
+ const MAX_DELAY = 30_000;
26
+
27
+ // ── Private helpers ────────────────────────────────────────────────────
28
+ function _dispatch(event) {
29
+ _handlers.forEach(fn => {
30
+ try { fn(event); } catch (e) { console.error("[WS] handler error", e); }
31
+ });
32
+ }
33
+
34
+ function _flushQueue() {
35
+ const pending = _queue.splice(0);
36
+ pending.forEach(msg => {
37
+ try { _socket.send(JSON.stringify(msg)); }
38
+ catch (e) { _queue.unshift(msg); } // re-queue on send error
39
+ });
40
+ }
41
+
42
+ function _onOpen() {
43
+ _ready = true;
44
+ _retryDelay = 1000; // reset backoff on successful connect
45
+
46
+ // Always request fresh session list on (re)connect
47
+ _socket.send(JSON.stringify({ type: "list_sessions" }));
48
+
49
+ // Restore subscription if we had one before disconnect
50
+ if (_subscribedId) {
51
+ _socket.send(JSON.stringify({ type: "subscribe", session_id: _subscribedId }));
52
+ }
53
+
54
+ _flushQueue();
55
+ _dispatch({ type: "_ws_connected" });
56
+ }
57
+
58
+ function _onMessage(e) {
59
+ let event;
60
+ try { event = JSON.parse(e.data); }
61
+ catch (ex) { console.error("[WS] parse error", ex); return; }
62
+ _dispatch(event);
63
+ }
64
+
65
+ function _onClose(e) {
66
+ // 1006 = abnormal closure (server rejected handshake, likely 401)
67
+ if (e.code === 1006 && !Auth.passed) {
68
+ Auth.reset();
69
+ Auth.check();
70
+ return;
71
+ }
72
+ _ready = false;
73
+ _socket = null;
74
+ console.warn(`[WS] closed — retry in ${_retryDelay}ms`);
75
+ _dispatch({ type: "_ws_disconnected" });
76
+
77
+ _retryTimer = setTimeout(() => {
78
+ _retryDelay = Math.min(_retryDelay * 2, MAX_DELAY);
79
+ _connect();
80
+ }, _retryDelay);
81
+ }
82
+
83
+ function _onError(err) {
84
+ console.error("[WS] error", err);
85
+ // onclose will fire automatically after onerror
86
+ }
87
+
88
+ function _connect() {
89
+ if (_retryTimer) { clearTimeout(_retryTimer); _retryTimer = null; }
90
+ if (_socket && (_socket.readyState === WebSocket.OPEN ||
91
+ _socket.readyState === WebSocket.CONNECTING)) return;
92
+
93
+ const accessKey = Auth.getKey();
94
+ const protocol = location.protocol === "https:" ? "wss:" : "ws:";
95
+ const url = accessKey
96
+ ? `${protocol}//${location.host}/ws?access_key=${encodeURIComponent(accessKey)}`
97
+ : `${protocol}//${location.host}/ws`;
98
+ _socket = new WebSocket(url);
99
+ _socket.onopen = _onOpen;
100
+ _socket.onmessage = _onMessage;
101
+ _socket.onclose = _onClose;
102
+ _socket.onerror = _onError;
103
+ }
104
+
105
+ // ── Public API ─────────────────────────────────────────────────────────
106
+ return {
107
+ /** Register a handler for all server events. */
108
+ onEvent(fn) { _handlers.push(fn); },
109
+
110
+ /** Send a message. If not connected, queue it for later. */
111
+ send(obj) {
112
+ if (_ready && _socket) {
113
+ try { _socket.send(JSON.stringify(obj)); return; }
114
+ catch (e) { /* fall through to queue */ }
115
+ }
116
+ _queue.push(obj);
117
+ },
118
+
119
+ /** Track which session is currently subscribed (used for reconnect). */
120
+ setSubscribedSession(id) { _subscribedId = id; },
121
+
122
+ /** Start the connection. Call once on boot. */
123
+ connect: _connect,
124
+
125
+ /** True when the socket is open and ready. */
126
+ get ready() { return _ready; },
127
+ };
128
+ })();
data/lib/octo.rb ADDED
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ── Global encoding defaults ──────────────────────────────────────────────────
4
+ # Force UTF-8 as the default external/internal encoding for all IO operations
5
+ # (File.read, Open3.capture3, HTTP bodies, etc.) so that binary-encoded strings
6
+ # from external processes or network I/O never cause "invalid byte sequence in
7
+ # UTF-8" errors on Ruby 2.6+.
8
+ # Binary-specific operations (File.binread, IO#read with "b" mode, .b) are
9
+ # unaffected — they always bypass this setting.
10
+ Encoding.default_external = Encoding::UTF_8
11
+ Encoding.default_internal = Encoding::UTF_8
12
+
13
+ # ── Ruby < 2.7 polyfills ──────────────────────────────────────────────────────
14
+
15
+ # Enumerable#filter_map was added in Ruby 2.7.
16
+ if RUBY_VERSION < "2.7"
17
+ module Enumerable
18
+ def filter_map(&block)
19
+ return to_enum(:filter_map) unless block
20
+
21
+ each_with_object([]) do |item, result|
22
+ mapped = block.call(item)
23
+ result << mapped if mapped
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ # File.absolute_path? was added in Ruby 2.7.
30
+ # Polyfill: a path is absolute if it starts with "/" (Unix) or a drive letter (Windows).
31
+ unless File.respond_to?(:absolute_path?)
32
+ def File.absolute_path?(path)
33
+ File.expand_path(path) == path.to_s
34
+ end
35
+ end
36
+
37
+ # URI.encode_uri_component was added in Ruby 3.2.
38
+ # CGI.escape encodes spaces as '+'; replace with '%20' to match URI encoding.
39
+ require "uri"
40
+ require "cgi"
41
+ unless URI.respond_to?(:encode_uri_component)
42
+ def URI.encode_uri_component(str)
43
+ CGI.escape(str.to_s).gsub("+", "%20")
44
+ end
45
+ end
46
+
47
+ # YAML.safe_load with permitted_classes: keyword was added in Psych 4 (Ruby 3.1).
48
+ # On older Ruby, the second positional argument serves the same purpose.
49
+ # This helper provides a unified interface across Ruby versions.
50
+ module YAMLCompat
51
+ def self.safe_load(yaml_string, permitted_classes: [])
52
+ if Psych::VERSION >= "4.0"
53
+ YAML.safe_load(yaml_string, permitted_classes: permitted_classes)
54
+ else
55
+ YAML.safe_load(yaml_string, permitted_classes)
56
+ end
57
+ end
58
+
59
+ def self.load_file(path, permitted_classes: [])
60
+ safe_load(File.read(path), permitted_classes: permitted_classes)
61
+ end
62
+ end
63
+
64
+ require_relative "octo/version"
65
+ require_relative "octo/message_format/anthropic"
66
+ require_relative "octo/message_format/open_ai"
67
+ require_relative "octo/message_format/bedrock"
68
+ require_relative "octo/bedrock_stream_aggregator"
69
+ require_relative "octo/openai_stream_aggregator"
70
+ require_relative "octo/anthropic_stream_aggregator"
71
+ require_relative "octo/client"
72
+ require_relative "octo/skill"
73
+ require_relative "octo/skill_loader"
74
+
75
+ # Agent system
76
+ require_relative "octo/message_history"
77
+ require_relative "octo/agent_config"
78
+ require_relative "octo/agent_profile"
79
+ require_relative "octo/providers"
80
+ require_relative "octo/session_manager"
81
+ require_relative "octo/idle_compression_timer"
82
+
83
+ # Agent modules
84
+ require_relative "octo/agent/message_compressor"
85
+ require_relative "octo/agent/hook_manager"
86
+ require_relative "octo/agent/tool_registry"
87
+
88
+ # UI modules
89
+ require_relative "octo/ui2/thinking_verbs"
90
+ require_relative "octo/ui2/progress_indicator"
91
+
92
+ # Utils
93
+ require_relative "octo/utils/logger"
94
+ require_relative "octo/utils/encoding"
95
+ require_relative "octo/utils/environment_detector"
96
+ require_relative "octo/utils/browser_detector"
97
+ require_relative "octo/utils/scripts_manager"
98
+ require_relative "octo/utils/model_pricing"
99
+ require_relative "octo/utils/gitignore_parser"
100
+ require_relative "octo/utils/limit_stack"
101
+ require_relative "octo/utils/path_helper"
102
+ require_relative "octo/utils/file_ignore_helper"
103
+ require_relative "octo/utils/string_matcher"
104
+ require_relative "octo/utils/login_shell"
105
+ require_relative "octo/tools/base"
106
+ require_relative "octo/utils/file_processor"
107
+
108
+ require_relative "octo/tools/security"
109
+ require_relative "octo/tools/file_reader"
110
+ require_relative "octo/tools/write"
111
+ require_relative "octo/tools/edit"
112
+ require_relative "octo/tools/glob"
113
+ require_relative "octo/tools/grep"
114
+ require_relative "octo/tools/web_search"
115
+ require_relative "octo/tools/web_fetch"
116
+ require_relative "octo/tools/todo_manager"
117
+ require_relative "octo/tools/trash_manager"
118
+ require_relative "octo/tools/request_user_feedback"
119
+ require_relative "octo/tools/invoke_skill"
120
+ require_relative "octo/tools/undo_task"
121
+ require_relative "octo/tools/redo_task"
122
+ require_relative "octo/tools/list_tasks"
123
+ require_relative "octo/tools/browser"
124
+ require_relative "octo/tools/terminal"
125
+ require_relative "octo/agent"
126
+
127
+ require_relative "octo/server/session_registry"
128
+ require_relative "octo/server/web_ui_controller"
129
+ require_relative "octo/server/browser_manager"
130
+ require_relative "octo/cli"
131
+
132
+ module Octo
133
+ class AgentInterrupted < Exception; end # Inherit from Exception to bypass rescue StandardError
134
+ class AgentError < StandardError; end
135
+ class BadRequestError < AgentError; end # 400 errors — our request was malformed, history should be rolled back
136
+ class RetryableError < StandardError; end # Transient errors that should be retried (5xx, HTML response, rate limit)
137
+ # Upstream (model/router like OpenRouter/Bedrock) returned finish_reason="stop" together with
138
+ # one or more tool_calls whose `arguments` JSON was truncated (empty, "{}" placeholder, or
139
+ # otherwise unparseable). Subclass of RetryableError so it flows through the existing
140
+ # retry/fallback pipeline in LlmCaller#call_llm.
141
+ class UpstreamTruncatedError < RetryableError; end
142
+ class ToolCallError < AgentError; end # Raised when tool call fails due to invalid parameters
143
+ class BrowserNotReachableError < AgentError; end # Chrome/Edge not running or remote debugging disabled
144
+ # BrowserManager singleton: Octo::BrowserManager.instance
145
+ end