hud 0.6.18.dev1__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 (282) hide show
  1. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/PKG-INFO +1 -1
  2. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/agent.py +3 -0
  3. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/test_gemini_agent.py +16 -0
  4. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/test_tool_agent.py +17 -0
  5. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tool_agent.py +5 -0
  6. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tools/ssh.py +1 -1
  7. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/types.py +1 -0
  8. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/eval.py +17 -8
  9. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/test_eval.py +269 -3
  10. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/__init__.py +21 -7
  11. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/namespace.py +77 -29
  12. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_loader.py +140 -0
  13. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_workspace.py +96 -1
  14. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/utils.py +16 -2
  15. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/workspace.py +66 -58
  16. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/runtime/local.py +25 -23
  17. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/tests/test_local_runtime.py +114 -3
  18. hud-0.6.18.dev2/hud/utils/modules.py +122 -0
  19. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/process.py +57 -0
  20. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/version.py +1 -1
  21. hud-0.6.18.dev1/hud/utils/modules.py +0 -79
  22. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/.gitignore +0 -0
  23. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/LICENSE +0 -0
  24. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/README.md +0 -0
  25. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/__init__.py +0 -0
  26. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/__main__.py +0 -0
  27. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/__init__.py +0 -0
  28. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/base.py +0 -0
  29. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/browser_use/__init__.py +0 -0
  30. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/browser_use/agent.py +0 -0
  31. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/__init__.py +0 -0
  32. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/agent.py +0 -0
  33. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/sdk/__init__.py +0 -0
  34. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/sdk/agent.py +0 -0
  35. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/sdk/computer_mcp.py +0 -0
  36. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/tools/__init__.py +0 -0
  37. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/tools/base.py +0 -0
  38. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/tools/coding.py +0 -0
  39. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/tools/computer.py +0 -0
  40. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/tools/hosted.py +0 -0
  41. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/tools/mcp_proxy.py +0 -0
  42. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/tools/settings.py +0 -0
  43. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/tools/tests/__init__.py +0 -0
  44. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/claude/tools/tests/test_computer.py +0 -0
  45. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/__init__.py +0 -0
  46. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/settings.py +0 -0
  47. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/tools/__init__.py +0 -0
  48. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/tools/base.py +0 -0
  49. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/tools/coding.py +0 -0
  50. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/tools/computer.py +0 -0
  51. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/tools/filesystem.py +0 -0
  52. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/tools/hosted.py +0 -0
  53. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/tools/mcp_proxy.py +0 -0
  54. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/tools/tests/__init__.py +0 -0
  55. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/gemini/tools/tests/test_computer.py +0 -0
  56. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/misc/__init__.py +0 -0
  57. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/misc/response_automation.py +0 -0
  58. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/__init__.py +0 -0
  59. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/agent.py +0 -0
  60. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/tools/__init__.py +0 -0
  61. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/tools/apply_patch.py +0 -0
  62. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/tools/base.py +0 -0
  63. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/tools/coding.py +0 -0
  64. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/tools/computer.py +0 -0
  65. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/tools/hosted.py +0 -0
  66. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/tools/mcp_proxy.py +0 -0
  67. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/tools/strict_schema.py +0 -0
  68. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/tools/tests/__init__.py +0 -0
  69. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/tools/tests/test_computer.py +0 -0
  70. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai/tools/tests/test_strict_schema.py +0 -0
  71. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai_compatible/__init__.py +0 -0
  72. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai_compatible/agent.py +0 -0
  73. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai_compatible/tools/__init__.py +0 -0
  74. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai_compatible/tools/base.py +0 -0
  75. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai_compatible/tools/filesystem.py +0 -0
  76. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/openai_compatible/tools/mcp_proxy.py +0 -0
  77. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/robot/__init__.py +0 -0
  78. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/robot/adapter.py +0 -0
  79. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/robot/agent.py +0 -0
  80. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/robot/batching.py +0 -0
  81. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/robot/dataset.py +0 -0
  82. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/robot/model.py +0 -0
  83. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/__init__.py +0 -0
  84. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/test_apply_patch.py +0 -0
  85. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/test_base.py +0 -0
  86. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/test_claude_agent.py +0 -0
  87. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/test_claude_sdk_agent.py +0 -0
  88. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/test_openai_agent.py +0 -0
  89. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/test_openai_compatible_agent.py +0 -0
  90. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/test_provider_native_tools.py +0 -0
  91. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/test_robot_dataset_writer.py +0 -0
  92. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tests/test_trace.py +0 -0
  93. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tools/__init__.py +0 -0
  94. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tools/base.py +0 -0
  95. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tools/hosted.py +0 -0
  96. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tools/mcp.py +0 -0
  97. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/agents/tools/rfb.py +0 -0
  98. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/capabilities/__init__.py +0 -0
  99. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/capabilities/base.py +0 -0
  100. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/capabilities/cdp.py +0 -0
  101. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/capabilities/mcp.py +0 -0
  102. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/capabilities/rfb.py +0 -0
  103. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/capabilities/robot.py +0 -0
  104. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/capabilities/ssh.py +0 -0
  105. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/capabilities/tests/__init__.py +0 -0
  106. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/capabilities/tests/test_rfb.py +0 -0
  107. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/capabilities/tests/test_ssh.py +0 -0
  108. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/__init__.py +0 -0
  109. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/__main__.py +0 -0
  110. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/deploy.py +0 -0
  111. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/init.py +0 -0
  112. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/jobs.py +0 -0
  113. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/models.py +0 -0
  114. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/project.py +0 -0
  115. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/qa.py +0 -0
  116. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/serve.py +0 -0
  117. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/sync.py +0 -0
  118. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/task.py +0 -0
  119. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/__init__.py +0 -0
  120. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/test_cli.py +0 -0
  121. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/test_deploy.py +0 -0
  122. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/test_init.py +0 -0
  123. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/test_jobs.py +0 -0
  124. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/test_main.py +0 -0
  125. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/test_project.py +0 -0
  126. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/test_qa.py +0 -0
  127. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/test_sync.py +0 -0
  128. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/test_task.py +0 -0
  129. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/tests/test_trace.py +0 -0
  130. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/cli/trace.py +0 -0
  131. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/clients/__init__.py +0 -0
  132. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/clients/client.py +0 -0
  133. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/clients/tests/__init__.py +0 -0
  134. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/clients/tests/test_connect.py +0 -0
  135. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/conftest.py +0 -0
  136. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/arguments.py +0 -0
  137. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/egress.py +0 -0
  138. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/env.py +0 -0
  139. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/file_tracker.py +0 -0
  140. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/robot/__init__.py +0 -0
  141. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/robot/bridge.py +0 -0
  142. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/robot/endpoint.py +0 -0
  143. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/robot/gym.py +0 -0
  144. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/server.py +0 -0
  145. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/__init__.py +0 -0
  146. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/conftest.py +0 -0
  147. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_arguments.py +0 -0
  148. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_capability_backing.py +0 -0
  149. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_file_tracker.py +0 -0
  150. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_file_tracking.py +0 -0
  151. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_manifest.py +0 -0
  152. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_platform.py +0 -0
  153. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_robot_regressions.py +0 -0
  154. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_server.py +0 -0
  155. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_sessions.py +0 -0
  156. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/environment/tests/test_tunnel.py +0 -0
  157. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/__init__.py +0 -0
  158. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/chat.py +0 -0
  159. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/docker-seccomp.json +0 -0
  160. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/file_tracking.py +0 -0
  161. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/job.py +0 -0
  162. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/run.py +0 -0
  163. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/runtime/__init__.py +0 -0
  164. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/runtime/compose.py +0 -0
  165. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/runtime/core.py +0 -0
  166. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/runtime/daytona.py +0 -0
  167. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/runtime/docker.py +0 -0
  168. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/runtime/hosted.py +0 -0
  169. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/runtime/hud.py +0 -0
  170. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/runtime/modal.py +0 -0
  171. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/sync.py +0 -0
  172. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/task.py +0 -0
  173. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/taskset.py +0 -0
  174. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/tests/__init__.py +0 -0
  175. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/tests/test_chat.py +0 -0
  176. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/tests/test_docker_provider.py +0 -0
  177. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/tests/test_file_tracking_observer.py +0 -0
  178. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/tests/test_hosted.py +0 -0
  179. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/tests/test_job.py +0 -0
  180. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/tests/test_rollout.py +0 -0
  181. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/tests/test_shared.py +0 -0
  182. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/tests/test_sync.py +0 -0
  183. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/eval/tests/test_task.py +0 -0
  184. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/graders/__init__.py +0 -0
  185. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/graders/base.py +0 -0
  186. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/graders/bash.py +0 -0
  187. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/graders/combine.py +0 -0
  188. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/graders/judge.py +0 -0
  189. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/graders/results.py +0 -0
  190. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/graders/text.py +0 -0
  191. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/__init__.py +0 -0
  192. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/Dockerfile +0 -0
  193. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/__init__.py +0 -0
  194. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/adapt.py +0 -0
  195. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/env.py +0 -0
  196. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/export.py +0 -0
  197. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/install.sh +0 -0
  198. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/__init__.py +0 -0
  199. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/conftest.py +0 -0
  200. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/environment/Dockerfile +0 -0
  201. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/instruction.md +0 -0
  202. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/solution/solve.sh +0 -0
  203. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/task.toml +0 -0
  204. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/tests/test.sh +0 -0
  205. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/Dockerfile +0 -0
  206. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/docker-compose.yaml +0 -0
  207. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/Dockerfile +0 -0
  208. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/__init__.py +0 -0
  209. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/server.py +0 -0
  210. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/instruction.md +0 -0
  211. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/solution/solve.sh +0 -0
  212. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/task.toml +0 -0
  213. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/tests/test.sh +0 -0
  214. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/Dockerfile +0 -0
  215. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/entrypoint.sh +0 -0
  216. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/instruction.md +0 -0
  217. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/solution/solve.sh +0 -0
  218. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/task.toml +0 -0
  219. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/tests/test.sh +0 -0
  220. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/Dockerfile +0 -0
  221. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/docker-compose.yaml +0 -0
  222. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/instruction.md +0 -0
  223. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/solution/solve.sh +0 -0
  224. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/task.toml +0 -0
  225. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/Dockerfile +0 -0
  226. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/test.sh +0 -0
  227. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/Dockerfile +0 -0
  228. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/entrypoint.sh +0 -0
  229. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/instruction.md +0 -0
  230. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/solution/solve.sh +0 -0
  231. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/task.toml +0 -0
  232. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/tests/test.sh +0 -0
  233. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/test_contract.py +0 -0
  234. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/test_harbor.py +0 -0
  235. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/integrations/harbor/tests/test_integration.py +0 -0
  236. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/patches/__init__.py +0 -0
  237. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/patches/mcp_patches.py +0 -0
  238. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/patches/tests/__init__.py +0 -0
  239. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/patches/tests/test_warnings.py +0 -0
  240. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/patches/warnings.py +0 -0
  241. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/py.typed +0 -0
  242. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/settings.py +0 -0
  243. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/telemetry/__init__.py +0 -0
  244. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/telemetry/context.py +0 -0
  245. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/telemetry/exporter.py +0 -0
  246. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/telemetry/instrument.py +0 -0
  247. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/telemetry/robot/__init__.py +0 -0
  248. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/telemetry/robot/recorder.py +0 -0
  249. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/telemetry/robot/video.py +0 -0
  250. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/telemetry/span.py +0 -0
  251. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/telemetry/tests/__init__.py +0 -0
  252. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/telemetry/tests/test_exporter.py +0 -0
  253. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/telemetry/tests/test_instrument.py +0 -0
  254. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/train/__init__.py +0 -0
  255. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/train/base.py +0 -0
  256. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/train/client.py +0 -0
  257. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/train/tests/__init__.py +0 -0
  258. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/train/tests/test_client.py +0 -0
  259. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/train/types.py +0 -0
  260. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/types.py +0 -0
  261. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/__init__.py +0 -0
  262. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/docker.py +0 -0
  263. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/exceptions.py +0 -0
  264. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/gateway.py +0 -0
  265. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/hints.py +0 -0
  266. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/hud_console.py +0 -0
  267. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/naming.py +0 -0
  268. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/platform.py +0 -0
  269. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/requests.py +0 -0
  270. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/serialization.py +0 -0
  271. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/tests/__init__.py +0 -0
  272. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/tests/test_exceptions.py +0 -0
  273. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/tests/test_gateway.py +0 -0
  274. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/tests/test_hints.py +0 -0
  275. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/tests/test_hud_console.py +0 -0
  276. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/tests/test_naming.py +0 -0
  277. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/tests/test_platform.py +0 -0
  278. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/tests/test_process.py +0 -0
  279. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/tests/test_requests.py +0 -0
  280. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/tests/test_serialization.py +0 -0
  281. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/hud/utils/time.py +0 -0
  282. {hud-0.6.18.dev1 → hud-0.6.18.dev2}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hud
