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,318 @@
1
+ # Billing System
2
+
3
+ ## Overview
4
+
5
+ The Billing System provides persistent tracking of API usage and costs across all
6
+ sessions. It records every LLM API call with token counts and calculated costs,
7
+ storing them in monthly JSONL files for easy querying and analysis.
8
+
9
+ ## Design Principles
10
+
11
+ - **Non-blocking** — Billing persistence is fire-and-forget; failures never interrupt agent flow
12
+ - **Minimal footprint** — JSONL format, one file per month, no database dependency
13
+ - **Privacy-first** — Data stored locally in `~/.octo/billing/`, never uploaded
14
+ - **Accurate costing** — Uses the same `ModelPricing` module as real-time display
15
+
16
+ ---
17
+
18
+ ## Architecture
19
+
20
+ ```
21
+ ┌─────────────────────────────────────────────────────────────────┐
22
+ │ Agent │
23
+ │ CostTracker module │
24
+ │ └── track_cost() │
25
+ │ ├── Calculate cost (ModelPricing) │
26
+ │ ├── Update UI (real-time) │
27
+ │ └── persist_billing_record() ──────┐ │
28
+ └─────────────────────────────────────────────┼───────────────────┘
29
+
30
+
31
+ ┌─────────────────────────────────────────────────────────────────┐
32
+ │ Billing Module │
33
+ │ lib/octo/billing/ │
34
+ │ ├── billing_record.rb (data structure) │
35
+ │ └── billing_store.rb (JSONL persistence) │
36
+ └─────────────────────────────────────────────────────────────────┘
37
+
38
+
39
+ ┌─────────────────────────────────────────────────────────────────┐
40
+ │ Storage │
41
+ │ ~/.octo/billing/ │
42
+ │ ├── 2026-05.jsonl │
43
+ │ ├── 2026-04.jsonl │
44
+ │ └── ... │
45
+ └─────────────────────────────────────────────────────────────────┘
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Components
51
+
52
+ ### BillingRecord (`lib/octo/billing/billing_record.rb`)
53
+
54
+ A Struct representing a single API call:
55
+
56
+ | Field | Type | Description |
57
+ |-------|------|-------------|
58
+ | `id` | String | UUID, auto-generated |
59
+ | `session_id` | String | Associated session |
60
+ | `timestamp` | Time | When the call was made |
61
+ | `model` | String | Model name (e.g., "claude-sonnet-4.5") |
62
+ | `prompt_tokens` | Integer | Input tokens |
63
+ | `completion_tokens` | Integer | Output tokens |
64
+ | `cache_read_tokens` | Integer | Tokens read from cache |
65
+ | `cache_write_tokens` | Integer | Tokens written to cache |
66
+ | `cost_usd` | Float | Calculated cost in USD |
67
+ | `cost_source` | Symbol | `:api`, `:price`, or `:estimated` |
68
+
69
+ ### BillingStore (`lib/octo/billing/billing_store.rb`)
70
+
71
+ Handles persistence and querying:
72
+
73
+ ```ruby
74
+ store = Octo::Billing::BillingStore.new
75
+
76
+ # Append a record
77
+ store.append(record)
78
+
79
+ # Query with filters
80
+ records = store.query(from: 1.week.ago, model: "claude-sonnet-4.5", limit: 100)
81
+
82
+ # Get summary statistics
83
+ summary = store.summary(period: :month)
84
+ # => { total_cost: 12.34, total_tokens: 500000, by_model: {...}, ... }
85
+
86
+ # Daily breakdown for charts
87
+ daily = store.daily_breakdown(days: 30)
88
+ # => [{ date: "2026-05-01", cost: 1.23, tokens: 50000, requests: 42 }, ...]
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Storage Format
94
+
95
+ Records are stored as JSON Lines (one JSON object per line):
96
+
97
+ ```jsonl
98
+ {"id":"abc123","session_id":"def456","timestamp":"2026-05-22T15:30:00+08:00","model":"claude-sonnet-4.5","prompt_tokens":1500,"completion_tokens":500,"cache_read_tokens":1000,"cache_write_tokens":0,"cost_usd":0.0045,"cost_source":"price"}
99
+ {"id":"abc124","session_id":"def456","timestamp":"2026-05-22T15:31:00+08:00","model":"claude-sonnet-4.5","prompt_tokens":2000,"completion_tokens":800,"cache_read_tokens":1500,"cache_write_tokens":0,"cost_usd":0.0052,"cost_source":"price"}
100
+ ```
101
+
102
+ **Why JSONL?**
103
+ - Append-only writes (no file locking needed)
104
+ - Easy to parse line-by-line (memory efficient)
105
+ - Human-readable for debugging
106
+ - Simple monthly rotation
107
+
108
+ ---
109
+
110
+ ## API Endpoints
111
+
112
+ ### GET /api/billing/summary
113
+
114
+ Returns aggregated statistics for a time period.
115
+
116
+ **Query Parameters:**
117
+ - `period` — `day`, `week`, `month`, `year`, or `all` (default: `month`)
118
+
119
+ **Response:**
120
+ ```json
121
+ {
122
+ "period": "month",
123
+ "from": "2026-05-01T00:00:00+08:00",
124
+ "to": "2026-05-22T15:30:00+08:00",
125
+ "total_cost": 12.3456,
126
+ "total_tokens": 500000,
127
+ "prompt_tokens": 350000,
128
+ "completion_tokens": 150000,
129
+ "cache_read_tokens": 200000,
130
+ "cache_write_tokens": 50000,
131
+ "by_model": {
132
+ "claude-sonnet-4.5": { "cost": 10.00, "requests": 100 },
133
+ "deepseek-v4-flash": { "cost": 2.34, "requests": 50 }
134
+ },
135
+ "by_day": {
136
+ "2026-05-22": 1.23,
137
+ "2026-05-21": 2.34
138
+ },
139
+ "record_count": 150
140
+ }
141
+ ```
142
+
143
+ ### GET /api/billing/daily
144
+
145
+ Returns daily cost breakdown for charting.
146
+
147
+ **Query Parameters:**
148
+ - `days` — Number of days (default: 30, max: 90)
149
+
150
+ **Response:**
151
+ ```json
152
+ {
153
+ "days": [
154
+ { "date": "2026-05-22", "cost": 1.2345, "tokens": 50000, "requests": 42 },
155
+ { "date": "2026-05-21", "cost": 2.3456, "tokens": 80000, "requests": 65 }
156
+ ]
157
+ }
158
+ ```
159
+
160
+ ### GET /api/billing/records
161
+
162
+ Returns raw billing records.
163
+
164
+ **Query Parameters:**
165
+ - `limit` — Max records (default: 100, max: 500)
166
+ - `model` — Filter by model name
167
+ - `session_id` — Filter by session ID
168
+
169
+ **Response:**
170
+ ```json
171
+ {
172
+ "records": [
173
+ { "id": "...", "timestamp": "...", "model": "...", "cost_usd": 0.01, ... }
174
+ ],
175
+ "count": 100
176
+ }
177
+ ```
178
+
179
+ ---
180
+
181
+ ## CLI Command
182
+
183
+ ```bash
184
+ # Show current month's billing
185
+ octo billing
186
+
187
+ # Show specific period
188
+ octo billing --period week
189
+ octo billing --period day
190
+ octo billing --period all
191
+
192
+ # Output as JSON (for scripting)
193
+ octo billing --json
194
+ ```
195
+
196
+ **Sample Output:**
197
+ ```
198
+ 📊 Billing Summary (month)
199
+ ──────────────────────────────────────────────────
200
+
201
+ 💰 Total Cost: $12.3456
202
+ 📝 Total Tokens: 500,000
203
+ 📥 Prompt Tokens: 350,000
204
+ 📤 Completion: 150,000
205
+ 🗄️ Cache Read: 200,000
206
+ 📝 Cache Write: 50,000
207
+ 🔢 API Requests: 150
208
+
209
+ 📈 By Model:
210
+ ──────────────────────────────────────────────────
211
+ claude-sonnet-4.5
212
+ Cost: $10.0000 | Requests: 100
213
+ deepseek-v4-flash
214
+ Cost: $2.3456 | Requests: 50
215
+
216
+ 📅 Recent Daily Usage:
217
+ ──────────────────────────────────────────────────
218
+ 2026-05-22 $1.2345 ████████████
219
+ 2026-05-21 $2.3456 ████████████████████████
220
+
221
+ ──────────────────────────────────────────────────
222
+ Data stored in: ~/.octo/billing/
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Web UI
228
+
229
+ The Billing panel is accessible from the sidebar under "My Data":
230
+
231
+ - **Summary cards** — Total cost, tokens, API requests
232
+ - **Token breakdown** — Prompt, completion, cache read/write
233
+ - **By Model table** — Cost and request count per model
234
+ - **Daily chart** — Visual bar chart of recent usage
235
+ - **Period selector** — Filter by day/week/month/year/all
236
+
237
+ ---
238
+
239
+ ## Currency Settings
240
+
241
+ The Web UI supports multiple currencies for cost display:
242
+
243
+ | Currency | Symbol | Exchange Rate |
244
+ |----------|--------|---------------|
245
+ | USD | $ | 1.0 (base) |
246
+ | CNY | ¥ | 6.7944 |
247
+
248
+ ### Configuration
249
+
250
+ 1. Go to **Settings** page
251
+ 2. Find the **Currency** section
252
+ 3. Select `$ USD` or `¥ CNY`
253
+
254
+ ### Scope
255
+
256
+ Currency settings apply to:
257
+ - Billing panel (total cost, model costs, daily chart)
258
+ - Session info bar (top cost display)
259
+ - Token usage lines (per-API-call cost)
260
+ - Task completion messages
261
+
262
+ **Note:** CLI always displays costs in USD (API's native currency).
263
+
264
+ ### Implementation
265
+
266
+ Currency preference is stored in browser `localStorage` under key `octo-currency`.
267
+
268
+ ```javascript
269
+ // Access currency utilities from Billing module
270
+ Billing.getCurrency() // "USD" or "CNY"
271
+ Billing.getCurrencySymbol() // "$" or "¥"
272
+ Billing.convertCost(usd) // Convert USD to selected currency
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Integration with CostTracker
278
+
279
+ The billing system hooks into `Agent::CostTracker#track_cost`:
280
+
281
+ ```ruby
282
+ def track_cost(usage, raw_api_usage: nil)
283
+ # ... existing cost calculation ...
284
+
285
+ # Persist billing record (skip for subagents to avoid double-counting)
286
+ unless @is_subagent
287
+ persist_billing_record(usage, iteration_cost)
288
+ end
289
+
290
+ token_data
291
+ end
292
+ ```
293
+
294
+ **Key behaviors:**
295
+ - Subagent costs are NOT recorded separately (parent agent merges them)
296
+ - Unknown model costs (nil) are skipped
297
+ - Persistence failures are logged but never raise
298
+
299
+ ---
300
+
301
+ ## Data Retention
302
+
303
+ - Records are stored indefinitely by default
304
+ - Monthly files can be manually deleted from `~/.octo/billing/`
305
+ - Future: `BillingStore#cleanup(before: 1.year.ago)` for automated retention
306
+
307
+ ---
308
+
309
+ ## Future Enhancements
310
+
311
+ - [ ] Export to CSV/JSON
312
+ - [ ] Budget alerts (daily/monthly limits)
313
+ - [ ] Cost comparison across models
314
+ - [ ] Session-level cost breakdown in UI
315
+ - [x] i18n support for billing labels (English/Chinese)
316
+ - [x] Currency settings (USD/CNY)
317
+ - [ ] Dynamic exchange rate updates
318
+ - [ ] More currency options (EUR, JPY, etc.)
@@ -0,0 +1,235 @@
1
+ # Channel Architecture
2
+
3
+ ## Overview
4
+
5
+ Channel is a feature that bridges Octo's Server Sessions to IM platforms
6
+ (Feishu, WeCom, DingTalk, etc.). It reuses the existing Agent + SessionRegistry
7
+ infrastructure — the Agent knows nothing about IM; the Channel layer is purely
8
+ a transport adapter.
9
+
10
+ ## Design Principles
11
+
12
+ - **Zero Agent intrusion** — Agent only speaks `UIInterface`; swap the controller, get IM output
13
+ - **Reuse SessionRegistry** — IM chats resolve to the same `SessionRegistry` sessions as Web UI
14
+ - **WebSocket long connection** — No public domain required; adapters hold a persistent WSS connection to the IM platform
15
+ - **One platform = 2 threads** — read loop thread + ping/heartbeat thread (constant, small footprint)
16
+
17
+ ---
18
+
19
+ ## Layer Diagram
20
+
21
+ ```
22
+ IM Platforms (Feishu / WeCom / DingTalk)
23
+ │ WebSocket long connection (wss://)
24
+
25
+ ┌─────────────────────────────────────┐
26
+ │ Channel Adapter Layer │
27
+ │ Feishu::Adapter │
28
+ │ ├── WSClient (read loop + ping) │
29
+ │ ├── Bot (send API) │
30
+ │ └── MessageParser │
31
+ │ Wecom::Adapter │
32
+ │ └── WSClient (read loop + ping) │
33
+ │ (future) Dingtalk::Adapter │
34
+ └──────────────┬──────────────────────┘
35
+ │ standardized event Hash
36
+
37
+ ┌─────────────────────────────────────┐
38
+ │ ChannelManager │
39
+ │ • Owns adapter threads │
40
+ │ • Routes inbound event → │
41
+ │ ChannelBinding → session_id │
42
+ │ • Calls agent.run in Thread.new │
43
+ └──────────────┬──────────────────────┘
44
+
45
+ ┌───────┴────────┐
46
+ ▼ ▼
47
+ SessionRegistry ChannelUIController
48
+ (existing) (implements UIInterface)
49
+ │ │
50
+ ▼ ▼
51
+ Agent IM Platform reply
52
+ (unchanged) via adapter.send_text
53
+ ```
54
+
55
+ ---
56
+
57
+ ## File Structure
58
+
59
+ ```
60
+ lib/octo/channel/
61
+ ├── adapters/
62
+ │ ├── base.rb # Adapter abstract base + registry
63
+ │ ├── feishu/
64
+ │ │ ├── adapter.rb # Feishu::Adapter < Base
65
+ │ │ ├── bot.rb # HTTP send API (token cache, Markdown/card)
66
+ │ │ ├── message_parser.rb # Raw WS event → standardized Hash
67
+ │ │ └── ws_client.rb # Feishu protobuf WS long connection
68
+ │ └── wecom/
69
+ │ ├── adapter.rb # Wecom::Adapter < Base
70
+ │ └── ws_client.rb # WeCom JSON WS long connection
71
+ ├── channel_message.rb # Struct: standardized inbound message
72
+ ├── channel_binding.rb # (platform, user_id) → session_id mapping
73
+ ├── channel_ui_controller.rb # UIInterface impl — pushes events to IM
74
+ └── channel_manager.rb # Lifecycle: start/stop adapters, route messages
75
+ lib/octo/channel.rb # Top-level require entry point
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Standardized Inbound Event
81
+
82
+ All adapters yield the same Hash shape to `ChannelManager`:
83
+
84
+ ```ruby
85
+ {
86
+ platform: :feishu, # Symbol
87
+ chat_id: "oc_xxx", # String — IM chat/group identifier
88
+ user_id: "ou_xxx", # String — IM user identifier
89
+ text: "deploy now", # String — cleaned user text
90
+ message_id: "om_xxx", # String — for threading / update
91
+ timestamp: Time, # Time object
92
+ chat_type: :direct | :group, # Symbol
93
+ raw: { ... } # Original platform payload
94
+ }
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Adapter Interface (Base)
100
+
101
+ ```ruby
102
+ class Adapters::Base
103
+ def self.platform_id → Symbol
104
+ def self.platform_config(raw_config) → Hash # symbol-keyed
105
+ def self.env_keys → Array<String> # for config serialization
106
+
107
+ def start(&on_message) # blocks; yields event Hash per inbound message
108
+ def stop # graceful shutdown
109
+ def send_text(chat_id, text, reply_to: nil) → Hash
110
+ def update_message(chat_id, message_id, text) → Boolean
111
+ def supports_message_updates? → Boolean
112
+ def validate_config(config) → Array<String> # error messages
113
+ end
114
+ ```
115
+
116
+ ---
117
+
118
+ ## ChannelManager
119
+
120
+ ```ruby
121
+ class ChannelManager
122
+ def initialize(session_registry:, session_builder:, channel_config:, agent_config:)
123
+
124
+ def start # Thread.new per enabled platform adapter
125
+ def stop # kills all adapter threads gracefully
126
+
127
+ private
128
+
129
+ def route_message(adapter, event)
130
+ session_id = @binding.resolve_or_create(event, session_builder: @session_builder)
131
+ ui = ChannelUIController.new(event, adapter)
132
+ Thread.new { run_agent(session_id, event[:text], ui) }
133
+ end
134
+ end
135
+ ```
136
+
137
+ ---
138
+
139
+ ## ChannelBinding
140
+
141
+ Maps `(platform, user_id)` → `session_id`. Persisted to `~/.octo/channel_bindings.yml`.
142
+
143
+ Binding modes (configurable per platform):
144
+
145
+ | Mode | Key | Description |
146
+ |------|-----|-------------|
147
+ | `user` | `(platform, user_id)` | Each IM user gets their own session (default) |
148
+ | `chat` | `(platform, chat_id)` | Whole group shares one session |
149
+
150
+ ---
151
+
152
+ ## ChannelUIController
153
+
154
+ Implements `UIInterface`. Key behaviours:
155
+
156
+ - `show_assistant_message` → `adapter.send_text(chat_id, content)`
157
+ - `show_tool_call` → buffers as `⚙️ \`tool summary\`` (flushed on next message)
158
+ - `show_progress` → `adapter.update_message(...)` if `supports_message_updates?`
159
+ - `show_complete` → sends `✅ Complete • N iterations • $cost`
160
+ - `request_confirmation` → **not supported in IM** (returns auto-approved / raises)
161
+
162
+ ---
163
+
164
+ ## Thread Model
165
+
166
+ ```
167
+ Main thread (WEBrick server.start — blocks)
168
+ ├── WEBrick request threads (existing)
169
+ ├── Agent task threads (existing, per task)
170
+ ├── Scheduler thread (existing, octo-scheduler)
171
+ └── ChannelManager
172
+ ├── feishu-adapter thread (WSClient read loop, constant)
173
+ │ └── feishu-ping thread (heartbeat, 90s)
174
+ └── wecom-adapter thread (WSClient read loop, constant)
175
+ └── wecom-ping thread (heartbeat, 30s)
176
+ ```
177
+
178
+ Per enabled platform: **2 constant threads**. Agent task threads are spawned
179
+ on demand (same as Web UI path) and exit when done.
180
+
181
+ ---
182
+
183
+ ## Configuration
184
+
185
+ Channel credentials live in `~/.octo/channels.yml` (managed by `ChannelConfig`
186
+ which already exists in main branch):
187
+
188
+ ```yaml
189
+ channels:
190
+ feishu:
191
+ enabled: true
192
+ app_id: cli_xxx
193
+ app_secret: xxx
194
+ allowed_users:
195
+ - ou_xxx
196
+ wecom:
197
+ enabled: false
198
+ bot_id: xxx
199
+ secret: xxx
200
+ ```
201
+
202
+ `ChannelManager` reads this via `ChannelConfig#platform_config(platform)`.
203
+
204
+ ---
205
+
206
+ ## Integration with HttpServer
207
+
208
+ ```ruby
209
+ # HttpServer#initialize
210
+ @channel_manager = ChannelManager.new(
211
+ session_registry: @registry,
212
+ session_builder: method(:build_session),
213
+ channel_config: Octo::ChannelConfig.load,
214
+ agent_config: @agent_config
215
+ )
216
+
217
+ # HttpServer#start (after scheduler.start)
218
+ @channel_manager.start
219
+ ```
220
+
221
+ `ChannelManager#start` is non-blocking (spawns threads internally),
222
+ mirroring `Scheduler#start` behaviour.
223
+
224
+ ---
225
+
226
+ ## Future: DingTalk
227
+
228
+ DingTalk also supports a WebSocket Stream mode. Adding it means:
229
+
230
+ 1. `lib/octo/channel/adapters/dingtalk/adapter.rb` inheriting `Base`
231
+ 2. `lib/octo/channel/adapters/dingtalk/ws_client.rb`
232
+ 3. Register: `Adapters.register(:dingtalk, Adapter)`
233
+ 4. Add credentials to `ChannelConfig`
234
+
235
+ No changes needed to `ChannelManager`, `ChannelUIController`, or `ChannelBinding`.