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
data/install.sh
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# rubino installer
|
|
4
|
+
#
|
|
5
|
+
# curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install.sh | bash
|
|
6
|
+
#
|
|
7
|
+
# What it does (all in user space, no sudo):
|
|
8
|
+
# 1. Provisions a Ruby toolchain:
|
|
9
|
+
# - Linux: via `rv` (https://github.com/spinel-coop/rv), a fast Ruby
|
|
10
|
+
# version manager that fetches a precompiled Ruby (no build step).
|
|
11
|
+
# - macOS: if Homebrew is present you're asked whether to use Homebrew
|
|
12
|
+
# (`brew install ruby`) or rv; if Homebrew is absent it uses rv directly.
|
|
13
|
+
# 2. Installs the `rubino-agent` gem under that Ruby. If a published gem with
|
|
14
|
+
# the CLI isn't available yet, it falls back to building from this repo.
|
|
15
|
+
# 3. Prints the exact PATH line for the `rubino` executable.
|
|
16
|
+
#
|
|
17
|
+
# Non-interactive override: set RUBINO_INSTALL_METHOD=brew|rv to skip the prompt.
|
|
18
|
+
#
|
|
19
|
+
# Security note: you are piping a script from the internet into a shell.
|
|
20
|
+
# Review it first: curl -fsSL <url> -o install.sh && less install.sh && bash install.sh
|
|
21
|
+
#
|
|
22
|
+
# Re-running is safe: every step is idempotent.
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
# --- configuration ----------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
REPO_OWNER="Jhonnyr97"
|
|
29
|
+
REPO_NAME="rubino-agent"
|
|
30
|
+
REPO_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}.git"
|
|
31
|
+
REPO_RAW="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main"
|
|
32
|
+
|
|
33
|
+
# Ruby to install via rv. Matches the gem's .ruby-version; the gem itself
|
|
34
|
+
# requires >= 3.1.0, so any 3.1+ works, but we pin a known-good precompiled one.
|
|
35
|
+
# (Homebrew installs its current `ruby` formula instead; the gem supports both.)
|
|
36
|
+
RUBY_VERSION="${RUBINO_RUBY_VERSION:-3.3.3}"
|
|
37
|
+
|
|
38
|
+
# The gem name on RubyGems (rubino-agent) vs. the executable it ships (rubino).
|
|
39
|
+
GEM_NAME="rubino-agent"
|
|
40
|
+
BIN_NAME="rubino"
|
|
41
|
+
|
|
42
|
+
# Optional: brew | rv. When unset on macOS with Homebrew present, we prompt.
|
|
43
|
+
INSTALL_METHOD="${RUBINO_INSTALL_METHOD:-}"
|
|
44
|
+
|
|
45
|
+
# --- output helpers ---------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
if [ -t 1 ]; then
|
|
48
|
+
BOLD=$(printf '\033[1m'); GREEN=$(printf '\033[32m'); YELLOW=$(printf '\033[33m')
|
|
49
|
+
RED=$(printf '\033[31m'); DIM=$(printf '\033[2m'); RESET=$(printf '\033[0m')
|
|
50
|
+
else
|
|
51
|
+
BOLD=""; GREEN=""; YELLOW=""; RED=""; DIM=""; RESET=""
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
info() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*"; }
|
|
55
|
+
ok() { printf '%s==>%s %s\n' "$GREEN" "$RESET" "$*"; }
|
|
56
|
+
warn() { printf '%s==>%s %s\n' "$YELLOW" "$RESET" "$*" >&2; }
|
|
57
|
+
die() { printf '%serror:%s %s\n' "$RED" "$RESET" "$*" >&2; exit 1; }
|
|
58
|
+
|
|
59
|
+
# --- preflight: OS / arch ---------------------------------------------------
|
|
60
|
+
|
|
61
|
+
OS="$(uname -s)"
|
|
62
|
+
ARCH="$(uname -m)"
|
|
63
|
+
|
|
64
|
+
case "$OS" in
|
|
65
|
+
Linux) PLATFORM="linux" ;;
|
|
66
|
+
Darwin) PLATFORM="macos" ;;
|
|
67
|
+
*) die "unsupported OS: ${OS}. rubino's installer supports Linux and macOS (x86_64/arm64)." ;;
|
|
68
|
+
esac
|
|
69
|
+
|
|
70
|
+
case "$ARCH" in
|
|
71
|
+
x86_64|amd64) ;;
|
|
72
|
+
aarch64|arm64) ;;
|
|
73
|
+
*) die "unsupported architecture: ${ARCH}. Supported: x86_64, arm64." ;;
|
|
74
|
+
esac
|
|
75
|
+
|
|
76
|
+
need() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1 (please install it and re-run)"; }
|
|
77
|
+
need curl
|
|
78
|
+
need uname
|
|
79
|
+
|
|
80
|
+
# --- choose install method (macOS may use Homebrew or rv) -------------------
|
|
81
|
+
|
|
82
|
+
# Decide how we get Ruby. Linux always uses rv. macOS: honor an explicit
|
|
83
|
+
# RUBINO_INSTALL_METHOD; else if Homebrew is present, ask (when a terminal is
|
|
84
|
+
# available); else fall back to rv. The prompt reads from /dev/tty so it works
|
|
85
|
+
# even under `curl ... | bash`, where stdin is the script itself.
|
|
86
|
+
choose_method() {
|
|
87
|
+
if [ "$PLATFORM" = "linux" ]; then
|
|
88
|
+
printf 'rv\n'; return 0
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
case "$INSTALL_METHOD" in
|
|
92
|
+
brew) printf 'brew\n'; return 0 ;;
|
|
93
|
+
rv) printf 'rv\n'; return 0 ;;
|
|
94
|
+
"") ;;
|
|
95
|
+
*) die "RUBINO_INSTALL_METHOD must be 'brew' or 'rv' (got '${INSTALL_METHOD}')." ;;
|
|
96
|
+
esac
|
|
97
|
+
|
|
98
|
+
if ! command -v brew >/dev/null 2>&1; then
|
|
99
|
+
# No Homebrew → rv directly, as requested.
|
|
100
|
+
printf 'rv\n'; return 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Homebrew present. Ask, if we have a terminal to ask on.
|
|
104
|
+
if [ -r /dev/tty ] && [ -w /dev/tty ]; then
|
|
105
|
+
{
|
|
106
|
+
printf '\n%sHomebrew detected.%s How should Ruby be installed?\n' "$BOLD" "$RESET"
|
|
107
|
+
printf ' %s1)%s Homebrew %s(brew install ruby)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
108
|
+
printf ' %s2)%s rv %s(fast, self-contained, no Homebrew)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
109
|
+
printf 'Choose %s[1/2]%s (default 1): ' "$BOLD" "$RESET"
|
|
110
|
+
} >/dev/tty
|
|
111
|
+
local ans=""
|
|
112
|
+
read -r ans </dev/tty || ans=""
|
|
113
|
+
case "$ans" in
|
|
114
|
+
2|rv|RV) printf 'rv\n' ;;
|
|
115
|
+
""|1|brew) printf 'brew\n' ;;
|
|
116
|
+
*) printf 'brew\n' ;;
|
|
117
|
+
esac
|
|
118
|
+
return 0
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Homebrew present but no terminal to prompt on → default to Homebrew
|
|
122
|
+
# (the native macOS expectation). Override with RUBINO_INSTALL_METHOD=rv.
|
|
123
|
+
warn "Homebrew detected but no interactive terminal; defaulting to Homebrew. Set RUBINO_INSTALL_METHOD=rv to use rv instead."
|
|
124
|
+
printf 'brew\n'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
METHOD="$(choose_method)"
|
|
128
|
+
|
|
129
|
+
# `rubyx <cmd...>` runs a command (gem/bundle/rake/ruby) under the Ruby we set
|
|
130
|
+
# up, regardless of method. Each setup_* defines it plus RUBY_LABEL.
|
|
131
|
+
rubyx() { die "internal: ruby toolchain not initialized"; }
|
|
132
|
+
|
|
133
|
+
# --- ruby toolchain: rv -----------------------------------------------------
|
|
134
|
+
|
|
135
|
+
setup_ruby_rv() {
|
|
136
|
+
# rv's installer (cargo-dist) drops the binary in $CARGO_HOME/bin or
|
|
137
|
+
# $HOME/.cargo/bin. We pass RV_NO_MODIFY_PATH=1 so it doesn't edit the user's
|
|
138
|
+
# shell rc, and locate the binary ourselves.
|
|
139
|
+
locate_rv() {
|
|
140
|
+
if command -v rv >/dev/null 2>&1; then command -v rv; return 0; fi
|
|
141
|
+
for d in "${CARGO_HOME:-}/bin" "$HOME/.cargo/bin" "$HOME/.local/bin"; do
|
|
142
|
+
[ -n "$d" ] && [ -x "$d/rv" ] && { printf '%s\n' "$d/rv"; return 0; }
|
|
143
|
+
done
|
|
144
|
+
return 1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
local rv_bin
|
|
148
|
+
if rv_bin="$(locate_rv)"; then
|
|
149
|
+
ok "rv already installed: ${rv_bin}"
|
|
150
|
+
else
|
|
151
|
+
info "Installing rv (fast Ruby version manager)..."
|
|
152
|
+
RV_NO_MODIFY_PATH=1 curl -fsSL https://rv.dev/install | sh
|
|
153
|
+
rv_bin="$(locate_rv)" || die "rv install completed but the rv binary wasn't found on PATH or in ~/.cargo/bin."
|
|
154
|
+
ok "Installed rv: ${rv_bin}"
|
|
155
|
+
fi
|
|
156
|
+
export PATH="$(dirname "$rv_bin"):${PATH}"
|
|
157
|
+
|
|
158
|
+
info "Installing Ruby ${RUBY_VERSION} via rv (precompiled, no build step)..."
|
|
159
|
+
"$rv_bin" ruby install "${RUBY_VERSION}" # idempotent
|
|
160
|
+
local ruby_bin
|
|
161
|
+
ruby_bin="$("$rv_bin" ruby find "${RUBY_VERSION}")"
|
|
162
|
+
[ -x "$ruby_bin" ] || die "rv reported Ruby ${RUBY_VERSION} installed but its ruby binary wasn't found."
|
|
163
|
+
RUBY_BIN_DIR="$(dirname "$ruby_bin")"
|
|
164
|
+
RUBY_LABEL="Ruby ${RUBY_VERSION} (rv)"
|
|
165
|
+
|
|
166
|
+
rubyx() { "$rv_bin" run --ruby "${RUBY_VERSION}" "$@"; }
|
|
167
|
+
ok "${RUBY_LABEL} ready: ${RUBY_BIN_DIR}"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# --- ruby toolchain: Homebrew ----------------------------------------------
|
|
171
|
+
|
|
172
|
+
setup_ruby_brew() {
|
|
173
|
+
need brew
|
|
174
|
+
if brew list --formula ruby >/dev/null 2>&1; then
|
|
175
|
+
ok "Homebrew Ruby already installed."
|
|
176
|
+
else
|
|
177
|
+
info "Installing Ruby via Homebrew (brew install ruby)..."
|
|
178
|
+
brew install ruby || die "brew install ruby failed."
|
|
179
|
+
fi
|
|
180
|
+
local prefix
|
|
181
|
+
prefix="$(brew --prefix ruby 2>/dev/null)" || die "could not resolve 'brew --prefix ruby'."
|
|
182
|
+
RUBY_BIN_DIR="${prefix}/bin"
|
|
183
|
+
[ -x "${RUBY_BIN_DIR}/ruby" ] || die "Homebrew ruby not found at ${RUBY_BIN_DIR}."
|
|
184
|
+
local ver
|
|
185
|
+
ver="$("${RUBY_BIN_DIR}/ruby" -e 'print RUBY_VERSION' 2>/dev/null || echo '?')"
|
|
186
|
+
RUBY_LABEL="Ruby ${ver} (Homebrew)"
|
|
187
|
+
|
|
188
|
+
# Run gem/bundle/rake from Homebrew's keg-only ruby bin without relinking.
|
|
189
|
+
rubyx() { PATH="${RUBY_BIN_DIR}:${PATH}" "$@"; }
|
|
190
|
+
ok "${RUBY_LABEL} ready: ${RUBY_BIN_DIR}"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
info "Detected ${OS} ${ARCH}. Installing rubino via ${METHOD}."
|
|
194
|
+
|
|
195
|
+
case "$METHOD" in
|
|
196
|
+
rv) setup_ruby_rv ;;
|
|
197
|
+
brew) setup_ruby_brew ;;
|
|
198
|
+
*) die "internal: unknown method '${METHOD}'." ;;
|
|
199
|
+
esac
|
|
200
|
+
|
|
201
|
+
# Where gem-installed executables land for the chosen Ruby. `gem environment
|
|
202
|
+
# gembindir` is correct for both rv and Homebrew; fall back to the ruby bin dir.
|
|
203
|
+
GEM_BIN_DIR="$(rubyx gem environment gembindir 2>/dev/null | tail -n1)" || GEM_BIN_DIR=""
|
|
204
|
+
[ -n "${GEM_BIN_DIR:-}" ] && [ -d "$GEM_BIN_DIR" ] || GEM_BIN_DIR="$RUBY_BIN_DIR"
|
|
205
|
+
|
|
206
|
+
# --- install the rubino gem -------------------------------------------------
|
|
207
|
+
|
|
208
|
+
gem_bin_present() { [ -x "${GEM_BIN_DIR}/${BIN_NAME}" ]; }
|
|
209
|
+
|
|
210
|
+
install_published() {
|
|
211
|
+
info "Trying published gem: gem install ${GEM_NAME}..."
|
|
212
|
+
if rubyx gem install "${GEM_NAME}" >/dev/null 2>&1; then
|
|
213
|
+
if gem_bin_present; then
|
|
214
|
+
ok "Installed ${GEM_NAME} from RubyGems."
|
|
215
|
+
return 0
|
|
216
|
+
fi
|
|
217
|
+
warn "A '${GEM_NAME}' gem was installed but it doesn't provide the '${BIN_NAME}' CLI; building from source instead."
|
|
218
|
+
rubyx gem uninstall "${GEM_NAME}" -aIx >/dev/null 2>&1 || true
|
|
219
|
+
fi
|
|
220
|
+
return 1
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
install_from_git() {
|
|
224
|
+
warn "Building ${GEM_NAME} from ${REPO_URL} (the CLI gem isn't on RubyGems yet)."
|
|
225
|
+
need git
|
|
226
|
+
local work
|
|
227
|
+
work="$(mktemp -d)"
|
|
228
|
+
trap 'rm -rf "$work"' RETURN
|
|
229
|
+
git clone --depth 1 "$REPO_URL" "$work/${REPO_NAME}" >/dev/null 2>&1 \
|
|
230
|
+
|| die "git clone of ${REPO_URL} failed."
|
|
231
|
+
(
|
|
232
|
+
cd "$work/${REPO_NAME}"
|
|
233
|
+
info "Resolving dependencies (bundle install)..."
|
|
234
|
+
rubyx bundle install >/dev/null 2>&1 || die "bundle install failed."
|
|
235
|
+
info "Building the gem (rake build)..."
|
|
236
|
+
rubyx rake build >/dev/null 2>&1 || die "rake build failed."
|
|
237
|
+
local pkg
|
|
238
|
+
pkg="$(ls -1 pkg/${GEM_NAME}-*.gem 2>/dev/null | head -n1)"
|
|
239
|
+
[ -n "$pkg" ] || die "rake build produced no gem in pkg/."
|
|
240
|
+
info "Installing ${pkg}..."
|
|
241
|
+
rubyx gem install "$pkg" >/dev/null 2>&1 || die "gem install of the built package failed."
|
|
242
|
+
)
|
|
243
|
+
gem_bin_present || die "built and installed ${GEM_NAME} but the '${BIN_NAME}' executable is missing."
|
|
244
|
+
ok "Installed ${GEM_NAME} from source."
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if gem_bin_present; then
|
|
248
|
+
CURRENT_VER="$("${GEM_BIN_DIR}/${BIN_NAME}" version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)"
|
|
249
|
+
ok "${BIN_NAME} ${CURRENT_VER:+v$CURRENT_VER }is already installed (re-run safe)."
|
|
250
|
+
elif ! install_published; then
|
|
251
|
+
install_from_git
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
# --- PATH guidance + success ------------------------------------------------
|
|
255
|
+
|
|
256
|
+
printf '\n'
|
|
257
|
+
ok "rubino installed (${RUBY_LABEL})."
|
|
258
|
+
printf '\n'
|
|
259
|
+
|
|
260
|
+
if command -v "${BIN_NAME}" >/dev/null 2>&1 && [ "$(command -v "${BIN_NAME}")" = "${GEM_BIN_DIR}/${BIN_NAME}" ]; then
|
|
261
|
+
PATH_OK=1
|
|
262
|
+
else
|
|
263
|
+
PATH_OK=0
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
if [ "$PATH_OK" -ne 1 ]; then
|
|
267
|
+
printf '%sAdd this line to your shell profile%s (~/.bashrc, ~/.zshrc, ~/.profile):\n' "$BOLD" "$RESET"
|
|
268
|
+
printf '\n %sexport PATH="%s:$PATH"%s\n\n' "$DIM" "${GEM_BIN_DIR}" "$RESET"
|
|
269
|
+
printf 'Then open a new shell (or run the export above) so %s%s%s is on your PATH.\n\n' "$BOLD" "${BIN_NAME}" "$RESET"
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
printf '%sNext step:%s\n\n' "$BOLD" "$RESET"
|
|
273
|
+
printf ' %s%s setup%s %s# guided first-run: pick a provider, paste a key%s\n\n' "$GREEN" "${BIN_NAME}" "$RESET" "$DIM" "$RESET"
|
|
274
|
+
|
|
275
|
+
printf 'Run: %s%s setup%s\n' "$BOLD" "${BIN_NAME}" "$RESET"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
# In-process switch holding the ONE skill the user has pinned active for the
|
|
5
|
+
# session (MVP: one at a time). Mirrors Rubino::Modes: a process-level slot,
|
|
6
|
+
# set via `/skills <name>` (the completion-dropdown picker) and cleared via
|
|
7
|
+
# `/skills none`. The active skill is force-loaded into the system prompt each
|
|
8
|
+
# turn (Context::PromptAssembler), so the model actually uses it — not just a
|
|
9
|
+
# cosmetic chip.
|
|
10
|
+
#
|
|
11
|
+
# Lives at the process level intentionally — alpha rule: no premature
|
|
12
|
+
# persistence. A fresh `rubino chat` boots with NO active skill; an explicit
|
|
13
|
+
# `/skills <name>` takes effect for the rest of that process. We can move it
|
|
14
|
+
# onto Session later if users want it sticky across restarts.
|
|
15
|
+
#
|
|
16
|
+
# The sentinel "none" (and the `✗ none` dropdown entry) clears the slot.
|
|
17
|
+
module ActiveSkill
|
|
18
|
+
# The dropdown/CLI sentinel that clears the active skill.
|
|
19
|
+
NONE = "none"
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# The active skill name (String), or nil when none is pinned.
|
|
23
|
+
attr_reader :current
|
|
24
|
+
|
|
25
|
+
# Pins +name+ as the active skill. A nil/empty/"none" clears it. Returns
|
|
26
|
+
# the new value (the name String, or nil when cleared). The caller is
|
|
27
|
+
# responsible for validating the name against the registry BEFORE calling
|
|
28
|
+
# this — ActiveSkill is a dumb slot, like Modes.
|
|
29
|
+
def set(name)
|
|
30
|
+
normalized = name.to_s.strip
|
|
31
|
+
@current = normalized.empty? || normalized.casecmp?(NONE) ? nil : normalized
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Clears the active skill (the `/skills none` / `✗ none` path).
|
|
35
|
+
def clear
|
|
36
|
+
@current = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# True when a skill is pinned.
|
|
40
|
+
def active?
|
|
41
|
+
!@current.nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Test/teardown hook. Not part of the public API.
|
|
45
|
+
def reset!
|
|
46
|
+
@current = nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module Agent
|
|
5
|
+
# Registry of all defined agents (primary, sub, utility).
|
|
6
|
+
# Default agent system prompts are stored in agent/prompts/*.txt so they
|
|
7
|
+
# can be edited without modifying Ruby source.
|
|
8
|
+
class AgentRegistry
|
|
9
|
+
PROMPTS_DIR = File.expand_path("prompts", __dir__)
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@agents = {}
|
|
13
|
+
register_defaults!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns all primary agents.
|
|
17
|
+
def primary_agents
|
|
18
|
+
@agents.values.select(&:primary?)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns all visible subagents (for @mention).
|
|
22
|
+
def subagents
|
|
23
|
+
@agents.values.select { |a| a.subagent? && !a.hidden? }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns all agents including hidden.
|
|
27
|
+
def all
|
|
28
|
+
@agents.values
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Finds an agent by name.
|
|
32
|
+
def find(name)
|
|
33
|
+
@agents[name.to_s]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Registers a custom agent definition.
|
|
37
|
+
def register(definition)
|
|
38
|
+
@agents[definition.name] = definition
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the default primary agent.
|
|
42
|
+
def default
|
|
43
|
+
find("build") || primary_agents.first
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def register_defaults!
|
|
49
|
+
register(Definition.new(
|
|
50
|
+
name: "build",
|
|
51
|
+
type: :primary,
|
|
52
|
+
description: "Full-access development agent with all tools",
|
|
53
|
+
system_prompt: load_prompt("build"),
|
|
54
|
+
tools: :all
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
register(Definition.new(
|
|
58
|
+
name: "plan",
|
|
59
|
+
type: :primary,
|
|
60
|
+
description: "Read-only analysis and planning agent",
|
|
61
|
+
system_prompt: load_prompt("plan"),
|
|
62
|
+
tools: :read_only,
|
|
63
|
+
permissions: { "edit *" => "ask", "shell *" => "ask" }
|
|
64
|
+
))
|
|
65
|
+
|
|
66
|
+
register(Definition.new(
|
|
67
|
+
name: "explore",
|
|
68
|
+
type: :subagent,
|
|
69
|
+
description: "Fast read-only codebase exploration",
|
|
70
|
+
system_prompt: load_prompt("explore"),
|
|
71
|
+
tools: :read_only,
|
|
72
|
+
max_turns: 20
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
register(Definition.new(
|
|
76
|
+
name: "general",
|
|
77
|
+
type: :subagent,
|
|
78
|
+
description: "General-purpose agent for complex multi-step tasks",
|
|
79
|
+
system_prompt: load_prompt("general"),
|
|
80
|
+
tools: :all,
|
|
81
|
+
max_turns: 50
|
|
82
|
+
))
|
|
83
|
+
|
|
84
|
+
register(Definition.new(
|
|
85
|
+
name: "compaction",
|
|
86
|
+
type: :utility,
|
|
87
|
+
description: "Compresses long contexts",
|
|
88
|
+
system_prompt: load_prompt("compaction"),
|
|
89
|
+
hidden: true,
|
|
90
|
+
tools: []
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
register(Definition.new(
|
|
94
|
+
name: "title",
|
|
95
|
+
type: :utility,
|
|
96
|
+
description: "Generates session titles",
|
|
97
|
+
system_prompt: "Generate a concise title (max 6 words) for this conversation based on the first user message.",
|
|
98
|
+
hidden: true,
|
|
99
|
+
tools: []
|
|
100
|
+
))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Loads a prompt for a role. Checks the customer config for an
|
|
104
|
+
# explicit override first (prompts.overrides.<role>) and falls back
|
|
105
|
+
# to the built-in agent/prompts/<role>.txt. Missing files resolve
|
|
106
|
+
# to an empty string so a stripped-down distribution doesn't crash
|
|
107
|
+
# the registry at boot.
|
|
108
|
+
def load_prompt(name)
|
|
109
|
+
override = Rubino.configuration.prompts_override_for(name)
|
|
110
|
+
return override if override
|
|
111
|
+
|
|
112
|
+
path = File.join(PROMPTS_DIR, "#{name}.txt")
|
|
113
|
+
File.exist?(path) ? File.read(path).strip : ""
|
|
114
|
+
rescue StandardError
|
|
115
|
+
path = File.join(PROMPTS_DIR, "#{name}.txt")
|
|
116
|
+
File.exist?(path) ? File.read(path).strip : ""
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module Agent
|
|
5
|
+
# Jittered exponential backoff for retries, a faithful port of the
|
|
6
|
+
# reference jittered_backoff:
|
|
7
|
+
#
|
|
8
|
+
# delay = min(base * 2^(attempt-1), max)
|
|
9
|
+
# result = delay + uniform(0, jitter_ratio * delay) # jitter_ratio = 0.5
|
|
10
|
+
#
|
|
11
|
+
# Jitter decorrelates concurrent retries so multiple sessions hitting the
|
|
12
|
+
# same rate-limited provider don't all retry at the same instant.
|
|
13
|
+
#
|
|
14
|
+
# Deviation from the reference (intentional): the reference
|
|
15
|
+
# seeds a fresh RNG from a process-global monotonic counter + time on every
|
|
16
|
+
# call to stay decorrelated across threads with a coarse clock. We use
|
|
17
|
+
# Ruby's `rand`, whose Mersenne-Twister default RNG is already per-process
|
|
18
|
+
# and well-distributed — no global counter, no lock, less code. The
|
|
19
|
+
# decorrelation property (jitter spread over [0, 0.5*delay]) is preserved.
|
|
20
|
+
#
|
|
21
|
+
# Two presets mirror the conversation loop's two backoff sites:
|
|
22
|
+
# * INVALID_RESPONSE — base 5s, cap 120s
|
|
23
|
+
# * ERROR_PATH — base 2s, cap 60s
|
|
24
|
+
class BackoffPolicy
|
|
25
|
+
JITTER_RATIO = 0.5
|
|
26
|
+
|
|
27
|
+
# Preset = [base_delay, max_delay] in seconds.
|
|
28
|
+
INVALID_RESPONSE = { base: 5.0, max: 120.0 }.freeze
|
|
29
|
+
ERROR_PATH = { base: 2.0, max: 60.0 }.freeze
|
|
30
|
+
|
|
31
|
+
# Retry-After header values larger than this are clamped, matching the
|
|
32
|
+
# reference 2-minute cap.
|
|
33
|
+
RETRY_AFTER_CAP = 120.0
|
|
34
|
+
|
|
35
|
+
# cancel_token: an Interaction::CancelToken (or anything answering #check!)
|
|
36
|
+
# so a backoff wait aborts promptly on Ctrl+C instead of blocking for the
|
|
37
|
+
# full delay. Optional — nil means a plain (still sliced) sleep.
|
|
38
|
+
def initialize(cancel_token: nil)
|
|
39
|
+
@cancel_token = cancel_token
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Jittered delay in seconds for a 1-based attempt. `base`/`max` default to
|
|
43
|
+
# the error-path preset; pass a preset hash's values for the other site.
|
|
44
|
+
def jittered(attempt, base: ERROR_PATH[:base], max: ERROR_PATH[:max])
|
|
45
|
+
exponent = [0, attempt - 1].max
|
|
46
|
+
delay = base <= 0 || exponent >= 63 ? max : [base * (2**exponent), max].min
|
|
47
|
+
delay + (rand * JITTER_RATIO * delay)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The wait to honour for a retry. When the upstream sent a Retry-After we
|
|
51
|
+
# respect it (clamped to RETRY_AFTER_CAP), exactly as the reference does on the
|
|
52
|
+
# rate-limited path; otherwise fall back
|
|
53
|
+
# to the jittered backoff.
|
|
54
|
+
def wait_seconds(attempt, base:, max:, retry_after: nil)
|
|
55
|
+
ra = parse_retry_after(retry_after)
|
|
56
|
+
return [ra, RETRY_AFTER_CAP].min if ra
|
|
57
|
+
|
|
58
|
+
jittered(attempt, base: base, max: max)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Sleep `seconds`, sliced into 100ms ticks, polling the cancel token
|
|
62
|
+
# between ticks so Ctrl+C aborts within ~100ms instead of blocking the
|
|
63
|
+
# whole wait. On cancel, CancelToken#check! raises Interrupted. Mirrors
|
|
64
|
+
# the adapter's former cancellable_sleep and the reference incremental sleep
|
|
65
|
+
# loop.
|
|
66
|
+
def sleep(seconds)
|
|
67
|
+
deadline = monotonic_now + seconds
|
|
68
|
+
while (remaining = deadline - monotonic_now).positive?
|
|
69
|
+
@cancel_token&.check!
|
|
70
|
+
Kernel.sleep([0.1, remaining].min)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Pull a Retry-After value from a raw header value (String/Numeric) or a
|
|
75
|
+
# typed error carrying a Faraday response. Returns Float seconds or nil.
|
|
76
|
+
#
|
|
77
|
+
# NOTE: only the delta-seconds form (e.g. "30") is parsed. The HTTP-date
|
|
78
|
+
# form of Retry-After is not handled — no provider this gem targets sends
|
|
79
|
+
# it, and the reference likewise only parses the numeric form. TODO: handle the
|
|
80
|
+
# date form if a provider ever needs it.
|
|
81
|
+
def parse_retry_after(value)
|
|
82
|
+
return if value.nil?
|
|
83
|
+
|
|
84
|
+
raw =
|
|
85
|
+
if value.is_a?(Numeric) || value.is_a?(String)
|
|
86
|
+
value
|
|
87
|
+
else
|
|
88
|
+
retry_after_header(value)
|
|
89
|
+
end
|
|
90
|
+
return if raw.nil?
|
|
91
|
+
|
|
92
|
+
f = Float(raw, exception: false)
|
|
93
|
+
f if f&.positive?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Reach a Retry-After header off a typed error's Faraday response, if
|
|
99
|
+
# present. ruby_llm wraps the Faraday::Response on the error (#response),
|
|
100
|
+
# whose #headers is a case-insensitive hash. Returns nil when unreachable.
|
|
101
|
+
def retry_after_header(error)
|
|
102
|
+
return unless error.respond_to?(:response)
|
|
103
|
+
|
|
104
|
+
response = error.response
|
|
105
|
+
headers = response.respond_to?(:headers) ? response.headers : nil
|
|
106
|
+
return unless headers.respond_to?(:[])
|
|
107
|
+
|
|
108
|
+
headers["retry-after"] || headers["Retry-After"]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def monotonic_now
|
|
112
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module Agent
|
|
5
|
+
# Defines an agent type with its own model, system prompt, permissions, and tools.
|
|
6
|
+
# Agents can be primary (user-facing) or subagents (invokable by other agents).
|
|
7
|
+
class Definition
|
|
8
|
+
attr_reader :name, :type, :model, :system_prompt, :description,
|
|
9
|
+
:permissions, :tools, :hidden
|
|
10
|
+
|
|
11
|
+
# Types: :primary (user-switchable), :subagent (invokable), :utility (hidden)
|
|
12
|
+
TYPES = %i[primary subagent utility].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(attrs = {})
|
|
15
|
+
@name = attrs[:name]
|
|
16
|
+
@type = attrs[:type] || :primary
|
|
17
|
+
@model = attrs[:model]
|
|
18
|
+
@system_prompt = attrs[:system_prompt]
|
|
19
|
+
@description = attrs[:description] || ""
|
|
20
|
+
@permissions = attrs[:permissions] || {}
|
|
21
|
+
@mcp_servers = attrs[:mcp_servers] # :all or array of server names
|
|
22
|
+
@tools = attrs[:tools] || :all # :all, :read_only, or array of tool names
|
|
23
|
+
@hidden = attrs[:hidden] || false
|
|
24
|
+
@max_turns = attrs[:max_turns]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def primary?
|
|
28
|
+
@type == :primary
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def subagent?
|
|
32
|
+
@type == :subagent
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def utility?
|
|
36
|
+
@type == :utility
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def hidden?
|
|
40
|
+
@hidden
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Which MCP servers this agent may use: :all, or an array of server
|
|
44
|
+
# names. An explicit value passed in code wins; otherwise the
|
|
45
|
+
# `agents.<name>.mcp_servers` block in config.yml applies (#92), and
|
|
46
|
+
# absent both the agent sees every server. YAML has no symbols, so the
|
|
47
|
+
# literal string "all" from config normalizes to :all — the value
|
|
48
|
+
# #resolved_tools compares against.
|
|
49
|
+
def mcp_servers
|
|
50
|
+
return @mcp_servers if @mcp_servers
|
|
51
|
+
|
|
52
|
+
configured = Rubino.configuration.dig("agents", name.to_s, "mcp_servers")
|
|
53
|
+
case configured
|
|
54
|
+
when Array then configured.map(&:to_s)
|
|
55
|
+
else :all
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns the max turns for this agent (falls back to global config)
|
|
60
|
+
def max_turns
|
|
61
|
+
@max_turns || Rubino.configuration.agent_max_turns
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns the resolved model (falls back to global default)
|
|
65
|
+
def resolved_model
|
|
66
|
+
@model || Rubino.configuration.model_default
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns tool list based on the agent's tool configuration.
|
|
70
|
+
#
|
|
71
|
+
# Scoped nesting (S1): a subagent now KEEPS the delegation tools (`task` and
|
|
72
|
+
# its companions `task_result`/`task_stop`) so it can spawn its own
|
|
73
|
+
# subagents. Runaway recursion / fan-out is no longer prevented by hiding
|
|
74
|
+
# the tool here — it is bounded in ONE place, Tools::BackgroundTasks#reserve,
|
|
75
|
+
# by the depth / per-owner / global caps. (DELEGATION_TOOLS is kept as a
|
|
76
|
+
# named set for any reader that still wants to reason about the group.)
|
|
77
|
+
DELEGATION_TOOLS = %w[task task_result task_stop].freeze
|
|
78
|
+
|
|
79
|
+
# Tools that ONLY make sense for a subagent and must be hidden from a
|
|
80
|
+
# primary/top-level agent. ask_parent escalates a question to the PARENT — a
|
|
81
|
+
# top-level agent has no parent, so exposing it there would be a dead tool.
|
|
82
|
+
# Subagents keep it; everyone else drops it. This is the single enforcement
|
|
83
|
+
# point and is UNCHANGED by S1 (re-enabling nesting does not expose
|
|
84
|
+
# ask_parent to top-level agents).
|
|
85
|
+
SUBAGENT_ONLY_TOOLS = %w[ask_parent].freeze
|
|
86
|
+
|
|
87
|
+
def resolved_tools
|
|
88
|
+
tools =
|
|
89
|
+
case @tools
|
|
90
|
+
when :all
|
|
91
|
+
Tools::Registry.enabled_tools
|
|
92
|
+
when :read_only
|
|
93
|
+
Tools::Registry.enabled_tools.select { |t| t.risk_level == :low }
|
|
94
|
+
when Array
|
|
95
|
+
@tools.filter_map { |name| Tools::Registry.find(name) }
|
|
96
|
+
else
|
|
97
|
+
Tools::Registry.enabled_tools
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Per-agent MCP scoping (#92/#173): every consumer of this agent's tool
|
|
101
|
+
# set (Lifecycle#load_tools, prompt assembler) goes through here, so
|
|
102
|
+
# filtering MCP wrappers HERE is what actually keeps an out-of-scope
|
|
103
|
+
# server's tools away from the model.
|
|
104
|
+
tools = reject_unscoped_mcp_tools(tools)
|
|
105
|
+
|
|
106
|
+
# ask_parent is subagent-only; a primary/top-level agent has no parent.
|
|
107
|
+
# Nesting is otherwise allowed for everyone — the delegation tools stay.
|
|
108
|
+
if subagent?
|
|
109
|
+
tools
|
|
110
|
+
else
|
|
111
|
+
tools.reject { |t| SUBAGENT_ONLY_TOOLS.include?(t.name) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# Drops MCPToolWrapper instances whose server is not in this agent's
|
|
118
|
+
# mcp_servers allowlist (:all keeps everything). Built-in tools pass
|
|
119
|
+
# through untouched.
|
|
120
|
+
def reject_unscoped_mcp_tools(tools)
|
|
121
|
+
allowed = mcp_servers
|
|
122
|
+
return tools if allowed == :all
|
|
123
|
+
|
|
124
|
+
tools.reject { |t| t.is_a?(MCP::MCPToolWrapper) && !allowed.include?(t.server_name.to_s) }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|