hud 0.6.12.dev0__tar.gz → 0.6.13.dev1__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 (261) hide show
  1. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/PKG-INFO +9 -4
  2. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/__init__.py +2 -5
  3. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/__init__.py +19 -5
  4. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/tools/computer.py +17 -6
  5. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/tools/tests/test_computer.py +33 -0
  6. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/misc/response_automation.py +0 -1
  7. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/agent.py +6 -4
  8. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/tools/base.py +1 -2
  9. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/tools/coding.py +60 -17
  10. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/tools/computer.py +7 -1
  11. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai_compatible/tools/filesystem.py +2 -9
  12. hud-0.6.13.dev1/hud/agents/robot/__init__.py +39 -0
  13. hud-0.6.13.dev1/hud/agents/robot/adapter.py +118 -0
  14. hud-0.6.13.dev1/hud/agents/robot/agent.py +174 -0
  15. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/robot/batching.py +23 -26
  16. hud-0.6.13.dev1/hud/agents/robot/dataset.py +243 -0
  17. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/robot/model.py +1 -1
  18. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tests/test_base.py +42 -6
  19. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tests/test_openai_agent.py +37 -0
  20. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tests/test_provider_native_tools.py +164 -41
  21. hud-0.6.13.dev1/hud/agents/tests/test_robot_dataset_writer.py +162 -0
  22. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tool_agent.py +5 -2
  23. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tools/rfb.py +17 -5
  24. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tools/ssh.py +22 -6
  25. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/types.py +5 -1
  26. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/capabilities/base.py +2 -1
  27. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/capabilities/rfb.py +15 -7
  28. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/capabilities/robot.py +44 -19
  29. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/capabilities/ssh.py +7 -32
  30. hud-0.6.13.dev1/hud/capabilities/tests/test_rfb.py +76 -0
  31. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/__init__.py +4 -10
  32. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/eval.py +2 -1
  33. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/serve.py +16 -21
  34. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/tests/test_cli_init.py +0 -11
  35. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/trace.py +11 -8
  36. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/source.py +2 -10
  37. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/tests/test_version_check.py +3 -15
  38. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/version_check.py +2 -34
  39. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/clients/client.py +26 -11
  40. hud-0.6.13.dev1/hud/environment/__init__.py +100 -0
  41. hud-0.6.13.dev1/hud/environment/egress.py +661 -0
  42. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/env.py +35 -0
  43. hud-0.6.13.dev1/hud/environment/robot/__init__.py +38 -0
  44. hud-0.6.13.dev1/hud/environment/robot/bridge.py +618 -0
  45. hud-0.6.13.dev1/hud/environment/robot/endpoint.py +350 -0
  46. hud-0.6.13.dev1/hud/environment/robot/gym.py +718 -0
  47. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/server.py +172 -88
  48. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/tests/test_capability_backing.py +173 -0
  49. hud-0.6.13.dev1/hud/environment/tests/test_loader.py +161 -0
  50. hud-0.6.13.dev1/hud/environment/tests/test_robot_regressions.py +493 -0
  51. hud-0.6.13.dev1/hud/environment/tests/test_sessions.py +89 -0
  52. hud-0.6.13.dev1/hud/environment/tests/test_workspace.py +1140 -0
  53. hud-0.6.13.dev1/hud/environment/workspace.py +1533 -0
  54. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/__init__.py +2 -0
  55. hud-0.6.13.dev1/hud/eval/docker-seccomp.json +21 -0
  56. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/job.py +37 -6
  57. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/run.py +131 -73
  58. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/runtime.py +284 -75
  59. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/sync.py +1 -1
  60. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/task.py +18 -12
  61. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/taskset.py +54 -28
  62. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/tests/test_docker_provider.py +295 -11
  63. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/tests/test_hosted.py +136 -5
  64. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/tests/test_job.py +48 -0
  65. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/tests/test_rollout.py +227 -10
  66. hud-0.6.13.dev1/hud/eval/tests/test_shared.py +178 -0
  67. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/tests/test_task.py +28 -7
  68. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/graders/bash.py +14 -14
  69. hud-0.6.13.dev1/hud/telemetry/robot/__init__.py +12 -0
  70. hud-0.6.13.dev1/hud/telemetry/robot/recorder.py +313 -0
  71. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/train/client.py +49 -6
  72. hud-0.6.13.dev1/hud/train/tests/__init__.py +1 -0
  73. hud-0.6.13.dev1/hud/train/tests/test_client.py +86 -0
  74. hud-0.6.13.dev1/hud/utils/docker.py +32 -0
  75. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/gateway.py +33 -3
  76. hud-0.6.13.dev1/hud/utils/naming.py +19 -0
  77. hud-0.6.13.dev1/hud/utils/process.py +222 -0
  78. hud-0.6.13.dev1/hud/utils/tests/__init__.py +0 -0
  79. hud-0.6.13.dev1/hud/utils/tests/test_gateway.py +160 -0
  80. hud-0.6.13.dev1/hud/utils/tests/test_process.py +85 -0
  81. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/version.py +1 -1
  82. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/pyproject.toml +13 -7
  83. hud-0.6.12.dev0/hud/agents/robot/__init__.py +0 -41
  84. hud-0.6.12.dev0/hud/agents/robot/_types.py +0 -12
  85. hud-0.6.12.dev0/hud/agents/robot/adapter.py +0 -105
  86. hud-0.6.12.dev0/hud/agents/robot/agent.py +0 -169
  87. hud-0.6.12.dev0/hud/agents/robot/record.py +0 -230
  88. hud-0.6.12.dev0/hud/environment/__init__.py +0 -58
  89. hud-0.6.12.dev0/hud/environment/robot/__init__.py +0 -29
  90. hud-0.6.12.dev0/hud/environment/robot/bridge.py +0 -176
  91. hud-0.6.12.dev0/hud/environment/robot/endpoint.py +0 -210
  92. hud-0.6.12.dev0/hud/environment/robot/sim_runner.py +0 -111
  93. hud-0.6.12.dev0/hud/environment/tests/test_loader.py +0 -31
  94. hud-0.6.12.dev0/hud/environment/tests/test_workspace.py +0 -244
  95. hud-0.6.12.dev0/hud/environment/workspace.py +0 -724
  96. hud-0.6.12.dev0/hud/utils/process.py +0 -130
  97. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/.gitignore +0 -0
  98. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/LICENSE +0 -0
  99. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/README.md +0 -0
  100. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/__main__.py +0 -0
  101. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/_legacy.py +0 -0
  102. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/base.py +0 -0
  103. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/browser_use/__init__.py +0 -0
  104. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/browser_use/agent.py +0 -0
  105. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/__init__.py +0 -0
  106. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/agent.py +0 -0
  107. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/sdk/__init__.py +0 -0
  108. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/sdk/agent.py +0 -0
  109. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/sdk/computer_mcp.py +0 -0
  110. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/tools/__init__.py +0 -0
  111. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/tools/base.py +0 -0
  112. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/tools/coding.py +0 -0
  113. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/tools/hosted.py +0 -0
  114. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/tools/mcp_proxy.py +0 -0
  115. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/tools/settings.py +0 -0
  116. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/claude/tools/tests/__init__.py +0 -0
  117. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/__init__.py +0 -0
  118. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/agent.py +0 -0
  119. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/settings.py +0 -0
  120. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/tools/__init__.py +0 -0
  121. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/tools/base.py +0 -0
  122. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/tools/coding.py +0 -0
  123. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/tools/computer.py +0 -0
  124. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/tools/filesystem.py +0 -0
  125. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/tools/hosted.py +0 -0
  126. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/tools/mcp_proxy.py +0 -0
  127. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/tools/tests/__init__.py +0 -0
  128. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/gemini/tools/tests/test_computer.py +0 -0
  129. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/misc/__init__.py +0 -0
  130. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/__init__.py +0 -0
  131. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/tools/__init__.py +0 -0
  132. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/tools/apply_patch.py +0 -0
  133. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/tools/hosted.py +0 -0
  134. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/tools/mcp_proxy.py +0 -0
  135. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/tools/strict_schema.py +0 -0
  136. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/tools/tests/__init__.py +0 -0
  137. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/tools/tests/test_computer.py +0 -0
  138. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai/tools/tests/test_strict_schema.py +0 -0
  139. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai_compatible/__init__.py +0 -0
  140. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai_compatible/agent.py +0 -0
  141. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai_compatible/tools/__init__.py +0 -0
  142. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai_compatible/tools/base.py +0 -0
  143. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/openai_compatible/tools/mcp_proxy.py +0 -0
  144. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tests/__init__.py +0 -0
  145. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tests/test_apply_patch.py +0 -0
  146. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tests/test_claude_agent.py +0 -0
  147. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tests/test_claude_sdk_agent.py +0 -0
  148. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tests/test_gemini_agent.py +0 -0
  149. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tests/test_openai_compatible_agent.py +0 -0
  150. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tests/test_tool_agent.py +0 -0
  151. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tests/test_trace.py +0 -0
  152. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tools/__init__.py +0 -0
  153. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tools/base.py +0 -0
  154. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tools/hosted.py +0 -0
  155. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/agents/tools/mcp.py +0 -0
  156. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/capabilities/__init__.py +0 -0
  157. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/capabilities/cdp.py +0 -0
  158. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/capabilities/mcp.py +0 -0
  159. {hud-0.6.12.dev0/hud/cli/utils → hud-0.6.13.dev1/hud/capabilities}/tests/__init__.py +0 -0
  160. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/__main__.py +0 -0
  161. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/cancel.py +0 -0
  162. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/client.py +0 -0
  163. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/deploy.py +0 -0
  164. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/init.py +0 -0
  165. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/jobs.py +0 -0
  166. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/login.py +0 -0
  167. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/models.py +0 -0
  168. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/presets.py +0 -0
  169. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/sync.py +0 -0
  170. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/task.py +0 -0
  171. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/templates.py +0 -0
  172. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/tests/__init__.py +0 -0
  173. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/tests/test_cli_main.py +0 -0
  174. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/tests/test_cli_more_wrappers.py +0 -0
  175. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/tests/test_deploy.py +0 -0
  176. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/tests/test_eval_bedrock.py +0 -0
  177. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/tests/test_eval_config.py +0 -0
  178. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/tests/test_init.py +0 -0
  179. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/tests/test_main_module.py +0 -0
  180. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/tests/test_sync_export.py +0 -0
  181. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/__init__.py +0 -0
  182. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/api.py +0 -0
  183. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/build_display.py +0 -0
  184. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/build_logs.py +0 -0
  185. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/config.py +0 -0
  186. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/context.py +0 -0
  187. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/display.py +0 -0
  188. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/jobs.py +0 -0
  189. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/registry.py +0 -0
  190. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/tasks.py +0 -0
  191. {hud-0.6.12.dev0/hud/environment → hud-0.6.13.dev1/hud/cli/utils}/tests/__init__.py +0 -0
  192. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/tests/test_build_display.py +0 -0
  193. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/tests/test_config.py +0 -0
  194. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/tests/test_context.py +0 -0
  195. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/tests/test_registry.py +0 -0
  196. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/tests/test_source.py +0 -0
  197. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/cli/utils/tests/test_tasks.py +0 -0
  198. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/clients/__init__.py +0 -0
  199. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/clients/tests/__init__.py +0 -0
  200. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/clients/tests/test_connect.py +0 -0
  201. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/conftest.py +0 -0
  202. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/file_tracker.py +0 -0
  203. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/legacy.py +0 -0
  204. {hud-0.6.12.dev0/hud/eval → hud-0.6.13.dev1/hud/environment}/tests/__init__.py +0 -0
  205. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/tests/conftest.py +0 -0
  206. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/tests/test_file_tracker.py +0 -0
  207. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/tests/test_file_tracking.py +0 -0
  208. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/tests/test_legacy.py +0 -0
  209. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/tests/test_manifest.py +0 -0
  210. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/tests/test_server.py +0 -0
  211. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/tests/test_tunnel.py +0 -0
  212. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/environment/utils.py +0 -0
  213. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/chat.py +0 -0
  214. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/file_tracking.py +0 -0
  215. {hud-0.6.12.dev0/hud/telemetry → hud-0.6.13.dev1/hud/eval}/tests/__init__.py +0 -0
  216. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/tests/test_chat.py +0 -0
  217. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/tests/test_file_tracking_observer.py +0 -0
  218. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/tests/test_local_runtime.py +0 -0
  219. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/eval/tests/test_sync.py +0 -0
  220. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/graders/__init__.py +0 -0
  221. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/graders/base.py +0 -0
  222. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/graders/combine.py +0 -0
  223. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/graders/judge.py +0 -0
  224. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/graders/results.py +0 -0
  225. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/graders/text.py +0 -0
  226. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/patches/__init__.py +0 -0
  227. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/patches/mcp_patches.py +0 -0
  228. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/patches/tests/__init__.py +0 -0
  229. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/patches/tests/test_warnings.py +0 -0
  230. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/patches/warnings.py +0 -0
  231. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/py.typed +0 -0
  232. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/server.py +0 -0
  233. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/settings.py +0 -0
  234. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/telemetry/__init__.py +0 -0
  235. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/telemetry/context.py +0 -0
  236. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/telemetry/exporter.py +0 -0
  237. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/telemetry/instrument.py +0 -0
  238. {hud-0.6.12.dev0/hud/agents → hud-0.6.13.dev1/hud/telemetry}/robot/video.py +0 -0
  239. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/telemetry/span.py +0 -0
  240. {hud-0.6.12.dev0/hud/utils → hud-0.6.13.dev1/hud/telemetry}/tests/__init__.py +0 -0
  241. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/telemetry/tests/test_exporter.py +0 -0
  242. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/telemetry/tests/test_instrument.py +0 -0
  243. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/train/__init__.py +0 -0
  244. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/train/base.py +0 -0
  245. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/train/types.py +0 -0
  246. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/types.py +0 -0
  247. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/__init__.py +0 -0
  248. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/exceptions.py +0 -0
  249. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/hints.py +0 -0
  250. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/hud_console.py +0 -0
  251. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/modules.py +0 -0
  252. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/platform.py +0 -0
  253. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/requests.py +0 -0
  254. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/serialization.py +0 -0
  255. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/tests/test_exceptions.py +0 -0
  256. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/tests/test_hints.py +0 -0
  257. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/tests/test_hud_console.py +0 -0
  258. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/tests/test_platform.py +0 -0
  259. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/tests/test_requests.py +0 -0
  260. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/tests/test_serialization.py +0 -0
  261. {hud-0.6.12.dev0 → hud-0.6.13.dev1}/hud/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hud
