hud-python 0.5.5__tar.gz → 0.5.7__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 (306) hide show
  1. {hud_python-0.5.5 → hud_python-0.5.7}/PKG-INFO +1 -1
  2. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/claude.py +45 -9
  3. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/gemini_cua.py +3 -0
  4. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/operator.py +2 -0
  5. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/test_claude.py +2 -4
  6. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/dev.py +11 -2
  7. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/__init__.py +5 -3
  8. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/connection.py +32 -4
  9. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/environment.py +134 -69
  10. hud_python-0.5.7/hud/environment/router.py +263 -0
  11. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/scenarios.py +212 -153
  12. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/tests/test_environment.py +16 -0
  13. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/tests/test_scenarios.py +351 -14
  14. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/context.py +1 -3
  15. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/tests/test_version.py +1 -1
  16. {hud_python-0.5.5 → hud_python-0.5.7}/hud/version.py +1 -1
  17. {hud_python-0.5.5 → hud_python-0.5.7}/pyproject.toml +1 -1
  18. hud_python-0.5.5/hud/environment/router.py +0 -112
  19. {hud_python-0.5.5 → hud_python-0.5.7}/.gitignore +0 -0
  20. {hud_python-0.5.5 → hud_python-0.5.7}/LICENSE +0 -0
  21. {hud_python-0.5.5 → hud_python-0.5.7}/README.md +0 -0
  22. {hud_python-0.5.5 → hud_python-0.5.7}/examples/README.md +0 -0
  23. {hud_python-0.5.5 → hud_python-0.5.7}/hud/__init__.py +0 -0
  24. {hud_python-0.5.5 → hud_python-0.5.7}/hud/__main__.py +0 -0
  25. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/__init__.py +0 -0
  26. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/base.py +0 -0
  27. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/gateway.py +0 -0
  28. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/gemini.py +0 -0
  29. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/grounded_openai.py +0 -0
  30. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/misc/__init__.py +0 -0
  31. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/misc/integration_test_agent.py +0 -0
  32. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/misc/response_agent.py +0 -0
  33. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/openai.py +0 -0
  34. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/openai_chat.py +0 -0
  35. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/resolver.py +0 -0
  36. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/__init__.py +0 -0
  37. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/conftest.py +0 -0
  38. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/test_base.py +0 -0
  39. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/test_base_runtime.py +0 -0
  40. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/test_client.py +0 -0
  41. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/test_gemini.py +0 -0
  42. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/test_grounded_openai_agent.py +0 -0
  43. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/test_openai.py +0 -0
  44. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/test_operator.py +0 -0
  45. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/test_resolver.py +0 -0
  46. {hud_python-0.5.5 → hud_python-0.5.7}/hud/agents/tests/test_run_eval.py +0 -0
  47. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/__init__.py +0 -0
  48. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/__main__.py +0 -0
  49. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/analyze.py +0 -0
  50. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/build.py +0 -0
  51. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/clone.py +0 -0
  52. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/debug.py +0 -0
  53. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/eval.py +0 -0
  54. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/flows/__init__.py +0 -0
  55. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/flows/dev.py +0 -0
  56. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/flows/init.py +0 -0
  57. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/flows/tasks.py +0 -0
  58. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/flows/templates.py +0 -0
  59. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/flows/tests/__init__.py +0 -0
  60. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/flows/tests/test_dev.py +0 -0
  61. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/get.py +0 -0
  62. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/init.py +0 -0
  63. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/list_func.py +0 -0
  64. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/pull.py +0 -0
  65. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/push.py +0 -0
  66. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/remove.py +0 -0
  67. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/rft.py +0 -0
  68. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/rft_status.py +0 -0
  69. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/__init__.py +0 -0
  70. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_analyze.py +0 -0
  71. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_analyze_metadata.py +0 -0
  72. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_analyze_module.py +0 -0
  73. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_build.py +0 -0
  74. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_build_failure.py +0 -0
  75. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_build_module.py +0 -0
  76. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_cli_init.py +0 -0
  77. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_cli_main.py +0 -0
  78. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_cli_more_wrappers.py +0 -0
  79. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_cli_root.py +0 -0
  80. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_clone.py +0 -0
  81. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_convert.py +0 -0
  82. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_cursor.py +0 -0
  83. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_debug.py +0 -0
  84. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_dev.py +0 -0
  85. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_eval.py +0 -0
  86. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_eval_bedrock.py +0 -0
  87. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_init.py +0 -0
  88. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_list_func.py +0 -0
  89. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_main_module.py +0 -0
  90. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_mcp_server.py +0 -0
  91. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_pull.py +0 -0
  92. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_push.py +0 -0
  93. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_push_happy.py +0 -0
  94. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_push_wrapper.py +0 -0
  95. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_registry.py +0 -0
  96. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/tests/test_utils.py +0 -0
  97. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/__init__.py +0 -0
  98. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/celebrate.py +0 -0
  99. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/config.py +0 -0
  100. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/cursor.py +0 -0
  101. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/docker.py +0 -0
  102. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/env_check.py +0 -0
  103. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/environment.py +0 -0
  104. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/git.py +0 -0
  105. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/interactive.py +0 -0
  106. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/local_runner.py +0 -0
  107. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/logging.py +0 -0
  108. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/metadata.py +0 -0
  109. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/package_runner.py +0 -0
  110. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/registry.py +0 -0
  111. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/remote_runner.py +0 -0
  112. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/runner.py +0 -0
  113. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/server.py +0 -0
  114. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/source_hash.py +0 -0
  115. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tasks.py +0 -0
  116. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/__init__.py +0 -0
  117. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_config.py +0 -0
  118. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_docker.py +0 -0
  119. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_docker_hints.py +0 -0
  120. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_env_check.py +0 -0
  121. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_environment.py +0 -0
  122. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_git.py +0 -0
  123. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_interactive_module.py +0 -0
  124. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_local_runner.py +0 -0
  125. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_logging_utils.py +0 -0
  126. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_metadata.py +0 -0
  127. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_package_runner.py +0 -0
  128. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_registry_utils.py +0 -0
  129. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_remote_runner.py +0 -0
  130. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_runner_modules.py +0 -0
  131. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_source_hash.py +0 -0
  132. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/tests/test_tasks.py +0 -0
  133. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/version_check.py +0 -0
  134. {hud_python-0.5.5 → hud_python-0.5.7}/hud/cli/utils/viewer.py +0 -0
  135. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/README.md +0 -0
  136. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/__init__.py +0 -0
  137. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/base.py +0 -0
  138. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/environment.py +0 -0
  139. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/fastmcp.py +0 -0
  140. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/mcp_use.py +0 -0
  141. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/tests/__init__.py +0 -0
  142. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/tests/test_analyze_scenarios.py +0 -0
  143. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/tests/test_client_integration.py +0 -0
  144. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/tests/test_fastmcp.py +0 -0
  145. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/tests/test_mcp_use_retry.py +0 -0
  146. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/tests/test_protocol.py +0 -0
  147. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/utils/__init__.py +0 -0
  148. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/utils/mcp_use_retry.py +0 -0
  149. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/utils/retry.py +0 -0
  150. {hud_python-0.5.5 → hud_python-0.5.7}/hud/clients/utils/retry_transport.py +0 -0
  151. {hud_python-0.5.5 → hud_python-0.5.7}/hud/datasets/__init__.py +0 -0
  152. {hud_python-0.5.5 → hud_python-0.5.7}/hud/datasets/loader.py +0 -0
  153. {hud_python-0.5.5 → hud_python-0.5.7}/hud/datasets/runner.py +0 -0
  154. {hud_python-0.5.5 → hud_python-0.5.7}/hud/datasets/tests/__init__.py +0 -0
  155. {hud_python-0.5.5 → hud_python-0.5.7}/hud/datasets/tests/test_loader.py +0 -0
  156. {hud_python-0.5.5 → hud_python-0.5.7}/hud/datasets/tests/test_utils.py +0 -0
  157. {hud_python-0.5.5 → hud_python-0.5.7}/hud/datasets/utils.py +0 -0
  158. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/connectors/__init__.py +0 -0
  159. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/connectors/base.py +0 -0
  160. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/connectors/local.py +0 -0
  161. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/connectors/mcp_config.py +0 -0
  162. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/connectors/openai.py +0 -0
  163. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/connectors/remote.py +0 -0
  164. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/integrations/__init__.py +0 -0
  165. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/integrations/adk.py +0 -0
  166. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/integrations/anthropic.py +0 -0
  167. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/integrations/gemini.py +0 -0
  168. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/integrations/langchain.py +0 -0
  169. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/integrations/llamaindex.py +0 -0
  170. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/integrations/openai.py +0 -0
  171. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/mock.py +0 -0
  172. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/tests/__init__.py +0 -0
  173. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/tests/test_connection.py +0 -0
  174. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/tests/test_connectors.py +0 -0
  175. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/tests/test_integrations.py +0 -0
  176. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/tests/test_local_connectors.py +0 -0
  177. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/tests/test_tools.py +0 -0
  178. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/types.py +0 -0
  179. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/utils/__init__.py +0 -0
  180. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/utils/formats.py +0 -0
  181. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/utils/schema.py +0 -0
  182. {hud_python-0.5.5 → hud_python-0.5.7}/hud/environment/utils/tool_wrappers.py +0 -0
  183. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/__init__.py +0 -0
  184. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/display.py +0 -0
  185. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/instrument.py +0 -0
  186. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/manager.py +0 -0
  187. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/parallel.py +0 -0
  188. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/task.py +0 -0
  189. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/tests/__init__.py +0 -0
  190. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/tests/test_context.py +0 -0
  191. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/tests/test_eval.py +0 -0
  192. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/tests/test_manager.py +0 -0
  193. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/tests/test_parallel.py +0 -0
  194. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/tests/test_task.py +0 -0
  195. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/types.py +0 -0
  196. {hud_python-0.5.5 → hud_python-0.5.7}/hud/eval/utils.py +0 -0
  197. {hud_python-0.5.5 → hud_python-0.5.7}/hud/native/__init__.py +0 -0
  198. {hud_python-0.5.5 → hud_python-0.5.7}/hud/native/comparator.py +0 -0
  199. {hud_python-0.5.5 → hud_python-0.5.7}/hud/native/tests/__init__.py +0 -0
  200. {hud_python-0.5.5 → hud_python-0.5.7}/hud/native/tests/test_comparator.py +0 -0
  201. {hud_python-0.5.5 → hud_python-0.5.7}/hud/native/tests/test_native_init.py +0 -0
  202. {hud_python-0.5.5 → hud_python-0.5.7}/hud/patches/__init__.py +0 -0
  203. {hud_python-0.5.5 → hud_python-0.5.7}/hud/patches/mcp_patches.py +0 -0
  204. {hud_python-0.5.5 → hud_python-0.5.7}/hud/patches/warnings.py +0 -0
  205. {hud_python-0.5.5 → hud_python-0.5.7}/hud/py.typed +0 -0
  206. {hud_python-0.5.5 → hud_python-0.5.7}/hud/samples/__init__.py +0 -0
  207. {hud_python-0.5.5 → hud_python-0.5.7}/hud/samples/browser.py +0 -0
  208. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/__init__.py +0 -0
  209. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/context.py +0 -0
  210. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/helper/__init__.py +0 -0
  211. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/low_level.py +0 -0
  212. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/router.py +0 -0
  213. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/server.py +0 -0
  214. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/tests/__init__.py +0 -0
  215. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/tests/test_add_tool.py +0 -0
  216. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/tests/test_context.py +0 -0
  217. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/tests/test_mcp_server_handlers.py +0 -0
  218. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/tests/test_mcp_server_integration.py +0 -0
  219. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/tests/test_mcp_server_more.py +0 -0
  220. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/tests/test_run_wrapper.py +0 -0
  221. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/tests/test_server_extra.py +0 -0
  222. {hud_python-0.5.5 → hud_python-0.5.7}/hud/server/tests/test_sigterm_runner.py +0 -0
  223. {hud_python-0.5.5 → hud_python-0.5.7}/hud/settings.py +0 -0
  224. {hud_python-0.5.5 → hud_python-0.5.7}/hud/shared/__init__.py +0 -0
  225. {hud_python-0.5.5 → hud_python-0.5.7}/hud/shared/exceptions.py +0 -0
  226. {hud_python-0.5.5 → hud_python-0.5.7}/hud/shared/hints.py +0 -0
  227. {hud_python-0.5.5 → hud_python-0.5.7}/hud/shared/requests.py +0 -0
  228. {hud_python-0.5.5 → hud_python-0.5.7}/hud/shared/tests/__init__.py +0 -0
  229. {hud_python-0.5.5 → hud_python-0.5.7}/hud/shared/tests/test_exceptions.py +0 -0
  230. {hud_python-0.5.5 → hud_python-0.5.7}/hud/shared/tests/test_hints.py +0 -0
  231. {hud_python-0.5.5 → hud_python-0.5.7}/hud/shared/tests/test_requests.py +0 -0
  232. {hud_python-0.5.5 → hud_python-0.5.7}/hud/telemetry/__init__.py +0 -0
  233. {hud_python-0.5.5 → hud_python-0.5.7}/hud/telemetry/exporter.py +0 -0
  234. {hud_python-0.5.5 → hud_python-0.5.7}/hud/telemetry/instrument.py +0 -0
  235. {hud_python-0.5.5 → hud_python-0.5.7}/hud/telemetry/tests/__init__.py +0 -0
  236. {hud_python-0.5.5 → hud_python-0.5.7}/hud/telemetry/tests/test_eval_telemetry.py +0 -0
  237. {hud_python-0.5.5 → hud_python-0.5.7}/hud/telemetry/tests/test_exporter.py +0 -0
  238. {hud_python-0.5.5 → hud_python-0.5.7}/hud/telemetry/tests/test_instrument.py +0 -0
  239. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/__init__.py +0 -0
  240. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/agent.py +0 -0
  241. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/apply_patch.py +0 -0
  242. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/base.py +0 -0
  243. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/bash.py +0 -0
  244. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/computer/__init__.py +0 -0
  245. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/computer/anthropic.py +0 -0
  246. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/computer/gemini.py +0 -0
  247. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/computer/hud.py +0 -0
  248. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/computer/openai.py +0 -0
  249. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/computer/qwen.py +0 -0
  250. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/computer/settings.py +0 -0
  251. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/edit.py +0 -0
  252. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/executors/__init__.py +0 -0
  253. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/executors/base.py +0 -0
  254. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/executors/pyautogui.py +0 -0
  255. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/executors/tests/__init__.py +0 -0
  256. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/executors/tests/test_base_executor.py +0 -0
  257. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/executors/tests/test_pyautogui_executor.py +0 -0
  258. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/executors/xdo.py +0 -0
  259. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/grounding/__init__.py +0 -0
  260. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/grounding/config.py +0 -0
  261. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/grounding/grounded_tool.py +0 -0
  262. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/grounding/grounder.py +0 -0
  263. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/grounding/tests/__init__.py +0 -0
  264. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/grounding/tests/test_grounded_tool.py +0 -0
  265. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/jupyter.py +0 -0
  266. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/playwright.py +0 -0
  267. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/response.py +0 -0
  268. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/shell.py +0 -0
  269. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/submit.py +0 -0
  270. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/__init__.py +0 -0
  271. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_agent_tool.py +0 -0
  272. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_apply_patch.py +0 -0
  273. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_base.py +0 -0
  274. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_bash.py +0 -0
  275. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_bash_extended.py +0 -0
  276. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_computer.py +0 -0
  277. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_computer_actions.py +0 -0
  278. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_edit.py +0 -0
  279. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_init.py +0 -0
  280. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_jupyter_tool.py +0 -0
  281. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_playwright_tool.py +0 -0
  282. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_response.py +0 -0
  283. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_shell.py +0 -0
  284. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_submit.py +0 -0
  285. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_tools.py +0 -0
  286. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_tools_init.py +0 -0
  287. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_types.py +0 -0
  288. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/tests/test_utils.py +0 -0
  289. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/types.py +0 -0
  290. {hud_python-0.5.5 → hud_python-0.5.7}/hud/tools/utils.py +0 -0
  291. {hud_python-0.5.5 → hud_python-0.5.7}/hud/types.py +0 -0
  292. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/__init__.py +0 -0
  293. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/env.py +0 -0
  294. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/hud_console.py +0 -0
  295. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/mcp.py +0 -0
  296. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/pretty_errors.py +0 -0
  297. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/strict_schema.py +0 -0
  298. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/telemetry.py +0 -0
  299. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/tests/__init__.py +0 -0
  300. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/tests/test_init.py +0 -0
  301. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/tests/test_mcp.py +0 -0
  302. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/tests/test_pretty_errors.py +0 -0
  303. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/tests/test_telemetry.py +0 -0
  304. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/tests/test_tool_shorthand.py +0 -0
  305. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/tool_shorthand.py +0 -0
  306. {hud_python-0.5.5 → hud_python-0.5.7}/hud/utils/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hud-python
