hud 0.6.16.dev3__tar.gz → 0.6.16.dev4__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 (311) hide show
  1. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/PKG-INFO +1 -1
  2. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/agent.py +38 -6
  3. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/test_claude_agent.py +50 -8
  4. hud-0.6.16.dev4/hud/cli/qa.py +419 -0
  5. hud-0.6.16.dev4/hud/cli/qa_analysis.py +207 -0
  6. hud-0.6.16.dev4/hud/cli/tests/test_qa.py +474 -0
  7. hud-0.6.16.dev4/hud/cli/tests/test_qa_analysis.py +87 -0
  8. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/version.py +1 -1
  9. hud-0.6.16.dev3/hud/cli/qa.py +0 -218
  10. hud-0.6.16.dev3/hud/cli/tests/test_qa.py +0 -190
  11. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/.gitignore +0 -0
  12. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/LICENSE +0 -0
  13. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/README.md +0 -0
  14. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/__init__.py +0 -0
  15. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/__main__.py +0 -0
  16. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/__init__.py +0 -0
  17. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/base.py +0 -0
  18. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/browser_use/__init__.py +0 -0
  19. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/browser_use/agent.py +0 -0
  20. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/__init__.py +0 -0
  21. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/sdk/__init__.py +0 -0
  22. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/sdk/agent.py +0 -0
  23. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/sdk/computer_mcp.py +0 -0
  24. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/tools/__init__.py +0 -0
  25. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/tools/base.py +0 -0
  26. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/tools/coding.py +0 -0
  27. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/tools/computer.py +0 -0
  28. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/tools/hosted.py +0 -0
  29. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/tools/mcp_proxy.py +0 -0
  30. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/tools/settings.py +0 -0
  31. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/tools/tests/__init__.py +0 -0
  32. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/claude/tools/tests/test_computer.py +0 -0
  33. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/__init__.py +0 -0
  34. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/agent.py +0 -0
  35. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/settings.py +0 -0
  36. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/tools/__init__.py +0 -0
  37. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/tools/base.py +0 -0
  38. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/tools/coding.py +0 -0
  39. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/tools/computer.py +0 -0
  40. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/tools/filesystem.py +0 -0
  41. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/tools/hosted.py +0 -0
  42. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/tools/mcp_proxy.py +0 -0
  43. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/tools/tests/__init__.py +0 -0
  44. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/gemini/tools/tests/test_computer.py +0 -0
  45. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/misc/__init__.py +0 -0
  46. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/misc/response_automation.py +0 -0
  47. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/__init__.py +0 -0
  48. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/agent.py +0 -0
  49. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/tools/__init__.py +0 -0
  50. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/tools/apply_patch.py +0 -0
  51. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/tools/base.py +0 -0
  52. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/tools/coding.py +0 -0
  53. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/tools/computer.py +0 -0
  54. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/tools/hosted.py +0 -0
  55. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/tools/mcp_proxy.py +0 -0
  56. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/tools/strict_schema.py +0 -0
  57. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/tools/tests/__init__.py +0 -0
  58. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/tools/tests/test_computer.py +0 -0
  59. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai/tools/tests/test_strict_schema.py +0 -0
  60. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai_compatible/__init__.py +0 -0
  61. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai_compatible/agent.py +0 -0
  62. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai_compatible/tools/__init__.py +0 -0
  63. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai_compatible/tools/base.py +0 -0
  64. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai_compatible/tools/filesystem.py +0 -0
  65. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/openai_compatible/tools/mcp_proxy.py +0 -0
  66. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/robot/__init__.py +0 -0
  67. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/robot/adapter.py +0 -0
  68. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/robot/agent.py +0 -0
  69. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/robot/batching.py +0 -0
  70. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/robot/dataset.py +0 -0
  71. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/robot/model.py +0 -0
  72. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/__init__.py +0 -0
  73. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/test_apply_patch.py +0 -0
  74. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/test_base.py +0 -0
  75. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/test_claude_sdk_agent.py +0 -0
  76. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/test_gemini_agent.py +0 -0
  77. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/test_openai_agent.py +0 -0
  78. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/test_openai_compatible_agent.py +0 -0
  79. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/test_provider_native_tools.py +0 -0
  80. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/test_robot_dataset_writer.py +0 -0
  81. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/test_tool_agent.py +0 -0
  82. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tests/test_trace.py +0 -0
  83. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tool_agent.py +0 -0
  84. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tools/__init__.py +0 -0
  85. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tools/base.py +0 -0
  86. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tools/hosted.py +0 -0
  87. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tools/mcp.py +0 -0
  88. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tools/rfb.py +0 -0
  89. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/tools/ssh.py +0 -0
  90. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/agents/types.py +0 -0
  91. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/capabilities/__init__.py +0 -0
  92. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/capabilities/base.py +0 -0
  93. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/capabilities/cdp.py +0 -0
  94. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/capabilities/mcp.py +0 -0
  95. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/capabilities/rfb.py +0 -0
  96. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/capabilities/robot.py +0 -0
  97. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/capabilities/ssh.py +0 -0
  98. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/capabilities/tests/__init__.py +0 -0
  99. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/capabilities/tests/test_rfb.py +0 -0
  100. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/capabilities/tests/test_ssh.py +0 -0
  101. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/__init__.py +0 -0
  102. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/__main__.py +0 -0
  103. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/cancel.py +0 -0
  104. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/client.py +0 -0
  105. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/deploy.py +0 -0
  106. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/eval.py +0 -0
  107. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/init.py +0 -0
  108. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/jobs.py +0 -0
  109. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/login.py +0 -0
  110. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/models.py +0 -0
  111. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/presets.py +0 -0
  112. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/serve.py +0 -0
  113. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/sync.py +0 -0
  114. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/task.py +0 -0
  115. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/templates.py +0 -0
  116. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/__init__.py +0 -0
  117. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_cli_init.py +0 -0
  118. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_cli_main.py +0 -0
  119. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_cli_more_wrappers.py +0 -0
  120. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_deploy.py +0 -0
  121. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_eval_bedrock.py +0 -0
  122. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_eval_config.py +0 -0
  123. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_init.py +0 -0
  124. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_jobs.py +0 -0
  125. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_main_module.py +0 -0
  126. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_sync_export.py +0 -0
  127. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_trace.py +0 -0
  128. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/tests/test_usage.py +0 -0
  129. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/trace.py +0 -0
  130. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/__init__.py +0 -0
  131. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/api.py +0 -0
  132. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/build_display.py +0 -0
  133. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/build_logs.py +0 -0
  134. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/config.py +0 -0
  135. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/context.py +0 -0
  136. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/display.py +0 -0
  137. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/jobs.py +0 -0
  138. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/registry.py +0 -0
  139. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/source.py +0 -0
  140. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/tasks.py +0 -0
  141. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/tests/__init__.py +0 -0
  142. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/tests/test_build_display.py +0 -0
  143. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/tests/test_config.py +0 -0
  144. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/tests/test_context.py +0 -0
  145. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/tests/test_registry.py +0 -0
  146. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/tests/test_source.py +0 -0
  147. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/tests/test_tasks.py +0 -0
  148. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/tests/test_version_check.py +0 -0
  149. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/usage.py +0 -0
  150. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/cli/utils/version_check.py +0 -0
  151. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/clients/__init__.py +0 -0
  152. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/clients/client.py +0 -0
  153. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/clients/tests/__init__.py +0 -0
  154. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/clients/tests/test_connect.py +0 -0
  155. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/conftest.py +0 -0
  156. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/__init__.py +0 -0
  157. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/arguments.py +0 -0
  158. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/egress.py +0 -0
  159. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/env.py +0 -0
  160. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/file_tracker.py +0 -0
  161. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/namespace.py +0 -0
  162. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/robot/__init__.py +0 -0
  163. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/robot/bridge.py +0 -0
  164. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/robot/endpoint.py +0 -0
  165. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/robot/gym.py +0 -0
  166. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/server.py +0 -0
  167. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/__init__.py +0 -0
  168. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/conftest.py +0 -0
  169. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_arguments.py +0 -0
  170. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_capability_backing.py +0 -0
  171. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_file_tracker.py +0 -0
  172. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_file_tracking.py +0 -0
  173. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_loader.py +0 -0
  174. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_manifest.py +0 -0
  175. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_platform.py +0 -0
  176. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_robot_regressions.py +0 -0
  177. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_server.py +0 -0
  178. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_sessions.py +0 -0
  179. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_tunnel.py +0 -0
  180. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/tests/test_workspace.py +0 -0
  181. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/utils.py +0 -0
  182. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/environment/workspace.py +0 -0
  183. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/__init__.py +0 -0
  184. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/chat.py +0 -0
  185. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/docker-seccomp.json +0 -0
  186. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/file_tracking.py +0 -0
  187. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/job.py +0 -0
  188. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/run.py +0 -0
  189. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/runtime/__init__.py +0 -0
  190. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/runtime/compose.py +0 -0
  191. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/runtime/core.py +0 -0
  192. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/runtime/daytona.py +0 -0
  193. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/runtime/docker.py +0 -0
  194. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/runtime/hosted.py +0 -0
  195. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/runtime/hud.py +0 -0
  196. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/runtime/local.py +0 -0
  197. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/runtime/modal.py +0 -0
  198. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/sync.py +0 -0
  199. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/task.py +0 -0
  200. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/taskset.py +0 -0
  201. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/tests/__init__.py +0 -0
  202. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/tests/test_chat.py +0 -0
  203. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/tests/test_docker_provider.py +0 -0
  204. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/tests/test_file_tracking_observer.py +0 -0
  205. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/tests/test_hosted.py +0 -0
  206. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/tests/test_job.py +0 -0
  207. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/tests/test_local_runtime.py +0 -0
  208. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/tests/test_rollout.py +0 -0
  209. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/tests/test_shared.py +0 -0
  210. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/tests/test_sync.py +0 -0
  211. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/eval/tests/test_task.py +0 -0
  212. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/graders/__init__.py +0 -0
  213. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/graders/base.py +0 -0
  214. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/graders/bash.py +0 -0
  215. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/graders/combine.py +0 -0
  216. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/graders/judge.py +0 -0
  217. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/graders/results.py +0 -0
  218. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/graders/text.py +0 -0
  219. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/__init__.py +0 -0
  220. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/Dockerfile +0 -0
  221. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/__init__.py +0 -0
  222. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/adapt.py +0 -0
  223. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/env.py +0 -0
  224. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/export.py +0 -0
  225. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/install.sh +0 -0
  226. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/__init__.py +0 -0
  227. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/conftest.py +0 -0
  228. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/agent-lifecycle/environment/Dockerfile +0 -0
  229. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/agent-lifecycle/instruction.md +0 -0
  230. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/agent-lifecycle/solution/solve.sh +0 -0
  231. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/agent-lifecycle/task.toml +0 -0
  232. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/agent-lifecycle/tests/test.sh +0 -0
  233. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/Dockerfile +0 -0
  234. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/docker-compose.yaml +0 -0
  235. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/Dockerfile +0 -0
  236. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/__init__.py +0 -0
  237. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/server.py +0 -0
  238. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/hello-mcp/instruction.md +0 -0
  239. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/hello-mcp/solution/solve.sh +0 -0
  240. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/hello-mcp/task.toml +0 -0
  241. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/hello-mcp/tests/test.sh +0 -0
  242. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/Dockerfile +0 -0
  243. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/entrypoint.sh +0 -0
  244. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/phase-boundary/instruction.md +0 -0
  245. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/phase-boundary/solution/solve.sh +0 -0
  246. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/phase-boundary/task.toml +0 -0
  247. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/phase-boundary/tests/test.sh +0 -0
  248. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/Dockerfile +0 -0
  249. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/docker-compose.yaml +0 -0
  250. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/sidecar-reachability/instruction.md +0 -0
  251. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/sidecar-reachability/solution/solve.sh +0 -0
  252. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/sidecar-reachability/task.toml +0 -0
  253. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/Dockerfile +0 -0
  254. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/test.sh +0 -0
  255. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/Dockerfile +0 -0
  256. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/entrypoint.sh +0 -0
  257. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/instruction.md +0 -0
  258. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/solution/solve.sh +0 -0
  259. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/task.toml +0 -0
  260. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/tests/test.sh +0 -0
  261. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/test_contract.py +0 -0
  262. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/test_harbor.py +0 -0
  263. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/integrations/harbor/tests/test_integration.py +0 -0
  264. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/patches/__init__.py +0 -0
  265. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/patches/mcp_patches.py +0 -0
  266. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/patches/tests/__init__.py +0 -0
  267. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/patches/tests/test_warnings.py +0 -0
  268. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/patches/warnings.py +0 -0
  269. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/py.typed +0 -0
  270. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/settings.py +0 -0
  271. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/telemetry/__init__.py +0 -0
  272. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/telemetry/context.py +0 -0
  273. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/telemetry/exporter.py +0 -0
  274. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/telemetry/instrument.py +0 -0
  275. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/telemetry/robot/__init__.py +0 -0
  276. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/telemetry/robot/recorder.py +0 -0
  277. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/telemetry/robot/video.py +0 -0
  278. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/telemetry/span.py +0 -0
  279. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/telemetry/tests/__init__.py +0 -0
  280. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/telemetry/tests/test_exporter.py +0 -0
  281. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/telemetry/tests/test_instrument.py +0 -0
  282. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/train/__init__.py +0 -0
  283. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/train/base.py +0 -0
  284. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/train/client.py +0 -0
  285. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/train/tests/__init__.py +0 -0
  286. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/train/tests/test_client.py +0 -0
  287. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/train/types.py +0 -0
  288. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/types.py +0 -0
  289. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/__init__.py +0 -0
  290. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/docker.py +0 -0
  291. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/exceptions.py +0 -0
  292. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/gateway.py +0 -0
  293. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/hints.py +0 -0
  294. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/hud_console.py +0 -0
  295. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/modules.py +0 -0
  296. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/naming.py +0 -0
  297. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/platform.py +0 -0
  298. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/process.py +0 -0
  299. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/requests.py +0 -0
  300. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/serialization.py +0 -0
  301. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/tests/__init__.py +0 -0
  302. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/tests/test_exceptions.py +0 -0
  303. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/tests/test_gateway.py +0 -0
  304. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/tests/test_hints.py +0 -0
  305. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/tests/test_hud_console.py +0 -0
  306. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/tests/test_platform.py +0 -0
  307. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/tests/test_process.py +0 -0
  308. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/tests/test_requests.py +0 -0
  309. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/tests/test_serialization.py +0 -0
  310. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/hud/utils/time.py +0 -0
  311. {hud-0.6.16.dev3 → hud-0.6.16.dev4}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hud
