hud 0.6.19.dev2__tar.gz → 0.6.20__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.
- {hud-0.6.19.dev2 → hud-0.6.20}/.gitignore +6 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/PKG-INFO +1 -1
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/agent.py +46 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/sdk/agent.py +2 -1
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/sdk/events.py +63 -16
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/codex/agent.py +4 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/agent.py +23 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_claude_agent.py +138 -5
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_claude_cli_agent.py +88 -3
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_codex_cli_agent.py +10 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_gemini_agent.py +121 -5
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/types.py +53 -1
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/__init__.py +1 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/__main__.py +26 -1
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/deploy.py +44 -38
- hud-0.6.20/hud/cli/harbor.py +571 -0
- hud-0.6.20/hud/cli/qa.py +633 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_cli.py +25 -2
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_deploy.py +35 -2
- hud-0.6.20/hud/cli/tests/test_harbor.py +584 -0
- hud-0.6.20/hud/cli/tests/test_qa.py +586 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/namespace.py +7 -13
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/robot/__init__.py +4 -0
- hud-0.6.20/hud/environment/robot/control.py +714 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/robot/endpoint.py +28 -1
- hud-0.6.20/hud/environment/robot/orientation.py +305 -0
- hud-0.6.20/hud/environment/tests/test_robot_control.py +515 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_robot_regressions.py +36 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_workspace.py +9 -13
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/workspace.py +2 -1
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/run.py +12 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/compose.py +49 -16
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/core.py +9 -2
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/docker.py +54 -46
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/modal.py +51 -24
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/sync.py +4 -1
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/task.py +3 -2
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_docker_provider.py +213 -111
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_rollout.py +82 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_task.py +1 -1
- hud-0.6.20/hud/imports.py +197 -0
- hud-0.6.20/hud/integrations/harbor/Dockerfile +32 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/__init__.py +2 -2
- hud-0.6.20/hud/integrations/harbor/adapt.py +863 -0
- hud-0.6.20/hud/integrations/harbor/build.py +189 -0
- hud-0.6.20/hud/integrations/harbor/config.py +178 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/env.py +247 -427
- hud-0.6.20/hud/integrations/harbor/importing.py +136 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/install.sh +8 -2
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/instruction.md +1 -1
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/solution/solve.sh +5 -1
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/solution/solve.sh +3 -3
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/Dockerfile +1 -1
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/test.sh +1 -1
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/test_contract.py +529 -174
- hud-0.6.20/hud/integrations/harbor/tests/test_importing.py +101 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/test_integration.py +256 -3
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/robot/recorder.py +17 -1
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/robot/video.py +33 -13
- hud-0.6.20/hud/telemetry/tests/test_robot_video.py +30 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/exceptions.py +4 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/requests.py +33 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_requests.py +21 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/version.py +1 -1
- hud-0.6.19.dev2/hud/cli/qa.py +0 -574
- hud-0.6.19.dev2/hud/cli/tests/test_qa.py +0 -587
- hud-0.6.19.dev2/hud/integrations/harbor/Dockerfile +0 -27
- hud-0.6.19.dev2/hud/integrations/harbor/adapt.py +0 -1148
- {hud-0.6.19.dev2 → hud-0.6.20}/LICENSE +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/README.md +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/__main__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/base.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/browser_use/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/browser_use/agent.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/images.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/sdk/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/sdk/computer_mcp.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/base.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/coding.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/computer.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/hosted.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/mcp_proxy.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/settings.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/tests/test_computer.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/cli.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/codex/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/settings.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/base.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/coding.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/computer.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/filesystem.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/hosted.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/mcp_proxy.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/tests/test_computer.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/misc/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/misc/response_automation.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/agent.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/apply_patch.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/base.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/coding.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/computer.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/hosted.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/mcp_proxy.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/strict_schema.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/tests/test_computer.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/tests/test_strict_schema.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/agent.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/tools/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/tools/base.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/tools/filesystem.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/tools/mcp_proxy.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/adapter.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/agent.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/batching.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/dataset.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/model.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/cli_fakes.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_apply_patch.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_base.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_openai_agent.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_openai_compatible_agent.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_provider_native_tools.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_robot_dataset_writer.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_tool_agent.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_trace.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tool_agent.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/base.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/file_view.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/hosted.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/mcp.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/rfb.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/ssh.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/base.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/cdp.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/connection.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/mcp.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/rfb.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/robot.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/ssh.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/tests/test_rfb.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/tests/test_ssh.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/eval.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/init.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/jobs.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/models.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/project.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/serve.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/sync.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/task.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_eval.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_init.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_jobs.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_main.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_project.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_sync.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_task.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_trace.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/trace.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/clients/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/clients/client.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/clients/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/clients/tests/test_connect.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/conftest.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/arguments.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/egress.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/env.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/file_tracker.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/process_guard.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/robot/bridge.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/robot/gym.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/server.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/conftest.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_arguments.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_capability_backing.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_file_tracker.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_file_tracking.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_loader.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_manifest.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_platform.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_process_connection.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_server.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_sessions.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_tunnel.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/utils.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/chat.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/docker-seccomp.json +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/file_tracking.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/job.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/daytona.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/hosted.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/hud.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/local.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/taskset.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_chat.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_file_tracking_observer.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_hosted.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_job.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_local_runtime.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_shared.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_sync.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/base.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/bash.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/combine.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/judge.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/results.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/text.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/export.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/conftest.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/agent-lifecycle/environment/Dockerfile +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/agent-lifecycle/instruction.md +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/agent-lifecycle/solution/solve.sh +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/agent-lifecycle/task.toml +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/agent-lifecycle/tests/test.sh +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/Dockerfile +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/docker-compose.yaml +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/Dockerfile +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/server.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/instruction.md +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/solution/solve.sh +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/task.toml +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/tests/test.sh +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/Dockerfile +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/entrypoint.sh +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/task.toml +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/tests/test.sh +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/Dockerfile +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/docker-compose.yaml +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/instruction.md +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/task.toml +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/Dockerfile +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/entrypoint.sh +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/instruction.md +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/solution/solve.sh +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/task.toml +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/tests/test.sh +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/test_harbor.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/patches/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/patches/mcp_patches.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/patches/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/patches/tests/test_warnings.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/patches/warnings.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/py.typed +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/settings.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/context.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/exporter.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/instrument.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/robot/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/span.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/tests/test_exporter.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/tests/test_instrument.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/base.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/client.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/tests/test_client.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/types.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/types.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/docker.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/gateway.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/hints.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/hud_console.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/modules.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/naming.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/platform.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/process.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/serialization.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/__init__.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_exceptions.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_gateway.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_hints.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_hud_console.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_naming.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_platform.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_process.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_serialization.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/time.py +0 -0
- {hud-0.6.19.dev2 → hud-0.6.20}/pyproject.toml +0 -0
|
@@ -71,3 +71,9 @@ experiments/
|
|
|
71
71
|
!.codex/environments/
|
|
72
72
|
.codex/environments/*
|
|
73
73
|
!.codex/environments/environment.toml
|
|
74
|
+
|
|
75
|
+
# Runtime fixtures and dependency locks for featured environment templates.
|
|
76
|
+
!environments/featured/**/uv.lock
|
|
77
|
+
!environments/featured/file-analysis/fixtures/*.csv
|
|
78
|
+
!environments/featured/libbet/games/test/
|
|
79
|
+
!environments/featured/libbet/games/test/**
|
|
@@ -30,6 +30,7 @@ from anthropic.types.beta import (
|
|
|
30
30
|
BetaPlainTextSourceParam,
|
|
31
31
|
BetaRequestDocumentBlockParam,
|
|
32
32
|
BetaTextBlockParam,
|
|
33
|
+
BetaThinkingConfigParam,
|
|
33
34
|
BetaToolChoiceAutoParam,
|
|
34
35
|
BetaToolResultBlockParam,
|
|
35
36
|
BetaToolUnionParam,
|
|
@@ -69,6 +70,7 @@ _STREAM_MAX_ATTEMPTS = 3
|
|
|
69
70
|
_STREAM_RETRY_BASE_DELAY_SECONDS = 1.0
|
|
70
71
|
_STREAM_RETRY_MAX_DELAY_SECONDS = 5.0
|
|
71
72
|
_STREAM_JITTER = SystemRandom()
|
|
73
|
+
_THINKING_BINDING_BETA = "thinking-binding-controls-2026-08-01"
|
|
72
74
|
|
|
73
75
|
|
|
74
76
|
def _stream_retry_delay(attempt: int, error: Exception) -> float:
|
|
@@ -232,6 +234,7 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
|
|
|
232
234
|
tools: list[BetaToolUnionParam],
|
|
233
235
|
tool_choice: BetaToolChoiceAutoParam,
|
|
234
236
|
betas: list[str] | Omit,
|
|
237
|
+
thinking: BetaThinkingConfigParam | Omit,
|
|
235
238
|
) -> BetaMessage:
|
|
236
239
|
attempt = 1
|
|
237
240
|
while True:
|
|
@@ -245,6 +248,7 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
|
|
|
245
248
|
tools=tools,
|
|
246
249
|
tool_choice=tool_choice,
|
|
247
250
|
betas=betas,
|
|
251
|
+
thinking=thinking,
|
|
248
252
|
) as stream:
|
|
249
253
|
stream_opened = True
|
|
250
254
|
async for _ in stream:
|
|
@@ -294,6 +298,17 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
|
|
|
294
298
|
required_betas = {
|
|
295
299
|
beta for tool in state.tools.values() if (beta := getattr(tool.spec, "beta", None))
|
|
296
300
|
}
|
|
301
|
+
thinking_payload = dict(self.config.thinking) if self.config.thinking is not None else None
|
|
302
|
+
if self.config.max_tool_result_images is not None:
|
|
303
|
+
self._prune_tool_result_images(state.messages, self.config.max_tool_result_images)
|
|
304
|
+
required_betas.add(_THINKING_BINDING_BETA)
|
|
305
|
+
assert thinking_payload is not None
|
|
306
|
+
thinking_payload["block_binding"] = {"prefix_mismatch_behavior": "drop_block"}
|
|
307
|
+
thinking = (
|
|
308
|
+
cast("BetaThinkingConfigParam", thinking_payload)
|
|
309
|
+
if thinking_payload is not None
|
|
310
|
+
else Omit()
|
|
311
|
+
)
|
|
297
312
|
betas: list[str] | Omit = list(required_betas) if required_betas else Omit()
|
|
298
313
|
tool_choice = BetaToolChoiceAutoParam(type="auto", disable_parallel_tool_use=True)
|
|
299
314
|
tools = cast("list[BetaToolUnionParam]", list(state.params))
|
|
@@ -315,6 +330,7 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
|
|
|
315
330
|
tools=tools,
|
|
316
331
|
tool_choice=tool_choice,
|
|
317
332
|
betas=betas,
|
|
333
|
+
thinking=thinking,
|
|
318
334
|
)
|
|
319
335
|
else:
|
|
320
336
|
client = cast("AsyncAnthropic", self.anthropic_client)
|
|
@@ -325,6 +341,7 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
|
|
|
325
341
|
tools=tools,
|
|
326
342
|
tool_choice=tool_choice,
|
|
327
343
|
betas=betas,
|
|
344
|
+
thinking=thinking,
|
|
328
345
|
)
|
|
329
346
|
|
|
330
347
|
state.messages.append(
|
|
@@ -371,6 +388,35 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
|
|
|
371
388
|
|
|
372
389
|
return self.message_to_agent_step(response, citations_enabled=citations_enabled)
|
|
373
390
|
|
|
391
|
+
@staticmethod
|
|
392
|
+
def _prune_tool_result_images(messages: list[BetaMessageParam], limit: int) -> None:
|
|
393
|
+
retained = 0
|
|
394
|
+
removed = 0
|
|
395
|
+
for message in reversed(messages):
|
|
396
|
+
content = message["content"]
|
|
397
|
+
if not isinstance(content, list):
|
|
398
|
+
continue
|
|
399
|
+
for block in reversed(content):
|
|
400
|
+
if not isinstance(block, dict) or block.get("type") != "tool_result":
|
|
401
|
+
continue
|
|
402
|
+
result_content = block.get("content")
|
|
403
|
+
if not isinstance(result_content, list):
|
|
404
|
+
continue
|
|
405
|
+
result_content = cast("list[ClaudeToolResultContent]", result_content)
|
|
406
|
+
for index in range(len(result_content) - 1, -1, -1):
|
|
407
|
+
image = result_content[index]
|
|
408
|
+
if not isinstance(image, dict) or image.get("type") != "image":
|
|
409
|
+
continue
|
|
410
|
+
retained += 1
|
|
411
|
+
if retained > limit:
|
|
412
|
+
result_content[index] = BetaTextBlockParam(
|
|
413
|
+
type="text",
|
|
414
|
+
text="[Earlier tool-result image omitted by image-history policy.]",
|
|
415
|
+
)
|
|
416
|
+
removed += 1
|
|
417
|
+
if removed:
|
|
418
|
+
logger.info("Removed %d older tool-result images; retaining %d", removed, limit)
|
|
419
|
+
|
|
374
420
|
@classmethod
|
|
375
421
|
def message_to_agent_step(
|
|
376
422
|
cls,
|
|
@@ -119,7 +119,6 @@ class ClaudeCLIAgent(Agent[ClaudeCLIConfig]):
|
|
|
119
119
|
else:
|
|
120
120
|
raise ValueError("HUD_API_KEY is required for HUD gateway routing")
|
|
121
121
|
env["CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS"] = "1"
|
|
122
|
-
env["DISABLE_AUTO_COMPACT"] = "1"
|
|
123
122
|
# Alias every model tier so background requests never bypass the gateway.
|
|
124
123
|
env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = self.config.model
|
|
125
124
|
env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = self.config.model
|
|
@@ -141,6 +140,8 @@ class ClaudeCLIAgent(Agent[ClaudeCLIConfig]):
|
|
|
141
140
|
]
|
|
142
141
|
if self.config.max_steps > 0:
|
|
143
142
|
args.append(f"--max-turns={self.config.max_steps}")
|
|
143
|
+
if self.config.reasoning_effort is not None:
|
|
144
|
+
args.extend(["--effort", self.config.reasoning_effort])
|
|
144
145
|
if self.config.system_prompt:
|
|
145
146
|
args.extend(["--system-prompt", self.config.system_prompt])
|
|
146
147
|
for tool in self.config.allowed_tools:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import logging
|
|
6
7
|
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
import mcp.types as mcp_types
|
|
@@ -16,6 +17,8 @@ from hud.utils.time import now_iso
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
from hud.eval.run import Run
|
|
18
19
|
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
class ClaudeEvents:
|
|
21
24
|
"""Translate Claude CLI stream messages into canonical HUD steps."""
|
|
@@ -58,22 +61,51 @@ class ClaudeEvents:
|
|
|
58
61
|
raw_result = block.get("content")
|
|
59
62
|
raw_items = raw_result if isinstance(raw_result, list) else [raw_result]
|
|
60
63
|
content: list[mcp_types.ContentBlock] = []
|
|
61
|
-
for item in raw_items:
|
|
64
|
+
for index, item in enumerate(raw_items):
|
|
62
65
|
if isinstance(item, str):
|
|
63
66
|
content.append(mcp_types.TextContent(type="text", text=item))
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
continue
|
|
68
|
+
match item["type"]:
|
|
69
|
+
case "text":
|
|
70
|
+
content.append(
|
|
71
|
+
mcp_types.TextContent(type="text", text=item["text"])
|
|
72
|
+
)
|
|
73
|
+
case "image":
|
|
74
|
+
source = item["source"]
|
|
75
|
+
content.append(
|
|
76
|
+
mcp_types.ImageContent(
|
|
77
|
+
type="image",
|
|
78
|
+
data=source["data"],
|
|
79
|
+
mimeType=source["media_type"],
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
case "document" if item["source"]["type"] in ("base64", "text"):
|
|
83
|
+
# The CLI's Read on a PDF.
|
|
84
|
+
source = item["source"]
|
|
85
|
+
uri = f"document://{call_id}/{index}"
|
|
86
|
+
resource: mcp_types.ResourceContents = (
|
|
87
|
+
mcp_types.BlobResourceContents(
|
|
88
|
+
uri=uri,
|
|
89
|
+
mimeType=source.get("media_type"),
|
|
90
|
+
blob=source["data"],
|
|
91
|
+
)
|
|
92
|
+
if source["type"] == "base64"
|
|
93
|
+
else mcp_types.TextResourceContents(
|
|
94
|
+
uri=uri,
|
|
95
|
+
mimeType=source.get("media_type"),
|
|
96
|
+
text=source["data"],
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
content.append(
|
|
100
|
+
mcp_types.EmbeddedResource(type="resource", resource=resource)
|
|
101
|
+
)
|
|
102
|
+
case kind:
|
|
103
|
+
logger.warning("unsupported Claude tool result block: %s", kind)
|
|
104
|
+
content.append(
|
|
105
|
+
mcp_types.TextContent(
|
|
106
|
+
type="text", text=f"[unsupported {kind} block]"
|
|
107
|
+
)
|
|
73
108
|
)
|
|
74
|
-
)
|
|
75
|
-
else:
|
|
76
|
-
raise ValueError(f"unsupported Claude tool result block: {item!r}")
|
|
77
109
|
|
|
78
110
|
self.run.record(
|
|
79
111
|
ToolStep(
|
|
@@ -111,16 +143,31 @@ class ClaudeEvents:
|
|
|
111
143
|
trace.extra[key] = value
|
|
112
144
|
|
|
113
145
|
def finish(self, *, returncode: int, stderr: str) -> None:
|
|
146
|
+
"""Fail the rollout unless the CLI delivered a complete terminal result.
|
|
147
|
+
|
|
148
|
+
A nonzero exit after a result event is only recorded on the trace when
|
|
149
|
+
stderr is empty; the CLI exits that way after successful runs.
|
|
150
|
+
"""
|
|
114
151
|
trace = self.run.trace
|
|
115
|
-
error = self.error
|
|
116
152
|
if returncode != 0:
|
|
117
153
|
trace.extra["returncode"] = returncode
|
|
118
|
-
|
|
154
|
+
|
|
155
|
+
reported = stderr.strip()
|
|
156
|
+
if self.error is not None:
|
|
157
|
+
error = self.error
|
|
119
158
|
elif not self.saw_result:
|
|
120
|
-
error =
|
|
159
|
+
error = reported or (
|
|
160
|
+
f"claude CLI exited with return code {returncode}"
|
|
161
|
+
if returncode != 0
|
|
162
|
+
else "claude CLI exited without a result event"
|
|
163
|
+
)
|
|
164
|
+
elif returncode != 0 and reported:
|
|
165
|
+
error = reported
|
|
121
166
|
elif self.pending_calls:
|
|
122
167
|
missing = ", ".join(sorted(self.pending_calls))
|
|
123
168
|
error = f"claude CLI exited without results for tool calls: {missing}"
|
|
169
|
+
else:
|
|
170
|
+
error = None
|
|
124
171
|
|
|
125
172
|
if error is not None and stderr:
|
|
126
173
|
trace.extra["stderr"] = stderr
|
|
@@ -246,6 +246,10 @@ class CodexCLIAgent(Agent[CodexCLIConfig]):
|
|
|
246
246
|
"--model",
|
|
247
247
|
self.config.model,
|
|
248
248
|
]
|
|
249
|
+
if self.config.reasoning_effort is not None:
|
|
250
|
+
args.extend(
|
|
251
|
+
["-c", f"model_reasoning_effort={json.dumps(self.config.reasoning_effort)}"]
|
|
252
|
+
)
|
|
249
253
|
|
|
250
254
|
if connection is not None or routes_to_gateway("openai", gateway=self.config.gateway):
|
|
251
255
|
if connection is not None:
|
|
@@ -35,6 +35,26 @@ from .tools import (
|
|
|
35
35
|
logger = logging.getLogger(__name__)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def _bound_text(text: str, limit: int) -> str:
|
|
39
|
+
"""Keep ``limit`` characters of ``text``, weighted to its tail.
|
|
40
|
+
|
|
41
|
+
The kept span is announced so the model can re-run the call scoped to what
|
|
42
|
+
it needs rather than treating the remainder as absent.
|
|
43
|
+
"""
|
|
44
|
+
if limit <= 0 or len(text) <= limit:
|
|
45
|
+
return text
|
|
46
|
+
head_chars = limit // 5
|
|
47
|
+
tail_chars = limit - head_chars
|
|
48
|
+
omitted = len(text) - limit
|
|
49
|
+
return (
|
|
50
|
+
f"Output too large: showing the first {head_chars} and last {tail_chars} "
|
|
51
|
+
f"characters, {omitted} omitted. Re-run scoped to the part you need.\n"
|
|
52
|
+
f"{text[:head_chars]}\n\n"
|
|
53
|
+
f"... [{omitted} characters omitted] ...\n\n"
|
|
54
|
+
f"{text[-tail_chars:]}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
38
58
|
class GeminiAgent(ToolAgent[genai_types.Content, GeminiConfig]):
|
|
39
59
|
"""Gemini agent. Drives SSH (coding/filesystem), RFB (computer), and MCP capabilities."""
|
|
40
60
|
|
|
@@ -67,6 +87,7 @@ class GeminiAgent(ToolAgent[genai_types.Content, GeminiConfig]):
|
|
|
67
87
|
self.thinking_level = config.thinking_level
|
|
68
88
|
self.include_thoughts = config.include_thoughts
|
|
69
89
|
self.excluded_predefined_functions = list(config.excluded_predefined_functions)
|
|
90
|
+
self.max_tool_result_chars = config.max_tool_result_chars
|
|
70
91
|
self.max_recent_turn_with_screenshots = (
|
|
71
92
|
gemini_agent_settings.MAX_RECENT_TURN_WITH_SCREENSHOTS
|
|
72
93
|
)
|
|
@@ -95,6 +116,8 @@ class GeminiAgent(ToolAgent[genai_types.Content, GeminiConfig]):
|
|
|
95
116
|
(c.text for c in result.content if isinstance(c, mcp_types.TextContent)),
|
|
96
117
|
None,
|
|
97
118
|
)
|
|
119
|
+
if text is not None:
|
|
120
|
+
text = _bound_text(text, self.max_tool_result_chars)
|
|
98
121
|
response: dict[str, Any] = (
|
|
99
122
|
{"error": text or "Tool execution failed"} if result.isError else {"success": True}
|
|
100
123
|
)
|
|
@@ -8,23 +8,33 @@ import base64
|
|
|
8
8
|
import random
|
|
9
9
|
import struct
|
|
10
10
|
import zlib
|
|
11
|
+
from contextlib import AsyncExitStack
|
|
11
12
|
from io import BytesIO
|
|
12
13
|
from types import SimpleNamespace
|
|
13
|
-
from typing import Any, cast
|
|
14
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
14
15
|
from unittest.mock import AsyncMock, Mock, call
|
|
15
16
|
|
|
17
|
+
import fastmcp
|
|
16
18
|
import httpx
|
|
17
19
|
import httpx2
|
|
20
|
+
import mcp.types as mcp_types
|
|
18
21
|
import pytest
|
|
19
|
-
from anthropic import APIStatusError
|
|
22
|
+
from anthropic import APIStatusError, Omit
|
|
23
|
+
from fastmcp.tools import ToolResult
|
|
20
24
|
from mcp.types import ImageContent
|
|
21
25
|
from PIL import Image
|
|
26
|
+
from pydantic import ValidationError
|
|
22
27
|
|
|
23
28
|
from hud.agents.claude.agent import ClaudeAgent
|
|
24
29
|
from hud.agents.claude.tools.computer import ClaudeComputerTool
|
|
25
30
|
from hud.agents.tool_agent import RunState
|
|
26
|
-
from hud.
|
|
27
|
-
from hud.
|
|
31
|
+
from hud.agents.types import ClaudeConfig, ToolStep
|
|
32
|
+
from hud.capabilities import Capability, MCPClient, RFBClient
|
|
33
|
+
from hud.settings import settings
|
|
34
|
+
from hud.types import MCPToolCall, MCPToolResult, Trace
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from hud.eval.run import Run
|
|
28
38
|
|
|
29
39
|
|
|
30
40
|
class FakeStream:
|
|
@@ -53,8 +63,10 @@ class FakeMessages:
|
|
|
53
63
|
def __init__(self, *outcomes: Any) -> None:
|
|
54
64
|
self._outcomes = list(outcomes)
|
|
55
65
|
self.calls = 0
|
|
66
|
+
self.requests: list[dict[str, Any]] = []
|
|
56
67
|
|
|
57
|
-
def stream(self, **
|
|
68
|
+
def stream(self, **kwargs: Any) -> FakeStream:
|
|
69
|
+
self.requests.append(kwargs)
|
|
58
70
|
outcome = self._outcomes[self.calls]
|
|
59
71
|
self.calls += 1
|
|
60
72
|
return FakeStream(outcome)
|
|
@@ -461,3 +473,124 @@ def test_claude_preserves_computer_screenshot_coordinate_space() -> None:
|
|
|
461
473
|
source = result["content"][0]["source"]
|
|
462
474
|
assert source["media_type"] == "image/png"
|
|
463
475
|
assert base64.b64decode(source["data"]) == data
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
@pytest.mark.parametrize("limit", [None, 1])
|
|
479
|
+
async def test_image_history_preserves_tool_results_and_thinking(
|
|
480
|
+
limit: int | None, monkeypatch: pytest.MonkeyPatch
|
|
481
|
+
) -> None:
|
|
482
|
+
monkeypatch.setattr(settings, "telemetry_enabled", False)
|
|
483
|
+
buffer = BytesIO()
|
|
484
|
+
Image.new("RGB", (32, 24), "white").save(buffer, format="PNG")
|
|
485
|
+
image = ImageContent(
|
|
486
|
+
type="image", mimeType="image/png", data=base64.b64encode(buffer.getvalue()).decode()
|
|
487
|
+
)
|
|
488
|
+
server = fastmcp.FastMCP("screenshots")
|
|
489
|
+
|
|
490
|
+
@server.tool
|
|
491
|
+
def screenshot() -> ToolResult:
|
|
492
|
+
"""Capture the current screen."""
|
|
493
|
+
return ToolResult(content=[mcp_types.TextContent(type="text", text="screen"), image])
|
|
494
|
+
|
|
495
|
+
client = FakeAnthropic(
|
|
496
|
+
*(
|
|
497
|
+
_final(
|
|
498
|
+
SimpleNamespace(
|
|
499
|
+
type="thinking", thinking=f"reason-{turn}", signature=f"sig-{turn}"
|
|
500
|
+
),
|
|
501
|
+
SimpleNamespace(type="tool_use", id=f"shot-{turn}", name="screenshot", input={}),
|
|
502
|
+
stop_reason="tool_use",
|
|
503
|
+
)
|
|
504
|
+
for turn in (1, 2)
|
|
505
|
+
),
|
|
506
|
+
_final(SimpleNamespace(type="text", text="done", citations=None), stop_reason="end_turn"),
|
|
507
|
+
)
|
|
508
|
+
config = ClaudeConfig(
|
|
509
|
+
model="claude-test",
|
|
510
|
+
model_client=client,
|
|
511
|
+
max_steps=3,
|
|
512
|
+
max_tool_result_images=limit,
|
|
513
|
+
thinking={"type": "adaptive"} if limit is not None else None,
|
|
514
|
+
)
|
|
515
|
+
restored = ClaudeConfig.model_validate(config.model_dump(mode="json"))
|
|
516
|
+
assert restored.max_tool_result_images == limit
|
|
517
|
+
assert restored.thinking == config.thinking
|
|
518
|
+
async with fastmcp.Client(server) as mcp_client:
|
|
519
|
+
cap = Capability.mcp(name="screenshots", url="https://tools.invalid/mcp")
|
|
520
|
+
connection = MCPClient(cap, mcp_client, AsyncExitStack())
|
|
521
|
+
trace = Trace()
|
|
522
|
+
run = cast(
|
|
523
|
+
"Run",
|
|
524
|
+
SimpleNamespace(
|
|
525
|
+
trace=trace,
|
|
526
|
+
record=trace.record,
|
|
527
|
+
client=SimpleNamespace(
|
|
528
|
+
manifest=SimpleNamespace(bindings=[cap]),
|
|
529
|
+
open=AsyncMock(return_value=connection),
|
|
530
|
+
),
|
|
531
|
+
prompt_messages=[
|
|
532
|
+
mcp_types.PromptMessage(
|
|
533
|
+
role="user", content=mcp_types.TextContent(type="text", text="go")
|
|
534
|
+
)
|
|
535
|
+
],
|
|
536
|
+
),
|
|
537
|
+
)
|
|
538
|
+
await ClaudeAgent(config)(run)
|
|
539
|
+
|
|
540
|
+
assert trace.status == "completed"
|
|
541
|
+
requests = client.beta.messages.requests
|
|
542
|
+
assert len(requests) == 3
|
|
543
|
+
for request in requests:
|
|
544
|
+
if limit is None:
|
|
545
|
+
assert isinstance(request.get("thinking", Omit()), Omit)
|
|
546
|
+
assert isinstance(request.get("betas", Omit()), Omit)
|
|
547
|
+
else:
|
|
548
|
+
assert "thinking-binding-controls-2026-08-01" in request["betas"]
|
|
549
|
+
assert request["thinking"] == {
|
|
550
|
+
"type": "adaptive",
|
|
551
|
+
"block_binding": {"prefix_mismatch_behavior": "drop_block"},
|
|
552
|
+
}
|
|
553
|
+
messages = requests[-1]["messages"]
|
|
554
|
+
results = [message["content"][0] for message in messages[2::2]]
|
|
555
|
+
assert [result["tool_use_id"] for result in results] == ["shot-1", "shot-2"]
|
|
556
|
+
assert [result["content"][0]["text"] for result in results] == ["screen", "screen"]
|
|
557
|
+
assert [result["content"][1]["type"] for result in results] == (
|
|
558
|
+
["image", "image"] if limit is None else ["text", "image"]
|
|
559
|
+
)
|
|
560
|
+
assert [
|
|
561
|
+
(message["content"][0].thinking, message["content"][0].signature)
|
|
562
|
+
for message in messages[1::2]
|
|
563
|
+
] == [("reason-1", "sig-1"), ("reason-2", "sig-2")]
|
|
564
|
+
for step in trace.steps:
|
|
565
|
+
if isinstance(step, ToolStep):
|
|
566
|
+
assert step.result is not None
|
|
567
|
+
recorded_image = step.result.content[1]
|
|
568
|
+
assert isinstance(recorded_image, ImageContent)
|
|
569
|
+
assert recorded_image.data == image.data
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
@pytest.mark.parametrize("thinking", [None, {"type": "disabled"}])
|
|
573
|
+
def test_image_history_requires_enabled_thinking(thinking: dict[str, str] | None) -> None:
|
|
574
|
+
with pytest.raises(ValidationError, match="requires explicit adaptive or enabled thinking"):
|
|
575
|
+
ClaudeConfig.model_validate({"max_tool_result_images": 1, "thinking": thinking})
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@pytest.mark.parametrize(
|
|
579
|
+
("max_tokens", "budget_tokens"),
|
|
580
|
+
[(16384, 16384), (16384, 16385), (2048, 2048), (2048, 2049)],
|
|
581
|
+
)
|
|
582
|
+
def test_enabled_thinking_budget_must_be_below_max_tokens(
|
|
583
|
+
max_tokens: int, budget_tokens: int
|
|
584
|
+
) -> None:
|
|
585
|
+
with pytest.raises(ValidationError, match="budget_tokens must be less than max_tokens"):
|
|
586
|
+
ClaudeConfig(
|
|
587
|
+
max_tokens=max_tokens, thinking={"type": "enabled", "budget_tokens": budget_tokens}
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@pytest.mark.parametrize("max_tokens", [1025, 2048])
|
|
592
|
+
def test_enabled_thinking_budget_below_max_tokens_is_allowed(max_tokens: int) -> None:
|
|
593
|
+
config = ClaudeConfig(
|
|
594
|
+
max_tokens=max_tokens, thinking={"type": "enabled", "budget_tokens": max_tokens - 1}
|
|
595
|
+
)
|
|
596
|
+
assert config.thinking == {"type": "enabled", "budget_tokens": max_tokens - 1}
|
|
@@ -21,11 +21,12 @@ from unittest.mock import AsyncMock, Mock
|
|
|
21
21
|
|
|
22
22
|
import fastmcp
|
|
23
23
|
import pytest
|
|
24
|
-
from mcp.types import ImageContent, TextContent
|
|
24
|
+
from mcp.types import BlobResourceContents, EmbeddedResource, ImageContent, TextContent
|
|
25
25
|
|
|
26
26
|
from hud.agents import create_agent
|
|
27
27
|
from hud.agents.claude.sdk import computer_mcp
|
|
28
28
|
from hud.agents.claude.sdk.agent import ClaudeCLIAgent
|
|
29
|
+
from hud.agents.claude.sdk.events import ClaudeEvents
|
|
29
30
|
from hud.agents.tests.cli_fakes import FakeClient as _FakeClient
|
|
30
31
|
from hud.agents.tests.cli_fakes import FakeProcess as _FakeStreamProcess
|
|
31
32
|
from hud.agents.tests.cli_fakes import fake_run as _fake_run
|
|
@@ -57,7 +58,7 @@ async def test_command_follows_explicit_gateway_routing(monkeypatch: pytest.Monk
|
|
|
57
58
|
assert f"ANTHROPIC_BASE_URL={settings.hud_gateway_url}" in gateway
|
|
58
59
|
assert "ANTHROPIC_API_KEY=hud-key" in gateway
|
|
59
60
|
assert "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1" in gateway
|
|
60
|
-
assert "DISABLE_AUTO_COMPACT
|
|
61
|
+
assert "DISABLE_AUTO_COMPACT" not in gateway
|
|
61
62
|
assert "ANTHROPIC_API_KEY=anthropic-key" in provider
|
|
62
63
|
assert "ANTHROPIC_BASE_URL" not in provider
|
|
63
64
|
assert "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS" not in provider
|
|
@@ -141,7 +142,6 @@ async def test_windows_command_encodes_environment_and_arguments(
|
|
|
141
142
|
script = base64.b64decode(encoded).decode("utf-16-le")
|
|
142
143
|
assert "$env:ANTHROPIC_API_KEY='hud&key''s'" in script
|
|
143
144
|
assert "$env:CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS='1'" in script
|
|
144
|
-
assert "$env:DISABLE_AUTO_COMPACT='1'" in script
|
|
145
145
|
assert "'--system-prompt' 'don''t $expand'" in script
|
|
146
146
|
assert "Get-Content -Raw -Encoding UTF8 '.hud_input.jsonl' | & 'claude'" in script
|
|
147
147
|
assert "'--input-format=stream-json'" in script
|
|
@@ -264,6 +264,16 @@ async def _sent_command(
|
|
|
264
264
|
return command
|
|
265
265
|
|
|
266
266
|
|
|
267
|
+
async def test_command_passes_the_configured_reasoning_effort() -> None:
|
|
268
|
+
command = await _sent_command(ClaudeCLIAgent(ClaudeCLIConfig(reasoning_effort="max")))
|
|
269
|
+
assert "--effort max" in command
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
async def test_command_omits_effort_when_unconfigured() -> None:
|
|
273
|
+
command = await _sent_command(ClaudeCLIAgent(ClaudeCLIConfig()))
|
|
274
|
+
assert "--effort" not in command
|
|
275
|
+
|
|
276
|
+
|
|
267
277
|
async def test_exec_on_windows_writes_batch_and_execs_via_cmd() -> None:
|
|
268
278
|
sink: dict[str, bytes] = {}
|
|
269
279
|
conn = _FakeConn(sink, _FakeStreamProcess(_STREAM_JSON))
|
|
@@ -477,6 +487,20 @@ async def test_exec_nonzero_exit_with_result_stream_remains_an_error() -> None:
|
|
|
477
487
|
assert "messages" not in run.trace.extra
|
|
478
488
|
|
|
479
489
|
|
|
490
|
+
async def test_exec_nonzero_exit_with_result_stream_and_no_stderr_is_graded() -> None:
|
|
491
|
+
"""The CLI exits nonzero after a successful result; the run stays gradeable."""
|
|
492
|
+
sink: dict[str, bytes] = {}
|
|
493
|
+
conn = _FakeConn(sink, _FakeStreamProcess(_STREAM_JSON, exit_status=1))
|
|
494
|
+
ssh = _ssh_with_conn("bash", conn)
|
|
495
|
+
|
|
496
|
+
run = _fake_run()
|
|
497
|
+
await run_claude(ClaudeCLIConfig(), run, ssh=ssh, prompt="x")
|
|
498
|
+
|
|
499
|
+
assert run.trace.content == "done"
|
|
500
|
+
assert run.trace.extra["returncode"] == 1
|
|
501
|
+
assert "stderr" not in run.trace.extra
|
|
502
|
+
|
|
503
|
+
|
|
480
504
|
async def test_exec_zero_exit_without_result_event_is_an_error() -> None:
|
|
481
505
|
sink: dict[str, bytes] = {}
|
|
482
506
|
stdout = _STREAM_JSON.rsplit('{"type":"result"', 1)[0]
|
|
@@ -833,3 +857,64 @@ async def test_concurrent_runs_keep_their_ssh_state_isolated() -> None:
|
|
|
833
857
|
"second"
|
|
834
858
|
)
|
|
835
859
|
assert run_a.trace.content == run_b.trace.content == "done"
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def test_events_record_document_and_unknown_tool_result_blocks() -> None:
|
|
863
|
+
"""A PDF Read returns a document block; neither it nor an unknown block ends the run."""
|
|
864
|
+
run = _fake_run()
|
|
865
|
+
events = ClaudeEvents(run, started_at="2026-01-01T00:00:00Z")
|
|
866
|
+
events.consume(
|
|
867
|
+
json.dumps(
|
|
868
|
+
{
|
|
869
|
+
"type": "assistant",
|
|
870
|
+
"message": {
|
|
871
|
+
"id": "msg-1",
|
|
872
|
+
"type": "message",
|
|
873
|
+
"role": "assistant",
|
|
874
|
+
"model": "claude-test",
|
|
875
|
+
"content": [{"type": "tool_use", "id": "tool-1", "name": "Read", "input": {}}],
|
|
876
|
+
"stop_reason": "tool_use",
|
|
877
|
+
"stop_sequence": None,
|
|
878
|
+
"usage": {"input_tokens": 1, "output_tokens": 1},
|
|
879
|
+
},
|
|
880
|
+
}
|
|
881
|
+
)
|
|
882
|
+
)
|
|
883
|
+
events.consume(
|
|
884
|
+
json.dumps(
|
|
885
|
+
{
|
|
886
|
+
"type": "user",
|
|
887
|
+
"message": {
|
|
888
|
+
"content": [
|
|
889
|
+
{
|
|
890
|
+
"type": "tool_result",
|
|
891
|
+
"tool_use_id": "tool-1",
|
|
892
|
+
"content": [
|
|
893
|
+
{
|
|
894
|
+
"type": "document",
|
|
895
|
+
"source": {
|
|
896
|
+
"type": "base64",
|
|
897
|
+
"media_type": "application/pdf",
|
|
898
|
+
"data": "JVBERi0=",
|
|
899
|
+
},
|
|
900
|
+
},
|
|
901
|
+
{"type": "novelty", "payload": 1},
|
|
902
|
+
],
|
|
903
|
+
}
|
|
904
|
+
]
|
|
905
|
+
},
|
|
906
|
+
}
|
|
907
|
+
)
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
tool = cast("ToolStep", run.steps[1])
|
|
911
|
+
assert tool.result is not None
|
|
912
|
+
document, unknown = tool.result.content
|
|
913
|
+
assert isinstance(document, EmbeddedResource)
|
|
914
|
+
assert isinstance(document.resource, BlobResourceContents)
|
|
915
|
+
assert document.resource.mimeType == "application/pdf"
|
|
916
|
+
assert document.resource.blob == "JVBERi0="
|
|
917
|
+
assert str(document.resource.uri) == "document://tool-1/0"
|
|
918
|
+
assert isinstance(unknown, TextContent)
|
|
919
|
+
assert unknown.text == "[unsupported novelty block]"
|
|
920
|
+
assert events.error is None
|
|
@@ -97,6 +97,16 @@ _STREAM_JSON = (
|
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
async def test_command_passes_the_configured_reasoning_effort() -> None:
|
|
101
|
+
command = await _sent_command(CodexCLIConfig(reasoning_effort="max"))
|
|
102
|
+
assert 'model_reasoning_effort="max"' in command
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def test_command_omits_reasoning_effort_when_unconfigured() -> None:
|
|
106
|
+
command = await _sent_command(CodexCLIConfig())
|
|
107
|
+
assert "model_reasoning_effort" not in command
|
|
108
|
+
|
|
109
|
+
|
|
100
110
|
async def test_command_follows_explicit_gateway_routing(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
101
111
|
monkeypatch.setattr(settings, "api_key", "hud-key")
|
|
102
112
|
monkeypatch.setattr(settings, "openai_api_key", "openai-key")
|