hud-python 0.6.4__tar.gz → 0.6.5__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 (245) hide show
  1. {hud_python-0.6.4 → hud_python-0.6.5}/PKG-INFO +2 -1
  2. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/robot/__init__.py +9 -3
  3. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/robot/adapter.py +10 -0
  4. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/robot/agent.py +26 -14
  5. hud_python-0.6.5/hud/agents/robot/batching.py +130 -0
  6. hud_python-0.6.5/hud/agents/robot/model.py +127 -0
  7. hud_python-0.6.5/hud/agents/robot/record.py +230 -0
  8. hud_python-0.6.5/hud/agents/robot/video.py +267 -0
  9. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/types.py +31 -18
  10. {hud_python-0.6.4 → hud_python-0.6.5}/hud/capabilities/robot.py +4 -0
  11. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/init.py +65 -26
  12. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/presets.py +67 -12
  13. hud_python-0.6.5/hud/cli/tests/test_init.py +113 -0
  14. {hud_python-0.6.4 → hud_python-0.6.5}/hud/clients/client.py +1 -1
  15. {hud_python-0.6.4 → hud_python-0.6.5}/hud/types.py +5 -13
  16. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/hud_console.py +24 -6
  17. hud_python-0.6.5/hud/utils/tests/test_hud_console.py +165 -0
  18. {hud_python-0.6.4 → hud_python-0.6.5}/hud/version.py +1 -1
  19. {hud_python-0.6.4 → hud_python-0.6.5}/pyproject.toml +2 -1
  20. hud_python-0.6.4/hud/agents/robot/model.py +0 -138
  21. hud_python-0.6.4/hud/cli/tests/test_init.py +0 -59
  22. hud_python-0.6.4/hud/utils/tests/test_hud_console.py +0 -62
  23. {hud_python-0.6.4 → hud_python-0.6.5}/.gitignore +0 -0
  24. {hud_python-0.6.4 → hud_python-0.6.5}/LICENSE +0 -0
  25. {hud_python-0.6.4 → hud_python-0.6.5}/README.md +0 -0
  26. {hud_python-0.6.4 → hud_python-0.6.5}/cookbooks/a2a-chat/README.md +0 -0
  27. {hud_python-0.6.4 → hud_python-0.6.5}/cookbooks/a2a-chat/pyproject.toml +0 -0
  28. {hud_python-0.6.4 → hud_python-0.6.5}/cookbooks/codex-coding/README.md +0 -0
  29. {hud_python-0.6.4 → hud_python-0.6.5}/cookbooks/codex-coding/pyproject.toml +0 -0
  30. {hud_python-0.6.4 → hud_python-0.6.5}/cookbooks/connect4-selfplay/README.md +0 -0
  31. {hud_python-0.6.4 → hud_python-0.6.5}/cookbooks/fireworks-rl-training/README.md +0 -0
  32. {hud_python-0.6.4 → hud_python-0.6.5}/cookbooks/fireworks-rl-training/pyproject.toml +0 -0
  33. {hud_python-0.6.4 → hud_python-0.6.5}/cookbooks/rl-training/README.md +0 -0
  34. {hud_python-0.6.4 → hud_python-0.6.5}/cookbooks/rl-training/pyproject.toml +0 -0
  35. {hud_python-0.6.4 → hud_python-0.6.5}/hud/__init__.py +0 -0
  36. {hud_python-0.6.4 → hud_python-0.6.5}/hud/__main__.py +0 -0
  37. {hud_python-0.6.4 → hud_python-0.6.5}/hud/_legacy.py +0 -0
  38. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/__init__.py +0 -0
  39. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/base.py +0 -0
  40. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/browser_use/__init__.py +0 -0
  41. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/browser_use/agent.py +0 -0
  42. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/__init__.py +0 -0
  43. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/agent.py +0 -0
  44. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/sdk/__init__.py +0 -0
  45. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/sdk/agent.py +0 -0
  46. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/sdk/computer_mcp.py +0 -0
  47. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/tools/__init__.py +0 -0
  48. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/tools/base.py +0 -0
  49. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/tools/coding.py +0 -0
  50. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/tools/computer.py +0 -0
  51. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/tools/hosted.py +0 -0
  52. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/tools/mcp_proxy.py +0 -0
  53. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/tools/settings.py +0 -0
  54. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/tools/tests/__init__.py +0 -0
  55. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/claude/tools/tests/test_computer.py +0 -0
  56. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/__init__.py +0 -0
  57. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/agent.py +0 -0
  58. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/settings.py +0 -0
  59. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/tools/__init__.py +0 -0
  60. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/tools/base.py +0 -0
  61. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/tools/coding.py +0 -0
  62. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/tools/computer.py +0 -0
  63. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/tools/filesystem.py +0 -0
  64. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/tools/hosted.py +0 -0
  65. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/tools/mcp_proxy.py +0 -0
  66. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/tools/tests/__init__.py +0 -0
  67. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/gemini/tools/tests/test_computer.py +0 -0
  68. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/misc/__init__.py +0 -0
  69. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/misc/response_automation.py +0 -0
  70. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/__init__.py +0 -0
  71. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/agent.py +0 -0
  72. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/tools/__init__.py +0 -0
  73. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/tools/apply_patch.py +0 -0
  74. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/tools/base.py +0 -0
  75. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/tools/coding.py +0 -0
  76. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/tools/computer.py +0 -0
  77. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/tools/hosted.py +0 -0
  78. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/tools/mcp_proxy.py +0 -0
  79. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/tools/strict_schema.py +0 -0
  80. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/tools/tests/__init__.py +0 -0
  81. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/tools/tests/test_computer.py +0 -0
  82. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai/tools/tests/test_strict_schema.py +0 -0
  83. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai_compatible/__init__.py +0 -0
  84. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai_compatible/agent.py +0 -0
  85. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai_compatible/tools/__init__.py +0 -0
  86. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai_compatible/tools/base.py +0 -0
  87. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai_compatible/tools/filesystem.py +0 -0
  88. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/openai_compatible/tools/mcp_proxy.py +0 -0
  89. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/robot/_types.py +0 -0
  90. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tests/__init__.py +0 -0
  91. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tests/test_apply_patch.py +0 -0
  92. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tests/test_base.py +0 -0
  93. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tests/test_claude_agent.py +0 -0
  94. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tests/test_claude_sdk_agent.py +0 -0
  95. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tests/test_gemini_agent.py +0 -0
  96. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tests/test_openai_agent.py +0 -0
  97. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tests/test_openai_compatible_agent.py +0 -0
  98. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tests/test_provider_native_tools.py +0 -0
  99. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tests/test_tool_agent.py +0 -0
  100. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tests/test_trace.py +0 -0
  101. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tool_agent.py +0 -0
  102. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tools/__init__.py +0 -0
  103. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tools/base.py +0 -0
  104. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tools/hosted.py +0 -0
  105. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tools/mcp.py +0 -0
  106. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tools/rfb.py +0 -0
  107. {hud_python-0.6.4 → hud_python-0.6.5}/hud/agents/tools/ssh.py +0 -0
  108. {hud_python-0.6.4 → hud_python-0.6.5}/hud/capabilities/__init__.py +0 -0
  109. {hud_python-0.6.4 → hud_python-0.6.5}/hud/capabilities/base.py +0 -0
  110. {hud_python-0.6.4 → hud_python-0.6.5}/hud/capabilities/cdp.py +0 -0
  111. {hud_python-0.6.4 → hud_python-0.6.5}/hud/capabilities/filetracking.py +0 -0
  112. {hud_python-0.6.4 → hud_python-0.6.5}/hud/capabilities/mcp.py +0 -0
  113. {hud_python-0.6.4 → hud_python-0.6.5}/hud/capabilities/rfb.py +0 -0
  114. {hud_python-0.6.4 → hud_python-0.6.5}/hud/capabilities/ssh.py +0 -0
  115. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/__init__.py +0 -0
  116. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/__main__.py +0 -0
  117. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/cancel.py +0 -0
  118. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/client.py +0 -0
  119. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/deploy.py +0 -0
  120. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/eval.py +0 -0
  121. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/jobs.py +0 -0
  122. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/login.py +0 -0
  123. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/models.py +0 -0
  124. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/serve.py +0 -0
  125. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/sync.py +0 -0
  126. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/task.py +0 -0
  127. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/templates.py +0 -0
  128. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/tests/__init__.py +0 -0
  129. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/tests/test_cli_init.py +0 -0
  130. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/tests/test_cli_main.py +0 -0
  131. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/tests/test_cli_more_wrappers.py +0 -0
  132. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/tests/test_deploy.py +0 -0
  133. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/tests/test_eval_bedrock.py +0 -0
  134. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/tests/test_eval_config.py +0 -0
  135. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/tests/test_main_module.py +0 -0
  136. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/tests/test_sync_export.py +0 -0
  137. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/trace.py +0 -0
  138. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/__init__.py +0 -0
  139. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/api.py +0 -0
  140. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/build_display.py +0 -0
  141. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/build_logs.py +0 -0
  142. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/config.py +0 -0
  143. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/context.py +0 -0
  144. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/display.py +0 -0
  145. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/jobs.py +0 -0
  146. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/registry.py +0 -0
  147. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/source.py +0 -0
  148. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/tasks.py +0 -0
  149. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/tests/__init__.py +0 -0
  150. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/tests/test_build_display.py +0 -0
  151. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/tests/test_config.py +0 -0
  152. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/tests/test_context.py +0 -0
  153. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/tests/test_registry.py +0 -0
  154. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/tests/test_source.py +0 -0
  155. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/tests/test_tasks.py +0 -0
  156. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/tests/test_version_check.py +0 -0
  157. {hud_python-0.6.4 → hud_python-0.6.5}/hud/cli/utils/version_check.py +0 -0
  158. {hud_python-0.6.4 → hud_python-0.6.5}/hud/clients/__init__.py +0 -0
  159. {hud_python-0.6.4 → hud_python-0.6.5}/hud/clients/tests/__init__.py +0 -0
  160. {hud_python-0.6.4 → hud_python-0.6.5}/hud/clients/tests/test_connect.py +0 -0
  161. {hud_python-0.6.4 → hud_python-0.6.5}/hud/conftest.py +0 -0
  162. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/__init__.py +0 -0
  163. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/env.py +0 -0
  164. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/file_tracker.py +0 -0
  165. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/file_tracking.py +0 -0
  166. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/legacy.py +0 -0
  167. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/robot/__init__.py +0 -0
  168. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/robot/bridge.py +0 -0
  169. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/robot/endpoint.py +0 -0
  170. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/robot/sim_runner.py +0 -0
  171. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/server.py +0 -0
  172. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/tests/__init__.py +0 -0
  173. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/tests/conftest.py +0 -0
  174. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/tests/test_capability_backing.py +0 -0
  175. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/tests/test_file_tracker.py +0 -0
  176. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/tests/test_file_tracking.py +0 -0
  177. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/tests/test_legacy.py +0 -0
  178. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/tests/test_loader.py +0 -0
  179. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/tests/test_manifest.py +0 -0
  180. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/tests/test_server.py +0 -0
  181. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/tests/test_tunnel.py +0 -0
  182. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/utils.py +0 -0
  183. {hud_python-0.6.4 → hud_python-0.6.5}/hud/environment/workspace.py +0 -0
  184. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/__init__.py +0 -0
  185. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/chat.py +0 -0
  186. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/file_tracking.py +0 -0
  187. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/job.py +0 -0
  188. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/run.py +0 -0
  189. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/runtime.py +0 -0
  190. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/sync.py +0 -0
  191. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/task.py +0 -0
  192. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/taskset.py +0 -0
  193. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/tests/__init__.py +0 -0
  194. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/tests/test_chat.py +0 -0
  195. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/tests/test_docker_provider.py +0 -0
  196. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/tests/test_file_tracking_observer.py +0 -0
  197. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/tests/test_hosted.py +0 -0
  198. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/tests/test_job.py +0 -0
  199. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/tests/test_rollout.py +0 -0
  200. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/tests/test_sync.py +0 -0
  201. {hud_python-0.6.4 → hud_python-0.6.5}/hud/eval/tests/test_task.py +0 -0
  202. {hud_python-0.6.4 → hud_python-0.6.5}/hud/graders/__init__.py +0 -0
  203. {hud_python-0.6.4 → hud_python-0.6.5}/hud/graders/base.py +0 -0
  204. {hud_python-0.6.4 → hud_python-0.6.5}/hud/graders/bash.py +0 -0
  205. {hud_python-0.6.4 → hud_python-0.6.5}/hud/graders/combine.py +0 -0
  206. {hud_python-0.6.4 → hud_python-0.6.5}/hud/graders/judge.py +0 -0
  207. {hud_python-0.6.4 → hud_python-0.6.5}/hud/graders/results.py +0 -0
  208. {hud_python-0.6.4 → hud_python-0.6.5}/hud/graders/text.py +0 -0
  209. {hud_python-0.6.4 → hud_python-0.6.5}/hud/patches/__init__.py +0 -0
  210. {hud_python-0.6.4 → hud_python-0.6.5}/hud/patches/mcp_patches.py +0 -0
  211. {hud_python-0.6.4 → hud_python-0.6.5}/hud/patches/tests/__init__.py +0 -0
  212. {hud_python-0.6.4 → hud_python-0.6.5}/hud/patches/tests/test_warnings.py +0 -0
  213. {hud_python-0.6.4 → hud_python-0.6.5}/hud/patches/warnings.py +0 -0
  214. {hud_python-0.6.4 → hud_python-0.6.5}/hud/py.typed +0 -0
  215. {hud_python-0.6.4 → hud_python-0.6.5}/hud/server.py +0 -0
  216. {hud_python-0.6.4 → hud_python-0.6.5}/hud/settings.py +0 -0
  217. {hud_python-0.6.4 → hud_python-0.6.5}/hud/telemetry/__init__.py +0 -0
  218. {hud_python-0.6.4 → hud_python-0.6.5}/hud/telemetry/context.py +0 -0
  219. {hud_python-0.6.4 → hud_python-0.6.5}/hud/telemetry/exporter.py +0 -0
  220. {hud_python-0.6.4 → hud_python-0.6.5}/hud/telemetry/filetracking.py +0 -0
  221. {hud_python-0.6.4 → hud_python-0.6.5}/hud/telemetry/instrument.py +0 -0
  222. {hud_python-0.6.4 → hud_python-0.6.5}/hud/telemetry/span.py +0 -0
  223. {hud_python-0.6.4 → hud_python-0.6.5}/hud/telemetry/tests/__init__.py +0 -0
  224. {hud_python-0.6.4 → hud_python-0.6.5}/hud/telemetry/tests/test_exporter.py +0 -0
  225. {hud_python-0.6.4 → hud_python-0.6.5}/hud/telemetry/tests/test_filetracking.py +0 -0
  226. {hud_python-0.6.4 → hud_python-0.6.5}/hud/telemetry/tests/test_instrument.py +0 -0
  227. {hud_python-0.6.4 → hud_python-0.6.5}/hud/train/__init__.py +0 -0
  228. {hud_python-0.6.4 → hud_python-0.6.5}/hud/train/base.py +0 -0
  229. {hud_python-0.6.4 → hud_python-0.6.5}/hud/train/client.py +0 -0
  230. {hud_python-0.6.4 → hud_python-0.6.5}/hud/train/types.py +0 -0
  231. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/__init__.py +0 -0
  232. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/exceptions.py +0 -0
  233. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/gateway.py +0 -0
  234. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/hints.py +0 -0
  235. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/modules.py +0 -0
  236. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/platform.py +0 -0
  237. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/requests.py +0 -0
  238. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/serialization.py +0 -0
  239. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/tests/__init__.py +0 -0
  240. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/tests/test_exceptions.py +0 -0
  241. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/tests/test_hints.py +0 -0
  242. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/tests/test_platform.py +0 -0
  243. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/tests/test_requests.py +0 -0
  244. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/tests/test_serialization.py +0 -0
  245. {hud_python-0.6.4 → hud_python-0.6.5}/hud/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hud-python