3
- Version: 0.6.16.dev3
3
+ Version: 0.6.16.dev4
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
@@ -6,6 +6,8 @@ import asyncio
6
6
  import copy
7
7
  import json
8
8
  import logging
9
+ import math
10
+ from random import SystemRandom
9
11
  from typing import TYPE_CHECKING, Literal, cast
10
12
 
11
13
  import httpx
@@ -63,7 +65,30 @@ _RETRYABLE_STREAM_ERROR_TYPES = frozenset(
63
65
  "upstream_error",
64
66
  }
65
67
  )
66
- _STREAM_RETRY_DELAY_SECONDS = 1.0
68
+ _STREAM_MAX_ATTEMPTS = 3
69
+ _STREAM_RETRY_BASE_DELAY_SECONDS = 1.0
70
+ _STREAM_RETRY_MAX_DELAY_SECONDS = 5.0
71
+ _STREAM_JITTER = SystemRandom()
72
+
73
+
74
+ def _stream_retry_delay(attempt: int, error: Exception) -> float:
75
+ if isinstance(error, APIStatusError):
76
+ for header, divisor in (("retry-after-ms", 1000.0), ("retry-after", 1.0)):
77
+ value = error.response.headers.get(header)
78
+ if value is None:
79
+ continue
80
+ try:
81
+ retry_after = float(value) / divisor
82
+ except ValueError:
83
+ continue
84
+ if math.isfinite(retry_after):
85
+ return min(max(retry_after, 0.0), _STREAM_RETRY_MAX_DELAY_SECONDS)
86
+
87
+ backoff_limit = min(
88
+ _STREAM_RETRY_BASE_DELAY_SECONDS * (2 ** (attempt - 1)),
89
+ _STREAM_RETRY_MAX_DELAY_SECONDS,
90
+ )
91
+ return _STREAM_JITTER.uniform(0.0, backoff_limit)
67
92
 
