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,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module LLM
5
+ # Streaming filter that splits text into :content and :thinking events by
6
+ # recognising inline <think>...</think> sentinels emitted by MiniMax,
7
+ # DeepSeek-R1, Qwen, and similar reasoning models that don't expose a
8
+ # dedicated reasoning channel.
9
+ #
10
+ # Holds back up to TAG_MAX_LEN-1 chars across chunks so a tag split between
11
+ # chunks (e.g. "<thi" + "nk>") still gets matched. Call #flush at end of
12
+ # stream to drain any tail.
13
+ class InlineThinkFilter
14
+ OPEN_RE = /<think>/i
15
+ CLOSE_RE = %r{</think>}i
16
+ TAG_MAX_LEN = "</think>".length
17
+
18
+ def initialize
19
+ @inside = false
20
+ @pending = +""
21
+ end
22
+
23
+ def feed(chunk)
24
+ @pending << chunk
25
+ loop do
26
+ re, sentinel = @inside ? [CLOSE_RE, :thinking] : [OPEN_RE, :content]
27
+ match = @pending.match(re)
28
+
29
+ if match
30
+ idx = match.begin(0)
31
+ tag_len = match[0].length
32
+ emit = @pending.slice!(0, idx)
33
+ @pending.slice!(0, tag_len)
34
+ yield sentinel, emit unless emit.empty?
35
+ @inside = !@inside
36
+ else
37
+ # Hold back last (TAG_MAX_LEN-1) chars in case the next chunk
38
+ # completes a tag that began at the tail of @pending.
39
+ safe_len = @pending.length - (TAG_MAX_LEN - 1)
40
+ if safe_len.positive?
41
+ emit = @pending.slice!(0, safe_len)
42
+ yield sentinel, emit unless emit.empty?
43
+ end
44
+ break
45
+ end
46
+ end
47
+ end
48
+
49
+ def flush
50
+ return if @pending.empty?
51
+
52
+ sentinel = @inside ? :thinking : :content
53
+ yield sentinel, @pending
54
+ @pending = +""
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module LLM
5
+ # Enumerates the model ids the ruby_llm registry knows for a provider —
6
+ # the source behind `/model` (bare listing + dropdown completion). Custom
7
+ # backends (minimax/gateway/anthropic-compatible proxies) are not registry
8
+ # providers, so they enumerate to [] and `/model` degrades to the
9
+ # current-model + usage-hint view; no hardcoded global list is invented.
10
+ module ModelCatalog
11
+ # ProviderResolver speaks "google"; the ruby_llm registry files the same
12
+ # models under "gemini". Only mismatch between the two vocabularies.
13
+ REGISTRY_ALIASES = { "google" => "gemini" }.freeze
14
+
15
+ module_function
16
+
17
+ # Model ids for +provider+, [] when the registry can't enumerate it.
18
+ def ids_for(provider)
19
+ return [] if provider.to_s.empty?
20
+
21
+ require "ruby_llm"
22
+ registry_name = REGISTRY_ALIASES.fetch(provider.to_s, provider.to_s)
23
+ RubyLLM.models.by_provider(registry_name).map(&:id)
24
+ rescue StandardError
25
+ []
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module LLM
5
+ # Resolves provider from model name or explicit configuration.
6
+ #
7
+ # Single place that interprets "auto": an explicit (non-"auto") provider is
8
+ # returned verbatim; nil/"auto" first honours the Bedrock-bearer override
9
+ # (Mantle short-term key → anthropic provider) and then falls back to
10
+ # pattern-matching the model id. AdapterFactory resolves once here and hands
11
+ # the concrete provider to RubyLLMAdapter, which no longer re-resolves.
12
+ class ProviderResolver
13
+ PROVIDER_PATTERNS = {
14
+ "fake" => /\Afake/i,
15
+ "openai" => /\A(openai|gpt|o1|o3|o4)/i,
16
+ "anthropic" => /\A(anthropic(?!\.)|claude)/i,
17
+ "google" => /\A(google|gemini)/i,
18
+ "bedrock" => /\A(anthropic\.|amazon\.|meta\.|mistral\.|cohere\.|ai21\.)/i,
19
+ "deepseek" => /\Adeepseek/i,
20
+ "mistral" => /\A(mistral|mixtral)/i,
21
+ "minimax" => /\A(minimax|abab)/i,
22
+ "qwen" => /\Aqwen/i
23
+ }.freeze
24
+
25
+ def self.resolve(model_id, explicit_provider: nil)
26
+ return explicit_provider if explicit_provider && explicit_provider != "auto"
27
+
28
+ # Bedrock bearer-token mode (Mantle short-term key: API key set, no
29
+ # secret) always routes through the Anthropic provider regardless of
30
+ # model id. Part of the "auto" interpretation, kept here so it lives in
31
+ # exactly one place (was duplicated in RubyLLMAdapter#resolve_provider).
32
+ return "anthropic" if bedrock_bearer_env?
33
+
34
+ PROVIDER_PATTERNS.each do |provider, pattern|
35
+ return provider if model_id.to_s.match?(pattern)
36
+ end
37
+
38
+ "openai" # Default fallback
39
+ end
40
+
41
+ # True when the environment carries a Bedrock bearer token (API key set,
42
+ # no secret key) — the Mantle short-term credential mode.
43
+ def self.bedrock_bearer_env?
44
+ !ENV["BEDROCK_API_KEY"].to_s.empty? && ENV["BEDROCK_SECRET_KEY"].to_s.empty?
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module LLM
5
+ # Renders the reasoning/thinking configuration to the Anthropic-compat wire
6
+ # params (manual mode). This is the Ruby port of the reference reasoning_config →
7
+ # `thinking` mapping: on the manual path
8
+ # (MiniMax /anthropic, older Anthropic, bedrock) thinking is enabled with a
9
+ # token budget, which FORCES temperature=1 and bumps max_tokens so the budget
10
+ # fits under it with text headroom to still answer.
11
+ #
12
+ # The numbers (budget 8000 "medium", text headroom 4096, 16384 ceiling) are
13
+ # sourced from config (model.thinking_budget / model.max_tokens_text_headroom
14
+ # / model.max_tokens) by the adapter and passed in — this object holds no
15
+ # magic numbers of its own; it only mirrors the reference combination rules.
16
+ #
17
+ # One source of truth: the adapter calls #render exactly once per chat build
18
+ # to derive the params, and applies them; the inline Slice 0(c) logic that
19
+ # used to live in RubyLLMAdapter#apply_generation_params now lives here.
20
+ class ReasoningManager
21
+ # The rendered wire params. +thinking+ is the Anthropic manual-mode block
22
+ # (nil when disabled), +temperature+ is forced to 1 with thinking on (else
23
+ # the configured value, possibly nil ⇒ provider default), +max_tokens+ is
24
+ # the ceiling grown to fit budget + headroom (nil ⇒ leave provider default).
25
+ Rendered = Struct.new(:thinking, :temperature, :max_tokens, keyword_init: true) do
26
+ def thinking_enabled?
27
+ !thinking.nil?
28
+ end
29
+ end
30
+
31
+ # Render the reasoning config to wire params.
32
+ #
33
+ # budget : Integer — thinking token budget; 0/nil disables thinking
34
+ # temperature : Float|nil — configured sampling temperature (ignored when
35
+ # thinking is enabled — Anthropic requires 1 then)
36
+ # max_tokens : Integer|nil — configured output ceiling; nil ⇒ leave the
37
+ # provider default UNLESS thinking forces a floor
38
+ # text_headroom : Integer — visible-output tokens reserved on top of budget
39
+ # apply_max_tokens: Bool — only the anthropic-family path raises the ceiling;
40
+ # openai/ollama/etc. leave token limits to the provider
41
+ #
42
+ # Mirrors anthropic_adapter.py:2238–2241:
43
+ # kwargs["thinking"] = {type: enabled, budget_tokens: budget}
44
+ # kwargs["temperature"] = 1
45
+ # kwargs["max_tokens"] = max(effective_max_tokens, budget + headroom)
46
+ def render(budget:, temperature: nil, max_tokens: nil,
47
+ text_headroom: 4096, apply_max_tokens: true)
48
+ budget = budget.to_i
49
+ enabled = budget.positive?
50
+
51
+ Rendered.new(
52
+ thinking: enabled ? { type: :enabled, budget_tokens: budget } : nil,
53
+ temperature: render_temperature(enabled, temperature),
54
+ max_tokens: apply_max_tokens ? render_max_tokens(enabled, budget, max_tokens, text_headroom) : nil
55
+ )
56
+ end
57
+
58
+ # Echo-back seam (reference reapply_reasoning_echo_for_provider): in the
59
+ # reference, prior-turn assistant
60
+ # reasoning_content is re-padded onto the api copy for providers that
61
+ # REQUIRE it back (DeepSeek/Kimi/MiMo thinking mode) or it 400s.
62
+ #
63
+ # DOCUMENTED NO-OP SEAM (per the boundary spike): the installed
64
+ # ruby_llm 1.15 RubyLLM::Message exposes +thinking+ as a read-only attr
65
+ # (no setter, only an initializer keyword) and our anthropic-compat target
66
+ # (MiniMax /anthropic) does NOT require reasoning echo-back — only the
67
+ # OpenAI-compat require-side providers do, which we do not yet target.
68
+ # So there is no transport to carry reasoning back through cleanly today;
69
+ # fabricating one would be wrong. This stays a no-op until a require-side
70
+ # provider lands (Slice 7 fallback may surface one) and ruby_llm offers a
71
+ # reachable reasoning field on replayed messages.
72
+ #
73
+ # Returns +history+ unchanged.
74
+ def carry(history)
75
+ # TODO(slice-7+): when a require-side provider (DeepSeek/Kimi/MiMo) is
76
+ # supported and ruby_llm exposes a settable reasoning field on replayed
77
+ # assistant messages, fold prior-turn reasoning back here.
78
+ history
79
+ end
80
+
81
+ private
82
+
83
+ def render_temperature(enabled, temperature)
84
+ return 1 if enabled
85
+
86
+ temperature
87
+ end
88
+
89
+ def render_max_tokens(enabled, budget, max_tokens, text_headroom)
90
+ ceiling = max_tokens
91
+ floor = budget + text_headroom.to_i
92
+ ceiling = [ceiling.to_i, floor].max if enabled && ceiling
93
+ ceiling = floor if enabled && ceiling.nil?
94
+ return nil unless ceiling&.positive?
95
+
96
+ ceiling
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module LLM
5
+ # The single value object the conversation loop hands the LLM boundary on
6
+ # each model call. Pure data — it carries everything a provider needs to
7
+ # issue one request, so the loop never threads positional args through the
8
+ # adapter. Mirrors the reference per-call request shape feeding the
9
+ # normalize_response seam: build a request,
10
+ # call the boundary, read back a normalized response.
11
+ #
12
+ # Fields:
13
+ # messages : [{role:, content:, tool_calls?, tool_call_id?}] — api copy
14
+ # tools : [tool schema] — may be [] (e.g. max-iter toolless summary)
15
+ # temperature : Float | nil — nil ⇒ provider default; forced to 1 w/ thinking
16
+ # max_tokens : Integer | nil — bumped on thinking + truncation continuation
17
+ # thinking : {enabled:, effort:|budget:} | nil — rendered to wire later
18
+ # prefill : String | nil — assistant-turn seed for prefill-to-continue
19
+ # image_paths : [path] — native attachments, first call of a turn only
20
+ # stream : Bool — loop decides (interactive turn ⇒ false)
21
+ class Request
22
+ attr_reader :messages, :tools, :temperature, :max_tokens, :thinking,
23
+ :prefill, :image_paths, :stream
24
+
25
+ def initialize(messages:, tools: nil, temperature: nil, max_tokens: nil,
26
+ thinking: nil, prefill: nil, image_paths: nil, stream: false)
27
+ @messages = messages || []
28
+ @tools = tools || []
29
+ @temperature = temperature
30
+ @max_tokens = max_tokens
31
+ @thinking = thinking
32
+ @prefill = prefill
33
+ @image_paths = image_paths || []
34
+ @stream = stream ? true : false
35
+ end
36
+
37
+ # True when the loop asked the boundary to stream this call.
38
+ def stream?
39
+ @stream
40
+ end
41
+
42
+ def to_h
43
+ {
44
+ messages: @messages,
45
+ tools: @tools,
46
+ temperature: @temperature,
47
+ max_tokens: @max_tokens,
48
+ thinking: @thinking,
49
+ prefill: @prefill,
50
+ image_paths: @image_paths,
51
+ stream: @stream
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end