3
- Version: 0.6.12.dev0
3
+ Version: 0.6.13.dev1
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
@@ -37,16 +37,16 @@ Requires-Python: <3.13,>=3.11
37
37
  Requires-Dist: anthropic>=0.78.0
38
38
  Requires-Dist: asyncssh>=2.23.0
39
39
  Requires-Dist: asyncvnc>=1.3.0
40
- Requires-Dist: fastmcp==3.0.2
40
+ Requires-Dist: fastmcp==3.2.0
41
41
  Requires-Dist: google-genai
42
42
  Requires-Dist: httpx<1,>=0.23.0
43
43
  Requires-Dist: mcp<2.0,>=1.24.0
44
- Requires-Dist: openai==2.44.0
44
+ Requires-Dist: openai>=2.45.0
45
45
  Requires-Dist: packaging>=21.0
46
46
  Requires-Dist: pillow>=11.0.0
47
47
  Requires-Dist: prompt-toolkit==3.0.51
48
48
  Requires-Dist: pydantic-settings<3,>=2.2
49
- Requires-Dist: pydantic<3,>=2.6
49
+ Requires-Dist: pydantic<3,>=2.10
50
50
  Requires-Dist: questionary==2.1.0
51
51
  Requires-Dist: rich>=13.0.0
52
52
  Requires-Dist: typer>=0.9.0