3
- Version: 0.6.18.dev1
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
@@ -224,6 +224,9 @@ class GeminiAgent(ToolAgent[genai_types.Content, GeminiConfig]):
224
224
 
225
225
  if candidate.finish_reason is not None:
226
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"
227
230
 
228
231
  return result
229
232
 
@@ -145,3 +145,19 @@ def test_grounding_citations() -> None:
145
145
  assert len(cites) == 1
146
146
  assert cites[0].source == "http://x"
147
147
  assert cites[0].type == "grounding"
148
+
149
+
150
+ async def test_malformed_function_call_is_a_normalized_error() -> None:
151
+ response = _api_response(
152
+ SimpleNamespace(
153
+ content=None,
154
+ grounding_metadata=None,
155
+ finish_reason=SimpleNamespace(name="MALFORMED_FUNCTION_CALL"),
156
+ )
157
+ )
158
+ agent = _agent(response)
159
+ result = await agent.get_response(_state(agent))
160
+ assert result.error == "Provider returned a malformed function call"
161
+ assert result.stop_reason == "malformed_tool_call"
162
+ assert result.finish_reason == "MALFORMED_FUNCTION_CALL"
163
+ assert result.usage is not None
@@ -788,3 +788,20 @@ async def test_loop_stops_on_length_when_configured() -> None:
788
788
  assert run.trace.stop_reason == "length"