3
- Version: 0.6.4
3
+ Version: 0.6.5
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
@@ -70,6 +70,7 @@ Requires-Dist: ruff<0.15.0,>=0.11.8; extra == 'dev'
70
70
  Provides-Extra: modal
71
71
  Requires-Dist: modal>=1.0; extra == 'modal'
72
72
  Provides-Extra: robot
73
+ Requires-Dist: av>=12; extra == 'robot'
73
74
  Requires-Dist: numpy>=1.24; extra == 'robot'
74
75
  Requires-Dist: openpi-client>=0.1.2; extra == 'robot'
75
76
  Provides-Extra: train
@@ -10,6 +10,9 @@ The harness splits a policy rollout into three seams, each replaceable on its ow
10
10
  - :class:`~hud.agents.robot.adapter.Adapter` — translate between the env's
11
11
  observation/action spaces (from the contract) and the policy's.
12
12
 
13
+ Wrap an agent in :class:`~hud.agents.robot.batching.BatchedAgent` to run many rollouts
14
+ concurrently off one batched GPU forward (``max_concurrent`` rollouts, shared model).
15
+
13
16
  Per-tick platform tracing is emitted by the loop itself: each step records an
14
17
  :class:`~hud.agents.types.ObservationStep`, and each re-inference an
15
18
  :class:`~hud.agents.types.InferenceStep`, so runs stream live into the HUD trace viewer.
