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,121 @@
1
+ # Repro scenario for the CLI streaming-table live-region trail/accumulation bug.
2
+ # Streams a WIDE (5-column) markdown table emitted as MANY small content deltas
3
+ # so the per-chunk live-region repaint cascade appears. Each data ROW is wider
4
+ # than a narrow terminal AND carries double-width emoji (✅ 🔄 ⚠️) spread across
5
+ # cells so the CLAMPED tail of the in-flight row still contains an emoji — whose
6
+ # display width (2) exceeds its char length (1). With the buggy char-count clamp
7
+ # that emoji pushes the "fits in one row" line past the real terminal width, so
8
+ # it wraps and the single-row clear leaves residue that accumulates downward.
9
+ events:
10
+ - type: content
11
+ text: "Here is the status table:\n\n"
12
+ - type: delay_seconds
13
+ value: 0.30
14
+ - type: content
15
+ text: "| Feature | Status | Priority | Owner | Notes |\n"
16
+ - type: delay_seconds
17
+ value: 0.30
18
+ - type: content
19
+ text: "| --- | --- | --- | --- | --- |\n"
20
+ - type: delay_seconds
21
+ value: 0.30
22
+ - type: content
23
+ text: "| HTTP API ✅ "
24
+ - type: delay_seconds
25
+ value: 0.30
26
+ - type: content
27
+ text: "| Done ✅ | High ⚠️ "
28
+ - type: delay_seconds
29
+ value: 0.30
30
+ - type: content
31
+ text: "| Backend ✅ | Stable in prod ✅ "
32
+ - type: delay_seconds
33
+ value: 0.30
34
+ - type: content
35
+ text: "since release v0.2 ✅ ✅ ✅ |\n"
36
+ - type: delay_seconds
37
+ value: 0.30
38
+ - type: content
39
+ text: "| CLI Chat ✅ "
40
+ - type: delay_seconds
41
+ value: 0.30
42
+ - type: content
43
+ text: "| Done ✅ | High ⚠️ "
44
+ - type: delay_seconds
45
+ value: 0.30
46
+ - type: content
47
+ text: "| Frontend ✅ | Live md blocks ✅ "
48
+ - type: delay_seconds
49
+ value: 0.30
50
+ - type: content
51
+ text: "committed and styled ✅ ✅ ✅ |\n"
52
+ - type: delay_seconds
53
+ value: 0.30
54
+ - type: content
55
+ text: "| TUI Mode 🔄 "
56
+ - type: delay_seconds
57
+ value: 0.30
58
+ - type: content
59
+ text: "| In Progress 🔄 | Medium ⚠️ "
60
+ - type: delay_seconds
61
+ value: 0.30
62
+ - type: content
63
+ text: "| Platform 🔄 | Live region 🔄 "
64
+ - type: delay_seconds
65
+ value: 0.30
66
+ - type: content
67
+ text: "repaint work ongoing 🔄 🔄 🔄 |\n"
68
+ - type: delay_seconds
69
+ value: 0.30
70
+ - type: content
71
+ text: "| MCP Bridge 🔄 "
72
+ - type: delay_seconds
73
+ value: 0.30
74
+ - type: content
75
+ text: "| In Progress 🔄 | Medium ⚠️ "
76
+ - type: delay_seconds
77
+ value: 0.30
78
+ - type: content
79
+ text: "| Integrations 🔄 | Tool schema 🔄 "
80
+ - type: delay_seconds
81
+ value: 0.30
82
+ - type: content
83
+ text: "negotiation pending 🔄 🔄 🔄 |\n"
84
+ - type: delay_seconds
85
+ value: 0.30
86
+ - type: content
87
+ text: "| Web UI ✅ "
88
+ - type: delay_seconds
89
+ value: 0.30
90
+ - type: content
91
+ text: "| Done ✅ | Low ⚠️ "
92
+ - type: delay_seconds
93
+ value: 0.30
94
+ - type: content
95
+ text: "| Frontend ✅ | Rails DOM ✅ "
96
+ - type: delay_seconds
97
+ value: 0.30
98
+ - type: content
99
+ text: "token streaming live ✅ ✅ ✅ |\n"
100
+ - type: delay_seconds
101
+ value: 0.30
102
+ - type: content
103
+ text: "| Memory 🔄 "
104
+ - type: delay_seconds
105
+ value: 0.30
106
+ - type: content
107
+ text: "| In Progress 🔄 | High ⚠️ "
108
+ - type: delay_seconds
109
+ value: 0.30
110
+ - type: content
111
+ text: "| Core 🔄 | SQLite tiny-Zep 🔄 "
112
+ - type: delay_seconds
113
+ value: 0.30
114
+ - type: content
115
+ text: "recall backend 🔄 🔄 🔄 |\n"
116
+ - type: delay_seconds
117
+ value: 0.30
118
+ - type: content
119
+ text: "\nAll tracked features above."
120
+ - type: delay_seconds
121
+ value: 0.30
@@ -0,0 +1,50 @@
1
+ # With-approvals scenario: tokenized reasoning, then a shell tool_call that
2
+ # the real ApprovalPolicy will gate on. Once approval lands and the tool
3
+ # executes, the next agent turn re-enters the provider — but for THIS turn
4
+ # the provider just emits thinking + the tool_call.
5
+ #
6
+ # Translation from the reference with-approvals scenario:
7
+ # - reasoning.available {text} → thinking chunks (token-by-token)
8
+ # - __pause_for_approval → DROPPED; the shell tool_call below
9
+ # triggers approval via ApprovalPolicy
10
+ # - tool.started/completed → collapsed into one tool_call
11
+ # - message.delta {delta} → content chunks (will be emitted on the
12
+ # follow-up turn after tool result; here
13
+ # we keep them so the fake turn is rich)
14
+ # - run.completed → DROPPED; fake provider terminates when
15
+ # events are exhausted
16
+ events:
17
+ - type: thinking
18
+ text: "I"
19
+ - type: delay_seconds
20
+ value: 0.02
21
+ - type: thinking
22
+ text: " need"
23
+ - type: delay_seconds
24
+ value: 0.02
25
+ - type: thinking
26
+ text: " your"
27
+ - type: delay_seconds
28
+ value: 0.02
29
+ - type: thinking
30
+ text: " approval"
31
+ - type: delay_seconds
32
+ value: 0.04
33
+ - type: thinking
34
+ text: " for"
35
+ - type: delay_seconds
36
+ value: 0.02
37
+ - type: thinking
38
+ text: " this"
39
+ - type: delay_seconds
40
+ value: 0.02
41
+ - type: thinking
42
+ text: " command."
43
+ - type: delay_seconds
44
+ value: 0.04
45
+
46
+ - type: tool_call
47
+ id: "call_fake_approve_1"
48
+ name: "shell"
49
+ arguments:
50
+ command: "rm -rf /tmp/cache"
@@ -0,0 +1,98 @@
1
+ # With-artifacts scenario: model writes a markdown report and hands it to
2
+ # the UI as a downloadable artifact. Mirrors the reference UX —
3
+ # token-by-token reasoning, a short content explanation, then two
4
+ # tool calls in one turn:
5
+ # 1. `write` drops the file on disk (workspace-scoped — bin/dev points
6
+ # terminal.cwd at $RUBINO_HOME/workspace so this lands under
7
+ # the strict-workspace check)
8
+ # 2. `attach_file` registers it as an artifact → ARTIFACT_CREATED on
9
+ # the bus → SSE artifact.created → the web UI downloads via
10
+ # GET /v1/files and renders a download link
11
+ #
12
+ # After the tools execute, FakeProvider's post_tool_turn? guard kicks in
13
+ # and emits a closing "Done." so the run terminates without re-replaying.
14
+ events:
15
+ - type: thinking
16
+ text: "Let"
17
+ - type: delay_seconds
18
+ value: 0.02
19
+ - type: thinking
20
+ text: " me"
21
+ - type: delay_seconds
22
+ value: 0.02
23
+ - type: thinking
24
+ text: " put"
25
+ - type: delay_seconds
26
+ value: 0.02
27
+ - type: thinking
28
+ text: " together"
29
+ - type: delay_seconds
30
+ value: 0.02
31
+ - type: thinking
32
+ text: " a"
33
+ - type: delay_seconds
34
+ value: 0.02
35
+ - type: thinking
36
+ text: " short"
37
+ - type: delay_seconds
38
+ value: 0.02
39
+ - type: thinking
40
+ text: " markdown"
41
+ - type: delay_seconds
42
+ value: 0.02
43
+ - type: thinking
44
+ text: " report"
45
+ - type: delay_seconds
46
+ value: 0.02
47
+ - type: thinking
48
+ text: " summarising"
49
+ - type: delay_seconds
50
+ value: 0.02
51
+ - type: thinking
52
+ text: " the"
53
+ - type: delay_seconds
54
+ value: 0.02
55
+ - type: thinking
56
+ text: " findings."
57
+ - type: delay_seconds
58
+ value: 0.04
59
+
60
+ - type: content
61
+ text: "I've put together a short markdown report. "
62
+ - type: delay_seconds
63
+ value: 0.04
64
+ - type: content
65
+ text: "Writing it to `report.md` now and attaching it so you can download it directly from this turn.\n"
66
+ - type: delay_seconds
67
+ value: 0.05
68
+
69
+ - type: tool_call
70
+ id: "call_fake_artifact_write"
71
+ name: "write"
72
+ arguments:
73
+ file_path: "report.md"
74
+ content: |
75
+ # Report
76
+
77
+ Generated by the fake LLM provider for the `with-artifacts` scenario.
78
+
79
+ ## Highlights
80
+
81
+ - The agent created this markdown file with the `write` tool.
82
+ - It was then attached to the run via `attach_file` so the web UI
83
+ could surface a download link in the timeline.
84
+ - This is exactly the round-trip the legacy fake provider
85
+ exercised: reasoning → content → tool call → artifact.
86
+
87
+ ## Next steps
88
+
89
+ Open the attached file from the session timeline to see the rendered
90
+ output. Real runs would replace this with whatever the user asked
91
+ for (CSV exports, configuration dumps, screenshots, etc.).
92
+
93
+ - type: tool_call
94
+ id: "call_fake_artifact_attach"
95
+ name: "attach_file"
96
+ arguments:
97
+ file_path: "report.md"
98
+ filename: "report.md"
@@ -0,0 +1,32 @@
1
+ # With-clarify scenario: the LLM emits a question tool_call so the real
2
+ # question pipeline gathers user input; afterwards it streams a short
3
+ # confirmation. __pause_for_clarify is dropped — the question tool_call
4
+ # triggers the clarify flow naturally.
5
+ events:
6
+ - type: thinking
7
+ text: "Let"
8
+ - type: thinking
9
+ text: " me"
10
+ - type: thinking
11
+ text: " ask"
12
+ - type: thinking
13
+ text: " you."
14
+ - type: delay_seconds
15
+ value: 0.04
16
+ - type: tool_call
17
+ id: "call_fake_clarify_1"
18
+ name: "question"
19
+ arguments:
20
+ question: "Which approach do you prefer for the API layer?"
21
+ options:
22
+ - label: "Option A: REST API"
23
+ - label: "Option B: GraphQL"
24
+ - label: "Option C: gRPC"
25
+ - type: thinking
26
+ text: "Great"
27
+ - type: thinking
28
+ text: " choice."
29
+ - type: delay_seconds
30
+ value: 0.04
31
+ - type: content
32
+ text: "Proceeding with your selection."
@@ -0,0 +1,175 @@
1
+ # With-reasoning scenario: multi-step reasoning with tool calls and
2
+ # streaming output. Best for exercising the timeline rendering and
3
+ # live-streaming UI — reasoning is emitted token-by-token to mimic a
4
+ # real LLM, and real tools (read, shell) are invoked so the downstream
5
+ # pipeline (approvals, output capture) runs end-to-end.
6
+ events:
7
+ - type: thinking
8
+ text: "Let"
9
+ - type: delay_seconds
10
+ value: 0.02
11
+ - type: thinking
12
+ text: " me"
13
+ - type: delay_seconds
14
+ value: 0.02
15
+ - type: thinking
16
+ text: " analyze"
17
+ - type: delay_seconds
18
+ value: 0.02
19
+ - type: thinking
20
+ text: " the"
21
+ - type: delay_seconds
22
+ value: 0.02
23
+ - type: thinking
24
+ text: " request"
25
+ - type: delay_seconds
26
+ value: 0.02
27
+ - type: thinking
28
+ text: " step"
29
+ - type: delay_seconds
30
+ value: 0.02
31
+ - type: thinking
32
+ text: " by"
33
+ - type: delay_seconds
34
+ value: 0.02
35
+ - type: thinking
36
+ text: " step."
37
+ - type: delay_seconds
38
+ value: 0.02
39
+ - type: thinking
40
+ text: "\n"
41
+ - type: delay_seconds
42
+ value: 0.02
43
+ - type: thinking
44
+ text: "First"
45
+ - type: delay_seconds
46
+ value: 0.04
47
+ - type: thinking
48
+ text: " I"
49
+ - type: delay_seconds
50
+ value: 0.02
51
+ - type: thinking
52
+ text: " need"
53
+ - type: delay_seconds
54
+ value: 0.02
55
+ - type: thinking
56
+ text: " to"
57
+ - type: delay_seconds
58
+ value: 0.02
59
+ - type: thinking
60
+ text: " check"
61
+ - type: delay_seconds
62
+ value: 0.02
63
+ - type: thinking
64
+ text: " the"
65
+ - type: delay_seconds
66
+ value: 0.02
67
+ - type: thinking
68
+ text: " current"
69
+ - type: delay_seconds
70
+ value: 0.02
71
+ - type: thinking
72
+ text: " state."
73
+ - type: delay_seconds
74
+ value: 0.04
75
+
76
+ - type: tool_call
77
+ id: "call_fake_reasoning_1"
78
+ name: "read"
79
+ arguments:
80
+ file_path: "/tmp/example.txt"
81
+
82
+ - type: delay_seconds
83
+ value: 0.5
84
+
85
+ - type: thinking
86
+ text: "Found"
87
+ - type: delay_seconds
88
+ value: 0.04
89
+ - type: thinking
90
+ text: " relevant"
91
+ - type: delay_seconds
92
+ value: 0.04
93
+ - type: thinking
94
+ text: " data."
95
+ - type: delay_seconds
96
+ value: 0.02
97
+ - type: thinking
98
+ text: "\n"
99
+ - type: delay_seconds
100
+ value: 0.02
101
+ - type: thinking
102
+ text: "Now"
103
+ - type: delay_seconds
104
+ value: 0.02
105
+ - type: thinking
106
+ text: " running"
107
+ - type: delay_seconds
108
+ value: 0.04
109
+ - type: thinking
110
+ text: " tests"
111
+ - type: delay_seconds
112
+ value: 0.02
113
+ - type: thinking
114
+ text: " to"
115
+ - type: delay_seconds
116
+ value: 0.02
117
+ - type: thinking
118
+ text: " validate."
119
+ - type: delay_seconds
120
+ value: 0.04
121
+
122
+ - type: tool_call
123
+ id: "call_fake_reasoning_2"
124
+ name: "shell"
125
+ arguments:
126
+ command: "npm test"
127
+
128
+ - type: delay_seconds
129
+ value: 0.8
130
+
131
+ - type: thinking
132
+ text: "Tests"
133
+ - type: delay_seconds
134
+ value: 0.04
135
+ - type: thinking
136
+ text: " pass."
137
+ - type: delay_seconds
138
+ value: 0.02
139
+ - type: thinking
140
+ text: "\n"
141
+ - type: delay_seconds
142
+ value: 0.02
143
+ - type: thinking
144
+ text: "Formulating"
145
+ - type: delay_seconds
146
+ value: 0.04
147
+ - type: thinking
148
+ text: " final"
149
+ - type: delay_seconds
150
+ value: 0.02
151
+ - type: thinking
152
+ text: " response."
153
+ - type: delay_seconds
154
+ value: 0.04
155
+
156
+ - type: content
157
+ text: "Here are the results of my analysis:\n\n"
158
+ - type: delay_seconds
159
+ value: 0.05
160
+ - type: content
161
+ text: "- File content analyzed\n"
162
+ - type: delay_seconds
163
+ value: 0.05
164
+ - type: content
165
+ text: "- Tests passed\n"
166
+ - type: delay_seconds
167
+ value: 0.05
168
+ - type: content
169
+ text: "- All checks complete\n\n"
170
+ - type: delay_seconds
171
+ value: 0.05
172
+ - type: content
173
+ text: "The system is running normally. No issues detected."
174
+ - type: delay_seconds
175
+ value: 0.05
@@ -0,0 +1,104 @@
1
+ # Scenario with file upload. Mirrors the reference with-uploads but uses the
2
+ # FakeProvider event vocabulary. The agent "reads" an uploaded file and
3
+ # emits a fake analysis as the final assistant message. {{input}} is the
4
+ # user's text plus the uploaded file names/content types.
5
+ events:
6
+ - type: thinking
7
+ text: "I"
8
+ - type: delay_seconds
9
+ value: 0.02
10
+ - type: thinking
11
+ text: " received"
12
+ - type: delay_seconds
13
+ value: 0.04
14
+ - type: thinking
15
+ text: " the"
16
+ - type: delay_seconds
17
+ value: 0.02
18
+ - type: thinking
19
+ text: " uploaded"
20
+ - type: delay_seconds
21
+ value: 0.04
22
+ - type: thinking
23
+ text: " file."
24
+ - type: delay_seconds
25
+ value: 0.02
26
+ - type: thinking
27
+ text: "\n"
28
+ - type: delay_seconds
29
+ value: 0.02
30
+ - type: thinking
31
+ text: "Analyzing"
32
+ - type: delay_seconds
33
+ value: 0.04
34
+ - type: thinking
35
+ text: " its"
36
+ - type: delay_seconds
37
+ value: 0.02
38
+ - type: thinking
39
+ text: " contents..."
40
+ - type: delay_seconds
41
+ value: 0.04
42
+
43
+ - type: delay_seconds
44
+ value: 0.4
45
+
46
+ - type: thinking
47
+ text: "The"
48
+ - type: delay_seconds
49
+ value: 0.02
50
+ - type: thinking
51
+ text: " file"
52
+ - type: delay_seconds
53
+ value: 0.02
54
+ - type: thinking
55
+ text: " appears"
56
+ - type: delay_seconds
57
+ value: 0.04
58
+ - type: thinking
59
+ text: " to"
60
+ - type: delay_seconds
61
+ value: 0.02
62
+ - type: thinking
63
+ text: " contain"
64
+ - type: delay_seconds
65
+ value: 0.04
66
+ - type: thinking
67
+ text: " valid"
68
+ - type: delay_seconds
69
+ value: 0.04
70
+ - type: thinking
71
+ text: " data."
72
+ - type: delay_seconds
73
+ value: 0.02
74
+ - type: thinking
75
+ text: "\n"
76
+ - type: delay_seconds
77
+ value: 0.02
78
+ - type: thinking
79
+ text: "Processing..."
80
+ - type: delay_seconds
81
+ value: 0.06
82
+
83
+ - type: delay_seconds
84
+ value: 0.3
85
+
86
+ - type: content
87
+ text: "File analysis complete:\n\n"
88
+ - type: delay_seconds
89
+ value: 0.05
90
+ # `{{input}}` is the web UI's already-prefixed text (e.g. "uploaded file:
91
+ # report.pdf" when the user attached a file with no message). Echo it
92
+ # straight so the agent reply always names the file the user sent.
93
+ - type: content
94
+ text: "Received {{input}}.\n"
95
+ - type: delay_seconds
96
+ value: 0.05
97
+ - type: content
98
+ text: "- Content validated\n"
99
+ - type: delay_seconds
100
+ value: 0.05
101
+ - type: content
102
+ text: "- No issues detected\n"
103
+ - type: delay_seconds
104
+ value: 0.05
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "provider_resolver"
4
+
5
+ module Rubino
6
+ module LLM
7
+ # Session-scoped memory of providers that rejected an Anthropic-style
8
+ # thinking budget, plus the detector for that rejection (#75), plus the
9
+ # static per-provider capability gate (#2).
10
+ #
11
+ # Process-level (not per-adapter) because Lifecycle rebuilds the adapter
12
+ # every turn — and one CLI process serves one chat session, so this is
13
+ # exactly "remember for the session". RubyLLMAdapter consults it before
14
+ # rendering a budget and marks it on a recognised rejection, so the
15
+ # provider is never sent a budget again this session.
16
+ module ThinkingSupport
17
+ @unsupported = {}
18
+
19
+ module_function
20
+
21
+ def unsupported?(provider)
22
+ @unsupported.key?(provider.to_s)
23
+ end
24
+
25
+ # Per-provider thinking CAPABILITY gate (#2). #unsupported?/#rejection?
26
+ # (#75) handle a provider that REJECTS a budget (hard 400 → retry +
27
+ # session memo); this handles one that ACCEPTS it and then, lacking a
28
+ # separate reasoning channel, dumps its chain-of-thought as plain content
29
+ # deltas — observed live on MiniMax. providers.<name>.supports_thinking
30
+ # (true/false) is the explicit override; unset, MiniMax-family model ids
31
+ # default to false (they return no thinking blocks and leak reasoning
32
+ # when sent a budget), everything else to true.
33
+ def supports?(provider_cfg, model_id)
34
+ configured = provider_cfg["supports_thinking"]
35
+ return configured unless configured.nil?
36
+
37
+ !model_id.to_s.match?(ProviderResolver::PROVIDER_PATTERNS["minimax"])
38
+ end
39
+
40
+ # providers.<name>.supports_thinking: true is the user's explicit promise
41
+ # that the backend accepts an Anthropic-style thinking block. ruby_llm
42
+ # 1.16 only renders with_thinking when the model's REGISTRY entry
43
+ # declares a budget_tokens reasoning option; an assume-model-exists model
44
+ # (MiniMax-M3 on the anthropic-compatible path) declares none, so
45
+ # with_thinking raised client-side before any request, the #75 rejection
46
+ # detector matched the message, and the documented opt-in silently died
47
+ # every turn (#175). On that path the adapter puts the wire payload on
48
+ # with_params instead, which ruby_llm deep-merges into the request body
49
+ # unconditionally.
50
+ def budget_via_params?(provider_cfg, chat)
51
+ return false unless provider_cfg["supports_thinking"] == true
52
+
53
+ model = chat.respond_to?(:model) ? chat.model : nil
54
+ !(model.respond_to?(:reasoning_option) && model.reasoning_option("budget_tokens"))
55
+ rescue StandardError
56
+ true
57
+ end
58
+
59
+ # Records the rejection and tells the user once with a dim note (only
60
+ # the marking path emits it). Cosmetic: a UI failure must never break
61
+ # the retried turn.
62
+ def mark_unsupported!(provider, notify: nil)
63
+ @unsupported[provider.to_s] = true
64
+ notify&.note("provider doesn't support thinking — effort off")
65
+ rescue StandardError
66
+ nil
67
+ end
68
+
69
+ # Test seam: forget all recorded rejections (a fresh "session").
70
+ def reset!
71
+ @unsupported = {}
72
+ end
73
+
74
+ # True when +error+ reads as a provider's "thinking (budget) is not
75
+ # supported" rejection. Kept narrow: the message must name thinking plus
76
+ # a not-supported phrasing.
77
+ def rejection?(error)
78
+ msg = error.message.to_s.downcase
79
+ msg.include?("thinking") &&
80
+ (msg.include?("not support") || msg.include?("unsupported"))
81
+ end
82
+ end
83
+ end
84
+ end