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