789
789
  assert run.trace.is_truncated is True
790
790
  assert all(not isinstance(step, ToolStep) for step in run.trace.steps)
791
+
792
+
793
+ @pytest.mark.parametrize("auto_respond", [False, True])
794
+ @pytest.mark.parametrize("reason", [None, "malformed_tool_call"])
795
+ async def test_provider_error_stops_without_auto_response(
796
+ auto_respond: bool, reason: Literal["malformed_tool_call"] | None
797
+ ) -> None:
798
+ agent = DictAgent(
799
+ [AgentStep(error="provider failure", stop_reason=reason)],
800
+ auto_respond=auto_respond,
801
+ )
802
+ run = cast("Run", _FakeRun())
803
+ await agent._loop(run, RunState(), max_steps=3)
804
+ assert run.trace.status == "error"
805
+ assert run.trace.stop_reason == reason
806
+ assert len(run.trace.steps) == 1
807
+ assert run.trace.steps[0].error == "provider failure"
@@ -253,6 +253,9 @@ class ToolAgent(Agent, Generic[MessageT, ConfigT]):
253
253
  step.started_at = step.started_at or started_at
254
254
  step.model = step.model or self.config.model
255
255
  run.record(step)
256
+ if step.error:
257
+ stopped = step.stop_reason
258
+ break
256
259
 