@@ -60,7 +60,11 @@ Requires-Dist: browser-use>=0.11.13; extra == 'browseruse'
60
60
  Provides-Extra: daytona
61
61
  Requires-Dist: daytona>=0.100; extra == 'daytona'
62
62
  Provides-Extra: dev
63
+ Requires-Dist: av>=12; extra == 'dev'
63
64
  Requires-Dist: dotenv>=0.9.9; extra == 'dev'
65
+ Requires-Dist: gymnasium>=1.0; extra == 'dev'
66
+ Requires-Dist: numpy>=1.24; extra == 'dev'
67
+ Requires-Dist: openpi-client>=0.1.2; extra == 'dev'
64
68
  Requires-Dist: pyright==1.1.407; extra == 'dev'
65
69
  Requires-Dist: pytest-asyncio; extra == 'dev'
66
70
  Requires-Dist: pytest-cov; extra == 'dev'
@@ -71,6 +75,7 @@ Provides-Extra: modal
71
75
  Requires-Dist: modal>=1.0; extra == 'modal'
72
76
  Provides-Extra: robot
73
77
  Requires-Dist: av>=12; extra == 'robot'
78
+ Requires-Dist: gymnasium>=1.0; extra == 'robot'
74
79
  Requires-Dist: numpy>=1.24; extra == 'robot'