3
- Version: 0.5.5
3
+ Version: 0.5.7
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
@@ -5,16 +5,18 @@ from __future__ import annotations
5
5
  import copy
6
6
  import logging
7
7
  from inspect import cleandoc
8
- from typing import Any, ClassVar, Literal, cast
8
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast
9
9
 
10
10
  import mcp.types as types
11
11
  from anthropic import AsyncAnthropic, AsyncAnthropicBedrock, Omit
12
12
  from anthropic.types import CacheControlEphemeralParam
13
13
  from anthropic.types.beta import (
14
14
  BetaBase64ImageSourceParam,
15
+ BetaBase64PDFSourceParam,
15
16
  BetaContentBlockParam,
16
17
  BetaImageBlockParam,
17
18
  BetaMessageParam,
19
+ BetaRequestDocumentBlockParam,
18
20
  BetaTextBlockParam,
19
21
  BetaToolBash20250124Param,
20
22
  BetaToolComputerUse20250124Param,
@@ -33,6 +35,9 @@ from hud.utils.types import with_signature
33
35
 
34
36
  from .base import BaseCreateParams, MCPAgent
35
37
 
38
+ if TYPE_CHECKING:
39
+ from collections.abc import Sequence
40
+
36
41
  logger = logging.getLogger(__name__)
37
42
 
38
43
 
@@ -220,7 +225,10 @@ class ClaudeAgent(MCPAgent):
220
225
  async def format_tool_results(
221
226
  self, tool_calls: list[MCPToolCall], tool_results: list[MCPToolResult]
222
227
  ) -> list[BetaMessageParam]:
223
- """Format tool results into Claude messages."""
228
+ """Format tool results into Claude messages.
229
+
230
+ Handles EmbeddedResource (PDFs), images, and text content.
231
+ """
224
232
  # Process each tool result
225
233
  user_content = []
226
234
 
@@ -232,7 +240,9 @@ class ClaudeAgent(MCPAgent):
232
240
  continue
233
241
 
234
242
  # Convert MCP tool results to Claude format
235
- claude_blocks = []
243
+ claude_blocks: list[
244
+ BetaTextBlockParam | BetaImageBlockParam | BetaRequestDocumentBlockParam
245
+ ] = []
236
246
 
237
247
  if result.isError:
238
248
  # Extract error message from content
@@ -249,6 +259,16 @@ class ClaudeAgent(MCPAgent):
249
259
  claude_blocks.append(text_to_content_block(content.text))
250
260
  elif isinstance(content, types.ImageContent):
251
261
  claude_blocks.append(base64_to_content_block(content.data))
262
+ elif isinstance(content, types.EmbeddedResource):
263
+ # Handle embedded resources (PDFs)
264
+ resource = content.resource
265
+ if (
266
+ isinstance(resource, types.BlobResourceContents)
267
+ and resource.mimeType == "application/pdf"
268
+ ):
269
+ claude_blocks.append(
270
+ document_to_content_block(base64_data=resource.blob)
271
+ )
252
272
 
253
273
  # Add tool result
254
274
  user_content.append(tool_use_content_block(tool_use_id, claude_blocks))
@@ -303,7 +323,7 @@ class ClaudeAgent(MCPAgent):
303
323
  display_width_px=computer_settings.ANTHROPIC_COMPUTER_WIDTH,
304
324
  display_height_px=computer_settings.ANTHROPIC_COMPUTER_HEIGHT,
305
325
  )