257
260
  if step.tool_calls:
258
261
  logger.info(" → %s", ", ".join(c.name for c in step.tool_calls))
@@ -304,6 +307,8 @@ class ToolAgent(Agent, Generic[MessageT, ConfigT]):
304
307
  trace.status = "error" if step is not None and step.error else "completed"
305
308
  if stopped is not None:
306
309
  trace.stop_reason = stopped
310
+ elif step is not None and step.error:
311
+ trace.stop_reason = None
307
312
  elif hit_max:
308
313
  trace.stop_reason = "max_steps"
309
314
  elif step is not None and step.finish_reason in TRUNCATION_FINISH_REASONS:
@@ -65,7 +65,7 @@ class SSHTool(AgentTool[SSHClient]):
65
65
  await self.client.write_text(path, content)
66
66
  except asyncssh.ProcessError as e:
67
67
  return tool_err(_remote_error(e))
68
- return tool_ok(f"wrote {len(content)} bytes to {path}")
68
+ return tool_ok(f"wrote {len(content.encode('utf-8'))} bytes to {path}")
69
69
 
70
70
  async def file_list(self, path: str = "/") -> MCPToolResult:
71
71
  """List directory entries through SSH exec."""
@@ -288,6 +288,7 @@ class AgentStep(Step):
288
288
  #: No further tool calls expected — the loop's stop signal.
289
289
  done: bool = False
290
290
  finish_reason: str | None = None
291
+ stop_reason: StopCondition | None = None
291
292
  refusal: str | None = None
292
293
  citations: list[Citation] = Field(default_factory=list[Citation])
293
294
  raw: Any | None = None
@@ -102,7 +102,7 @@ class EvalConfig(BaseModel):
102
102
  group_size: int = 1
103
103
  gateway: bool = False
104
104
  #: ``LOCAL`` spawns each row's env (Docker for container rows, a subprocess
105
- #: serving the bound env's source otherwise); other names hand rows to that
105
+ #: loading the task source otherwise); other names hand rows to that
106
106
  #: provider; a ``tcp://`` url attaches to an already-served env. ``None``
107
107
  #: infers from the source: a file on disk runs locally, a platform taskset hosted.
108
108
  runtime: Placement | TcpUrl | None = None
