hud 0.6.17__tar.gz → 0.6.18.dev2__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 (338) hide show
  1. {hud-0.6.17 → hud-0.6.18.dev2}/.gitignore +2 -0
  2. {hud-0.6.17 → hud-0.6.18.dev2}/PKG-INFO +2 -12
  3. {hud-0.6.17 → hud-0.6.18.dev2}/README.md +1 -11
  4. {hud-0.6.17 → hud-0.6.18.dev2}/hud/__main__.py +1 -1
  5. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/__init__.py +35 -69
  6. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/agent.py +5 -7
  7. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/agent.py +8 -11
  8. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/agent.py +18 -15
  9. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/tools/computer.py +58 -41
  10. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/tools/tests/test_computer.py +50 -10
  11. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai_compatible/agent.py +1 -1
  12. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/test_base.py +27 -6
  13. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/test_gemini_agent.py +16 -0
  14. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/test_openai_agent.py +120 -0
  15. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/test_tool_agent.py +17 -0
  16. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tool_agent.py +6 -1
  17. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tools/ssh.py +1 -1
  18. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/types.py +5 -1
  19. hud-0.6.18.dev2/hud/cli/__init__.py +425 -0
  20. hud-0.6.18.dev2/hud/cli/__main__.py +270 -0
  21. hud-0.6.18.dev2/hud/cli/deploy.py +541 -0
  22. hud-0.6.18.dev2/hud/cli/eval.py +573 -0
  23. hud-0.6.18.dev2/hud/cli/init.py +195 -0
  24. hud-0.6.18.dev2/hud/cli/jobs.py +280 -0
  25. hud-0.6.18.dev2/hud/cli/models.py +262 -0
  26. hud-0.6.18.dev2/hud/cli/project.py +247 -0
  27. hud-0.6.18.dev2/hud/cli/qa.py +574 -0
  28. {hud-0.6.17 → hud-0.6.18.dev2}/hud/cli/serve.py +23 -54
  29. hud-0.6.18.dev2/hud/cli/sync.py +434 -0
  30. hud-0.6.18.dev2/hud/cli/task.py +213 -0
  31. hud-0.6.18.dev2/hud/cli/tests/test_cli.py +853 -0
  32. hud-0.6.18.dev2/hud/cli/tests/test_deploy.py +850 -0
  33. hud-0.6.18.dev2/hud/cli/tests/test_eval.py +953 -0
  34. hud-0.6.18.dev2/hud/cli/tests/test_init.py +255 -0
  35. hud-0.6.18.dev2/hud/cli/tests/test_jobs.py +73 -0
  36. hud-0.6.18.dev2/hud/cli/tests/test_main.py +26 -0
  37. hud-0.6.18.dev2/hud/cli/tests/test_project.py +284 -0
  38. {hud-0.6.17 → hud-0.6.18.dev2}/hud/cli/tests/test_qa.py +117 -4
  39. hud-0.6.18.dev2/hud/cli/tests/test_sync.py +396 -0
  40. hud-0.6.18.dev2/hud/cli/tests/test_task.py +149 -0
  41. hud-0.6.18.dev2/hud/cli/tests/test_trace.py +76 -0
  42. hud-0.6.18.dev2/hud/cli/trace.py +158 -0
  43. {hud-0.6.17 → hud-0.6.18.dev2}/hud/conftest.py +4 -0
  44. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/__init__.py +21 -7
  45. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/file_tracker.py +1 -1
  46. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/namespace.py +77 -29
  47. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_loader.py +140 -0
  48. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_workspace.py +96 -1
  49. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/utils.py +16 -2
  50. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/workspace.py +66 -58
  51. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/job.py +2 -2
  52. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/run.py +1 -5
  53. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/runtime/hosted.py +2 -2
  54. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/runtime/local.py +51 -31
  55. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/sync.py +3 -0
  56. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/task.py +9 -0
  57. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/taskset.py +39 -19
  58. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/tests/test_hosted.py +1 -1
  59. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/tests/test_local_runtime.py +195 -4
  60. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/tests/test_rollout.py +6 -0
  61. {hud-0.6.17 → hud-0.6.18.dev2}/hud/settings.py +10 -1
  62. {hud-0.6.17 → hud-0.6.18.dev2}/hud/telemetry/exporter.py +4 -4
  63. {hud-0.6.17 → hud-0.6.18.dev2}/hud/telemetry/robot/recorder.py +2 -2
  64. {hud-0.6.17 → hud-0.6.18.dev2}/hud/telemetry/tests/test_exporter.py +10 -0
  65. {hud-0.6.17 → hud-0.6.18.dev2}/hud/train/base.py +15 -35
  66. {hud-0.6.17 → hud-0.6.18.dev2}/hud/train/tests/test_client.py +26 -0
  67. hud-0.6.18.dev2/hud/utils/gateway.py +202 -0
  68. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/hud_console.py +6 -9
  69. hud-0.6.18.dev2/hud/utils/modules.py +122 -0
  70. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/platform.py +9 -9
  71. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/process.py +57 -0
  72. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/requests.py +35 -90
  73. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/tests/test_gateway.py +111 -0
  74. hud-0.6.18.dev2/hud/utils/tests/test_naming.py +13 -0
  75. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/tests/test_platform.py +13 -13
  76. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/tests/test_requests.py +45 -20
  77. {hud-0.6.17 → hud-0.6.18.dev2}/hud/version.py +1 -1
  78. {hud-0.6.17 → hud-0.6.18.dev2}/pyproject.toml +1 -1
  79. hud-0.6.17/hud/cli/__init__.py +0 -175
  80. hud-0.6.17/hud/cli/__main__.py +0 -8
  81. hud-0.6.17/hud/cli/cancel.py +0 -111
  82. hud-0.6.17/hud/cli/client.py +0 -82
  83. hud-0.6.17/hud/cli/deploy.py +0 -898
  84. hud-0.6.17/hud/cli/eval.py +0 -1003
  85. hud-0.6.17/hud/cli/init.py +0 -129
  86. hud-0.6.17/hud/cli/jobs.py +0 -147
  87. hud-0.6.17/hud/cli/models.py +0 -274
  88. hud-0.6.17/hud/cli/presets.py +0 -115
  89. hud-0.6.17/hud/cli/project.py +0 -198
  90. hud-0.6.17/hud/cli/qa.py +0 -419
  91. hud-0.6.17/hud/cli/qa_analysis.py +0 -207
  92. hud-0.6.17/hud/cli/sync.py +0 -525
  93. hud-0.6.17/hud/cli/task.py +0 -224
  94. hud-0.6.17/hud/cli/tests/test_cli_init.py +0 -87
  95. hud-0.6.17/hud/cli/tests/test_cli_main.py +0 -27
  96. hud-0.6.17/hud/cli/tests/test_cli_more_wrappers.py +0 -7
  97. hud-0.6.17/hud/cli/tests/test_deploy.py +0 -636
  98. hud-0.6.17/hud/cli/tests/test_eval_bedrock.py +0 -51
  99. hud-0.6.17/hud/cli/tests/test_eval_config.py +0 -354
  100. hud-0.6.17/hud/cli/tests/test_init.py +0 -276
  101. hud-0.6.17/hud/cli/tests/test_jobs.py +0 -38
  102. hud-0.6.17/hud/cli/tests/test_main_module.py +0 -36
  103. hud-0.6.17/hud/cli/tests/test_project_command.py +0 -133
  104. hud-0.6.17/hud/cli/tests/test_qa_analysis.py +0 -87
  105. hud-0.6.17/hud/cli/tests/test_sync_export.py +0 -58
  106. hud-0.6.17/hud/cli/tests/test_sync_projects.py +0 -176
  107. hud-0.6.17/hud/cli/tests/test_task.py +0 -43
  108. hud-0.6.17/hud/cli/tests/test_trace.py +0 -47
  109. hud-0.6.17/hud/cli/tests/test_usage.py +0 -134
  110. hud-0.6.17/hud/cli/trace.py +0 -206
  111. hud-0.6.17/hud/cli/utils/__init__.py +0 -1
  112. hud-0.6.17/hud/cli/utils/api.py +0 -21
  113. hud-0.6.17/hud/cli/utils/build_display.py +0 -279
  114. hud-0.6.17/hud/cli/utils/build_logs.py +0 -230
  115. hud-0.6.17/hud/cli/utils/config.py +0 -115
  116. hud-0.6.17/hud/cli/utils/context.py +0 -282
  117. hud-0.6.17/hud/cli/utils/display.py +0 -100
  118. hud-0.6.17/hud/cli/utils/jobs.py +0 -38
  119. hud-0.6.17/hud/cli/utils/project.py +0 -270
  120. hud-0.6.17/hud/cli/utils/registry.py +0 -100
  121. hud-0.6.17/hud/cli/utils/source.py +0 -563
  122. hud-0.6.17/hud/cli/utils/tasks.py +0 -29
  123. hud-0.6.17/hud/cli/utils/tests/test_build_display.py +0 -49
  124. hud-0.6.17/hud/cli/utils/tests/test_config.py +0 -58
  125. hud-0.6.17/hud/cli/utils/tests/test_context.py +0 -92
  126. hud-0.6.17/hud/cli/utils/tests/test_project.py +0 -276
  127. hud-0.6.17/hud/cli/utils/tests/test_registry.py +0 -76
  128. hud-0.6.17/hud/cli/utils/tests/test_source.py +0 -321
  129. hud-0.6.17/hud/cli/utils/tests/test_tasks.py +0 -80
  130. hud-0.6.17/hud/cli/utils/tests/test_version_check.py +0 -109
  131. hud-0.6.17/hud/cli/utils/usage.py +0 -179
  132. hud-0.6.17/hud/cli/utils/version_check.py +0 -237
  133. hud-0.6.17/hud/utils/gateway.py +0 -141
  134. hud-0.6.17/hud/utils/modules.py +0 -79
  135. hud-0.6.17/hud/utils/tests/__init__.py +0 -0
  136. {hud-0.6.17 → hud-0.6.18.dev2}/LICENSE +0 -0
  137. {hud-0.6.17 → hud-0.6.18.dev2}/hud/__init__.py +0 -0
  138. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/base.py +0 -0
  139. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/browser_use/__init__.py +0 -0
  140. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/browser_use/agent.py +0 -0
  141. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/__init__.py +0 -0
  142. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/sdk/__init__.py +0 -0
  143. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/sdk/agent.py +0 -0
  144. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/sdk/computer_mcp.py +0 -0
  145. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/tools/__init__.py +0 -0
  146. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/tools/base.py +0 -0
  147. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/tools/coding.py +0 -0
  148. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/tools/computer.py +0 -0
  149. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/tools/hosted.py +0 -0
  150. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/tools/mcp_proxy.py +0 -0
  151. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/tools/settings.py +0 -0
  152. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/tools/tests/__init__.py +0 -0
  153. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/claude/tools/tests/test_computer.py +0 -0
  154. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/__init__.py +0 -0
  155. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/settings.py +0 -0
  156. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/tools/__init__.py +0 -0
  157. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/tools/base.py +0 -0
  158. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/tools/coding.py +0 -0
  159. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/tools/computer.py +0 -0
  160. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/tools/filesystem.py +0 -0
  161. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/tools/hosted.py +0 -0
  162. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/tools/mcp_proxy.py +0 -0
  163. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/tools/tests/__init__.py +0 -0
  164. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/gemini/tools/tests/test_computer.py +0 -0
  165. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/misc/__init__.py +0 -0
  166. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/misc/response_automation.py +0 -0
  167. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/__init__.py +0 -0
  168. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/tools/__init__.py +0 -0
  169. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/tools/apply_patch.py +0 -0
  170. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/tools/base.py +0 -0
  171. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/tools/coding.py +0 -0
  172. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/tools/hosted.py +0 -0
  173. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/tools/mcp_proxy.py +0 -0
  174. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/tools/strict_schema.py +0 -0
  175. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/tools/tests/__init__.py +0 -0
  176. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai/tools/tests/test_strict_schema.py +0 -0
  177. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai_compatible/__init__.py +0 -0
  178. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai_compatible/tools/__init__.py +0 -0
  179. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai_compatible/tools/base.py +0 -0
  180. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai_compatible/tools/filesystem.py +0 -0
  181. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/openai_compatible/tools/mcp_proxy.py +0 -0
  182. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/robot/__init__.py +0 -0
  183. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/robot/adapter.py +0 -0
  184. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/robot/agent.py +0 -0
  185. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/robot/batching.py +0 -0
  186. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/robot/dataset.py +0 -0
  187. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/robot/model.py +0 -0
  188. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/__init__.py +0 -0
  189. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/test_apply_patch.py +0 -0
  190. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/test_claude_agent.py +0 -0
  191. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/test_claude_sdk_agent.py +0 -0
  192. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/test_openai_compatible_agent.py +0 -0
  193. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/test_provider_native_tools.py +0 -0
  194. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/test_robot_dataset_writer.py +0 -0
  195. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tests/test_trace.py +0 -0
  196. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tools/__init__.py +0 -0
  197. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tools/base.py +0 -0
  198. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tools/hosted.py +0 -0
  199. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tools/mcp.py +0 -0
  200. {hud-0.6.17 → hud-0.6.18.dev2}/hud/agents/tools/rfb.py +0 -0
  201. {hud-0.6.17 → hud-0.6.18.dev2}/hud/capabilities/__init__.py +0 -0
  202. {hud-0.6.17 → hud-0.6.18.dev2}/hud/capabilities/base.py +0 -0
  203. {hud-0.6.17 → hud-0.6.18.dev2}/hud/capabilities/cdp.py +0 -0
  204. {hud-0.6.17 → hud-0.6.18.dev2}/hud/capabilities/mcp.py +0 -0
  205. {hud-0.6.17 → hud-0.6.18.dev2}/hud/capabilities/rfb.py +0 -0
  206. {hud-0.6.17 → hud-0.6.18.dev2}/hud/capabilities/robot.py +0 -0
  207. {hud-0.6.17 → hud-0.6.18.dev2}/hud/capabilities/ssh.py +0 -0
  208. {hud-0.6.17 → hud-0.6.18.dev2}/hud/capabilities/tests/__init__.py +0 -0
  209. {hud-0.6.17 → hud-0.6.18.dev2}/hud/capabilities/tests/test_rfb.py +0 -0
  210. {hud-0.6.17 → hud-0.6.18.dev2}/hud/capabilities/tests/test_ssh.py +0 -0
  211. {hud-0.6.17 → hud-0.6.18.dev2}/hud/cli/tests/__init__.py +0 -0
  212. {hud-0.6.17 → hud-0.6.18.dev2}/hud/clients/__init__.py +0 -0
  213. {hud-0.6.17 → hud-0.6.18.dev2}/hud/clients/client.py +0 -0
  214. {hud-0.6.17 → hud-0.6.18.dev2}/hud/clients/tests/__init__.py +0 -0
  215. {hud-0.6.17 → hud-0.6.18.dev2}/hud/clients/tests/test_connect.py +0 -0
  216. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/arguments.py +0 -0
  217. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/egress.py +0 -0
  218. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/env.py +0 -0
  219. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/robot/__init__.py +0 -0
  220. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/robot/bridge.py +0 -0
  221. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/robot/endpoint.py +0 -0
  222. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/robot/gym.py +0 -0
  223. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/server.py +0 -0
  224. {hud-0.6.17/hud/cli/utils → hud-0.6.18.dev2/hud/environment}/tests/__init__.py +0 -0
  225. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/conftest.py +0 -0
  226. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_arguments.py +0 -0
  227. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_capability_backing.py +0 -0
  228. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_file_tracker.py +0 -0
  229. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_file_tracking.py +0 -0
  230. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_manifest.py +0 -0
  231. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_platform.py +0 -0
  232. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_robot_regressions.py +0 -0
  233. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_server.py +0 -0
  234. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_sessions.py +0 -0
  235. {hud-0.6.17 → hud-0.6.18.dev2}/hud/environment/tests/test_tunnel.py +0 -0
  236. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/__init__.py +0 -0
  237. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/chat.py +0 -0
  238. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/docker-seccomp.json +0 -0
  239. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/file_tracking.py +0 -0
  240. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/runtime/__init__.py +0 -0
  241. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/runtime/compose.py +0 -0
  242. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/runtime/core.py +0 -0
  243. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/runtime/daytona.py +0 -0
  244. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/runtime/docker.py +0 -0
  245. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/runtime/hud.py +0 -0
  246. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/runtime/modal.py +0 -0
  247. {hud-0.6.17/hud/environment → hud-0.6.18.dev2/hud/eval}/tests/__init__.py +0 -0
  248. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/tests/test_chat.py +0 -0
  249. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/tests/test_docker_provider.py +0 -0
  250. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/tests/test_file_tracking_observer.py +0 -0
  251. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/tests/test_job.py +0 -0
  252. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/tests/test_shared.py +0 -0
  253. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/tests/test_sync.py +0 -0
  254. {hud-0.6.17 → hud-0.6.18.dev2}/hud/eval/tests/test_task.py +0 -0
  255. {hud-0.6.17 → hud-0.6.18.dev2}/hud/graders/__init__.py +0 -0
  256. {hud-0.6.17 → hud-0.6.18.dev2}/hud/graders/base.py +0 -0
  257. {hud-0.6.17 → hud-0.6.18.dev2}/hud/graders/bash.py +0 -0
  258. {hud-0.6.17 → hud-0.6.18.dev2}/hud/graders/combine.py +0 -0
  259. {hud-0.6.17 → hud-0.6.18.dev2}/hud/graders/judge.py +0 -0
  260. {hud-0.6.17 → hud-0.6.18.dev2}/hud/graders/results.py +0 -0
  261. {hud-0.6.17 → hud-0.6.18.dev2}/hud/graders/text.py +0 -0
  262. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/__init__.py +0 -0
  263. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/Dockerfile +0 -0
  264. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/__init__.py +0 -0
  265. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/adapt.py +0 -0
  266. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/env.py +0 -0
  267. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/export.py +0 -0
  268. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/install.sh +0 -0
  269. {hud-0.6.17/hud/eval → hud-0.6.18.dev2/hud/integrations/harbor}/tests/__init__.py +0 -0
  270. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/conftest.py +0 -0
  271. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/environment/Dockerfile +0 -0
  272. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/instruction.md +0 -0
  273. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/solution/solve.sh +0 -0
  274. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/task.toml +0 -0
  275. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/tests/test.sh +0 -0
  276. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/Dockerfile +0 -0
  277. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/docker-compose.yaml +0 -0
  278. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/Dockerfile +0 -0
  279. {hud-0.6.17/hud/integrations/harbor/tests → hud-0.6.18.dev2/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server}/__init__.py +0 -0
  280. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/server.py +0 -0
  281. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/instruction.md +0 -0
  282. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/solution/solve.sh +0 -0
  283. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/task.toml +0 -0
  284. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/tests/test.sh +0 -0
  285. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/Dockerfile +0 -0
  286. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/entrypoint.sh +0 -0
  287. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/instruction.md +0 -0
  288. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/solution/solve.sh +0 -0
  289. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/task.toml +0 -0
  290. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/tests/test.sh +0 -0
  291. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/Dockerfile +0 -0
  292. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/docker-compose.yaml +0 -0
  293. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/instruction.md +0 -0
  294. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/solution/solve.sh +0 -0
  295. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/task.toml +0 -0
  296. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/Dockerfile +0 -0
  297. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/test.sh +0 -0
  298. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/Dockerfile +0 -0
  299. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/entrypoint.sh +0 -0
  300. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/instruction.md +0 -0
  301. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/solution/solve.sh +0 -0
  302. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/task.toml +0 -0
  303. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/tests/test.sh +0 -0
  304. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/test_contract.py +0 -0
  305. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/test_harbor.py +0 -0
  306. {hud-0.6.17 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/test_integration.py +0 -0
  307. {hud-0.6.17 → hud-0.6.18.dev2}/hud/patches/__init__.py +0 -0
  308. {hud-0.6.17 → hud-0.6.18.dev2}/hud/patches/mcp_patches.py +0 -0
  309. {hud-0.6.17 → hud-0.6.18.dev2}/hud/patches/tests/__init__.py +0 -0
  310. {hud-0.6.17 → hud-0.6.18.dev2}/hud/patches/tests/test_warnings.py +0 -0
  311. {hud-0.6.17 → hud-0.6.18.dev2}/hud/patches/warnings.py +0 -0
  312. {hud-0.6.17 → hud-0.6.18.dev2}/hud/py.typed +0 -0
  313. {hud-0.6.17 → hud-0.6.18.dev2}/hud/telemetry/__init__.py +0 -0
  314. {hud-0.6.17 → hud-0.6.18.dev2}/hud/telemetry/context.py +0 -0
  315. {hud-0.6.17 → hud-0.6.18.dev2}/hud/telemetry/instrument.py +0 -0
  316. {hud-0.6.17 → hud-0.6.18.dev2}/hud/telemetry/robot/__init__.py +0 -0
  317. {hud-0.6.17 → hud-0.6.18.dev2}/hud/telemetry/robot/video.py +0 -0
  318. {hud-0.6.17 → hud-0.6.18.dev2}/hud/telemetry/span.py +0 -0
  319. {hud-0.6.17/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server → hud-0.6.18.dev2/hud/telemetry/tests}/__init__.py +0 -0
  320. {hud-0.6.17 → hud-0.6.18.dev2}/hud/telemetry/tests/test_instrument.py +0 -0
  321. {hud-0.6.17 → hud-0.6.18.dev2}/hud/train/__init__.py +0 -0
  322. {hud-0.6.17 → hud-0.6.18.dev2}/hud/train/client.py +0 -0
  323. {hud-0.6.17 → hud-0.6.18.dev2}/hud/train/tests/__init__.py +0 -0
  324. {hud-0.6.17 → hud-0.6.18.dev2}/hud/train/types.py +0 -0
  325. {hud-0.6.17 → hud-0.6.18.dev2}/hud/types.py +0 -0
  326. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/__init__.py +0 -0
  327. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/docker.py +0 -0
  328. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/exceptions.py +0 -0
  329. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/hints.py +0 -0
  330. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/naming.py +0 -0
  331. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/serialization.py +0 -0
  332. {hud-0.6.17/hud/telemetry → hud-0.6.18.dev2/hud/utils}/tests/__init__.py +0 -0
  333. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/tests/test_exceptions.py +0 -0
  334. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/tests/test_hints.py +0 -0
  335. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/tests/test_hud_console.py +0 -0
  336. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/tests/test_process.py +0 -0
  337. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/tests/test_serialization.py +0 -0
  338. {hud-0.6.17 → hud-0.6.18.dev2}/hud/utils/time.py +0 -0
@@ -59,6 +59,8 @@ hud/rl/checkpoints_test/
59
59
  .hud/
60
60
  .hud_eval_config
61
61
  .hud_eval.toml
62
+ # The template hud init copies into new projects.
63
+ !environments/*/.hud_eval.toml
62
64
 
63
65
  docs/internal
64
66
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hud
3
- Version: 0.6.17
3
+ Version: 0.6.18.dev2
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
@@ -234,17 +234,7 @@ A **capability** is a connection the environment exposes; a **harness** attaches
234
234
 
235
235
  From the [platform UI](https://hud.ai) you can run batches, compare models on the same taskset, and inspect every trace.
236
236
 
237
- A **project** holds the environments and tasksets a team creates and decides who can see them. Deploying without one uses your team's default project, so nothing here is required to get started. To put an environment somewhere else, pin the directory once and every later `hud deploy` and `hud sync tasks` follows it:
238
-
239
- ```bash
240
- hud project list # projects you can see
241
- hud project use browser-evals # writes projectId to .hud/config.json
242
- hud project # where does a deploy here land?
243
- ```
244
-
245
- `hud deploy` and `hud sync tasks` also take `--project <name-or-id>` as a one-command override; it does not change the directory configuration. Use `hud set HUD_DEFAULT_PROJECT=<name-or-id>` for a machine-wide fallback. Precedence is the flag, then the directory's `.hud/config.json`, then `HUD_DEFAULT_PROJECT`, then your team default. An environment or taskset that already exists stays where it is; naming a different project fails rather than moving it.
246
-
247
- → [Projects](https://docs.hud.ai/v6/reference/projects) · [Run & deploy](https://docs.hud.ai/v6/reference/runtime)
237
+ → [Run & deploy](https://docs.hud.ai/v6/reference/runtime)
248
238
 
249
239
  ## Train on rewards
250
240
 
@@ -148,17 +148,7 @@ A **capability** is a connection the environment exposes; a **harness** attaches
148
148
 
149
149
  From the [platform UI](https://hud.ai) you can run batches, compare models on the same taskset, and inspect every trace.
150
150
 
151
- A **project** holds the environments and tasksets a team creates and decides who can see them. Deploying without one uses your team's default project, so nothing here is required to get started. To put an environment somewhere else, pin the directory once and every later `hud deploy` and `hud sync tasks` follows it:
152
-
153
- ```bash
154
- hud project list # projects you can see
155
- hud project use browser-evals # writes projectId to .hud/config.json
156
- hud project # where does a deploy here land?
157
- ```
158
-
159
- `hud deploy` and `hud sync tasks` also take `--project <name-or-id>` as a one-command override; it does not change the directory configuration. Use `hud set HUD_DEFAULT_PROJECT=<name-or-id>` for a machine-wide fallback. Precedence is the flag, then the directory's `.hud/config.json`, then `HUD_DEFAULT_PROJECT`, then your team default. An environment or taskset that already exists stays where it is; naming a different project fails rather than moving it.
160
-
161
- → [Projects](https://docs.hud.ai/v6/reference/projects) · [Run & deploy](https://docs.hud.ai/v6/reference/runtime)
151
+ → [Run & deploy](https://docs.hud.ai/v6/reference/runtime)
162
152
 
163
153
  ## Train on rewards
164
154
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from hud.cli import main
5
+ from hud.cli.__main__ import main
6
6
 
7
7
  if __name__ == "__main__":
8
8
  main()
@@ -10,11 +10,7 @@ from typing import TYPE_CHECKING, Any, cast
10
10
  from hud.settings import settings
11
11
  from hud.types import AgentType
12
12
  from hud.utils.exceptions import HudAuthenticationError
13
- from hud.utils.gateway import (
14
- gateway_model_aliases,
15
- list_gateway_models,
16
- normalize_gateway_model_id,
17
- )
13
+ from hud.utils.gateway import resolve_gateway_model
18
14
 
19
15
  if TYPE_CHECKING:
20
16
  from typing import TypeAlias
@@ -31,10 +27,11 @@ if TYPE_CHECKING:
31
27
  def create_agent(model: str, **kwargs: Any) -> GatewayAgent:
32
28
  """Create an agent routed through the HUD gateway.
33
29
 
34
- Leaves ``model_client`` unset so provider agent constructors build the HUD
35
- gateway client locally, while :class:`~hud.eval.runtime.HostedRuntime` can
36
- serialize the config and rebuild the client remotely. Explicitly supplied
37
- clients remain custom/BYOK and are not serializable.
30
+ Sets ``gateway=True`` on the config instead of attaching a client, so the
31
+ provider agent builds the gateway client locally and
32
+ :class:`~hud.eval.runtime.HostedRuntime` can serialize the config and rebuild
33
+ it remotely. Explicitly supplied clients remain custom/BYOK and are not
34
+ serializable.
38
35
 
39
36
  For direct API access with provider API keys, instantiate the agent classes
40
37
  directly.
@@ -49,72 +46,40 @@ def create_agent(model: str, **kwargs: Any) -> GatewayAgent:
49
46
  if not settings.api_key:
50
47
  raise HudAuthenticationError("HUD_API_KEY is required to create a gateway agent")
51
48
 
52
- requested_model = model
53
- model = normalize_gateway_model_id(model)
54
- agent_type = next((candidate for candidate in AgentType if candidate.value == model), None)
55
- if agent_type is not None:
56
- model_id = model
57
- else:
58
- try:
59
- gateway_models = list_gateway_models()
60
- except Exception:
61
- gateway_models = []
62
- gateway_models = list(gateway_models)
63
- for gateway_model in gateway_models:
64
- if model in (
65
- gateway_model.id,
66
- gateway_model.name,
67
- gateway_model.model_name,
68
- ):
69
- agent_str = gateway_model.sdk_agent_type
70
- if agent_str == "operator":
71
- raise ValueError(
72
- "Operator agent is no longer supported; use openai with a supported "
73
- "OpenAI computer model."
74
- )
75
- if agent_str == "gemini_cua":
76
- raise ValueError(
77
- "Gemini CUA agent is no longer supported; use gemini with a supported "
78
- "Gemini computer-use model."
79
- )
80
- if not isinstance(agent_str, str):
81
- raise ValueError(f"Model '{model}' has invalid agent type metadata")
82
-
83
- try:
84
- agent_type = AgentType(agent_str)
85
- except ValueError as exc:
86
- raise ValueError(f"Model '{model}' has invalid agent type metadata") from exc
87
- model_id = gateway_model.model_name or model
88
- break
89
- else:
90
- import difflib
91
-
92
- known = [c.value for c in AgentType] + [
93
- n
94
- for gm in gateway_models
95
- for n in (gm.id, gm.name, gm.model_name)
96
- if isinstance(n, str)
97
- ]
98
- known.extend(gateway_model_aliases())
99
- near = difflib.get_close_matches(requested_model, known, n=3, cutoff=0.5)
100
- hint = (
101
- f" Did you mean: {', '.join(near)}?"
102
- if near
103
- else " Run `hud models` to list available models."
104
- )
105
- source = (
106
- "the HUD gateway registry"
107
- if gateway_models
108
- else "the HUD gateway registry (empty — is HUD_API_KEY set?)"
109
- )
110
- raise ValueError(f"Model {requested_model!r} not found in {source}.{hint}")
111
-
49
+ agent_type, model_id = resolve_agent_model(model)
112
50
  kwargs.setdefault("model", model_id)
51
+ kwargs["gateway"] = True
113
52
  # cls/config_cls are matched unions; the pairing is correct by construction.
114
53
  config = agent_type.config_cls(**kwargs)
115
54
  return agent_type.cls(cast("Any", config))
116
55
 
117
56
 
57
+ def resolve_agent_model(model: str) -> tuple[AgentType, str]:
58
+ """Resolve a catalog id/name/slug or an agent type without constructing a client."""
59
+ agent_type = next((candidate for candidate in AgentType if candidate.value == model), None)
60
+ if agent_type is not None:
61
+ return agent_type, model
62
+ try:
63
+ entry = resolve_gateway_model(model)
64
+ except HudAuthenticationError:
65
+ types = ", ".join(candidate.value for candidate in AgentType)
66
+ raise ValueError(
67
+ f"Resolving {model!r} needs the HUD model catalog; set HUD_API_KEY, or pass an "
68
+ f"agent type ({types}) and choose the model with its config."
69
+ ) from None
70
+ if entry.sdk_agent_type in ("operator", "gemini_cua"):
71
+ replacement = "openai" if entry.sdk_agent_type == "operator" else "gemini"
72
+ raise ValueError(
73
+ f"The {entry.sdk_agent_type} agent is no longer supported; use {replacement} with a "
74
+ "supported computer-use model."
75
+ )
76
+ try:
77
+ agent_type = AgentType(entry.sdk_agent_type)
78
+ except ValueError as exc:
79
+ raise ValueError(f"Model {model!r} has invalid agent type metadata") from exc
80
+ return agent_type, entry.model_name or model
81
+
82
+
118
83
  _LAZY_EXPORTS = {
119
84
  "ClaudeAgent": ("hud.agents.claude", "ClaudeAgent"),
120
85
  "ClaudeSDKAgent": ("hud.agents.claude", "ClaudeSDKAgent"),
@@ -134,6 +99,7 @@ __all__ = [
134
99
  "OpenAIAgent",
135
100
  "OpenAIChatAgent",
136
101
  "create_agent",
102
+ "resolve_agent_model",
137
103
  ]
138
104
 
139
105
 
@@ -38,7 +38,6 @@ from anthropic.types.beta import (
38
38
 
39
39
  from hud.agents.tool_agent import RunState, ToolAgent
40
40
  from hud.agents.types import AgentStep, Citation, ClaudeConfig, Usage
41
- from hud.settings import settings
42
41
  from hud.types import MCPToolCall, MCPToolResult
43
42
  from hud.utils import gateway
44
43
 
@@ -108,12 +107,11 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
108
107
  def _resolve_client(self) -> AsyncAnthropic | AsyncAnthropicBedrock:
109
108
  if self.config.model_client is not None:
110
109
  return cast("AsyncAnthropic | AsyncAnthropicBedrock", self.config.model_client)
111
- if settings.api_key:
112
- return cast("AsyncAnthropic", gateway.build_gateway_client("anthropic"))
113
- if settings.anthropic_api_key:
114
- return AsyncAnthropic(api_key=settings.anthropic_api_key)
115
- raise ValueError(
116
- "No API key found for Claude. Set HUD_API_KEY (gateway) or ANTHROPIC_API_KEY.",
110
+ return cast(
111
+ "AsyncAnthropic | AsyncAnthropicBedrock",
112
+ gateway.build_model_client(
113
+ "anthropic", model=self.config.model, gateway=self.config.gateway
114
+ ),
117
115
  )
118
116
 
119
117
  # ─── ToolAgent hooks ──────────────────────────────────────────────
@@ -4,18 +4,19 @@ from __future__ import annotations
4
4
 
5
5
  import base64
6
6
  import logging
7
- from typing import Any, cast
7
+ from typing import TYPE_CHECKING, Any, cast
8
8
 
9
9
  import mcp.types as mcp_types
10
- from google import genai
11
10
  from google.genai import types as genai_types
12
11
 
13
12
  from hud.agents.tool_agent import RunState, ToolAgent
14
13
  from hud.agents.types import AgentStep, Citation, GeminiConfig, Usage
15
- from hud.settings import settings
16
14
  from hud.types import MCPToolCall, MCPToolResult
17
15
  from hud.utils import gateway
18
16
 
17
+ if TYPE_CHECKING:
18
+ from google import genai
19
+
19
20
  from .settings import gemini_agent_settings
20
21
  from .tools import (
21
22
  PREDEFINED_COMPUTER_USE_FUNCTIONS,
@@ -54,14 +55,7 @@ class GeminiAgent(ToolAgent[genai_types.Content, GeminiConfig]):
54
55
 
55
56
  model_client = config.model_client
56
57
  if model_client is None:
57
- if settings.api_key:
58
- model_client = gateway.build_gateway_client("gemini")
59
- elif settings.gemini_api_key:
60
- model_client = genai.Client(api_key=settings.gemini_api_key)
61
- else:
62
- raise ValueError(
63
- "No API key for Gemini. Set HUD_API_KEY or GEMINI_API_KEY.",
64
- )
58
+ model_client = gateway.build_model_client("gemini", gateway=config.gateway)
65
59
 
66
60
  self.gemini_client: genai.Client = cast("genai.Client", model_client)
67
61
  self.temperature = config.temperature
@@ -230,6 +224,9 @@ class GeminiAgent(ToolAgent[genai_types.Content, GeminiConfig]):
230
224
 
231
225
  if candidate.finish_reason is not None:
232
226
  result.finish_reason = candidate.finish_reason.name
227
+ if result.finish_reason == "MALFORMED_FUNCTION_CALL":
228
+ result.error = "Provider returned a malformed function call"
229
+ result.stop_reason = "malformed_tool_call"
233
230
 
234
231
  return result
235
232
 
@@ -25,8 +25,8 @@ from openai.types.responses.response_input_param import (
25
25
  from openai.types.shared_params.reasoning import Reasoning # noqa: TC002
26
26
 
27
27
  from hud.agents.tool_agent import DegenerateTurnError, RunState, ToolAgent
28
+ from hud.agents.tools.base import result_text
28
29
  from hud.agents.types import AgentStep, Citation, OpenAIConfig, Usage
29
- from hud.settings import settings
30
30
  from hud.types import MCPToolCall, MCPToolResult
31
31
  from hud.utils import gateway
32
32
 
@@ -72,14 +72,7 @@ class OpenAIAgent(ToolAgent[ResponseInputItemParam, OpenAIConfig]):
72
72
 
73
73
  model_client = config.model_client
74
74
  if model_client is None:
75
- if settings.api_key:
76
- model_client = gateway.build_gateway_client("openai")
77
- elif settings.openai_api_key:
78
- model_client = AsyncOpenAI(api_key=settings.openai_api_key)
79
- else:
80
- raise ValueError(
81
- "No API key for OpenAI. Set HUD_API_KEY or OPENAI_API_KEY.",
82
- )
75
+ model_client = gateway.build_model_client("openai", gateway=config.gateway)
83
76
 
84
77
  self.openai_client: AsyncOpenAI = cast("AsyncOpenAI", model_client)
85
78
  self._model = config.model
@@ -116,8 +109,10 @@ class OpenAIAgent(ToolAgent[ResponseInputItemParam, OpenAIConfig]):
116
109
  if isinstance(tool, OpenAIComputerTool):
117
110
  screenshot = last_image_content(result)
118
111
  if screenshot is None:
119
- logger.warning("Computer tool result missing screenshot for call %s", call.name)
120
- return None
112
+ raise RuntimeError(
113
+ f"Computer tool result missing screenshot for call {call.id}: "
114
+ f"{result_text(result)}"
115
+ )
121
116
  output = ComputerCallOutput(
122
117
  type="computer_call_output",
123
118
  call_id=call.id,
@@ -142,7 +137,17 @@ class OpenAIAgent(ToolAgent[ResponseInputItemParam, OpenAIConfig]):
142
137
  acknowledged.append(raw_check)
143
138
  if acknowledged:
144
139
  output["acknowledged_safety_checks"] = acknowledged
145
- return cast("ResponseInputItemParam", output)
140
+ if result.isError:
141
+ return [
142
+ output,
143
+ self._format_message(
144
+ "user",
145
+ f"Computer call {call.id} stopped with an error. "
146
+ "Remaining actions in this call were not executed.\n"
147
+ f"{result_text(result)}",
148
+ ),
149
+ ]
150
+ return output
146
151
 
147
152
  if isinstance(tool, OpenAIShellTool):
148
153
  structured = (
@@ -150,8 +155,6 @@ class OpenAIAgent(ToolAgent[ResponseInputItemParam, OpenAIConfig]):
150
155
  )
151
156
  output_list = structured.get("output")
152
157
  if not isinstance(output_list, list):
153
- from hud.agents.tools.base import result_text
154
-
155
158
  text = result_text(result)
156
159
  output_list = [shell_output("", text, 1 if result.isError else 0)]
157
160
  response: dict[str, Any] = {
@@ -313,7 +316,7 @@ class OpenAIAgent(ToolAgent[ResponseInputItemParam, OpenAIConfig]):
313
316
  elif item.action is not None:
314
317
  arguments = item.action.to_dict()
315
318
  else:
316
- raise ValueError("OpenAI computer_call missing action")
319
+ arguments = {"actions": []}
317
320
  call_dict: dict[str, Any] = {
318
321
  "name": "computer",
319
322
  "arguments": arguments,
@@ -38,17 +38,24 @@ def last_image_content(result: MCPToolResult) -> mcp_types.ImageContent | None:
38
38
  OPENAI_KEY_ALIASES: dict[str, str] = {
39
39
  "return": "Return",
40
40
  "escape": "Escape",
41
+ "esc": "Escape",
41
42
  "arrowup": "Up",
43
+ "up": "Up",
42
44
  "arrowdown": "Down",
45
+ "down": "Down",
43
46
  "arrowleft": "Left",
47
+ "left": "Left",
44
48
  "arrowright": "Right",
49
+ "right": "Right",
45
50
  "backspace": "BackSpace",
46
51
  "delete": "Delete",
52
+ "del": "Delete",
47
53
  "tab": "Tab",
48
54
  "space": "space",
49
55
  "control": "Control_L",
50
56
  "ctrl": "Control_L",
51
57
  "alt": "Alt_L",
58
+ "option": "Alt_L",
52
59
  "shift": "Shift_L",
53
60
  "meta": "Super_L",
54
61
  "cmd": "Super_L",
@@ -62,18 +69,6 @@ OPENAI_KEY_ALIASES: dict[str, str] = {
62
69
  "enter": "Return",
63
70
  }
64
71
 
65
- _SCREENSHOT_ACTIONS = {
66
- "screenshot",
67
- "click",
68
- "double_click",
69
- "scroll",
70
- "type",
71
- "move",
72
- "keypress",
73
- "drag",
74
- "wait",
75
- }
76
-
77
72
 
78
73
  class OpenAIComputerTool(RFBTool):
79
74
  """Translate OpenAI native computer calls into RFBTool primitives."""
@@ -90,14 +85,16 @@ class OpenAIComputerTool(RFBTool):
90
85
 
91
86
  async def execute(self, arguments: dict[str, Any]) -> MCPToolResult:
92
87
  actions = arguments.get("actions")
93
- if isinstance(actions, list):
88
+ if "actions" in arguments:
89
+ if not isinstance(actions, list):
90
+ return await self._error_result("actions must be a list")
94
91
  action_list = cast("list[Any]", actions)
95
92
  if not action_list:
96
- return tool_err("actions list is empty")
93
+ return await self._error_result("actions list is empty")
97
94
  result = MCPToolResult(content=[], isError=False)
98
95
  for index, raw_action in enumerate(action_list):
99
96
  if not isinstance(raw_action, dict):
100
- return tool_err("actions must be objects")
97
+ return await self._error_result("actions must be objects")
101
98
  action = cast("dict[str, Any]", raw_action)
102
99
  result = await self._execute_one(
103
100
  action,
@@ -108,6 +105,17 @@ class OpenAIComputerTool(RFBTool):
108
105
  return result
109
106
  return await self._execute_one(arguments, ensure_screenshot=True)
110
107
 
108
+ async def _error_result(self, message: str) -> MCPToolResult:
109
+ result = tool_err(message)
110
+ try:
111
+ result.content.extend((await self.screenshot()).content)
112
+ except TimeoutError as exc:
113
+ exc.add_note(message)
114
+ raise
115
+ except Exception as exc:
116
+ return tool_err(f"{message}\nScreenshot capture also failed: {exc}")
117
+ return result
118
+
111
119
  async def _execute_one(
112
120
  self,
113
121
  arguments: dict[str, Any],
@@ -116,26 +124,28 @@ class OpenAIComputerTool(RFBTool):
116
124
  ) -> MCPToolResult:
117
125
  action_type = arguments.get("type")
118
126
  if not isinstance(action_type, str):
119
- return tool_err("type is required")
127
+ return await self._error_result("type is required")
120
128
 
121
129
  if action_type == "response":
122
130
  text = arguments.get("text")
123
131
  if not isinstance(text, str):
124
- return tool_err("text is required for response")
125
- return MCPToolResult(
132
+ return await self._error_result("text is required for response")
133
+ result = MCPToolResult(
126
134
  content=[mcp_types.TextContent(type="text", text=text)],
127
135
  )
136
+ if ensure_screenshot:
137
+ result.content.extend((await self.screenshot()).content)
138
+ return result
128
139
 
129
140
  try:
130
141
  await self._dispatch(action_type, arguments)
142
+ except TimeoutError:
143
+ raise
131
144
  except Exception as exc:
132
145
  logger.exception("OpenAIComputerTool action %s failed", action_type)
133
- return tool_err(f"computer action {action_type!r} failed: {exc}")
146
+ return await self._error_result(f"computer action {action_type!r} failed: {exc}")
134
147
 
135
- needs_screenshot = (
136
- ensure_screenshot and action_type in _SCREENSHOT_ACTIONS and action_type != "screenshot"
137
- )
138
- if action_type == "screenshot" or needs_screenshot:
148
+ if action_type == "screenshot" or ensure_screenshot:
139
149
  return await self.screenshot()
140
150
  return MCPToolResult(content=[], isError=False)
141
151
 
@@ -144,13 +154,13 @@ class OpenAIComputerTool(RFBTool):
144
154
  return
145
155
 
146
156
  if action_type == "click":
147
- button_raw = args.get("button")
148
- if button_raw == "wheel":
149
- button = "middle"
150
- elif isinstance(button_raw, str):
151
- button = button_raw
152
- else:
157
+ button = args.get("button", "left")
158
+ if button is None:
153
159
  button = "left"
160
+ elif button == "wheel":
161
+ button = "middle"
162
+ if button not in ("left", "middle", "right"):
163
+ raise ValueError(f"Unsupported mouse button: {button!r}")
154
164
  hold = _hold_keys(args.get("keys"))
155
165
  await self.click(
156
166
  args.get("x"),
@@ -183,23 +193,28 @@ class OpenAIComputerTool(RFBTool):
183
193
 
184
194
  elif action_type == "type":
185
195
  text = args.get("text")
186
- if isinstance(text, str):
187
- await self.type_text(text)
196
+ if not isinstance(text, str):
197
+ raise ValueError("text is required for type")
198
+ await self.type_text(text)
188
199
 
189
200
  elif action_type == "wait":
190
- ms = int(args.get("ms") or 1000)
201
+ duration = args.get("ms")
202
+ ms = 1000 if duration is None else int(duration)
203
+ if ms < 0:
204
+ raise ValueError("wait duration must be non-negative")
191
205
  await self.wait(ms)
192
206
 
193
207
  elif action_type == "move":
194
208
  x, y = args.get("x"), args.get("y")
195
- if x is not None and y is not None:
196
- await self.move(int(x), int(y))
209
+ if x is None or y is None:
210
+ raise ValueError("x and y are required for move")
211
+ await self.move(int(x), int(y))
197
212
 
198
213
  elif action_type == "keypress":
199
- keys = args.get("keys")
200
- if isinstance(keys, list):
201
- mapped = [_map_key(str(k)) for k in cast("list[Any]", keys)]
202
- await self.press_keys(mapped)
214
+ keys = _hold_keys(args.get("keys"))
215
+ if not keys:
216
+ raise ValueError("keypress requires at least one key")
217
+ await self.press_keys(keys)
203
218
 
204
219
  elif action_type == "drag":
205
220
  path_raw = args.get("path")
@@ -208,7 +223,7 @@ class OpenAIComputerTool(RFBTool):
208
223
  points = cast("list[dict[str, Any]]", path_raw)
209
224
  if len(points) < 2:
210
225
  raise ValueError("drag requires a path with at least 2 points")
211
- path = [(int(p.get("x", 0)), int(p.get("y", 0))) for p in points]
226
+ path = [(int(p["x"]), int(p["y"])) for p in points]
212
227
  hold = _hold_keys(args.get("keys"))
213
228
  await self.drag(path, hold_keys=hold)
214
229
 
@@ -224,9 +239,11 @@ def _map_key(key: str) -> str:
224
239
 
225
240
 
226
241
  def _hold_keys(keys: Any) -> list[str] | None:
227
- if not isinstance(keys, list):
242
+ if keys is None:
228
243
  return None
229
- return [_map_key(str(key)) for key in cast("list[Any]", keys)]
244
+ if not isinstance(keys, list) or not all(isinstance(key, str) and key for key in keys):
245
+ raise ValueError("keys must be a list of non-empty strings")
246
+ return [_map_key(key) for key in cast("list[str]", keys)]
230
247
 
231
248
 
232
249
  __all__ = ["OPENAI_COMPUTER_SPEC", "OpenAIComputerTool"]
@@ -9,12 +9,16 @@ from types import SimpleNamespace
9
9
  from typing import TYPE_CHECKING, Any
10
10
  from unittest.mock import AsyncMock, patch
11
11
 
12
+ import mcp.types as mcp_types
13
+ import pytest
14
+
12
15
  from hud.agents.openai.tools.computer import (
13
16
  OpenAIComputerTool,
14
17
  _hold_keys,
15
18
  _map_key,
16
19
  )
17
- from hud.agents.tools.base import result_text, tool_ok
20
+ from hud.agents.tools.base import result_text
21
+ from hud.types import MCPToolResult
18
22
 
19
23
  if TYPE_CHECKING:
20
24
  from collections.abc import Iterable
@@ -27,9 +31,11 @@ class RecordingOpenAI(OpenAIComputerTool):
27
31
  self.calls: list[tuple[Any, ...]] = []
28
32
  self.client = SimpleNamespace(width=200, height=100)
29
33
 
30
- async def screenshot(self) -> Any:
34
+ async def screenshot(self) -> MCPToolResult:
31
35
  self.calls.append(("screenshot",))
32
- return tool_ok("shot")
36
+ return MCPToolResult(
37
+ content=[mcp_types.ImageContent(type="image", data="c2hvdA==", mimeType="image/png")],
38
+ )
33
39
 
34
40
  async def click(
35
41
  self,
@@ -76,8 +82,10 @@ class RecordingOpenAI(OpenAIComputerTool):
76
82
  def test_key_mapping() -> None:
77
83
  assert _map_key("ctrl") == "Control_L"
78
84
  assert _map_key("x") == "x"
85
+ assert _map_key("ESC") == "Escape"
86
+ assert _map_key("RIGHT") == "Right"
79
87
  assert _hold_keys(["ctrl", "c"]) == ["Control_L", "c"]
80
- assert _hold_keys("notalist") is None
88
+ assert _hold_keys(None) is None
81
89
 
82
90
 
83
91
  def test_to_params() -> None:
@@ -99,19 +107,23 @@ async def test_type_and_keypress() -> None:
99
107
  assert ("keys", ("Control_L", "c")) in tool.calls
100
108
 
101
109
 
102
- async def test_drag_and_wait() -> None:
110
+ @pytest.mark.parametrize(("ms", "seconds"), [(500, 0.5), (0, 0)])
111
+ async def test_drag_and_wait(ms: int, seconds: float) -> None:
103
112
  tool = RecordingOpenAI()
104
113
  await tool.execute({"type": "drag", "path": [{"x": 0, "y": 0}, {"x": 5, "y": 5}]})
105
114
  with patch("hud.agents.tools.rfb.asyncio.sleep", new_callable=AsyncMock) as sleep:
106
- await tool.execute({"type": "wait", "ms": 500})
115
+ result = await tool.execute({"type": "wait", "ms": ms})
107
116
  assert ("drag", ((0, 0), (5, 5))) in tool.calls
108
- sleep.assert_awaited_once_with(0.5)
117
+ assert not result.isError
118
+ sleep.assert_awaited_once_with(seconds)
109
119
 
110
120
 
111
- async def test_response_action_returns_text() -> None:
121
+ async def test_response_action_returns_text_and_screenshot() -> None:
112
122
  tool = RecordingOpenAI()
113
123
  result = await tool.execute({"type": "response", "text": "all done"})
114
124
  assert result_text(result) == "all done"
125
+ assert any(isinstance(block, mcp_types.ImageContent) for block in result.content)
126
+ assert tool.calls[-1] == ("screenshot",)
115
127
 
116
128
 
117
129
  async def test_actions_list_runs_each() -> None:
@@ -123,12 +135,40 @@ async def test_actions_list_runs_each() -> None:
123
135
  assert ("type", "a") in tool.calls
124
136
 
125
137
 
126
- async def test_empty_actions_errors() -> None:
138
+ @pytest.mark.parametrize(
139
+ "arguments", [{"actions": []}, {"actions": "invalid", "type": "move", "x": 3, "y": 4}]
140
+ )
141
+ async def test_invalid_actions_errors(arguments: dict[str, Any]) -> None:
127
142
  tool = RecordingOpenAI()
128
- assert (await tool.execute({"actions": []})).isError
143
+ assert (await tool.execute(arguments)).isError
144
+ assert tool.calls == [("screenshot",)]
129
145
 
130
146
 
131
147
  async def test_invalid_type_errors() -> None:
132
148
  tool = RecordingOpenAI()
133
149
  assert (await tool.execute({"type": "frobnicate"})).isError
134
150
  assert (await tool.execute({})).isError
151
+
152
+
153
+ @pytest.mark.parametrize(
154
+ "action",
155
+ [
156
+ {"type": "type"},
157
+ {"type": "move", "x": 10},
158
+ {"type": "keypress", "keys": "ESC"},
159
+ {"type": "keypress", "keys": []},
160
+ {"type": "drag", "path": [{"x": 1, "y": 2}, {"x": 3}]},
161
+ {"type": "click", "button": 1},
162
+ {"type": "wait", "ms": -1},
163
+ ],
164
+ )
165
+ async def test_invalid_action_stops_batch_and_returns_screenshot(action: dict[str, Any]) -> None:
166
+ tool = RecordingOpenAI()
167
+ result = await tool.execute(
168
+ {"actions": [{"type": "move", "x": 3, "y": 4}, action, {"type": "type", "text": "later"}]},
169
+ )
170
+
171
+ assert result.isError
172
+ assert result_text(result)
173
+ assert any(isinstance(block, mcp_types.ImageContent) for block in result.content)
174
+ assert tool.calls == [("move", 3, 4), ("screenshot",)]