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.
Files changed (307) hide show
  1. {hud-0.6.19.dev2 → hud-0.6.20}/.gitignore +6 -0
  2. {hud-0.6.19.dev2 → hud-0.6.20}/PKG-INFO +1 -1
  3. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/agent.py +46 -0
  4. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/sdk/agent.py +2 -1
  5. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/sdk/events.py +63 -16
  6. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/codex/agent.py +4 -0
  7. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/agent.py +23 -0
  8. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_claude_agent.py +138 -5
  9. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_claude_cli_agent.py +88 -3
  10. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_codex_cli_agent.py +10 -0
  11. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_gemini_agent.py +121 -5
  12. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/types.py +53 -1
  13. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/__init__.py +1 -0
  14. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/__main__.py +26 -1
  15. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/deploy.py +44 -38
  16. hud-0.6.20/hud/cli/harbor.py +571 -0
  17. hud-0.6.20/hud/cli/qa.py +633 -0
  18. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_cli.py +25 -2
  19. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_deploy.py +35 -2
  20. hud-0.6.20/hud/cli/tests/test_harbor.py +584 -0
  21. hud-0.6.20/hud/cli/tests/test_qa.py +586 -0
  22. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/namespace.py +7 -13
  23. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/robot/__init__.py +4 -0
  24. hud-0.6.20/hud/environment/robot/control.py +714 -0
  25. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/robot/endpoint.py +28 -1
  26. hud-0.6.20/hud/environment/robot/orientation.py +305 -0
  27. hud-0.6.20/hud/environment/tests/test_robot_control.py +515 -0
  28. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_robot_regressions.py +36 -0
  29. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_workspace.py +9 -13
  30. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/workspace.py +2 -1
  31. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/run.py +12 -0
  32. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/compose.py +49 -16
  33. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/core.py +9 -2
  34. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/docker.py +54 -46
  35. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/modal.py +51 -24
  36. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/sync.py +4 -1
  37. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/task.py +3 -2
  38. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_docker_provider.py +213 -111
  39. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_rollout.py +82 -0
  40. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_task.py +1 -1
  41. hud-0.6.20/hud/imports.py +197 -0
  42. hud-0.6.20/hud/integrations/harbor/Dockerfile +32 -0
  43. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/__init__.py +2 -2
  44. hud-0.6.20/hud/integrations/harbor/adapt.py +863 -0
  45. hud-0.6.20/hud/integrations/harbor/build.py +189 -0
  46. hud-0.6.20/hud/integrations/harbor/config.py +178 -0
  47. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/env.py +247 -427
  48. hud-0.6.20/hud/integrations/harbor/importing.py +136 -0
  49. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/install.sh +8 -2
  50. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/instruction.md +1 -1
  51. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/solution/solve.sh +5 -1
  52. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/solution/solve.sh +3 -3
  53. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/Dockerfile +1 -1
  54. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/test.sh +1 -1
  55. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/test_contract.py +529 -174
  56. hud-0.6.20/hud/integrations/harbor/tests/test_importing.py +101 -0
  57. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/test_integration.py +256 -3
  58. {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/robot/recorder.py +17 -1
  59. {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/robot/video.py +33 -13
  60. hud-0.6.20/hud/telemetry/tests/test_robot_video.py +30 -0
  61. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/exceptions.py +4 -0
  62. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/requests.py +33 -0
  63. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_requests.py +21 -0
  64. {hud-0.6.19.dev2 → hud-0.6.20}/hud/version.py +1 -1
  65. hud-0.6.19.dev2/hud/cli/qa.py +0 -574
  66. hud-0.6.19.dev2/hud/cli/tests/test_qa.py +0 -587
  67. hud-0.6.19.dev2/hud/integrations/harbor/Dockerfile +0 -27
  68. hud-0.6.19.dev2/hud/integrations/harbor/adapt.py +0 -1148
  69. {hud-0.6.19.dev2 → hud-0.6.20}/LICENSE +0 -0
  70. {hud-0.6.19.dev2 → hud-0.6.20}/README.md +0 -0
  71. {hud-0.6.19.dev2 → hud-0.6.20}/hud/__init__.py +0 -0
  72. {hud-0.6.19.dev2 → hud-0.6.20}/hud/__main__.py +0 -0
  73. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/__init__.py +0 -0
  74. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/base.py +0 -0
  75. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/browser_use/__init__.py +0 -0
  76. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/browser_use/agent.py +0 -0
  77. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/__init__.py +0 -0
  78. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/images.py +0 -0
  79. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/sdk/__init__.py +0 -0
  80. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/sdk/computer_mcp.py +0 -0
  81. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/__init__.py +0 -0
  82. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/base.py +0 -0
  83. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/coding.py +0 -0
  84. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/computer.py +0 -0
  85. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/hosted.py +0 -0
  86. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/mcp_proxy.py +0 -0
  87. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/settings.py +0 -0
  88. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/tests/__init__.py +0 -0
  89. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/claude/tools/tests/test_computer.py +0 -0
  90. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/cli.py +0 -0
  91. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/codex/__init__.py +0 -0
  92. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/__init__.py +0 -0
  93. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/settings.py +0 -0
  94. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/__init__.py +0 -0
  95. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/base.py +0 -0
  96. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/coding.py +0 -0
  97. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/computer.py +0 -0
  98. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/filesystem.py +0 -0
  99. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/hosted.py +0 -0
  100. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/mcp_proxy.py +0 -0
  101. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/tests/__init__.py +0 -0
  102. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/gemini/tools/tests/test_computer.py +0 -0
  103. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/misc/__init__.py +0 -0
  104. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/misc/response_automation.py +0 -0
  105. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/__init__.py +0 -0
  106. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/agent.py +0 -0
  107. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/__init__.py +0 -0
  108. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/apply_patch.py +0 -0
  109. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/base.py +0 -0
  110. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/coding.py +0 -0
  111. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/computer.py +0 -0
  112. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/hosted.py +0 -0
  113. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/mcp_proxy.py +0 -0
  114. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/strict_schema.py +0 -0
  115. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/tests/__init__.py +0 -0
  116. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/tests/test_computer.py +0 -0
  117. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai/tools/tests/test_strict_schema.py +0 -0
  118. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/__init__.py +0 -0
  119. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/agent.py +0 -0
  120. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/tools/__init__.py +0 -0
  121. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/tools/base.py +0 -0
  122. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/tools/filesystem.py +0 -0
  123. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/openai_compatible/tools/mcp_proxy.py +0 -0
  124. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/__init__.py +0 -0
  125. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/adapter.py +0 -0
  126. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/agent.py +0 -0
  127. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/batching.py +0 -0
  128. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/dataset.py +0 -0
  129. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/robot/model.py +0 -0
  130. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/__init__.py +0 -0
  131. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/cli_fakes.py +0 -0
  132. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_apply_patch.py +0 -0
  133. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_base.py +0 -0
  134. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_openai_agent.py +0 -0
  135. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_openai_compatible_agent.py +0 -0
  136. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_provider_native_tools.py +0 -0
  137. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_robot_dataset_writer.py +0 -0
  138. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_tool_agent.py +0 -0
  139. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tests/test_trace.py +0 -0
  140. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tool_agent.py +0 -0
  141. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/__init__.py +0 -0
  142. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/base.py +0 -0
  143. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/file_view.py +0 -0
  144. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/hosted.py +0 -0
  145. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/mcp.py +0 -0
  146. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/rfb.py +0 -0
  147. {hud-0.6.19.dev2 → hud-0.6.20}/hud/agents/tools/ssh.py +0 -0
  148. {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/__init__.py +0 -0
  149. {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/base.py +0 -0
  150. {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/cdp.py +0 -0
  151. {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/connection.py +0 -0
  152. {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/mcp.py +0 -0
  153. {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/rfb.py +0 -0
  154. {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/robot.py +0 -0
  155. {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/ssh.py +0 -0
  156. {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/tests/__init__.py +0 -0
  157. {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/tests/test_rfb.py +0 -0
  158. {hud-0.6.19.dev2 → hud-0.6.20}/hud/capabilities/tests/test_ssh.py +0 -0
  159. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/eval.py +0 -0
  160. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/init.py +0 -0
  161. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/jobs.py +0 -0
  162. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/models.py +0 -0
  163. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/project.py +0 -0
  164. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/serve.py +0 -0
  165. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/sync.py +0 -0
  166. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/task.py +0 -0
  167. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/__init__.py +0 -0
  168. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_eval.py +0 -0
  169. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_init.py +0 -0
  170. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_jobs.py +0 -0
  171. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_main.py +0 -0
  172. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_project.py +0 -0
  173. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_sync.py +0 -0
  174. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_task.py +0 -0
  175. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/tests/test_trace.py +0 -0
  176. {hud-0.6.19.dev2 → hud-0.6.20}/hud/cli/trace.py +0 -0
  177. {hud-0.6.19.dev2 → hud-0.6.20}/hud/clients/__init__.py +0 -0
  178. {hud-0.6.19.dev2 → hud-0.6.20}/hud/clients/client.py +0 -0
  179. {hud-0.6.19.dev2 → hud-0.6.20}/hud/clients/tests/__init__.py +0 -0
  180. {hud-0.6.19.dev2 → hud-0.6.20}/hud/clients/tests/test_connect.py +0 -0
  181. {hud-0.6.19.dev2 → hud-0.6.20}/hud/conftest.py +0 -0
  182. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/__init__.py +0 -0
  183. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/arguments.py +0 -0
  184. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/egress.py +0 -0
  185. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/env.py +0 -0
  186. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/file_tracker.py +0 -0
  187. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/process_guard.py +0 -0
  188. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/robot/bridge.py +0 -0
  189. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/robot/gym.py +0 -0
  190. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/server.py +0 -0
  191. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/__init__.py +0 -0
  192. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/conftest.py +0 -0
  193. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_arguments.py +0 -0
  194. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_capability_backing.py +0 -0
  195. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_file_tracker.py +0 -0
  196. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_file_tracking.py +0 -0
  197. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_loader.py +0 -0
  198. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_manifest.py +0 -0
  199. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_platform.py +0 -0
  200. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_process_connection.py +0 -0
  201. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_server.py +0 -0
  202. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_sessions.py +0 -0
  203. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/tests/test_tunnel.py +0 -0
  204. {hud-0.6.19.dev2 → hud-0.6.20}/hud/environment/utils.py +0 -0
  205. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/__init__.py +0 -0
  206. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/chat.py +0 -0
  207. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/docker-seccomp.json +0 -0
  208. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/file_tracking.py +0 -0
  209. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/job.py +0 -0
  210. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/__init__.py +0 -0
  211. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/daytona.py +0 -0
  212. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/hosted.py +0 -0
  213. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/hud.py +0 -0
  214. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/runtime/local.py +0 -0
  215. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/taskset.py +0 -0
  216. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/__init__.py +0 -0
  217. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_chat.py +0 -0
  218. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_file_tracking_observer.py +0 -0
  219. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_hosted.py +0 -0
  220. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_job.py +0 -0
  221. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_local_runtime.py +0 -0
  222. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_shared.py +0 -0
  223. {hud-0.6.19.dev2 → hud-0.6.20}/hud/eval/tests/test_sync.py +0 -0
  224. {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/__init__.py +0 -0
  225. {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/base.py +0 -0
  226. {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/bash.py +0 -0
  227. {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/combine.py +0 -0
  228. {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/judge.py +0 -0
  229. {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/results.py +0 -0
  230. {hud-0.6.19.dev2 → hud-0.6.20}/hud/graders/text.py +0 -0
  231. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/__init__.py +0 -0
  232. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/export.py +0 -0
  233. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/__init__.py +0 -0
  234. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/conftest.py +0 -0
  235. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/agent-lifecycle/environment/Dockerfile +0 -0
  236. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/agent-lifecycle/instruction.md +0 -0
  237. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/agent-lifecycle/solution/solve.sh +0 -0
  238. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/agent-lifecycle/task.toml +0 -0
  239. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/agent-lifecycle/tests/test.sh +0 -0
  240. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/Dockerfile +0 -0
  241. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/docker-compose.yaml +0 -0
  242. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/Dockerfile +0 -0
  243. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/__init__.py +0 -0
  244. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/server.py +0 -0
  245. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/instruction.md +0 -0
  246. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/solution/solve.sh +0 -0
  247. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/task.toml +0 -0
  248. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/hello-mcp/tests/test.sh +0 -0
  249. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/Dockerfile +0 -0
  250. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/entrypoint.sh +0 -0
  251. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/task.toml +0 -0
  252. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/phase-boundary/tests/test.sh +0 -0
  253. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/Dockerfile +0 -0
  254. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/docker-compose.yaml +0 -0
  255. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/instruction.md +0 -0
  256. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/sidecar-reachability/task.toml +0 -0
  257. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/Dockerfile +0 -0
  258. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/entrypoint.sh +0 -0
  259. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/instruction.md +0 -0
  260. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/solution/solve.sh +0 -0
  261. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/task.toml +0 -0
  262. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/tests/test.sh +0 -0
  263. {hud-0.6.19.dev2 → hud-0.6.20}/hud/integrations/harbor/tests/test_harbor.py +0 -0
  264. {hud-0.6.19.dev2 → hud-0.6.20}/hud/patches/__init__.py +0 -0
  265. {hud-0.6.19.dev2 → hud-0.6.20}/hud/patches/mcp_patches.py +0 -0
  266. {hud-0.6.19.dev2 → hud-0.6.20}/hud/patches/tests/__init__.py +0 -0
  267. {hud-0.6.19.dev2 → hud-0.6.20}/hud/patches/tests/test_warnings.py +0 -0
  268. {hud-0.6.19.dev2 → hud-0.6.20}/hud/patches/warnings.py +0 -0
  269. {hud-0.6.19.dev2 → hud-0.6.20}/hud/py.typed +0 -0
  270. {hud-0.6.19.dev2 → hud-0.6.20}/hud/settings.py +0 -0
  271. {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/__init__.py +0 -0
  272. {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/context.py +0 -0
  273. {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/exporter.py +0 -0
  274. {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/instrument.py +0 -0
  275. {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/robot/__init__.py +0 -0
  276. {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/span.py +0 -0
  277. {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/tests/__init__.py +0 -0
  278. {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/tests/test_exporter.py +0 -0
  279. {hud-0.6.19.dev2 → hud-0.6.20}/hud/telemetry/tests/test_instrument.py +0 -0
  280. {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/__init__.py +0 -0
  281. {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/base.py +0 -0
  282. {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/client.py +0 -0
  283. {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/tests/__init__.py +0 -0
  284. {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/tests/test_client.py +0 -0
  285. {hud-0.6.19.dev2 → hud-0.6.20}/hud/train/types.py +0 -0
  286. {hud-0.6.19.dev2 → hud-0.6.20}/hud/types.py +0 -0
  287. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/__init__.py +0 -0
  288. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/docker.py +0 -0
  289. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/gateway.py +0 -0
  290. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/hints.py +0 -0
  291. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/hud_console.py +0 -0
  292. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/modules.py +0 -0
  293. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/naming.py +0 -0
  294. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/platform.py +0 -0
  295. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/process.py +0 -0
  296. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/serialization.py +0 -0
  297. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/__init__.py +0 -0
  298. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_exceptions.py +0 -0
  299. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_gateway.py +0 -0
  300. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_hints.py +0 -0
  301. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_hud_console.py +0 -0
  302. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_naming.py +0 -0
  303. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_platform.py +0 -0
  304. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_process.py +0 -0
  305. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/tests/test_serialization.py +0 -0
  306. {hud-0.6.19.dev2 → hud-0.6.20}/hud/utils/time.py +0 -0
  307. {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/**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hud
3
- Version: 0.6.19.dev2
3
+ Version: 0.6.20
4
4
  Summary: SDK for the HUD platform.
5
5
  Project-URL: Homepage, https://github.com/hud-evals/hud-python
6
6
  Project-URL: Bug Tracker, https://github.com/hud-evals/hud-python/issues
@@ -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
- elif item["type"] == "text":
65
- content.append(mcp_types.TextContent(type="text", text=item["text"]))
66
- elif item["type"] == "image":
67
- source = item["source"]
68
- content.append(
69
- mcp_types.ImageContent(
70
- type="image",
71
- data=source["data"],
72
- mimeType=source["media_type"],
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
- error = stderr.strip() or f"claude CLI exited with return code {returncode}"
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 = "claude CLI exited without a result event"
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.capabilities import RFBClient
27
- from hud.types import MCPToolCall, MCPToolResult
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, **_kwargs: Any) -> FakeStream:
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=1" in gateway
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")