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,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "provider"
|
|
4
|
+
require_relative "client"
|
|
5
|
+
require_relative "formatter"
|
|
6
|
+
|
|
7
|
+
module Rubik
|
|
8
|
+
module LLM
|
|
9
|
+
class AnthropicProvider < BaseProvider
|
|
10
|
+
attr_reader :name, :model, :formatter, :headers, :api_key, :api_key_env
|
|
11
|
+
|
|
12
|
+
def initialize(name: nil, model: "claude-3-7-sonnet-20250219", formatter: AnthropicFormatter.new, headers: {},
|
|
13
|
+
api_key: nil, api_key_env: nil)
|
|
14
|
+
super()
|
|
15
|
+
@name = name.to_s.downcase
|
|
16
|
+
@model = model
|
|
17
|
+
@formatter = formatter || AnthropicFormatter.new
|
|
18
|
+
@headers = headers || {}
|
|
19
|
+
# Plan: Anthropic api_key / api_key_env were silently dropped from the
|
|
20
|
+
# provider spec, so every Anthropic deployment (custom or registered
|
|
21
|
+
# via config) failed auth with no diagnostic. Both a direct api_key and
|
|
22
|
+
# an env-var source are honored, mirroring OpenAICompatibleProvider.
|
|
23
|
+
@api_key = api_key
|
|
24
|
+
@api_key_env = api_key_env
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Resolve the Anthropic key from (in order of precedence): an explicit
|
|
28
|
+
# per-call opts[:api_key], the provider-level @api_key, and finally the
|
|
29
|
+
# environment variable named by @api_key_env (default ANTHROPIC_API_KEY).
|
|
30
|
+
def resolve_api_key(opts = {})
|
|
31
|
+
return opts[:api_key] if opts.key?(:api_key) && !opts[:api_key].nil?
|
|
32
|
+
return @api_key if @api_key && !@api_key.to_s.empty?
|
|
33
|
+
|
|
34
|
+
env_name = opts[:api_key_env] || @api_key_env || "ANTHROPIC_API_KEY"
|
|
35
|
+
ENV.fetch(env_name, nil)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def complete(messages, opts = {})
|
|
39
|
+
api_key = resolve_api_key(opts)
|
|
40
|
+
unless api_key && !api_key.to_s.empty?
|
|
41
|
+
raise ClientError, "No Anthropic API key available: set ANTHROPIC_API_KEY, pass api_key in config, " \
|
|
42
|
+
"or configure api_key_env for the 'anthropic' provider."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
messages = Rubik::LLM::MessageSanitizer.normalize(messages)
|
|
46
|
+
target_model = opts[:model] || @model
|
|
47
|
+
# Allow endpoint override (opts[:endpoint] for tests/proxies)
|
|
48
|
+
endpoint = opts[:endpoint] || "https://api.anthropic.com/v1/messages"
|
|
49
|
+
|
|
50
|
+
headers = {
|
|
51
|
+
"x-api-key" => api_key,
|
|
52
|
+
"anthropic-version" => "2023-06-01"
|
|
53
|
+
}
|
|
54
|
+
@headers.each { |k, v| headers[k.to_s] = v.to_s }
|
|
55
|
+
headers.merge!(opts[:headers]) if opts[:headers].is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
# Messages API contract: exactly one top-level `system` field is
|
|
58
|
+
# allowed, so multiple system messages are deliberately joined here,
|
|
59
|
+
# never sent inline.
|
|
60
|
+
system_msgs, user_msgs = messages.partition { |m| m[:role] == "system" || m["role"] == "system" }
|
|
61
|
+
|
|
62
|
+
system_text = system_msgs.map { |m| m[:content] || m["content"] }.join("\n\n")
|
|
63
|
+
payload = build_payload(target_model, user_msgs, system_text, opts, stream: false)
|
|
64
|
+
|
|
65
|
+
timeout = opts[:timeout] || opts[:stream_timeout] || 120
|
|
66
|
+
open_timeout = opts[:open_timeout] || 15
|
|
67
|
+
|
|
68
|
+
res_json = HTTPClient.post_json(
|
|
69
|
+
endpoint,
|
|
70
|
+
headers: headers,
|
|
71
|
+
payload: payload,
|
|
72
|
+
timeout: timeout,
|
|
73
|
+
open_timeout: open_timeout,
|
|
74
|
+
on_retry: opts[:on_retry]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
content_blocks = res_json["content"] || []
|
|
78
|
+
text_blocks = content_blocks.select { |b| b["type"] == "text" }.map { |b| b["text"] }
|
|
79
|
+
text = text_blocks.join("\n")
|
|
80
|
+
|
|
81
|
+
thought_blocks = content_blocks.select { |b| b["type"] == "thinking" }.map { |b| b["thinking"] }
|
|
82
|
+
thought = thought_blocks.empty? ? nil : thought_blocks.join("\n")
|
|
83
|
+
|
|
84
|
+
tool_blocks = content_blocks.select { |b| b["type"] == "tool_use" }
|
|
85
|
+
parsed_tool_calls = tool_blocks.map do |b|
|
|
86
|
+
{
|
|
87
|
+
"id" => b["id"] || "call_#{rand(1000..9999)}",
|
|
88
|
+
"tool" => b["name"],
|
|
89
|
+
"args" => b["input"] || {}
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
usage = res_json["usage"] || {}
|
|
94
|
+
|
|
95
|
+
Response.new(
|
|
96
|
+
text: text,
|
|
97
|
+
usage: {
|
|
98
|
+
input_tokens: usage["input_tokens"] || 0,
|
|
99
|
+
output_tokens: usage["output_tokens"] || 0
|
|
100
|
+
},
|
|
101
|
+
raw: res_json,
|
|
102
|
+
tool_calls: parsed_tool_calls,
|
|
103
|
+
thought: thought
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Streaming variant. Yields Chunks. Without a block, returns an
|
|
108
|
+
# Enumerator (via `to_enum`) so the caller can chain `.each`.
|
|
109
|
+
def stream_chat(messages, opts = {}, &block)
|
|
110
|
+
api_key = resolve_api_key(opts)
|
|
111
|
+
unless api_key && !api_key.to_s.empty?
|
|
112
|
+
raise ClientError, "No Anthropic API key available: set ANTHROPIC_API_KEY, pass api_key in config, " \
|
|
113
|
+
"or configure api_key_env for the 'anthropic' provider."
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
messages = Rubik::LLM::MessageSanitizer.normalize(messages)
|
|
117
|
+
target_model = opts[:model] || @model
|
|
118
|
+
# Allow endpoint override (opts[:endpoint] for tests/proxies;
|
|
119
|
+
# default to the public Anthropic API).
|
|
120
|
+
endpoint = opts[:endpoint] || "https://api.anthropic.com/v1/messages"
|
|
121
|
+
|
|
122
|
+
headers = {
|
|
123
|
+
"x-api-key" => api_key,
|
|
124
|
+
"anthropic-version" => "2023-06-01"
|
|
125
|
+
}
|
|
126
|
+
@headers.each { |k, v| headers[k.to_s] = v.to_s }
|
|
127
|
+
headers.merge!(opts[:headers]) if opts[:headers].is_a?(Hash)
|
|
128
|
+
|
|
129
|
+
# Same Messages API contract as #complete: exactly one top-level
|
|
130
|
+
# `system` field is allowed, so multiple system messages are
|
|
131
|
+
# deliberately joined here rather than passed inline.
|
|
132
|
+
system_msgs, user_msgs = messages.partition { |m| m[:role] == "system" || m["role"] == "system" }
|
|
133
|
+
system_text = system_msgs.map { |m| m[:content] || m["content"] }.join("\n\n")
|
|
134
|
+
|
|
135
|
+
payload = build_payload(target_model, user_msgs, system_text, opts, stream: true)
|
|
136
|
+
|
|
137
|
+
return to_enum(:stream_chat, messages, opts) unless block
|
|
138
|
+
|
|
139
|
+
timeout = opts[:idle_timeout] || opts[:timeout] || opts[:stream_timeout] || 120
|
|
140
|
+
open_timeout = opts[:open_timeout] || 15
|
|
141
|
+
|
|
142
|
+
HTTPClient.post_stream_json(
|
|
143
|
+
endpoint,
|
|
144
|
+
headers: headers,
|
|
145
|
+
payload: payload,
|
|
146
|
+
timeout: timeout,
|
|
147
|
+
open_timeout: open_timeout,
|
|
148
|
+
idle_timeout: opts[:idle_timeout]
|
|
149
|
+
) do |event|
|
|
150
|
+
chunk = @formatter.parse_stream_event(event)
|
|
151
|
+
next if chunk.nil?
|
|
152
|
+
|
|
153
|
+
block.call(chunk)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def build_payload(target_model, user_msgs, system_text, opts, stream: false)
|
|
160
|
+
max_tokens = (opts[:max_tokens] || 1024).to_i
|
|
161
|
+
payload = {
|
|
162
|
+
model: target_model,
|
|
163
|
+
messages: format_messages(user_msgs),
|
|
164
|
+
max_tokens: max_tokens
|
|
165
|
+
}
|
|
166
|
+
payload[:stream] = true if stream
|
|
167
|
+
payload[:system] = system_text unless system_text.to_s.empty?
|
|
168
|
+
payload[:tools] = format_tools(opts[:tools]) if opts[:tools] && !opts[:tools].empty?
|
|
169
|
+
|
|
170
|
+
thinking_opt = opts[:thinking]
|
|
171
|
+
thinking_disabled = [false, :disabled].include?(thinking_opt)
|
|
172
|
+
ratio = (opts[:thinking_budget_ratio] || 0.5).to_f
|
|
173
|
+
|
|
174
|
+
if thinking_opt.is_a?(Hash)
|
|
175
|
+
payload[:thinking] = thinking_opt
|
|
176
|
+
elsif !thinking_disabled && (thinking_opt == true || target_model.to_s.include?("claude-3-7-sonnet"))
|
|
177
|
+
if max_tokens > 2048
|
|
178
|
+
budget = [(max_tokens * ratio).to_i, 1024].max
|
|
179
|
+
budget = [budget, max_tokens - 1024].min
|
|
180
|
+
payload[:thinking] = { type: "enabled", budget_tokens: budget } if budget >= 1024
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if payload[:thinking]
|
|
185
|
+
payload[:temperature] = 1.0 if opts.key?(:temperature)
|
|
186
|
+
elsif opts.key?(:temperature)
|
|
187
|
+
payload[:temperature] = opts[:temperature]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
payload
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def format_messages(messages)
|
|
194
|
+
@formatter.format_messages(messages)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def format_tools(tools)
|
|
198
|
+
@formatter.format_tools(tools)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Red-team: JSON.parse is used in the args paths; without this require a
|
|
4
|
+
# standalone load (or any load order where json is not yet loaded) turns
|
|
5
|
+
# every parse into a NameError that the StandardError rescue silently
|
|
6
|
+
# converts to empty args — tool arguments vanish without a trace.
|
|
7
|
+
require "json"
|
|
8
|
+
|
|
9
|
+
require_relative "provider"
|
|
10
|
+
|
|
11
|
+
module Rubik
|
|
12
|
+
module LLM
|
|
13
|
+
class Chunk
|
|
14
|
+
# Incremental, single-pass assembler (Plan 87 E-2, carrying Plan 86
|
|
15
|
+
# D-1): each pushed chunk updates O(1) accumulators — text/thought
|
|
16
|
+
# buffers, tool-call slots, last-writer-wins usage/raw/finish state —
|
|
17
|
+
# so batch callers never buffer the chunk list. #finish produces the
|
|
18
|
+
# exact Response the multi-pass ChunkAssembler.build produces; the
|
|
19
|
+
# equivalence is pinned by the golden snapshots and the build ↔
|
|
20
|
+
# incremental equivalence test in test_chunk_assembler.rb.
|
|
21
|
+
#
|
|
22
|
+
# Known accepted divergence (same class as Red-team B3): build runs
|
|
23
|
+
# the tool_use_delta pass over ALL chunks before the raw pass, while
|
|
24
|
+
# push interleaves both per chunk. For same-index tool calls arriving
|
|
25
|
+
# as one raw message and one delta with DIFFERENT ids, the resulting
|
|
26
|
+
# slot order may differ; runtime matching is by call id, not order.
|
|
27
|
+
class Assembler
|
|
28
|
+
def initialize
|
|
29
|
+
@text = +""
|
|
30
|
+
@thought = +""
|
|
31
|
+
@tool_calls_map = {}
|
|
32
|
+
@last_usage = nil
|
|
33
|
+
@last_raw = nil
|
|
34
|
+
@finish_raw = nil
|
|
35
|
+
@pushed = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether any chunk reached the assembler (used by #complete to
|
|
39
|
+
# decide the Enumerator drain fallback).
|
|
40
|
+
def pushed?
|
|
41
|
+
@pushed
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def push(chunk)
|
|
45
|
+
@pushed = true
|
|
46
|
+
@text << chunk.text_delta.to_s unless chunk.text_delta.nil?
|
|
47
|
+
@thought << chunk.reasoning_delta.to_s if chunk.respond_to?(:reasoning_delta?) && chunk.reasoning_delta? && !chunk.reasoning_delta.nil?
|
|
48
|
+
|
|
49
|
+
deltas = chunk.tool_use_delta
|
|
50
|
+
unless deltas.nil?
|
|
51
|
+
deltas_array = deltas.is_a?(Array) ? deltas : [deltas]
|
|
52
|
+
deltas_array.each { |d| process_tool_delta(d) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
process_raw_tool_calls(chunk.raw) if chunk.raw.is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
@last_usage = chunk.usage if chunk.usage && !chunk.usage.empty?
|
|
58
|
+
@last_raw = chunk.raw if chunk.raw && !chunk.raw.empty?
|
|
59
|
+
@finish_raw = chunk.raw if chunk.raw.is_a?(Hash) && extract_finish_value(chunk.raw)
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def finish(streamed: false, estimate_history: nil, estimate_text: nil, on_malformed_args: nil)
|
|
64
|
+
tool_calls, degenerate = parsed_tool_calls_with_degenerate(on_malformed_args)
|
|
65
|
+
@text = append_degenerate_text(degenerate)
|
|
66
|
+
final_usage = resolve_usage(@text, estimate_history, estimate_text)
|
|
67
|
+
|
|
68
|
+
Rubik::LLM::Response.new(
|
|
69
|
+
text: @text,
|
|
70
|
+
usage: final_usage,
|
|
71
|
+
raw: promoted_raw,
|
|
72
|
+
tool_calls: tool_calls,
|
|
73
|
+
streamed: streamed == true,
|
|
74
|
+
thought: @thought.empty? ? nil : @thought
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def process_tool_delta(delta)
|
|
81
|
+
return unless delta.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
base_idx = delta["index"] || delta[:index] || 0
|
|
84
|
+
d_id = delta["id"] || delta[:id]
|
|
85
|
+
d_tool = delta["tool"] || delta[:tool] || delta["name"] || delta[:name]
|
|
86
|
+
|
|
87
|
+
key = resolve_slot(base_idx, d_id)
|
|
88
|
+
@tool_calls_map[key]["id"] ||= d_id
|
|
89
|
+
@tool_calls_map[key]["tool"] ||= d_tool
|
|
90
|
+
@tool_calls_map[key]["args_json"] += (delta["arguments_delta"] || delta[:arguments_delta]).to_s if delta["arguments_delta"] || delta[:arguments_delta]
|
|
91
|
+
@tool_calls_map[key]["args"] = delta["args"] || delta[:args] if delta["args"] || delta[:args]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def process_raw_tool_calls(raw)
|
|
95
|
+
if raw["type"] == "content_block_start" && raw["content_block"].is_a?(Hash)
|
|
96
|
+
cb = raw["content_block"]
|
|
97
|
+
if cb["type"] == "tool_use"
|
|
98
|
+
key = resolve_slot(raw["index"] || 0, cb["id"])
|
|
99
|
+
@tool_calls_map[key]["id"] ||= cb["id"]
|
|
100
|
+
@tool_calls_map[key]["tool"] ||= cb["name"]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
return unless raw["message"] && raw["message"]["tool_calls"].is_a?(Array)
|
|
105
|
+
|
|
106
|
+
raw["message"]["tool_calls"].each_with_index do |tc, idx|
|
|
107
|
+
fn = tc["function"] || {}
|
|
108
|
+
key = resolve_slot(idx, tc["id"])
|
|
109
|
+
@tool_calls_map[key]["id"] ||= tc["id"]
|
|
110
|
+
@tool_calls_map[key]["tool"] ||= fn["name"]
|
|
111
|
+
@tool_calls_map[key]["args"] ||= fn["arguments"]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Red-team B2: üç montaj yolu (tool_use_delta, content_block_start, raw
|
|
116
|
+
# message) aynı slot-çözme mantığını kullansın. Aynı index üzerinde dolu ve
|
|
117
|
+
# farklı bir id görüldüğünde ayrı bir slot açılır: InternLM/lmdeploy ve bazı
|
|
118
|
+
# stream'ler aynı index'i yeni bir logical call için yeniden kullanabilir;
|
|
119
|
+
# katıştırmak arguments'ları karıştırıp bozuk JSON'a yol açar. Boş/nil id bir
|
|
120
|
+
# call'ı tanımlamaz — split'i tetiklemez.
|
|
121
|
+
#
|
|
122
|
+
# Red-team notları (bilinen sınırlar):
|
|
123
|
+
# - B3: `tool_calls_map` insertion-order üretir; interleaved delta sırası
|
|
124
|
+
# `values` sırasını değiştirebilir. Runtime'da zararsızdır çünkü sonraki
|
|
125
|
+
# eşleşme call id/tool_call_id üzerindendir, sıra değil.
|
|
126
|
+
# - B4: aynı index üzerinde id'siz iki FARKLI call ayırt edilemez (bilgi
|
|
127
|
+
# yok). lmdeploy her call'a en az bir id ya da benzersiz index verdiği
|
|
128
|
+
# sürece oluşmaz.
|
|
129
|
+
# - B5: composite key `"#{base_idx}:#{d_id}"`; d_id içinde ':' olsa bile
|
|
130
|
+
# farklı key'ler ayrı kalır (çakışma yok, çünkü tam-string eşleşmesi).
|
|
131
|
+
def resolve_slot(base_idx, d_id)
|
|
132
|
+
key = base_idx
|
|
133
|
+
if @tool_calls_map.key?(base_idx) &&
|
|
134
|
+
!d_id.nil? && !d_id.to_s.empty? &&
|
|
135
|
+
!@tool_calls_map[base_idx]["id"].nil? &&
|
|
136
|
+
@tool_calls_map[base_idx]["id"] != d_id
|
|
137
|
+
key = "#{base_idx}:#{d_id}"
|
|
138
|
+
end
|
|
139
|
+
@tool_calls_map[key] ||= { "id" => nil, "tool" => nil, "args_json" => "", "args" => nil }
|
|
140
|
+
key
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def parsed_tool_calls_with_degenerate(on_malformed_args)
|
|
144
|
+
parsed_tool_calls = @tool_calls_map.values.map do |tc_data|
|
|
145
|
+
args = tc_data["args"]
|
|
146
|
+
malformed = nil
|
|
147
|
+
if args.nil?
|
|
148
|
+
args_str = tc_data["args_json"].strip
|
|
149
|
+
if args_str.empty?
|
|
150
|
+
args = {}
|
|
151
|
+
else
|
|
152
|
+
begin
|
|
153
|
+
parsed = JSON.parse(args_str)
|
|
154
|
+
if parsed.is_a?(Hash)
|
|
155
|
+
args = parsed
|
|
156
|
+
else
|
|
157
|
+
args = {}
|
|
158
|
+
malformed = args_str
|
|
159
|
+
end
|
|
160
|
+
rescue JSON::ParserError
|
|
161
|
+
on_malformed_args&.call(tc_data["tool"], args_str)
|
|
162
|
+
args = {}
|
|
163
|
+
malformed = args_str
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
elsif args.is_a?(String)
|
|
167
|
+
begin
|
|
168
|
+
parsed = JSON.parse(args)
|
|
169
|
+
if parsed.is_a?(Hash)
|
|
170
|
+
args = parsed
|
|
171
|
+
else
|
|
172
|
+
malformed = args
|
|
173
|
+
args = {}
|
|
174
|
+
end
|
|
175
|
+
rescue JSON::ParserError
|
|
176
|
+
on_malformed_args&.call(tc_data["tool"], args)
|
|
177
|
+
malformed = args
|
|
178
|
+
args = {}
|
|
179
|
+
end
|
|
180
|
+
elsif !args.is_a?(Hash)
|
|
181
|
+
malformed = args.to_s
|
|
182
|
+
args = {}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
row = {
|
|
186
|
+
"id" => tc_data["id"] || "call_#{rand(1000..9999)}",
|
|
187
|
+
"tool" => tc_data["tool"],
|
|
188
|
+
"args" => args || {}
|
|
189
|
+
}
|
|
190
|
+
# Issue #55 3.4: the executor turns this marker into the
|
|
191
|
+
# model-visible error row quoting the rejected payload — the
|
|
192
|
+
# degrade must not be silent at both layers.
|
|
193
|
+
row["malformed_args"] = malformed if malformed
|
|
194
|
+
row
|
|
195
|
+
end
|
|
196
|
+
# Red-team B1: tool name'siz yarım call'ı sessizce silmek, üretilmiş
|
|
197
|
+
# arguments'ı da yok eder ve (text boşken) "Empty response from model"a
|
|
198
|
+
# yol açabilir. Arguments'ı metne bağlayarak bilgiyi koruyor ve modelin
|
|
199
|
+
# boş olmayan bir yanıt görmesini sağlıyoruz; gerçekten boş call'lar elenir.
|
|
200
|
+
degenerate = []
|
|
201
|
+
parsed_tool_calls = parsed_tool_calls.reject do |tc|
|
|
202
|
+
if tc["tool"].nil? || tc["tool"].to_s.empty?
|
|
203
|
+
args = tc["args"]
|
|
204
|
+
degenerate << args if args.is_a?(Hash) && !args.empty?
|
|
205
|
+
true
|
|
206
|
+
else
|
|
207
|
+
false
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
[parsed_tool_calls, degenerate]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def append_degenerate_text(degenerate)
|
|
214
|
+
return @text if degenerate.empty?
|
|
215
|
+
|
|
216
|
+
appended = degenerate.map { |a| "[Incomplete tool call — arguments: #{a.to_json}]" }.join(" ")
|
|
217
|
+
@text.to_s.empty? ? appended : "#{@text}\n#{appended}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def resolve_usage(text, estimate_history, estimate_text)
|
|
221
|
+
in_tokens = @last_usage && (@last_usage[:input_tokens] || @last_usage["input_tokens"])
|
|
222
|
+
out_tokens = @last_usage && (@last_usage[:output_tokens] || @last_usage["output_tokens"])
|
|
223
|
+
|
|
224
|
+
in_tokens ||= estimate_history.respond_to?(:call) ? estimate_history.call : nil
|
|
225
|
+
out_tokens = estimate_text.call(text) || (text.length / 4.0).ceil if out_tokens.nil? && estimate_text.respond_to?(:call)
|
|
226
|
+
|
|
227
|
+
if in_tokens || out_tokens
|
|
228
|
+
{ input_tokens: in_tokens || 0, output_tokens: out_tokens || 0 }
|
|
229
|
+
else
|
|
230
|
+
{}
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def promoted_raw
|
|
235
|
+
last_raw = @last_raw || {}
|
|
236
|
+
if @finish_raw && !extract_finish_value(last_raw)
|
|
237
|
+
last_raw = last_raw.dup
|
|
238
|
+
finish_val = @finish_raw["finish_reason"] || @finish_raw.dig("choices", 0, "finish_reason")
|
|
239
|
+
last_raw["finish_reason"] = finish_val if finish_val
|
|
240
|
+
stop_val = @finish_raw["stop_reason"] || @finish_raw.dig("delta", "stop_reason")
|
|
241
|
+
last_raw["stop_reason"] = stop_val if stop_val
|
|
242
|
+
last_raw["done_reason"] = @finish_raw["done_reason"] if @finish_raw["done_reason"]
|
|
243
|
+
end
|
|
244
|
+
last_raw
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def extract_finish_value(raw)
|
|
248
|
+
return nil unless raw.is_a?(Hash)
|
|
249
|
+
|
|
250
|
+
raw["finish_reason"] || raw.dig("choices", 0, "finish_reason") ||
|
|
251
|
+
raw["stop_reason"] || raw.dig("delta", "stop_reason") || raw["done_reason"]
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Assembles a final Response from accumulated stream Chunks (Plan 61
|
|
257
|
+
# D-4). Single source of truth shared by Orchestrator::StreamManager
|
|
258
|
+
# (live agent loop) and BaseProvider#complete (batch fallback for
|
|
259
|
+
# stream-only providers), so both paths produce identical text,
|
|
260
|
+
# tool_calls and usage — provider-agnostic assembly is the invariant.
|
|
261
|
+
#
|
|
262
|
+
# Optional callbacks:
|
|
263
|
+
# estimate_history:: ->( ) { Integer } input-token fallback (loop history estimate)
|
|
264
|
+
# estimate_text:: ->(text) { Integer } output-token fallback (provider estimator)
|
|
265
|
+
# on_malformed_args:: ->(tool_name, fragment) — D-10 diagnostics for
|
|
266
|
+
# tool-call arguments that arrived truncated/unparseable; the call
|
|
267
|
+
# still executes with {} args so the model sees the tool's complaint.
|
|
268
|
+
module ChunkAssembler
|
|
269
|
+
module_function
|
|
270
|
+
|
|
271
|
+
# Plan 87 E-2: build feeds the chunk list through the incremental
|
|
272
|
+
# Chunk::Assembler — one assembly logic, two consumption styles.
|
|
273
|
+
def build(chunks, streamed: false, estimate_history: nil, estimate_text: nil, on_malformed_args: nil)
|
|
274
|
+
assembler = Chunk::Assembler.new
|
|
275
|
+
chunks.each { |chunk| assembler.push(chunk) }
|
|
276
|
+
assembler.finish(
|
|
277
|
+
streamed: streamed,
|
|
278
|
+
estimate_history: estimate_history,
|
|
279
|
+
estimate_text: estimate_text,
|
|
280
|
+
on_malformed_args: on_malformed_args
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|