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,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module Config
|
|
5
|
+
# Single source of truth for how reasoning/thinking preferences are resolved
|
|
6
|
+
# from config, shared by the LLM adapter gate and the CLI render path so they
|
|
7
|
+
# can never drift.
|
|
8
|
+
#
|
|
9
|
+
# Two orthogonal knobs:
|
|
10
|
+
# * display.reasoning — how reasoning is RENDERED (hidden | collapsed | full)
|
|
11
|
+
# * thinking.effort — how HARD the model thinks (off | low | medium | high)
|
|
12
|
+
#
|
|
13
|
+
# display.show_reasoning (legacy boolean) maps in for back-compat ONLY when
|
|
14
|
+
# display.reasoning is unset: true→full, false→hidden.
|
|
15
|
+
module ReasoningPrefs
|
|
16
|
+
RENDER_MODES = %i[hidden collapsed full].freeze
|
|
17
|
+
DEFAULT_MODE = :collapsed
|
|
18
|
+
|
|
19
|
+
EFFORTS = %i[off low medium high].freeze
|
|
20
|
+
DEFAULT_EFFORT = :medium
|
|
21
|
+
|
|
22
|
+
# Effort → Anthropic thinking-token budget. off disables thinking (0).
|
|
23
|
+
EFFORT_BUDGETS = {
|
|
24
|
+
off: 0,
|
|
25
|
+
low: 4_000,
|
|
26
|
+
medium: 8_000,
|
|
27
|
+
high: 16_000
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# The render mode symbol for a config object. Prefers display.reasoning;
|
|
33
|
+
# falls back to the legacy display.show_reasoning boolean; else the default.
|
|
34
|
+
def mode(config)
|
|
35
|
+
raw = config&.dig("display", "reasoning")
|
|
36
|
+
sym = raw.to_s.strip.downcase.to_sym unless raw.nil?
|
|
37
|
+
return sym if RENDER_MODES.include?(sym)
|
|
38
|
+
|
|
39
|
+
legacy = config&.dig("display", "show_reasoning")
|
|
40
|
+
return :full if legacy == true
|
|
41
|
+
return :hidden if legacy == false
|
|
42
|
+
|
|
43
|
+
DEFAULT_MODE
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The effort symbol for a config object, or nil when thinking.effort is
|
|
47
|
+
# unset (so callers can fall back to the existing thinking_budget chain).
|
|
48
|
+
def effort(config)
|
|
49
|
+
raw = config&.dig("thinking", "effort")
|
|
50
|
+
return nil if raw.nil?
|
|
51
|
+
# YAML parses an unquoted `off` as the boolean false, which used to
|
|
52
|
+
# silently break thinking-budget gating (#79) — coerce it back to :off
|
|
53
|
+
# here, the single read boundary, so a doc-following config keeps working.
|
|
54
|
+
return :off if raw == false
|
|
55
|
+
|
|
56
|
+
sym = raw.to_s.strip.downcase.to_sym
|
|
57
|
+
EFFORTS.include?(sym) ? sym : nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Token budget for an effort symbol (nil/unknown → nil so the caller can
|
|
61
|
+
# fall back to its own default chain).
|
|
62
|
+
def effort_budget(effort)
|
|
63
|
+
EFFORT_BUDGETS[effort]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Rubino
|
|
7
|
+
module Config
|
|
8
|
+
# Writes configuration changes back to the YAML file.
|
|
9
|
+
class Writer
|
|
10
|
+
def initialize(config_path:)
|
|
11
|
+
@config_path = config_path
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Sets a single key (dot-notation) to a value and persists
|
|
15
|
+
def set(key_path, value)
|
|
16
|
+
raw = load_raw
|
|
17
|
+
keys = key_path.split(".")
|
|
18
|
+
hash = raw
|
|
19
|
+
|
|
20
|
+
keys[0..-2].each_with_index do |k, i|
|
|
21
|
+
hash[k] ||= {}
|
|
22
|
+
hash = hash[k]
|
|
23
|
+
next if hash.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
traversed = keys[0..i].join(".")
|
|
26
|
+
raise ConfigurationError,
|
|
27
|
+
"cannot set '#{key_path}': '#{traversed}' is a scalar value, not a section"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
hash[keys.last] = coerce_value(value)
|
|
31
|
+
save(raw)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the value at a dot-notation key path
|
|
35
|
+
def get(key_path)
|
|
36
|
+
raw = load_raw
|
|
37
|
+
keys = key_path.split(".")
|
|
38
|
+
# A scalar intermediate node (e.g. a String) has no #dig; treat such a
|
|
39
|
+
# path as "not found" rather than crashing with a TypeError.
|
|
40
|
+
raw.dig(*keys)
|
|
41
|
+
rescue TypeError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def load_raw
|
|
48
|
+
if File.exist?(@config_path)
|
|
49
|
+
YAML.safe_load_file(@config_path, permitted_classes: [Symbol]) || {}
|
|
50
|
+
else
|
|
51
|
+
{}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def save(raw)
|
|
56
|
+
FileUtils.mkdir_p(File.dirname(@config_path))
|
|
57
|
+
File.write(@config_path, raw.to_yaml)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def coerce_value(value)
|
|
61
|
+
case value
|
|
62
|
+
when "true" then true
|
|
63
|
+
when "false" then false
|
|
64
|
+
when "nil", "null" then nil
|
|
65
|
+
when /\A\d+\z/ then value.to_i
|
|
66
|
+
when /\A\d+\.\d+\z/ then value.to_f
|
|
67
|
+
else value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module Context
|
|
7
|
+
# Orchestrates context compaction: flush memory, split messages into
|
|
8
|
+
# head/middle/tail, generate summary, create child session.
|
|
9
|
+
class Compressor
|
|
10
|
+
def initialize(session_id:, config: nil, db: nil)
|
|
11
|
+
@session_id = session_id
|
|
12
|
+
@config = config || Rubino.configuration
|
|
13
|
+
@db = db || Rubino.database.db
|
|
14
|
+
@message_store = Session::Store.new(db: @db)
|
|
15
|
+
@session_repo = Session::Repository.new(db: @db)
|
|
16
|
+
@summary_store = Session::SummaryStore.new(db: @db)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Performs full compaction and returns metadata
|
|
20
|
+
def compact!
|
|
21
|
+
session = @session_repo.find(@session_id)
|
|
22
|
+
raise CompactionError, "Session not found: #{@session_id}" unless session
|
|
23
|
+
|
|
24
|
+
messages = @message_store.for_session(@session_id)
|
|
25
|
+
return no_op_result if messages.size < minimum_messages
|
|
26
|
+
|
|
27
|
+
# 1. Flush memory before compaction
|
|
28
|
+
flush_memory!
|
|
29
|
+
|
|
30
|
+
# 2. Split messages into head / middle / tail
|
|
31
|
+
boundary = MessageBoundary.new(messages: messages, config: @config)
|
|
32
|
+
head = boundary.head
|
|
33
|
+
middle = boundary.middle
|
|
34
|
+
tail = boundary.tail
|
|
35
|
+
|
|
36
|
+
return no_op_result if middle.empty?
|
|
37
|
+
|
|
38
|
+
# 3. Sanitize tool pairs in middle
|
|
39
|
+
if @config.compression_preserve_tool_pairs?
|
|
40
|
+
sanitizer = ToolPairSanitizer.new
|
|
41
|
+
middle = sanitizer.sanitize(middle)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 4. Load previous summary (capture id now, before the insert below
|
|
45
|
+
# overwrites "latest" — the lineage link must point at the prior row)
|
|
46
|
+
previous = @summary_store.latest(@session_id)
|
|
47
|
+
previous_summary = previous&.dig(:content)
|
|
48
|
+
previous_summary_id = previous&.dig(:id)
|
|
49
|
+
|
|
50
|
+
# 5. Generate new summary
|
|
51
|
+
summary_builder = SummaryBuilder.new(session_id: @session_id)
|
|
52
|
+
new_summary = summary_builder.build(
|
|
53
|
+
messages: middle,
|
|
54
|
+
previous_summary: previous_summary
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 6. Save summary (chains parent_summary_id to the previous row)
|
|
58
|
+
summary_id = @summary_store.insert(session_id: @session_id, content: new_summary)
|
|
59
|
+
|
|
60
|
+
# 7. Create child session with compacted context
|
|
61
|
+
child_session = create_child_session(session, head, new_summary, tail)
|
|
62
|
+
|
|
63
|
+
# 8. Record compaction lineage
|
|
64
|
+
record_compaction(
|
|
65
|
+
source_id: @session_id,
|
|
66
|
+
target_id: child_session[:id],
|
|
67
|
+
previous_summary_id: previous_summary_id,
|
|
68
|
+
new_summary_id: summary_id,
|
|
69
|
+
original_tokens: estimate_tokens(messages),
|
|
70
|
+
compacted_tokens: estimate_tokens(head + tail)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
source_session_id: @session_id,
|
|
75
|
+
target_session_id: child_session[:id],
|
|
76
|
+
original_messages: messages.size,
|
|
77
|
+
compacted_messages: head.size + tail.size + 1, # +1 for summary
|
|
78
|
+
saved_tokens: estimate_tokens(middle),
|
|
79
|
+
summary_id: summary_id
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def flush_memory!
|
|
86
|
+
flusher = Memory::Flusher.new
|
|
87
|
+
flusher.flush_before_compaction!(@session_id)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def create_child_session(parent_session, head, summary, tail)
|
|
91
|
+
child = @session_repo.create(
|
|
92
|
+
source: "compaction",
|
|
93
|
+
model: parent_session[:model],
|
|
94
|
+
provider: parent_session[:provider],
|
|
95
|
+
title: parent_session[:title],
|
|
96
|
+
parent_session_id: parent_session[:id]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Copy head messages — faithful copy preserves metadata[:tool_calls]
|
|
100
|
+
# and token_count, otherwise compaction strips the assistant toolUse
|
|
101
|
+
# block and orphans the matching tool result (400 on resume).
|
|
102
|
+
@message_store.copy_into(child[:id], head)
|
|
103
|
+
|
|
104
|
+
# Insert summary as system message
|
|
105
|
+
@message_store.create(
|
|
106
|
+
session_id: child[:id],
|
|
107
|
+
role: "system",
|
|
108
|
+
content: "[Compacted Summary]\n#{summary}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Copy tail messages (same faithful copy as head)
|
|
112
|
+
@message_store.copy_into(child[:id], tail)
|
|
113
|
+
|
|
114
|
+
# End the parent session
|
|
115
|
+
@session_repo.update(parent_session[:id], status: "compacted")
|
|
116
|
+
|
|
117
|
+
child
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def record_compaction(source_id:, target_id:, previous_summary_id:, new_summary_id:,
|
|
121
|
+
original_tokens:, compacted_tokens:)
|
|
122
|
+
@db[:compactions].insert(
|
|
123
|
+
id: SecureRandom.uuid,
|
|
124
|
+
source_session_id: source_id,
|
|
125
|
+
target_session_id: target_id,
|
|
126
|
+
previous_summary_id: previous_summary_id,
|
|
127
|
+
new_summary_id: new_summary_id,
|
|
128
|
+
original_token_count: original_tokens,
|
|
129
|
+
compacted_token_count: compacted_tokens,
|
|
130
|
+
saved_token_count: original_tokens - compacted_tokens,
|
|
131
|
+
created_at: Time.now.utc.iso8601
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def estimate_tokens(messages)
|
|
136
|
+
total = messages.sum { |m| (m.respond_to?(:content) ? m.content : m[:content] || "").length }
|
|
137
|
+
(total / 4.0).ceil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def minimum_messages
|
|
141
|
+
@config.compression_protect_first_n + @config.compression_protect_last_n + 5
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def no_op_result
|
|
145
|
+
{ source_session_id: @session_id, saved_tokens: 0, skipped: true }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module Rubino
|
|
7
|
+
module Context
|
|
8
|
+
# Builds the "[Environment]" block injected into every system prompt.
|
|
9
|
+
#
|
|
10
|
+
# Probes the host once per process for the static bits (OS, ruby/python
|
|
11
|
+
# versions, external utilities on PATH) and asks for the dynamic bits
|
|
12
|
+
# (date, cwd, git branch) on every build. The static cache survives the
|
|
13
|
+
# length of the process — long enough for an HTTP server's lifetime,
|
|
14
|
+
# short enough that a `gem install` between deploys repopulates it.
|
|
15
|
+
#
|
|
16
|
+
# The goal is a concrete, honest description of the *actual* runtime the
|
|
17
|
+
# model is talking to. If markitdown isn't installed in the VM image, we
|
|
18
|
+
# don't list it — the agent will then ask the user instead of confidently
|
|
19
|
+
# invoking a binary that doesn't exist.
|
|
20
|
+
class EnvironmentInspector
|
|
21
|
+
# External CLI tools we probe by default. The list mixes hard
|
|
22
|
+
# dependencies (git, ruby) and useful-but-optional binaries the agent
|
|
23
|
+
# may want to shell out to (markitdown, pandoc, …). Anything not
|
|
24
|
+
# found on PATH is silently dropped — see #available_utilities.
|
|
25
|
+
DEFAULT_UTILITIES = %w[
|
|
26
|
+
git gh rg jq curl wget
|
|
27
|
+
ruby python3 node npm bundle
|
|
28
|
+
docker psql sqlite3 redis-cli
|
|
29
|
+
ffmpeg pandoc markitdown pdftotext tesseract
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# Process-wide cache of the static fields. Reset via #reset_cache!
|
|
34
|
+
# from specs.
|
|
35
|
+
def cache
|
|
36
|
+
@cache ||= {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reset_cache!
|
|
40
|
+
@cache = {}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(extra_utilities: [], cwd: nil, clock: -> { Time.now })
|
|
45
|
+
@extra_utilities = Array(extra_utilities).map(&:to_s)
|
|
46
|
+
@cwd = cwd
|
|
47
|
+
@clock = clock
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the assembled [Environment] block, or nil if the caller
|
|
51
|
+
# disabled it at the config layer (PromptAssembler decides — this
|
|
52
|
+
# class always renders when asked).
|
|
53
|
+
def render
|
|
54
|
+
lines = []
|
|
55
|
+
lines << "[Environment]"
|
|
56
|
+
lines << "- Today's date: #{today}"
|
|
57
|
+
lines << "- Platform: #{platform}"
|
|
58
|
+
lines << "- Shell: #{shell}"
|
|
59
|
+
lines << "- Working dir: #{working_dir}"
|
|
60
|
+
git = git_description
|
|
61
|
+
lines << "- Git: #{git}" if git
|
|
62
|
+
lines << "- Runtimes: #{runtimes}"
|
|
63
|
+
utilities = available_utilities
|
|
64
|
+
lines << "- Available CLI tools on PATH: #{utilities.join(", ")}" if utilities.any?
|
|
65
|
+
docs = document_formats
|
|
66
|
+
if docs.any?
|
|
67
|
+
lines << "- Document reading: the `read_attachment` tool converts these formats " \
|
|
68
|
+
"to Markdown in-process (no external binary needed): #{docs.join(", ")}"
|
|
69
|
+
end
|
|
70
|
+
lines.join("\n")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The CORE document formats readable in-process via read_attachment
|
|
74
|
+
# (driven by which optional extraction gems loaded). Advertised so the
|
|
75
|
+
# model knows it can read a docx/pdf even when no `markitdown` binary
|
|
76
|
+
# exists on PATH -- closing the gap this file's own comment describes.
|
|
77
|
+
def document_formats
|
|
78
|
+
self.class.cache[:document_formats] ||= begin
|
|
79
|
+
Rubino::Documents::Registry.available_formats
|
|
80
|
+
rescue StandardError
|
|
81
|
+
[]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Public for spec inspection. The list is sorted to keep the prompt
|
|
86
|
+
# stable turn-to-turn (otherwise reordering would invalidate the
|
|
87
|
+
# provider-side prompt cache).
|
|
88
|
+
def available_utilities
|
|
89
|
+
probes = (DEFAULT_UTILITIES + @extra_utilities).uniq
|
|
90
|
+
self.class.cache[:utilities] ||= probes.select { |bin| on_path?(bin) }.sort
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def today
|
|
96
|
+
@clock.call.strftime("%Y-%m-%d")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def platform
|
|
100
|
+
self.class.cache[:platform] ||= begin
|
|
101
|
+
host_os = RbConfig::CONFIG["host_os"]
|
|
102
|
+
arch = RbConfig::CONFIG["host_cpu"]
|
|
103
|
+
os_name =
|
|
104
|
+
case host_os
|
|
105
|
+
when /darwin/ then "macOS"
|
|
106
|
+
when /linux/ then linux_distro || "Linux"
|
|
107
|
+
when /mswin|mingw|cygwin/ then "Windows"
|
|
108
|
+
else host_os
|
|
109
|
+
end
|
|
110
|
+
"#{os_name} (#{arch})"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def linux_distro
|
|
115
|
+
return nil unless File.readable?("/etc/os-release")
|
|
116
|
+
|
|
117
|
+
pretty = File.read("/etc/os-release").lines.find { |l| l.start_with?("PRETTY_NAME=") }
|
|
118
|
+
pretty&.split("=", 2)&.last&.strip&.delete('"')
|
|
119
|
+
rescue StandardError
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def shell
|
|
124
|
+
self.class.cache[:shell] ||= File.basename(ENV["SHELL"] || "sh")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def working_dir
|
|
128
|
+
@cwd || Dir.pwd
|
|
129
|
+
rescue StandardError
|
|
130
|
+
"(unavailable)"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def git_description
|
|
134
|
+
dir = working_dir
|
|
135
|
+
return nil unless File.directory?(File.join(dir, ".git"))
|
|
136
|
+
|
|
137
|
+
branch = `git -C #{shellescape(dir)} rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
138
|
+
branch.empty? ? "repo (detached HEAD)" : "repo on branch #{branch}"
|
|
139
|
+
rescue StandardError
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def runtimes
|
|
144
|
+
self.class.cache[:runtimes] ||= begin
|
|
145
|
+
parts = []
|
|
146
|
+
parts << "Ruby #{RUBY_VERSION}"
|
|
147
|
+
py = probe_version("python3", "--version")
|
|
148
|
+
parts << "Python #{py}" if py
|
|
149
|
+
node = probe_version("node", "--version")
|
|
150
|
+
parts << "Node #{node.sub(/\Av/, "")}" if node
|
|
151
|
+
parts.join(", ")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def probe_version(bin, flag)
|
|
156
|
+
return nil unless on_path?(bin)
|
|
157
|
+
|
|
158
|
+
out = `#{shellescape(bin)} #{flag} 2>&1`.strip
|
|
159
|
+
out.split.last
|
|
160
|
+
rescue StandardError
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def on_path?(bin)
|
|
165
|
+
ENV["PATH"].to_s.split(File::PATH_SEPARATOR).any? do |dir|
|
|
166
|
+
path = File.join(dir, bin)
|
|
167
|
+
File.executable?(path) && !File.directory?(path)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def shellescape(str)
|
|
172
|
+
str.to_s.gsub(%r{([^A-Za-z0-9_\-.,:/@\n])}, "\\\\\\1")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module Context
|
|
5
|
+
# Discovers and loads project context files from the working directory.
|
|
6
|
+
# Supports multiple file conventions (.rubino.md, AGENTS.md, etc.)
|
|
7
|
+
class FileDiscovery
|
|
8
|
+
CONTEXT_FILES = %w[
|
|
9
|
+
.rubino.md
|
|
10
|
+
RUBINO.md
|
|
11
|
+
AGENTS.md
|
|
12
|
+
CLAUDE.md
|
|
13
|
+
.cursorrules
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
def initialize(base_path: nil)
|
|
17
|
+
@base_path = base_path || Dir.pwd
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Loads and concatenates all found project context files
|
|
21
|
+
def load_project_context
|
|
22
|
+
files = discover_files
|
|
23
|
+
return nil if files.empty?
|
|
24
|
+
|
|
25
|
+
files.map { |f| File.read(f) }.join("\n\n---\n\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns list of discovered context file paths
|
|
29
|
+
def discover_files
|
|
30
|
+
CONTEXT_FILES.filter_map do |filename|
|
|
31
|
+
path = File.join(@base_path, filename)
|
|
32
|
+
path if File.exist?(path)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Checks a subdirectory for local context files
|
|
37
|
+
def local_context(subdir)
|
|
38
|
+
CONTEXT_FILES.filter_map do |filename|
|
|
39
|
+
path = File.join(@base_path, subdir, filename)
|
|
40
|
+
File.read(path) if File.exist?(path)
|
|
41
|
+
end.join("\n")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module Context
|
|
5
|
+
# Splits messages into head (protected), middle (compressible), tail (protected).
|
|
6
|
+
class MessageBoundary
|
|
7
|
+
def initialize(messages:, config: nil)
|
|
8
|
+
@messages = messages
|
|
9
|
+
@config = config || Rubino.configuration
|
|
10
|
+
@protect_first = @config.compression_protect_first_n
|
|
11
|
+
@protect_last = @config.compression_protect_last_n
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Returns the protected head messages (system prompt + first N)
|
|
15
|
+
def head
|
|
16
|
+
@messages.first(@protect_first)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns the compressible middle messages
|
|
20
|
+
def middle
|
|
21
|
+
return [] if @messages.size <= (@protect_first + @protect_last)
|
|
22
|
+
|
|
23
|
+
@messages[@protect_first...-@protect_last]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the protected tail messages (recent context)
|
|
27
|
+
def tail
|
|
28
|
+
return [] if @messages.size <= @protect_last
|
|
29
|
+
|
|
30
|
+
@messages.last(@protect_last)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns true if there are enough messages to have a middle section
|
|
34
|
+
def has_compressible_middle?
|
|
35
|
+
!middle.empty?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|