@@ -20,16 +23,19 @@ This subpackage needs the ``robot`` extra (``pip install 'hud-python[robot]'``)
20
23
 
21
24
  from __future__ import annotations
22
25
 
23
- from .adapter import Adapter, LeRobotAdapter
26
+ from .adapter import Adapter, LeRobotAdapter, OpenPIAdapter
24
27
  from .agent import ROBOT_PROTOCOL, RobotAgent
25
- from .model import LeRobotModel, Model, lerobot_infer
28
+ from .batching import BatchedAgent, BatchedModel
29
+ from .model import LeRobotModel, Model
26
30
 
27
31
  __all__ = [
28
32
  "ROBOT_PROTOCOL",
29
33
  "Adapter",
34
+ "BatchedAgent",
35
+ "BatchedModel",
30
36
  "LeRobotAdapter",
31
37
  "LeRobotModel",
32
38
  "Model",
39
+ "OpenPIAdapter",
33
40
  "RobotAgent",
34
- "lerobot_infer",
35
41
  ]
@@ -89,7 +89,17 @@ class LeRobotAdapter(Adapter):
89
89
  return action
90
90
 
91
91
 
92
+ class OpenPIAdapter(Adapter):
93
+ """unwraps obs['data'] to OpenPI wire keys, attaches prompt; actions are passthrough"""
94
+
95
+ def adapt_observation(self, obs: dict[str, Any], prompt: str) -> dict[str, Any]:
96
+ out = dict(obs["data"])
97
+ out.setdefault("prompt", prompt)
98
+ return out
99
+
100
+
92
101
  __all__ = [
93
102
  "Adapter",
94
103
  "LeRobotAdapter",
104
+ "OpenPIAdapter",
95
105
  ]