68
93
 
69
94
  class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
@@ -201,7 +226,7 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
201
226
  tool_choice: BetaToolChoiceAutoParam,
202
227
  betas: list[str] | Omit,
203
228
  ) -> BetaMessage:
204
- retries = 0
229
+ attempt = 1
205
230
  while True:
206
231
  stream_opened = False
207
232
  try:
@@ -237,13 +262,20 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
237
262
  | httpx.TransportError
238
263
  | httpx2.TransportError,
239
264
  )
240
- if retries or not (inline_error or interrupted_stream):
265
+ if attempt >= _STREAM_MAX_ATTEMPTS or not (inline_error or interrupted_stream):
241
266
  raise
242
267
 
243
- retries += 1
268
+ retry_delay = _stream_retry_delay(attempt, exc)
244
269
  error_type = exc.type if isinstance(exc, APIStatusError) else type(exc).__name__
245
- logger.warning("Claude stream failed with %s; retrying once", error_type)
246
- await asyncio.sleep(_STREAM_RETRY_DELAY_SECONDS)
270
+ logger.warning(
271
+ "Claude stream failed with %s; retrying in %.2fs (attempt %d/%d)",
272
+ error_type,
273
+ retry_delay,
274
+ attempt + 1,
275
+ _STREAM_MAX_ATTEMPTS,
276
+ )
277
+ attempt += 1
278
+ await asyncio.sleep(retry_delay)
247
279
 