306
- elif tool.name == "computer":
326
+ elif tool.name == "computer" or tool.name.endswith("_computer"):
307
327
  logger.warning(
308
328
  "Renamed tool %s to 'computer', dropping original 'computer' tool",
309
329
  selected_computer_tool.name,
@@ -330,11 +350,14 @@ class ClaudeAgent(MCPAgent):
330
350
  self.claude_tools = []
331
351
  for tool in available_tools:
332
352
  claude_tool = to_api_tool(tool)
333
- if claude_tool is None or "name" not in claude_tool:
353
+ if claude_tool is None:
334
354
  continue
335
- if claude_tool["name"] == "computer":
355
+ tool_name = claude_tool.get("name")
356
+ if tool_name is None:
357
+ continue
358
+ if tool_name == "computer":
336
359
  self.has_computer_tool = True
337
- self.tool_mapping[claude_tool["name"]] = tool.name
360
+ self.tool_mapping[tool_name] = tool.name
338
361
  self.claude_tools.append(claude_tool)
339
362
 
340
363
  def _add_prompt_caching(self, messages: list[BetaMessageParam]) -> list[BetaMessageParam]:
@@ -380,8 +403,21 @@ def text_to_content_block(text: str) -> BetaTextBlockParam:
380
403
  return {"type": "text", "text": text}
381
404
 
382
405
 
406
+ def document_to_content_block(base64_data: str) -> BetaRequestDocumentBlockParam:
407
+ """Convert base64 PDF to Claude document content block."""
408
+ return BetaRequestDocumentBlockParam(
409
+ type="document",
410
+ source=BetaBase64PDFSourceParam(
411
+ type="base64",
412
+ media_type="application/pdf",
413
+ data=base64_data,
414
+ ),
415
+ )
416
+
417
+
383
418
  def tool_use_content_block(
384
- tool_use_id: str, content: list[BetaTextBlockParam | BetaImageBlockParam]
419
+ tool_use_id: str,
420
+ content: Sequence[BetaTextBlockParam | BetaImageBlockParam | BetaRequestDocumentBlockParam],
385
421
  ) -> BetaToolResultBlockParam:
386
422
  """Create tool result content block."""
387
- return {"type": "tool_result", "tool_use_id": tool_use_id, "content": content}
423
+ return {"type": "tool_result", "tool_use_id": tool_use_id, "content": content} # pyright: ignore[reportReturnType]
@@ -123,6 +123,9 @@ class GeminiCUAAgent(GeminiAgent):
123
123
  )
