drydock-cli 3.1.36__tar.gz → 3.1.37__tar.gz
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.
- {drydock_cli-3.1.36/drydock_cli.egg-info → drydock_cli-3.1.37}/PKG-INFO +1 -1
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/agent.py +12 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/cli.py +9 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/compaction.py +13 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/providers.py +43 -7
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tui/app.py +1 -1
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tuning.py +21 -3
- {drydock_cli-3.1.36 → drydock_cli-3.1.37/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/pyproject.toml +1 -1
- drydock_cli-3.1.37/tests/test_text_only_model.py +78 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tuning.py +11 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/LICENSE +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/NOTICE +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/README.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/__init__.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/__main__.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/advisor.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/asr.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/bottleneck.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/budget.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/document-canvas.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/nist-csf.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/capacity.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/cci.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/comms/__init__.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/comms/attention.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/comms/channels.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/comms/events.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/config.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/detect.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/doccanvas.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/eratchet.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/events.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/extract.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/gittools.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/graphrag.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/groundtruth.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/guards.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/jobs.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/mcp.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/mission.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/mission_cli.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/mission_run.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/pdfredbox.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/phases.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/poam.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/predictions.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/progress.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/ratchet.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/recipes.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/recovery.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/resume.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/rmf.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/skills.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/stig.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/subagents.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/suggest.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/swarm.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/task_state.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tool_policy.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tool_result.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tool_select.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tool_validate.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/trajectory.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/verification.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/web.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/setup.cfg +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_advisor.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_approval.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_asr.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_back_command.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bottleneck.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_budget.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_capacity.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_cci.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_comms.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_compaction.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_config.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_context_limit_issue25.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_criteria_coverage.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_cycling_detection.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_declared_deps.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_degenerate_argument.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_detect.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_doccanvas.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_edit_thrash_and_compaction.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_effort_governor.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_eratchet.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_events.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_extract.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_gittools.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_graphrag_sqlite.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_groundtruth_predictions.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_jobs.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_knowledge_tool_available.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_main_api_key.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_mcp.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_mission.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_mission_cli.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_mission_run.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_model_registry.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_pdfredbox.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_phases.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_poam.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_progress.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_ratchet.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_ratchet_offer.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_read_index.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_recipes.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_recovery.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_recovery_config.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_recovery_integration.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_repeated_outcome.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_resume_events.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_resume_restore.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_resume_snapshot.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_rmf.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_rolling_plan.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_skills.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_sqlite_events.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_stig.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_stop.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_subagent.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_subagents.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_suggest.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_swarm.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_task_state.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_timeline.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_todo.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_canonical.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_policy.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_result.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_select.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_validate.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_trajectory.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tui.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_worker_subagent.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_xccdf.py +0 -0
|
@@ -47,6 +47,7 @@ from drydock.tools import register_all
|
|
|
47
47
|
register_all()
|
|
48
48
|
from drydock.compaction import (
|
|
49
49
|
maybe_compact, emergency_compact, is_context_length_error, is_image_load_error,
|
|
50
|
+
is_text_only_model_error,
|
|
50
51
|
extract_server_n_ctx,
|
|
51
52
|
)
|
|
52
53
|
from drydock.loop_detect import LoopTracker, degenerate_argument
|
|
@@ -409,6 +410,17 @@ def run(
|
|
|
409
410
|
if retries >= 2:
|
|
410
411
|
raise
|
|
411
412
|
yield TextChunk("\n[context limit hit — compacting and retrying...]\n")
|
|
413
|
+
elif is_text_only_model_error(err):
|
|
414
|
+
# The model can't take images at all. Stop attaching them for this
|
|
415
|
+
# endpoint and retry the step as text — a vision-less model can
|
|
416
|
+
# still do the task (read the file with tools), it just can't SEE it.
|
|
417
|
+
from drydock.providers import mark_text_only
|
|
418
|
+
if not mark_text_only(config):
|
|
419
|
+
raise
|
|
420
|
+
yield TextChunk(
|
|
421
|
+
"\n[This model is text-only — sending image references as plain "
|
|
422
|
+
"text from now on and retrying...]\n"
|
|
423
|
+
)
|
|
412
424
|
elif is_image_load_error(err):
|
|
413
425
|
# Corrupt/truncated/unsupported image the server couldn't decode —
|
|
414
426
|
# end the turn cleanly rather than dumping the raw 400.
|
|
@@ -507,6 +507,15 @@ def main():
|
|
|
507
507
|
# the PRD" content fought the system prompt and risked turning a plain
|
|
508
508
|
# greeting into a runaway build. The TUI reads this from config; CLI modes
|
|
509
509
|
# call load_project_instructions directly (see run_interactive/run_oneshot).
|
|
510
|
+
# A context_limit above the server's real window overflows it mid-task and the
|
|
511
|
+
# ctx gauge lies. Unless the user pinned --context-limit, ask the server once and
|
|
512
|
+
# adopt its window when smaller (never grow past config). Best-effort, short timeout.
|
|
513
|
+
if not args.context_limit and config.get("base_url"):
|
|
514
|
+
from drydock.providers import probe_server_context
|
|
515
|
+
n_ctx = probe_server_context(config["base_url"], timeout=2.0)
|
|
516
|
+
if n_ctx and n_ctx < int(config.get("context_limit") or 65536):
|
|
517
|
+
config["context_limit"] = n_ctx
|
|
518
|
+
|
|
510
519
|
config["project_instructions"] = load_project_instructions()
|
|
511
520
|
# In a git project, drop a discoverable per-project prompt template at
|
|
512
521
|
# <project>/.drydock/system_prompt.md (inert until edited). Then load the
|
|
@@ -51,6 +51,19 @@ def extract_server_n_ctx(err: str) -> int | None:
|
|
|
51
51
|
return None
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def is_text_only_model_error(err: str) -> bool:
|
|
55
|
+
"""Whether a provider 400 says the MODEL can't take images at all (a text-only
|
|
56
|
+
model, e.g. vLLM "X is not a multimodal model", llama.cpp "image input is not
|
|
57
|
+
supported" when no --mmproj is loaded). Distinct from a bad image: the fix is to
|
|
58
|
+
drop attachments and retry, not to ask the user for a different file."""
|
|
59
|
+
e = (err or "").lower()
|
|
60
|
+
return (
|
|
61
|
+
"not a multimodal model" in e
|
|
62
|
+
or "image input is not supported" in e
|
|
63
|
+
or ("does not support" in e and ("image" in e or "multimodal" in e or "vision" in e))
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
54
67
|
def is_image_load_error(err: str) -> bool:
|
|
55
68
|
"""Whether a provider 400 is the server failing to decode an attached image
|
|
56
69
|
(corrupt / truncated / unsupported), so the agent ends the turn with a clean
|
|
@@ -39,7 +39,9 @@ class RepetitionDetected(StallRetry):
|
|
|
39
39
|
re-issue would just loop again, so the agent goes straight to decisive mode."""
|
|
40
40
|
|
|
41
41
|
from drydock.loop_detect import runaway_repetition_len
|
|
42
|
-
from drydock.tuning import
|
|
42
|
+
from drydock.tuning import (
|
|
43
|
+
extract_thinking, split_think_tags, strip_leaked_tool_calls, strip_thinking_tokens, use_streaming,
|
|
44
|
+
)
|
|
43
45
|
|
|
44
46
|
# ── Provider registry ─────────────────────────────────────────────────────
|
|
45
47
|
|
|
@@ -430,6 +432,31 @@ def _image_url_block(path: str) -> dict | None:
|
|
|
430
432
|
return {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}
|
|
431
433
|
|
|
432
434
|
|
|
435
|
+
# (base_url, model) pairs whose server rejected image input as text-only. Process-wide
|
|
436
|
+
# so every later request (and turn) sends plain text instead of re-hitting the 400.
|
|
437
|
+
_TEXT_ONLY: set[tuple[str, str]] = set()
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _vision_key(config: dict) -> tuple[str, str]:
|
|
441
|
+
prov = PROVIDERS.get(config.get("provider", "vllm"), PROVIDERS["vllm"])
|
|
442
|
+
base_url = config.get("base_url") or prov.get("base_url", "http://localhost:8000/v1")
|
|
443
|
+
return (str(base_url).rstrip("/"), str(config.get("model") or ""))
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def mark_text_only(config: dict) -> bool:
|
|
447
|
+
"""Record that this endpoint's model can't take images. Returns True if newly marked."""
|
|
448
|
+
key = _vision_key(config)
|
|
449
|
+
if key in _TEXT_ONLY:
|
|
450
|
+
return False
|
|
451
|
+
_TEXT_ONLY.add(key)
|
|
452
|
+
return True
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def vision_enabled(config: dict) -> bool:
|
|
456
|
+
"""False when vision is disabled in config or the server proved text-only."""
|
|
457
|
+
return config.get("vision", True) is not False and _vision_key(config) not in _TEXT_ONLY
|
|
458
|
+
|
|
459
|
+
|
|
433
460
|
def _content_with_images(content):
|
|
434
461
|
"""Turn text that references on-disk image paths into a multimodal content
|
|
435
462
|
list ([text, image_url…]); text without resolvable images passes through
|
|
@@ -451,15 +478,17 @@ def _content_with_images(content):
|
|
|
451
478
|
_user_content_with_images = _content_with_images
|
|
452
479
|
|
|
453
480
|
|
|
454
|
-
def messages_to_openai(messages: list, system: str) -> list:
|
|
455
|
-
"""Convert neutral messages to OpenAI API format.
|
|
481
|
+
def messages_to_openai(messages: list, system: str, vision: bool = True) -> list:
|
|
482
|
+
"""Convert neutral messages to OpenAI API format. vision=False sends image
|
|
483
|
+
references as plain text (text-only model)."""
|
|
456
484
|
# value type is mixed (str content, multimodal list content, tool_calls list)
|
|
457
485
|
result: list[dict] = [{"role": "system", "content": system}]
|
|
458
486
|
for m in messages:
|
|
459
487
|
role = m["role"]
|
|
460
488
|
if role == "user":
|
|
461
489
|
result.append({"role": "user",
|
|
462
|
-
"content": _user_content_with_images(m["content"])
|
|
490
|
+
"content": (_user_content_with_images(m["content"])
|
|
491
|
+
if vision else m["content"])})
|
|
463
492
|
elif role == "assistant":
|
|
464
493
|
msg = {"role": "assistant", "content": m.get("content") or None}
|
|
465
494
|
tcs = m.get("tool_calls", [])
|
|
@@ -482,7 +511,7 @@ def messages_to_openai(messages: list, system: str) -> list:
|
|
|
482
511
|
# to look at — attach it so the model SEES it (servers read images
|
|
483
512
|
# from tool messages too, verified). Only ViewImage, so an unrelated
|
|
484
513
|
# tool mentioning a .png path never balloons the request.
|
|
485
|
-
if m.get("name") == "ViewImage" and isinstance(content, str):
|
|
514
|
+
if vision and m.get("name") == "ViewImage" and isinstance(content, str):
|
|
486
515
|
content = _content_with_images(content)
|
|
487
516
|
result.append({
|
|
488
517
|
"role": "tool",
|
|
@@ -559,7 +588,7 @@ def stream(
|
|
|
559
588
|
# dict(config) shallow-copy) so the TUI's STOP can reach this exact client.
|
|
560
589
|
config.setdefault("_abort", {})["client"] = client
|
|
561
590
|
|
|
562
|
-
oai_messages = messages_to_openai(messages, system)
|
|
591
|
+
oai_messages = messages_to_openai(messages, system, vision=vision_enabled(config))
|
|
563
592
|
|
|
564
593
|
has_tools = bool(tool_schemas)
|
|
565
594
|
# Gemma (and any local server whose model name we can't trust) corrupts
|
|
@@ -647,6 +676,9 @@ def stream(
|
|
|
647
676
|
if delta.content:
|
|
648
677
|
# Strip any leaked thinking-token markers (best-effort per chunk).
|
|
649
678
|
chunk_text = strip_thinking_tokens(delta.content)
|
|
679
|
+
# <think>/</think> tags usually arrive as whole tokens; keep them in the
|
|
680
|
+
# buffer (split off at the end) but don't display the bare markers.
|
|
681
|
+
shown = chunk_text.replace("<think>", "").replace("</think>", "")
|
|
650
682
|
# Keep ANY non-empty chunk — including whitespace-only ones. A
|
|
651
683
|
# `.strip()` guard here used to DROP newline-only chunks (the blank
|
|
652
684
|
# line a model streams between markdown blocks arrives as its own
|
|
@@ -654,7 +686,8 @@ def stream(
|
|
|
654
686
|
# skips chunks that stripping emptied (e.g. a lone thinking marker).
|
|
655
687
|
if chunk_text:
|
|
656
688
|
text += chunk_text
|
|
657
|
-
|
|
689
|
+
if shown:
|
|
690
|
+
yield TextChunk(shown)
|
|
658
691
|
# Throttled: only scan once text is long enough and every ~200
|
|
659
692
|
# new chars, so the common path pays almost nothing.
|
|
660
693
|
if len(text) - _rep_checked_at >= 200:
|
|
@@ -705,6 +738,9 @@ def stream(
|
|
|
705
738
|
"input": inp,
|
|
706
739
|
})
|
|
707
740
|
|
|
741
|
+
# A template-opened <think> leaves "reasoning…</think>answer" in the content —
|
|
742
|
+
# keep only the answer in the recorded turn (it was already shown while streaming).
|
|
743
|
+
_, text = split_think_tags(text)
|
|
708
744
|
yield AssistantTurn(text, tool_calls, in_tok, out_tok)
|
|
709
745
|
|
|
710
746
|
|
|
@@ -515,7 +515,7 @@ class DrydockApp(App):
|
|
|
515
515
|
# actual attach happens at the API boundary in providers).
|
|
516
516
|
from drydock import providers
|
|
517
517
|
imgs = providers.detect_image_paths(text)
|
|
518
|
-
if imgs:
|
|
518
|
+
if imgs and providers.vision_enabled(self.config):
|
|
519
519
|
import os as _os
|
|
520
520
|
names = ", ".join(_os.path.basename(p) for p in imgs)
|
|
521
521
|
self._info(f"📎 attached {len(imgs)} image(s) for the model to see: {names}")
|
|
@@ -157,13 +157,31 @@ def is_gemma(model: str | None) -> bool:
|
|
|
157
157
|
return bool(model) and "gemma" in model.lower()
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
def split_think_tags(text: str) -> tuple[str, str]:
|
|
161
|
+
"""Split <think>…</think> reasoning (Qwen/Nemotron/DeepSeek style) off the answer.
|
|
162
|
+
|
|
163
|
+
Chat templates often open <think> in the PROMPT, so the completion carries only
|
|
164
|
+
the closing tag: everything before the last </think> is reasoning. Returns
|
|
165
|
+
("", text) when there is no closing tag."""
|
|
166
|
+
if not text or "</think>" not in text:
|
|
167
|
+
return "", text
|
|
168
|
+
thinking, answer = text.rsplit("</think>", 1)
|
|
169
|
+
thinking = thinking.replace("<think>", "").strip()
|
|
170
|
+
return thinking, answer.lstrip()
|
|
171
|
+
|
|
172
|
+
|
|
160
173
|
def extract_thinking(text: str) -> tuple[str, str]:
|
|
161
174
|
"""Return (thinking_content, cleaned_text).
|
|
162
175
|
|
|
163
|
-
Extracts the content of Gemma's <|channel>…<channel|> blocks
|
|
164
|
-
can surface it in the UI, then strips the
|
|
165
|
-
Returns ("", text) when no thinking block is
|
|
176
|
+
Extracts the content of Gemma's <|channel>…<channel|> blocks (or a
|
|
177
|
+
<think>…</think> span) so callers can surface it in the UI, then strips the
|
|
178
|
+
markers from the returned text. Returns ("", text) when no thinking block is
|
|
179
|
+
present.
|
|
166
180
|
"""
|
|
181
|
+
if text and "</think>" in text:
|
|
182
|
+
thinking, answer = split_think_tags(text)
|
|
183
|
+
inner, answer = extract_thinking(answer)
|
|
184
|
+
return "\n\n".join(t for t in (thinking, inner) if t), answer
|
|
167
185
|
if not text or "<|channel>" not in text:
|
|
168
186
|
return "", text
|
|
169
187
|
spans = _THINKING_RE.findall(text)
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.1.
|
|
10
|
+
version = "3.1.37"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Text-only model fallback: when the server rejects image input because the model
|
|
2
|
+
isn't multimodal (vLLM "X is not a multimodal model", llama.cpp "image input is not
|
|
3
|
+
supported"), the agent stops attaching images for that endpoint and retries the step
|
|
4
|
+
as plain text instead of dead-ending the turn on a raw 400."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from drydock import agent, providers
|
|
8
|
+
from drydock.agent import AgentState
|
|
9
|
+
from drydock.compaction import is_image_load_error, is_text_only_model_error
|
|
10
|
+
from drydock.providers import AssistantTurn, messages_to_openai
|
|
11
|
+
|
|
12
|
+
from tests.test_vision_input import _write_png
|
|
13
|
+
|
|
14
|
+
VLLM_ERR = ("Error code: 400 - {'error': {'message': 'nemotron is not a multimodal model', "
|
|
15
|
+
"'type': 'BadRequestError', 'param': None, 'code': 400}}")
|
|
16
|
+
LLAMA_ERR = ("Error code: 400 - {'error': {'code': 400, 'message': 'image input is not supported "
|
|
17
|
+
"- hint: if this is unexpected, you may need to provide the mmproj', "
|
|
18
|
+
"'type': 'invalid_request_error'}}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_classifies_text_only_errors():
|
|
22
|
+
assert is_text_only_model_error(VLLM_ERR)
|
|
23
|
+
assert is_text_only_model_error(LLAMA_ERR)
|
|
24
|
+
assert not is_text_only_model_error("Failed to load image or audio file")
|
|
25
|
+
assert not is_text_only_model_error("context_length_exceeded")
|
|
26
|
+
# a corrupt image is still an image-load error, not a text-only model
|
|
27
|
+
assert is_image_load_error("Failed to load image or audio file")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_vision_false_keeps_image_reference_as_text(tmp_path):
|
|
31
|
+
p = _write_png(tmp_path / "board.png")
|
|
32
|
+
msgs = messages_to_openai([{"role": "user", "content": f"see {p}"}], "sys", vision=False)
|
|
33
|
+
assert msgs[1]["content"] == f"see {p}"
|
|
34
|
+
tool = messages_to_openai([{"role": "tool", "tool_call_id": "c", "name": "ViewImage",
|
|
35
|
+
"content": f"viewing {p}"}], "sys", vision=False)
|
|
36
|
+
assert tool[1]["content"] == f"viewing {p}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_agent_drops_images_and_retries(monkeypatch, tmp_path):
|
|
40
|
+
p = _write_png(tmp_path / "board.png")
|
|
41
|
+
cfg = {"model": "textonly-test", "base_url": "http://tx.invalid:9/v1",
|
|
42
|
+
"max_tokens": 1024, "context_limit": 65536}
|
|
43
|
+
calls = []
|
|
44
|
+
|
|
45
|
+
def fake_stream(model, system, messages, tool_schemas, config):
|
|
46
|
+
vision = providers.vision_enabled(config)
|
|
47
|
+
calls.append(vision)
|
|
48
|
+
if vision:
|
|
49
|
+
raise RuntimeError(VLLM_ERR)
|
|
50
|
+
yield AssistantTurn(text="done", tool_calls=[], input_tokens=10, output_tokens=5)
|
|
51
|
+
|
|
52
|
+
monkeypatch.setattr(agent, "stream", fake_stream)
|
|
53
|
+
monkeypatch.setattr(providers, "_TEXT_ONLY", set())
|
|
54
|
+
events = list(agent.run(f"what is in {p}", AgentState(), cfg, "SYS"))
|
|
55
|
+
assert calls == [True, False]
|
|
56
|
+
assert any("text-only" in getattr(e, "text", "") for e in events)
|
|
57
|
+
assert not providers.vision_enabled(cfg)
|
|
58
|
+
# a different endpoint is unaffected
|
|
59
|
+
assert providers.vision_enabled({**cfg, "base_url": "http://other.invalid:9/v1"})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_second_text_only_error_raises_not_loops(monkeypatch):
|
|
63
|
+
cfg = {"model": "textonly-test2", "base_url": "http://tx2.invalid:9/v1",
|
|
64
|
+
"max_tokens": 1024, "context_limit": 65536}
|
|
65
|
+
calls = []
|
|
66
|
+
|
|
67
|
+
def fake_stream(model, system, messages, tool_schemas, config):
|
|
68
|
+
calls.append(1)
|
|
69
|
+
raise RuntimeError(VLLM_ERR)
|
|
70
|
+
yield # pragma: no cover
|
|
71
|
+
|
|
72
|
+
monkeypatch.setattr(agent, "stream", fake_stream)
|
|
73
|
+
monkeypatch.setattr(providers, "_TEXT_ONLY", set())
|
|
74
|
+
try:
|
|
75
|
+
list(agent.run("hi", AgentState(), cfg, "SYS"))
|
|
76
|
+
except RuntimeError:
|
|
77
|
+
pass
|
|
78
|
+
assert len(calls) == 2
|
|
@@ -150,3 +150,14 @@ def test_extract_thinking_noop_when_no_marker():
|
|
|
150
150
|
thinking, text = extract_thinking(raw)
|
|
151
151
|
assert thinking == ""
|
|
152
152
|
assert text == raw
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_split_think_tags_template_opened():
|
|
156
|
+
from drydock.tuning import split_think_tags
|
|
157
|
+
assert split_think_tags("plan it\n</think>\n74 done") == ("plan it", "74 done")
|
|
158
|
+
assert split_think_tags("<think>x</think>ok") == ("x", "ok")
|
|
159
|
+
assert split_think_tags("no tags") == ("", "no tags")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_extract_thinking_handles_think_tags():
|
|
163
|
+
assert extract_thinking("reason</think>answer") == ("reason", "answer")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|