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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +115 -0
- data/.rubocop_todo.yml +955 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +97 -0
- data/CHANGELOG.md +344 -0
- data/CONTRIBUTING.md +69 -0
- data/LICENSE +21 -0
- data/README.md +200 -0
- data/Rakefile +8 -0
- data/docs/agents.md +190 -0
- data/docs/api/v1.md +414 -0
- data/docs/architecture.md +177 -0
- data/docs/commands.md +375 -0
- data/docs/configuration.md +590 -0
- data/docs/getting-started.md +143 -0
- data/docs/jobs.md +332 -0
- data/docs/mcp.md +128 -0
- data/docs/memory.md +98 -0
- data/docs/models-and-keys.md +173 -0
- data/docs/oauth-providers.md +145 -0
- data/docs/plugins.md +195 -0
- data/docs/security.md +145 -0
- data/docs/skills.md +322 -0
- data/docs/tools.md +395 -0
- data/docs/troubleshooting.md +73 -0
- data/exe/rubino +9 -0
- data/install.sh +275 -0
- data/lib/rubino/active_skill.rb +50 -0
- data/lib/rubino/agent/agent_registry.rb +120 -0
- data/lib/rubino/agent/backoff_policy.rb +116 -0
- data/lib/rubino/agent/definition.rb +128 -0
- data/lib/rubino/agent/degenerate_recovery.rb +271 -0
- data/lib/rubino/agent/fallback_chain.rb +194 -0
- data/lib/rubino/agent/iteration_budget.rb +50 -0
- data/lib/rubino/agent/loop.rb +617 -0
- data/lib/rubino/agent/model_call_runner.rb +383 -0
- data/lib/rubino/agent/prompts/build.txt +69 -0
- data/lib/rubino/agent/prompts/compaction.txt +20 -0
- data/lib/rubino/agent/prompts/explore.txt +19 -0
- data/lib/rubino/agent/prompts/general.txt +20 -0
- data/lib/rubino/agent/prompts/plan.txt +31 -0
- data/lib/rubino/agent/response_validator.rb +70 -0
- data/lib/rubino/agent/router.rb +65 -0
- data/lib/rubino/agent/runner.rb +195 -0
- data/lib/rubino/agent/tool_executor.rb +402 -0
- data/lib/rubino/agent/truncation_continuation.rb +137 -0
- data/lib/rubino/api/middleware/auth.rb +43 -0
- data/lib/rubino/api/middleware/error_handler.rb +65 -0
- data/lib/rubino/api/middleware/json_parser.rb +100 -0
- data/lib/rubino/api/middleware/observability.rb +59 -0
- data/lib/rubino/api/middleware/rate_limit.rb +136 -0
- data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
- data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
- data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
- data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
- data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
- data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
- data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
- data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
- data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
- data/lib/rubino/api/operations/files/read_operation.rb +40 -0
- data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
- data/lib/rubino/api/operations/health_operation.rb +46 -0
- data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
- data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
- data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
- data/lib/rubino/api/operations/metrics_operation.rb +18 -0
- data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
- data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
- data/lib/rubino/api/operations/models/list_operation.rb +45 -0
- data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
- data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
- data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
- data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
- data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
- data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
- data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
- data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
- data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
- data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
- data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
- data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
- data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
- data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
- data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
- data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
- data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
- data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
- data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
- data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
- data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
- data/lib/rubino/api/request.rb +54 -0
- data/lib/rubino/api/responses.rb +64 -0
- data/lib/rubino/api/router.rb +72 -0
- data/lib/rubino/api/schemas.rb +103 -0
- data/lib/rubino/api/server.rb +102 -0
- data/lib/rubino/api/tls.rb +108 -0
- data/lib/rubino/attachments/classification.rb +16 -0
- data/lib/rubino/attachments/classify.rb +171 -0
- data/lib/rubino/attachments/defang.rb +47 -0
- data/lib/rubino/attachments/policy.rb +36 -0
- data/lib/rubino/attachments/preamble.rb +120 -0
- data/lib/rubino/boot/encryption_key.rb +32 -0
- data/lib/rubino/cli/chat/bang_shell.rb +257 -0
- data/lib/rubino/cli/chat/completion_builder.rb +290 -0
- data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
- data/lib/rubino/cli/chat/image_inbox.rb +168 -0
- data/lib/rubino/cli/chat/session_resolver.rb +176 -0
- data/lib/rubino/cli/chat_command.rb +1674 -0
- data/lib/rubino/cli/commands.rb +250 -0
- data/lib/rubino/cli/config_command.rb +96 -0
- data/lib/rubino/cli/doctor_command.rb +251 -0
- data/lib/rubino/cli/jobs_command.rb +60 -0
- data/lib/rubino/cli/memory_command.rb +135 -0
- data/lib/rubino/cli/onboarding_wizard.rb +207 -0
- data/lib/rubino/cli/server_command.rb +139 -0
- data/lib/rubino/cli/session_command.rb +125 -0
- data/lib/rubino/cli/setup_command.rb +107 -0
- data/lib/rubino/cli/skills_command.rb +85 -0
- data/lib/rubino/cli/tools_command.rb +81 -0
- data/lib/rubino/cli/trust_gate.rb +71 -0
- data/lib/rubino/commands/built_ins.rb +46 -0
- data/lib/rubino/commands/command.rb +116 -0
- data/lib/rubino/commands/executor.rb +550 -0
- data/lib/rubino/commands/handlers/agents.rb +510 -0
- data/lib/rubino/commands/handlers/config.rb +88 -0
- data/lib/rubino/commands/handlers/help.rb +148 -0
- data/lib/rubino/commands/handlers/jobs.rb +71 -0
- data/lib/rubino/commands/handlers/mcp.rb +229 -0
- data/lib/rubino/commands/handlers/memory.rb +200 -0
- data/lib/rubino/commands/handlers/sessions.rb +207 -0
- data/lib/rubino/commands/handlers/skills.rb +195 -0
- data/lib/rubino/commands/handlers/status.rb +211 -0
- data/lib/rubino/commands/loader.rb +90 -0
- data/lib/rubino/config/configuration.rb +455 -0
- data/lib/rubino/config/defaults.rb +569 -0
- data/lib/rubino/config/loader.rb +115 -0
- data/lib/rubino/config/reasoning_prefs.rb +67 -0
- data/lib/rubino/config/writer.rb +72 -0
- data/lib/rubino/context/compressor.rb +149 -0
- data/lib/rubino/context/environment_inspector.rb +176 -0
- data/lib/rubino/context/file_discovery.rb +45 -0
- data/lib/rubino/context/message_boundary.rb +39 -0
- data/lib/rubino/context/prompt_assembler.rb +382 -0
- data/lib/rubino/context/summary_builder.rb +159 -0
- data/lib/rubino/context/token_budget.rb +68 -0
- data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
- data/lib/rubino/database/connection.rb +77 -0
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
- data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
- data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
- data/lib/rubino/database/migrator.rb +48 -0
- data/lib/rubino/documents/converters/csv.rb +79 -0
- data/lib/rubino/documents/converters/docx.rb +129 -0
- data/lib/rubino/documents/converters/html.rb +28 -0
- data/lib/rubino/documents/converters/json.rb +35 -0
- data/lib/rubino/documents/converters/pdf.rb +59 -0
- data/lib/rubino/documents/converters/plain.rb +68 -0
- data/lib/rubino/documents/converters/pptx.rb +64 -0
- data/lib/rubino/documents/converters/xlsx.rb +62 -0
- data/lib/rubino/documents/converters/xml.rb +45 -0
- data/lib/rubino/documents/html.rb +71 -0
- data/lib/rubino/documents/registry.rb +68 -0
- data/lib/rubino/documents/table.rb +63 -0
- data/lib/rubino/documents.rb +50 -0
- data/lib/rubino/errors.rb +119 -0
- data/lib/rubino/files/workspace.rb +93 -0
- data/lib/rubino/interaction/cancel_token.rb +43 -0
- data/lib/rubino/interaction/clipboard_image.rb +84 -0
- data/lib/rubino/interaction/event_bus.rb +48 -0
- data/lib/rubino/interaction/events.rb +101 -0
- data/lib/rubino/interaction/image_input.rb +127 -0
- data/lib/rubino/interaction/input_queue.rb +117 -0
- data/lib/rubino/interaction/lifecycle.rb +299 -0
- data/lib/rubino/interaction/probe.rb +65 -0
- data/lib/rubino/interaction/state.rb +56 -0
- data/lib/rubino/jobs/cron_job_repository.rb +75 -0
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
- data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
- data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
- data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
- data/lib/rubino/jobs/queue.rb +184 -0
- data/lib/rubino/jobs/registry.rb +45 -0
- data/lib/rubino/jobs/runner.rb +79 -0
- data/lib/rubino/jobs/scheduler.rb +138 -0
- data/lib/rubino/jobs/webhook_delivery.rb +225 -0
- data/lib/rubino/jobs/worker.rb +59 -0
- data/lib/rubino/llm/adapter_factory.rb +47 -0
- data/lib/rubino/llm/adapter_response.rb +65 -0
- data/lib/rubino/llm/auxiliary_client.rb +61 -0
- data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
- data/lib/rubino/llm/content_builder.rb +55 -0
- data/lib/rubino/llm/credential_check.rb +93 -0
- data/lib/rubino/llm/error_classifier.rb +364 -0
- data/lib/rubino/llm/fake_provider.rb +292 -0
- data/lib/rubino/llm/inline_think_filter.rb +58 -0
- data/lib/rubino/llm/model_catalog.rb +29 -0
- data/lib/rubino/llm/provider_resolver.rb +48 -0
- data/lib/rubino/llm/reasoning_manager.rb +100 -0
- data/lib/rubino/llm/request.rb +56 -0
- data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
- data/lib/rubino/llm/scenario_loader.rb +68 -0
- data/lib/rubino/llm/scenario_selector.rb +80 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
- data/lib/rubino/llm/scenarios/analysis.yml +501 -0
- data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
- data/lib/rubino/llm/scenarios/failure.yml +65 -0
- data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
- data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
- data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
- data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
- data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
- data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
- data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
- data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
- data/lib/rubino/llm/thinking_support.rb +84 -0
- data/lib/rubino/llm/tool_bridge.rb +89 -0
- data/lib/rubino/logger.rb +99 -0
- data/lib/rubino/mcp/manager.rb +180 -0
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
- data/lib/rubino/mcp.rb +57 -0
- data/lib/rubino/memory/backend.rb +104 -0
- data/lib/rubino/memory/backends/default.rb +101 -0
- data/lib/rubino/memory/backends/sqlite.rb +653 -0
- data/lib/rubino/memory/backends.rb +53 -0
- data/lib/rubino/memory/deduplicator.rb +74 -0
- data/lib/rubino/memory/extractor.rb +85 -0
- data/lib/rubino/memory/flusher.rb +31 -0
- data/lib/rubino/memory/retriever.rb +50 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
- data/lib/rubino/memory/sqlite_graph.rb +154 -0
- data/lib/rubino/memory/store.rb +228 -0
- data/lib/rubino/memory/threat_scanner.rb +68 -0
- data/lib/rubino/metrics.rb +175 -0
- data/lib/rubino/modes.rb +93 -0
- data/lib/rubino/oauth/connection_repository.rb +95 -0
- data/lib/rubino/oauth/provider/github.rb +75 -0
- data/lib/rubino/oauth/provider/google.rb +59 -0
- data/lib/rubino/oauth/provider.rb +149 -0
- data/lib/rubino/oauth/registry.rb +86 -0
- data/lib/rubino/oauth/token_encryptor.rb +87 -0
- data/lib/rubino/plugins/registry.rb +75 -0
- data/lib/rubino/plugins.rb +86 -0
- data/lib/rubino/run/approval_gate.rb +243 -0
- data/lib/rubino/run/attachment_downloader.rb +166 -0
- data/lib/rubino/run/event_store.rb +74 -0
- data/lib/rubino/run/executor.rb +383 -0
- data/lib/rubino/run/gate_registry.rb +39 -0
- data/lib/rubino/run/recorder.rb +69 -0
- data/lib/rubino/run/repository.rb +118 -0
- data/lib/rubino/run/session_approval_cache.rb +118 -0
- data/lib/rubino/security/allowlist_persister.rb +55 -0
- data/lib/rubino/security/approval_policy.rb +227 -0
- data/lib/rubino/security/command_allowlist.rb +24 -0
- data/lib/rubino/security/dangerous_patterns.rb +118 -0
- data/lib/rubino/security/deny_persister.rb +73 -0
- data/lib/rubino/security/doom_loop_detector.rb +43 -0
- data/lib/rubino/security/hardline_guard.rb +105 -0
- data/lib/rubino/security/pattern_matcher.rb +62 -0
- data/lib/rubino/security/prefix_deriver.rb +124 -0
- data/lib/rubino/security/readonly_commands.rb +211 -0
- data/lib/rubino/session/exporter.rb +101 -0
- data/lib/rubino/session/message.rb +77 -0
- data/lib/rubino/session/repository.rb +295 -0
- data/lib/rubino/session/store.rb +198 -0
- data/lib/rubino/session/summary_store.rb +65 -0
- data/lib/rubino/skills/prompt_index.rb +85 -0
- data/lib/rubino/skills/registry.rb +208 -0
- data/lib/rubino/skills/skill.rb +176 -0
- data/lib/rubino/skills/skill_tool.rb +215 -0
- data/lib/rubino/skills/state_repository.rb +37 -0
- data/lib/rubino/skills/toggle.rb +26 -0
- data/lib/rubino/tools/answer_child_tool.rb +83 -0
- data/lib/rubino/tools/ask_parent_tool.rb +232 -0
- data/lib/rubino/tools/attach_file_tool.rb +120 -0
- data/lib/rubino/tools/background_tasks.rb +520 -0
- data/lib/rubino/tools/base.rb +222 -0
- data/lib/rubino/tools/custom_tool_loader.rb +119 -0
- data/lib/rubino/tools/edit_tool.rb +122 -0
- data/lib/rubino/tools/git_tool.rb +71 -0
- data/lib/rubino/tools/github_tool.rb +233 -0
- data/lib/rubino/tools/glob_tool.rb +69 -0
- data/lib/rubino/tools/grep_tool.rb +206 -0
- data/lib/rubino/tools/memory_tool.rb +184 -0
- data/lib/rubino/tools/multi_edit_tool.rb +110 -0
- data/lib/rubino/tools/patch_tool.rb +260 -0
- data/lib/rubino/tools/probe_tool.rb +175 -0
- data/lib/rubino/tools/question_tool.rb +128 -0
- data/lib/rubino/tools/read_attachment_tool.rb +180 -0
- data/lib/rubino/tools/read_tool.rb +212 -0
- data/lib/rubino/tools/read_tracker.rb +98 -0
- data/lib/rubino/tools/registry.rb +166 -0
- data/lib/rubino/tools/result.rb +113 -0
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/session_search_tool.rb +103 -0
- data/lib/rubino/tools/shell_input_tool.rb +96 -0
- data/lib/rubino/tools/shell_kill_tool.rb +76 -0
- data/lib/rubino/tools/shell_output_tool.rb +72 -0
- data/lib/rubino/tools/shell_registry.rb +158 -0
- data/lib/rubino/tools/shell_tail_tool.rb +118 -0
- data/lib/rubino/tools/shell_tool.rb +330 -0
- data/lib/rubino/tools/steer_tool.rb +118 -0
- data/lib/rubino/tools/subagent_probe.rb +89 -0
- data/lib/rubino/tools/summarize_file_tool.rb +182 -0
- data/lib/rubino/tools/task_result_tool.rb +90 -0
- data/lib/rubino/tools/task_stop_tool.rb +80 -0
- data/lib/rubino/tools/task_tool.rb +622 -0
- data/lib/rubino/tools/test_tool.rb +454 -0
- data/lib/rubino/tools/todo_tool.rb +93 -0
- data/lib/rubino/tools/tool_call_repository.rb +33 -0
- data/lib/rubino/tools/vision_tool.rb +85 -0
- data/lib/rubino/tools/webfetch_tool.rb +153 -0
- data/lib/rubino/tools/websearch_tool.rb +179 -0
- data/lib/rubino/tools/write_tool.rb +61 -0
- data/lib/rubino/trust.rb +88 -0
- data/lib/rubino/ui/api.rb +296 -0
- data/lib/rubino/ui/base.rb +252 -0
- data/lib/rubino/ui/bottom_composer.rb +1599 -0
- data/lib/rubino/ui/cli.rb +1987 -0
- data/lib/rubino/ui/completion_menu.rb +321 -0
- data/lib/rubino/ui/completion_source.rb +284 -0
- data/lib/rubino/ui/escape_reader.rb +169 -0
- data/lib/rubino/ui/indented_io.rb +88 -0
- data/lib/rubino/ui/input_history.rb +108 -0
- data/lib/rubino/ui/live_region.rb +183 -0
- data/lib/rubino/ui/markdown_renderer.rb +506 -0
- data/lib/rubino/ui/notifier.rb +163 -0
- data/lib/rubino/ui/null.rb +195 -0
- data/lib/rubino/ui/paste_store.rb +176 -0
- data/lib/rubino/ui/printer_base.rb +79 -0
- data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
- data/lib/rubino/ui/queued_indicators.rb +66 -0
- data/lib/rubino/ui/status_bar.rb +100 -0
- data/lib/rubino/ui/stdout_proxy.rb +161 -0
- data/lib/rubino/ui/streaming_markdown.rb +186 -0
- data/lib/rubino/ui/subagent_cards.rb +134 -0
- data/lib/rubino/ui/subagent_view.rb +255 -0
- data/lib/rubino/ui.rb +21 -0
- data/lib/rubino/update_check.rb +187 -0
- data/lib/rubino/util/duration.rb +23 -0
- data/lib/rubino/util/hyperlink.rb +105 -0
- data/lib/rubino/util/output.rb +145 -0
- data/lib/rubino/util/secrets_mask.rb +83 -0
- data/lib/rubino/version.rb +5 -0
- data/lib/rubino/workspace.rb +85 -0
- data/lib/rubino-agent.rb +5 -0
- data/lib/rubino.rb +318 -0
- data/mise.toml +2 -0
- data/rubino-agent.gemspec +103 -0
- data/skills/ruby-expert/SKILL.md +67 -0
- data/skills/ruby-expert/references/concurrency.md +357 -0
- data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
- data/skills/ruby-expert/references/errors-and-types.md +460 -0
- data/skills/ruby-expert/references/gem-authoring.md +459 -0
- data/skills/ruby-expert/references/language-idioms.md +465 -0
- data/skills/ruby-expert/references/metaprogramming.md +339 -0
- data/skills/ruby-expert/references/oo-design.md +553 -0
- data/skills/ruby-expert/references/performance.md +383 -0
- data/skills/ruby-expert/references/rails.md +424 -0
- data/skills/ruby-expert/references/security.md +404 -0
- data/skills/ruby-expert/references/testing.md +473 -0
- data/skills/ruby-expert/references/tooling.md +466 -0
- metadata +856 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
From nothing to a working first answer in about five minutes. This is the happy path; the model/key decision is made interactively, not by hand-editing YAML.
|
|
4
|
+
|
|
5
|
+
## 1. Install
|
|
6
|
+
|
|
7
|
+
The fastest path on Linux (x86_64 / arm64) is the one-line installer. It installs a compatible Ruby via [`rv`](https://github.com/spinel-coop/rv), then the gem — all in user space, no sudo:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install.sh | bash
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Piping a script into your shell runs whatever it contains, so review it first if you like:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install.sh -o install.sh
|
|
17
|
+
less install.sh && bash install.sh
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The installer is idempotent (safe to re-run) and prints the exact `PATH` line for the `rubino` executable when it finishes.
|
|
21
|
+
|
|
22
|
+
**Already manage Ruby yourself?** Requirements are Ruby >= 3.1 and SQLite3; then:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
gem install rubino-agent
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Verify the binary is on your `PATH`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
rubino version
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
In a development checkout, prefix commands with `bundle exec` (`bundle exec rubino ...`).
|
|
35
|
+
|
|
36
|
+
## 2. Run the setup wizard
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
rubino setup
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`setup` creates the home directory (`~/.rubino`, mode `0700`), a default `config.yml` (`0600`), an `.env` template (`0600`), and initializes the SQLite database with all migrations. Then, **if no usable API key is configured and you're on a real terminal**, it launches the onboarding wizard:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Welcome to rubino — let's get you connected to a model.
|
|
46
|
+
No API key is configured yet. Pick a provider (or press Enter to skip).
|
|
47
|
+
|
|
48
|
+
1) OpenAI (GPT) — recommended default
|
|
49
|
+
2) MiniMax (Anthropic-compatible)
|
|
50
|
+
3) Anthropic (Claude)
|
|
51
|
+
4) Google (Gemini)
|
|
52
|
+
5) OpenAI-compatible gateway
|
|
53
|
+
Choose a provider [1-5, Enter to skip]: 1
|
|
54
|
+
Paste your OPENAI_API_KEY (input hidden; Enter to skip): ••••••••
|
|
55
|
+
Configured OpenAI (GPT) — recommended default with model gpt-4.1.
|
|
56
|
+
Saved to ~/.rubino/config.yml and ~/.rubino/.env.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
What the wizard does, exactly:
|
|
60
|
+
|
|
61
|
+
- Writes `model.default` and `model.provider` for the chosen provider into `config.yml`.
|
|
62
|
+
- Writes any provider block it needs (e.g. MiniMax sets `providers.minimax.base_url` + `anthropic_compatible: true` + `api_key: ${MINIMAX_API_KEY}`).
|
|
63
|
+
- Appends `KEY=value` to `~/.rubino/.env` (mode `0600`); the key is **never echoed back** and is exported into the current process so the very next message works.
|
|
64
|
+
- The **OpenAI-compatible gateway** option additionally asks for the gateway base URL.
|
|
65
|
+
|
|
66
|
+
The defaults written per provider:
|
|
67
|
+
|
|
68
|
+
| Choice | provider | default model | key var |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| OpenAI (default) | `openai` | `gpt-4.1` | `OPENAI_API_KEY` |
|
|
71
|
+
| MiniMax | `minimax` | `MiniMax-M2.7` | `MINIMAX_API_KEY` |
|
|
72
|
+
| Anthropic | `anthropic` | `claude-sonnet-4-5` | `ANTHROPIC_API_KEY` |
|
|
73
|
+
| Google | `google` | `gemini-2.5-pro` | `GEMINI_API_KEY` |
|
|
74
|
+
| OpenAI-compatible gateway | `gateway` | `auto` | `OPENAI_API_KEY` |
|
|
75
|
+
|
|
76
|
+
Press **Enter** at the provider prompt to skip. You can also configure things by hand — see [models-and-keys.md](models-and-keys.md).
|
|
77
|
+
|
|
78
|
+
## 3. Start chatting
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
rubino chat
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The first thing you see is a banner with the workspace, git branch, and model. The input line leads with a red `▍` rail and a clean `❯` caret; the dim status bar underneath shows the session mode, model, and context saturation. Then ask something:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
▍❯ what does this project do?
|
|
88
|
+
default · MiniMax-M3 · ctx ~0/128k
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> If you skipped the wizard during `setup`, a bare `rubino chat` re-runs it before the first turn (when on a TTY). If you're piping input or using `-q`, there's no prompt to run — instead you get a clear, actionable error telling you how to set a key (see below).
|
|
92
|
+
|
|
93
|
+
## 4. Make a first edit
|
|
94
|
+
|
|
95
|
+
Ask the agent to change something:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
▍❯ add a docstring to the top of lib/foo.rb
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
When the agent wants to run `shell` (or any approval-gated tool), it pauses and asks for your decision. Approve once, approve for the session, or deny — see [security.md](security.md) for the full approval model.
|
|
102
|
+
|
|
103
|
+
You can keep typing while the agent works: **Enter** interrupts the current turn and runs your line next; **Alt+Enter** (or `/queued <message>`) queues it to run after the turn finishes. See [commands.md](commands.md#typing-while-the-agent-is-working).
|
|
104
|
+
|
|
105
|
+
## 5. Exit and resume
|
|
106
|
+
|
|
107
|
+
Type `exit` (or `/exit`, Ctrl+D, or a double Ctrl+C) to end the session. On exit you get a resume hint:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
Resume with: rubino chat --resume "my session title"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Resuming:
|
|
114
|
+
|
|
115
|
+
- `rubino chat` — a **bare** interactive chat auto-resumes your most recent resumable session and replays its history.
|
|
116
|
+
- `rubino chat --new` — force a fresh session instead.
|
|
117
|
+
- `rubino chat --continue` (`-c`) — resume the most recent session explicitly.
|
|
118
|
+
- `rubino chat --resume <id|title>` (`-r`) — resume a specific session.
|
|
119
|
+
|
|
120
|
+
In-chat, `/sessions` lists recent sessions and resumes one in place, and `/new` starts a fresh one without leaving the REPL.
|
|
121
|
+
|
|
122
|
+
## If the first message fails
|
|
123
|
+
|
|
124
|
+
A brand-new user with no key used to see ~80 seconds of silent retries then an empty answer. That trap is fixed: the run now fails fast with guidance. In a non-interactive context (e.g. `rubino prompt "hi"` with no key) you'll see:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
No API key configured for provider 'openai' (model openai/gpt-4.1).
|
|
128
|
+
Set it up one of these ways:
|
|
129
|
+
• run `rubino setup` for a guided first-run setup, or
|
|
130
|
+
• add OPENAI_API_KEY=<your-key> to ~/.rubino/.env, or
|
|
131
|
+
• set providers.openai.api_key in ~/.rubino/config.yml.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
(The shipped default model `openai/gpt-4.1` resolves to OpenRouter in ruby_llm's registry; this is why a first run without a key or the right provider fails. The cleanest fix is `rubino setup`. See [models-and-keys.md](models-and-keys.md) and [troubleshooting.md](troubleshooting.md).)
|
|
135
|
+
|
|
136
|
+
Run `rubino doctor` at any time to check config, the resolved provider, credentials, and database health.
|
|
137
|
+
|
|
138
|
+
## Next steps
|
|
139
|
+
|
|
140
|
+
- [Models & keys](models-and-keys.md) — per-provider setup blocks and the default→OpenRouter note.
|
|
141
|
+
- [Commands](commands.md) — every CLI subcommand and slash command.
|
|
142
|
+
- [Configuration](configuration.md) — full reference, env vars, precedence.
|
|
143
|
+
- [Tools](tools.md) — what the agent can do and how each tool is gated.
|
data/docs/jobs.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Jobs
|
|
2
|
+
|
|
3
|
+
Background work in rubino is split into two surfaces that share the `Rubino::Jobs::*` namespace but operate independently:
|
|
4
|
+
|
|
5
|
+
1. **Internal background queue** — async side-effects the agent enqueues for itself (memory extraction, context compaction, session summarization, retention sweeps).
|
|
6
|
+
2. **Cron jobs** — user-defined schedules that fire fresh agent runs on a cron expression, with optional webhook delivery on completion. HTTP surface lives at [`/v1/jobs`](api/v1.md#cron-jobs).
|
|
7
|
+
|
|
8
|
+
This doc describes both, plus the **Backend Adapter contract** that the queue and the scheduler are designed to be plugged into so the gem can be hosted on Sidekiq / SolidQueue / GoodJob / ActiveJob without rewriting handlers.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Internal background queue
|
|
13
|
+
|
|
14
|
+
### Purpose
|
|
15
|
+
|
|
16
|
+
Defer slow or out-of-band work off the request and chat paths. Anything an agent doesn't need to block on — extracting memories from a finished turn, compacting a session that crossed the threshold, GC'ing ended sessions — goes through the queue.
|
|
17
|
+
|
|
18
|
+
### Storage (default `Sqlite` backend)
|
|
19
|
+
|
|
20
|
+
```sql
|
|
21
|
+
CREATE TABLE jobs (
|
|
22
|
+
id text PRIMARY KEY, -- uuid
|
|
23
|
+
type text NOT NULL, -- Jobs::Registry key
|
|
24
|
+
status text NOT NULL, -- queued | running | completed | failed | dead
|
|
25
|
+
priority integer NOT NULL, -- lower runs first
|
|
26
|
+
payload_json text NOT NULL, -- JSON-serialised hash
|
|
27
|
+
attempts integer NOT NULL,
|
|
28
|
+
max_attempts integer NOT NULL,
|
|
29
|
+
run_at text NOT NULL, -- iso8601, "not before"
|
|
30
|
+
locked_at text,
|
|
31
|
+
locked_by text, -- worker_id
|
|
32
|
+
last_error text,
|
|
33
|
+
created_at text NOT NULL,
|
|
34
|
+
updated_at text NOT NULL
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE job_runs (
|
|
38
|
+
id text PRIMARY KEY, -- uuid, one row per execution attempt
|
|
39
|
+
job_id text NOT NULL,
|
|
40
|
+
status text NOT NULL,
|
|
41
|
+
started_at text,
|
|
42
|
+
finished_at text,
|
|
43
|
+
error text
|
|
44
|
+
);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Lifecycle
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
enqueue ──► (status=queued) ──► dequeue (worker locks row)
|
|
51
|
+
│
|
|
52
|
+
▼
|
|
53
|
+
handler.perform(payload)
|
|
54
|
+
│
|
|
55
|
+
┌──────────┴───────────┐
|
|
56
|
+
▼ ▼
|
|
57
|
+
Queue#complete! Queue#fail!(error:)
|
|
58
|
+
status=completed attempts += 1
|
|
59
|
+
retry_at = now + backoff·attempts
|
|
60
|
+
status=queued (or dead at max_attempts)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Retry uses linear backoff: `retry_backoff_seconds * attempts`. After `max_attempts` the row stays at `status=dead` for inspection — nothing reaps it automatically.
|
|
64
|
+
|
|
65
|
+
### Execution modes
|
|
66
|
+
|
|
67
|
+
`config.jobs_mode` selects how enqueued jobs actually run:
|
|
68
|
+
|
|
69
|
+
| Mode | Behavior | When to use |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `inline` | `Queue#enqueue` runs the handler synchronously in the same call stack. A failed inline job is marked terminal (`failed`) rather than re-queued, since nothing drains it. | dev, tests, smoke runs |
|
|
72
|
+
| `manual` | enqueue only — nothing runs until `rubino jobs process` is invoked. | air-gapped or CI batch flows |
|
|
73
|
+
| `worker` | a long-running `rubino jobs worker` polls and dequeues. | production single-process |
|
|
74
|
+
|
|
75
|
+
The worker is a single-threaded poll loop with `SIGINT`/`SIGTERM` graceful stop. It is not safe to run more than one worker per SQLite file — locking is row-level but `WAL` contention will dominate. Multi-process scaling is what the [Backend Adapter](#backend-adapter-planned-design) section is for.
|
|
76
|
+
|
|
77
|
+
### Handler contract
|
|
78
|
+
|
|
79
|
+
A handler is any class that responds to `#perform(payload)`. The Registry maps a type string → handler class:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
module Rubino
|
|
83
|
+
module Jobs
|
|
84
|
+
module Handlers
|
|
85
|
+
class MyJob
|
|
86
|
+
def perform(payload)
|
|
87
|
+
# payload comes back as a symbol-keyed Hash
|
|
88
|
+
session_id = payload[:session_id]
|
|
89
|
+
# ...
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Rubino::Jobs::Registry.register("MyJob", Rubino::Jobs::Handlers::MyJob)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Enqueue:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
Rubino::Jobs::Queue.new.enqueue("MyJob", session_id: "abc")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Built-in handlers
|
|
106
|
+
|
|
107
|
+
| Type | Handler | Payload | Side-effect |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| `ExtractMemoryJob` | `Handlers::ExtractMemoryJob` | `{session_id}` | `Memory::Extractor#extract_from_session` |
|
|
110
|
+
| `CompactSessionJob` | `Handlers::CompactSessionJob` | `{session_id}` | `Context::Compressor#compact!` |
|
|
111
|
+
| `SummarizeSessionJob` | `Handlers::SummarizeSessionJob` | `{session_id}` | `Context::SummaryBuilder#build_and_save!` |
|
|
112
|
+
| `CleanupSessionsJob` | `Handlers::CleanupSessionsJob` | `{retention_days?}` | deletes `sessions` rows with `status="ended"` older than retention (default 30d) |
|
|
113
|
+
| `DistillSkillJob` | `Handlers::DistillSkillJob` | `{session_id}` | post-turn skill distillation — one aux-LLM call distils a tool-heavy turn into a reusable `SKILL.md` (gated on `skills.auto_distill`; see [skills.md](skills.md#creating-skills)) |
|
|
114
|
+
|
|
115
|
+
### CLI
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
rubino jobs list # show recent rows from jobs table
|
|
119
|
+
rubino jobs process # drain queued rows once (uses Runner#run_pending)
|
|
120
|
+
rubino jobs worker # start the polling worker (long-running)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
In an interactive chat, `/jobs` shows the same list (with status counts) and
|
|
124
|
+
`/jobs <id>` one job in full, including its last error — see
|
|
125
|
+
[commands.md](commands.md#jobs-in-chat-jobs). Running jobs stays CLI-only.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Cron jobs
|
|
130
|
+
|
|
131
|
+
User-defined cron schedules, persisted in the `cron_jobs` table, dispatched by `Jobs::Scheduler` — a process-wide singleton wrapping `rufus-scheduler`.
|
|
132
|
+
|
|
133
|
+
Each cron tick:
|
|
134
|
+
|
|
135
|
+
1. Creates a fresh `Session` (source=`"cron"`).
|
|
136
|
+
2. Creates a `Run` stamped with `cron_job_id`.
|
|
137
|
+
3. Hands the run to `Run::Executor#start`.
|
|
138
|
+
4. On completion: optionally posts a payload to `RUBINO_WEBHOOK_URL` when `deliver: "webhook"`.
|
|
139
|
+
|
|
140
|
+
Configuration is HTTP-only — there is no YAML loader for cron jobs. Full request/response shapes are in [`docs/api/v1.md`](api/v1.md#cron-jobs); the routes are:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
POST /v1/jobs # create
|
|
144
|
+
GET /v1/jobs # list
|
|
145
|
+
GET /v1/jobs/:id # show
|
|
146
|
+
PATCH /v1/jobs/:id # update
|
|
147
|
+
DELETE /v1/jobs/:id # delete + unschedule
|
|
148
|
+
POST /v1/jobs/:id/pause # disable + unschedule
|
|
149
|
+
POST /v1/jobs/:id/resume # enable + reschedule
|
|
150
|
+
POST /v1/jobs/:id/trigger # fire once now
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Scheduler boot
|
|
154
|
+
|
|
155
|
+
`Jobs::Scheduler.instance.load_all!` is called once at server boot. It loads every `enabled: true` row from `cron_jobs` and registers a rufus cron handle for each.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
scheduler = Rubino::Jobs::Scheduler.instance
|
|
159
|
+
scheduler.load_all! # at server boot
|
|
160
|
+
scheduler.schedule(job_row) # after POST /v1/jobs
|
|
161
|
+
scheduler.unschedule(job_id) # after DELETE
|
|
162
|
+
scheduler.trigger(job_id) # one-shot
|
|
163
|
+
scheduler.shutdown! # at server stop
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Webhook delivery
|
|
167
|
+
|
|
168
|
+
`Jobs::WebhookDelivery` is a thin Faraday + faraday-retry client. Best-effort: every failure path (no URL, non-2xx, transport error) is logged and counted, never raised.
|
|
169
|
+
|
|
170
|
+
| Setting | Value |
|
|
171
|
+
|---|---|
|
|
172
|
+
| URL | `RUBINO_WEBHOOK_URL` env (single URL for the whole process) |
|
|
173
|
+
| Timeout | 10s |
|
|
174
|
+
| Retry | 2 attempts, 0.5s initial interval, exponential backoff factor 2 |
|
|
175
|
+
| Retry triggers | `Faraday::TimeoutError`, `Faraday::ConnectionFailed` |
|
|
176
|
+
| Payload | `{ job_id, job_name, run_id, status, session_id }` |
|
|
177
|
+
| Metrics | `webhook_deliveries_total{outcome="ok"|"http_error"|"error"}` |
|
|
178
|
+
|
|
179
|
+
Per-job webhook URLs and signed payloads are planned.
|
|
180
|
+
|
|
181
|
+
### Multi-process limitation
|
|
182
|
+
|
|
183
|
+
Because rufus lives in the Ruby heap, **every Puma worker** would run **every cron tick** if you scaled `rubino server` horizontally. The scheduler currently ships as single-instance only. The [Backend Adapter](#backend-adapter-planned-design) section sketches what cluster-safe scheduling looks like.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Backend Adapter (planned design)
|
|
188
|
+
|
|
189
|
+
The internal queue today is hardwired to SQLite via Sequel. For real production deployments — multi-process Puma, k8s pods, heavy fanout — the natural move is to plug Sidekiq / SolidQueue / GoodJob / Resque underneath without rewriting any handler.
|
|
190
|
+
|
|
191
|
+
The contract below is **not implemented yet**. It is documented so that:
|
|
192
|
+
|
|
193
|
+
1. Today's `Jobs::Queue` keeps a stable public method set (`enqueue / find / list / pending_count`) that can become the adapter facade later.
|
|
194
|
+
2. Anyone writing a new handler today knows what *not* to depend on (DB queries on `:jobs`, transaction semantics, Sequel handles).
|
|
195
|
+
3. When the adapter lands, the migration is a config flip, not a rewrite.
|
|
196
|
+
|
|
197
|
+
### `Jobs::Backend` contract
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
module Rubino
|
|
201
|
+
module Jobs
|
|
202
|
+
# Single point of contact between Jobs::Queue (a thin facade once this
|
|
203
|
+
# ships) and whatever runs the work in production.
|
|
204
|
+
#
|
|
205
|
+
# Implementations MUST be thread-safe AND process-safe. The agent
|
|
206
|
+
# assumes nothing about how work is durably stored or dispatched —
|
|
207
|
+
# only that the four methods below behave per their contract.
|
|
208
|
+
module Backend
|
|
209
|
+
# Schedule a job for execution.
|
|
210
|
+
#
|
|
211
|
+
# @param type [String] handler type (matches Jobs::Registry key)
|
|
212
|
+
# @param payload [Hash] JSON-serialisable. No symbols on the wire,
|
|
213
|
+
# no Time objects, no Sequel rows.
|
|
214
|
+
# @param priority [Integer] lower runs first; semantics are best-effort.
|
|
215
|
+
# @param run_at [Time, nil] not-before timestamp; nil means ASAP.
|
|
216
|
+
# @return [String] backend-specific job id, round-trippable via #find.
|
|
217
|
+
def enqueue(type:, payload:, priority: 100, run_at: nil); end
|
|
218
|
+
|
|
219
|
+
# Look up a single job by the id returned from #enqueue.
|
|
220
|
+
# @return [Hash, nil] {id:, type:, status:, attempts:, last_error:, ...}
|
|
221
|
+
def find(id); end
|
|
222
|
+
|
|
223
|
+
# Snapshot for /v1/health and `rubino jobs list`.
|
|
224
|
+
# @return [Hash] e.g. {queued: 4, running: 1, dead: 0, completed: 132}
|
|
225
|
+
def stats; end
|
|
226
|
+
|
|
227
|
+
# Drain up to `limit` queued jobs in-process. Used by `inline` mode
|
|
228
|
+
# and tests. Adapters with no in-process executor (Sidekiq client-only
|
|
229
|
+
# mode) MAY raise NotImplementedError.
|
|
230
|
+
def drain!(limit: 100); end
|
|
231
|
+
|
|
232
|
+
# Optional. GC for completed/dead rows. Operators call this via
|
|
233
|
+
# `rubino jobs cleanup`. Implementations without a "completed"
|
|
234
|
+
# state (e.g. fire-and-forget transports) may no-op.
|
|
235
|
+
def purge_completed!(older_than:); end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Wire-up:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
Rubino.configure do |c|
|
|
245
|
+
c.jobs_backend = Rubino::Jobs::Backend::Sqlite.new # current default
|
|
246
|
+
# c.jobs_backend = MyApp::SidekiqAdapter.new # opt-in
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`Jobs::Queue.new` becomes a façade that delegates to `Rubino.configuration.jobs_backend`. No handler changes; no API changes; `inline` / `manual` / `worker` modes collapse into whatever the adapter exposes.
|
|
251
|
+
|
|
252
|
+
### Sketch adapters (NOT shipped yet)
|
|
253
|
+
|
|
254
|
+
Each row below shows the **only file** an integrator would have to write. Handlers stay the same Ruby object with `#perform(payload)`.
|
|
255
|
+
|
|
256
|
+
| Adapter | Underlying lib | Transport | Retry / DLQ | Cron |
|
|
257
|
+
|---|---|---|---|---|
|
|
258
|
+
| `Backend::Sqlite` | Sequel | SQLite table polling | linear backoff, `dead` status | rufus (in-process) |
|
|
259
|
+
| `Backend::Sidekiq` | sidekiq | Redis | sidekiq retry + DLQ | sidekiq-cron |
|
|
260
|
+
| `Backend::SolidQueue` | solid_queue | ActiveRecord (no Redis) | SolidQueue retry | SolidQueue recurring jobs |
|
|
261
|
+
| `Backend::GoodJob` | good_job | Postgres LISTEN/NOTIFY | GoodJob retry | GoodJob cron |
|
|
262
|
+
| `Backend::ActiveJob` | activejob | host app's queue_adapter | adapter-dependent | adapter-dependent |
|
|
263
|
+
|
|
264
|
+
Indicative shape (informational only):
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
# This file would live in the host application — NOT in the gem.
|
|
268
|
+
class MyApp::Jobs::SidekiqAdapter
|
|
269
|
+
def enqueue(type:, payload:, priority: 100, run_at: nil)
|
|
270
|
+
handler = Rubino::Jobs::Registry.handler_for(type) or raise "unknown type: #{type}"
|
|
271
|
+
# Wrap the handler once so Sidekiq has a class with `perform`.
|
|
272
|
+
job_class = MyApp::Jobs::RubinoSidekiqWrapper
|
|
273
|
+
args = [type, JSON.generate(payload)]
|
|
274
|
+
if run_at
|
|
275
|
+
job_class.perform_at(run_at, *args)
|
|
276
|
+
else
|
|
277
|
+
job_class.perform_async(*args)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
# find/stats/drain!/purge_completed! similarly
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The wrapper class on the Sidekiq side just dispatches:
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
class MyApp::Jobs::RubinoSidekiqWrapper
|
|
288
|
+
include Sidekiq::Job
|
|
289
|
+
def perform(type, payload_json)
|
|
290
|
+
payload = JSON.parse(payload_json, symbolize_names: true)
|
|
291
|
+
Rubino::Jobs::Registry.handler_for(type).new.perform(payload)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Cron scheduler adapter (parallel design)
|
|
297
|
+
|
|
298
|
+
Cron has the same shape. Today rufus is a singleton wrapping in-process callbacks; tomorrow a `Scheduler::Backend` decides whether the tick happens here or somewhere else.
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
module Rubino::Jobs::Scheduler::Backend
|
|
302
|
+
def schedule(job); end # job row from cron_jobs
|
|
303
|
+
def unschedule(id); end
|
|
304
|
+
def trigger(id); end # one-shot now
|
|
305
|
+
def load_all!; end # boot
|
|
306
|
+
def shutdown!; end
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
| Adapter | What ticks the cron |
|
|
311
|
+
|---|---|
|
|
312
|
+
| `Scheduler::Backend::Rufus` | rufus-scheduler in-process (current) |
|
|
313
|
+
| `Scheduler::Backend::SidekiqCron` | sidekiq-cron / sidekiq-scheduler |
|
|
314
|
+
| `Scheduler::Backend::SolidCron` | SolidQueue recurring jobs table |
|
|
315
|
+
| `Scheduler::Backend::Kubernetes` | host cluster's `CronJob` POSTs `/v1/jobs/:id/trigger` on schedule; rubino never schedules anything itself |
|
|
316
|
+
|
|
317
|
+
In every case the HTTP surface (`/v1/jobs`) does not change. Only **who actually fires the tick** changes.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Non-goals
|
|
322
|
+
|
|
323
|
+
- **Pluggable backends are designed, not shipped.** The default and only backend is `Sqlite`. The `Jobs::Backend` module above does not exist in the code yet — `Jobs::Queue` is the SQLite implementation directly.
|
|
324
|
+
- **No cluster-safe cron.** The rufus scheduler is single-instance only. Running more than one `rubino server` in front of the same DB will multi-fire every cron job.
|
|
325
|
+
- **No per-job webhook URLs**, no payload signing, no signed JWT-style retry tokens. One URL per process via `RUBINO_WEBHOOK_URL`.
|
|
326
|
+
- **No queue web UI.** `rubino jobs list` and `/v1/jobs` are the only inspection surfaces.
|
|
327
|
+
- **No fan-out, no per-tenant queues, no priority classes.** Priority is a single integer column, best-effort.
|
|
328
|
+
- **No automatic dead-row GC.** Failed-past-max-attempts rows stay at `status=dead` until an operator removes them.
|
|
329
|
+
|
|
330
|
+
## Why a contract, not just a config flag
|
|
331
|
+
|
|
332
|
+
The handler classes are the API. As long as `perform(payload)` is stable and idempotent, swapping the backend is a deploy concern, not a rewrite. Documenting the contract now (and keeping `Jobs::Queue`'s public method surface aligned with `Backend`) is what keeps "let's move to Sidekiq" a one-day task instead of a one-month refactor.
|
data/docs/mcp.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# MCP Integration
|
|
2
|
+
|
|
3
|
+
> **Status: EXPERIMENTAL.** stdio servers are wired end-to-end (connect at chat boot, tools registered, `doctor`/`tools`/in-chat `/mcp` surfaces). `sse`/`streamable` configs are forwarded to [ruby_llm-mcp](https://github.com/patvice/ruby_llm-mcp) but less battle-tested, and OAuth is **not implemented** on the rubino side (see below). Don't depend on it in production yet.
|
|
4
|
+
|
|
5
|
+
rubino supports the [Model Context Protocol](https://modelcontextprotocol.io/) via [ruby_llm-mcp](https://github.com/patvice/ruby_llm-mcp).
|
|
6
|
+
|
|
7
|
+
## Configuration
|
|
8
|
+
|
|
9
|
+
Configuring at least one server under `mcp.servers` in `config.yml` **is the opt-in** — there is no separate feature flag to flip. Set `mcp.enabled: false` to switch MCP off without deleting the server definitions.
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
mcp:
|
|
13
|
+
# enabled: false # optional kill switch; defaults to true when servers exist
|
|
14
|
+
servers:
|
|
15
|
+
# Local server via stdio
|
|
16
|
+
filesystem:
|
|
17
|
+
transport: stdio
|
|
18
|
+
command: "npx"
|
|
19
|
+
args: ["@modelcontextprotocol/server-filesystem", "/path/to/project"]
|
|
20
|
+
env:
|
|
21
|
+
DEBUG: "1"
|
|
22
|
+
|
|
23
|
+
# Remote server via SSE
|
|
24
|
+
remote_api:
|
|
25
|
+
transport: sse
|
|
26
|
+
url: "https://mcp.example.com/sse"
|
|
27
|
+
headers:
|
|
28
|
+
Authorization: "Bearer {env:MCP_TOKEN}"
|
|
29
|
+
|
|
30
|
+
# Remote server via streamable HTTP
|
|
31
|
+
streaming_api:
|
|
32
|
+
transport: streamable
|
|
33
|
+
url: "https://mcp.example.com/api"
|
|
34
|
+
timeout: 15000
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Transport Types
|
|
38
|
+
|
|
39
|
+
| Transport | Use Case | Config |
|
|
40
|
+
|-----------|----------|--------|
|
|
41
|
+
| `stdio` | Local MCP servers, CLI tools | `command`, `args`, `env` |
|
|
42
|
+
| `sse` | Web-based servers with Server-Sent Events | `url`, `headers` |
|
|
43
|
+
| `streamable` | HTTP servers with streaming support | `url`, `headers`, `oauth` |
|
|
44
|
+
|
|
45
|
+
## How It Works
|
|
46
|
+
|
|
47
|
+
1. At chat boot (and in `rubino tools`), `MCP::Manager` connects to all configured servers — best-effort: a server that fails to start prints a warning and is skipped, it never blocks the session
|
|
48
|
+
2. Each server's tools are wrapped in `MCPToolWrapper` (adapts to `Tools::Base` interface), forwarding the server-declared input schema so the model calls them with the right argument names
|
|
49
|
+
3. Wrapped tools are registered in `Tools::Registry` with a prefix (`servername_toolname`)
|
|
50
|
+
4. The agent can use MCP tools like any built-in tool; a failed MCP call (including a server-side argument rejection) surfaces as an `Error: …` tool result and renders ✗ like any failed built-in tool
|
|
51
|
+
5. `ruby_llm-mcp`'s own log lines (including everything a stdio server prints on its stderr) go to `<home>/logs/mcp.log`, never to stdout — one-shot `rubino prompt` output stays machine-readable
|
|
52
|
+
|
|
53
|
+
MCP tools are dynamic — they come from whatever servers you configure — so they are not part of the drift-checked built-in tool list in [tools.md](tools.md) and have no `tools.<key>` config gate; disable a server (`/mcp <server> off` for the session, or set `mcp.enabled: false`) to remove its tools.
|
|
54
|
+
|
|
55
|
+
## Per-Agent Scoping
|
|
56
|
+
|
|
57
|
+
Control which MCP servers each agent can access in `config.yml`:
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
agents:
|
|
61
|
+
explore:
|
|
62
|
+
mcp_servers: ["filesystem"] # Only filesystem MCP
|
|
63
|
+
build:
|
|
64
|
+
mcp_servers: all # All MCP servers (default)
|
|
65
|
+
plan:
|
|
66
|
+
mcp_servers: [] # No MCP tools
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
An agent with no `mcp_servers` key sees every server. The YAML string `all` is normalized to `:all`. The scoping is enforced in `Agent::Definition#resolved_tools` — the single seam every consumer of an agent's tool set (chat lifecycle, prompt assembler) goes through — so a scoped agent's model request simply does not contain the out-of-scope servers' tool definitions.
|
|
70
|
+
|
|
71
|
+
In code (an explicit value here wins over config):
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
Rubino::Agent::Definition.new(
|
|
75
|
+
name: "secure_agent",
|
|
76
|
+
mcp_servers: ["internal_api"] # Only this server's tools
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Authentication
|
|
81
|
+
|
|
82
|
+
Remote-server credentials are passed through config: use `headers` (e.g. `Authorization: "Bearer {env:MCP_TOKEN}"`) or the server process `env` for stdio servers.
|
|
83
|
+
|
|
84
|
+
An `oauth` hash on a `streamable` server is forwarded verbatim to `ruby_llm-mcp` — rubino itself implements **no** OAuth flow: there is no PKCE/browser handshake and no rubino-side token storage (no `~/.rubino/oauth_tokens.json`). Whatever OAuth behavior you get is whatever your installed `ruby_llm-mcp` version provides; treat it as not yet supported.
|
|
85
|
+
|
|
86
|
+
## Managing from Chat
|
|
87
|
+
|
|
88
|
+
`/mcp` is the in-chat management surface ([commands.md](commands.md#mcp-servers-mcp)):
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
/mcp # server list: name, transport, reachability, tool count
|
|
92
|
+
/mcp <server> # drill-in: transport + command/url, health, registered tools, last start error
|
|
93
|
+
/mcp <server> off # stop the client and deregister its tools for this session
|
|
94
|
+
/mcp <server> on # (re)start the client and register its tools
|
|
95
|
+
/mcp reload # re-read config.yml and reconnect every server (no chat restart needed)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`off`/`on` are session-scoped — config is untouched. `/mcp reload` is how a server added to `config.yml` mid-session becomes usable. When servers are configured, `/status` includes an `mcp` line (`2 servers · 1 reachable · 14 tools`).
|
|
99
|
+
|
|
100
|
+
## Manual Management
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# Start all servers
|
|
104
|
+
manager = Rubino::MCP::Manager.new
|
|
105
|
+
manager.start_all!
|
|
106
|
+
|
|
107
|
+
# Get tools for a specific agent (mcp_servers scoping applied)
|
|
108
|
+
tools = agent_definition.resolved_tools
|
|
109
|
+
|
|
110
|
+
# Health check
|
|
111
|
+
manager.health_check
|
|
112
|
+
# => [{ name: "filesystem", alive: true }, { name: "api", alive: false }]
|
|
113
|
+
|
|
114
|
+
# Stop a server
|
|
115
|
+
manager.stop_server("filesystem")
|
|
116
|
+
|
|
117
|
+
# Stop all
|
|
118
|
+
manager.stop_all!
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## CLI
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
rubino doctor # "Optional (MCP servers, experimental)" section: per-server reachability.
|
|
125
|
+
# Informational only — an unreachable MCP server never fails doctor.
|
|
126
|
+
rubino tools # "MCP Tools (experimental)" section: prefixed servername_toolname rows
|
|
127
|
+
# per server, after the built-in table.
|
|
128
|
+
```
|