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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Rubino
6
+ module LLM
7
+ # Loads a fake-provider scenario YAML by name and returns the parsed
8
+ # event list. The fake provider drives a deterministic chunk/tool-call
9
+ # stream from these events so the UI and tool plumbing can be exercised
10
+ # without burning provider tokens.
11
+ #
12
+ # Search order:
13
+ # 1. <Rubino.configuration.fake_scenarios_dir>/<name>.yml
14
+ # 2. <gem_root>/lib/rubino/llm/scenarios/<name>.yml
15
+ #
16
+ # The YAML file must contain a top-level `events:` key with an array
17
+ # of event hashes — see scenarios/happy-path.yml for the shape.
18
+ class ScenarioLoader
19
+ class NotFound < Rubino::Error; end
20
+
21
+ DEFAULT_DIR = File.expand_path("scenarios", __dir__)
22
+
23
+ def self.load(name, scenarios_dir: nil)
24
+ new(scenarios_dir: scenarios_dir).load(name)
25
+ end
26
+
27
+ def initialize(scenarios_dir: nil)
28
+ @scenarios_dir = scenarios_dir || configured_dir
29
+ end
30
+
31
+ # Returns the array of event hashes under the YAML `events:` key.
32
+ # Raises NotFound when the scenario can't be located under either
33
+ # path, citing both so the operator can fix the misconfiguration.
34
+ def load(name)
35
+ path = resolve_path(name)
36
+ raise NotFound, build_not_found_message(name) unless path
37
+
38
+ data = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true) || {}
39
+ Array(data["events"] || data[:events])
40
+ end
41
+
42
+ private
43
+
44
+ def resolve_path(name)
45
+ filename = "#{name}.yml"
46
+ [@scenarios_dir, DEFAULT_DIR].compact.uniq.each do |dir|
47
+ candidate = File.join(dir, filename)
48
+ return candidate if File.file?(candidate)
49
+ end
50
+ nil
51
+ end
52
+
53
+ def configured_dir
54
+ cfg = Rubino.configuration
55
+ return nil unless cfg.respond_to?(:dig)
56
+
57
+ cfg.dig("fake_provider", "scenarios_dir") || cfg.dig("providers", "fake", "scenarios_dir")
58
+ rescue StandardError
59
+ nil
60
+ end
61
+
62
+ def build_not_found_message(name)
63
+ tried = [@scenarios_dir, DEFAULT_DIR].compact.uniq.map { |d| File.join(d, "#{name}.yml") }
64
+ "fake scenario '#{name}' not found. Tried:\n #{tried.join("\n ")}"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module LLM
5
+ # Maps a user-supplied prompt to a fake scenario name using a keyword
6
+ # router ported verbatim from the reference fake-provider SCENARIO_ROUTER.
7
+ # The ordering of the rules is
8
+ # significant — see the notes block in fake-provider-spec.md before
9
+ # touching anything here.
10
+ class ScenarioSelector
11
+ ROUTER = [
12
+ { keywords: ["simula quota exceeded", "quota exceeded", "provider quota"],
13
+ scenario: "provider-quota-completed" },
14
+
15
+ { keywords: ["cron fail", "cron failure", "cron error", "broken cron",
16
+ "failing cron", "fallisce cron", "cron fallisce"],
17
+ scenario: "agent-creates-cron-failure" },
18
+
19
+ { keywords: ["crea cron", "create cron", "create a cron", "cron job",
20
+ "scheduled task", "schedula", "pianifica",
21
+ "schedule daily", "schedule every", "schedule a daily",
22
+ "schedule a weekly", "daily cron", "weekly cron",
23
+ "every minute cron", "cron giornaliero", "report giornaliero",
24
+ "with-cron-success"],
25
+ scenario: "agent-creates-cron" },
26
+
27
+ { keywords: %w[approve approval autorizza conferma permit allow],
28
+ scenario: "with-approvals" },
29
+
30
+ { keywords: ["artifact", "generate report", "create file", "crea file",
31
+ "genera report", "write file", "save as"],
32
+ scenario: "with-artifacts" },
33
+
34
+ { keywords: ["upload", "allegato", "allega", "attached file",
35
+ "read this file", "analizza file"],
36
+ scenario: "with-uploads" },
37
+
38
+ { keywords: %w[fail error errore fallito broken crash crash],
39
+ scenario: "failure" },
40
+
41
+ { keywords: ["complex-analysis", "report", "analysis report", "comprehensive",
42
+ "full refactor", "refactoring completo", "multi-tool",
43
+ "multi step analysis"],
44
+ scenario: "complex-analysis" },
45
+
46
+ { keywords: ["multi-step", "analizza", "analyze", "refactor", "debug",
47
+ "investigate", "ricerca", "search", "trova",
48
+ "how would you", "what do you think"],
49
+ scenario: "with-reasoning" },
50
+
51
+ { keywords: ["analisi", "analysis", "spiegami", "explain in detail",
52
+ "descrivi l'architettura", "tell me about",
53
+ "how does it work", "full breakdown"],
54
+ scenario: "analysis" },
55
+
56
+ { keywords: ["which approach", "which option", "what do you think",
57
+ "choose between", "help me decide", "clarify",
58
+ "non so quale", "quale scegliere", "aiutami a scegliere"],
59
+ scenario: "with-clarify" }
60
+ ].freeze
61
+
62
+ # Resolves the input text to a scenario name. First keyword match wins;
63
+ # `default:` is returned when nothing matches. Matching is case-insensitive
64
+ # on both the input and the keyword list. Blank/nil input → default.
65
+ def self.resolve(input_text, default: "happy-path")
66
+ return default if input_text.nil?
67
+
68
+ text = input_text.to_s.downcase
69
+ return default if text.strip.empty?
70
+
71
+ ROUTER.each do |rule|
72
+ rule[:keywords].each do |kw|
73
+ return rule[:scenario] if text.include?(kw.downcase)
74
+ end
75
+ end
76
+ default
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,29 @@
1
+ # Agent-creates-cron-failure scenario: the LLM registers a cron job that
2
+ # is wired to the with-cron-failure scenario, so "Run now" exercises the
3
+ # failure path in the real pipeline (run.failed, no delivery).
4
+ #
5
+ # Ported from the reference agent-creates-cron-failure scenario. The fake
6
+ # provider only produces what the LLM would say or call; the cron registration,
7
+ # token expansion, tracing and run accounting all happen downstream once the
8
+ # real `cronjob` tool runs.
9
+ events:
10
+ - type: thinking
11
+ text: "User wants a cron that will fail when triggered — useful to test the failure UI."
12
+ - type: delay_seconds
13
+ value: 0.05
14
+
15
+ - type: content
16
+ text: "Scheduled **Flaky Job** (every 1m) — Run now will exercise the failure path."
17
+ - type: delay_seconds
18
+ value: 0.05
19
+
20
+ - type: tool_call
21
+ id: "call_fake_cron_failure_1"
22
+ name: "cronjob"
23
+ arguments:
24
+ action: "create"
25
+ name: "Flaky Job"
26
+ schedule: "every 1m"
27
+ prompt: "Run a flaky job that fails deterministically."
28
+ deliver: "web"
29
+ scenario: "with-cron-failure"
@@ -0,0 +1,36 @@
1
+ # Agent-creates-cron scenario: the LLM emits a single tool_call to the real
2
+ # `cronjob` tool (action="create"), exactly as the reference does in its
3
+ # cronjob tooling. Everything downstream (approval,
4
+ # scheduler registration, artifacts, trace token) runs on the real pipeline
5
+ # once the tool_call is consumed.
6
+ #
7
+ # Translated from the reference agent-creates-cron scenario — see the
8
+ # translation rules in the fake-provider scenario spec.
9
+ events:
10
+ - type: thinking
11
+ text: "User wants a recurring task."
12
+ - type: delay_seconds
13
+ value: 0.05
14
+ - type: thinking
15
+ text: " I'll create a cron job with a 1-minute interval so it's easy to see in dev."
16
+ - type: delay_seconds
17
+ value: 0.05
18
+
19
+ - type: content
20
+ text: "Scheduling **Daily Summary** (every 1m). "
21
+ - type: delay_seconds
22
+ value: 0.05
23
+ - type: content
24
+ text: "Look for the trace token in the Cron Jobs index entry and in every session title produced by Run now."
25
+ - type: delay_seconds
26
+ value: 0.05
27
+
28
+ - type: tool_call
29
+ id: "call_fake_cronjob_1"
30
+ name: "cronjob"
31
+ arguments:
32
+ action: "create"
33
+ name: "Daily Summary"
34
+ schedule: "every 1m"
35
+ prompt: "Summarize yesterday's activity and produce a 3-bullet recap."
36
+ deliver: "web"
@@ -0,0 +1,501 @@
1
+ # Analysis scenario: model streams long reasoning token-by-token,
2
+ # reads a file via the real `read` tool, then continues reasoning.
3
+ # Mirrors the reference analysis scenario in the FakeProvider vocabulary.
4
+ # The fake provider terminates when events run out, so no completion event
5
+ # is needed.
6
+ events:
7
+ - type: thinking
8
+ text: "Let"
9
+ - type: thinking
10
+ text: " me"
11
+ - type: thinking
12
+ text: " analyze"
13
+ - type: thinking
14
+ text: " the"
15
+ - type: thinking
16
+ text: " full"
17
+ - type: thinking
18
+ text: " architecture"
19
+ - type: thinking
20
+ text: " step"
21
+ - type: thinking
22
+ text: " by"
23
+ - type: thinking
24
+ text: " step."
25
+ - type: thinking
26
+ text: "\n"
27
+ - type: thinking
28
+ text: "First,"
29
+ - type: thinking
30
+ text: " let"
31
+ - type: thinking
32
+ text: " me"
33
+ - type: thinking
34
+ text: " understand"
35
+ - type: thinking
36
+ text: " what"
37
+ - type: thinking
38
+ text: " we"
39
+ - type: thinking
40
+ text: " are"
41
+ - type: thinking
42
+ text: " looking"
43
+ - type: thinking
44
+ text: " at."
45
+ - type: thinking
46
+ text: "\n"
47
+ - type: thinking
48
+ text: "The"
49
+ - type: thinking
50
+ text: " system"
51
+ - type: thinking
52
+ text: " has"
53
+ - type: thinking
54
+ text: " multiple"
55
+ - type: thinking
56
+ text: " layers"
57
+ - type: thinking
58
+ text: " that"
59
+ - type: thinking
60
+ text: " interact"
61
+ - type: thinking
62
+ text: " in"
63
+ - type: thinking
64
+ text: " specific"
65
+ - type: thinking
66
+ text: " ways."
67
+
68
+ - type: delay_seconds
69
+ value: 0.8
70
+
71
+ - type: thinking
72
+ text: "\n\n"
73
+ - type: thinking
74
+ text: "Layer"
75
+ - type: thinking
76
+ text: " one:"
77
+ - type: thinking
78
+ text: " the"
79
+ - type: thinking
80
+ text: " Acme"
81
+ - type: thinking
82
+ text: " API"
83
+ - type: thinking
84
+ text: " server."
85
+ - type: thinking
86
+ text: "\nIt"
87
+ - type: thinking
88
+ text: " exposes"
89
+ - type: thinking
90
+ text: " endpoints"
91
+ - type: thinking
92
+ text: " for"
93
+ - type: thinking
94
+ text: " creating"
95
+ - type: thinking
96
+ text: " runs,"
97
+ - type: thinking
98
+ text: " streaming"
99
+ - type: thinking
100
+ text: " events,"
101
+ - type: thinking
102
+ text: " and"
103
+ - type: thinking
104
+ text: " handling"
105
+ - type: thinking
106
+ text: " approvals."
107
+ - type: thinking
108
+ text: "\n"
109
+ - type: thinking
110
+ text: "It"
111
+ - type: thinking
112
+ text: " uses"
113
+ - type: thinking
114
+ text: " aiohttp"
115
+ - type: thinking
116
+ text: " for"
117
+ - type: thinking
118
+ text: " async"
119
+ - type: thinking
120
+ text: " HTTP"
121
+ - type: thinking
122
+ text: " and"
123
+ - type: thinking
124
+ text: " SSE"
125
+ - type: thinking
126
+ text: " for"
127
+ - type: thinking
128
+ text: " real-time"
129
+ - type: thinking
130
+ text: " communication."
131
+ - type: thinking
132
+ text: "\n"
133
+ - type: thinking
134
+ text: "The"
135
+ - type: thinking
136
+ text: " key"
137
+ - type: thinking
138
+ text: " endpoints"
139
+ - type: thinking
140
+ text: " are:"
141
+ - type: thinking
142
+ text: " POST"
143
+ - type: thinking
144
+ text: " /v1/runs,"
145
+ - type: thinking
146
+ text: " GET"
147
+ - type: thinking
148
+ text: " /v1/runs/{id}"
149
+ - type: thinking
150
+ text: "/events,"
151
+ - type: thinking
152
+ text: " POST"
153
+ - type: thinking
154
+ text: " /v1/runs/{id}"
155
+ - type: thinking
156
+ text: "/stop,"
157
+ - type: thinking
158
+ text: " and"
159
+ - type: thinking
160
+ text: " POST"
161
+ - type: thinking
162
+ text: " /v1/runs/{id}"
163
+ - type: thinking
164
+ text: "/approvals"
165
+ - type: thinking
166
+ text: "/{aid}."
167
+
168
+ - type: delay_seconds
169
+ value: 1.0
170
+
171
+ - type: thinking
172
+ text: "\n\n"
173
+ - type: thinking
174
+ text: "Layer"
175
+ - type: thinking
176
+ text: " two:"
177
+ - type: thinking
178
+ text: " the"
179
+ - type: thinking
180
+ text: " Rails"
181
+ - type: thinking
182
+ text: " UI."
183
+ - type: thinking
184
+ text: "\n"
185
+ - type: thinking
186
+ text: "It"
187
+ - type: thinking
188
+ text: " connects"
189
+ - type: thinking
190
+ text: " via"
191
+ - type: thinking
192
+ text: " AcmeApiService"
193
+ - type: thinking
194
+ text: " which"
195
+ - type: thinking
196
+ text: " wraps"
197
+ - type: thinking
198
+ text: " all"
199
+ - type: thinking
200
+ text: " HTTP"
201
+ - type: thinking
202
+ text: " calls"
203
+ - type: thinking
204
+ text: " to"
205
+ - type: thinking
206
+ text: " the"
207
+ - type: thinking
208
+ text: " API"
209
+ - type: thinking
210
+ text: " server."
211
+ - type: thinking
212
+ text: "\n"
213
+ - type: thinking
214
+ text: "AcmeRunJob"
215
+ - type: thinking
216
+ text: " processes"
217
+ - type: thinking
218
+ text: " the"
219
+ - type: thinking
220
+ text: " SSE"
221
+ - type: thinking
222
+ text: " stream"
223
+ - type: thinking
224
+ text: " and"
225
+ - type: thinking
226
+ text: " persists"
227
+ - type: thinking
228
+ text: " events"
229
+ - type: thinking
230
+ text: " as"
231
+ - type: thinking
232
+ text: " RunEvent"
233
+ - type: thinking
234
+ text: " records."
235
+ - type: thinking
236
+ text: "\n"
237
+ - type: thinking
238
+ text: "The"
239
+ - type: thinking
240
+ text: " timeline"
241
+ - type: thinking
242
+ text: " rendering"
243
+ - type: thinking
244
+ text: " reads"
245
+ - type: thinking
246
+ text: " from"
247
+ - type: thinking
248
+ text: " RunEvent"
249
+ - type: thinking
250
+ text: " via"
251
+ - type: thinking
252
+ text: " the"
253
+ - type: thinking
254
+ text: " build_items"
255
+ - type: thinking
256
+ text: " method."
257
+
258
+ - type: delay_seconds
259
+ value: 1.2
260
+
261
+ - type: tool_call
262
+ name: read
263
+ arguments:
264
+ file_path: "/app/app/jobs/acme_run_job.rb"
265
+
266
+ - type: delay_seconds
267
+ value: 1.0
268
+
269
+ - type: thinking
270
+ text: "\n\n"
271
+
272
+ - type: thinking
273
+ text: "Looking"
274
+ - type: thinking
275
+ text: " at"
276
+ - type: thinking
277
+ text: " the"
278
+ - type: thinking
279
+ text: " job"
280
+ - type: thinking
281
+ text: " code,"
282
+ - type: thinking
283
+ text: " I"
284
+ - type: thinking
285
+ text: " can"
286
+ - type: thinking
287
+ text: " see"
288
+ - type: thinking
289
+ text: " the"
290
+ - type: thinking
291
+ text: " refactoring"
292
+ - type: thinking
293
+ text: " removed"
294
+ - type: thinking
295
+ text: " content_buffer"
296
+ - type: thinking
297
+ text: " and"
298
+ - type: thinking
299
+ text: " reasoning_buffer."
300
+ - type: thinking
301
+ text: "\n"
302
+ - type: thinking
303
+ text: "These"
304
+ - type: thinking
305
+ text: " were"
306
+ - type: thinking
307
+ text: " accumulating"
308
+ - type: thinking
309
+ text: " text"
310
+ - type: thinking
311
+ text: " in"
312
+ - type: thinking
313
+ text: " memory"
314
+ - type: thinking
315
+ text: " and"
316
+ - type: thinking
317
+ text: " flushing"
318
+ - type: thinking
319
+ text: " to"
320
+ - type: thinking
321
+ text: " the"
322
+ - type: thinking
323
+ text: " database."
324
+ - type: thinking
325
+ text: "\n"
326
+ - type: thinking
327
+ text: "Now"
328
+ - type: thinking
329
+ text: " RunEvent"
330
+ - type: thinking
331
+ text: " is"
332
+ - type: thinking
333
+ text: " the"
334
+ - type: thinking
335
+ text: " single"
336
+ - type: thinking
337
+ text: " source"
338
+ - type: thinking
339
+ text: " of"
340
+ - type: thinking
341
+ text: " truth"
342
+ - type: thinking
343
+ text: " for"
344
+ - type: thinking
345
+ text: " both"
346
+ - type: thinking
347
+ text: " content"
348
+ - type: thinking
349
+ text: " and"
350
+ - type: thinking
351
+ text: " reasoning."
352
+ - type: thinking
353
+ text: "\n"
354
+ - type: thinking
355
+ text: "The"
356
+ - type: thinking
357
+ text: " flush_now"
358
+ - type: thinking
359
+ text: " method"
360
+ - type: thinking
361
+ text: " now"
362
+ - type: thinking
363
+ text: " only"
364
+ - type: thinking
365
+ text: " handles"
366
+ - type: thinking
367
+ text: " tool_calls_cache"
368
+ - type: thinking
369
+ text: " for"
370
+ - type: thinking
371
+ text: " real-time"
372
+ - type: thinking
373
+ text: " tool"
374
+ - type: thinking
375
+ text: " status"
376
+ - type: thinking
377
+ text: " updates."
378
+ - type: thinking
379
+ text: "\n"
380
+ - type: thinking
381
+ text: "Everything"
382
+ - type: thinking
383
+ text: " else"
384
+ - type: thinking
385
+ text: " is"
386
+ - type: thinking
387
+ text: " event-driven."
388
+
389
+ - type: delay_seconds
390
+ value: 0.8
391
+
392
+ - type: thinking
393
+ text: "\n\n"
394
+ - type: thinking
395
+ text: "This"
396
+ - type: thinking
397
+ text: " changes"
398
+ - type: thinking
399
+ text: " the"
400
+ - type: thinking
401
+ text: " data"
402
+ - type: thinking
403
+ text: " flow"
404
+ - type: thinking
405
+ text: " significantly."
406
+ - type: thinking
407
+ text: "\n"
408
+ - type: thinking
409
+ text: "When"
410
+ - type: thinking
411
+ text: " a"
412
+ - type: thinking
413
+ text: " user"
414
+ - type: thinking
415
+ text: " sends"
416
+ - type: thinking
417
+ text: " a"
418
+ - type: thinking
419
+ text: " message:"
420
+ - type: thinking
421
+ text: "\n1."
422
+ - type: thinking
423
+ text: " AcmeRunJob"
424
+ - type: thinking
425
+ text: " starts"
426
+ - type: thinking
427
+ text: " and"
428
+ - type: thinking
429
+ text: " calls"
430
+ - type: thinking
431
+ text: " POST"
432
+ - type: thinking
433
+ text: " /v1/runs"
434
+ - type: thinking
435
+ text: "\n2."
436
+ - type: thinking
437
+ text: " Acme"
438
+ - type: thinking
439
+ text: " returns"
440
+ - type: thinking
441
+ text: " a"
442
+ - type: thinking
443
+ text: " run_id"
444
+ - type: thinking
445
+ text: "\n3."
446
+ - type: thinking
447
+ text: " The"
448
+ - type: thinking
449
+ text: " job"
450
+ - type: thinking
451
+ text: " connects"
452
+ - type: thinking
453
+ text: " to"
454
+ - type: thinking
455
+ text: " the"
456
+ - type: thinking
457
+ text: " SSE"
458
+ - type: thinking
459
+ text: " stream"
460
+ - type: thinking
461
+ text: "\n4."
462
+ - type: thinking
463
+ text: " Events"
464
+ - type: thinking
465
+ text: " arrive"
466
+ - type: thinking
467
+ text: " and"
468
+ - type: thinking
469
+ text: " are"
470
+ - type: thinking
471
+ text: " persisted"
472
+ - type: thinking
473
+ text: "\n5."
474
+ - type: thinking
475
+ text: " On"
476
+ - type: thinking
477
+ text: " run.completed,"
478
+ - type: thinking
479
+ text: " final_output"
480
+ - type: thinking
481
+ text: " is"
482
+ - type: thinking
483
+ text: " set"
484
+ - type: thinking
485
+ text: "\n6."
486
+ - type: thinking
487
+ text: " Timeline"
488
+ - type: thinking
489
+ text: " renders"
490
+ - type: thinking
491
+ text: " from"
492
+ - type: thinking
493
+ text: " RunEvent"
494
+ - type: thinking
495
+ text: " records"
496
+
497
+ - type: delay_seconds
498
+ value: 1.5
499
+
500
+ - type: thinking
501
+ text: "\n\n"