248
280
  async def get_response(
249
281
  self,
@@ -6,7 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  from types import SimpleNamespace
8
8
  from typing import Any, cast
9
- from unittest.mock import AsyncMock
9
+ from unittest.mock import AsyncMock, Mock, call
10
10
 
11
11
  import httpx
12
12
  import httpx2
@@ -78,10 +78,11 @@ def _state(agent: ClaudeAgent) -> Any:
78
78
  return RunState(messages=[agent._format_message("user", "go")])
79
79
 
80
80
 
81
- def _inline_error(type_: str) -> APIStatusError:
81
+ def _inline_error(type_: str, *, headers: dict[str, str] | None = None) -> APIStatusError:
82
82
  body = {"type": "error", "error": {"type": type_, "message": "stream failed"}}
83
83
  response = httpx.Response(
84
84
  200,
85
+ headers=headers,
85
86
  request=httpx.Request("POST", "https://api.anthropic.com/v1/messages"),
86
87
  )
87
88
  return APIStatusError(str(body), response=response, body=body)
@@ -149,33 +150,74 @@ async def test_get_response_retries_transient_inline_stream_error(
149
150
  SimpleNamespace(type="text", text="recovered", citations=None),
150
151
  stop_reason="end_turn",
151
152
  )
152
- agent = _agent(_inline_error(error_type), final)
153
+ agent = _agent(_inline_error(error_type), _inline_error(error_type), final)
153
154
  state = _state(agent)
154
155
  sleep = AsyncMock()
156
+ jitter = Mock(side_effect=[0.25, 1.5])
155
157
  monkeypatch.setattr("hud.agents.claude.agent.asyncio.sleep", sleep)
158
+ monkeypatch.setattr("hud.agents.claude.agent._STREAM_JITTER.uniform", jitter)
156
159
 
157
160
  result = await agent.get_response(state)
158
161
 
159
162
  messages = cast("FakeMessages", cast("Any", agent.anthropic_client).beta.messages)
160
- assert messages.calls == 2
163
+ assert messages.calls == 3
161
164
  assert result.content == "recovered"
162
165
  assert len(state.messages) == 2
163
- sleep.assert_awaited_once_with(1.0)
166
+ assert sleep.await_args_list == [call(0.25), call(1.5)]
167
+ assert jitter.call_args_list == [call(0.0, 1.0), call(0.0, 2.0)]
164
168
 
165
169
 
166
170
  async def test_get_response_raises_after_transient_stream_retry_is_exhausted(
167
171
  monkeypatch: pytest.MonkeyPatch,
168
172
  ) -> None:
169
- agent = _agent(_inline_error("gateway_timeout"), _inline_error("gateway_timeout"))
173
+ agent = _agent(
174
+ _inline_error("gateway_timeout"),
175
+ _inline_error("gateway_timeout"),
176
+ _inline_error("gateway_timeout"),
177
+ )
170
178
  state = _state(agent)
171
- monkeypatch.setattr("hud.agents.claude.agent.asyncio.sleep", AsyncMock())
179
+ sleep = AsyncMock()
180
+ monkeypatch.setattr("hud.agents.claude.agent.asyncio.sleep", sleep)
181
+ monkeypatch.setattr("hud.agents.claude.agent._STREAM_JITTER.uniform", Mock(return_value=0.0))
172
182
 
173
183
  with pytest.raises(APIStatusError, match="gateway_timeout"):
174
184
  await agent.get_response(state)
175
185
 
176
186
  messages = cast("FakeMessages", cast("Any", agent.anthropic_client).beta.messages)
177
- assert messages.calls == 2
187
+ assert messages.calls == 3
178
188
  assert len(state.messages) == 1
189
+ assert sleep.await_count == 2
190
+
191
+
192
+ @pytest.mark.parametrize(
193
+ ("headers", "expected_delay"),
194
+ [
195
+ ({"retry-after": "1.75"}, 1.75),
196
+ ({"retry-after-ms": "1750"}, 1.75),
197
+ ({"retry-after": "60"}, 5.0),
198
+ ],
199
+ ids=["seconds", "milliseconds", "capped"],
200
+ )
201
+ async def test_get_response_honors_retry_after(
202
+ monkeypatch: pytest.MonkeyPatch,
203
+ headers: dict[str, str],
204
+ expected_delay: float,
205
+ ) -> None:
206
+ final = _final(
207
+ SimpleNamespace(type="text", text="recovered", citations=None),
208
+ stop_reason="end_turn",
209
+ )
210
+ agent = _agent(_inline_error("overloaded_error", headers=headers), final)
211
+ sleep = AsyncMock()
212
+ jitter = Mock()
213
+ monkeypatch.setattr("hud.agents.claude.agent.asyncio.sleep", sleep)
214
+ monkeypatch.setattr("hud.agents.claude.agent._STREAM_JITTER.uniform", jitter)
215
+
216
+ result = await agent.get_response(_state(agent))
217
+
218
+ assert result.content == "recovered"
219
+ sleep.assert_awaited_once_with(expected_delay)
220
+ jitter.assert_not_called()
179
221
 
180
222
 
181
223
  @pytest.mark.parametrize(
@@ -0,0 +1,419 @@
1
+ """List, run, and inspect trace-level platform QA agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from typing import Any, cast
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.text import Text
13
+
14
+ from hud.cli.qa_analysis import is_standard_result_blob, presentation_for_result
15
+ from hud.cli.utils.api import require_api_key
16
+ from hud.settings import settings
17
+ from hud.utils.exceptions import HudTimeoutError
18
+ from hud.utils.hud_console import DIM, GOLD, GREEN, RED, SECONDARY, HUDConsole
19
+ from hud.utils.platform import PlatformClient
20
+
21
+ hud_console = HUDConsole()
22
+ _console = Console()
23
+
24
+ _POLL_INTERVAL_SECONDS = 2.0
25
+ _TERMINAL_STATUSES = {"completed", "error"}
26
+ _TRACE_SUBJECT = "trace"
27
+ _RESULT_LINE_CAP = 12
28
+
29
+ qa_app = typer.Typer(
30
+ name="qa",
31
+ help="List, run, and inspect trace-level platform QA agents.",
32
+ add_completion=False,
33
+ rich_markup_mode="rich",
34
+ no_args_is_help=False,
35
+ )
36
+
37
+
38
+ def _platform() -> PlatformClient:
39
+ require_api_key("use platform QA agents")
40
+ return PlatformClient.from_settings()
41
+
42
+
43
+ def _print_json(payload: Any) -> None:
44
+ typer.echo(json.dumps(payload, indent=2, sort_keys=True, default=str))
45
+
46
+
47
+ def _print_agent(agent: dict[str, Any]) -> None:
48
+ typer.echo(f"{agent.get('name', '-')}\t{agent.get('id', '-')}")
49
+
50
+
51
+ def _print_results(results: list[dict[str, Any]], *, json_output: bool) -> None:
52
+ if json_output:
53
+ _print_json(results)
54
+ return
55
+ if not results:
56
+ typer.echo("No QA results found.")
57
+ return
58
+ for result in results:
59
+ view = presentation_for_result(result)
60
+ verdict = result.get("status", "unknown") if view.kind == "pending" else view.tag
61
+ summary = view.summary or result.get("error")
62
+ subject_id = result.get("subject_trace_id") or "-"
63
+ agent = result.get("agent_name") or result.get("qa_agent_id") or "-"
64
+ stale = " stale" if result.get("stale") is True else ""
65
+ line = f"{subject_id}\t{agent}\t{verdict}{stale}"
66
+ typer.echo(f"{line}\t{summary}" if summary else line)
67
+
68
+
69
+ def _fetch_rollout(platform: PlatformClient, result_id: str) -> list[dict[str, Any]]:
70
+ events: list[dict[str, Any]] = []
71
+ since_seq = -1
72
+ while True:
73
+ page = cast(
74
+ "dict[str, Any]",
75
+ platform.get(
76
+ f"/qa-agents/results/{result_id}/rollout",
77
+ params={"since_seq": since_seq, "limit": 100},
78
+ ),
79
+ )
80
+ events.extend(cast("list[dict[str, Any]]", page.get("events") or []))
81
+ if not page.get("has_more"):
82
+ return events
83
+ next_seq = int(page["next_seq"])
84
+ if next_seq == since_seq:
85
+ return events
86
+ since_seq = next_seq
87
+
88
+
89
+ def _tool_command(event: dict[str, Any]) -> str:
90
+ args = event.get("arguments") or {}
91
+ if not isinstance(args, dict):
92
+ return str(args)
93
+ commands = args.get("commands")
94
+ if isinstance(commands, list):
95
+ return "\n".join(str(item) for item in commands)
96
+ claims = args.get("claims")
97
+ if claims:
98
+ return str(claims)
99
+ return ", ".join(f"{k}={v!r}" for k, v in args.items())
100
+
101
+
102
+ def _capped_text(value: str, *, max_lines: int) -> Text:
103
+ lines = value.splitlines() or [value]
104
+ shown = lines[:max_lines]
105
+ text = Text("\n".join(shown))
106
+ extra = len(lines) - len(shown)
107
+ if extra > 0:
108
+ text.append(f"\n… {extra} more lines", style=DIM)
109
+ return text
110
+
111
+
112
+ def _bold_title(label: str) -> Text:
113
+ return Text(label, style="bold")
114
+
115
+
116
+ def _render_qa_rollout(events: list[dict[str, Any]]) -> None:
117
+ turn = 0
118
+ for event in events:
119
+ kind = event.get("kind")
120
+ if kind == "agent_message":
121
+ text = event.get("text")
122
+ reasoning = event.get("reasoning")
123
+ if isinstance(text, str) and is_standard_result_blob(text):
124
+ continue
125
+ if not text and not reasoning:
126
+ continue
127
+ turn += 1
128
+ body = Text()
129
+ if reasoning:
130
+ body.append(str(reasoning), style=f"italic {DIM}")
131
+ if text:
132
+ body.append("\n")
133
+ if text:
134
+ body.append(str(text))
135
+ _console.print(
136
+ Panel(
137
+ body,
138
+ title=_bold_title(f"Turn {turn} · agent"),
139
+ border_style=SECONDARY,
140
+ padding=(0, 1),
141
+ )
142
+ )
143
+ elif kind in ("tool_call", "tool_result"):
144
+ name = str(event.get("tool_name") or event.get("name") or "tool")
145
+ error = event.get("error")
146
+ result = event.get("result_text") or event.get("result") or ""
147
+ body = Text(_tool_command(event))
148
+ if error:
149
+ body.append(f"\n\nerror: {error}", style=RED)
150
+ border = RED
151
+ else:
152
+ if result:
153
+ body.append("\n\n")
154
+ body.append_text(_capped_text(str(result), max_lines=_RESULT_LINE_CAP))
155
+ border = GREEN
156
+ _console.print(
157
+ Panel(
158
+ body,
159
+ title=_bold_title(name),
160
+ border_style=border,
161
+ padding=(0, 1),
162
+ )
163
+ )
164
+ elif kind == "subagent":
165
+ name = str(event.get("agent_name") or "subagent")
166
+ args = event.get("arguments") or {}
167
+ _console.print(
168
+ Panel(
169
+ Text(_tool_command(event) if args else name, style=DIM),
170
+ title=_bold_title(name),
171
+ border_style=GOLD,
172
+ padding=(0, 1),
173
+ )
174
+ )
175
+
176
+
177
+ def _print_result_tui(
178
+ result: dict[str, Any],
179
+ events: list[dict[str, Any]] | None,
180
+ ) -> None:
181
+ agent = str(result.get("agent_name") or result.get("qa_agent_id") or "QA")
182
+ subject_id = str(result.get("subject_trace_id") or "-")
183
+ view = presentation_for_result(result)
184
+ hud_console.header(agent, icon="", stderr=False)
185
+ if view.kind == "pending":
186
+ hud_console.status_item(
187
+ "status",
188
+ str(result.get("status") or "unknown"),
189
+ status="info",
190
+ stderr=False,
191
+ )
192
+ else:
193
+ if view.tag == "passed":
194
+ status = "success"
195
+ elif view.tag == "failed":
196
+ status = "error"
197
+ else:
198
+ status = "info"
199
+ hud_console.status_item("verdict", view.tag, status=status, stderr=False)
200
+ if view.kind == "boolean" and view.answer:
201
+ hud_console.dim_info(view.label.lower(), view.answer, stderr=False)
202
+ elif view.answer:
203
+ hud_console.dim_info("cause", view.answer, stderr=False)
204
+ hud_console.dim_info("trace", subject_id, stderr=False)
205
+ if view.confidence:
206
+ hud_console.dim_info("confidence", view.confidence, stderr=False)
207
+ if result.get("stale") is True:
208
+ hud_console.warning(
209
+ "This result is stale relative to the current agent config.",
210
+ stderr=False,
211
+ )
212
+ if view.summary:
213
+ _console.print(
214
+ Panel(
215
+ Text(view.summary),
216
+ title=_bold_title("Summary"),
217
+ border_style=GOLD,
218
+ padding=(0, 1),
219
+ )
220
+ )
221
+ for index, finding in enumerate(view.findings, start=1):
222
+ body = Text(finding.description)
223
+ if finding.fault:
224
+ if finding.description:
225
+ body.append("\n\n")
226
+ body.append(f"fault: {finding.fault}", style=DIM)
227
+ _console.print(
228
+ Panel(
229
+ body,
230
+ title=_bold_title(f"{index}. {finding.title}"),
231
+ border_style=GOLD,
232
+ padding=(0, 1),
233
+ )
234
+ )
235
+ if events is None:
236
+ hud_console.dim_info("trajectory", "hidden; pass --rollout to show", stderr=False)
237
+ elif events:
238
+ hud_console.section_title("Rollout", stderr=False)
239
+ _render_qa_rollout(events)
240
+ web = settings.hud_web_url.rstrip("/")
241
+ hud_console.link(f"{web}/trace/{subject_id}", stderr=False)
242
+
243
+
244
+ def _require_trace_agent(platform: PlatformClient, agent_id: str) -> None:
245
+ agent = cast("dict[str, Any]", platform.get(f"/qa-agents/{agent_id}"))
246
+ if agent.get("subject_type") != _TRACE_SUBJECT:
247
+ typer.echo(
248
+ f"Agent {agent_id} is a {agent.get('subject_type')} QA agent. "
249
+ "The CLI currently supports trace agents only.",
250
+ err=True,
251
+ )
252
+ raise typer.Exit(1)
253
+
254
+
255
+ def _latest_agent_result(
256
+ results: list[dict[str, Any]],
257
+ *,
258
+ agent_id: str,
259
+ trace_id: str,
260
+ ) -> dict[str, Any] | None:
261
+ latest: dict[str, Any] | None = None
262
+ for result in results:
263
+ if (
264
+ result.get("qa_agent_id") == agent_id
265
+ and str(result.get("subject_trace_id")) == trace_id
266
+ ):
267
+ latest = result
268
+ return latest
269
+
270
+
271
+ def _wait_for_results(
272
+ platform: PlatformClient,
273
+ timeout: float,
274
+ *,
275
+ trace_ids: list[str],
276
+ agent_id: str,
277
+ launched: list[dict[str, Any]],
278
+ ) -> list[dict[str, Any]]:
279
+ launched_id_by_trace = {str(run["subject_trace_id"]): str(run["id"]) for run in launched}
280
+ deadline = time.monotonic() + timeout
281
+ while True:
282
+ listed = cast(
283
+ "list[dict[str, Any]]",
284
+ platform.get("/qa-agents/results", params={"subject_trace_ids": trace_ids}),
285
+ )
286
+ by_id = {str(result["id"]): result for result in listed}
287
+ selected: list[dict[str, Any]] = []
288
+ ready = True
289
+ for trace_id in trace_ids:
290
+ result_id = launched_id_by_trace.get(trace_id)
291
+ result = (
292
+ by_id.get(result_id)
293
+ if result_id is not None
294
+ else _latest_agent_result(listed, agent_id=agent_id, trace_id=trace_id)
295
+ )
296
+ if result is None or result["status"] not in _TERMINAL_STATUSES:
297
+ ready = False
298
+ break
299
+ selected.append(result)
300
+ if ready:
301
+ return selected
302
+ if time.monotonic() >= deadline:
303
+ raise HudTimeoutError(f"Timed out after {timeout:g}s waiting for QA runs.")
304
+ time.sleep(_POLL_INTERVAL_SECONDS)
305
+
306
+
307
+ @qa_app.callback(invoke_without_command=True)
308
+ def list_agents(
309
+ ctx: typer.Context,
310
+ json_output: bool = typer.Option(False, "--json", help="Output the machine-readable response."),
311
+ limit: int = typer.Option(50, "--limit", min=1, max=500, help="Maximum agents to return."),
312
+ offset: int = typer.Option(0, "--offset", min=0, help="Number of agents to skip."),
313
+ ) -> None:
314
+ """List trace QA agents available to this team."""
315
+ if ctx.invoked_subcommand is not None:
316
+ return
317
+ response = cast(
318
+ "dict[str, Any]",
319
+ _platform().get(
320
+ "/qa-agents",
321
+ params={"subject_type": _TRACE_SUBJECT, "limit": limit, "offset": offset},
322
+ ),
323
+ )
324
+ if json_output:
325
+ _print_json(response)
326
+ return
327
+ agents = response["items"]
328
+ if not agents:
329
+ typer.echo("No trace QA agents found.")
330
+ return
331
+ for agent in agents:
332
+ _print_agent(agent)
333
+
334
+
335
+ @qa_app.command("run")
336
+ def run_agent(
337
+ agent_id: str = typer.Argument(..., help="QA agent UUID."),
338
+ trace_ids: list[str] = typer.Argument( # noqa: B008
339
+ ...,
340
+ help="One or more trace UUIDs.",
341
+ ),
342
+ overwrite: bool = typer.Option(
343
+ False,
344
+ "--overwrite",
345
+ help="Create a fresh attempt even when current evidence already exists.",
346
+ ),
347
+ wait: bool = typer.Option(
348
+ True,
349
+ "--wait/--no-wait",
350
+ help="Wait for every launched analysis to finish.",
351
+ ),
352
+ timeout: float = typer.Option(
353
+ 900,
354
+ "--timeout",
355
+ min=1,
356
+ help="Maximum seconds to wait for QA execution.",
357
+ ),
358
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable results."),
359
+ ) -> None:
360
+ """Run one trace QA agent against the given traces."""
361
+ platform = _platform()
362
+ _require_trace_agent(platform, agent_id)
363
+ runs = cast(
364
+ "list[dict[str, Any]]",
365
+ platform.post(
366
+ f"/qa-agents/{agent_id}/run",
367
+ json={"trace_ids": trace_ids, "overwrite": overwrite},
368
+ ),
369
+ )
370
+ if not wait:
371
+ _print_results(runs, json_output=json_output)
372
+ return
373
+
374
+ results = _wait_for_results(
375
+ platform,
376
+ timeout,
377
+ trace_ids=trace_ids,
378
+ agent_id=agent_id,
379
+ launched=runs,
380
+ )
381
+ _print_results(results, json_output=json_output)
382
+ if any(
383
+ result["status"] == "error" or presentation_for_result(result).tag != "passed"
384
+ for result in results
385
+ ):
386
+ raise typer.Exit(1)
387
+
388
+
389
+ @qa_app.command("results")
390
+ def list_results(
391
+ trace_ids: list[str] = typer.Argument( # noqa: B008
392
+ ...,
393
+ help="One or more trace UUIDs.",
394
+ ),
395
+ json_output: bool = typer.Option(False, "--json", help="Output the machine-readable response."),
396
+ rollout: bool = typer.Option(
397
+ False,
398
+ "--rollout",
399
+ help="Show the sanitized analysis trajectory (agent turns and tool calls).",
400
+ ),
401
+ ) -> None:
402
+ """Inspect QA results for the given traces. Pass --rollout for the trajectory."""
403
+ platform = _platform()
404
+ results = cast(
405
+ "list[dict[str, Any]]",
406
+ platform.get("/qa-agents/results", params={"subject_trace_ids": trace_ids}),
407
+ )
408
+ if json_output:
409
+ _print_results(results, json_output=True)
410
+ return
411
+ if not results:
412
+ typer.echo("No QA results found.")
413
+ return
414
+ for result in results:
415
+ events: list[dict[str, Any]] | None = None
416
+ result_id = result.get("id")
417
+ if rollout and result_id:
418
+ events = _fetch_rollout(platform, str(result_id))
419
+ _print_result_tui(result, events)