rubino-agent 0.3.0

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 (376) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +115 -0
  4. data/.rubocop_todo.yml +955 -0
  5. data/.ruby-version +1 -0
  6. data/AGENTS.md +97 -0
  7. data/CHANGELOG.md +344 -0
  8. data/CONTRIBUTING.md +69 -0
  9. data/LICENSE +21 -0
  10. data/README.md +200 -0
  11. data/Rakefile +8 -0
  12. data/docs/agents.md +190 -0
  13. data/docs/api/v1.md +414 -0
  14. data/docs/architecture.md +177 -0
  15. data/docs/commands.md +375 -0
  16. data/docs/configuration.md +590 -0
  17. data/docs/getting-started.md +143 -0
  18. data/docs/jobs.md +332 -0
  19. data/docs/mcp.md +128 -0
  20. data/docs/memory.md +98 -0
  21. data/docs/models-and-keys.md +173 -0
  22. data/docs/oauth-providers.md +145 -0
  23. data/docs/plugins.md +195 -0
  24. data/docs/security.md +145 -0
  25. data/docs/skills.md +322 -0
  26. data/docs/tools.md +395 -0
  27. data/docs/troubleshooting.md +73 -0
  28. data/exe/rubino +9 -0
  29. data/install.sh +275 -0
  30. data/lib/rubino/active_skill.rb +50 -0
  31. data/lib/rubino/agent/agent_registry.rb +120 -0
  32. data/lib/rubino/agent/backoff_policy.rb +116 -0
  33. data/lib/rubino/agent/definition.rb +128 -0
  34. data/lib/rubino/agent/degenerate_recovery.rb +271 -0
  35. data/lib/rubino/agent/fallback_chain.rb +194 -0
  36. data/lib/rubino/agent/iteration_budget.rb +50 -0
  37. data/lib/rubino/agent/loop.rb +617 -0
  38. data/lib/rubino/agent/model_call_runner.rb +383 -0
  39. data/lib/rubino/agent/prompts/build.txt +69 -0
  40. data/lib/rubino/agent/prompts/compaction.txt +20 -0
  41. data/lib/rubino/agent/prompts/explore.txt +19 -0
  42. data/lib/rubino/agent/prompts/general.txt +20 -0
  43. data/lib/rubino/agent/prompts/plan.txt +31 -0
  44. data/lib/rubino/agent/response_validator.rb +70 -0
  45. data/lib/rubino/agent/router.rb +65 -0
  46. data/lib/rubino/agent/runner.rb +195 -0
  47. data/lib/rubino/agent/tool_executor.rb +402 -0
  48. data/lib/rubino/agent/truncation_continuation.rb +137 -0
  49. data/lib/rubino/api/middleware/auth.rb +43 -0
  50. data/lib/rubino/api/middleware/error_handler.rb +65 -0
  51. data/lib/rubino/api/middleware/json_parser.rb +100 -0
  52. data/lib/rubino/api/middleware/observability.rb +59 -0
  53. data/lib/rubino/api/middleware/rate_limit.rb +136 -0
  54. data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
  55. data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
  56. data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
  57. data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
  58. data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
  59. data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
  60. data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
  61. data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
  62. data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
  63. data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
  64. data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
  65. data/lib/rubino/api/operations/files/read_operation.rb +40 -0
  66. data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
  67. data/lib/rubino/api/operations/health_operation.rb +46 -0
  68. data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
  69. data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
  70. data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
  71. data/lib/rubino/api/operations/metrics_operation.rb +18 -0
  72. data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
  73. data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
  74. data/lib/rubino/api/operations/models/list_operation.rb +45 -0
  75. data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
  76. data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
  77. data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
  78. data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
  79. data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
  80. data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
  81. data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
  82. data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
  83. data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
  84. data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
  85. data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
  86. data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
  87. data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
  88. data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
  89. data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
  90. data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
  91. data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
  92. data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
  93. data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
  94. data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
  95. data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
  96. data/lib/rubino/api/request.rb +54 -0
  97. data/lib/rubino/api/responses.rb +64 -0
  98. data/lib/rubino/api/router.rb +72 -0
  99. data/lib/rubino/api/schemas.rb +103 -0
  100. data/lib/rubino/api/server.rb +102 -0
  101. data/lib/rubino/api/tls.rb +108 -0
  102. data/lib/rubino/attachments/classification.rb +16 -0
  103. data/lib/rubino/attachments/classify.rb +171 -0
  104. data/lib/rubino/attachments/defang.rb +47 -0
  105. data/lib/rubino/attachments/policy.rb +36 -0
  106. data/lib/rubino/attachments/preamble.rb +120 -0
  107. data/lib/rubino/boot/encryption_key.rb +32 -0
  108. data/lib/rubino/cli/chat/bang_shell.rb +257 -0
  109. data/lib/rubino/cli/chat/completion_builder.rb +290 -0
  110. data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
  111. data/lib/rubino/cli/chat/image_inbox.rb +168 -0
  112. data/lib/rubino/cli/chat/session_resolver.rb +176 -0
  113. data/lib/rubino/cli/chat_command.rb +1674 -0
  114. data/lib/rubino/cli/commands.rb +250 -0
  115. data/lib/rubino/cli/config_command.rb +96 -0
  116. data/lib/rubino/cli/doctor_command.rb +251 -0
  117. data/lib/rubino/cli/jobs_command.rb +60 -0
  118. data/lib/rubino/cli/memory_command.rb +135 -0
  119. data/lib/rubino/cli/onboarding_wizard.rb +207 -0
  120. data/lib/rubino/cli/server_command.rb +139 -0
  121. data/lib/rubino/cli/session_command.rb +125 -0
  122. data/lib/rubino/cli/setup_command.rb +107 -0
  123. data/lib/rubino/cli/skills_command.rb +85 -0
  124. data/lib/rubino/cli/tools_command.rb +81 -0
  125. data/lib/rubino/cli/trust_gate.rb +71 -0
  126. data/lib/rubino/commands/built_ins.rb +46 -0
  127. data/lib/rubino/commands/command.rb +116 -0
  128. data/lib/rubino/commands/executor.rb +550 -0
  129. data/lib/rubino/commands/handlers/agents.rb +510 -0
  130. data/lib/rubino/commands/handlers/config.rb +88 -0
  131. data/lib/rubino/commands/handlers/help.rb +148 -0
  132. data/lib/rubino/commands/handlers/jobs.rb +71 -0
  133. data/lib/rubino/commands/handlers/mcp.rb +229 -0
  134. data/lib/rubino/commands/handlers/memory.rb +200 -0
  135. data/lib/rubino/commands/handlers/sessions.rb +207 -0
  136. data/lib/rubino/commands/handlers/skills.rb +195 -0
  137. data/lib/rubino/commands/handlers/status.rb +211 -0
  138. data/lib/rubino/commands/loader.rb +90 -0
  139. data/lib/rubino/config/configuration.rb +455 -0
  140. data/lib/rubino/config/defaults.rb +569 -0
  141. data/lib/rubino/config/loader.rb +115 -0
  142. data/lib/rubino/config/reasoning_prefs.rb +67 -0
  143. data/lib/rubino/config/writer.rb +72 -0
  144. data/lib/rubino/context/compressor.rb +149 -0
  145. data/lib/rubino/context/environment_inspector.rb +176 -0
  146. data/lib/rubino/context/file_discovery.rb +45 -0
  147. data/lib/rubino/context/message_boundary.rb +39 -0
  148. data/lib/rubino/context/prompt_assembler.rb +382 -0
  149. data/lib/rubino/context/summary_builder.rb +159 -0
  150. data/lib/rubino/context/token_budget.rb +68 -0
  151. data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
  152. data/lib/rubino/database/connection.rb +77 -0
  153. data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
  154. data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
  155. data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
  156. data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
  157. data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
  158. data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
  159. data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
  160. data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
  161. data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
  162. data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
  163. data/lib/rubino/database/migrator.rb +48 -0
  164. data/lib/rubino/documents/converters/csv.rb +79 -0
  165. data/lib/rubino/documents/converters/docx.rb +129 -0
  166. data/lib/rubino/documents/converters/html.rb +28 -0
  167. data/lib/rubino/documents/converters/json.rb +35 -0
  168. data/lib/rubino/documents/converters/pdf.rb +59 -0
  169. data/lib/rubino/documents/converters/plain.rb +68 -0
  170. data/lib/rubino/documents/converters/pptx.rb +64 -0
  171. data/lib/rubino/documents/converters/xlsx.rb +62 -0
  172. data/lib/rubino/documents/converters/xml.rb +45 -0
  173. data/lib/rubino/documents/html.rb +71 -0
  174. data/lib/rubino/documents/registry.rb +68 -0
  175. data/lib/rubino/documents/table.rb +63 -0
  176. data/lib/rubino/documents.rb +50 -0
  177. data/lib/rubino/errors.rb +119 -0
  178. data/lib/rubino/files/workspace.rb +93 -0
  179. data/lib/rubino/interaction/cancel_token.rb +43 -0
  180. data/lib/rubino/interaction/clipboard_image.rb +84 -0
  181. data/lib/rubino/interaction/event_bus.rb +48 -0
  182. data/lib/rubino/interaction/events.rb +101 -0
  183. data/lib/rubino/interaction/image_input.rb +127 -0
  184. data/lib/rubino/interaction/input_queue.rb +117 -0
  185. data/lib/rubino/interaction/lifecycle.rb +299 -0
  186. data/lib/rubino/interaction/probe.rb +65 -0
  187. data/lib/rubino/interaction/state.rb +56 -0
  188. data/lib/rubino/jobs/cron_job_repository.rb +75 -0
  189. data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
  190. data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
  191. data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
  192. data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
  193. data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
  194. data/lib/rubino/jobs/queue.rb +184 -0
  195. data/lib/rubino/jobs/registry.rb +45 -0
  196. data/lib/rubino/jobs/runner.rb +79 -0
  197. data/lib/rubino/jobs/scheduler.rb +138 -0
  198. data/lib/rubino/jobs/webhook_delivery.rb +225 -0
  199. data/lib/rubino/jobs/worker.rb +59 -0
  200. data/lib/rubino/llm/adapter_factory.rb +47 -0
  201. data/lib/rubino/llm/adapter_response.rb +65 -0
  202. data/lib/rubino/llm/auxiliary_client.rb +61 -0
  203. data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
  204. data/lib/rubino/llm/content_builder.rb +55 -0
  205. data/lib/rubino/llm/credential_check.rb +93 -0
  206. data/lib/rubino/llm/error_classifier.rb +364 -0
  207. data/lib/rubino/llm/fake_provider.rb +292 -0
  208. data/lib/rubino/llm/inline_think_filter.rb +58 -0
  209. data/lib/rubino/llm/model_catalog.rb +29 -0
  210. data/lib/rubino/llm/provider_resolver.rb +48 -0
  211. data/lib/rubino/llm/reasoning_manager.rb +100 -0
  212. data/lib/rubino/llm/request.rb +56 -0
  213. data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
  214. data/lib/rubino/llm/scenario_loader.rb +68 -0
  215. data/lib/rubino/llm/scenario_selector.rb +80 -0
  216. data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
  217. data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
  218. data/lib/rubino/llm/scenarios/analysis.yml +501 -0
  219. data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
  220. data/lib/rubino/llm/scenarios/failure.yml +65 -0
  221. data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
  222. data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
  223. data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
  224. data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
  225. data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
  226. data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
  227. data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
  228. data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
  229. data/lib/rubino/llm/thinking_support.rb +84 -0
  230. data/lib/rubino/llm/tool_bridge.rb +89 -0
  231. data/lib/rubino/logger.rb +99 -0
  232. data/lib/rubino/mcp/manager.rb +180 -0
  233. data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
  234. data/lib/rubino/mcp.rb +57 -0
  235. data/lib/rubino/memory/backend.rb +104 -0
  236. data/lib/rubino/memory/backends/default.rb +101 -0
  237. data/lib/rubino/memory/backends/sqlite.rb +653 -0
  238. data/lib/rubino/memory/backends.rb +53 -0
  239. data/lib/rubino/memory/deduplicator.rb +74 -0
  240. data/lib/rubino/memory/extractor.rb +85 -0
  241. data/lib/rubino/memory/flusher.rb +31 -0
  242. data/lib/rubino/memory/retriever.rb +50 -0
  243. data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
  244. data/lib/rubino/memory/sqlite_graph.rb +154 -0
  245. data/lib/rubino/memory/store.rb +228 -0
  246. data/lib/rubino/memory/threat_scanner.rb +68 -0
  247. data/lib/rubino/metrics.rb +175 -0
  248. data/lib/rubino/modes.rb +93 -0
  249. data/lib/rubino/oauth/connection_repository.rb +95 -0
  250. data/lib/rubino/oauth/provider/github.rb +75 -0
  251. data/lib/rubino/oauth/provider/google.rb +59 -0
  252. data/lib/rubino/oauth/provider.rb +149 -0
  253. data/lib/rubino/oauth/registry.rb +86 -0
  254. data/lib/rubino/oauth/token_encryptor.rb +87 -0
  255. data/lib/rubino/plugins/registry.rb +75 -0
  256. data/lib/rubino/plugins.rb +86 -0
  257. data/lib/rubino/run/approval_gate.rb +243 -0
  258. data/lib/rubino/run/attachment_downloader.rb +166 -0
  259. data/lib/rubino/run/event_store.rb +74 -0
  260. data/lib/rubino/run/executor.rb +383 -0
  261. data/lib/rubino/run/gate_registry.rb +39 -0
  262. data/lib/rubino/run/recorder.rb +69 -0
  263. data/lib/rubino/run/repository.rb +118 -0
  264. data/lib/rubino/run/session_approval_cache.rb +118 -0
  265. data/lib/rubino/security/allowlist_persister.rb +55 -0
  266. data/lib/rubino/security/approval_policy.rb +227 -0
  267. data/lib/rubino/security/command_allowlist.rb +24 -0
  268. data/lib/rubino/security/dangerous_patterns.rb +118 -0
  269. data/lib/rubino/security/deny_persister.rb +73 -0
  270. data/lib/rubino/security/doom_loop_detector.rb +43 -0
  271. data/lib/rubino/security/hardline_guard.rb +105 -0
  272. data/lib/rubino/security/pattern_matcher.rb +62 -0
  273. data/lib/rubino/security/prefix_deriver.rb +124 -0
  274. data/lib/rubino/security/readonly_commands.rb +211 -0
  275. data/lib/rubino/session/exporter.rb +101 -0
  276. data/lib/rubino/session/message.rb +77 -0
  277. data/lib/rubino/session/repository.rb +295 -0
  278. data/lib/rubino/session/store.rb +198 -0
  279. data/lib/rubino/session/summary_store.rb +65 -0
  280. data/lib/rubino/skills/prompt_index.rb +85 -0
  281. data/lib/rubino/skills/registry.rb +208 -0
  282. data/lib/rubino/skills/skill.rb +176 -0
  283. data/lib/rubino/skills/skill_tool.rb +215 -0
  284. data/lib/rubino/skills/state_repository.rb +37 -0
  285. data/lib/rubino/skills/toggle.rb +26 -0
  286. data/lib/rubino/tools/answer_child_tool.rb +83 -0
  287. data/lib/rubino/tools/ask_parent_tool.rb +232 -0
  288. data/lib/rubino/tools/attach_file_tool.rb +120 -0
  289. data/lib/rubino/tools/background_tasks.rb +520 -0
  290. data/lib/rubino/tools/base.rb +222 -0
  291. data/lib/rubino/tools/custom_tool_loader.rb +119 -0
  292. data/lib/rubino/tools/edit_tool.rb +122 -0
  293. data/lib/rubino/tools/git_tool.rb +71 -0
  294. data/lib/rubino/tools/github_tool.rb +233 -0
  295. data/lib/rubino/tools/glob_tool.rb +69 -0
  296. data/lib/rubino/tools/grep_tool.rb +206 -0
  297. data/lib/rubino/tools/memory_tool.rb +184 -0
  298. data/lib/rubino/tools/multi_edit_tool.rb +110 -0
  299. data/lib/rubino/tools/patch_tool.rb +260 -0
  300. data/lib/rubino/tools/probe_tool.rb +175 -0
  301. data/lib/rubino/tools/question_tool.rb +128 -0
  302. data/lib/rubino/tools/read_attachment_tool.rb +180 -0
  303. data/lib/rubino/tools/read_tool.rb +212 -0
  304. data/lib/rubino/tools/read_tracker.rb +98 -0
  305. data/lib/rubino/tools/registry.rb +166 -0
  306. data/lib/rubino/tools/result.rb +113 -0
  307. data/lib/rubino/tools/ruby_tool.rb +0 -0
  308. data/lib/rubino/tools/session_search_tool.rb +103 -0
  309. data/lib/rubino/tools/shell_input_tool.rb +96 -0
  310. data/lib/rubino/tools/shell_kill_tool.rb +76 -0
  311. data/lib/rubino/tools/shell_output_tool.rb +72 -0
  312. data/lib/rubino/tools/shell_registry.rb +158 -0
  313. data/lib/rubino/tools/shell_tail_tool.rb +118 -0
  314. data/lib/rubino/tools/shell_tool.rb +330 -0
  315. data/lib/rubino/tools/steer_tool.rb +118 -0
  316. data/lib/rubino/tools/subagent_probe.rb +89 -0
  317. data/lib/rubino/tools/summarize_file_tool.rb +182 -0
  318. data/lib/rubino/tools/task_result_tool.rb +90 -0
  319. data/lib/rubino/tools/task_stop_tool.rb +80 -0
  320. data/lib/rubino/tools/task_tool.rb +622 -0
  321. data/lib/rubino/tools/test_tool.rb +454 -0
  322. data/lib/rubino/tools/todo_tool.rb +93 -0
  323. data/lib/rubino/tools/tool_call_repository.rb +33 -0
  324. data/lib/rubino/tools/vision_tool.rb +85 -0
  325. data/lib/rubino/tools/webfetch_tool.rb +153 -0
  326. data/lib/rubino/tools/websearch_tool.rb +179 -0
  327. data/lib/rubino/tools/write_tool.rb +61 -0
  328. data/lib/rubino/trust.rb +88 -0
  329. data/lib/rubino/ui/api.rb +296 -0
  330. data/lib/rubino/ui/base.rb +252 -0
  331. data/lib/rubino/ui/bottom_composer.rb +1599 -0
  332. data/lib/rubino/ui/cli.rb +1987 -0
  333. data/lib/rubino/ui/completion_menu.rb +321 -0
  334. data/lib/rubino/ui/completion_source.rb +284 -0
  335. data/lib/rubino/ui/escape_reader.rb +169 -0
  336. data/lib/rubino/ui/indented_io.rb +88 -0
  337. data/lib/rubino/ui/input_history.rb +108 -0
  338. data/lib/rubino/ui/live_region.rb +183 -0
  339. data/lib/rubino/ui/markdown_renderer.rb +506 -0
  340. data/lib/rubino/ui/notifier.rb +163 -0
  341. data/lib/rubino/ui/null.rb +195 -0
  342. data/lib/rubino/ui/paste_store.rb +176 -0
  343. data/lib/rubino/ui/printer_base.rb +79 -0
  344. data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
  345. data/lib/rubino/ui/queued_indicators.rb +66 -0
  346. data/lib/rubino/ui/status_bar.rb +100 -0
  347. data/lib/rubino/ui/stdout_proxy.rb +161 -0
  348. data/lib/rubino/ui/streaming_markdown.rb +186 -0
  349. data/lib/rubino/ui/subagent_cards.rb +134 -0
  350. data/lib/rubino/ui/subagent_view.rb +255 -0
  351. data/lib/rubino/ui.rb +21 -0
  352. data/lib/rubino/update_check.rb +187 -0
  353. data/lib/rubino/util/duration.rb +23 -0
  354. data/lib/rubino/util/hyperlink.rb +105 -0
  355. data/lib/rubino/util/output.rb +145 -0
  356. data/lib/rubino/util/secrets_mask.rb +83 -0
  357. data/lib/rubino/version.rb +5 -0
  358. data/lib/rubino/workspace.rb +85 -0
  359. data/lib/rubino-agent.rb +5 -0
  360. data/lib/rubino.rb +318 -0
  361. data/mise.toml +2 -0
  362. data/rubino-agent.gemspec +103 -0
  363. data/skills/ruby-expert/SKILL.md +67 -0
  364. data/skills/ruby-expert/references/concurrency.md +357 -0
  365. data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
  366. data/skills/ruby-expert/references/errors-and-types.md +460 -0
  367. data/skills/ruby-expert/references/gem-authoring.md +459 -0
  368. data/skills/ruby-expert/references/language-idioms.md +465 -0
  369. data/skills/ruby-expert/references/metaprogramming.md +339 -0
  370. data/skills/ruby-expert/references/oo-design.md +553 -0
  371. data/skills/ruby-expert/references/performance.md +383 -0
  372. data/skills/ruby-expert/references/rails.md +424 -0
  373. data/skills/ruby-expert/references/security.md +404 -0
  374. data/skills/ruby-expert/references/testing.md +473 -0
  375. data/skills/ruby-expert/references/tooling.md +466 -0
  376. metadata +856 -0
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+ require_relative "inline_think_filter"
7
+
8
+ module Rubino
9
+ module LLM
10
+ # Direct Bedrock runtime client using Bearer token authentication.
11
+ # Used when BEDROCK_API_KEY is set without BEDROCK_SECRET_KEY.
12
+ # Calls the Bedrock Converse API with Authorization: Bearer header.
13
+ # Supports tool calls via the native Bedrock Converse toolConfig format.
14
+ class BedrockBearerClient
15
+ BEDROCK_RUNTIME_HOST = "bedrock-runtime.%s.amazonaws.com"
16
+
17
+ def initialize(api_key:, region:, model_id:, show_reasoning: false, event_bus: nil)
18
+ @api_key = api_key
19
+ @region = region
20
+ @model_id = model_id
21
+ @host = BEDROCK_RUNTIME_HOST % region
22
+ @show_reasoning = show_reasoning
23
+ @event_bus = event_bus
24
+ end
25
+
26
+ # Sends a non-streaming chat request, returns AdapterResponse
27
+ def chat(messages:, tools: nil)
28
+ body = build_body(messages, tools: tools)
29
+ response = post("/model/#{URI.encode_uri_component(@model_id)}/converse", body)
30
+ parse_response(response)
31
+ end
32
+
33
+ # Sends a "streaming" chat request and returns an AdapterResponse, yielding
34
+ # chunk HASHES shaped exactly like every other adapter:
35
+ # { type: :content | :thinking, text: String, message_id: Integer }
36
+ #
37
+ # Real Bedrock ConverseStream (binary eventstream) is out of scope: bearer-
38
+ # token auth isn't supported by ruby_llm's SigV4 Bedrock provider, and this
39
+ # is a plain Net::HTTP transport. We buffer the non-streaming /converse
40
+ # response FULLY, then replay it through InlineThinkFilter in slices so the
41
+ # SHAPE matches the streaming contract (typed deltas, :thinking channel,
42
+ # a single content block id, an explicit MESSAGE_COMPLETED boundary).
43
+ # Only the token cadence is synthetic.
44
+ #
45
+ # INVARIANT: we buffer the entire response BEFORE the first emit. That is
46
+ # what makes retrying this call (now in Agent::ModelCallRunner) safe — a
47
+ # transport error can only fire during post() (before any chunk reached the
48
+ # UI), never mid-replay, so a retry can't double output.
49
+ def stream(messages:, tools: nil, &block)
50
+ body = build_body(messages, tools: tools)
51
+ data = post("/model/#{URI.encode_uri_component(@model_id)}/converse", body)
52
+
53
+ # Single buffered content block ⇒ message_id is always 0. Mirrors the
54
+ # 2-arg emit lambda RubyLLMAdapter feeds into InlineThinkFilter.feed/flush.
55
+ emit = lambda do |type, text|
56
+ return if text.nil? || text.empty?
57
+ return if type == :thinking && !@show_reasoning
58
+
59
+ block&.call({ type: type, text: text, message_id: 0 })
60
+ end
61
+
62
+ think_filter = InlineThinkFilter.new
63
+ extract_text(data).chars.each_slice(5) do |slice|
64
+ think_filter.feed(slice.join, &emit)
65
+ end
66
+ think_filter.flush(&emit)
67
+
68
+ @event_bus&.emit(Interaction::Events::MESSAGE_COMPLETED, message_id: 0)
69
+
70
+ parse_response(data)
71
+ end
72
+
73
+ private
74
+
75
+ def build_body(messages, tools: nil)
76
+ system_msgs = messages.select { |m| role_of(m) == "system" }
77
+ chat_msgs = messages.reject { |m| role_of(m) == "system" }
78
+
79
+ body = {
80
+ messages: chat_msgs.map { |m| format_message(m) }
81
+ }
82
+
83
+ body[:system] = system_msgs.map { |m| { text: content_of(m).to_s } } if system_msgs.any?
84
+
85
+ # Attach tool definitions when provided
86
+ if tools && !tools.empty?
87
+ body[:toolConfig] = {
88
+ tools: tools.map { |t| format_tool(t) }
89
+ }
90
+ end
91
+
92
+ body
93
+ end
94
+
95
+ # Format a message for the Bedrock Converse API.
96
+ # Handles plain text, assistant tool_use turns, and tool_result turns.
97
+ def format_message(msg)
98
+ role = role_of(msg)
99
+ content = content_of(msg)
100
+ tc = msg[:tool_calls] || msg["tool_calls"]
101
+
102
+ case role
103
+ when "assistant"
104
+ # Assistant message with tool use blocks
105
+ if tc && !tc.empty?
106
+ content_blocks = []
107
+ content_blocks << { text: content.to_s } if content && !content.to_s.empty?
108
+ tc.each do |call|
109
+ content_blocks << {
110
+ toolUse: {
111
+ toolUseId: call[:id] || call["id"],
112
+ name: call[:name] || call["name"],
113
+ input: call[:arguments] || call["arguments"] || {}
114
+ }
115
+ }
116
+ end
117
+ { role: "assistant", content: content_blocks }
118
+ else
119
+ { role: "assistant", content: [{ text: content.to_s }] }
120
+ end
121
+ when "tool"
122
+ # Tool result — Bedrock expects role: "user" with toolResult content block
123
+ {
124
+ role: "user",
125
+ content: [{
126
+ toolResult: {
127
+ toolUseId: msg[:tool_call_id] || msg["tool_call_id"] || "unknown",
128
+ content: [{ text: content.to_s }]
129
+ }
130
+ }]
131
+ }
132
+ else
133
+ { role: role, content: [{ text: content.to_s }] }
134
+ end
135
+ end
136
+
137
+ # Format a tool definition for Bedrock toolConfig.
138
+ # Accepts Rubino::Tools::Base instances or plain hashes with
139
+ # :name/:description/:parameters keys.
140
+ def format_tool(tool)
141
+ if tool.respond_to?(:name)
142
+ name = tool.name
143
+ description = tool.description
144
+ schema = tool.input_schema
145
+ else
146
+ name = tool[:name] || tool["name"]
147
+ description = tool[:description] || tool["description"]
148
+ schema = tool[:parameters] || tool["parameters"] || {}
149
+ end
150
+
151
+ {
152
+ toolSpec: {
153
+ name: name,
154
+ description: description,
155
+ inputSchema: { json: schema }
156
+ }
157
+ }
158
+ end
159
+
160
+ def post(path, body)
161
+ uri = URI("https://#{@host}#{path}")
162
+ http = Net::HTTP.new(uri.host, uri.port)
163
+ http.use_ssl = true
164
+ http.read_timeout = 120
165
+ http.open_timeout = 30
166
+
167
+ request = Net::HTTP::Post.new(uri.path)
168
+ request["Authorization"] = "Bearer #{@api_key}"
169
+ request["Content-Type"] = "application/json"
170
+ request["Accept"] = "application/json"
171
+ request.body = JSON.generate(body)
172
+
173
+ response = http.request(request)
174
+
175
+ unless response.code.to_i == 200
176
+ error_body = begin
177
+ JSON.parse(response.body)
178
+ rescue StandardError
179
+ { "message" => response.body }
180
+ end
181
+ raise Rubino::Error, "Bedrock error #{response.code}: #{error_body["message"] || error_body}"
182
+ end
183
+
184
+ JSON.parse(response.body)
185
+ end
186
+
187
+ def parse_response(data)
188
+ text = extract_text(data)
189
+ tool_calls = extract_tool_calls(data)
190
+
191
+ input_tokens = data.dig("usage", "inputTokens") || 0
192
+ output_tokens = data.dig("usage", "outputTokens") || 0
193
+
194
+ Rubino::LLM::AdapterResponse.new(
195
+ content: text,
196
+ tool_calls: tool_calls,
197
+ input_tokens: input_tokens,
198
+ output_tokens: output_tokens,
199
+ model_id: @model_id
200
+ )
201
+ end
202
+
203
+ def extract_text(data)
204
+ data.dig("output", "message", "content")
205
+ &.select { |c| c["text"] }
206
+ &.map { |c| c["text"] }
207
+ &.join("") || ""
208
+ end
209
+
210
+ # Extract tool use blocks from a Bedrock Converse response.
211
+ # Returns an array of { id:, name:, arguments: } hashes.
212
+ def extract_tool_calls(data)
213
+ content_blocks = data.dig("output", "message", "content") || []
214
+ content_blocks.filter_map do |block|
215
+ next unless block["toolUse"]
216
+
217
+ tu = block["toolUse"]
218
+ {
219
+ id: tu["toolUseId"],
220
+ name: tu["name"],
221
+ arguments: tu["input"] || {}
222
+ }
223
+ end
224
+ end
225
+
226
+ def role_of(msg)
227
+ (msg[:role] || msg["role"]).to_s
228
+ end
229
+
230
+ def content_of(msg)
231
+ msg[:content] || msg["content"]
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module LLM
5
+ # Image-path helpers shared by the vision tool, run executor, and the
6
+ # interaction lifecycle: extension matching, in-text image extraction,
7
+ # and the model-family vision heuristic.
8
+ class ContentBuilder
9
+ SUPPORTED_IMAGE_TYPES = %w[.png .jpg .jpeg .gif .webp .bmp].freeze
10
+
11
+ # True when the path has a recognised image extension. Centralised here
12
+ # so Executor and tools share one definition.
13
+ def self.image_file?(path)
14
+ return false if path.nil? || path.to_s.empty?
15
+
16
+ SUPPORTED_IMAGE_TYPES.include?(File.extname(path.to_s).downcase)
17
+ end
18
+
19
+ # Detects image references in text (file paths or URLs)
20
+ # Extracts them and returns [cleaned_text, image_list]
21
+ def self.extract_images(text)
22
+ images = []
23
+ cleaned = text.dup
24
+
25
+ # Match file paths to images: /path/to/image.png or ./image.jpg
26
+ cleaned.gsub!(%r{(?:^|\s)((?:/|\./|~/)[^\s]+\.(?:png|jpg|jpeg|gif|webp|bmp))}i) do
27
+ path = ::Regexp.last_match(1).strip
28
+ if File.exist?(File.expand_path(path))
29
+ images << { type: :file, path: File.expand_path(path) }
30
+ "" # Remove from text
31
+ else
32
+ ::Regexp.last_match(0)
33
+ end
34
+ end
35
+
36
+ # Match image URLs
37
+ cleaned.gsub!(%r{(https?://[^\s]+\.(?:png|jpg|jpeg|gif|webp|bmp)(?:\?[^\s]*)?)}i) do
38
+ url = ::Regexp.last_match(1)
39
+ images << { type: :url, url: url }
40
+ "" # Remove from text
41
+ end
42
+
43
+ [cleaned.strip, images]
44
+ end
45
+
46
+ # Returns true if the model_id matches a known vision-capable family.
47
+ # Heuristic only — Configuration#model_supports_vision? lets callers
48
+ # override per-tenant (e.g. behind a proxy where model_id is the literal
49
+ # "auto" and the real upstream is decided server-side).
50
+ def self.supports_vision?(model_id)
51
+ model_id.match?(/gpt-4|claude|gemini|minimax-m3|mimo-v|qwen.*-vl|llava/i)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module LLM
5
+ # Single source of truth for "does the configured model have a usable
6
+ # credential?" — shared by the chat boot preflight (fail-fast before the
7
+ # ~80s retry storm, #93), the onboarding wizard, and `doctor`.
8
+ #
9
+ # It answers two questions the same way the adapter resolves them at call
10
+ # time (ProviderResolver + RubyLLMAdapter#*_compatible_api_key!), so a
11
+ # preflight "no key" verdict matches what the model call would actually hit:
12
+ #
13
+ # * resolved_provider(config) — the concrete provider the model id /
14
+ # model.provider resolves to (interprets "auto").
15
+ # * usable?(config) — true when a key for that provider is
16
+ # resolvable from config (providers.<name>.api_key) or the native ENV
17
+ # var, false otherwise.
18
+ module CredentialCheck
19
+ module_function
20
+
21
+ # The concrete provider the configured model will be routed to. Mirrors
22
+ # DoctorCommand#resolved_provider and the adapter's resolution: an explicit
23
+ # model.provider (not "auto") wins; otherwise derive from the model id.
24
+ def resolved_provider(config = Rubino.configuration)
25
+ configured = config.model_provider
26
+ return configured if configured && configured != "auto"
27
+
28
+ ProviderResolver.resolve(config.model_default.to_s)
29
+ end
30
+
31
+ # True when a credential for the resolved provider is available. The "fake"
32
+ # provider needs no upstream key. Honours providers.<name>.api_key first
33
+ # (custom / openai-compatible / anthropic-compatible gateways), then the
34
+ # provider's native ENV var — the same order RubyLLMAdapter uses.
35
+ def usable?(config = Rubino.configuration)
36
+ provider = resolved_provider(config)
37
+ return true if provider == "fake"
38
+
39
+ prov_cfg = config.provider_config(provider)
40
+ return true if present?(prov_cfg["api_key"])
41
+ return present?(ENV.fetch("OPENAI_API_KEY", nil)) if prov_cfg["openai_compatible"] == true
42
+ return present?(ENV.fetch("ANTHROPIC_API_KEY", nil)) if prov_cfg["anthropic_compatible"] == true
43
+
44
+ present?(provider_env_key(provider))
45
+ end
46
+
47
+ # The native ENV credential a provider reads when no config key is set.
48
+ def provider_env_key(provider)
49
+ case provider
50
+ when "openai" then ENV.fetch("OPENAI_API_KEY", nil)
51
+ when "anthropic" then ENV.fetch("ANTHROPIC_API_KEY", nil)
52
+ when "google" then ENV["GEMINI_API_KEY"] || ENV.fetch("GOOGLE_API_KEY", nil)
53
+ when "bedrock" then ENV.fetch("BEDROCK_API_KEY", nil)
54
+ when "minimax" then ENV.fetch("MINIMAX_API_KEY", nil)
55
+ else
56
+ # Unknown / self-hosted provider: no native ENV mapping. Fall back to
57
+ # the OpenAI key, which most openai-compatible backends accept.
58
+ ENV.fetch("OPENAI_API_KEY", nil)
59
+ end
60
+ end
61
+
62
+ # The ENV var NAME we'd suggest the user set for a given provider — used by
63
+ # the actionable error message and the wizard.
64
+ def provider_env_var_name(provider)
65
+ {
66
+ "openai" => "OPENAI_API_KEY",
67
+ "anthropic" => "ANTHROPIC_API_KEY",
68
+ "google" => "GEMINI_API_KEY",
69
+ "bedrock" => "BEDROCK_API_KEY",
70
+ "minimax" => "MINIMAX_API_KEY"
71
+ }.fetch(provider, "#{provider.to_s.upcase}_API_KEY")
72
+ end
73
+
74
+ # A clear, actionable message for an unconfigured provider/model — the
75
+ # text surfaced on the fail-fast path and in non-interactive contexts.
76
+ def missing_key_message(config = Rubino.configuration)
77
+ provider = resolved_provider(config)
78
+ env_var = provider_env_var_name(provider)
79
+ <<~MSG.strip
80
+ No API key configured for provider '#{provider}' (model #{config.model_default}).
81
+ Set it up one of these ways:
82
+ • run `rubino setup` for a guided first-run setup, or
83
+ • add #{env_var}=<your-key> to #{Config::Loader.new.env_path}, or
84
+ • set providers.#{provider}.api_key in #{Config::Loader.new.config_path}.
85
+ MSG
86
+ end
87
+
88
+ def present?(value)
89
+ !value.nil? && !value.to_s.empty?
90
+ end
91
+ end
92
+ end
93
+ end