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,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module Tools
5
+ # Summarizes a large text file WITHOUT pulling its bytes into the main agent
6
+ # context. The file is chunked and map-reduced through the `summarize`
7
+ # auxiliary LLM; only the final summary string returns to the caller. This
8
+ # is the in-house realization of the "summarization subagent" pattern: the
9
+ # raw 30k-line document lives only in the aux calls, so it never bloats the
10
+ # primary prompt (which is what pushes time-to-first-token past the
11
+ # provider's stream idle-timeout and gets a run cut mid-stream).
12
+ #
13
+ # Algorithm (LangChain/OpenAI-cookbook map-reduce):
14
+ # 1. MAP — split the file into ~CHUNK_BYTES chunks, summarize each.
15
+ # 2. REDUCE— combine the chunk summaries; if the combined text still
16
+ # overflows a chunk, group + re-summarize recursively (capped).
17
+ class SummarizeFileTool < Base
18
+ # ~6k tokens/chunk at 4 bytes/token — leaves room for the prompt and the
19
+ # chunk's own summary inside a modest context window.
20
+ CHUNK_BYTES = 24_000
21
+ # Refuse absurdly large inputs rather than fan out hundreds of LLM calls.
22
+ MAX_FILE_BYTES = 8_000_000
23
+ # Bound the reduce recursion so a pathological fan-in can't loop forever.
24
+ REDUCE_DEPTH_CAP = 4
25
+ GROUP_SIZE = 5
26
+ AUX_TASK = "summarize"
27
+
28
+ # Test seam: inject a stub LLM client. Production lazily builds the real
29
+ # AuxiliaryClient, which routes to the `auxiliary.summarize` config.
30
+ attr_writer :aux_client
31
+
32
+ def name
33
+ "summarize_file"
34
+ end
35
+
36
+ def description
37
+ "Summarize a large text file WITHOUT loading it into this conversation. " \
38
+ "The file is read and map-reduced by a separate summarization model; only the " \
39
+ "final summary returns here, so the raw bytes never enter context. " \
40
+ "PREFER this over `read` whenever you need the gist of a big document — converted " \
41
+ "PDFs, logs, transcripts, anything more than a few hundred lines. For binary docs " \
42
+ "(PDF/DOCX/XLSX/PPTX) use the `read_attachment` tool, which converts them to text " \
43
+ "in-process and summarizes oversized output automatically. " \
44
+ "Use `focus` to steer what the summary must preserve."
45
+ end
46
+
47
+ def input_schema
48
+ {
49
+ type: "object",
50
+ properties: {
51
+ file_path: { type: "string", description: "Absolute or relative path to a text file" },
52
+ focus: { type: "string",
53
+ description: "What the summary must preserve, e.g. 'chapter titles and page numbers' or 'API errors with timestamps'. Optional." },
54
+ max_words: { type: "integer",
55
+ description: "Approximate length of the final summary in words (default 500)." }
56
+ },
57
+ required: %w[file_path]
58
+ }
59
+ end
60
+
61
+ def risk_level
62
+ :low
63
+ end
64
+
65
+ def call(arguments)
66
+ file_path = arguments["file_path"] || arguments[:file_path]
67
+ focus = (arguments["focus"] || arguments[:focus]).to_s.strip
68
+ focus = "the key facts, structure, decisions, and any errors" if focus.empty?
69
+ max_words = (arguments["max_words"] || arguments[:max_words] || 500).to_i.clamp(50, 4000)
70
+
71
+ return "Error: file_path is required" if file_path.nil? || file_path.to_s.empty?
72
+
73
+ expanded = File.expand_path(file_path)
74
+ return "Error: File not found: #{file_path}" unless File.exist?(expanded)
75
+ return "Error: Not a regular file: #{file_path}" unless File.file?(expanded)
76
+
77
+ size = File.size(expanded)
78
+ return "#{file_path} is empty — nothing to summarize." if size.zero?
79
+ if binary?(expanded)
80
+ return "Error: #{file_path} looks binary. Read it with the `read_attachment` tool " \
81
+ "(it converts documents to text in-process and summarizes oversized output), " \
82
+ "rather than summarizing raw bytes."
83
+ end
84
+ if size > MAX_FILE_BYTES
85
+ return "Error: #{file_path} is #{size / 1_000_000}MB, over the " \
86
+ "#{MAX_FILE_BYTES / 1_000_000}MB summarize cap. Split it (e.g. with split/sed) " \
87
+ "or grep to the relevant section, then summarize that."
88
+ end
89
+
90
+ chunks = chunk_file(expanded)
91
+ return "#{file_path} is empty — nothing to summarize." if chunks.empty?
92
+
93
+ summaries = chunks.each_with_index.map do |chunk, i|
94
+ raise Rubino::Interrupted if cancellation_requested?
95
+
96
+ emit_chunk("summarizing chunk #{i + 1}/#{chunks.size}…\n")
97
+ map_summarize(chunk, focus)
98
+ end
99
+
100
+ summary = reduce(summaries, focus, max_words)
101
+ {
102
+ output: summary,
103
+ metrics: "#{chunks.size} chunk#{"s" if chunks.size != 1} → summary"
104
+ }
105
+ rescue Rubino::Interrupted
106
+ raise
107
+ rescue StandardError => e
108
+ "Error summarizing #{file_path}: #{e.message}"
109
+ end
110
+
111
+ private
112
+
113
+ def aux_client
114
+ @aux_client ||= LLM::AuxiliaryClient.new
115
+ end
116
+
117
+ # Streams the file into ~CHUNK_BYTES blocks on line boundaries so we never
118
+ # slurp a multi-MB file whole, and a chunk never splits a line.
119
+ def chunk_file(path)
120
+ chunks = []
121
+ buf = +""
122
+ File.foreach(path) do |line|
123
+ buf << line
124
+ if buf.bytesize >= CHUNK_BYTES
125
+ chunks << buf
126
+ buf = +""
127
+ end
128
+ end
129
+ chunks << buf unless buf.empty?
130
+ chunks
131
+ end
132
+
133
+ def map_summarize(chunk, focus)
134
+ complete(
135
+ "Write a concise summary of the following text, preserving #{focus}. " \
136
+ "This is only PART of a larger document, so do NOT conclude with wording " \
137
+ "like \"Finally\" or \"In conclusion\".\n\n" \
138
+ "#{chunk}\n\nCONCISE SUMMARY:"
139
+ )
140
+ end
141
+
142
+ # Combine chunk summaries into one. If the combined summaries still
143
+ # overflow a chunk, group and re-summarize recursively (tree reduce).
144
+ def reduce(summaries, focus, max_words, depth = 0)
145
+ return summaries.first.to_s if summaries.size <= 1
146
+
147
+ combined = summaries.join("\n\n")
148
+ if combined.bytesize <= CHUNK_BYTES || depth >= REDUCE_DEPTH_CAP
149
+ return complete(
150
+ "Combine these partial summaries of one document into a single coherent " \
151
+ "summary of about #{max_words} words, preserving #{focus}. Remove redundancy " \
152
+ "and keep it well-structured.\n\n#{combined}\n\nFINAL SUMMARY:"
153
+ )
154
+ end
155
+
156
+ regrouped = summaries.each_slice(GROUP_SIZE).map do |group|
157
+ raise Rubino::Interrupted if cancellation_requested?
158
+
159
+ map_summarize(group.join("\n\n"), focus)
160
+ end
161
+ reduce(regrouped, focus, max_words, depth + 1)
162
+ end
163
+
164
+ def complete(prompt)
165
+ response = aux_client.call(task: AUX_TASK, messages: [{ role: "user", content: prompt }])
166
+ text = response.respond_to?(:content) ? response.content.to_s : response.to_s
167
+ text.strip
168
+ end
169
+
170
+ # Light binary guard: a NUL byte in the first KB. summarize_file is for
171
+ # text; binary docs must be converted upstream.
172
+ def binary?(path)
173
+ sample = File.binread(path, 1024)
174
+ return false if sample.nil? || sample.empty?
175
+
176
+ sample.include?("\x00")
177
+ rescue Errno::ENOENT, Errno::EACCES
178
+ false
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module Tools
5
+ # Reads the status and result of a background subagent started by `task`
6
+ # (the default background path). The BashOutput / TaskOutput analogue: lets
7
+ # the model poll a background subagent deterministically even if it hasn't
8
+ # yet received the auto-injected `[background-task] … completed` notice.
9
+ #
10
+ # Returns `running` (still working), `completed` (with the full final
11
+ # result — not the truncated notice), or `failed` (with the error). With no
12
+ # `task_id` it lists every tracked background subagent (the /tasks analogue).
13
+ class TaskResultTool < Base
14
+ def name
15
+ "task_result"
16
+ end
17
+
18
+ # Shares the `task` config gate — disabling delegation disables its
19
+ # companion poll/stop tools too.
20
+ def config_key
21
+ "task"
22
+ end
23
+
24
+ def description
25
+ "Fetch the status and result of a background subagent started by `task`. " \
26
+ "Returns `running` (still working), `completed` (with the full final " \
27
+ "result), or `failed` (with the error). Call without a task_id to list " \
28
+ "all tracked background subagents."
29
+ end
30
+
31
+ def input_schema
32
+ {
33
+ type: "object",
34
+ properties: {
35
+ task_id: {
36
+ type: "string",
37
+ description: "The task id (sa_…) returned by `task`. Omit to list all background subagents."
38
+ }
39
+ }
40
+ }
41
+ end
42
+
43
+ def risk_level
44
+ :low
45
+ end
46
+
47
+ def call(arguments)
48
+ task_id = (arguments["task_id"] || arguments[:task_id]).to_s.strip
49
+ registry = BackgroundTasks.instance
50
+
51
+ return list_all(registry) if task_id.empty?
52
+
53
+ entry = registry.find(task_id)
54
+ return "Error: no background subagent with task_id=#{task_id}" unless entry
55
+
56
+ render(entry)
57
+ end
58
+
59
+ private
60
+
61
+ def render(entry)
62
+ case entry.status
63
+ when :running
64
+ "[#{entry.id}] status=running (subagent '#{entry.subagent}', " \
65
+ "started #{elapsed(entry)}s ago) — not finished yet; you'll be notified on completion."
66
+ when :completed
67
+ "[#{entry.id}] status=completed (subagent '#{entry.subagent}')\n#{entry.result}"
68
+ when :failed
69
+ "[#{entry.id}] status=failed (subagent '#{entry.subagent}'): #{entry.error}"
70
+ else
71
+ "[#{entry.id}] status=#{entry.status}"
72
+ end
73
+ end
74
+
75
+ def list_all(registry)
76
+ entries = registry.list
77
+ return "No background subagents have been started." if entries.empty?
78
+
79
+ lines = entries.map do |e|
80
+ "[#{e.id}] #{e.status} · #{e.subagent} · started #{elapsed(e)}s ago"
81
+ end
82
+ "Background subagents:\n#{lines.join("\n")}"
83
+ end
84
+
85
+ def elapsed(entry)
86
+ ((entry.finished_at || Time.now) - entry.started_at).round
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module Tools
5
+ # Cancels a running background subagent started by `task`. The KillShell
6
+ # analogue: flips the child Runner's CancelToken (the exact mechanism
7
+ # Run::Executor's stop-watcher uses for top-level runs), which unwinds the
8
+ # child loop cooperatively at its next cancel checkpoint.
9
+ class TaskStopTool < Base
10
+ def name
11
+ "task_stop"
12
+ end
13
+
14
+ def config_key
15
+ "task"
16
+ end
17
+
18
+ # The live statuses a stop applies to. A child parked on a human approval
19
+ # or an ask_parent gate still holds its thread + concurrency slot
20
+ # (BackgroundTasks#live_status?), so it MUST be stoppable — refusing left
21
+ # a blocked child as a zombie holding its slot until the ask-gate timeout
22
+ # (#197). :stopping is excluded: a second stop is honestly "already
23
+ # stopping — nothing to stop".
24
+ STOPPABLE = %i[running needs_approval blocked_on_human blocked_on_parent].freeze
25
+
26
+ def description
27
+ "Stop a running background subagent started by `task` — including one " \
28
+ "parked on an approval or an ask_parent question. Cancels the " \
29
+ "subagent's nested run; its task_result will then report failed/cancelled."
30
+ end
31
+
32
+ def input_schema
33
+ {
34
+ type: "object",
35
+ properties: {
36
+ task_id: {
37
+ type: "string",
38
+ description: "The task id (sa_…) returned by `task`."
39
+ }
40
+ },
41
+ required: %w[task_id]
42
+ }
43
+ end
44
+
45
+ def risk_level
46
+ :medium
47
+ end
48
+
49
+ def call(arguments)
50
+ task_id = (arguments["task_id"] || arguments[:task_id]).to_s.strip
51
+ return "Error: task_id is required" if task_id.empty?
52
+
53
+ registry = BackgroundTasks.instance
54
+ entry = registry.find(task_id)
55
+ return "Error: no background subagent with task_id=#{task_id}" unless entry
56
+
57
+ return "[#{task_id}] already #{entry.status} — nothing to stop." unless STOPPABLE.include?(entry.status)
58
+
59
+ # Mark the stop first so the list/cards immediately show ◌ stopping and
60
+ # the unwind records as :stopped, not failed (#108/#13).
61
+ registry.request_stop(task_id)
62
+ # Flip the runner's CancelToken BEFORE waking any gate, so a child woken
63
+ # from a parked wait observes the flipped token at its very next
64
+ # checkpoint and unwinds immediately.
65
+ entry.runner&.cancel!
66
+ # A child parked on its OWN approval or ask gate is blocked inside the
67
+ # gate's wait; cancel the gates so it wakes (Interrupted → deny/cancel)
68
+ # and unwinds NOW instead of holding its thread + slot until the bound
69
+ # elapses (#197) — exactly what the human /agents <id> --stop path does.
70
+ entry.approval_gate&.cancel!
71
+ entry.ask_gate&.cancel!
72
+ # Stop-cascade (S5a): wake any descendant parked on a blocking ask_parent
73
+ # so the whole subtree unwinds at once (no orphaned blocked grandchild).
74
+ registry.cancel_descendant_ask_gates(task_id)
75
+ "[#{task_id}] stop requested (subagent '#{entry.subagent}'). " \
76
+ "It will unwind at its next checkpoint; check task_result(\"#{task_id}\")."
77
+ end
78
+ end
79
+ end
80
+ end