@@ -5,8 +5,8 @@ Subclass :class:`RobotAgent`, set ``self.model`` and ``self.adapter`` in
5
5
 
6
6
  The base calls the adapter and model at the right moments::
7
7
 
8
- setup_robot -> adapter.bind(spaces) # once after connect
9
- on_episode_start -> model.reset(); adapter.reset() # once per episode
8
+ setup_robot -> adapter.bind(spaces) # once after connect
9
+ on_episode_start -> adapter.reset() # per episode; model is stateless
10
10
  select_action -> adapt_observation -> model.ainfer -> pop chunk -> adapt_action
11
11
 
12
12
  ``model.ainfer`` always returns a ``[T, A]`` chunk; :meth:`RobotAgent.select_action`
@@ -24,9 +24,10 @@ from typing import TYPE_CHECKING, Any, ClassVar
24
24
  import numpy as np
25
25
 
26
26
  from hud.agents.base import Agent
27
- from hud.agents.types import InferenceStep, ObservationStep
28
27
  from hud.capabilities.robot import RobotClient
29
28
 
29
+ from .record import Recorder
30
+
30
31
  if TYPE_CHECKING:
31
32
  from hud.eval.run import Run
32
33
 
@@ -57,6 +58,9 @@ class RobotAgent(Agent):
57
58
  robot_protocol: ClassVar[str] = ROBOT_PROTOCOL
58
59
  #: How often (in steps) to print a step-progress line. 0 = off.
59
60
  log_every: ClassVar[int] = 20
61
+ #: Opt-in: also save a LeRobot v3 dataset of every (obs, action) pair to disk
62
+ #: (the ``--save`` flag). Telemetry streams regardless; see :mod:`.record`.
63
+ save: bool = False
60
64
 
61
65
  #: Runs the policy (preprocess → forward → postprocess). Subclasses set this.
62
66
  model: Model | None = None
@@ -70,9 +74,11 @@ class RobotAgent(Agent):
70
74
  _env_obs_space: dict[str, Any]
71
75
  #: Unexecuted tail of the current policy chunk; popped one action per step.
72
76
  _active_chunk: deque[ActionArray]
73
- #: The live run + control-tick index, so ``select_action`` can record its own InferenceStep.
74
- _run: Run
77
+ #: Control-tick index, incremented per executed action.
75
78
  _tick: int
79
+ #: Records all telemetry (observation/inference steps + video) and, when ``save``, a
80
+ #: LeRobot dataset. Agent-lifetime (the dataset spans every episode); created lazily.
81
+ _recorder: Recorder | None = None
76
82
 
77
83
  def setup_robot(self, client: RobotClient) -> None:
78
84
  """Discover the env's action/observation layout and bind the adapter to it."""
@@ -81,16 +87,19 @@ class RobotAgent(Agent):
81
87
  self.adapter.bind(self._env_action_space, self._env_obs_space)
82
88
 
83
89
  def on_episode_start(self, run: Run, client: RobotClient, *, prompt: str) -> None:
84
- """Store the prompt and reset the model and adapter before the act loop.
90
+ """Store the prompt and reset per-episode state before the act loop.
85
91
 
