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,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Octo
6
+ # Loads and represents an agent profile (system prompt + skill whitelist).
7
+ #
8
+ # Lookup order for a profile named "coding":
9
+ # 1. ~/.octo/agents/coding/ (user override)
10
+ # 2. <gem>/lib/octo/default_agents/coding/ (built-in default)
11
+ #
12
+ # Each profile directory must contain:
13
+ # - profile.yml — name, description, skills whitelist
14
+ # - system_prompt.md — agent-specific system prompt content
15
+ #
16
+ # Global files (shared across all agents), also with user-override support:
17
+ # - SOUL.md — agent personality/values
18
+ # - USER.md — user profile information
19
+ # - base_prompt.md — universal behavioral rules (todo manager, tool usage, etc.)
20
+ class AgentProfile
21
+ DEFAULT_AGENTS_DIR = File.expand_path("../default_agents", __FILE__).freeze
22
+ USER_AGENTS_DIR = File.expand_path("~/.octo/agents").freeze
23
+
24
+ attr_reader :name, :description
25
+
26
+ def initialize(name)
27
+ @name = name.to_s
28
+ profile_data = load_profile_yml
29
+ @description = profile_data["description"] || ""
30
+ @system_prompt_content = load_agent_file("system_prompt.md")
31
+ end
32
+
33
+ # Load a named profile. Raises ArgumentError if profile directory not found.
34
+ # @param name [String, Symbol] profile name (e.g. "coding", "general")
35
+ # @return [AgentProfile]
36
+ def self.load(name)
37
+ new(name)
38
+ end
39
+
40
+ # @return [String] agent-specific system prompt content
41
+ def system_prompt
42
+ @system_prompt_content
43
+ end
44
+
45
+ # @return [String] base prompt shared by all agents
46
+ def base_prompt
47
+ load_global_file("base_prompt.md")
48
+ end
49
+
50
+ # @return [String] soul content (user override → built-in default)
51
+ def soul
52
+ load_global_file("SOUL.md")
53
+ end
54
+
55
+ # @return [String] user profile content (user override → built-in default)
56
+ def user_profile
57
+ load_global_file("USER.md")
58
+ end
59
+
60
+ private def load_profile_yml
61
+ path = find_agent_file("profile.yml")
62
+ raise ArgumentError, "Agent profile '#{@name}' not found. " \
63
+ "Looked in #{user_agent_dir} and #{default_agent_dir}" unless path
64
+
65
+ YAML.safe_load(File.read(path)) || {}
66
+ end
67
+
68
+ # Load a file from the agent-specific directory (user override → built-in)
69
+ private def load_agent_file(filename)
70
+ path = find_agent_file(filename)
71
+ return "" unless path
72
+
73
+ File.read(path).strip
74
+ end
75
+
76
+ # Load a global file shared across all agents (user override → built-in)
77
+ private def load_global_file(filename)
78
+ user_path = File.join(USER_AGENTS_DIR, filename)
79
+ default_path = File.join(DEFAULT_AGENTS_DIR, filename)
80
+
81
+ path = if File.exist?(user_path) && !File.zero?(user_path)
82
+ user_path
83
+ elsif File.exist?(default_path)
84
+ default_path
85
+ end
86
+
87
+ return "" unless path
88
+
89
+ File.read(path).strip
90
+ end
91
+
92
+ # Find a file in user override dir first, then built-in default dir
93
+ private def find_agent_file(filename)
94
+ user_path = File.join(user_agent_dir, filename)
95
+ default_path = File.join(default_agent_dir, filename)
96
+
97
+ if File.exist?(user_path) && !File.zero?(user_path)
98
+ user_path
99
+ elsif File.exist?(default_path)
100
+ default_path
101
+ end
102
+ end
103
+
104
+ private def user_agent_dir
105
+ File.join(USER_AGENTS_DIR, @name)
106
+ end
107
+
108
+ private def default_agent_dir
109
+ File.join(DEFAULT_AGENTS_DIR, @name)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Octo
6
+ # Reassembles an Anthropic Messages SSE stream (event: message_start /
7
+ # content_block_start / content_block_delta / content_block_stop /
8
+ # message_delta / message_stop / ping) into the same hash shape that
9
+ # MessageFormat::Anthropic.parse_response expects from a non-streaming
10
+ # response, while invoking on_chunk(input_tokens:, output_tokens:) as
11
+ # usage accumulates.
12
+ #
13
+ # Wire reference: https://docs.anthropic.com/en/api/messages-streaming
14
+ class AnthropicStreamAggregator
15
+ def initialize(on_chunk: nil)
16
+ @on_chunk = on_chunk
17
+ @blocks = {}
18
+ @stop_reason = nil
19
+ @usage = {}
20
+ @last_input_tokens = 0
21
+ @last_output_tokens = 0
22
+ end
23
+
24
+ def handle(event, data_str)
25
+ data = parse_or_nil(data_str)
26
+ return unless data
27
+
28
+ case event
29
+ when "message_start"
30
+ msg = data["message"] || {}
31
+ if (u = msg["usage"])
32
+ @usage.merge!(u)
33
+ emit_usage_progress
34
+ end
35
+ when "content_block_start"
36
+ idx = data["index"] || @blocks.size
37
+ cb = data["content_block"] || {}
38
+ case cb["type"]
39
+ when "tool_use"
40
+ @blocks[idx] = { kind: :tool_use, id: cb["id"], name: cb["name"], input_str: +"" }
41
+ else
42
+ @blocks[idx] = { kind: :text, text: +"" }
43
+ end
44
+ when "content_block_delta"
45
+ idx = data["index"] || 0
46
+ delta = data["delta"] || {}
47
+ block = (@blocks[idx] ||= { kind: :text, text: +"" })
48
+ case delta["type"]
49
+ when "text_delta"
50
+ block[:kind] ||= :text
51
+ block[:text] ||= +""
52
+ block[:text] << delta["text"].to_s
53
+ when "input_json_delta"
54
+ block[:kind] = :tool_use
55
+ block[:input_str] ||= +""
56
+ block[:input_str] << delta["partial_json"].to_s
57
+ when "thinking_delta"
58
+ block[:kind] = :thinking
59
+ block[:thinking] ||= +""
60
+ block[:thinking] << delta["thinking"].to_s
61
+ end
62
+ emit_estimate_progress
63
+ when "content_block_stop"
64
+ # Nothing to do: blocks are finalised in to_h.
65
+ when "message_delta"
66
+ if (d = data["delta"])
67
+ @stop_reason = d["stop_reason"] if d["stop_reason"]
68
+ end
69
+ if (u = data["usage"])
70
+ @usage.merge!(u)
71
+ emit_usage_progress
72
+ end
73
+ when "message_stop", "ping", "error"
74
+ # no-op
75
+ end
76
+ end
77
+
78
+ # Canonical non-streaming Anthropic response shape consumed by
79
+ # MessageFormat::Anthropic.parse_response.
80
+ def to_h
81
+ content_blocks = @blocks.keys.sort.map do |idx|
82
+ b = @blocks[idx]
83
+ case b[:kind]
84
+ when :tool_use
85
+ input_value =
86
+ if b[:input_str].to_s.empty?
87
+ {}
88
+ else
89
+ JSON.parse(b[:input_str]) rescue b[:input_str]
90
+ end
91
+ { "type" => "tool_use", "id" => b[:id], "name" => b[:name], "input" => input_value }
92
+ when :thinking
93
+ { "type" => "thinking", "thinking" => b[:thinking].to_s }
94
+ else
95
+ { "type" => "text", "text" => b[:text].to_s }
96
+ end
97
+ end
98
+
99
+ { "content" => content_blocks, "stop_reason" => @stop_reason, "usage" => @usage }
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
109
+ return unless @on_chunk
110
+ input = @usage["input_tokens"].to_i + @usage["cache_read_input_tokens"].to_i
111
+ output = @usage["output_tokens"].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("[AnthropicStreamAggregator] 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("[AnthropicStreamAggregator] 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[:thinking].to_s.bytesize
133
+ end
134
+ (total_chars / 4.0).ceil
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Octo
6
+ class BackgroundTaskRegistry
7
+ @tasks = {}
8
+ @callbacks = {}
9
+ @mutex = Mutex.new
10
+ @sweep_running = false
11
+
12
+ HANDLE_ALPHABET = (('a'..'z').to_a + ('0'..'9').to_a).freeze
13
+ HANDLE_LENGTH = 9
14
+
15
+ TTL_UNWATCHED = 600
16
+ SWEEP_INTERVAL = 30
17
+
18
+ class << self
19
+ def create_task(type:, metadata: {}, on_cancel: nil, dedup_key: nil)
20
+ @mutex.synchronize do
21
+ if dedup_key
22
+ existing = @tasks.values.find do |t|
23
+ t[:status] == "running" &&
24
+ t[:type] == type &&
25
+ t[:metadata]&.[](:dedup_key) == dedup_key
26
+ end
27
+ if existing
28
+ return { duplicate: true, handle_id: existing[:id], created_at: existing[:created_at] }
29
+ end
30
+ end
31
+
32
+ handle_id = generate_unique_handle_id
33
+ @tasks[handle_id] = {
34
+ id: handle_id,
35
+ type: type,
36
+ status: "running",
37
+ metadata: dedup_key ? metadata.merge(dedup_key: dedup_key) : metadata,
38
+ result: nil,
39
+ created_at: Time.now,
40
+ completed_at: nil,
41
+ on_cancel: on_cancel,
42
+ last_activity_at: metadata[:watched] ? nil : Time.now
43
+ }
44
+
45
+ ensure_sweep_thread unless metadata[:watched]
46
+
47
+ handle_id
48
+ end
49
+ end
50
+
51
+ def register_callback(handle_id:, agent:, &block)
52
+ fire_immediately = nil
53
+ captured_task = nil
54
+
55
+ @mutex.synchronize do
56
+ task = @tasks[handle_id]
57
+ return false unless task
58
+
59
+ if task[:status] == "completed" || task[:status] == "cancelled"
60
+ fire_immediately = task[:result] || {
61
+ cancelled: task[:status] == "cancelled",
62
+ output: task[:cancel_reason] || (task[:status] == "cancelled" ? "Task was cancelled by user." : ""),
63
+ exit_code: nil,
64
+ state: task[:status]
65
+ }
66
+ captured_task = task
67
+ else
68
+ @callbacks[handle_id] = {
69
+ agent: agent,
70
+ callback: block,
71
+ registered_at: Time.now
72
+ }
73
+ end
74
+ end
75
+
76
+ fire_immediately = enrich_with_timing(fire_immediately, captured_task) if fire_immediately
77
+
78
+ if fire_immediately
79
+ Thread.new do
80
+ Thread.current.name = "bg-task-notify-late-#{handle_id[0, 8]}"
81
+ begin
82
+ block.call(fire_immediately)
83
+ rescue => e
84
+ Octo::Logger.warn("background_task_callback_retry",
85
+ handle_id: handle_id,
86
+ agent_session: agent&.session_id,
87
+ error: e
88
+ )
89
+ begin
90
+ sleep 0.5
91
+ block.call(fire_immediately)
92
+ rescue => e2
93
+ Octo::Logger.error("background_task_callback_error",
94
+ handle_id: handle_id,
95
+ agent_session: agent&.session_id,
96
+ error: e2
97
+ )
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ true
104
+ end
105
+
106
+ def cancel(handle_id, reason: nil)
107
+ task = nil
108
+ handler = nil
109
+
110
+ @mutex.synchronize do
111
+ task = @tasks[handle_id]
112
+ return false unless task
113
+ return false if task[:status] == "completed" || task[:status] == "cancelled"
114
+
115
+ task[:status] = "cancelled"
116
+ task[:completed_at] = Time.now
117
+ task[:cancel_reason] = reason || "Task was cancelled by user."
118
+ handler = @callbacks.delete(handle_id)
119
+ end
120
+
121
+ begin
122
+ task[:on_cancel]&.call(task)
123
+ rescue => e
124
+ Octo::Logger.error("background_task_cancel_hook_error",
125
+ handle_id: handle_id,
126
+ error: e
127
+ )
128
+ end
129
+
130
+ if handler
131
+ enriched = enrich_with_timing({
132
+ cancelled: true,
133
+ output: task[:cancel_reason],
134
+ exit_code: nil,
135
+ state: "cancelled"
136
+ }, task)
137
+ Thread.new do
138
+ Thread.current.name = "bg-task-cancel-#{handle_id[0, 8]}"
139
+ begin
140
+ handler[:callback].call(enriched)
141
+ rescue => e
142
+ Octo::Logger.warn("background_task_callback_retry",
143
+ handle_id: handle_id,
144
+ agent_session: handler[:agent]&.session_id,
145
+ error: e
146
+ )
147
+ begin
148
+ sleep 0.5
149
+ handler[:callback].call(enriched)
150
+ rescue => e2
151
+ Octo::Logger.error("background_task_callback_error",
152
+ handle_id: handle_id,
153
+ agent_session: handler[:agent]&.session_id,
154
+ error: e2
155
+ )
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ true
162
+ end
163
+
164
+ def complete(handle_id, result)
165
+ task = nil
166
+ handler = nil
167
+
168
+ @mutex.synchronize do
169
+ task = @tasks[handle_id]
170
+ return unless task
171
+ return if task[:status] == "cancelled"
172
+
173
+ task[:status] = "completed"
174
+ task[:result] = result
175
+ task[:completed_at] = Time.now
176
+
177
+ handler = @callbacks.delete(handle_id)
178
+ end
179
+
180
+ return unless handler
181
+
182
+ enriched = enrich_with_timing(result, task)
183
+
184
+ Thread.new do
185
+ Thread.current.name = "bg-task-notify-#{handle_id[0, 8]}"
186
+ begin
187
+ handler[:callback].call(enriched)
188
+ rescue => e
189
+ Octo::Logger.warn("background_task_callback_retry",
190
+ handle_id: handle_id,
191
+ agent_session: handler[:agent]&.session_id,
192
+ error: e
193
+ )
194
+ begin
195
+ sleep 0.5
196
+ handler[:callback].call(enriched)
197
+ rescue => e2
198
+ Octo::Logger.error("background_task_callback_error",
199
+ handle_id: handle_id,
200
+ agent_session: handler[:agent]&.session_id,
201
+ error: e2
202
+ )
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ def list_running(agent_session_id: nil)
209
+ @mutex.synchronize do
210
+ tasks = @tasks.values.select { |t| t[:status] == "running" }
211
+ tasks = tasks.select { |t| t[:metadata][:agent_session_id] == agent_session_id } if agent_session_id
212
+ tasks.map do |t|
213
+ {
214
+ handle_id: t[:id],
215
+ type: t[:type],
216
+ command: t[:metadata][:command],
217
+ started_at: t[:created_at]
218
+ }
219
+ end
220
+ end
221
+ end
222
+
223
+ def get(handle_id)
224
+ @mutex.synchronize { @tasks[handle_id]&.dup }
225
+ end
226
+
227
+ def record_activity(handle_id)
228
+ @mutex.synchronize do
229
+ task = @tasks[handle_id]
230
+ task[:last_activity_at] = Time.now if task
231
+ end
232
+ end
233
+
234
+ def forget(handle_id)
235
+ @mutex.synchronize do
236
+ @tasks.delete(handle_id)
237
+ @callbacks.delete(handle_id)
238
+ end
239
+ end
240
+
241
+ def prune_completed(max_age: 3600, agent_session_id: nil)
242
+ cutoff = Time.now - max_age
243
+ @mutex.synchronize do
244
+ @tasks.delete_if do |_id, task|
245
+ next false unless task[:status] == "completed"
246
+ next false unless task[:completed_at] && task[:completed_at] < cutoff
247
+ next false if agent_session_id && task[:metadata][:agent_session_id] != agent_session_id
248
+ true
249
+ end
250
+ end
251
+ end
252
+
253
+ def reset!
254
+ @mutex.synchronize do
255
+ @tasks.clear
256
+ @callbacks.clear
257
+ @sweep_running = false
258
+ end
259
+ end
260
+
261
+ private def ensure_sweep_thread
262
+ return if @sweep_running
263
+ @sweep_running = true
264
+ Thread.new do
265
+ Thread.current.name = "bg-task-sweep"
266
+ sweep_loop
267
+ end
268
+ end
269
+
270
+ private def sweep_loop
271
+ while @sweep_running
272
+ sleep SWEEP_INTERVAL
273
+ break unless @sweep_running
274
+
275
+ to_cancel = []
276
+ @mutex.synchronize do
277
+ now = Time.now
278
+ @tasks.each do |handle_id, task|
279
+ next unless task[:status] == "running"
280
+ next if task[:metadata]&.[](:watched)
281
+
282
+ last_activity = task[:last_activity_at] || task[:created_at]
283
+ next unless last_activity
284
+ ttl = task[:metadata]&.[](:max_duration) || TTL_UNWATCHED
285
+ next if now - last_activity < ttl
286
+
287
+ to_cancel << { handle_id: handle_id, ttl: ttl }
288
+ end
289
+ end
290
+
291
+ to_cancel.each do |item|
292
+ begin
293
+ cancel(item[:handle_id], reason: "Task timed out after #{item[:ttl]}s")
294
+ Octo::Logger.info("bg_task_ttl_cleanup",
295
+ handle_id: item[:handle_id],
296
+ reason: "unwatched handle exceeded #{item[:ttl]}s TTL"
297
+ )
298
+ rescue => e
299
+ Octo::Logger.error("bg_task_sweep_error",
300
+ handle_id: item[:handle_id],
301
+ error: e
302
+ )
303
+ end
304
+ end
305
+ end
306
+ end
307
+
308
+ private def generate_unique_handle_id
309
+ loop do
310
+ id = HANDLE_LENGTH.times.map { HANDLE_ALPHABET[SecureRandom.random_number(HANDLE_ALPHABET.size)] }.join
311
+ return id unless @tasks.key?(id)
312
+ end
313
+ end
314
+
315
+ private def enrich_with_timing(result, task)
316
+ return result unless task && task[:created_at] && task[:completed_at]
317
+ result.merge(
318
+ started_at: task[:created_at],
319
+ elapsed_seconds: (task[:completed_at] - task[:created_at]).round
320
+ )
321
+ end
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+ require_relative "version"
5
+ require_relative "block_font"
6
+
7
+ module Octo
8
+ # Banner provides logo for CLI startup.
9
+ class Banner
10
+ DEFAULT_CLI_LOGO = Octo::BlockFont.render("octo")
11
+
12
+ TAGLINE = "[>] Your personal Assistant & Technical Co-founder"
13
+
14
+ def initialize
15
+ @pastel = Pastel.new
16
+ end
17
+
18
+ def logo
19
+ @pastel.cyan(DEFAULT_CLI_LOGO)
20
+ end
21
+
22
+ def tagline
23
+ @pastel.dim(TAGLINE)
24
+ end
25
+
26
+ def highlight(text)
27
+ @pastel.bright_white(text)
28
+ end
29
+
30
+ def full_banner
31
+ "#{logo}\n#{tagline}"
32
+ end
33
+ end
34
+ end