rubik 0.1.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/ATTRIBUTION.md +139 -0
- data/CHANGELOG.md +236 -0
- data/LICENSE +21 -0
- data/README.md +269 -0
- data/bin/rubik +82 -0
- data/lib/rubik/adapter/base.rb +195 -0
- data/lib/rubik/adapter/c.rb +36 -0
- data/lib/rubik/adapter/crystal.rb +78 -0
- data/lib/rubik/adapter/generic.rb +86 -0
- data/lib/rubik/adapter/go.rb +36 -0
- data/lib/rubik/adapter/node.rb +146 -0
- data/lib/rubik/adapter/process.rb +78 -0
- data/lib/rubik/adapter/python.rb +30 -0
- data/lib/rubik/adapter/registry.rb +81 -0
- data/lib/rubik/adapter/ruby.rb +30 -0
- data/lib/rubik/adapter/rust.rb +36 -0
- data/lib/rubik/adapter/zig.rb +36 -0
- data/lib/rubik/adapter.rb +18 -0
- data/lib/rubik/ast/body_loader.rb +19 -0
- data/lib/rubik/ast/c_extractor.rb +114 -0
- data/lib/rubik/ast/cache_store.rb +191 -0
- data/lib/rubik/ast/extractor.rb +59 -0
- data/lib/rubik/ast/fallback_cleaner.rb +145 -0
- data/lib/rubik/ast/go_extractor.rb +148 -0
- data/lib/rubik/ast/graph.rb +272 -0
- data/lib/rubik/ast/python_extractor.rb +120 -0
- data/lib/rubik/ast/ruby_extractor.rb +260 -0
- data/lib/rubik/ast/rust_extractor.rb +127 -0
- data/lib/rubik/ast/symbol.rb +103 -0
- data/lib/rubik/ast/treesitter/base_walker.rb +109 -0
- data/lib/rubik/ast/treesitter/c_walker.rb +122 -0
- data/lib/rubik/ast/treesitter/go_walker.rb +120 -0
- data/lib/rubik/ast/treesitter/python_walker.rb +107 -0
- data/lib/rubik/ast/treesitter/rust_walker.rb +130 -0
- data/lib/rubik/ast/treesitter/typescript_walker.rb +182 -0
- data/lib/rubik/ast/treesitter/zig_walker.rb +109 -0
- data/lib/rubik/ast/treesitter_extractor.rb +93 -0
- data/lib/rubik/ast/typescript_extractor.rb +283 -0
- data/lib/rubik/ast/zig_extractor.rb +116 -0
- data/lib/rubik/ast.rb +13 -0
- data/lib/rubik/benchmark/runner.rb +224 -0
- data/lib/rubik/benchmark/synth_repo.rb +59 -0
- data/lib/rubik/benchmark.rb +9 -0
- data/lib/rubik/buffer/diff.rb +43 -0
- data/lib/rubik/buffer/manager.rb +427 -0
- data/lib/rubik/buffer/path_validator.rb +60 -0
- data/lib/rubik/buffer/snapshot.rb +99 -0
- data/lib/rubik/buffer.rb +10 -0
- data/lib/rubik/coeffects.rb +129 -0
- data/lib/rubik/concurrency/boundary.rb +74 -0
- data/lib/rubik/concurrency/dns_pool.rb +68 -0
- data/lib/rubik/concurrency/errors.rb +17 -0
- data/lib/rubik/concurrency/event_loop.rb +441 -0
- data/lib/rubik/concurrency/join.rb +55 -0
- data/lib/rubik/concurrency/process_waiter.rb +55 -0
- data/lib/rubik/concurrency/scheduler.rb +84 -0
- data/lib/rubik/concurrency/timers.rb +109 -0
- data/lib/rubik/concurrency.rb +132 -0
- data/lib/rubik/config/deep_ops.rb +80 -0
- data/lib/rubik/config/defaults.rb +248 -0
- data/lib/rubik/config/model_catalog.rb +93 -0
- data/lib/rubik/config/store.rb +155 -0
- data/lib/rubik/config.rb +216 -0
- data/lib/rubik/context/git_info.rb +84 -0
- data/lib/rubik/context/git_lister.rb +37 -0
- data/lib/rubik/context/index.rb +134 -0
- data/lib/rubik/context/repo_context_reader.rb +121 -0
- data/lib/rubik/context/scanner.rb +439 -0
- data/lib/rubik/context.rb +11 -0
- data/lib/rubik/diagnostics/collector.rb +200 -0
- data/lib/rubik/diagnostics/entry.rb +60 -0
- data/lib/rubik/diagnostics/parser.rb +107 -0
- data/lib/rubik/diagnostics.rb +10 -0
- data/lib/rubik/event_bus.rb +56 -0
- data/lib/rubik/kernel/dispatcher.rb +171 -0
- data/lib/rubik/kernel/dmesg.rb +116 -0
- data/lib/rubik/kernel/init.rb +177 -0
- data/lib/rubik/kernel/record.rb +85 -0
- data/lib/rubik/kernel/unit.rb +160 -0
- data/lib/rubik/llm/anthropic.rb +202 -0
- data/lib/rubik/llm/chunk_assembler.rb +285 -0
- data/lib/rubik/llm/client.rb +526 -0
- data/lib/rubik/llm/formatter.rb +450 -0
- data/lib/rubik/llm/message_sanitizer.rb +87 -0
- data/lib/rubik/llm/ollama.rb +167 -0
- data/lib/rubik/llm/openai.rb +21 -0
- data/lib/rubik/llm/openai_compatible.rb +216 -0
- data/lib/rubik/llm/provider.rb +125 -0
- data/lib/rubik/llm/registry.rb +336 -0
- data/lib/rubik/llm/router.rb +100 -0
- data/lib/rubik/llm.rb +18 -0
- data/lib/rubik/log.rb +38 -0
- data/lib/rubik/lsp/client.rb +475 -0
- data/lib/rubik/lsp/manager.rb +366 -0
- data/lib/rubik/lsp.rb +9 -0
- data/lib/rubik/mcp/boot_lifecycle.rb +114 -0
- data/lib/rubik/mcp/client.rb +169 -0
- data/lib/rubik/mcp/errors.rb +11 -0
- data/lib/rubik/mcp/manager.rb +619 -0
- data/lib/rubik/mcp/proxy_tool.rb +104 -0
- data/lib/rubik/mcp/transport/stdio.rb +255 -0
- data/lib/rubik/mcp.rb +12 -0
- data/lib/rubik/orchestrator/activity_log.rb +75 -0
- data/lib/rubik/orchestrator/auto_apply.rb +166 -0
- data/lib/rubik/orchestrator/budget_manager.rb +97 -0
- data/lib/rubik/orchestrator/continuation_manager.rb +161 -0
- data/lib/rubik/orchestrator/finish_message_selector.rb +55 -0
- data/lib/rubik/orchestrator/goal_ledger/drift.rb +71 -0
- data/lib/rubik/orchestrator/goal_ledger/ledger.rb +399 -0
- data/lib/rubik/orchestrator/goal_ledger/observation_projection.rb +179 -0
- data/lib/rubik/orchestrator/goal_ledger/patch.rb +268 -0
- data/lib/rubik/orchestrator/goal_ledger/schema.rb +123 -0
- data/lib/rubik/orchestrator/goal_ledger/validator.rb +361 -0
- data/lib/rubik/orchestrator/goal_ledger/verifier.rb +129 -0
- data/lib/rubik/orchestrator/goal_ledger/verify_spec.rb +50 -0
- data/lib/rubik/orchestrator/goal_ledger.rb +65 -0
- data/lib/rubik/orchestrator/goal_state.rb +184 -0
- data/lib/rubik/orchestrator/goal_wait.rb +125 -0
- data/lib/rubik/orchestrator/loop.rb +388 -0
- data/lib/rubik/orchestrator/lsp_feedback.rb +55 -0
- data/lib/rubik/orchestrator/maestro_gate.rb +65 -0
- data/lib/rubik/orchestrator/provider_recovery.rb +90 -0
- data/lib/rubik/orchestrator/provider_resolver.rb +69 -0
- data/lib/rubik/orchestrator/shell_deps.rb +85 -0
- data/lib/rubik/orchestrator/step_executor.rb +328 -0
- data/lib/rubik/orchestrator/stream_manager.rb +118 -0
- data/lib/rubik/orchestrator/subagent/activation.rb +65 -0
- data/lib/rubik/orchestrator/subagent/budget.rb +108 -0
- data/lib/rubik/orchestrator/subagent/completion.rb +61 -0
- data/lib/rubik/orchestrator/subagent/concurrency_gate.rb +37 -0
- data/lib/rubik/orchestrator/subagent/conversation_id.rb +67 -0
- data/lib/rubik/orchestrator/subagent/dependency.rb +398 -0
- data/lib/rubik/orchestrator/subagent/instance.rb +384 -0
- data/lib/rubik/orchestrator/subagent/lifecycle.rb +118 -0
- data/lib/rubik/orchestrator/subagent/messaging.rb +61 -0
- data/lib/rubik/orchestrator/subagent/narration.rb +125 -0
- data/lib/rubik/orchestrator/subagent/notices.rb +209 -0
- data/lib/rubik/orchestrator/subagent/questioner.rb +80 -0
- data/lib/rubik/orchestrator/subagent/reaper.rb +98 -0
- data/lib/rubik/orchestrator/subagent/resilience.rb +70 -0
- data/lib/rubik/orchestrator/subagent/reuse.rb +127 -0
- data/lib/rubik/orchestrator/subagent/scoped_registry.rb +79 -0
- data/lib/rubik/orchestrator/subagent/step_runner.rb +240 -0
- data/lib/rubik/orchestrator/subagent/storage_id.rb +26 -0
- data/lib/rubik/orchestrator/subagent/supervisor.rb +327 -0
- data/lib/rubik/orchestrator/subagent/transcript_formatter.rb +141 -0
- data/lib/rubik/orchestrator/subagent/type_registry.rb +199 -0
- data/lib/rubik/orchestrator/subagent_notice_inbox.rb +80 -0
- data/lib/rubik/orchestrator/tool_call_id.rb +47 -0
- data/lib/rubik/orchestrator/tool_circuit_breaker.rb +91 -0
- data/lib/rubik/orchestrator/tool_history.rb +39 -0
- data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +102 -0
- data/lib/rubik/orchestrator/turn_executor/user_channel.rb +172 -0
- data/lib/rubik/orchestrator/turn_executor.rb +376 -0
- data/lib/rubik/orchestrator.rb +11 -0
- data/lib/rubik/platform/filesystem.rb +64 -0
- data/lib/rubik/platform/io_drain.rb +53 -0
- data/lib/rubik/platform/path.rb +210 -0
- data/lib/rubik/platform/process.rb +38 -0
- data/lib/rubik/platform/sanitize.rb +157 -0
- data/lib/rubik/platform.rb +113 -0
- data/lib/rubik/plugin/dsl.rb +51 -0
- data/lib/rubik/plugin/manager.rb +247 -0
- data/lib/rubik/plugin/storage.rb +195 -0
- data/lib/rubik/plugin.rb +10 -0
- data/lib/rubik/prompt/context_builder.rb +377 -0
- data/lib/rubik/prompt/context_compactor.rb +326 -0
- data/lib/rubik/prompt/conversation_rows.rb +74 -0
- data/lib/rubik/prompt/doctrine.rb +160 -0
- data/lib/rubik/prompt/emergency_compactor.rb +81 -0
- data/lib/rubik/prompt/services.rb +60 -0
- data/lib/rubik/prompt/token_estimator.rb +63 -0
- data/lib/rubik/prompt/tool_teachings.rb +78 -0
- data/lib/rubik/prompt.rb +13 -0
- data/lib/rubik/repair/loop.rb +95 -0
- data/lib/rubik/repair.rb +8 -0
- data/lib/rubik/repl/ast_warmup.rb +129 -0
- data/lib/rubik/repl/command_context.rb +214 -0
- data/lib/rubik/repl/command_parser.rb +48 -0
- data/lib/rubik/repl/commands/agent.rb +30 -0
- data/lib/rubik/repl/commands/agent_context.rb +289 -0
- data/lib/rubik/repl/commands/agent_execution.rb +342 -0
- data/lib/rubik/repl/commands/agent_models.rb +212 -0
- data/lib/rubik/repl/commands/agent_providers.rb +232 -0
- data/lib/rubik/repl/commands/base.rb +55 -0
- data/lib/rubik/repl/commands/buffer.rb +124 -0
- data/lib/rubik/repl/commands/dmesg.rb +87 -0
- data/lib/rubik/repl/commands/lsp.rb +235 -0
- data/lib/rubik/repl/commands/mcp.rb +194 -0
- data/lib/rubik/repl/commands/plugins.rb +208 -0
- data/lib/rubik/repl/commands/pruner.rb +71 -0
- data/lib/rubik/repl/commands/session.rb +183 -0
- data/lib/rubik/repl/commands/skills.rb +111 -0
- data/lib/rubik/repl/commands/storage.rb +85 -0
- data/lib/rubik/repl/commands/subagents.rb +195 -0
- data/lib/rubik/repl/commands/system.rb +85 -0
- data/lib/rubik/repl/commands/tasks.rb +120 -0
- data/lib/rubik/repl/commands/workspace.rb +443 -0
- data/lib/rubik/repl/commands.rb +44 -0
- data/lib/rubik/repl/composition.rb +32 -0
- data/lib/rubik/repl/config_wiring.rb +142 -0
- data/lib/rubik/repl/confirmations.rb +56 -0
- data/lib/rubik/repl/hooks.rb +47 -0
- data/lib/rubik/repl/input_execution.rb +323 -0
- data/lib/rubik/repl/input_loop.rb +213 -0
- data/lib/rubik/repl/interrupt_handler.rb +217 -0
- data/lib/rubik/repl/lifecycle.rb +410 -0
- data/lib/rubik/repl/paste_support.rb +88 -0
- data/lib/rubik/repl/pending_notice_queue.rb +60 -0
- data/lib/rubik/repl/presentation.rb +132 -0
- data/lib/rubik/repl/pruner/brief.rb +101 -0
- data/lib/rubik/repl/pruner/gate.rb +279 -0
- data/lib/rubik/repl/pruner/harness.rb +236 -0
- data/lib/rubik/repl/pruner/rules.rb +316 -0
- data/lib/rubik/repl/pruner/service.rb +82 -0
- data/lib/rubik/repl/pruner.rb +39 -0
- data/lib/rubik/repl/service_catalog.rb +374 -0
- data/lib/rubik/repl/setup_factories.rb +66 -0
- data/lib/rubik/repl/shell.rb +231 -0
- data/lib/rubik/repl/step_formatter.rb +324 -0
- data/lib/rubik/repl/tee_io.rb +105 -0
- data/lib/rubik/repl/terminal_guard.rb +157 -0
- data/lib/rubik/repl/turn_recorder.rb +234 -0
- data/lib/rubik/repl.rb +14 -0
- data/lib/rubik/runtime/ast_service.rb +78 -0
- data/lib/rubik/runtime/context.rb +335 -0
- data/lib/rubik/runtime/diagnostic_engine.rb +188 -0
- data/lib/rubik/runtime/discovery.rb +342 -0
- data/lib/rubik/runtime/fiber.rb +142 -0
- data/lib/rubik/runtime/fiber_ordering.rb +52 -0
- data/lib/rubik/runtime/fiber_retry_gate.rb +84 -0
- data/lib/rubik/runtime/index_service.rb +58 -0
- data/lib/rubik/runtime/loop_service.rb +73 -0
- data/lib/rubik/runtime/manager.rb +241 -0
- data/lib/rubik/runtime/overlay_storage.rb +481 -0
- data/lib/rubik/runtime/service_definition.rb +54 -0
- data/lib/rubik/runtime/service_registration.rb +33 -0
- data/lib/rubik/runtime/task_manager.rb +302 -0
- data/lib/rubik/runtime.rb +19 -0
- data/lib/rubik/sandbox/driver/base.rb +30 -0
- data/lib/rubik/sandbox/driver/bubblewrap.rb +111 -0
- data/lib/rubik/sandbox/driver/docker.rb +113 -0
- data/lib/rubik/sandbox/driver/podman.rb +98 -0
- data/lib/rubik/sandbox/driver/process.rb +190 -0
- data/lib/rubik/sandbox/driver.rb +91 -0
- data/lib/rubik/sandbox/runner.rb +218 -0
- data/lib/rubik/sandbox/shell_resolver.rb +178 -0
- data/lib/rubik/sandbox.rb +8 -0
- data/lib/rubik/security/blocked_guidance.rb +187 -0
- data/lib/rubik/security/classifier/classifier.rb +440 -0
- data/lib/rubik/security/classifier/commands/agent-browser.yaml +9 -0
- data/lib/rubik/security/classifier/commands/apt.yaml +25 -0
- data/lib/rubik/security/classifier/commands/awk.yaml +10 -0
- data/lib/rubik/security/classifier/commands/basename.yaml +7 -0
- data/lib/rubik/security/classifier/commands/bash-classify.yaml +8 -0
- data/lib/rubik/security/classifier/commands/bash.yaml +20 -0
- data/lib/rubik/security/classifier/commands/black.yaml +12 -0
- data/lib/rubik/security/classifier/commands/brew.yaml +31 -0
- data/lib/rubik/security/classifier/commands/builtin.yaml +8 -0
- data/lib/rubik/security/classifier/commands/cargo.yaml +114 -0
- data/lib/rubik/security/classifier/commands/cat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/chgrp.yaml +10 -0
- data/lib/rubik/security/classifier/commands/chmod.yaml +10 -0
- data/lib/rubik/security/classifier/commands/chown.yaml +10 -0
- data/lib/rubik/security/classifier/commands/column.yaml +7 -0
- data/lib/rubik/security/classifier/commands/comm.yaml +7 -0
- data/lib/rubik/security/classifier/commands/command.yaml +12 -0
- data/lib/rubik/security/classifier/commands/cp.yaml +7 -0
- data/lib/rubik/security/classifier/commands/crystal.yaml +63 -0
- data/lib/rubik/security/classifier/commands/curl.yaml +30 -0
- data/lib/rubik/security/classifier/commands/cut.yaml +7 -0
- data/lib/rubik/security/classifier/commands/date.yaml +10 -0
- data/lib/rubik/security/classifier/commands/df.yaml +7 -0
- data/lib/rubik/security/classifier/commands/diff.yaml +7 -0
- data/lib/rubik/security/classifier/commands/dig.yaml +7 -0
- data/lib/rubik/security/classifier/commands/dirname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/docker.yaml +177 -0
- data/lib/rubik/security/classifier/commands/du.yaml +7 -0
- data/lib/rubik/security/classifier/commands/echo.yaml +7 -0
- data/lib/rubik/security/classifier/commands/env.yaml +15 -0
- data/lib/rubik/security/classifier/commands/eslint.yaml +12 -0
- data/lib/rubik/security/classifier/commands/eval.yaml +8 -0
- data/lib/rubik/security/classifier/commands/exec.yaml +12 -0
- data/lib/rubik/security/classifier/commands/expand.yaml +7 -0
- data/lib/rubik/security/classifier/commands/false.yaml +7 -0
- data/lib/rubik/security/classifier/commands/file.yaml +7 -0
- data/lib/rubik/security/classifier/commands/find.yaml +25 -0
- data/lib/rubik/security/classifier/commands/fmt.yaml +7 -0
- data/lib/rubik/security/classifier/commands/fold.yaml +7 -0
- data/lib/rubik/security/classifier/commands/free.yaml +7 -0
- data/lib/rubik/security/classifier/commands/gawk.yaml +10 -0
- data/lib/rubik/security/classifier/commands/gcloud.yaml +979 -0
- data/lib/rubik/security/classifier/commands/gh.yaml +615 -0
- data/lib/rubik/security/classifier/commands/git.yaml +524 -0
- data/lib/rubik/security/classifier/commands/glab-discussion.yaml +21 -0
- data/lib/rubik/security/classifier/commands/glab.yaml +807 -0
- data/lib/rubik/security/classifier/commands/go.yaml +44 -0
- data/lib/rubik/security/classifier/commands/gog.yaml +983 -0
- data/lib/rubik/security/classifier/commands/gradle.yaml +92 -0
- data/lib/rubik/security/classifier/commands/gradlew.yaml +92 -0
- data/lib/rubik/security/classifier/commands/grep.yaml +7 -0
- data/lib/rubik/security/classifier/commands/groups.yaml +7 -0
- data/lib/rubik/security/classifier/commands/gunzip.yaml +8 -0
- data/lib/rubik/security/classifier/commands/gws.yaml +211 -0
- data/lib/rubik/security/classifier/commands/gzip.yaml +7 -0
- data/lib/rubik/security/classifier/commands/hadolint.yaml +9 -0
- data/lib/rubik/security/classifier/commands/halt.yaml +8 -0
- data/lib/rubik/security/classifier/commands/head.yaml +7 -0
- data/lib/rubik/security/classifier/commands/helm.yaml +129 -0
- data/lib/rubik/security/classifier/commands/hostname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/id.yaml +7 -0
- data/lib/rubik/security/classifier/commands/install.yaml +7 -0
- data/lib/rubik/security/classifier/commands/isort.yaml +14 -0
- data/lib/rubik/security/classifier/commands/java.yaml +35 -0
- data/lib/rubik/security/classifier/commands/javac.yaml +39 -0
- data/lib/rubik/security/classifier/commands/join.yaml +7 -0
- data/lib/rubik/security/classifier/commands/journalctl.yaml +14 -0
- data/lib/rubik/security/classifier/commands/jq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/kill.yaml +7 -0
- data/lib/rubik/security/classifier/commands/killall.yaml +7 -0
- data/lib/rubik/security/classifier/commands/kubectl.yaml +211 -0
- data/lib/rubik/security/classifier/commands/less.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ln.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ls.yaml +7 -0
- data/lib/rubik/security/classifier/commands/make.yaml +21 -0
- data/lib/rubik/security/classifier/commands/md5sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mise.yaml +579 -0
- data/lib/rubik/security/classifier/commands/mkdir.yaml +8 -0
- data/lib/rubik/security/classifier/commands/more.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mv.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mvn.yaml +83 -0
- data/lib/rubik/security/classifier/commands/mvnw.yaml +83 -0
- data/lib/rubik/security/classifier/commands/mypy.yaml +9 -0
- data/lib/rubik/security/classifier/commands/netstat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/nice.yaml +11 -0
- data/lib/rubik/security/classifier/commands/nl.yaml +7 -0
- data/lib/rubik/security/classifier/commands/node.yaml +20 -0
- data/lib/rubik/security/classifier/commands/nohup.yaml +9 -0
- data/lib/rubik/security/classifier/commands/npm.yaml +71 -0
- data/lib/rubik/security/classifier/commands/npx.yaml +42 -0
- data/lib/rubik/security/classifier/commands/nslookup.yaml +7 -0
- data/lib/rubik/security/classifier/commands/paste.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ping.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pip.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pip3.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pipx.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pkill.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pnpm.yaml +346 -0
- data/lib/rubik/security/classifier/commands/poetry.yaml +97 -0
- data/lib/rubik/security/classifier/commands/poweroff.yaml +8 -0
- data/lib/rubik/security/classifier/commands/prettier.yaml +13 -0
- data/lib/rubik/security/classifier/commands/printenv.yaml +7 -0
- data/lib/rubik/security/classifier/commands/printf.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ps.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pup.yaml +882 -0
- data/lib/rubik/security/classifier/commands/pwd.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pytest.yaml +7 -0
- data/lib/rubik/security/classifier/commands/python.yaml +9 -0
- data/lib/rubik/security/classifier/commands/python3.yaml +9 -0
- data/lib/rubik/security/classifier/commands/read.yaml +7 -0
- data/lib/rubik/security/classifier/commands/readlink.yaml +7 -0
- data/lib/rubik/security/classifier/commands/realpath.yaml +7 -0
- data/lib/rubik/security/classifier/commands/reboot.yaml +8 -0
- data/lib/rubik/security/classifier/commands/rev.yaml +7 -0
- data/lib/rubik/security/classifier/commands/rg.yaml +7 -0
- data/lib/rubik/security/classifier/commands/rm.yaml +8 -0
- data/lib/rubik/security/classifier/commands/rsync.yaml +12 -0
- data/lib/rubik/security/classifier/commands/rtk.yaml +140 -0
- data/lib/rubik/security/classifier/commands/ruff.yaml +29 -0
- data/lib/rubik/security/classifier/commands/rustfmt.yaml +14 -0
- data/lib/rubik/security/classifier/commands/scp.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sed.yaml +14 -0
- data/lib/rubik/security/classifier/commands/seq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sh.yaml +14 -0
- data/lib/rubik/security/classifier/commands/sha1sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sha256sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/shellcheck.yaml +10 -0
- data/lib/rubik/security/classifier/commands/shutdown.yaml +8 -0
- data/lib/rubik/security/classifier/commands/slack.yaml +113 -0
- data/lib/rubik/security/classifier/commands/sleep.yaml +8 -0
- data/lib/rubik/security/classifier/commands/snip.yaml +79 -0
- data/lib/rubik/security/classifier/commands/sort.yaml +10 -0
- data/lib/rubik/security/classifier/commands/ss.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ssh.yaml +41 -0
- data/lib/rubik/security/classifier/commands/stat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/stdbuf.yaml +15 -0
- data/lib/rubik/security/classifier/commands/strace.yaml +20 -0
- data/lib/rubik/security/classifier/commands/strings.yaml +11 -0
- data/lib/rubik/security/classifier/commands/sudo.yaml +24 -0
- data/lib/rubik/security/classifier/commands/systemctl.yaml +58 -0
- data/lib/rubik/security/classifier/commands/systemd-run.yaml +30 -0
- data/lib/rubik/security/classifier/commands/tac.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tail.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tar.yaml +25 -0
- data/lib/rubik/security/classifier/commands/tee.yaml +8 -0
- data/lib/rubik/security/classifier/commands/terraform.yaml +70 -0
- data/lib/rubik/security/classifier/commands/test.yaml +7 -0
- data/lib/rubik/security/classifier/commands/time.yaml +8 -0
- data/lib/rubik/security/classifier/commands/timeout.yaml +18 -0
- data/lib/rubik/security/classifier/commands/top.yaml +7 -0
- data/lib/rubik/security/classifier/commands/touch.yaml +8 -0
- data/lib/rubik/security/classifier/commands/tr.yaml +7 -0
- data/lib/rubik/security/classifier/commands/traceroute.yaml +7 -0
- data/lib/rubik/security/classifier/commands/true.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tsc.yaml +16 -0
- data/lib/rubik/security/classifier/commands/type.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/unexpand.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uniq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/unzip.yaml +10 -0
- data/lib/rubik/security/classifier/commands/uptime.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uv.yaml +156 -0
- data/lib/rubik/security/classifier/commands/wait.yaml +11 -0
- data/lib/rubik/security/classifier/commands/wc.yaml +7 -0
- data/lib/rubik/security/classifier/commands/wget.yaml +7 -0
- data/lib/rubik/security/classifier/commands/whereis.yaml +7 -0
- data/lib/rubik/security/classifier/commands/which.yaml +7 -0
- data/lib/rubik/security/classifier/commands/whoami.yaml +7 -0
- data/lib/rubik/security/classifier/commands/xargs.yaml +26 -0
- data/lib/rubik/security/classifier/commands/xmllint.yaml +7 -0
- data/lib/rubik/security/classifier/commands/yarn.yaml +72 -0
- data/lib/rubik/security/classifier/commands/yes.yaml +7 -0
- data/lib/rubik/security/classifier/commands/yq.yaml +10 -0
- data/lib/rubik/security/classifier/commands/zip.yaml +8 -0
- data/lib/rubik/security/classifier/commands/zsh.yaml +14 -0
- data/lib/rubik/security/classifier/database.rb +216 -0
- data/lib/rubik/security/classifier/matcher/delegation.rb +190 -0
- data/lib/rubik/security/classifier/matcher/options.rb +245 -0
- data/lib/rubik/security/classifier/matcher/subcommands.rb +103 -0
- data/lib/rubik/security/classifier/matcher.rb +249 -0
- data/lib/rubik/security/classifier/models.rb +170 -0
- data/lib/rubik/security/classifier/parser.rb +23 -0
- data/lib/rubik/security/classifier/parsers/bash.rb +280 -0
- data/lib/rubik/security/classifier/parsers/bash_extractors.rb +221 -0
- data/lib/rubik/security/classifier/parsers/powershell.rb +185 -0
- data/lib/rubik/security/classifier/pwsh_rules.rb +224 -0
- data/lib/rubik/security/classifier.rb +25 -0
- data/lib/rubik/security/errors.rb +39 -0
- data/lib/rubik/security/guard.rb +954 -0
- data/lib/rubik/security/redactor.rb +89 -0
- data/lib/rubik/security.rb +11 -0
- data/lib/rubik/session/manager.rb +331 -0
- data/lib/rubik/session/recorder.rb +149 -0
- data/lib/rubik/session/replayer.rb +65 -0
- data/lib/rubik/session.rb +10 -0
- data/lib/rubik/skills/frontmatter.rb +41 -0
- data/lib/rubik/skills/manager.rb +223 -0
- data/lib/rubik/skills/registry.rb +64 -0
- data/lib/rubik/skills/skill.rb +33 -0
- data/lib/rubik/skills/storage.rb +77 -0
- data/lib/rubik/skills/validator.rb +116 -0
- data/lib/rubik/skills.rb +20 -0
- data/lib/rubik/storage/folding.rb +23 -0
- data/lib/rubik/storage/layout.rb +145 -0
- data/lib/rubik/storage/render.rb +97 -0
- data/lib/rubik/storage/service.rb +42 -0
- data/lib/rubik/storage/store.rb +514 -0
- data/lib/rubik/storage/transcript_indexer.rb +116 -0
- data/lib/rubik/storage/uri_reader.rb +168 -0
- data/lib/rubik/storage/vakanuvis/annal_writer.rb +92 -0
- data/lib/rubik/storage/vakanuvis/distill_gate.rb +323 -0
- data/lib/rubik/storage/vakanuvis/gate.rb +250 -0
- data/lib/rubik/storage/vakanuvis/harness.rb +421 -0
- data/lib/rubik/storage/vakanuvis/memory_steward.rb +163 -0
- data/lib/rubik/storage/vakanuvis/service.rb +69 -0
- data/lib/rubik/storage/vakanuvis/vocabulary.rb +99 -0
- data/lib/rubik/storage/vakanuvis/wiki_skeleton.rb +67 -0
- data/lib/rubik/storage/vakanuvis.rb +77 -0
- data/lib/rubik/storage.rb +29 -0
- data/lib/rubik/tools/base.rb +107 -0
- data/lib/rubik/tools/builtins/dmesg.rb +74 -0
- data/lib/rubik/tools/builtins/exec.rb +293 -0
- data/lib/rubik/tools/builtins/files.rb +354 -0
- data/lib/rubik/tools/builtins/goal_ledger.rb +89 -0
- data/lib/rubik/tools/builtins/grep.rb +281 -0
- data/lib/rubik/tools/builtins/interactive.rb +175 -0
- data/lib/rubik/tools/builtins/lifecycle.rb +359 -0
- data/lib/rubik/tools/builtins/lsp.rb +283 -0
- data/lib/rubik/tools/builtins/pruner.rb +88 -0
- data/lib/rubik/tools/builtins/runtime.rb +120 -0
- data/lib/rubik/tools/builtins/send_message.rb +109 -0
- data/lib/rubik/tools/builtins/show_file.rb +84 -0
- data/lib/rubik/tools/builtins/skill.rb +72 -0
- data/lib/rubik/tools/builtins/storage.rb +232 -0
- data/lib/rubik/tools/builtins/subagent.rb +356 -0
- data/lib/rubik/tools/builtins/subagent_lifecycle.rb +72 -0
- data/lib/rubik/tools/builtins/symbols.rb +209 -0
- data/lib/rubik/tools/builtins/synthesis.rb +314 -0
- data/lib/rubik/tools/builtins/task.rb +168 -0
- data/lib/rubik/tools/builtins/unsynthesize.rb +111 -0
- data/lib/rubik/tools/builtins/vakanuvis.rb +161 -0
- data/lib/rubik/tools/builtins.rb +30 -0
- data/lib/rubik/tools/catalog.rb +70 -0
- data/lib/rubik/tools/declaration.rb +90 -0
- data/lib/rubik/tools/diagnostic_reporter.rb +283 -0
- data/lib/rubik/tools/fuzzy_matcher.rb +172 -0
- data/lib/rubik/tools/native_search.rb +180 -0
- data/lib/rubik/tools/output.rb +54 -0
- data/lib/rubik/tools/preflight_runner.rb +209 -0
- data/lib/rubik/tools/registry.rb +247 -0
- data/lib/rubik/tools/result.rb +71 -0
- data/lib/rubik/tools/services.rb +96 -0
- data/lib/rubik/tools/truncator.rb +78 -0
- data/lib/rubik/tools/uri_resolver.rb +195 -0
- data/lib/rubik/tools.rb +14 -0
- data/lib/rubik/ui/approvals.rb +52 -0
- data/lib/rubik/ui/banner.rb +43 -0
- data/lib/rubik/ui/capability.rb +122 -0
- data/lib/rubik/ui/dialog.rb +106 -0
- data/lib/rubik/ui/diff_viewer.rb +78 -0
- data/lib/rubik/ui/frame.rb +95 -0
- data/lib/rubik/ui/markdown_stream.rb +107 -0
- data/lib/rubik/ui/panel.rb +98 -0
- data/lib/rubik/ui/renderer.rb +206 -0
- data/lib/rubik/ui/spinner.rb +120 -0
- data/lib/rubik/ui/statusline.rb +134 -0
- data/lib/rubik/ui/theme.rb +118 -0
- data/lib/rubik/ui.rb +57 -0
- data/lib/rubik/uris.rb +101 -0
- data/lib/rubik/version.rb +5 -0
- data/lib/rubik.rb +65 -0
- metadata +770 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../prompt"
|
|
5
|
+
require_relative "../prompt/conversation_rows"
|
|
6
|
+
require_relative "activity_log"
|
|
7
|
+
require_relative "shell_deps"
|
|
8
|
+
require_relative "step_executor"
|
|
9
|
+
require_relative "budget_manager"
|
|
10
|
+
require_relative "stream_manager"
|
|
11
|
+
require_relative "provider_resolver"
|
|
12
|
+
require_relative "provider_recovery"
|
|
13
|
+
require_relative "continuation_manager"
|
|
14
|
+
require_relative "goal_wait"
|
|
15
|
+
require_relative "subagent_notice_inbox"
|
|
16
|
+
require_relative "turn_executor"
|
|
17
|
+
require_relative "turn_executor/user_channel"
|
|
18
|
+
require_relative "tool_call_id"
|
|
19
|
+
require_relative "subagent/notices"
|
|
20
|
+
|
|
21
|
+
module Rubik
|
|
22
|
+
module Orchestrator
|
|
23
|
+
class Loop
|
|
24
|
+
attr_accessor :history, :invalid_tool_count, :last_turn_streamed, :context_warning_emitted
|
|
25
|
+
attr_reader :max_steps, :step_count, :model, :total_tokens, :warn_at_ratio, :runtime_manager, :loop_context, :context_limit, :max_output_tokens, :config
|
|
26
|
+
# Collaborator-facing state (Plan 54 D-5): loop state physically stays on Loop;
|
|
27
|
+
# extracted collaborators reach it through these accessors.
|
|
28
|
+
attr_reader :provider, :on_step, :tool_registry, :shell, :buffer_manager, :lsp_manager, :step_executor,
|
|
29
|
+
:budget_manager, :stream_manager, :activity
|
|
30
|
+
|
|
31
|
+
# Channel-owned counter (TurnExecutor::NudgePolicy); the public reader
|
|
32
|
+
# name is load-bearing (tests, REPL surfaces).
|
|
33
|
+
def empty_response_count = turn_executor.nudge_policy.count
|
|
34
|
+
|
|
35
|
+
def initialize(provider:, tool_registry:, max_steps: 40, warn_at_ratio: 0.75, model: nil, on_step: nil, shell: nil, context_limit: nil,
|
|
36
|
+
max_output_tokens: nil)
|
|
37
|
+
@provider = provider
|
|
38
|
+
@tool_registry = tool_registry
|
|
39
|
+
@max_steps = (max_steps || 40).to_i
|
|
40
|
+
@warn_at_ratio = (warn_at_ratio || 0.75).to_f
|
|
41
|
+
@model = model
|
|
42
|
+
@on_step = on_step
|
|
43
|
+
# The shell must be STABLE from construction through run(): the
|
|
44
|
+
# ivars below snapshot its services at initialize (execution uses
|
|
45
|
+
# these), while the system prompt re-reads the public readers at
|
|
46
|
+
# run() start (Prompt::Services.from_shell) — a service swapped
|
|
47
|
+
# in between would make prompt and execution disagree.
|
|
48
|
+
@shell = shell
|
|
49
|
+
@buffer_manager = shell&.buffer_manager
|
|
50
|
+
@snapshot_store = shell&.snapshot_store
|
|
51
|
+
@context_index = shell&.context_index
|
|
52
|
+
@lsp_manager = shell&.lsp_manager
|
|
53
|
+
@config = shell&.config
|
|
54
|
+
@root_dir = File.expand_path(shell&.buffer_manager&.root_dir || Dir.pwd)
|
|
55
|
+
@runtime_manager = shell&.runtime_manager
|
|
56
|
+
@context_limit = context_limit || 32_768
|
|
57
|
+
@max_output_tokens = (max_output_tokens || (@config.respond_to?(:fetch) ? @config.fetch("llm.default_max_output_tokens", 4_096) : 4_096)).to_i
|
|
58
|
+
@loop_context = nil
|
|
59
|
+
@step_count = 0
|
|
60
|
+
@history = []
|
|
61
|
+
@total_tokens = { input: 0, output: 0 }
|
|
62
|
+
@invalid_tool_count = 0
|
|
63
|
+
@last_turn_streamed = false
|
|
64
|
+
@context_warning_emitted = false
|
|
65
|
+
# Plan 61 D-15: per-run activity ledger feeding the turn summary.
|
|
66
|
+
@activity = ActivityLog.new
|
|
67
|
+
# Plan 87 E-1: collaborators reach loop state only through the
|
|
68
|
+
# ShellDeps façade, not through raw @loop reach-through.
|
|
69
|
+
# Review Öneri 3: provider-config resolution follows the same
|
|
70
|
+
# collaborator-extraction pattern (ProviderResolver, built lazily
|
|
71
|
+
# on first use — see #provider_resolver).
|
|
72
|
+
@shell_deps = ShellDeps.new(self)
|
|
73
|
+
@step_executor = StepExecutor.new(@shell_deps)
|
|
74
|
+
@budget_manager = BudgetManager.new(@shell_deps)
|
|
75
|
+
@stream_manager = StreamManager.new(@shell_deps)
|
|
76
|
+
@continuation_manager = ContinuationManager.new(config: @config)
|
|
77
|
+
@subagent_notice_inbox = SubagentNoticeInbox.new
|
|
78
|
+
@goal_mode = false
|
|
79
|
+
@goal_wait = nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# §7.2 item 3 (ContinuationManager mold): the notice queue lives in
|
|
83
|
+
# SubagentNoticeInbox; Loop keeps the original public names as
|
|
84
|
+
# delegation shims — repl/lifecycle.rb routes by respond_to?(
|
|
85
|
+
# :enqueue_subagent_notice), so that name is load-bearing.
|
|
86
|
+
def enqueue_subagent_notice(notice) = subagent_notice_inbox.enqueue(notice)
|
|
87
|
+
|
|
88
|
+
def drain_subagent_notices_into_history!
|
|
89
|
+
notices = subagent_notice_inbox.drain_all
|
|
90
|
+
return notices if notices.empty?
|
|
91
|
+
|
|
92
|
+
merge_notice_block_into_history!(Rubik::Orchestrator::Subagent::Notices.history_block(notices))
|
|
93
|
+
notices
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The merge policy lives in Prompt::ConversationRows (the row-dialect
|
|
97
|
+
# single home, shared with the REPL injector and the compaction layer).
|
|
98
|
+
def merge_notice_block_into_history!(xml_payload)
|
|
99
|
+
Prompt::ConversationRows.append_notice!(@history, xml_payload)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def subagent_notice_inbox = (@subagent_notice_inbox ||= SubagentNoticeInbox.new)
|
|
103
|
+
|
|
104
|
+
# Goal-mode flag (run/continue_loop lifecycle above) and the
|
|
105
|
+
# premature-completion guard collaborator. Lazy so Class#allocate
|
|
106
|
+
# test doubles — which skip initialize — answer goal_mode? false and
|
|
107
|
+
# never build the collaborator off a nil @shell_deps.
|
|
108
|
+
def goal_mode? = @goal_mode == true
|
|
109
|
+
|
|
110
|
+
def goal_wait
|
|
111
|
+
@goal_wait ||= GoalWait.new(@shell_deps)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def continuation_manager
|
|
115
|
+
@continuation_manager ||= ContinuationManager.new(config: @config)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Agent-turn-executor plan: response processing is shared with the
|
|
119
|
+
# subagent channel through TurnExecutor; Loop supplies the
|
|
120
|
+
# user-channel adapter. Built lazily on first use, NOT in
|
|
121
|
+
# initialize: several test doubles construct loops via
|
|
122
|
+
# Class#allocate + instance_variable_set (build_knob_loop), which
|
|
123
|
+
# skips initialize.
|
|
124
|
+
def turn_executor
|
|
125
|
+
@turn_executor ||= TurnExecutor.new(channel: TurnExecutor::UserChannel.new(self))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def extend_budget(extra_steps) = (@max_steps += extra_steps.to_i)
|
|
129
|
+
|
|
130
|
+
def run(task_prompt, model: nil, plan_only: false, goal_mode: false, initial_context: nil, conversation_history: [])
|
|
131
|
+
@step_count = 0
|
|
132
|
+
@history = []
|
|
133
|
+
# Goal-mode marker for the premature-completion guard (GoalWait):
|
|
134
|
+
# set per run so a reused Loop instance cannot inherit the flag;
|
|
135
|
+
# continue_loop deliberately keeps it (same run resumed).
|
|
136
|
+
@goal_mode = goal_mode
|
|
137
|
+
# Fresh inbox per run, aligned with the history reset; continue_loop
|
|
138
|
+
# deliberately keeps the queue (SubagentNoticeInbox contract docs).
|
|
139
|
+
subagent_notice_inbox.reset_for_new_run!
|
|
140
|
+
@total_tokens = { input: 0, output: 0 }
|
|
141
|
+
@invalid_tool_count = 0
|
|
142
|
+
turn_executor.reset!
|
|
143
|
+
@context_warning_emitted = false
|
|
144
|
+
@activity&.reset!
|
|
145
|
+
continuation_manager&.reset!
|
|
146
|
+
@loop_context = @runtime_manager&.context&.extend
|
|
147
|
+
|
|
148
|
+
begin
|
|
149
|
+
target_model = model || @model
|
|
150
|
+
opts = target_model ? { model: target_model } : {}
|
|
151
|
+
opts[:tools] = @tool_registry.schemas unless plan_only
|
|
152
|
+
|
|
153
|
+
mode = if plan_only
|
|
154
|
+
:plan
|
|
155
|
+
else
|
|
156
|
+
goal_mode ? :goal : :default
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Prompt view built once at the composition boundary: the
|
|
160
|
+
# orchestrator resolves coeffects (skills index, type registry)
|
|
161
|
+
# and the prompt layer only renders (candidate 2, Phase 2).
|
|
162
|
+
services = Rubik::Prompt::Services.from_shell(@shell,
|
|
163
|
+
skills_index: Rubik::Coeffects.get(@runtime_manager&.context, Rubik::Coeffects::SKILL_MANAGER)&.prompt_index,
|
|
164
|
+
type_registry: resolve_type_registry,
|
|
165
|
+
context_limit: @context_limit,
|
|
166
|
+
max_output_tokens: @max_output_tokens,
|
|
167
|
+
root_dir: @root_dir,
|
|
168
|
+
extra_context: initial_context)
|
|
169
|
+
system_prompt = Rubik::Prompt::ContextBuilder.build_system_prompt(
|
|
170
|
+
mode: mode,
|
|
171
|
+
tool_schemas: plan_only ? [] : @tool_registry.schemas,
|
|
172
|
+
native_tools: !plan_only,
|
|
173
|
+
services: services
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
formatted_task = Rubik::Prompt::ContextBuilder.format_user_task(task_prompt)
|
|
177
|
+
|
|
178
|
+
@history << { role: "system", content: system_prompt }
|
|
179
|
+
|
|
180
|
+
# Inject prior dialogue turns if provided
|
|
181
|
+
if conversation_history && !conversation_history.empty?
|
|
182
|
+
conversation_history.each do |turn|
|
|
183
|
+
r = turn[:role] || turn["role"]
|
|
184
|
+
c = turn[:content] || turn["content"]
|
|
185
|
+
next if r.nil? || c.nil? || c.to_s.strip.empty?
|
|
186
|
+
|
|
187
|
+
@history << { role: r.to_s, content: c.to_s }
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
@history << { role: "user", content: formatted_task }
|
|
192
|
+
|
|
193
|
+
execute_steps(opts: opts, plan_only: plan_only)
|
|
194
|
+
ensure
|
|
195
|
+
@loop_context&.dispose
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def continue_loop(extra_steps = 20, plan_only: false)
|
|
200
|
+
# Retention seam — see SubagentNoticeInbox#keep_for_continuation.
|
|
201
|
+
subagent_notice_inbox.keep_for_continuation
|
|
202
|
+
extend_budget(extra_steps)
|
|
203
|
+
target_model = @model
|
|
204
|
+
opts = target_model ? { model: target_model } : {}
|
|
205
|
+
opts[:tools] = @tool_registry.schemas
|
|
206
|
+
|
|
207
|
+
# Plan 61 D-14: notifications travel as their own user message —
|
|
208
|
+
# appending to @history.last may target a tool result or a
|
|
209
|
+
# content-less assistant message, which provider formatters then
|
|
210
|
+
# mangle.
|
|
211
|
+
continuation_msg = "<step_budget_extended extra=\"#{extra_steps}\" new_max=\"#{@max_steps}\" " \
|
|
212
|
+
"current=\"#{@step_count}\" " \
|
|
213
|
+
"message=\"User approved extending step budget by #{extra_steps} steps. " \
|
|
214
|
+
"Continue working toward completion.\" />"
|
|
215
|
+
@history << { role: "user", content: continuation_msg }
|
|
216
|
+
execute_steps(opts: opts, plan_only: plan_only)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Shared by both request paths (batch + streaming, Plan 63 Faz A):
|
|
220
|
+
# compact the history in place and emit the :emergency_compact event
|
|
221
|
+
# carrying the layer that produced the final history. Public because
|
|
222
|
+
# StreamManager's compact callable drives it through the ShellDeps
|
|
223
|
+
# façade. The compaction body lives in TurnExecutor#emergency_compact!
|
|
224
|
+
# — one home for both channels; the announce is channel-parameterized.
|
|
225
|
+
def emergency_compact_and_notify!(reason)
|
|
226
|
+
turn_executor.emergency_compact!(reason)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def safe_config(key, default)
|
|
230
|
+
return default unless @config.respond_to?(:fetch)
|
|
231
|
+
|
|
232
|
+
@config.fetch(key, default) || default
|
|
233
|
+
rescue KeyError, TypeError, NoMethodError
|
|
234
|
+
default
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Turn-result contract (public: the TurnExecutor user-channel adapter
|
|
238
|
+
# builds every terminal through it, and the shell drives
|
|
239
|
+
# continue_loop off the carried loop_instance).
|
|
240
|
+
def turn_result(success:, error: nil, output: nil)
|
|
241
|
+
r = { ok: success, steps: @step_count, tokens: @total_tokens, streamed: @last_turn_streamed == true, loop_instance: self }
|
|
242
|
+
r[:output] = output if output
|
|
243
|
+
r[:error] = error if error
|
|
244
|
+
r
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
private
|
|
248
|
+
|
|
249
|
+
# Registry-key + llm.* override resolution lives in ProviderResolver
|
|
250
|
+
# (review Öneri 3). Loop keeps thin private delegates with the
|
|
251
|
+
# original names/signatures so call sites and send()-based tests are
|
|
252
|
+
# untouched.
|
|
253
|
+
def provider_registry_key
|
|
254
|
+
provider_resolver.registry_key
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def provider_llm_config(key, default)
|
|
258
|
+
provider_resolver.llm_config(key, default)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Built lazily on first use, NOT in initialize: several test doubles
|
|
262
|
+
# construct loops via Class#allocate + instance_variable_set
|
|
263
|
+
# (build_knob_loop), which skips initialize. Reading @provider/
|
|
264
|
+
# @config at first use keeps that pattern working. Memoization is
|
|
265
|
+
# safe: the provider reference is immutable for the loop's lifetime
|
|
266
|
+
# and the config object is held by reference (values re-read per
|
|
267
|
+
# fetch — config reactivity preserved).
|
|
268
|
+
def provider_resolver
|
|
269
|
+
@provider_resolver ||= ProviderResolver.new(provider: @provider, config: @config)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def resolve_type_registry
|
|
273
|
+
if @runtime_manager&.context.respond_to?(:get)
|
|
274
|
+
sup = Rubik::Coeffects.get(@runtime_manager.context, Rubik::Coeffects::SUBAGENT_SUPERVISOR)
|
|
275
|
+
return sup.type_registry if sup&.type_registry
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
Rubik::Orchestrator::Subagent::TypeRegistry.new if @tool_registry&.get("invoke_subagent") && defined?(Rubik::Orchestrator::Subagent::TypeRegistry)
|
|
279
|
+
rescue StandardError => e
|
|
280
|
+
Rubik::Platform.debug_warn("type registry resolution failed (#{e.class}: #{e.message}); subagent typing degraded")
|
|
281
|
+
nil
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def execute_steps(opts:, plan_only: false)
|
|
285
|
+
@invalid_tool_count = 0
|
|
286
|
+
turn_executor.reset!
|
|
287
|
+
warned = false
|
|
288
|
+
|
|
289
|
+
while @step_count < @max_steps
|
|
290
|
+
# Late-notice asymmetry is intentional: notices arriving after the
|
|
291
|
+
# final step drain nowhere — the user channel (turn-end flush) and
|
|
292
|
+
# manage_subagents wait remain their delivery path.
|
|
293
|
+
drain_subagent_notices_into_history!
|
|
294
|
+
@step_count += 1
|
|
295
|
+
|
|
296
|
+
warned = @budget_manager.evaluate_step_budget_warning!(warned)
|
|
297
|
+
verify_goal_ledger! if goal_mode?
|
|
298
|
+
@budget_manager.evaluate_context_compaction!
|
|
299
|
+
refresh_goal_ledger_row!
|
|
300
|
+
|
|
301
|
+
opts[:tools] = @tool_registry.schemas unless plan_only
|
|
302
|
+
|
|
303
|
+
prov_name = provider_registry_key || "custom"
|
|
304
|
+
|
|
305
|
+
@on_step&.call(
|
|
306
|
+
type: :thinking,
|
|
307
|
+
step: @step_count,
|
|
308
|
+
max_steps: @max_steps,
|
|
309
|
+
model: opts[:model] || @model,
|
|
310
|
+
provider: prov_name
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
res = send_llm_request_with_recovery(opts)
|
|
314
|
+
result = turn_executor.call(res, plan_only: plan_only)
|
|
315
|
+
return result if result
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
{ ok: false, error: "Max steps budget exceeded (#{@max_steps})", steps: @step_count, tokens: @total_tokens, loop_instance: self }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Goal-ledger adım-baı yenilemesi (goal-ledger planı A): compaction
|
|
322
|
+
# SONRADA, request ÖNCE — refresh her zaman güncel history referansını
|
|
323
|
+
# görür (BudgetManager history'yi yeni diziye bağlar) ve güncel Σ
|
|
324
|
+
# satırı her adımda garanti edilir. GOAL_LEDGER coeffect'i yoksa
|
|
325
|
+
# (knob OFF ya da !goal dışı) tek satırda no-op — davranış değişmez.
|
|
326
|
+
# Goal-ledger yeniden-doğrulama (plan B): :turn_finished goal
|
|
327
|
+
# çalışırken HİÇ fire etmez (senkron bloklamalı !goal) — turn sınırı
|
|
328
|
+
# execute_steps iterasyonudur, bu yüzden doğrudan hook. Hata
|
|
329
|
+
# izolasyonu: doğrulama asla akışı bozmaz.
|
|
330
|
+
def verify_goal_ledger!
|
|
331
|
+
Rubik::Orchestrator::GoalLedger::Verifier.at_step_boundary!(self)
|
|
332
|
+
rescue StandardError => e
|
|
333
|
+
# Dürüst kanal (Log.warn): debug_warn ENV-gated no-op'tur — doğrulama
|
|
334
|
+
# çöktüğünde drift tespiti sessizce duramaz (sessiz-yutma yasağı).
|
|
335
|
+
Rubik::Log.warn(:orchestrator, "goal ledger verification failed (#{e.class}: #{e.message}); step continues unverified")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def refresh_goal_ledger_row!
|
|
339
|
+
ledger = Rubik::Coeffects.get(@runtime_manager&.context, Rubik::Coeffects::GOAL_LEDGER)
|
|
340
|
+
ledger&.refresh_in_history!(@history)
|
|
341
|
+
rescue StandardError => e
|
|
342
|
+
Rubik::Log.warn(:orchestrator, "goal ledger refresh failed (#{e.class}: #{e.message}); step continues")
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def send_llm_request_with_recovery(opts)
|
|
346
|
+
streaming_enabled = if opts.key?(:stream)
|
|
347
|
+
opts[:stream] == true
|
|
348
|
+
else
|
|
349
|
+
provider_llm_config("stream", true) != false
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Plan 61 D-1/D-3: the wire request must share the compaction math's
|
|
353
|
+
# assumptions. Ollama needs context_limit (sent as num_ctx) or its
|
|
354
|
+
# server silently clips the prompt head to the model default; strict
|
|
355
|
+
# providers need max_tokens or output falls back to tiny defaults
|
|
356
|
+
# (e.g. Anthropic's 1024).
|
|
357
|
+
effective_opts = opts.key?(:on_retry) ? opts.dup : opts.merge(on_retry: @stream_manager.llm_retry_notifier)
|
|
358
|
+
effective_opts[:context_limit] = @context_limit if @context_limit
|
|
359
|
+
effective_opts[:max_tokens] = @max_output_tokens if @max_output_tokens&.positive?
|
|
360
|
+
effective_opts[:stream_timeout] ||= provider_llm_config("stream_timeout", ProviderRecovery::STREAM_REQUEST_TIMEOUT)
|
|
361
|
+
effective_opts[:open_timeout] ||= provider_llm_config("open_timeout", ProviderRecovery::DEFAULT_OPEN_TIMEOUT)
|
|
362
|
+
effective_opts[:timeout] ||= effective_opts[:stream_timeout]
|
|
363
|
+
# Plan 90 completion: the llm.thinking_budget_ratio / llm.reasoning_effort
|
|
364
|
+
# knobs reach the adapters only through this bridge. Both default to nil
|
|
365
|
+
# (never sent); the key must stay ABSENT when unset — openai_compatible's
|
|
366
|
+
# reasoning_model? tests opts.key?(:reasoning_effort), so even a nil
|
|
367
|
+
# value would flip every model onto the reasoning payload branch.
|
|
368
|
+
ratio = provider_llm_config("thinking_budget_ratio", nil)
|
|
369
|
+
effective_opts[:thinking_budget_ratio] ||= ratio if ratio
|
|
370
|
+
effort = provider_llm_config("reasoning_effort", nil)
|
|
371
|
+
effective_opts[:reasoning_effort] ||= effort if effort
|
|
372
|
+
|
|
373
|
+
if streaming_enabled && @provider.respond_to?(:stream_chat)
|
|
374
|
+
@stream_manager.execute_streaming_request(effective_opts)
|
|
375
|
+
else
|
|
376
|
+
execute_batch_request(effective_opts)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def execute_batch_request(opts)
|
|
381
|
+
@last_turn_streamed = false
|
|
382
|
+
ProviderRecovery.with_recovery(config: @config, compact: method(:emergency_compact_and_notify!)) do
|
|
383
|
+
@provider.complete(@history, opts)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# Gerçek zamanlı LSP tampon-tanı geri beslemesi (Plan 56 D-5) —
|
|
6
|
+
# StepExecutor'dan dürüst çıkarım (satır bütçesi; storage-show-surface
|
|
7
|
+
# planı A'nın :user_output kancası yerine yer açar). Yazma araçlarının
|
|
8
|
+
# ardından tampon LSP'ye senkronize edilir; severity-1 tanılar varsa
|
|
9
|
+
# sonuç çıktısına düzeltme yönlendirmesi eklenir.
|
|
10
|
+
module LspFeedback
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
WRITER_TOOLS = ["write_file", "replace_file_content"].freeze
|
|
14
|
+
|
|
15
|
+
def refresh(deps:, tool_instance:, tool_args:, tool_result:)
|
|
16
|
+
return tool_result unless tool_result.status == :ok
|
|
17
|
+
return tool_result unless WRITER_TOOLS.include?(tool_instance&.name)
|
|
18
|
+
return tool_result unless deps.lsp_manager&.enabled?
|
|
19
|
+
|
|
20
|
+
written_file = tool_args["path"] || tool_args[:path]
|
|
21
|
+
return tool_result unless written_file && deps.buffer_manager
|
|
22
|
+
|
|
23
|
+
content = deps.buffer_manager.read(written_file)
|
|
24
|
+
synced = deps.lsp_manager.sync_buffer_file(written_file, content)
|
|
25
|
+
return tool_result unless synced && deps.lsp_manager.auto_diagnostics?
|
|
26
|
+
|
|
27
|
+
append_diagnostics(deps, written_file, tool_result)
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
# Loop exposes no logger; the old @loop.logger&.debug raised
|
|
30
|
+
# NoMethodError from inside this rescue itself (fresh-eye
|
|
31
|
+
# review, Plan 87). warn keeps the failure visible on stderr;
|
|
32
|
+
# ANSI stripping keeps LSP-server-controlled messages from
|
|
33
|
+
# injecting terminal escape sequences (red-team E1).
|
|
34
|
+
warn("LSP sync skipped: #{e.message.gsub(/\e\[[0-9;]*[A-Za-z]/, '')}")
|
|
35
|
+
tool_result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def append_diagnostics(deps, written_file, tool_result)
|
|
39
|
+
sleep 0.05
|
|
40
|
+
diags = deps.lsp_manager.diagnostics_for(written_file)
|
|
41
|
+
errors = diags.select { |d| d["severity"] == 1 }
|
|
42
|
+
return tool_result if errors.empty?
|
|
43
|
+
|
|
44
|
+
err_lines = errors.map { |e| " • Line #{(e.dig('range', 'start', 'line') || 0) + 1}: #{e['message']}" }.join("\n")
|
|
45
|
+
lsp_msg = "\n\n⚠️ [LSP Real-Time Compiler Diagnostics - #{errors.size} error(s)]:\n#{err_lines}\n" \
|
|
46
|
+
"Please fix these compilation/type errors before proceeding."
|
|
47
|
+
Rubik::Tools::Result.new(
|
|
48
|
+
tool_name: tool_result.tool_name, call_id: tool_result.call_id, status: tool_result.status,
|
|
49
|
+
output: "#{tool_result.output}#{lsp_msg}", duration_ms: tool_result.duration_ms,
|
|
50
|
+
truncated: tool_result.truncated
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../tools/catalog"
|
|
4
|
+
require_relative "tool_history"
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module Orchestrator
|
|
8
|
+
# Maestro kapısı (kullanıcı kararı 2026-10-01, opencode-personal
|
|
9
|
+
# bulgusu): maestro HER modda saf orkestratördür — kök ajanın doğrudan
|
|
10
|
+
# yürütme araç çağrıları ÇALIŞTIRILMADAN reddedilir ve subagent'a
|
|
11
|
+
# yönlendirilir. Yalnız StepExecutor (Loop'un kök yolu) bu modülü
|
|
12
|
+
# içerir: subagent'lar StepRunner'ın kendi dispatch'inden gider, buraya
|
|
13
|
+
# asla uğramaz. Config subagents.maestro_gate ile aç/kapa (default
|
|
14
|
+
# açık); kapı canlı okur ama ajan config'i değiştiremez (workspace.rb
|
|
15
|
+
# Paket D: subagents.* operatör-only) — self-unlock yolu kapalı.
|
|
16
|
+
module MaestroGate
|
|
17
|
+
# Kapı sınıfı Tools::Catalog'un maestro_blocked projeksiyonundan türetil
|
|
18
|
+
# (tek kaynak: her aracın DECLARATION'ı); her :write/:execute araç beyanı
|
|
19
|
+
# açıkça vermek zorundadır — yeni bir mutasyon aracı sessizce kapıdan
|
|
20
|
+
# kaçamaz (no-silent-escape invariant'i,
|
|
21
|
+
# test_tool_declaration_catalog.rb). Yalnız YÜRÜTME araçları kapılı:
|
|
22
|
+
# salt-okunur keşif, storage, ask_question ve finish serbest.
|
|
23
|
+
# manage_task action-bazlıdır: start = run_command eşdeğeri arka plan
|
|
24
|
+
# shell'i; list/status/logs/send_input/kill orkestrasyon komşusu olarak
|
|
25
|
+
# serbest kalır.
|
|
26
|
+
def maestro_gate_blocks?(canonical_name, tool_args = {})
|
|
27
|
+
return false unless Rubik::Tools::Catalog.maestro_blocked?(canonical_name)
|
|
28
|
+
return false if canonical_name == "manage_task" &&
|
|
29
|
+
!["start"].include?((tool_args["action"] || tool_args[:action]).to_s.downcase.strip)
|
|
30
|
+
|
|
31
|
+
# Kapıyı çalışma anında config'den oku (!config set ile operatör
|
|
32
|
+
# aç/kapa mümkün). Gerçek Config dotted fetch'i çözer
|
|
33
|
+
# ("subagents.maestro_gate"); düz Hash (test doubles) nested tutar —
|
|
34
|
+
# dig fallback'i iki şekli de kapsar (dotted-key config gotchası).
|
|
35
|
+
cfg = @deps.shell&.config
|
|
36
|
+
return true unless cfg.respond_to?(:fetch)
|
|
37
|
+
|
|
38
|
+
raw = cfg.fetch("subagents.maestro_gate", nil)
|
|
39
|
+
raw = cfg.dig("subagents", "maestro_gate") if raw.nil? && cfg.respond_to?(:dig)
|
|
40
|
+
raw != false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Red = yönlendirme: mesaj ne olduğunu, neden ve NASIL düzgün
|
|
44
|
+
# yapılacağını söyler (empathetic error, AGENTS.md §5). invalid-tool
|
|
45
|
+
# sayacına dokunmaz — bu çağrı geçersiz değil, devredilmiş bir istek.
|
|
46
|
+
def maestro_refusal(canonical_name, tool_name, call_id)
|
|
47
|
+
result = Rubik::Tools::Result.new(
|
|
48
|
+
tool_name: tool_name,
|
|
49
|
+
call_id: call_id,
|
|
50
|
+
status: :error,
|
|
51
|
+
error: "Maestro gate: '#{canonical_name}' is delegated to subagents.",
|
|
52
|
+
output: "Maestro doctrine: the maestro is a pure orchestrator and never executes '#{canonical_name}' itself — " \
|
|
53
|
+
"execution work belongs to subagents in every mode. This call was NOT executed. Dispatch a subagent instead: " \
|
|
54
|
+
"invoke_subagent with type_name 'coder' (file edits, tests, commands, background processes) or 'research' " \
|
|
55
|
+
"(read-only exploration), then collect results via manage_subagents (action: 'wait' or 'transcript') and " \
|
|
56
|
+
"steer with send_message, or hand follow-ups to a finished subagent with manage_subagents (action: 'redeploy').",
|
|
57
|
+
duration_ms: 0.0
|
|
58
|
+
)
|
|
59
|
+
Rubik::Orchestrator::ToolHistory.write(@deps.history, tool_call_id: call_id, tool_name: tool_name,
|
|
60
|
+
status: :error, duration_ms: 0.0, content: result.to_xml)
|
|
61
|
+
@deps.activity.record(canonical_name, {}, :error) if @deps.respond_to?(:activity) && @deps.activity
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# Deep module (Candidate 03): the ONE home for provider-call recovery
|
|
6
|
+
# policy. Three call sites carried private copies of the same
|
|
7
|
+
# rescue/retry/compact control flow and drifted (a streaming retry that
|
|
8
|
+
# re-used the failed attempt's chunks, a silent subagent compaction,
|
|
9
|
+
# three config-gate spellings). This module owns everything the policy
|
|
10
|
+
# needs and nothing it does not.
|
|
11
|
+
#
|
|
12
|
+
# owns : overflow classification, auto-recover gate, retry-once
|
|
13
|
+
# control flow, compaction-config resolution, request-timeout
|
|
14
|
+
# defaults
|
|
15
|
+
# leaves : the provider call itself (the block); which history is
|
|
16
|
+
# compacted and what observability fires (the compact
|
|
17
|
+
# callable — each site's emergency_compact_and_notify!)
|
|
18
|
+
#
|
|
19
|
+
# Idiom (load-bearing): keep ALL per-attempt state inside the block.
|
|
20
|
+
# `retry` re-executes the begin, which re-yields, which re-runs the
|
|
21
|
+
# block with fresh block-locals — the structural fix for the streaming
|
|
22
|
+
# chunk-pollution bug. Interrupt is not a StandardError and escapes.
|
|
23
|
+
module ProviderRecovery
|
|
24
|
+
# Provider errors matching this are context-window overflows (OpenAI
|
|
25
|
+
# context_length_exceeded, Anthropic "prompt is too long", generic
|
|
26
|
+
# token-limit wording). Collapsed from the three former inline regexes.
|
|
27
|
+
#
|
|
28
|
+
# NOTE: a bare /context/ alternative is deliberately absent. Go-based
|
|
29
|
+
# provider proxies surface transport failures as "context deadline
|
|
30
|
+
# exceeded" / "context canceled" — those must fail (or retry at the
|
|
31
|
+
# transport layer), never trigger the emergency compaction this gate
|
|
32
|
+
# unwinds into, which is destructive and irreversible.
|
|
33
|
+
OVERFLOW_PATTERN = /context_length|context window|maximum context|prompt is too long|token limit|too many tokens/i
|
|
34
|
+
|
|
35
|
+
# Config gate for the automatic compaction+retry (default: on).
|
|
36
|
+
AUTO_RECOVER_KEY = "context.auto_recover_on_overflow"
|
|
37
|
+
|
|
38
|
+
# Config section every compaction call site resolves.
|
|
39
|
+
COMPACTION_CONFIG_KEY = "context"
|
|
40
|
+
|
|
41
|
+
# Named request-timeout defaults. Per-site values preserved on
|
|
42
|
+
# purpose: a subagent step is a short focused call; the shell's
|
|
43
|
+
# streaming turn may legitimately think longer.
|
|
44
|
+
DEFAULT_OPEN_TIMEOUT = 15
|
|
45
|
+
SUBAGENT_REQUEST_TIMEOUT = 90
|
|
46
|
+
STREAM_REQUEST_TIMEOUT = 120
|
|
47
|
+
|
|
48
|
+
module_function
|
|
49
|
+
|
|
50
|
+
# Executes the provider call with context-overflow recovery. On the
|
|
51
|
+
# first overflow-classified StandardError: run the site's compact
|
|
52
|
+
# callable (reason = the provider's error message), then retry the
|
|
53
|
+
# block exactly once. Everything else re-raises untouched, including
|
|
54
|
+
# Interrupt (not a StandardError) and errors raised by compact itself
|
|
55
|
+
# (fail loud — half-recovered state must surface, never be swallowed).
|
|
56
|
+
def with_recovery(config:, compact:)
|
|
57
|
+
retry_attempted = false
|
|
58
|
+
begin
|
|
59
|
+
yield
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
raise e unless !retry_attempted && auto_recover?(config) && overflow_error?(e)
|
|
62
|
+
|
|
63
|
+
retry_attempted = true
|
|
64
|
+
compact.call(e.message)
|
|
65
|
+
retry
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Defensive resolution of the "context" config section shared by every
|
|
70
|
+
# compaction call site: nil config, fetch-less objects, and null YAML
|
|
71
|
+
# sections all collapse to {}. One spelling replaces five.
|
|
72
|
+
def compaction_config(config)
|
|
73
|
+
cfg = config.respond_to?(:fetch) ? config.fetch(COMPACTION_CONFIG_KEY, {}) : {}
|
|
74
|
+
cfg.is_a?(Hash) ? cfg : {}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# ── implementation details (not interface) ────────────────────────
|
|
78
|
+
|
|
79
|
+
def auto_recover?(config)
|
|
80
|
+
config.respond_to?(:fetch) ? config.fetch(AUTO_RECOVER_KEY, true) : true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def overflow_error?(err)
|
|
84
|
+
err.message.to_s.match?(OVERFLOW_PATTERN)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private_class_method :auto_recover?, :overflow_error?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# Provider-scoped configuration resolution for the orchestrator loop
|
|
6
|
+
# (review Öneri 3; extracted from Loop's provider-config section).
|
|
7
|
+
#
|
|
8
|
+
# Owns two lookups:
|
|
9
|
+
# - registry_key: registry-style key of the active provider — the
|
|
10
|
+
# explicit provider name (downcased, the way the registry stores
|
|
11
|
+
# them) when it carries one, else the class name derived the way
|
|
12
|
+
# builtin providers are registered (Rubik::LLM::OllamaProvider ->
|
|
13
|
+
# "ollama"). An empty name counts as unnamed, so instances built
|
|
14
|
+
# outside the registry keep falling back.
|
|
15
|
+
# - llm_config: per-provider llm.* override — providers.<key>.<field>
|
|
16
|
+
# beats the global llm.<field>, so a provider-specific quirk (e.g. a
|
|
17
|
+
# broken SSE stack) can be tuned without flipping the knob for every
|
|
18
|
+
# provider. A scoped nil counts as unset — also the only way to
|
|
19
|
+
# clear an override back to the global value.
|
|
20
|
+
#
|
|
21
|
+
# The config object is held by reference and values are re-read on
|
|
22
|
+
# every fetch, so !config set / !provider set still apply from the
|
|
23
|
+
# very next turn (config reactivity, Plan 75).
|
|
24
|
+
class ProviderResolver
|
|
25
|
+
def initialize(provider:, config:)
|
|
26
|
+
@provider = provider
|
|
27
|
+
@config = config
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Registry-style key for the active provider: the explicit provider name
|
|
31
|
+
# when it carries one (downcased, the way the registry stores them),
|
|
32
|
+
# else the class name derived the way builtin providers are registered
|
|
33
|
+
# (Rubik::LLM::OllamaProvider -> "ollama"). An empty name counts as
|
|
34
|
+
# unnamed, so instances built outside the registry keep falling back.
|
|
35
|
+
def registry_key
|
|
36
|
+
explicit = @provider.respond_to?(:name) ? @provider.name.to_s.downcase : ""
|
|
37
|
+
return explicit unless explicit.empty?
|
|
38
|
+
|
|
39
|
+
klass_name = @provider.class.name
|
|
40
|
+
base = klass_name&.split("::")&.last
|
|
41
|
+
base&.delete_suffix("Provider")&.downcase
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Per-provider llm.* override: providers.<key>.<field> beats the global
|
|
45
|
+
# llm.<field>, so a provider-specific quirk (e.g. a broken SSE stack)
|
|
46
|
+
# can be tuned without flipping the knob for every provider. Re-read on
|
|
47
|
+
# each request: !config set / !provider set apply from the very next
|
|
48
|
+
# turn. A scoped nil counts as unset — also the only way to clear an
|
|
49
|
+
# override back to the global value.
|
|
50
|
+
def llm_config(key, default)
|
|
51
|
+
prov_key = registry_key
|
|
52
|
+
if prov_key.is_a?(String) && !prov_key.empty? && @config.respond_to?(:fetch)
|
|
53
|
+
val = @config.fetch("providers.#{prov_key}.#{key}", nil)
|
|
54
|
+
return val unless val.nil?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Two-arg fetch, not safe_config: safe_config's `fetch(...) || default`
|
|
58
|
+
# would coerce an explicit false (llm.stream=false) into the truthy
|
|
59
|
+
# default, and a one-arg fetch on a plain-Hash config (tests) raises
|
|
60
|
+
# KeyError instead of falling through.
|
|
61
|
+
return default unless @config.respond_to?(:fetch)
|
|
62
|
+
|
|
63
|
+
@config.fetch("llm.#{key}", default)
|
|
64
|
+
rescue StandardError
|
|
65
|
+
default
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|