124
124
  )
125
125
 
126
+ if tool.name == "computer" or tool.name.endswith("_computer"):
127
+ return None
128
+
126
129
  # For non-computer tools, use the parent implementation
127
130
  return super()._to_gemini_tool(tool)
128
131
 
@@ -129,6 +129,8 @@ class OperatorAgent(OpenAIAgent):
129
129
  display_height=self._operator_display_height,
130
130
  environment=self._operator_environment,
131
131
  )
132
+ if tool.name == "computer" or tool.name.endswith("_computer"):
133
+ return None
132
134
  return super()._to_openai_tool(tool)
133
135
 
134
136
  def _extract_tool_call(self, item: Any) -> MCPToolCall | None:
@@ -22,7 +22,7 @@ from hud.types import MCPToolCall, MCPToolResult
22
22
  if TYPE_CHECKING:
23
23
  from collections.abc import Generator
24
24
 
25
- from anthropic.types.beta import BetaImageBlockParam, BetaMessageParam, BetaTextBlockParam
25
+ from anthropic.types.beta import BetaMessageParam
26
26
 
27
27
 
28
28
  class MockEvalContext(EvalContext):
@@ -123,9 +123,7 @@ class TestClaudeHelperFunctions:
123
123
  def test_tool_use_content_block(self) -> None:
