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,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Octo
6
+ # Reassembles a Bedrock Converse event stream into the same hash shape that
7
+ # MessageFormat::Bedrock.parse_response expects from a non-streaming response,
8
+ # while invoking on_chunk(input_tokens:, output_tokens:) as usage information
9
+ # accumulates.
10
+ #
11
+ # Bedrock event-stream events handled (passed through as raw event JSON):
12
+ #
13
+ # messageStart → { role: "assistant" }
14
+ # contentBlockStart → { start: {toolUse: {toolUseId, name}} | {}, contentBlockIndex: N }
15
+ # contentBlockDelta → { delta: {text: "..."} | {toolUse: {input: "..."}}, contentBlockIndex: N }
16
+ # contentBlockStop → { contentBlockIndex: N }
17
+ # messageStop → { stopReason: "end_turn" | "tool_use" | "max_tokens" | ... }
18
+ # metadata → { usage: {inputTokens, outputTokens, cacheReadInputTokens, cacheWriteInputTokens}, metrics: {...} }
19
+ #
20
+ # Tool-use input is streamed as a sequence of partial JSON strings; we
21
+ # concatenate and let the response parser leave it as a string for downstream
22
+ # tool dispatch (which calls JSON.parse with a {} fallback).
23
+ class BedrockStreamAggregator
24
+ def initialize(on_chunk: nil)
25
+ @on_chunk = on_chunk
26
+ @role = "assistant"
27
+ @blocks = {}
28
+ @stop_reason = nil
29
+ @usage = {}
30
+ @last_input_tokens = 0
31
+ @last_output_tokens = 0
32
+ end
33
+
34
+ def handle(event, data_str)
35
+ data = parse_or_nil(data_str)
36
+ return unless data
37
+
38
+ case event
39
+ when "messageStart"
40
+ @role = data["role"] || @role
41
+ when "contentBlockStart"
42
+ idx = data["contentBlockIndex"] || @blocks.size
43
+ start = data["start"] || {}
44
+ if (tu = start["toolUse"])
45
+ @blocks[idx] = { kind: :tool_use, id: tu["toolUseId"], name: tu["name"], input_str: +"" }
46
+ else
47
+ @blocks[idx] = { kind: :text, text: +"" }
48
+ end
49
+ when "contentBlockDelta"
50
+ idx = data["contentBlockIndex"] || 0
51
+ delta = data["delta"] || {}
52
+ block = (@blocks[idx] ||= { kind: :text, text: +"" })
53
+ if delta["text"]
54
+ block[:kind] ||= :text
55
+ block[:text] ||= +""
56
+ block[:text] << delta["text"]
57
+ elsif (tu = delta["toolUse"])
58
+ block[:kind] = :tool_use
59
+ block[:input_str] ||= +""
60
+ block[:input_str] << tu["input"].to_s
61
+ block[:id] ||= tu["toolUseId"]
62
+ block[:name] ||= tu["name"]
63
+ elsif (rc = delta["reasoningContent"])
64
+ block[:kind] = :reasoning
65
+ block[:reasoning] ||= +""
66
+ block[:reasoning] << rc["text"].to_s
67
+ end
68
+ emit_estimate_progress
69
+ when "contentBlockStop"
70
+ # Nothing to assemble: blocks are kept as-is until messageStop.
71
+ when "messageStop"
72
+ @stop_reason = data["stopReason"] || @stop_reason
73
+ when "metadata"
74
+ if (u = data["usage"])
75
+ @usage.merge!(u)
76
+ emit_usage_progress(u)
77
+ end
78
+ end
79
+ end
80
+
81
+ # Render the canonical non-streaming Bedrock response hash so the existing
82
+ # MessageFormat::Bedrock.parse_response can consume it unchanged.
83
+ def to_h
84
+ content_blocks = @blocks.keys.sort.map do |idx|
85
+ b = @blocks[idx]
86
+ case b[:kind]
87
+ when :tool_use
88
+ input_value = b[:input_str].to_s.empty? ? {} : (JSON.parse(b[:input_str]) rescue b[:input_str])
89
+ { "toolUse" => { "toolUseId" => b[:id], "name" => b[:name], "input" => input_value } }
90
+ else
91
+ { "text" => b[:text].to_s }
92
+ end
93
+ end
94
+
95
+ {
96
+ "output" => { "message" => { "role" => @role, "content" => content_blocks } },
97
+ "stopReason" => @stop_reason,
98
+ "usage" => @usage
99
+ }
100
+ end
101
+
102
+ private def parse_or_nil(s)
103
+ JSON.parse(s)
104
+ rescue JSON::ParserError
105
+ nil
106
+ end
107
+
108
+ private def emit_usage_progress(u)
109
+ return unless @on_chunk
110
+ input = u["inputTokens"].to_i + u["cacheReadInputTokens"].to_i
111
+ output = u["outputTokens"].to_i
112
+ return if input == @last_input_tokens && output == @last_output_tokens
113
+ @last_input_tokens = input
114
+ @last_output_tokens = output
115
+ @on_chunk.call(input_tokens: input, output_tokens: output)
116
+ rescue => e
117
+ Octo::Logger.warn("[BedrockStreamAggregator] on_chunk: #{e.class}: #{e.message}")
118
+ end
119
+
120
+ private def emit_estimate_progress
121
+ return unless @on_chunk
122
+ output = approximate_output_tokens
123
+ return if output == @last_output_tokens
124
+ @last_output_tokens = output
125
+ @on_chunk.call(input_tokens: @last_input_tokens, output_tokens: output)
126
+ rescue => e
127
+ Octo::Logger.warn("[BedrockStreamAggregator] on_chunk: #{e.class}: #{e.message}")
128
+ end
129
+
130
+ private def approximate_output_tokens
131
+ total_chars = @blocks.values.sum do |b|
132
+ b[:text].to_s.bytesize + b[:input_str].to_s.bytesize + b[:reasoning].to_s.bytesize
133
+ end
134
+ (total_chars / 4.0).ceil
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ # BlockFont renders text using Unicode box-drawing characters (█ ║ ╗ ╔ ╚ ╝ ═)
5
+ # in the same visual style as the hardcoded Octo logo.
6
+ #
7
+ # Each glyph is 6 lines tall. Characters are joined horizontally with one
8
+ # space column between them.
9
+ module BlockFont
10
+ # Each letter is defined as an array of exactly 6 strings (equal width).
11
+ # rubocop:disable Layout/ExtraSpacing, Layout/SpaceInsideArrayLiteralBrackets
12
+ GLYPHS = {
13
+ "0" => [
14
+ " ██████╗ ",
15
+ "██╔═══██╗",
16
+ "██║██╗██║",
17
+ "██║╚█╔╝██║",
18
+ "╚██████╔╝",
19
+ " ╚═════╝ ",
20
+ ],
21
+ "1" => [
22
+ " ██╗",
23
+ "███║",
24
+ "╚██║",
25
+ " ██║",
26
+ " ██║",
27
+ " ╚═╝",
28
+ ],
29
+ "2" => [
30
+ "██████╗ ",
31
+ "╚════██╗",
32
+ " █████╔╝",
33
+ "██╔═══╝ ",
34
+ "███████╗",
35
+ "╚══════╝",
36
+ ],
37
+ "3" => [
38
+ "██████╗ ",
39
+ "╚════██╗",
40
+ " █████╔╝",
41
+ " ╚═══██╗",
42
+ "██████╔╝",
43
+ "╚═════╝ ",
44
+ ],
45
+ "4" => [
46
+ "██╗ ██╗",
47
+ "██║ ██║",
48
+ "███████║",
49
+ "╚════██║",
50
+ " ██║",
51
+ " ╚═╝",
52
+ ],
53
+ "5" => [
54
+ "███████╗",
55
+ "██╔════╝",
56
+ "███████╗",
57
+ "╚════██║",
58
+ "███████║",
59
+ "╚══════╝",
60
+ ],
61
+ "6" => [
62
+ " ██████╗ ",
63
+ "██╔════╝ ",
64
+ "███████╗ ",
65
+ "██╔═══██╗",
66
+ "╚██████╔╝",
67
+ " ╚═════╝ ",
68
+ ],
69
+ "7" => [
70
+ "███████╗",
71
+ "╚════██║",
72
+ " ██╔╝",
73
+ " ██╔╝ ",
74
+ " ██║ ",
75
+ " ╚═╝ ",
76
+ ],
77
+ "8" => [
78
+ " █████╗ ",
79
+ "██╔══██╗",
80
+ "╚█████╔╝",
81
+ "██╔══██╗",
82
+ "╚█████╔╝",
83
+ " ╚════╝ ",
84
+ ],
85
+ "9" => [
86
+ " █████╗ ",
87
+ "██╔══██╗",
88
+ "╚██████║",
89
+ " ╚═══██║",
90
+ " █████╔╝",
91
+ " ╚════╝ ",
92
+ ],
93
+ " " => [
94
+ " ",
95
+ " ",
96
+ " ",
97
+ " ",
98
+ " ",
99
+ " ",
100
+ ],
101
+ "a" => [
102
+ " ████╗ ",
103
+ "██╔══██╗",
104
+ "███████║",
105
+ "██╔══██║",
106
+ "██║ ██║",
107
+ "╚═╝ ╚═╝",
108
+ ],
109
+ "b" => [
110
+ "██████╗ ",
111
+ "██╔══██╗",
112
+ "██████╔╝",
113
+ "██╔══██╗",
114
+ "██████╔╝",
115
+ "╚═════╝ ",
116
+ ],
117
+ "c" => [
118
+ " ██████╗",
119
+ "██╔════╝",
120
+ "██║ ",
121
+ "██║ ",
122
+ "╚██████╗",
123
+ " ╚═════╝",
124
+ ],
125
+ "d" => [
126
+ "██████╗ ",
127
+ "██╔══██╗",
128
+ "██║ ██║",
129
+ "██║ ██║",
130
+ "██████╔╝",
131
+ "╚═════╝ ",
132
+ ],
133
+ "e" => [
134
+ "███████╗",
135
+ "██╔════╝",
136
+ "█████╗ ",
137
+ "██╔══╝ ",
138
+ "███████╗",
139
+ "╚══════╝",
140
+ ],
141
+ "f" => [
142
+ "███████╗",
143
+ "██╔════╝",
144
+ "█████╗ ",
145
+ "██╔══╝ ",
146
+ "██║ ",
147
+ "╚═╝ ",
148
+ ],
149
+ "g" => [
150
+ " ██████╗ ",
151
+ "██╔════╝ ",
152
+ "██║ ███╗",
153
+ "██║ ██║",
154
+ "╚██████╔╝",
155
+ " ╚═════╝ ",
156
+ ],
157
+ "h" => [
158
+ "██╗ ██╗",
159
+ "██║ ██║",
160
+ "███████║",
161
+ "██╔══██║",
162
+ "██║ ██║",
163
+ "╚═╝ ╚═╝",
164
+ ],
165
+ "i" => [
166
+ "██╗",
167
+ "██║",
168
+ "██║",
169
+ "██║",
170
+ "██║",
171
+ "╚═╝",
172
+ ],
173
+ "j" => [
174
+ " ██╗",
175
+ " ██║",
176
+ " ██║",
177
+ " ██║",
178
+ "███║",
179
+ "╚══╝",
180
+ ],
181
+ "k" => [
182
+ "██╗ ██╗",
183
+ "██║ ██╔╝",
184
+ "█████╔╝ ",
185
+ "██╔═██╗ ",
186
+ "██║ ██╗",
187
+ "╚═╝ ╚═╝",
188
+ ],
189
+ "l" => [
190
+ "██╗ ",
191
+ "██║ ",
192
+ "██║ ",
193
+ "██║ ",
194
+ "███████╗",
195
+ "╚══════╝",
196
+ ],
197
+ "m" => [
198
+ "███╗ ███╗",
199
+ "████╗ ████║",
200
+ "██╔████╔██║",
201
+ "██║╚██╔╝██║",
202
+ "██║ ╚═╝ ██║",
203
+ "╚═╝ ╚═╝",
204
+ ],
205
+ "n" => [
206
+ "███╗ ██╗",
207
+ "████╗ ██║",
208
+ "██╔██╗ ██║",
209
+ "██║╚██╗██║",
210
+ "██║ ╚████║",
211
+ "╚═╝ ╚═══╝",
212
+ ],
213
+ "o" => [
214
+ " ██████╗ ",
215
+ "██╔═══██╗",
216
+ "██║ ██║",
217
+ "██║ ██║",
218
+ "╚██████╔╝",
219
+ " ╚═════╝ ",
220
+ ],
221
+ "p" => [
222
+ "██████╗ ",
223
+ "██╔══██╗",
224
+ "██████╔╝",
225
+ "██╔═══╝ ",
226
+ "██║ ",
227
+ "╚═╝ ",
228
+ ],
229
+ "q" => [
230
+ " ██████╗ ",
231
+ "██╔═══██╗",
232
+ "██║ ██║",
233
+ "██║▄▄ ██║",
234
+ "╚██████╔╝",
235
+ " ╚══▀▀═╝ ",
236
+ ],
237
+ "r" => [
238
+ "██████╗ ",
239
+ "██╔══██╗",
240
+ "██████╔╝",
241
+ "██╔══██╗",
242
+ "██║ ██║",
243
+ "╚═╝ ╚═╝",
244
+ ],
245
+ "s" => [
246
+ "███████╗",
247
+ "██╔════╝",
248
+ "███████╗",
249
+ "╚════██║",
250
+ "███████║",
251
+ "╚══════╝",
252
+ ],
253
+ "t" => [
254
+ "████████╗",
255
+ "╚══██╔══╝",
256
+ " ██║ ",
257
+ " ██║ ",
258
+ " ██║ ",
259
+ " ╚═╝ ",
260
+ ],
261
+ "u" => [
262
+ "██╗ ██╗",
263
+ "██║ ██║",
264
+ "██║ ██║",
265
+ "██║ ██║",
266
+ "╚██████╔╝",
267
+ " ╚═════╝ ",
268
+ ],
269
+ "v" => [
270
+ "██╗ ██╗",
271
+ "██║ ██║",
272
+ "██║ ██║",
273
+ "╚██╗ ██╔╝",
274
+ " ╚████╔╝ ",
275
+ " ╚═══╝ ",
276
+ ],
277
+ "w" => [
278
+ "██╗ ██╗",
279
+ "██║ ██║",
280
+ "██║ █╗ ██║",
281
+ "██║███╗██║",
282
+ "╚███╔███╔╝",
283
+ " ╚══╝╚══╝ ",
284
+ ],
285
+ "x" => [
286
+ "██╗ ██╗",
287
+ "╚██╗██╔╝",
288
+ " ╚███╔╝ ",
289
+ " ██╔██╗ ",
290
+ "██╔╝ ██╗",
291
+ "╚═╝ ╚═╝",
292
+ ],
293
+ "y" => [
294
+ "██╗ ██╗",
295
+ "╚██╗ ██╔╝",
296
+ " ╚████╔╝ ",
297
+ " ╚██╔╝ ",
298
+ " ██║ ",
299
+ " ╚═╝ ",
300
+ ],
301
+ "z" => [
302
+ "███████╗",
303
+ "╚════██║",
304
+ " ██╔╝",
305
+ " ██╔╝ ",
306
+ "███████╗",
307
+ "╚══════╝",
308
+ ],
309
+ }.freeze
310
+ # rubocop:enable Layout/ExtraSpacing, Layout/SpaceInsideArrayLiteralBrackets
311
+
312
+ HEIGHT = 6
313
+ GLYPH_GAP = " "
314
+
315
+ # Render a string as block-font art. Unknown characters fall back to space.
316
+ # Returns a multi-line string (6 lines joined by newlines).
317
+ def self.render(text)
318
+ chars = text.downcase.chars
319
+ glyphs = chars.map { |c| GLYPHS[c] || GLYPHS[" "] }
320
+
321
+ HEIGHT.times.map do |row|
322
+ glyphs.map { |g| g[row] }.join(GLYPH_GAP)
323
+ end.join("\n")
324
+ end
325
+
326
+ # Return the pixel width of the rendered text (longest line length).
327
+ def self.width(text)
328
+ render(text).lines.map { |l| l.chomp.length }.max.to_i
329
+ end
330
+ end
331
+ end