75
80
  Requires-Dist: openpi-client>=0.1.2; extra == 'robot'
76
81
  Provides-Extra: train
@@ -32,6 +32,7 @@ from .eval import (
32
32
  from .telemetry.instrument import instrument
33
33
  from .train import TrainingClient
34
34
  from .types import Trace
35
+ from .version import __version__
35
36
 
36
37
  _install_v5_compat()
37
38
 
@@ -56,11 +57,7 @@ __all__ = [
56
57
  "Taskset",
57
58
  "Trace",
58
59
  "TrainingClient",
60
+ "__version__",
59
61
  "connect",
60
62
  "instrument",
61
63
  ]
62
-
63
- try:
64
- from .version import __version__
65
- except ImportError:
66
- __version__ = "unknown"
@@ -7,9 +7,10 @@ from __future__ import annotations
7
7
 
8
8
  from typing import TYPE_CHECKING, Any, cast
9
9
 
10
+ from hud.settings import settings
10
11
  from hud.types import AgentType
12
+ from hud.utils.exceptions import HudAuthenticationError
11
13
  from hud.utils.gateway import (
12
- build_gateway_client,
13
14
  gateway_model_aliases,
14
15
  list_gateway_models,
15
16
  normalize_gateway_model_id,
@@ -30,14 +31,29 @@ if TYPE_CHECKING:
30
31
  def create_agent(model: str, **kwargs: Any) -> GatewayAgent:
31
32
  """Create an agent routed through the HUD gateway.
32
33
 
33
- For direct API access with provider API keys, instantiate the agent classes directly.
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.
38
+
39
+ For direct API access with provider API keys, instantiate the agent classes
40
+ directly.
34
41
  """
42
+ direct_credentials = [name for name in ("api_key", "base_url") if name in kwargs]
43
+ if direct_credentials:
44
+ names = ", ".join(direct_credentials)
45
+ raise ValueError(
46
+ f"create_agent routes through the HUD gateway and does not accept {names}; "
47
+ "instantiate the provider agent directly for custom/BYOK credentials"
48
+ )
49
+ if not settings.api_key:
50
+ raise HudAuthenticationError("HUD_API_KEY is required to create a gateway agent")
51
+
35
52
  requested_model = model
36
53
  model = normalize_gateway_model_id(model)
37
54
  agent_type = next((candidate for candidate in AgentType if candidate.value == model), None)
38
55
  if agent_type is not None:
39
56
  model_id = model
40
- provider_name = agent_type.gateway_provider
41
57
  else:
42
58
  try:
43
59
  gateway_models = list_gateway_models()
@@ -69,7 +85,6 @@ def create_agent(model: str, **kwargs: Any) -> GatewayAgent:
69
85
  except ValueError as exc:
70
86
  raise ValueError(f"Model '{model}' has invalid agent type metadata") from exc
71
87
  model_id = gateway_model.model_name or model
72
- provider_name = gateway_model.provider.name or "openai"
73
88
  break
74
89
  else:
75
90
  import difflib
@@ -95,7 +110,6 @@ def create_agent(model: str, **kwargs: Any) -> GatewayAgent:
95
110
  raise ValueError(f"Model {requested_model!r} not found in {source}.{hint}")
96
111
 
97
112
  kwargs.setdefault("model", model_id)
98
- kwargs.setdefault("model_client", build_gateway_client(provider_name))
99
113
  # cls/config_cls are matched unions; the pairing is correct by construction.
100
114
  config = agent_type.config_cls(**kwargs)
101
115
  return agent_type.cls(cast("Any", config))
@@ -278,13 +278,17 @@ class ClaudeComputerTool(RFBTool):
278
278
  x0, y0, x1, y1 = (int(v) for v in region_seq)
279
279
  except (TypeError, ValueError):
280
280
  return tool_err("region must contain 4 integers")
281
- png = await self.client.screenshot_png()
282
- cropped = _crop_png(png, (x0, y0, x1, y1))
281
+ screenshot, _ = await self.client.screenshot_png("image/png")
282
+ cropped, mime_type = _crop_png(
283
+ screenshot,
284
+ (x0, y0, x1, y1),
285
+ self.screenshot_mime_type,
286
+ )
283
287
  return MCPToolResult(
284
288
  content=[
285
289
  mcp_types.ImageContent(
286
290
  type="image",
287
- mimeType="image/png",
291
+ mimeType=mime_type,
288
292
  data=base64.b64encode(cropped).decode("ascii"),
289
293
  )
290
294
  ],
@@ -349,14 +353,21 @@ def _drag_path(arguments: dict[str, Any]) -> list[tuple[int, int]]:
349
353
  return path
350
354
 
351
355
 
352
- def _crop_png(png: bytes, region: tuple[int, int, int, int]) -> bytes:
356
+ def _crop_png(
357
+ png: bytes,
358
+ region: tuple[int, int, int, int],
359
+ mime_type: str,
360
+ ) -> tuple[bytes, str]:
353
361
  from PIL import Image
354
362
 
355
363
  image = Image.open(BytesIO(png))
356
364
  cropped = image.crop(region)
357
365
  buf = BytesIO()
358
- cropped.save(buf, format="PNG")
359
- return buf.getvalue()
366
+ if mime_type == "image/webp":
367
+ cropped.save(buf, format="WEBP", quality=85)
368
+ else:
369
+ cropped.save(buf, format="PNG")
370
+ return buf.getvalue(), mime_type
360
371
 
361
372
 
362
373
  __all__ = ["CLAUDE_COMPUTER_SPECS", "ClaudeComputerTool"]
@@ -5,12 +5,18 @@ computer-use action dispatch (translation to RFB primitives), without a live VNC
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ from io import BytesIO
8
9
  from types import SimpleNamespace
9
10
  from typing import Any
11
+ from unittest.mock import AsyncMock, patch
12
+
13
+ import mcp.types as mcp_types
14
+ from PIL import Image
10
15
 
11
16
  from hud.agents.claude.tools.computer import (
12
17
  CLAUDE_COMPUTER_SPECS,
13
18
  ClaudeComputerTool,
19
+ _crop_png,
14
20
  _hold_keys,
15
21
  _split_keys,
16
22
  _translate_key,
@@ -25,6 +31,7 @@ class RecordingComputer(ClaudeComputerTool):
25
31
 
26
32
  def __init__(self) -> None:
27
33
  self.calls: list[tuple[Any, ...]] = []
34
+ self.screenshot_mime_type = "image/webp"
28
35
  self.client = SimpleNamespace(width=200, height=100)
29
36
 
30
37
  async def screenshot(self) -> Any:
@@ -147,3 +154,29 @@ async def test_unsupported_action_errors() -> None:
147
154
  result = await tool.execute({"action": "frobnicate"})
148
155
  assert result.isError
149
156
  assert "unsupported" in result_text(result).lower()
157
+
158
+
159
+ def test_crop_reports_encoded_mime_type() -> None:
160
+ source = BytesIO()
161
+ Image.effect_noise((128, 128), 100).convert("RGB").save(source, format="PNG")
162
+
163
+ cropped, mime_type = _crop_png(source.getvalue(), (0, 0, 128, 128), "image/webp")
164
+
165
+ assert mime_type == "image/webp"
166
+ assert cropped.startswith(b"RIFF")
167
+
168
+
169
+ async def test_zoom_reports_encoded_mime_type() -> None:
170
+ tool = RecordingComputer()
171
+ tool.client.screenshot_png = AsyncMock(return_value=(b"png", "image/png"))
172
+
173
+ with patch(
174
+ "hud.agents.claude.tools.computer._crop_png",
175
+ return_value=(b"webp", "image/webp"),
176
+ ) as crop:
177
+ result = await tool._zoom({"region": [0, 0, 10, 10]})
178
+
179
+ tool.client.screenshot_png.assert_awaited_once_with("image/png")
180
+ crop.assert_called_once_with(b"png", (0, 0, 10, 10), "image/webp")
181
+ image = next(block for block in result.content if isinstance(block, mcp_types.ImageContent))
182
+ assert image.mimeType == "image/webp"
@@ -85,7 +85,6 @@ async def _determine_response(
85
85
  ],
86
86
  reasoning={"effort": "low"},
87
87
  max_output_tokens=256,
88
- extra_headers={"Trace-Id": ""},
89
88
  )
90
89
 
91
90
  text_parts: list[str] = []
@@ -33,7 +33,7 @@ from hud.utils import gateway
33
33
  from .tools import OpenAIComputerTool, OpenAIMCPProxyTool, OpenAIShellTool
34
34
  from .tools.base import format_openai_result
35
35
  from .tools.coding import shell_output
36
- from .tools.computer import last_image_data
36
+ from .tools.computer import last_image_content
37
37
 
38
38
  if TYPE_CHECKING:
39
39
  import mcp.types as mcp_types
@@ -104,8 +104,8 @@ class OpenAIAgent(ToolAgent[ResponseInputItemParam, OpenAIConfig]):
104
104
  tool = state.tools.get(call.name)
105
105
 
106
106
  if isinstance(tool, OpenAIComputerTool):
107
- screenshot = last_image_data(result)
108
- if not screenshot:
107
+ screenshot = last_image_content(result)
108
+ if screenshot is None:
109
109
  logger.warning("Computer tool result missing screenshot for call %s", call.name)
110
110
  return None
111
111
  output = ComputerCallOutput(
@@ -115,7 +115,9 @@ class OpenAIAgent(ToolAgent[ResponseInputItemParam, OpenAIConfig]):
115
115
  "Any",
116
116
  {
117
117
  "type": "computer_screenshot",
118
- "image_url": f"data:image/png;base64,{screenshot}",
118
+ "image_url": (
119
+ f"data:{screenshot.mimeType or 'image/png'};base64,{screenshot.data}"
120
+ ),
119
121
  "detail": "original",
120
122
  },
121
123
  ),
@@ -12,7 +12,6 @@ from openai.types.responses import (
12
12
  ResponseInputFileContentParam,
13
13
  ResponseInputImageContentParam,
14
14
  ResponseInputTextContentParam,
15
- ResponseInputTextParam,
16
15
  )
17
16
  from openai.types.responses.response_input_param import FunctionCallOutput, ResponseInputItemParam
18
17
 
@@ -76,7 +75,7 @@ def format_openai_result(call: MCPToolCall, result: MCPToolResult) -> ResponseIn
76
75
  logger.warning("Unknown content block type: %s", type(block))
77
76
 
78
77
  if not output_items:
79
- output_items.append(ResponseInputTextParam(type="input_text", text=""))
78
+ output_items.append(ResponseInputTextContentParam(type="input_text", text=""))
80
79
 
81
80
  return cast(
82
81
  "ResponseInputItemParam",
@@ -7,7 +7,6 @@ from typing import Any, cast
7
7
  import mcp.types as mcp_types
8
8
 
9
9
  from hud.agents.tools import SSHTool
10
- from hud.agents.tools.base import result_text
11
10
  from hud.types import MCPToolResult
12
11
 
13
12
  from .base import OpenAIToolSpec
@@ -17,6 +16,9 @@ OPENAI_SHELL_SPEC = OpenAIToolSpec(
17
16
  api_name="shell",
18
17
  )
19
18
 
19
+ MAX_SHELL_OUTPUT_LENGTH = 10 * 1024 * 1024
20
+ TRUNCATION_MARKER = "[truncated]"
21
+
20
22
 
21
23
  class OpenAIShellTool(SSHTool):
22
24
  name = "shell"
@@ -32,14 +34,31 @@ class OpenAIShellTool(SSHTool):
32
34
  return {"type": "shell", "environment": {"type": "local"}}
33
35
 
34
36
  async def execute(self, arguments: dict[str, Any]) -> MCPToolResult:
37
+ requested_limit = arguments.get("max_output_length")
38
+ if requested_limit is None:
39
+ max_output_length = MAX_SHELL_OUTPUT_LENGTH
40
+ elif (
41
+ isinstance(requested_limit, bool)
42
+ or not isinstance(requested_limit, int)
43
+ or requested_limit <= 0
44
+ ):
45
+ text = "max_output_length must be a positive integer"
46
+ return _shell_result(
47
+ [text],
48
+ is_error=True,
49
+ structured={"output": [shell_output("", text, 1)]},
50
+ )
51
+ else:
52
+ max_output_length = min(requested_limit, MAX_SHELL_OUTPUT_LENGTH)
53
+
35
54
  def invalid_commands_result() -> MCPToolResult:
36
55
  text = "commands must be a list of strings"
37
56
  return _shell_result(
38
- text,
57
+ [text],
39
58
  is_error=True,
40
59
  structured={
41
60
  "output": [shell_output("", text, 1)],
42
- "max_output_length": arguments.get("max_output_length"),
61
+ "max_output_length": max_output_length,
43
62
  },
44
63
  )
45
64
 
@@ -54,7 +73,7 @@ class OpenAIShellTool(SSHTool):
54
73
  command_list = cast("list[str]", raw_commands)
55
74
 
56
75
  outputs: list[dict[str, Any]] = []
57
- text_parts: list[str] = []
76
+ display_outputs: list[str] = []
58
77
  is_error = False
59
78
  env_arguments: dict[str, Any] = {}
60
79
  timeout_ms = arguments.get("timeout_ms")
@@ -66,40 +85,64 @@ class OpenAIShellTool(SSHTool):
66
85
  full_cmd = f"timeout {int(env_arguments['timeout_seconds'])} {command}"
67
86
  else:
68
87
  full_cmd = command
69
- result = await self.bash(full_cmd)
70
- text = result_text(result)
71
- if result.isError:
72
- outputs.append(shell_output("", text, 1))
73
- is_error = True
74
- else:
75
- outputs.append(shell_output(text, "", 0))
76
- if text:
77
- text_parts.append(text)
88
+ completed = await self.client.conn.run(full_cmd, check=False)
89
+ stdout = completed.stdout if isinstance(completed.stdout, str) else ""
90
+ stderr = completed.stderr if isinstance(completed.stderr, str) else ""
91
+ exit_code = completed.exit_status if completed.exit_status is not None else 1
92
+ stdout, stderr = _bound_output(stdout, stderr, max_output_length)
93
+ outputs.append(shell_output(stdout, stderr, exit_code))
94
+ display_outputs.append(stdout + stderr)
95
+ is_error = is_error or bool(exit_code)
78
96
 
79
97
  return _shell_result(
80
- "\n".join(text_parts),
98
+ display_outputs,
81
99
  is_error=is_error,
82
100
  structured={
83
101
  "output": outputs,
84
- "max_output_length": arguments.get("max_output_length"),
102
+ "max_output_length": max_output_length,
85
103
  },
86
104
  )
87
105
 
88
106
 
89
107
  def _shell_result(
90
- text: str,
108
+ texts: list[str],
91
109
  *,
92
110
  is_error: bool = False,
93
111
  structured: dict[str, Any] | None = None,
94
112
  ) -> MCPToolResult:
95
113
  payload = {"provider_tool": "shell", **(structured or {})}
96
114
  return MCPToolResult(
97
- content=[mcp_types.TextContent(type="text", text=text)] if text else [],
115
+ content=[mcp_types.TextContent(type="text", text=text) for text in texts if text],
98
116
  isError=is_error,
99
117
  structuredContent=payload,
100
118
  )
101
119
 
102
120
 
121
+ def _bound_output(stdout: str, stderr: str, limit: int) -> tuple[str, str]:
122
+ total_length = len(stdout) + len(stderr)
123
+ if total_length <= limit:
124
+ return stdout, stderr
125
+
126
+ marker = TRUNCATION_MARKER[:limit]
127
+ available = limit - len(marker)
128
+ prefix_length = (available + 1) // 2
129
+ suffix_length = available // 2
130
+
131
+ stdout_prefix = stdout[:prefix_length]
132
+ stderr_prefix = stderr[: max(0, prefix_length - len(stdout))]
133
+ stderr_suffix = stderr[-suffix_length:] if suffix_length else ""
134
+ stdout_suffix_length = max(0, suffix_length - len(stderr))
135
+ stdout_suffix = stdout[-stdout_suffix_length:] if stdout_suffix_length else ""
136
+
137
+ if len(stdout_prefix) + len(stdout_suffix) < len(stdout):
138
+ stdout = stdout_prefix + marker + stdout_suffix
139
+ stderr = stderr_prefix + stderr_suffix
140
+ else:
141
+ stdout = stdout_prefix + stdout_suffix
142
+ stderr = stderr_prefix + marker + stderr_suffix
143
+ return stdout, stderr
144
+
145
+
103
146
  def shell_output(stdout: str, stderr: str, exit_code: int) -> dict[str, Any]:
104
147
  return {
105
148
  "stdout": stdout,
@@ -23,9 +23,15 @@ OPENAI_COMPUTER_SPEC = OpenAIToolSpec(
23
23
 
24
24
  def last_image_data(result: MCPToolResult) -> str | None:
25
25
  """Base64 data of the most recent screenshot block in a tool result."""
26
+ image = last_image_content(result)
27
+ return image.data if image is not None else None
28
+
29
+
30
+ def last_image_content(result: MCPToolResult) -> mcp_types.ImageContent | None:
31
+ """Most recent screenshot block in a tool result."""
26
32
  for block in reversed(result.content):
27
33
  if isinstance(block, mcp_types.ImageContent):
28
- return block.data
34
+ return block
29
35
  return None
30
36
 
31
37
 
@@ -66,9 +66,6 @@ class ReadTool(_FilesystemTool):
66
66
  path = arguments.get("filePath")
67
67
  if not isinstance(path, str) or not path:
68
68
  raise ValueError("filePath is required")
69
- # Map once so the directory predicate and the file read agree on
70
- # the same workspace-anchored path.
71
- path = self.client.map_path(path)
72
69
  offset = _read_offset(arguments.get("offset"))
73
70
  limit = _positive_int(arguments.get("limit"), default=DEFAULT_READ_LIMIT, name="limit")
74
71
  if not (await self.bash(f"test -d {shlex.quote(path)}")).isError:
@@ -196,9 +193,6 @@ class EditTool(_FilesystemTool):
196
193
  path = arguments.get("filePath")
197
194
  if not isinstance(path, str) or not path:
198
195
  raise ValueError("filePath is required")
199
- # Map once so existence checks, mkdir, and the write all target
200
- # the same workspace-anchored path.
201
- path = self.client.map_path(path)
202
196
  old = arguments.get("oldString")
203
197
  new = arguments.get("newString")
204
198
  if not isinstance(old, str):
@@ -258,7 +252,6 @@ class WriteTool(_FilesystemTool):
258
252
  path = arguments.get("filePath")
259
253
  if not isinstance(path, str) or not path:
260
254
  raise ValueError("filePath is required")
261
- path = self.client.map_path(path)
262
255
  content = arguments.get("content")
263
256
  if not isinstance(content, str):
264
257
  raise ValueError("content is required")
@@ -294,7 +287,7 @@ class GrepTool(_FilesystemTool):
294
287
  pattern = arguments.get("pattern")
295
288
  if not isinstance(pattern, str):
296
289
  raise ValueError("pattern is required")
297
- path = self.client.map_path(str(arguments.get("path") or "."))
290
+ path = str(arguments.get("path") or ".")
298
291
  cmd = f"grep -rn {shlex.quote(pattern)} {shlex.quote(path)}"
299
292
  include = arguments.get("include")
300
293
  if isinstance(include, str) and include:
@@ -318,7 +311,7 @@ class GlobTool(_FilesystemTool):
318
311
  pattern = arguments.get("pattern")
319
312
  if not isinstance(pattern, str):
320
313
  raise ValueError("pattern is required")
321
- path = self.client.map_path(str(arguments.get("path") or "."))
314
+ path = str(arguments.get("path") or ".")
322
315
  return await self.bash(f"find {shlex.quote(path)} -name {shlex.quote(pattern)}")
323
316
 
324
317
 
@@ -0,0 +1,39 @@
1
+ """Agent-side robot harness: drive a ``robot`` env with a VLA policy.
2
+
3
+ - :class:`~.agent.RobotAgent` — the harness: connects to the ``robot``
4
+ capability (claiming a slot token from the setup step when present), reads the contract,
5
+ drives one scalar connection with an open-loop chunk queue. Subclass and
6
+ set ``self.model`` + ``self.adapter``.
7
+ - :class:`~.model.Model` / :class:`~.model.LeRobotModel` /
8
+ :class:`~.model.RemoteModel` — the policy and its inference mechanics.
9
+ - :class:`~.adapter.Adapter` / :class:`~.adapter.LeRobotAdapter` /
10
+ :class:`~.adapter.OpenPIAdapter` — env <-> policy space translation.
11
+ - :class:`~.batching.BatchedAgent` — many concurrent single-env rollouts
12
+ sharing one batched model.
13
+ - :class:`~.dataset.DatasetWriter` — opt-in LeRobot v3 dataset recording
14
+ (``agent.save = True``).
15
+
16
+ This subpackage needs the ``robot`` extra (``pip install 'hud[robot]'``).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from .adapter import Adapter, LeRobotAdapter, OpenPIAdapter
22
+ from .agent import ROBOT_PROTOCOL, RobotAgent
23
+ from .batching import BatchedAgent, BatchedModel
24
+ from .dataset import DatasetWriter
25
+ from .model import LeRobotModel, Model, RemoteModel
26
+
27
+ __all__ = [
28
+ "ROBOT_PROTOCOL",
29
+ "Adapter",
30
+ "BatchedAgent",
31
+ "BatchedModel",
32
+ "DatasetWriter",
33
+ "LeRobotAdapter",
34
+ "LeRobotModel",
35
+ "Model",
36
+ "OpenPIAdapter",
37
+ "RemoteModel",
38
+ "RobotAgent",
39
+ ]
@@ -0,0 +1,118 @@
1
+ """Translate observations and actions between env and policy spaces.
2
+
3
+ The loop calls ``bind``, ``reset``, ``adapt_observation``, and the action hooks.
4
+ Use :class:`LeRobotAdapter` for LeRobot models; subclass for custom wiring;
5
+ ``adapter=None`` for pass-through.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ import numpy as np
13
+ from numpy.typing import NDArray
14
+
15
+ #: A policy-emitted action / chunk array (the robot stack's shared alias).
16
+ ActionArray = NDArray[np.floating[Any]]
17
+
18
+ #: Image types the bundled adapters treat as cameras (vs the state vector).
19
+ IMAGE_TYPES = ("rgb", "bgr", "gray", "depth")
20
+
21
+
22
+ class Adapter:
23
+ """Translate between an env's observation/action spaces and a policy's.
24
+
25
+ Driven by :class:`~hud.agents.robot.agent.RobotAgent`: :meth:`bind` once
26
+ after connect, :meth:`reset` once per episode, then :meth:`adapt_observation`
27
+ per inference and the action hooks per chunk/step. Construct with the
28
+ policy's image-slot names; everything env-side is learned in :meth:`bind`.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ *,
34
+ model_image_keys: list[str] | None = None,
35
+ chunk_size: int | None = None,
36
+ ) -> None:
37
+ #: The policy's ordered image-slot names (model side; known at load time).
38
+ self.model_image_keys: list[str] = list(model_image_keys or [])
39
+ #: Open-loop horizon: how many predicted actions to execute before re-querying.
40
+ #: ``None`` = full model chunk. Applied by :class:`~.agent.RobotAgent`, not here —
41
+ #: so subclass ``adapt_chunk`` overrides stay free of truncation duty.
42
+ self.chunk_size: int | None = chunk_size
43
+ #: The env's action feature and observation layout (set in :meth:`bind`).
44
+ self.action_space: dict[str, Any] = {}
45
+ self.image_keys: list[str] = []
46
+ self.state_key: str | None = None
47
+
48
+ def bind(self, action_space: dict[str, Any], observation_space: dict[str, Any]) -> None:
49
+ """Learn the env's layout from the contract (``client.spaces()``): image
50
+ features become the camera keys (in contract order), the first non-image
51
+ feature is the state. Override to derive extra env-side parameters."""
52
+ self.action_space = action_space or {}
53
+ self.image_keys = [n for n, f in observation_space.items() if f.get("type") in IMAGE_TYPES]
54
+ self.state_key = next(
55
+ (n for n, f in observation_space.items() if f.get("type") not in IMAGE_TYPES), None
56
+ )
57
+
58
+ def reset(self) -> None:
59
+ """Override only if the adapter is stateful across steps within an episode."""
60
+
61
+ def adapt_observation(self, obs: dict[str, Any], prompt: str) -> Any:
62
+ """Translate an env observation + task prompt into the policy's input."""
63
+ raise NotImplementedError
64
+
65
+ def adapt_chunk(self, chunk: ActionArray, obs: dict[str, Any]) -> ActionArray:
66
+ """Translate a freshly-inferred ``[T, A]`` chunk to env space, given the
67
+ (per-slot) query-time observation it was inferred from (default identity).
68
+
69
+ Called once per slot at inference time, so a chunk expressed relative to
70
+ the query state — e.g. joint *deltas* to be added to the query-time
71
+ joints — converts in one shot.
72
+ """
73
+ return chunk
74
+
75
+ def adapt_action(self, action: ActionArray, obs: dict[str, Any]) -> ActionArray:
76
+ """Per-step execution-time hook on the popped action (default identity)."""
77
+ return action
78
+
79
+
80
+ class LeRobotAdapter(Adapter):
81
+ """Vanilla LeRobot adapter for a standard image/state env, single or batched.
82
+
83
+ Maps env cameras onto the model's image slots in order and converts HWC
84
+ ``uint8`` to CHW ``float`` in ``[0, 1]``; state and prompt pass through.
85
+ A batched observation (state ``[N, S]``) maps in one go — cameras become
86
+ ``[N, C, H, W]`` and the shared task is repeated to ``N``. Actions are
87
+ identity (postprocess already returns env-space actions).
88
+ """
89
+
90
+ def adapt_observation(self, obs: dict[str, Any], prompt: str) -> dict[str, Any]:
91
+ import torch # pyright: ignore[reportMissingImports]
92
+
93
+ torch_mod: Any = torch # torch ships no stubs; keep strict mode quiet
94
+ data = obs["data"]
95
+ state = np.asarray(data[self.state_key], dtype=np.float32)
96
+ batched = state.ndim > 1 # [N, S] vs [S]
97
+ batch: dict[str, Any] = {
98
+ "observation.state": torch_mod.from_numpy(state),
99
+ "task": [prompt] * len(state) if batched else prompt,
100
+ }
101
+ for model_key, env_key in zip(self.model_image_keys, self.image_keys, strict=False):
102
+ img = torch_mod.from_numpy(np.asarray(data[env_key]))
103
+ perm = (0, 3, 1, 2) if batched else (2, 0, 1)
104
+ batch[model_key] = img.permute(*perm).float() / 255.0
105
+ return batch
106
+
107
+
108
+ class OpenPIAdapter(Adapter):
109
+ """Unwraps ``obs['data']`` to OpenPI wire keys and attaches the prompt;
110
+ actions are pass-through."""
111
+
112
+ def adapt_observation(self, obs: dict[str, Any], prompt: str) -> dict[str, Any]:
113
+ out = dict(obs["data"])
114
+ out.setdefault("prompt", prompt)
115
+ return out
116
+
117
+
118
+ __all__ = ["ActionArray", "Adapter", "LeRobotAdapter", "OpenPIAdapter"]