124
124
  """Test tool result content block creation."""
125
125
  tool_use_id = "tool_123"
126
- content: list[BetaTextBlockParam | BetaImageBlockParam] = [
127
- text_to_content_block("Result text")
128
- ]
126
+ content = [text_to_content_block("Result text")]
129
127
 
130
128
  result = tool_use_content_block(tool_use_id, content)
131
129
 
@@ -250,6 +250,15 @@ async def run_mcp_module(
250
250
  elif hasattr(module, "__dict__") and attr_name in module.__dict__:
251
251
  mcp_server = module.__dict__[attr_name]
252
252
 
253
+ # If default 'mcp' not found, try 'env' as fallback
254
+ if mcp_server is None and attr_name == "mcp":
255
+ for fallback in ["env", "environment", "server"]:
256
+ if hasattr(module, fallback):
257
+ mcp_server = getattr(module, fallback)
258
+ if verbose:
259
+ hud_console.info(f"Found '{fallback}' instead of 'mcp'")
260
+ break
261
+
253
262
  if mcp_server is None:
254
263
  hud_console.error(f"Module '{module_name}' does not have '{attr_name}' defined")
255
264
  hud_console.info("")
@@ -258,8 +267,8 @@ async def run_mcp_module(
258
267
  hud_console.info("")
259
268
  hud_console.info("[bold cyan]Expected structure:[/bold cyan]")
260
269
  hud_console.info(" from hud.environment import Environment")
261
- hud_console.info(f" {attr_name} = Environment('my-env')")
262
- raise AttributeError(f"Module '{module_name}' must define '{attr_name}'")
270
+ hud_console.info(" env = Environment('my-env') # or mcp = ...")
271
+ raise AttributeError(f"Module '{module_name}' must define 'mcp', 'env', or 'environment'")
263
272
 
264
273
  # Only show full header on first run, brief message on reload
265
274
  if is_reload:
@@ -27,8 +27,8 @@ Usage:
27
27
  from hud.environment.connection import ConnectionConfig, ConnectionType, Connector
28
28
  from hud.environment.environment import Environment
29
29
  from hud.environment.mock import MockMixin, generate_mock_value
30
- from hud.environment.router import ConflictResolution, ToolRouter
31
- from hud.environment.scenarios import ScenarioMixin
30
+ from hud.environment.router import ConflictResolution, MCPRouter, ToolRouter
31
+ from hud.environment.scenarios import ScenarioMixin, ScenarioSession
32
32
  from hud.environment.types import EnvConfig
33
33
  from hud.environment.utils import ToolFormat, format_result, parse_tool_call, parse_tool_calls
34
34
 
@@ -39,10 +39,12 @@ __all__ = [
39
39
  "Connector",
40
40
  "EnvConfig",
41
41
  "Environment",
42
+ "MCPRouter",
42
43
  "MockMixin",
43
44
  "ScenarioMixin",
45
+ "ScenarioSession",
44
46
  "ToolFormat",
45
- "ToolRouter",
47
+ "ToolRouter", # Backwards compat alias for MCPRouter
46
48
  "format_result",
47
49
  "generate_mock_value",
48
50
  "parse_tool_call",
@@ -68,6 +68,8 @@ class Connector:
68
68
  self.connection_type = connection_type
69
69
  self.client: FastMCPClient[Any] | None = None
70
70
  self._tools_cache: list[mcp_types.Tool] | None = None
71
+ self._prompts_cache: list[mcp_types.Prompt] | None = None
72
+ self._resources_cache: list[mcp_types.Resource] | None = None
71
73
 
72
74
  def copy(self) -> Connector:
73
75
  """Create a copy of this connector with fresh (unconnected) state.
@@ -101,6 +103,14 @@ class Connector:
101
103
  def cached_tools(self) -> list[mcp_types.Tool]:
102
104
  return self._tools_cache or []
103
105
 
106
+ @property
107
+ def cached_prompts(self) -> list[mcp_types.Prompt]:
108
+ return self._prompts_cache or []
109
+
110
+ @property
111
+ def cached_resources(self) -> list[mcp_types.Resource]:
112
+ return self._resources_cache or []
113
+
104
114
  async def connect(self) -> None:
105
115
  """Create FastMCP client and connect.
106
116
 
@@ -115,14 +125,20 @@ class Connector:
115
125
  await self.client.__aenter__()
116
126
 
117
127
  async def disconnect(self) -> None:
118
- """Disconnect and clear cache."""
128
+ """Disconnect and clear all caches."""
119
129
  if self.client is not None and self.is_connected:
120
130
  await self.client.__aexit__(None, None, None)
121
131
  self.client = None
122
132
  self._tools_cache = None
133
+ self._prompts_cache = None
134
+ self._resources_cache = None
123
135
 
124
136
  async def list_tools(self) -> list[mcp_types.Tool]:
125
- """Fetch tools from server, apply filters/transforms/prefix, and cache."""
137
+ """Fetch tools from server, apply filters/transforms/prefix, and cache.
138
+
139
+ Always fetches fresh data from the server (no caching check).
140
+ The result is cached for use by router.build() via cached_tools property.
141
+ """
126
142
  if self.client is None:
127
143
  raise RuntimeError("Not connected - call connect() first")
128
144
  tools = await self.client.list_tools()
@@ -178,14 +194,26 @@ class Connector:
178
194
  return await self.client.call_tool_mcp(name, arguments or {})
179
195
 
180
196
  async def list_resources(self) -> list[mcp_types.Resource]:
197
+ """Fetch resources from server and cache.
198
+
199
+ Always fetches fresh data from the server (no caching check).
200
+ The result is cached for use by router.build_resources() via cached_resources property.
201
+ """
181
202
  if self.client is None:
182
203
  raise RuntimeError("Not connected - call connect() first")
183
- return await self.client.list_resources()
204
+ self._resources_cache = await self.client.list_resources()
205
+ return self._resources_cache
184
206
 
185
207
  async def list_prompts(self) -> list[mcp_types.Prompt]:
208
+ """Fetch prompts from server and cache.
209
+
210
+ Always fetches fresh data from the server (no caching check).
211
+ The result is cached for use by router.build_prompts() via cached_prompts property.
212
+ """
186
213
  if self.client is None:
187
214
  raise RuntimeError("Not connected - call connect() first")
188
- return await self.client.list_prompts()
215
+ self._prompts_cache = await self.client.list_prompts()
216
+ return self._prompts_cache
189
217
 
190
218
  async def read_resource(
191
219
  self, uri: str
@@ -119,6 +119,26 @@ class Environment(
119
119
 
120
120
  MAX_CONCURRENT_CONNECTIONS = 10
121
121
 
122
+ @staticmethod
123
+ def _normalize_name(name: str) -> str:
124
+ """Normalize environment name to lowercase with hyphens.
125
+
126
+ - Strips whitespace
127
+ - Replaces spaces and underscores with hyphens
128
+ - Lowercases the result
129
+ - Removes any non-alphanumeric characters except hyphens
130
+ """
131
+ import re
132
+
133
+ normalized = name.strip().lower()
134
+ normalized = normalized.replace(" ", "-").replace("_", "-")
135
+ # Keep only alphanumeric and hyphens
136
+ normalized = re.sub(r"[^a-z0-9-]", "", normalized)
137
+ # Collapse multiple hyphens
138
+ normalized = re.sub(r"-+", "-", normalized)
139
+ # Strip leading/trailing hyphens
140
+ return normalized.strip("-") or "environment"
141
+
122
142
  def __init__(
123
143
  self,
124
144
  name: str = "environment",
@@ -126,10 +146,15 @@ class Environment(
126
146
  conflict_resolution: ConflictResolution = ConflictResolution.PREFIX,
127
147
  **fastmcp_kwargs: Any,
128
148
  ) -> None:
149
+ # Normalize name to prevent casing/spacing issues
150
+ name = self._normalize_name(name)
129
151
  super().__init__(name=name, instructions=instructions, **fastmcp_kwargs)
130
152
  self._connections: dict[str, Connector] = {}
131
153
  self._router = ToolRouter(conflict_resolution=conflict_resolution)
132
- self._routing_built = False # Track if _build_routing has been called
154
+ # Granular routing flags - only rebuild what's invalidated
155
+ self._tool_routing_built = False
156
+ self._prompt_routing_built = False
157
+ self._resource_routing_built = False
133
158
  self._in_context = False
134
159
 
135
160
  # Tool call queues - run after connections established
@@ -182,6 +207,10 @@ class Environment(
182
207
 
183
208
  return tools
184
209
 
210
+ def add_tool(self, obj: Any, **kwargs: Any) -> None:
211
+ super().add_tool(obj, **kwargs)
212
+ self._tool_routing_built = False # Only invalidate tool routing
213
+
185
214
  async def call_tool(self, call: Any, /, **kwargs: Any) -> Any:
186
215
  """Call a tool, auto-detecting format and returning matching result format.
187
216
 
@@ -312,7 +341,7 @@ class Environment(
312
341
  """Connect all connectors, build routing, run setup tools."""
313
342
  self._in_context = True
314
343
 
315
- # Connect to all servers (on_connect callbacks run first within connect())
344
+ # Connect to all servers and fetch tools/prompts/resources in parallel
316
345
  sem = asyncio.Semaphore(self.MAX_CONCURRENT_CONNECTIONS)
317
346
  errors: list[tuple[str, Exception]] = []
318
347
 
@@ -320,7 +349,12 @@ class Environment(
320
349
  async with sem:
321
350
  try:
322
351
  await conn.connect()
323
- await conn.list_tools()
352
+ # Batch fetch all MCP primitives in parallel for performance
353
+ await asyncio.gather(
354
+ conn.list_tools(),
355
+ conn.list_prompts(),
356
+ conn.list_resources(),
357
+ )
324
358
  except Exception as e:
325
359
  errors.append((name, e))
326
360
 
@@ -369,7 +403,10 @@ class Environment(
369
403
  if self._connections:
370
404
  await asyncio.gather(*[c.disconnect() for c in self._connections.values()])
371
405
  self._router.clear()
372
- self._routing_built = False
406
+ self._tool_routing_built = False
407
+ self._prompt_routing_built = False
408
+ self._resource_routing_built = False
409
+ self._active_session = None # Clear stale scenario state
373
410
 
374
411
  async def run_async(
375
412
  self,
@@ -388,9 +425,22 @@ class Environment(
388
425
  )
389
426
 
390
427
  async def _build_routing(self) -> None:
428
+ """Build routing for tools, prompts, and resources in parallel.
429
+
430
+ Only rebuilds what's actually invalidated for performance.
431
+ """
432
+ tasks = []
433
+ if not self._tool_routing_built:
434
+ tasks.append(self._build_tool_routing())
435
+ if not self._prompt_routing_built:
436
+ tasks.append(self._build_prompt_routing())
437
+ if not self._resource_routing_built:
438
+ tasks.append(self._build_resource_routing())
439
+ if tasks:
440
+ await asyncio.gather(*tasks)
441
+
442
+ async def _build_tool_routing(self) -> None:
391
443
  """Build tool routing from local tools and connection caches."""
392
- # Use get_tools() not list_tools() - it includes mounted servers without
393
- # requiring MCP server communication (via_server=False)
394
444
  local_tools_dict = await self._tool_manager.get_tools()
395
445
  local_tools = list(local_tools_dict.values())
396
446
  self._router.build(
@@ -398,9 +448,23 @@ class Environment(
398
448
  connections=self._connections,
399
449
  connection_order=list(self._connections.keys()),
400
450
  )
401
- self._routing_built = True
402
451
  # Populate mock schemas for auto-generated mock values
403
452
  self._populate_mock_schemas()
453
+ self._tool_routing_built = True
454
+
455
+ async def _build_prompt_routing(self) -> None:
456
+ """Build prompt routing from local prompts and connections."""
457
+ local_prompts_dict = await self._prompt_manager.get_prompts()
458
+ local_prompts = [p.to_mcp_prompt() for p in local_prompts_dict.values()]
459
+ self._router.build_prompts(local_prompts, self._connections)
460
+ self._prompt_routing_built = True
461
+
462
+ async def _build_resource_routing(self) -> None:
463
+ """Build resource routing from local resources and connections."""
464
+ local_resources_dict = await self._resource_manager.get_resources()
465
+ local_resources = [r.to_mcp_resource() for r in local_resources_dict.values()]
466
+ self._router.build_resources(local_resources, self._connections)
467
+ self._resource_routing_built = True
404
468
 
405
469
  # =========================================================================
406
470
  # MCP Protocol Overrides - Include connector tools in MCP responses
@@ -416,8 +480,8 @@ class Environment(
416
480
 
417
481
  async def _env_list_tools(self) -> list[mcp_types.Tool]:
418
482
  """Return all tools including those from connectors."""
419
- if not self._routing_built:
420
- await self._build_routing()
483
+ if not self._tool_routing_built:
484
+ await self._build_tool_routing()
421
485
  return self._router.tools
422
486
 
423
487
  async def _env_call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> list[Any]:
@@ -430,10 +494,10 @@ class Environment(
430
494
  # =========================================================================
431
495
 
432
496
  async def list_tools(self) -> list[mcp_types.Tool]:
433
- """Refresh tools from all connections and rebuild routing."""
497
+ """Refresh tools from all connections and rebuild tool routing."""
434
498
  if self._connections:
435
499
  await asyncio.gather(*[c.list_tools() for c in self._connections.values()])
436
- await self._build_routing()
500
+ await self._build_tool_routing()
437
501
  return self._router.tools
438
502
 
439
503
  async def _execute_tool(self, name: str, arguments: dict[str, Any]) -> MCPToolResult:
@@ -446,6 +510,10 @@ class Environment(
446
510
  logger.debug("Mock mode: returning mock result for tool %s", name)
447
511
  return self._get_mock_result(name, arguments)
448
512
 
513
+ # Rebuild tool routing if invalidated (e.g., after add_tool)
514
+ if not self._tool_routing_built:
515
+ await self._build_tool_routing()
516
+
449
517
  if self._router.is_local(name):
450
518
  # Call tool manager directly to avoid FastMCP context requirement
451
519
  result = await self._tool_manager.call_tool(name, arguments)
@@ -471,86 +539,83 @@ class Environment(
471
539
  # =========================================================================
472
540
 
473
541
  async def list_resources(self) -> list[mcp_types.Resource]:
474
- """List all resources (local + remote)."""
475
- local = list((await self._resource_manager.get_resources()).values())
476
- resources: list[mcp_types.Resource] = [r.to_mcp_resource() for r in local]
477
-
542
+ """Refresh resources from all connections and rebuild resource routing."""
478
543
  if self._connections:
479
- results = await asyncio.gather(
480
- *[c.list_resources() for c in self._connections.values()], return_exceptions=True
481
- )
482
- for r in results:
483
- if isinstance(r, list):
484
- resources.extend(r)
485
-
486
- return resources
544
+ await asyncio.gather(*[c.list_resources() for c in self._connections.values()])
545
+ await self._build_resource_routing()
546
+ return self._router.resources
487
547
 
488
548
  async def read_resource(
489
549
  self, uri: str
490
550
  ) -> list[mcp_types.TextResourceContents | mcp_types.BlobResourceContents]:
491
- """Read a resource by URI (tries local first, then remote)."""
551
+ """Read a resource by URI using router for connection lookup."""
492
552
  from pydantic import AnyUrl
493
553
 
494
- try:
495
- result = await self._resource_manager.read_resource(uri)
496
- resource_uri = AnyUrl(uri)
497
- if isinstance(result, str):
498
- return [mcp_types.TextResourceContents(uri=resource_uri, text=result)]
499
- import base64
554
+ # Ensure resource routing is built
555
+ if not self._resource_routing_built:
556
+ await self._build_resource_routing()
500
557
 
501
- return [
502
- mcp_types.BlobResourceContents(
503
- uri=resource_uri, blob=base64.b64encode(result).decode()
504
- )
505
- ]
506
- except Exception as e:
507
- logger.debug("Local resource read failed for %s: %s", uri, e)
558
+ # Use router to find which connection has this resource
559
+ conn_name = self._router.get_resource_connection(uri)
508
560
 
509
- for conn in self._connections.values():
561
+ if conn_name is None:
562
+ # Local resource
510
563
  try:
511
- return await conn.read_resource(uri)
564
+ result = await self._resource_manager.read_resource(uri)
565
+ resource_uri = AnyUrl(uri)
566
+ if isinstance(result, str):
567
+ return [mcp_types.TextResourceContents(uri=resource_uri, text=result)]
568
+ import base64
569
+
570
+ return [
571
+ mcp_types.BlobResourceContents(
572
+ uri=resource_uri, blob=base64.b64encode(result).decode()
573
+ )
574
+ ]
512
575
  except Exception as e:
513
- logger.debug("Remote resource read failed for %s: %s", uri, e)
514
- continue
515
-
516
- raise ValueError(f"Resource not found: {uri}")
576
+ logger.debug("Local resource read failed for %s: %s", uri, e)
577
+ raise ValueError(f"Resource not found: {uri}") from e
578
+ else:
579
+ # Remote resource
580
+ conn = self._connections.get(conn_name)
581
+ if conn is None:
582
+ raise ValueError(f"Connection '{conn_name}' not found for resource '{uri}'")
583
+ return await conn.read_resource(uri)
517
584
 
518
585
  # =========================================================================
519
586
  # Prompt Operations
520
587
  # =========================================================================
521
588
 
522
589
  async def list_prompts(self) -> list[mcp_types.Prompt]:
523
- """List all prompts (local + remote)."""
524
- local = list((await self._prompt_manager.get_prompts()).values())
525
- prompts: list[mcp_types.Prompt] = [p.to_mcp_prompt() for p in local]
526
-
590
+ """Refresh prompts from all connections and rebuild prompt routing."""
527
591
  if self._connections:
528
- results = await asyncio.gather(
529
- *[c.list_prompts() for c in self._connections.values()], return_exceptions=True
530
- )
531
- for r in results:
532
- if isinstance(r, list):
533
- prompts.extend(r)
534
-
535
- return prompts
592
+ await asyncio.gather(*[c.list_prompts() for c in self._connections.values()])
593
+ await self._build_prompt_routing()
594
+ return self._router.prompts
536
595
 
537
596
  async def get_prompt(
538
597
  self, name: str, arguments: dict[str, Any] | None = None
539
598
  ) -> mcp_types.GetPromptResult:
540
- """Get a prompt by name (tries local first, then remote)."""
541
- try:
542
- return await self._prompt_manager.render_prompt(name, arguments or {})
543
- except Exception as e:
544
- logger.debug("Local prompt render failed for %s: %s", name, e)
599
+ """Get a prompt by name using router for connection lookup."""
600
+ # Ensure prompt routing is built
601
+ if not self._prompt_routing_built:
602
+ await self._build_prompt_routing()
603
+
604
+ # Use router to find which connection has this prompt
605
+ conn_name = self._router.get_prompt_connection(name)
545
606
 
546
- for conn in self._connections.values():
607
+ if conn_name is None:
608
+ # Local prompt
547
609
  try:
548
- return await conn.get_prompt(name, arguments)
610
+ return await self._prompt_manager.render_prompt(name, arguments or {})
549
611
  except Exception as e:
550
- logger.debug("Remote prompt get failed for %s: %s", name, e)
551
- continue
552
-
553
- raise ValueError(f"Prompt not found: {name}")
612
+ raise ValueError(f"Prompt not found: {name}") from e
613
+ else:
614
+ # Remote prompt
615
+ conn = self._connections.get(conn_name)
616
+ if conn is None:
617
+ raise ValueError(f"Connection '{conn_name}' not found for prompt '{name}'")
618
+ return await conn.get_prompt(name, arguments)
554
619
 
555
620
  # =========================================================================
556
621
  # Server Methods
@@ -602,7 +667,7 @@ class Environment(
602
667
  For v4 format: requires mcp_config, prompt, AND evaluate_tool
603
668
  """
604
669
  # Check for local tools (registered via @env.tool)
605
- if self._router._local_names:
670
+ if self._router._local_tool_names:
606
671
  return False
607
672
  # Check for local scenarios (registered via @env.scenario)
608
673
  if getattr(self, "_scenarios", {}):
@@ -639,10 +704,10 @@ class Environment(
639
704
  task.env.to_config() # {"prompt": "...", "mcp_config": {...}, ...}
640
705
  ```
641
706
  """
642
- if self._router._local_names:
707
+ if self._router._local_tool_names:
643
708
  raise ValueError(
644
709
  f"Cannot serialize Environment with local tools: "
645
- f"{list(self._router._local_names)}. "
710
+ f"{list(self._router._local_tool_names)}. "
646
711
  "Local tools require local execution. For remote submission, "
647
712
  "use dict config or connect to a remote hub."
648
713
  )