86
- Override (calling ``super()`` first) only for extra per-episode setup.
92
+ The model is stateless (per-episode state lives here, not on the shared model), so
93
+ only the adapter is reset. Override (calling ``super()`` first) for extra setup.
87
94
  """
88
95
  self._prompt = prompt
89
96
  self._active_chunk = deque()
90
- self._run = run
91
97
  self._tick = 0
92
- if self.model is not None:
93
- self.model.reset()
98
+ # One recorder for the agent's life so its LeRobot dataset spans every episode;
99
+ # begin() opens this episode (fresh video stream, prompt) and takes the run it records onto.
100
+ if self._recorder is None:
101
+ self._recorder = Recorder(client, save=self.save)
102
+ self._recorder.begin(run, prompt)
94
103
  if self.adapter is not None:
95
104
  self.adapter.reset()
96
105
 
@@ -110,9 +119,8 @@ class RobotAgent(Agent):
110
119
  )
111
120
  chunk = np.atleast_2d(await self.model.ainfer(batch)) # [T, A]
112
121
  self._active_chunk = deque(chunk)
113
- self._run.record(
114
- InferenceStep(tick=self._tick, chunk=chunk.tolist(), chunk_length=len(chunk))
115
- )
122
+ assert self._recorder is not None # set in on_episode_start
123
+ self._recorder.record_inference(chunk, tick=self._tick)
116
124
  self._tick += 1
117
125
  raw = self._active_chunk.popleft()
118
126
  return raw if self.adapter is None else self.adapter.adapt_action(raw, obs)
@@ -131,15 +139,17 @@ class RobotAgent(Agent):
131
139
  self.on_episode_start(run, client, prompt=prompt)
132
140
  print(f"[agent] episode started: {prompt!r} (max_steps={step_limit})", flush=True)
133
141
 
142
+ assert self._recorder is not None # set in on_episode_start above
134
143
  for step in range(step_limit):
135
144
  obs = await client.get_observation()
136
- run.record(ObservationStep.from_obs(obs, tick=step, obs_space=self._env_obs_space))
145
+ self._recorder.record_observation(obs, tick=step)
137
146
 
138
147
  if self.should_stop(obs, step=step, max_steps=step_limit):
139
148
  print(f"[agent] env reported terminated at step {step}", flush=True)
140
149
  break
141
150
 
142
151
  action = await self.select_action(obs)
152
+ self._recorder.record_action(action)
143
153
  await client.send_action(action)
144
154
 
145
155
  if self.log_every and step % self.log_every == 0:
@@ -151,6 +161,8 @@ class RobotAgent(Agent):
151
161
  run.trace.status = "completed"
152
162
  run.trace.content = "done"
153
163
  finally:
164
+ if self._recorder is not None:
165
+ self._recorder.end() # flush video tails + commit the LeRobot episode
154
166
  await client.close()
155
167
 
156
168
 
@@ -0,0 +1,130 @@
1
+ """Batched inference for concurrent robot rollouts.
2
+
3
+ - BatchedModel: stacks concurrent ainfer calls into one infer
4
+ - BatchedAgent: gives each rollout its own state, shares one batched model
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import copy
11
+ import importlib
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from hud.agents.base import Agent
15
+
16
+ from .model import Model
17
+
18
+ if TYPE_CHECKING:
19
+ from hud.eval.run import Run
20
+
21
+ from ._types import ActionArray
22
+ from .agent import RobotAgent
23
+
24
+
25
+ class BatchedModel(Model):
26
+ """Coalesce concurrent ``ainfer`` calls into one stacked ``inner.infer``.
27
+
28
+ A lazily-started worker drains up to ``batch_size`` queued calls (or waits up to
29
+ ``max_wait_s`` for stragglers — which avoids stalling when fewer rollouts are live,
30
+ e.g. the tail of a suite), stacks them into one ``[N, ...]`` batch, runs a single
31
+ forward, and scatters the ``[N, T, A]`` rows back to each caller.
32
+
33
+ ``inner`` must be an in-process, stateless model whose :meth:`~Model.infer` runs the
34
+ whole ``[N, ...]`` batch in one forward (e.g. :class:`~hud.agents.robot.model.LeRobotModel`).
35
+ :class:`~hud.agents.robot.model.RemoteModel` is **not** supported: it does one WebSocket
36
+ request per env and the OpenPI server protocol has no batched-request shape, so a stacked
37
+ batch would be mis-sent as a single env. Run one agent per rollout against it instead.
38
+ """
39
+
40
+ def __init__(self, inner: Model, *, batch_size: int, max_wait_s: float = 0.05) -> None:
41
+ self.inner = inner
42
+ self.batch_size = int(batch_size)
43
+ self.max_wait_s = float(max_wait_s)
44
+ # Bound to the running loop on first ainfer (the harness owns the loop).
45
+ self._queue: asyncio.Queue[tuple[Any, asyncio.Future[ActionArray]]] | None = None
46
+ self._worker: asyncio.Task[None] | None = None
47
+
48
+ def infer(self, batch: Any) -> ActionArray:
49
+ return self.inner.infer(batch)
50
+
51
+ async def ainfer(self, batch: Any) -> ActionArray:
52
+ loop = asyncio.get_running_loop()
53
+ if self._worker is None:
54
+ self._queue = asyncio.Queue()
55
+ self._worker = loop.create_task(self._batch_loop())
56
+ assert self._queue is not None
57
+ fut: asyncio.Future[ActionArray] = loop.create_future()
58
+ await self._queue.put((batch, fut))
59
+ return await fut
60
+
61
+ async def _batch_loop(self) -> None:
62
+ assert self._queue is not None
63
+ loop = asyncio.get_running_loop()
64
+ while True:
65
+ items = [await self._queue.get()] # block for the first caller
66
+ deadline = loop.time() + self.max_wait_s
67
+ while len(items) < self.batch_size:
68
+ timeout = deadline - loop.time()
69
+ if timeout <= 0:
70
+ break
71
+ try:
72
+ items.append(await asyncio.wait_for(self._queue.get(), timeout))
73
+ except TimeoutError:
74
+ break
75
+ samples = [b for b, _ in items]
76
+ try:
77
+ torch: Any = importlib.import_module("torch")
78
+
79
+ # Collate N raw observations into one [N, ...] batch: stack tensor
80
+ # fields on a new leading dim, gather scalars/strings into a list.
81
+ stacked: dict[str, Any] = {
82
+ k: torch.stack([s[k] for s in samples])
83
+ if torch.is_tensor(samples[0][k])
84
+ else [s[k] for s in samples]
85
+ for k in samples[0]
86
+ }
87
+ arr = await asyncio.to_thread(self.inner.infer, stacked) # [N, T, A]
88
+ for (_, fut), chunk in zip(items, arr, strict=True):
89
+ if not fut.done():
90
+ fut.set_result(chunk)
91
+ except Exception as exc: # isolate: a bad batch fails only its own callers
92
+ for _, fut in items:
93
+ if not fut.done():
94
+ fut.set_exception(exc)
95
+
96
+
97
+ class BatchedAgent(Agent):
98
+ """Drive many rollouts concurrently against one shared, batched model.
99
+
100
+ Per run: a shallow clone of ``agent`` (its own episode state) sharing a per-run
101
+ adapter copy and the single :class:`BatchedModel`, so concurrent ``ainfer`` calls
102
+ coalesce into one forward. Relies on the agent keeping per-run state out of
103
+ ``__init__`` (assigned in ``on_episode_start``) so the clones stay isolated, and on
104
+ the model being stateless (no per-episode ``reset``) since it is shared across clones.
105
+
106
+ Requires an in-process batchable model; :class:`~hud.agents.robot.model.RemoteModel`
107
+ is not supported (the OpenPI server protocol has no batched-request shape).
108
+
109
+ Takes ownership of ``agent``: it swaps ``agent.model`` for a :class:`BatchedModel` wrapper
110
+ in place (so the wrapper is shared by every per-run clone). The passed-in instance is
111
+ therefore permanently batched — hand :class:`BatchedAgent` a dedicated agent and don't
112
+ also use that same instance for direct, unbatched :class:`RobotAgent` rollouts.
113
+ """
114
+
115
+ def __init__(self, agent: RobotAgent, *, batch_size: int, max_wait_s: float = 0.05) -> None:
116
+ if agent.model is None:
117
+ raise RuntimeError("BatchedAgent needs agent.model set")
118
+ self._template = agent
119
+ # Wrap once, in place: the passed-in agent is now permanently batched (see class doc).
120
+ # Every per-run clone shares this batcher by reference.
121
+ agent.model = BatchedModel(agent.model, batch_size=batch_size, max_wait_s=max_wait_s)
122
+
123
+ async def __call__(self, run: Run, **kwargs: Any) -> None:
124
+ worker = copy.copy(self._template) # fresh __dict__; shares the batched model
125
+ if worker.adapter is not None: # defensive: a stateful custom adapter must be per-run
126
+ worker.adapter = copy.copy(worker.adapter)
127
+ await worker(run, **kwargs)
128
+
129
+
130
+ __all__ = ["BatchedAgent", "BatchedModel"]
@@ -0,0 +1,127 @@
1
+ """The ``Model``: wraps a policy and owns its inference mechanics.
2
+
3
+ A ``Model`` knows *how to run* a policy (preprocess → forward → postprocess); the
4
+ harness only awaits ``model.ainfer(batch)``. Use :class:`LeRobotModel` for stock
5
+ LeRobot checkpoints; subclass :class:`Model` and implement ``infer`` otherwise.
6
+
7
+ :meth:`Model.infer` is batch-shaped (one batch dict in, an ``[N, T, A]`` chunk out) and
8
+ stateless across calls, so one model can be shared and batched across concurrent rollouts
9
+ (see :mod:`hud.agents.robot.batching`); per-episode state belongs on the agent.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import importlib
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ import numpy as np
19
+
20
+ if TYPE_CHECKING:
21
+ from ._types import ActionArray
22
+
23
+
24
+ class Model:
25
+ """Owns a policy and its inference mechanics.
26
+
27
+ Stateless by contract: the agent owns all per-episode state (the open-loop chunk), so a
28
+ single model can be shared and batched across concurrent rollouts. There is deliberately
29
+ no ``reset`` hook — anything that resets per episode belongs on the agent, not here.
30
+ Driven by :class:`~hud.agents.robot.agent.RobotAgent`, which awaits :meth:`ainfer`.
31
+ """
32
+
33
+ def infer(self, batch: Any) -> ActionArray:
34
+ """Run the policy on an ``[N, ...]`` batch, return an ``[N, T, A]`` chunk.
35
+
36
+ Implementations MUST keep the leading batch dim ``N`` (even for ``N == 1``):
37
+ :meth:`ainfer` indexes ``[0]`` and :class:`~hud.agents.robot.batching.BatchedModel`
38
+ scatters rows along it, so a squeezed ``[T, A]`` silently breaks both.
39
+ """
40
+ raise NotImplementedError
41
+
42
+ async def ainfer(self, batch: Any) -> ActionArray:
43
+ """Awaited single-rollout entry: run :meth:`infer` in a thread, return its single
44
+ ``[T, A]`` row. Indexing ``[0]`` assumes :meth:`infer` honors the ``[N, T, A]`` contract.
45
+ """
46
+ return (await asyncio.to_thread(self.infer, batch))[0]
47
+
48
+
49
+ class LeRobotModel(Model):
50
+ """LeRobot policy with pre/post-processors: ``preprocess`` → ``predict_action_chunk`` →
51
+ ``postprocess``. ``preprocess`` adds the batch dim for an unbatched sample and is a no-op
52
+ for an already-stacked one, so :meth:`infer` handles both single and batched inputs.
53
+
54
+ Stateless: ``predict_action_chunk`` is a pure forward and the agent owns the open-loop
55
+ chunk, so LeRobot's internal action queue is never consumed here — hence no ``reset``.
56
+ """
57
+
58
+ def __init__(self, policy: Any, preprocess: Any, postprocess: Any) -> None:
59
+ self.policy = policy
60
+ self.preprocess = preprocess
61
+ self.postprocess = postprocess
62
+ #: Flipped to False after the first forward; used to print the one-time
63
+ #: CUDA/flow-matching warmup message.
64
+ self._first_inference = True
65
+
66
+ def infer(self, batch: Any) -> ActionArray:
67
+ """run batch dict (N dim) → [N, T, A] chunk"""
68
+ torch: Any = importlib.import_module("torch")
69
+ if self._first_inference:
70
+ print(
71
+ "[agent] first inference — flow-matching/CUDA warmup; this may take a while",
72
+ flush=True,
73
+ )
74
+ with torch.no_grad():
75
+ chunk = self.postprocess(self.policy.predict_action_chunk(self.preprocess(batch)))
76
+ if self._first_inference:
77
+ print("[agent] first inference done — inference is now fast", flush=True)
78
+ self._first_inference = False
79
+ arr = chunk.float().cpu().numpy()
80
+ assert arr.ndim == 3, (
81
+ f"expected [N, T, A] chunk, got {arr.shape}"
82
+ ) # LeRobot keeps the N dim
83
+ return arr
84
+
85
+
86
+ class RemoteModel(Model):
87
+ """Weightless client to an OpenPI-WebSocket policy server: ships the adapter's request
88
+ dict, returns the server's chunk. All pre/post-processing lives in the adapter + server.
89
+
90
+ Not batchable: each :meth:`infer` is one WebSocket request for one env and always adds a
91
+ single leading batch dim, and the OpenPI server protocol currently has no batched-request
92
+ shape. Do not wrap in :class:`~hud.agents.robot.batching.BatchedModel` — use one
93
+ :class:`~hud.agents.robot.agent.RobotAgent` per concurrent rollout instead.
94
+ """
95
+
96
+ def __init__(
97
+ self, host: str = "localhost", port: int = 8000, *, response_key: str = "actions"
98
+ ) -> None:
99
+ self.host = host
100
+ self.port = port
101
+ #: Server chunk key — "actions" (stock OpenPI) or "action" (Cosmos).
102
+ self.response_key = response_key
103
+ self._client: Any = None
104
+
105
+ def connect(self) -> None:
106
+ """Open the websocket (idempotent); blocks until the server is up."""
107
+ if self._client is None:
108
+ mod: Any = importlib.import_module("openpi_client.websocket_client_policy")
109
+
110
+ print(
111
+ f"[agent] connecting to openpi server ws://{self.host}:{self.port} — on hold...",
112
+ flush=True,
113
+ )
114
+ self._client = mod.WebsocketClientPolicy(self.host, self.port)
115
+
116
+ def infer(self, batch: Any) -> ActionArray:
117
+ """Ship one request dict → the server's ``[T, A]`` chunk, returned as ``[1, T, A]``."""
118
+ self.connect() # lazy connect on first call (blocks until the server is up)
119
+ chunk = np.asarray(self._client.infer(batch)[self.response_key], dtype=np.float32)
120
+ return chunk[None] # add the leading N=1 batch dim
121
+
122
+
123
+ __all__ = [
124
+ "LeRobotModel",
125
+ "Model",
126
+ "RemoteModel",
127
+ ]
@@ -0,0 +1,230 @@
1
+ """Per-episode recording for robot rollouts — telemetry, plus an optional LeRobot dataset.
2
+
3
+ The agent loop hands every tick to one :class:`Recorder`. It always streams the telemetry
4
+ the HUD viewer needs (an :class:`~hud.agents.types.ObservationStep` of numeric state +
5
+ per-camera H.264 video); when ``save`` is on it *also* appends each
6
+ ``(observation, executed action)`` pair to a LeRobot v3 dataset for offline
7
+ training/finetuning.
8
+
9
+ Saving is opt-in (the agent's ``save`` flag — the ``--save`` runner flag), so the heavy
10
+ LeRobot/PyAV imports stay deferred until a dataset is actually built. One dataset spans the
11
+ whole run (every episode the shared agent drives appends to it) and is finalized at process
12
+ exit, optionally pushed to the HF Hub. Destination + push come from the environment:
13
+
14
+ - ``RECORD_DIR`` — dataset root (default ``./data`` from where the rollout launched)
15
+ - ``HF_REPO`` — HF namespace to also push to (needs ``HF_TOKEN``)
16
+ - ``HF_PRIVATE`` — push the dataset private
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import atexit
22
+ import importlib.util
23
+ import logging
24
+ import os
25
+ import time
26
+ import uuid
27
+ from pathlib import Path
28
+ from typing import TYPE_CHECKING, Any
29
+
30
+ import numpy as np
31
+
32
+ from hud.agents.types import InferenceStep, ObservationStep
33
+ from hud.telemetry.context import get_current_trace_id
34
+
35
+ from .video import VideoStreamer
36
+
37
+ if TYPE_CHECKING:
38
+ from numpy.typing import NDArray
39
+
40
+ from hud.capabilities.robot import RobotClient
41
+ from hud.eval.run import Run
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ def _lerobot_features(contract: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], dict[str, str]]:
47
+ """Map a robot contract to LeRobot ``features`` + a wire-key -> LeRobot-key map.
48
+
49
+ Image obs -> ``observation.images.<leaf>`` (video); the lone vector obs ->
50
+ ``observation.state`` (else ``observation.<leaf>``); the action -> ``action``. String
51
+ obs are dropped (LeRobot carries the prompt as its per-frame ``task``).
52
+ """
53
+ feats = contract.get("features", {})
54
+ vectors = [
55
+ n
56
+ for n, f in feats.items()
57
+ if f.get("role") == "observation" and f.get("dtype") not in ("image", "string")
58
+ ]
59
+ single_state = len(vectors) == 1
60
+
61
+ features: dict[str, dict[str, Any]] = {}
62
+ key_map: dict[str, str] = {}
63
+ for name, f in feats.items():
64
+ role, dtype, shape = f.get("role"), f.get("dtype"), tuple(f.get("shape") or ())
65
+ leaf = name.split("/")[-1] # contract keys are slash-paths; LeRobot wants the leaf
66
+ if role == "observation" and dtype != "string":
67
+ if dtype == "image":
68
+ key, dtype = f"observation.images.{leaf}", "video"
69
+ elif leaf == "state" or single_state:
70
+ key = "observation.state"
71
+ else:
72
+ key = f"observation.{leaf}"
73
+ features[key] = {"dtype": dtype, "shape": shape, "names": _feature_names(f, leaf)}
74
+ key_map[name] = key
75
+ elif role == "action":
76
+ features["action"] = {"dtype": dtype, "shape": shape, "names": _feature_names(f, "act")}
77
+ return features, key_map
78
+
79
+
80
+ def _feature_names(feature: dict[str, Any], base: str) -> list[str]:
81
+ """Contract per-element labels, else positional defaults sized to the (rank-1) shape."""
82
+ if names := feature.get("names"):
83
+ return list(names)
84
+ if feature.get("dtype") == "image":
85
+ return ["height", "width", "channel"]
86
+ return [f"{base}_{i}" for i in range(int((feature.get("shape") or [1])[0]))]
87
+
88
+
89
+ class Recorder:
90
+ """Records one agent's rollouts: always telemetry, optionally a LeRobot dataset.
91
+
92
+ The agent owns a single instance for its lifetime and routes *all* recording through
93
+ it: :meth:`begin`/:meth:`end` bracket each episode, :meth:`record_observation` /
94
+ :meth:`record_inference` / :meth:`record_action` feed each tick (the first two write
95
+ telemetry steps onto the run passed to :meth:`begin`; the last completes a LeRobot
96
+ frame), and :meth:`save` (also an ``atexit`` hook) finalizes the cross-episode dataset.
97
+ With ``save=False`` only the telemetry path runs and the LeRobot deps are never imported.
98
+ """
99
+
100
+ def __init__(self, client: RobotClient, *, save: bool = False) -> None:
101
+ self._obs_space = client.spaces()[1]
102
+ self._fps = client.get_control_rate()
103
+ self._contract = client.contract
104
+ # Telemetry is always on; saving also needs lerobot installed.
105
+ if save and importlib.util.find_spec("lerobot") is None:
106
+ logger.warning(
107
+ "save=True but lerobot is not installed; streaming telemetry only "
108
+ "(pip install 'lerobot[dataset]')"
109
+ )
110
+ save = False
111
+ self._save = save
112
+ self._features: dict[str, dict[str, Any]] = {}
113
+ self._key_map: dict[str, str] = {}
114
+ if save:
115
+ self._features, self._key_map = _lerobot_features(self._contract)
116
+
117
+ self._video: VideoStreamer | None = None # per-episode
118
+ self._run: Run | None = None
119
+ self._task = ""
120
+ self._pending: dict[str, Any] | None = None # last obs awaiting its action
121
+ # LeRobot dataset spans every episode; created lazily on the first frame.
122
+ self._ds: Any | None = None
123
+ self._root: Path | None = None
124
+ self._repo_id = ""
125
+ if save:
126
+ atexit.register(self.save) # finalize even on an abrupt exit (parquet footer)
127
+
128
+ # ── episode lifecycle (called from the agent harness) ─────────────────────
129
+ def begin(self, run: Run, prompt: str) -> None:
130
+ """Open an episode: fresh per-camera video stream + the task prompt."""
131
+ self._run = run
132
+ self._task = prompt
133
+ self._pending = None
134
+ self._video = VideoStreamer(fps=self._fps, trace_id=get_current_trace_id())
135
+
136
+ def record_observation(self, obs: dict[str, Any], *, tick: int) -> None:
137
+ """One observation: numeric-state span + per-camera video (always streamed)."""
138
+ assert self._run is not None and self._video is not None # set in begin()
139
+ self._run.record(ObservationStep.from_obs(obs, tick=tick, obs_space=self._obs_space))
140
+ self._video.record(obs)
141
+ self._pending = obs.get("data") # paired with the action in record_action()
142
+
143
+ def record_inference(self, chunk: NDArray[Any], *, tick: int) -> None:
144
+ """One re-inference: the freshly inferred ``[T, A]`` action chunk, onto the run."""
145
+ assert self._run is not None # set in begin()
146
+ self._run.record(InferenceStep(tick=tick, chunk=chunk.tolist(), chunk_length=len(chunk)))
147
+
148
+ def record_action(self, action: NDArray[Any]) -> None:
149
+ """The executed (env-space) action: completes the pending LeRobot frame."""
150
+ if self._save and self._pending is not None:
151
+ self._add_frame(self._pending, action)
152
+ self._pending = None
153
+
154
+ def end(self) -> None:
155
+ """Close the episode: flush video tails; commit the LeRobot episode (if any frames)."""
156
+ if self._video is not None:
157
+ self._video.finalize()
158
+ if self._ds is not None and self._ds.has_pending_frames():
159
+ self._ds.save_episode()
160
+
161
+ def save(self) -> None:
162
+ """Finalize the dataset (writes the parquet footer) + optionally push to the Hub.
163
+
164
+ Idempotent; registered with ``atexit`` so the dataset stays loadable even if the
165
+ process exits without an explicit call.
166
+ """
167
+ if not self._save or self._ds is None:
168
+ return
169
+ self._save = False # idempotent across the explicit call + the atexit hook
170
+ self._ds.finalize()
171
+ print(f"[agent] saved LeRobot dataset -> {self._root}", flush=True)
172
+ if not os.environ.get("HF_REPO"):
173
+ return
174
+ private = os.environ.get("HF_PRIVATE", "0") not in ("0", "", "false", "False")
175
+ try: # best-effort: the on-disk dataset is the source of truth
176
+ self._ds.push_to_hub(private=private)
177
+ print(f"[agent] pushed -> https://huggingface.co/datasets/{self._repo_id}", flush=True)
178
+ except Exception as exc:
179
+ logger.exception("HF push failed for %s", self._repo_id)
180
+ print(f"[agent] WARNING: HF push failed: {exc!r} (dataset still on disk)", flush=True)
181
+
182
+ # ── LeRobot writing ───────────────────────────────────────────────────────
183
+ def _add_frame(self, data: dict[str, Any], action: NDArray[Any]) -> None:
184
+ ds = self._ensure_dataset()
185
+ row: dict[str, Any] = {}
186
+ for wire, key in self._key_map.items():
187
+ value = data.get(wire)
188
+ if value is None:
189
+ logger.warning("obs missing contract feature %r; skipping frame", wire)
190
+ return
191
+ ft = self._features[key]
192
+ row[key] = (
193
+ np.ascontiguousarray(value, dtype=np.uint8) # bridge images are uint8 HWC
194
+ if ft["dtype"] in ("video", "image")
195
+ else np.asarray(value, dtype=ft["dtype"]).reshape(ft["shape"])
196
+ )
197
+ act_ft = self._features["action"]
198
+ row["action"] = np.asarray(action, dtype=act_ft["dtype"]).reshape(act_ft["shape"])
199
+ row["task"] = self._task
200
+ ds.add_frame(row)
201
+
202
+ def _ensure_dataset(self) -> Any:
203
+ if self._ds is not None:
204
+ return self._ds
205
+ lerobot_dataset: Any = importlib.import_module("lerobot.datasets.lerobot_dataset")
206
+
207
+ name = self._contract.get("robot_type") or "robot"
208
+ stamp = time.strftime("%Y%m%d_%H%M%S")
209
+ # Unique per recorder so concurrent (batched) rollouts never share a root;
210
+ # tie it to the trace id when there is one so a shard maps back to its trace.
211
+ tag = (get_current_trace_id() or uuid.uuid4().hex)[:8]
212
+ # Default under ./data (relative to where the rollout was launched), created if absent.
213
+ record_dir = Path(os.environ.get("RECORD_DIR", "data"))
214
+ record_dir.mkdir(parents=True, exist_ok=True)
215
+ self._root = record_dir / f"{name}_{stamp}_{tag}"
216
+ self._repo_id = f"{os.environ.get('HF_REPO') or 'hud'}/{name}_{stamp}_{tag}"
217
+ # LeRobotDataset.create requires a fresh root; images encode to per-episode video.
218
+ self._ds = lerobot_dataset.LeRobotDataset.create(
219
+ repo_id=self._repo_id,
220
+ fps=self._fps,
221
+ features=self._features,
222
+ root=self._root,
223
+ robot_type=self._contract.get("robot_type"),
224
+ use_videos=True,
225
+ )
226
+ print(f"[agent] recording LeRobot dataset -> {self._root}", flush=True)
227
+ return self._ds
228
+
229
+
230
+ __all__ = ["Recorder"]