@@ -417,24 +417,33 @@ def eval_command(
417
417
  case AnyUrl():
418
418
  placement = Runtime(str(cfg.runtime))
419
419
  case Placement.LOCAL:
420
- # Isolate each row: its container, or a subprocess serving the bound env's
421
- # source (``Taskset.run`` alone would serve a live env in-process). Data
422
- # rows (JSON/JSONL) only name their env; its source lives beside the file.
423
- if not Path(source).exists():
420
+ rows = list(taskset)
421
+ rows.extend(
422
+ task.verifier
423
+ for task in taskset
424
+ if task.verifier is not None and not task.shares_verifier_runtime
425
+ )
426
+ if not Path(source).exists() and any(
427
+ not (
428
+ task.runtime_config
429
+ and (task.runtime_config.image or task.runtime_config.compose)
430
+ )
431
+ for task in rows
432
+ ):
424
433
  raise ValueError(
425
434
  f"{source} is a platform taskset, so there is no env source to spawn "
426
435
  "locally. Run it with --remote, --runtime hud, or --runtime tcp://host:port."
427
436
  )
428
437
  docker = DockerRuntime()
429
- source_dir = Path(source).resolve()
430
- beside = SubprocessRuntime(source_dir if source_dir.is_dir() else source_dir.parent)
438
+ source_path = Path(source).resolve()
439
+ beside = SubprocessRuntime(source_path if source_path.is_dir() else source_path.parent)
431
440
 
432
441
  def spawn(task: Task) -> AbstractAsyncContextManager[Runtime]:
433
442
  config = task.runtime_config
434
443
  if config and (config.image or config.compose):
435
444
  return docker(task)
436
445
  if task._env is not None:
437
- return SubprocessRuntime(task._env)(task)
446
+ return SubprocessRuntime(source_path)(task)
438
447
  return beside(task)
439
448
 
440
449
  placement = spawn
@@ -5,12 +5,14 @@ from __future__ import annotations
5
5
  import json
6
6
  import sys
7
7
  from dataclasses import dataclass, field
8
+ from pathlib import Path
8
9
  from typing import TYPE_CHECKING, Any
9
10
  from unittest.mock import MagicMock
10
11
 
11
12
  import pytest
12
13
  from typer.testing import CliRunner
13
14
 
15
+ from hud.agents import OpenAIAgent
14
16
  from hud.agents.types import ClaudeConfig
15
17
  from hud.cli import eval as eval_mod
16
18
  from hud.cli.__main__ import app
@@ -34,8 +36,7 @@ from hud.utils.gateway import GatewayModelInfo
34
36
  from hud.utils.hud_console import HUDConsole
35
37
 
36
38
  if TYPE_CHECKING:
37
- from collections.abc import Callable
38
- from pathlib import Path
39
+ from collections.abc import Callable, Iterator
39
40
 
40
41
  _TASKS_PY = """\
41
42
  from hud import Environment
@@ -405,7 +406,7 @@ def test_local_placement_routes_each_row(eval_cli: _EvalCli, tmp_path: Path, mon
405
406
  assert eval_cli.taskset is not None
406
407
  bound = next(iter(eval_cli.taskset))
407
408
 
408
- assert placement(bound) is subprocesses[bound._env].return_value
409
+ assert placement(bound) is subprocesses[(tmp_path / "mixed.py").resolve()].return_value
409
410
  assert placement(image) is docker.return_value
410
411
  # A row without a bound env is served from the tasks file's directory.
411
412
  assert placement(data_row) is subprocesses[tmp_path.resolve()].return_value
@@ -685,3 +686,268 @@ def test_result_payload_uses_job_metrics(eval_cli: _EvalCli) -> None:
685
686
  assert payload["error_count"] == 1
686
687
  assert [run["slug"] for run in payload["runs"]] == ["solve", None]
687
688
  assert [run["is_error"] for run in payload["runs"]] == [False, False]
689
+
690
+
691
+ @pytest.fixture
692
+ def local_eval(
693
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
694
+ ) -> Iterator[Callable[..., dict[str, Any]]]:
695
+ async def answer(self: OpenAIAgent, run: Run) -> None:
696
+ run.trace.content = "ok"
697
+
698
+ monkeypatch.setattr(OpenAIAgent, "__call__", answer)
699
+ monkeypatch.setattr(settings, "openai_api_key", "test-provider-key")
700
+ monkeypatch.setenv("HUD_API_KEY", "")
701
+ monkeypatch.setenv("HUD_TELEMETRY_ENABLED", "false")
702
+ monkeypatch.chdir(tmp_path)
703
+ original = dict(sys.modules)
704
+
705
+ def invoke(source: Path | str, *flags: str) -> dict[str, Any]:
706
+ result = CliRunner().invoke(
707
+ app, ["eval", str(source), "openai", "--all", "--yes", "--json", *flags]
708
+ )
709
+ assert result.exit_code == 0, result.output
710
+ payload = json.loads(result.stdout)
711
+ assert payload["error_count"] == 0, result.output
712
+ assert payload["mean_reward"] == 1.0, result.output
713
+ return payload
714
+
715
+ yield invoke
716
+ for name, module in list(sys.modules.items()):
717
+ file = getattr(module, "__file__", None)
718
+ if file and Path(file).is_relative_to(tmp_path):
719
+ if name in original:
720
+ sys.modules[name] = original[name]
721
+ else:
722
+ sys.modules.pop(name, None)
723
+
724
+
725
+ @pytest.mark.parametrize(
726
+ "layout",
727
+ [
728
+ "single",
729
+ "standalone",
730
+ "split",
731
+ "hooks",
732
+ "hooks_source",
733
+ "assembled",
734
+ "assembled_source",
735
+ "json",
736
+ "jsonl",
737
+ "data_python",
738
+ "directory",
739
+ "assembled_directory",
740
+ "package",
741
+ "lazy_package",
742
+ ],
743
+ )
744
+ def test_local_eval_project_layouts(
745
+ local_eval: Callable[..., dict[str, Any]], tmp_path: Path, layout: str
746
+ ) -> None:
747
+ project = tmp_path / "project"
748
+ project.mkdir()
749
+ events = project / "events"
750
+ events.mkdir()
751
+ (project / "asset.txt").write_text("ok")
752
+ package = layout in {"package", "lazy_package"}
753
+ prefix = "." if package else ""
754
+ if package:
755
+ (project / "__init__.py").write_text("")
756
+ template = (
757
+ "from pathlib import Path\n"
758
+ '@env.template(id="solve")\n'
759
+ "async def solve():\n"
760
+ ' answer = yield "answer ok"\n'
761
+ ' yield 1.0 if answer == Path("asset.txt").read_text() else 0.0\n'
762
+ )
763
+ hooks = (
764
+ "import os\nfrom pathlib import Path\n"
765
+ "@env.initialize\nasync def start():\n"
766
+ ' Path(f"events/{os.getpid()}").write_text("started")\n'
767
+ "@env.shutdown\nasync def stop():\n"
768
+ ' Path(f"events/{os.getpid()}").write_text("stopped")\n'
769
+ )
770
+ if layout == "lazy_package":
771
+ (project / "expected.py").write_text(
772
+ 'from pathlib import Path\nexpected = Path("asset.txt").read_text()\n'
773
+ )
774
+ template = template.replace(
775
+ ' answer = yield "answer ok"\n',
776
+ ' from .expected import expected\n answer = yield "answer ok"\n',
777
+ ).replace('Path("asset.txt").read_text()', "expected")
778
+ if layout in {
779
+ "hooks",
780
+ "hooks_source",
781
+ "assembled",
782
+ "assembled_source",
783
+ "assembled_directory",
784
+ "package",
785
+ }:
786
+ (project / "local_core.py").write_text(
787
+ 'from hud import Environment\nenv = Environment("local-test")\n'
788
+ )
789
+ (project / "local_templates.py").write_text(
790
+ f"from {prefix}local_core import env\n" + template
791
+ )
792
+ (project / "local_extra.py").write_text(
793
+ f"from {prefix}local_core import env\n"
794
+ '@env.template(id="extra")\nasync def extra():\n yield "extra"\n yield 1.0\n'
795
+ )
796
+ env_source = (
797
+ f"from {prefix}local_core import env\n"
798
+ f"from {prefix}local_templates import solve\n"
799
+ + (f"from {prefix}local_extra import extra\n" if not layout.startswith("hooks") else "")
800
+ + hooks
801
+ )
802
+ else:
803
+ env_source = (
804
+ 'from hud import Environment\nenv = Environment("local-test")\n' + template + hooks
805
+ )
806
+ if layout in {"single", "standalone", "lazy_package", "hooks_source", "assembled_source"}:
807
+ env_source += "tasks = [solve()]\n"
808
+ source = project / "env.py"
809
+ else:
810
+ (project / "tasks.py").write_text(
811
+ f"from {prefix}env import env, solve\ntasks = [solve()]\n"
812
+ )
813
+ source = project / "tasks.py"
814
+ (project / "env.py").write_text(env_source)
815
+ if layout == "standalone":
816
+ source = project / "standalone.py"
817
+ source.write_text(env_source)
818
+ (project / "env.py").write_text(
819
+ 'from hud import Environment\nenv = Environment("unrelated")\n'
820
+ )
821
+ if layout in {"json", "jsonl"}:
822
+ source = project / f"tasks.{layout}"
823
+ row = {"env": "local-test", "id": "solve"}
824
+ source.write_text(json.dumps([row] if layout == "json" else row))
825
+ if layout == "data_python":
826
+ source.write_text(
827
+ 'from hud.eval import Task\ntasks = [Task(env="local-test", id="solve")]\n'
828
+ )
829
+ if layout in {"directory", "assembled_directory"}:
830
+ source = project
831
+ payload = local_eval(source, "--group", "2", "--max-concurrent", "2")
832
+ assert payload["run_count"] == 2
833
+ assert len(list(events.iterdir())) == 2
834
+ assert {event.read_text() for event in events.iterdir()} == {"stopped"}
835
+
836
+
837
+ @pytest.mark.parametrize(
838
+ "source,same_name",
839
+ [
840
+ ("tasks.py", False),
841
+ ("tasks.py", True),
842
+ (".", False),
843
+ (".", True),
844
+ ("rows.json", False),
845
+ ("rows.json", True),
846
+ ("rows.jsonl", False),
847
+ ],
848
+ )
849
+ def test_local_eval_environment_selection(
850
+ local_eval: Callable[..., dict[str, Any]], tmp_path: Path, source: str, same_name: bool
851
+ ) -> None:
852
+ project = tmp_path / "project"
853
+ project.mkdir()
854
+ (project / "bound_env.py").write_text(
855
+ 'from hud import Environment\nenv = Environment("bound")\n'
856
+ '@env.template(id="solve")\nasync def solve():\n'
857
+ ' answer = yield "answer ok"\n yield float(answer == "ok")\n'
858
+ )
859
+ name = "bound" if same_name else "unrelated"
860
+ (project / "env.py").write_text(
861
+ f"from hud import Environment\nenv = Environment({name!r})\n"
862
+ '@env.template(id="solve")\nasync def solve():\n'
863
+ ' yield "unrelated"\n yield 0.0\n'
864
+ )
865
+ (project / "tasks.py").write_text("from bound_env import solve\ntasks = [solve()]\n")
866
+ if source in {"rows.json", "rows.jsonl"}:
867
+ (project / source).write_text('{"env": "bound", "id": "solve"}\n')
868
+ if same_name and source != "tasks.py":
869
+ result = CliRunner().invoke(
870
+ app, ["eval", str(project / source), "openai", "--all", "--yes", "--json"]
871
+ )
872
+ assert result.exit_code == 0, result.output
873
+ assert json.loads(result.stdout)["error_count"] == 1
874
+ assert "multiple Environments" in result.output
875
+ else:
876
+ assert local_eval(project / source)["run_count"] == 1
877
+
878
+
879
+ @pytest.mark.parametrize("shared", [True, False])
880
+ def test_local_eval_places_nested_verifier(
881
+ local_eval: Callable[..., dict[str, Any]], tmp_path: Path, shared: bool
882
+ ) -> None:
883
+ project = tmp_path / "project"
884
+ project.mkdir()
885
+ (project / "local_core.py").write_text(
886
+ 'from hud import Environment\nactor = Environment("actor")\n'
887
+ + ("judge = actor\n" if shared else 'judge = Environment("judge")\n')
888
+ )
889
+ (project / "local_actor.py").write_text(
890
+ "from local_core import actor\n"
891
+ '@actor.template(id="solve")\nasync def solve():\n'
892
+ ' answer = yield "answer ok"\n yield {"score": 0.0, "answer": answer}\n'
893
+ )
894
+ (project / "local_judge.py").write_text(
895
+ "from local_core import judge\n"
896
+ '@judge.template(id="verify")\nasync def verify():\n'
897
+ ' result = yield ""\n yield 1.0 if result["answer"] == "ok" else 0.0\n'
898
+ )
899
+ (project / "env.py").write_text(
900
+ "from local_core import actor, judge\n"
901
+ "from local_actor import solve\nfrom local_judge import verify\n"
902
+ )
903
+ (project / "tasks.py").write_text(
904
+ "from env import solve, verify\ntasks = [solve()]\ntasks[0].verifier = verify()\n"
905
+ )
906
+ assert local_eval(project / "tasks.py")["run_count"] == 1
907
+
908
+
909
+ @pytest.mark.parametrize("container", ["image", "compose"])
910
+ def test_platform_container_rows_can_use_local_placement(
911
+ eval_cli: _EvalCli, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, container: str
912
+ ) -> None:
913
+ compose = tmp_path / "compose.yaml"
914
+ compose.write_text("services:\n main:\n image: example:latest\n")
915
+ config = (
916
+ RuntimeConfig(image="example:latest")
917
+ if container == "image"
918
+ else RuntimeConfig.model_validate(
919
+ {"compose": {"document": str(compose), "root": str(tmp_path)}}
920
+ )
921
+ )
922
+ task = Task(env="demo", id="solve", runtime_config=config)
923
+ monkeypatch.setattr(Taskset, "from_api", classmethod(lambda cls, name: Taskset(name, [task])))
924
+ docker = MagicMock()
925
+ monkeypatch.setattr(eval_mod, "DockerRuntime", lambda: docker)
926
+ eval_cli.invoke("Platform Tasks", "openai", "--runtime", "local", "--yes")
927
+ assert eval_cli.kwargs["runtime"](task) is docker.return_value
928
+ docker.assert_called_once_with(task)
929
+
930
+
931
+ @pytest.mark.parametrize("shared,configured", [(True, False), (False, True), (False, False)])
932
+ def test_platform_local_placement_checks_separate_verifier(
933
+ eval_cli: _EvalCli, monkeypatch: pytest.MonkeyPatch, shared: bool, configured: bool
934
+ ) -> None:
935
+ config = RuntimeConfig(image="example:latest")
936
+ verifier = Task(
937
+ env="actor" if shared else "judge",
938
+ id="verify",
939
+ runtime_config=config if configured else None,
940
+ )
941
+ task = Task(env="actor", id="solve", runtime_config=config, verifier=verifier)
942
+ monkeypatch.setattr(Taskset, "from_api", classmethod(lambda cls, name: Taskset(name, [task])))
943
+ payload = eval_cli.invoke(
944
+ "Platform Tasks",
945
+ "openai",
946
+ "--runtime",
947
+ "local",
948
+ "--yes",
949
+ exit_code=0 if shared or configured else 2,
950
+ )
951
+ if not shared and not configured:
952
+ assert "no env source to spawn locally" in payload["message"]
953
+ assert eval_cli.taskset is None
@@ -37,8 +37,9 @@ def load_environment(
37
37
  """Resolve an environment reference to the one :class:`Environment` it names.
38
38
 
39
39
  A source path (``.py`` file or directory) is scanned for ``Environment``
40
- instances; *name* selects among several, matching the module attribute or
41
- ``Environment.name``. A dotted importable module reads attribute *name*
40
+ instances and environments bound to exported tasks; *name* selects among
41
+ them, matching the module attribute or ``Environment.name``.
42
+ A dotted importable module reads attribute *name*
42
43
  (default ``env``): an ``Environment`` directly, or a factory called with
43
44
  *args* to build one — how programmatic envs (integrations, adapted
44
45
  images) are served without a source file. Raises when nothing resolves.
@@ -63,19 +64,32 @@ def load_environment(
63
64
  and (path / "__init__.py").is_file()
64
65
  )
65
66
  if path.exists() and not package_attribute:
67
+ # Avoid the environment -> eval -> runtime import cycle.
68
+ from hud.eval.taskset import Taskset
69
+
66
70
  if args:
67
71
  raise ValueError(f"args= applies to factory targets, not source path {target}")
68
- matched = [
69
- env
70
- for module in iter_modules(path)
72
+ modules = list(iter_modules(path))
73
+ matched = {
74
+ id(env): env
75
+ for module in modules
71
76
  for attr, env in vars(module).items()
72
77
  if isinstance(env, Environment) and (name is None or name in (attr, env.name))
73
- ]
78
+ }
79
+ for module in modules:
80
+ for task in Taskset._scan_tasks(module):
81
+ for row in (task, task.verifier):
82
+ if (
83
+ row is not None
84
+ and (env := row._env) is not None
85
+ and (name is None or name == env.name)
86
+ ):
87
+ matched[id(env)] = env
74
88
  if not matched:
75
89
  raise ValueError(f"no Environment{f' named {name!r}' if name else ''} found in {path}")
76
90
  if len(matched) > 1:
77
91
  raise ValueError(f"multiple Environments in {path}; select one by name")
78
- return matched[0]
92
+ return next(iter(matched.values()))
79
93
 
80
94
  if path.is_file() or "/" in str(target):
81
95
  raise FileNotFoundError(f"no environment source at {target}")