hud-python 0.4.45__py3-none-any.whl → 0.5.1__py3-none-any.whl

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 (274) hide show
  1. hud/__init__.py +27 -7
  2. hud/agents/__init__.py +11 -5
  3. hud/agents/base.py +220 -500
  4. hud/agents/claude.py +200 -240
  5. hud/agents/gemini.py +275 -0
  6. hud/agents/gemini_cua.py +335 -0
  7. hud/agents/grounded_openai.py +98 -100
  8. hud/agents/misc/integration_test_agent.py +51 -20
  9. hud/agents/misc/response_agent.py +41 -36
  10. hud/agents/openai.py +291 -292
  11. hud/agents/{openai_chat_generic.py → openai_chat.py} +80 -34
  12. hud/agents/operator.py +211 -0
  13. hud/agents/tests/conftest.py +133 -0
  14. hud/agents/tests/test_base.py +300 -622
  15. hud/agents/tests/test_base_runtime.py +233 -0
  16. hud/agents/tests/test_claude.py +379 -210
  17. hud/agents/tests/test_client.py +9 -10
  18. hud/agents/tests/test_gemini.py +369 -0
  19. hud/agents/tests/test_grounded_openai_agent.py +65 -50
  20. hud/agents/tests/test_openai.py +376 -140
  21. hud/agents/tests/test_operator.py +362 -0
  22. hud/agents/tests/test_run_eval.py +179 -0
  23. hud/cli/__init__.py +461 -545
  24. hud/cli/analyze.py +43 -5
  25. hud/cli/build.py +664 -110
  26. hud/cli/debug.py +8 -5
  27. hud/cli/dev.py +882 -734
  28. hud/cli/eval.py +782 -668
  29. hud/cli/flows/dev.py +167 -0
  30. hud/cli/flows/init.py +191 -0
  31. hud/cli/flows/tasks.py +153 -56
  32. hud/cli/flows/templates.py +151 -0
  33. hud/cli/flows/tests/__init__.py +1 -0
  34. hud/cli/flows/tests/test_dev.py +126 -0
  35. hud/cli/init.py +60 -58
  36. hud/cli/push.py +29 -11
  37. hud/cli/rft.py +311 -0
  38. hud/cli/rft_status.py +145 -0
  39. hud/cli/tests/test_analyze.py +5 -5
  40. hud/cli/tests/test_analyze_metadata.py +3 -2
  41. hud/cli/tests/test_analyze_module.py +120 -0
  42. hud/cli/tests/test_build.py +108 -6
  43. hud/cli/tests/test_build_failure.py +41 -0
  44. hud/cli/tests/test_build_module.py +50 -0
  45. hud/cli/tests/test_cli_init.py +6 -1
  46. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  47. hud/cli/tests/test_cli_root.py +140 -0
  48. hud/cli/tests/test_convert.py +361 -0
  49. hud/cli/tests/test_debug.py +12 -10
  50. hud/cli/tests/test_dev.py +197 -0
  51. hud/cli/tests/test_eval.py +251 -0
  52. hud/cli/tests/test_eval_bedrock.py +51 -0
  53. hud/cli/tests/test_init.py +124 -0
  54. hud/cli/tests/test_main_module.py +11 -5
  55. hud/cli/tests/test_mcp_server.py +12 -100
  56. hud/cli/tests/test_push_happy.py +74 -0
  57. hud/cli/tests/test_push_wrapper.py +23 -0
  58. hud/cli/tests/test_registry.py +1 -1
  59. hud/cli/tests/test_utils.py +1 -1
  60. hud/cli/{rl → utils}/celebrate.py +14 -12
  61. hud/cli/utils/config.py +18 -1
  62. hud/cli/utils/docker.py +130 -4
  63. hud/cli/utils/env_check.py +9 -9
  64. hud/cli/utils/git.py +136 -0
  65. hud/cli/utils/interactive.py +39 -5
  66. hud/cli/utils/metadata.py +69 -0
  67. hud/cli/utils/runner.py +1 -1
  68. hud/cli/utils/server.py +2 -2
  69. hud/cli/utils/source_hash.py +3 -3
  70. hud/cli/utils/tasks.py +4 -1
  71. hud/cli/utils/tests/__init__.py +0 -0
  72. hud/cli/utils/tests/test_config.py +58 -0
  73. hud/cli/utils/tests/test_docker.py +93 -0
  74. hud/cli/utils/tests/test_docker_hints.py +71 -0
  75. hud/cli/utils/tests/test_env_check.py +74 -0
  76. hud/cli/utils/tests/test_environment.py +42 -0
  77. hud/cli/utils/tests/test_git.py +142 -0
  78. hud/cli/utils/tests/test_interactive_module.py +60 -0
  79. hud/cli/utils/tests/test_local_runner.py +50 -0
  80. hud/cli/utils/tests/test_logging_utils.py +23 -0
  81. hud/cli/utils/tests/test_metadata.py +49 -0
  82. hud/cli/utils/tests/test_package_runner.py +35 -0
  83. hud/cli/utils/tests/test_registry_utils.py +49 -0
  84. hud/cli/utils/tests/test_remote_runner.py +25 -0
  85. hud/cli/utils/tests/test_runner_modules.py +52 -0
  86. hud/cli/utils/tests/test_source_hash.py +36 -0
  87. hud/cli/utils/tests/test_tasks.py +80 -0
  88. hud/cli/utils/version_check.py +258 -0
  89. hud/cli/{rl → utils}/viewer.py +2 -2
  90. hud/clients/README.md +12 -11
  91. hud/clients/__init__.py +4 -3
  92. hud/clients/base.py +166 -26
  93. hud/clients/environment.py +51 -0
  94. hud/clients/fastmcp.py +13 -6
  95. hud/clients/mcp_use.py +40 -15
  96. hud/clients/tests/test_analyze_scenarios.py +206 -0
  97. hud/clients/tests/test_protocol.py +9 -3
  98. hud/datasets/__init__.py +23 -20
  99. hud/datasets/loader.py +327 -0
  100. hud/datasets/runner.py +192 -105
  101. hud/datasets/tests/__init__.py +0 -0
  102. hud/datasets/tests/test_loader.py +221 -0
  103. hud/datasets/tests/test_utils.py +315 -0
  104. hud/datasets/utils.py +270 -90
  105. hud/environment/__init__.py +50 -0
  106. hud/environment/connection.py +206 -0
  107. hud/environment/connectors/__init__.py +33 -0
  108. hud/environment/connectors/base.py +68 -0
  109. hud/environment/connectors/local.py +177 -0
  110. hud/environment/connectors/mcp_config.py +109 -0
  111. hud/environment/connectors/openai.py +101 -0
  112. hud/environment/connectors/remote.py +172 -0
  113. hud/environment/environment.py +694 -0
  114. hud/environment/integrations/__init__.py +45 -0
  115. hud/environment/integrations/adk.py +67 -0
  116. hud/environment/integrations/anthropic.py +196 -0
  117. hud/environment/integrations/gemini.py +92 -0
  118. hud/environment/integrations/langchain.py +82 -0
  119. hud/environment/integrations/llamaindex.py +68 -0
  120. hud/environment/integrations/openai.py +238 -0
  121. hud/environment/mock.py +306 -0
  122. hud/environment/router.py +112 -0
  123. hud/environment/scenarios.py +493 -0
  124. hud/environment/tests/__init__.py +1 -0
  125. hud/environment/tests/test_connection.py +317 -0
  126. hud/environment/tests/test_connectors.py +218 -0
  127. hud/environment/tests/test_environment.py +161 -0
  128. hud/environment/tests/test_integrations.py +257 -0
  129. hud/environment/tests/test_local_connectors.py +201 -0
  130. hud/environment/tests/test_scenarios.py +280 -0
  131. hud/environment/tests/test_tools.py +208 -0
  132. hud/environment/types.py +23 -0
  133. hud/environment/utils/__init__.py +35 -0
  134. hud/environment/utils/formats.py +215 -0
  135. hud/environment/utils/schema.py +171 -0
  136. hud/environment/utils/tool_wrappers.py +113 -0
  137. hud/eval/__init__.py +67 -0
  138. hud/eval/context.py +674 -0
  139. hud/eval/display.py +299 -0
  140. hud/eval/instrument.py +185 -0
  141. hud/eval/manager.py +466 -0
  142. hud/eval/parallel.py +268 -0
  143. hud/eval/task.py +340 -0
  144. hud/eval/tests/__init__.py +1 -0
  145. hud/eval/tests/test_context.py +178 -0
  146. hud/eval/tests/test_eval.py +210 -0
  147. hud/eval/tests/test_manager.py +152 -0
  148. hud/eval/tests/test_parallel.py +168 -0
  149. hud/eval/tests/test_task.py +145 -0
  150. hud/eval/types.py +63 -0
  151. hud/eval/utils.py +183 -0
  152. hud/patches/__init__.py +19 -0
  153. hud/patches/mcp_patches.py +151 -0
  154. hud/patches/warnings.py +54 -0
  155. hud/samples/browser.py +4 -4
  156. hud/server/__init__.py +2 -1
  157. hud/server/low_level.py +2 -1
  158. hud/server/router.py +164 -0
  159. hud/server/server.py +567 -80
  160. hud/server/tests/test_mcp_server_integration.py +11 -11
  161. hud/server/tests/test_mcp_server_more.py +1 -1
  162. hud/server/tests/test_server_extra.py +2 -0
  163. hud/settings.py +45 -3
  164. hud/shared/exceptions.py +36 -10
  165. hud/shared/hints.py +26 -1
  166. hud/shared/requests.py +15 -3
  167. hud/shared/tests/test_exceptions.py +40 -31
  168. hud/shared/tests/test_hints.py +167 -0
  169. hud/telemetry/__init__.py +20 -19
  170. hud/telemetry/exporter.py +201 -0
  171. hud/telemetry/instrument.py +158 -253
  172. hud/telemetry/tests/test_eval_telemetry.py +356 -0
  173. hud/telemetry/tests/test_exporter.py +258 -0
  174. hud/telemetry/tests/test_instrument.py +401 -0
  175. hud/tools/__init__.py +16 -2
  176. hud/tools/apply_patch.py +639 -0
  177. hud/tools/base.py +54 -4
  178. hud/tools/bash.py +2 -2
  179. hud/tools/computer/__init__.py +4 -0
  180. hud/tools/computer/anthropic.py +2 -2
  181. hud/tools/computer/gemini.py +385 -0
  182. hud/tools/computer/hud.py +23 -6
  183. hud/tools/computer/openai.py +20 -21
  184. hud/tools/computer/qwen.py +434 -0
  185. hud/tools/computer/settings.py +37 -0
  186. hud/tools/edit.py +3 -7
  187. hud/tools/executors/base.py +4 -2
  188. hud/tools/executors/pyautogui.py +1 -1
  189. hud/tools/grounding/grounded_tool.py +13 -18
  190. hud/tools/grounding/grounder.py +10 -31
  191. hud/tools/grounding/tests/test_grounded_tool.py +26 -44
  192. hud/tools/jupyter.py +330 -0
  193. hud/tools/playwright.py +18 -3
  194. hud/tools/shell.py +308 -0
  195. hud/tools/tests/test_apply_patch.py +718 -0
  196. hud/tools/tests/test_computer.py +4 -9
  197. hud/tools/tests/test_computer_actions.py +24 -2
  198. hud/tools/tests/test_jupyter_tool.py +181 -0
  199. hud/tools/tests/test_shell.py +596 -0
  200. hud/tools/tests/test_submit.py +85 -0
  201. hud/tools/tests/test_types.py +193 -0
  202. hud/tools/types.py +21 -1
  203. hud/types.py +167 -57
  204. hud/utils/__init__.py +2 -0
  205. hud/utils/env.py +67 -0
  206. hud/utils/hud_console.py +61 -3
  207. hud/utils/mcp.py +15 -58
  208. hud/utils/strict_schema.py +162 -0
  209. hud/utils/tests/test_init.py +1 -2
  210. hud/utils/tests/test_mcp.py +1 -28
  211. hud/utils/tests/test_pretty_errors.py +186 -0
  212. hud/utils/tests/test_tool_shorthand.py +154 -0
  213. hud/utils/tests/test_version.py +1 -1
  214. hud/utils/types.py +20 -0
  215. hud/version.py +1 -1
  216. hud_python-0.5.1.dist-info/METADATA +264 -0
  217. hud_python-0.5.1.dist-info/RECORD +299 -0
  218. {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/WHEEL +1 -1
  219. hud/agents/langchain.py +0 -261
  220. hud/agents/lite_llm.py +0 -72
  221. hud/cli/rl/__init__.py +0 -180
  222. hud/cli/rl/config.py +0 -101
  223. hud/cli/rl/display.py +0 -133
  224. hud/cli/rl/gpu.py +0 -63
  225. hud/cli/rl/gpu_utils.py +0 -321
  226. hud/cli/rl/local_runner.py +0 -595
  227. hud/cli/rl/presets.py +0 -96
  228. hud/cli/rl/remote_runner.py +0 -463
  229. hud/cli/rl/rl_api.py +0 -150
  230. hud/cli/rl/vllm.py +0 -177
  231. hud/cli/rl/wait_utils.py +0 -89
  232. hud/datasets/parallel.py +0 -687
  233. hud/misc/__init__.py +0 -1
  234. hud/misc/claude_plays_pokemon.py +0 -292
  235. hud/otel/__init__.py +0 -35
  236. hud/otel/collector.py +0 -142
  237. hud/otel/config.py +0 -181
  238. hud/otel/context.py +0 -570
  239. hud/otel/exporters.py +0 -369
  240. hud/otel/instrumentation.py +0 -135
  241. hud/otel/processors.py +0 -121
  242. hud/otel/tests/__init__.py +0 -1
  243. hud/otel/tests/test_processors.py +0 -197
  244. hud/rl/README.md +0 -30
  245. hud/rl/__init__.py +0 -1
  246. hud/rl/actor.py +0 -176
  247. hud/rl/buffer.py +0 -405
  248. hud/rl/chat_template.jinja +0 -101
  249. hud/rl/config.py +0 -192
  250. hud/rl/distributed.py +0 -132
  251. hud/rl/learner.py +0 -637
  252. hud/rl/tests/__init__.py +0 -1
  253. hud/rl/tests/test_learner.py +0 -186
  254. hud/rl/train.py +0 -382
  255. hud/rl/types.py +0 -101
  256. hud/rl/utils/start_vllm_server.sh +0 -30
  257. hud/rl/utils.py +0 -524
  258. hud/rl/vllm_adapter.py +0 -143
  259. hud/telemetry/job.py +0 -352
  260. hud/telemetry/replay.py +0 -74
  261. hud/telemetry/tests/test_replay.py +0 -40
  262. hud/telemetry/tests/test_trace.py +0 -63
  263. hud/telemetry/trace.py +0 -158
  264. hud/utils/agent_factories.py +0 -86
  265. hud/utils/async_utils.py +0 -65
  266. hud/utils/group_eval.py +0 -223
  267. hud/utils/progress.py +0 -149
  268. hud/utils/tasks.py +0 -127
  269. hud/utils/tests/test_async_utils.py +0 -173
  270. hud/utils/tests/test_progress.py +0 -261
  271. hud_python-0.4.45.dist-info/METADATA +0 -552
  272. hud_python-0.4.45.dist-info/RECORD +0 -228
  273. {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/entry_points.txt +0 -0
  274. {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/licenses/LICENSE +0 -0
hud/otel/context.py DELETED
@@ -1,570 +0,0 @@
1
- """OpenTelemetry context utilities for HUD telemetry.
2
-
3
- This module provides internal utilities for managing OpenTelemetry contexts.
4
- User-facing APIs are in hud.telemetry.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import contextvars
10
- import logging
11
- from contextlib import contextmanager
12
- from typing import TYPE_CHECKING, Any
13
-
14
- from opentelemetry import baggage, context
15
- from opentelemetry import trace as otel_trace
16
- from opentelemetry.trace import Status, StatusCode
17
-
18
- if TYPE_CHECKING:
19
- from collections.abc import Generator
20
- from types import TracebackType
21
-
22
- from hud.settings import settings
23
- from hud.shared import make_request, make_request_sync
24
- from hud.utils.async_utils import fire_and_forget
25
-
26
- logger = logging.getLogger(__name__)
27
-
28
- # Context variables for task tracking
29
- current_task_run_id: contextvars.ContextVar[str | None] = contextvars.ContextVar(
30
- "current_task_run_id", default=None
31
- )
32
- is_root_trace_var: contextvars.ContextVar[bool] = contextvars.ContextVar(
33
- "is_root_trace", default=False
34
- )
35
-
36
- # Step counters for different types
37
- current_base_mcp_steps: contextvars.ContextVar[int] = contextvars.ContextVar(
38
- "current_base_mcp_steps", default=0
39
- )
40
- current_mcp_tool_steps: contextvars.ContextVar[int] = contextvars.ContextVar(
41
- "current_mcp_tool_steps", default=0
42
- )
43
- current_agent_steps: contextvars.ContextVar[int] = contextvars.ContextVar(
44
- "current_agent_steps", default=0
45
- )
46
-
47
- # Keys for OpenTelemetry baggage
48
- TASK_RUN_ID_KEY = "hud.task_run_id"
49
- IS_ROOT_TRACE_KEY = "hud.is_root_trace"
50
- BASE_MCP_STEPS_KEY = "hud.base_mcp_steps"
51
- MCP_TOOL_STEPS_KEY = "hud.mcp_tool_steps"
52
- AGENT_STEPS_KEY = "hud.agent_steps"
53
-
54
-
55
- def set_current_task_run_id(task_run_id: str | None) -> contextvars.Token:
56
- """Set the current task run ID."""
57
- return current_task_run_id.set(task_run_id)
58
-
59
-
60
- def get_current_task_run_id() -> str | None:
61
- """Get current task_run_id from either contextvars or OTel baggage."""
62
- # First try OTel baggage
63
- task_run_id = baggage.get_baggage(TASK_RUN_ID_KEY)
64
- if task_run_id and isinstance(task_run_id, str):
65
- return task_run_id
66
-
67
- # Fallback to contextvars
68
- return current_task_run_id.get()
69
-
70
-
71
- def is_root_trace() -> bool:
72
- """Check if current context is a root trace."""
73
- # First try OTel baggage
74
- is_root = baggage.get_baggage(IS_ROOT_TRACE_KEY)
75
- if isinstance(is_root, str):
76
- return is_root.lower() == "true"
77
-
78
- # Fallback to contextvars
79
- return is_root_trace_var.get()
80
-
81
-
82
- def get_base_mcp_steps() -> int:
83
- """Get current base MCP step count from either contextvars or OTel baggage."""
84
- # First try OTel baggage
85
- step_count = baggage.get_baggage(BASE_MCP_STEPS_KEY)
86
- if step_count and isinstance(step_count, str):
87
- try:
88
- return int(step_count)
89
- except ValueError:
90
- pass
91
-
92
- # Fallback to contextvars
93
- return current_base_mcp_steps.get()
94
-
95
-
96
- def get_mcp_tool_steps() -> int:
97
- """Get current MCP tool step count from either contextvars or OTel baggage."""
98
- # First try OTel baggage
99
- step_count = baggage.get_baggage(MCP_TOOL_STEPS_KEY)
100
- if step_count and isinstance(step_count, str):
101
- try:
102
- return int(step_count)
103
- except ValueError:
104
- pass
105
-
106
- # Fallback to contextvars
107
- return current_mcp_tool_steps.get()
108
-
109
-
110
- def get_agent_steps() -> int:
111
- """Get current agent step count from either contextvars or OTel baggage."""
112
- # First try OTel baggage
113
- step_count = baggage.get_baggage(AGENT_STEPS_KEY)
114
- if step_count and isinstance(step_count, str):
115
- try:
116
- return int(step_count)
117
- except ValueError:
118
- pass
119
-
120
- # Fallback to contextvars
121
- return current_agent_steps.get()
122
-
123
-
124
- def increment_base_mcp_steps() -> int:
125
- """Increment the base MCP step count and update baggage.
126
-
127
- Returns:
128
- The new base MCP step count after incrementing
129
- """
130
- current = get_base_mcp_steps()
131
- new_count = current + 1
132
-
133
- # Update contextvar
134
- current_base_mcp_steps.set(new_count)
135
-
136
- # Update baggage for propagation
137
- ctx = baggage.set_baggage(BASE_MCP_STEPS_KEY, str(new_count))
138
- context.attach(ctx)
139
-
140
- # Update current span if one exists
141
- span = otel_trace.get_current_span()
142
- if span and span.is_recording():
143
- span.set_attribute("hud.base_mcp_steps", new_count)
144
-
145
- return new_count
146
-
147
-
148
- def increment_mcp_tool_steps() -> int:
149
- """Increment the MCP tool step count and update baggage.
150
-
151
- Returns:
152
- The new MCP tool step count after incrementing
153
- """
154
- current = get_mcp_tool_steps()
155
- new_count = current + 1
156
-
157
- # Update contextvar
158
- current_mcp_tool_steps.set(new_count)
159
-
160
- # Update baggage for propagation
161
- ctx = baggage.set_baggage(MCP_TOOL_STEPS_KEY, str(new_count))
162
- context.attach(ctx)
163
-
164
- # Update current span if one exists
165
- span = otel_trace.get_current_span()
166
- if span and span.is_recording():
167
- span.set_attribute("hud.mcp_tool_steps", new_count)
168
-
169
- return new_count
170
-
171
-
172
- def increment_agent_steps() -> int:
173
- """Increment the agent step count and update baggage.
174
-
175
- Returns:
176
- The new agent step count after incrementing
177
- """
178
- current = get_agent_steps()
179
- new_count = current + 1
180
-
181
- # Update contextvar
182
- current_agent_steps.set(new_count)
183
-
184
- # Update baggage for propagation
185
- ctx = baggage.set_baggage(AGENT_STEPS_KEY, str(new_count))
186
- context.attach(ctx)
187
-
188
- # Update current span if one exists
189
- span = otel_trace.get_current_span()
190
- if span and span.is_recording():
191
- span.set_attribute("hud.agent_steps", new_count)
192
-
193
- return new_count
194
-
195
-
196
- @contextmanager
197
- def span_context(
198
- name: str,
199
- attributes: dict[str, Any] | None = None,
200
- kind: otel_trace.SpanKind = otel_trace.SpanKind.INTERNAL,
201
- ) -> Generator[otel_trace.Span, None, None]:
202
- """Create a child span within the current trace context.
203
-
204
- This is a simple wrapper around OpenTelemetry's span creation that
205
- ensures the span inherits the current HUD context (task_run_id, etc).
206
-
207
- Args:
208
- name: Name for the span
209
- attributes: Additional attributes to add to the span
210
- kind: OpenTelemetry span kind
211
-
212
- Example:
213
- with span_context("process_data", {"items": 100}) as span:
214
- # Process data...
215
- span.set_attribute("processed", True)
216
- """
217
- tracer = otel_trace.get_tracer("hud-sdk")
218
-
219
- # Current task_run_id will be added by HudEnrichmentProcessor
220
- with tracer.start_as_current_span(
221
- name,
222
- attributes=attributes,
223
- kind=kind,
224
- ) as span:
225
- yield span
226
-
227
-
228
- async def _update_task_status_async(
229
- task_run_id: str,
230
- status: str,
231
- job_id: str | None = None,
232
- error_message: str | None = None,
233
- trace_name: str | None = None,
234
- task_id: str | None = None,
235
- ) -> None:
236
- """Async task status update."""
237
- if not settings.telemetry_enabled:
238
- return
239
-
240
- try:
241
- data: dict[str, Any] = {"status": status}
242
-
243
- # Resolve effective job_id from explicit param, OTel baggage, or current job context
244
- effective_job_id: str | None = job_id
245
- if not effective_job_id:
246
- bj = baggage.get_baggage("hud.job_id")
247
- if isinstance(bj, str) and bj:
248
- effective_job_id = bj
249
- if not effective_job_id:
250
- try:
251
- from hud.telemetry.job import get_current_job # Local import to avoid cycles
252
-
253
- current_job = get_current_job()
254
- if current_job:
255
- effective_job_id = current_job.id
256
- except Exception:
257
- effective_job_id = None
258
-
259
- if effective_job_id:
260
- data["job_id"] = effective_job_id
261
- if error_message:
262
- data["error_message"] = error_message
263
-
264
- # Build metadata with trace name and step counts
265
- metadata = {}
266
- if trace_name:
267
- metadata["trace_name"] = trace_name
268
-
269
- # Include all three step counts in metadata
270
- metadata["base_mcp_steps"] = get_base_mcp_steps()
271
- metadata["mcp_tool_steps"] = get_mcp_tool_steps()
272
- metadata["agent_steps"] = get_agent_steps()
273
-
274
- if metadata:
275
- data["metadata"] = metadata
276
-
277
- if task_id:
278
- data["task_id"] = task_id
279
-
280
- await make_request(
281
- method="POST",
282
- url=f"{settings.hud_telemetry_url}/trace/{task_run_id}/status",
283
- json=data,
284
- api_key=settings.api_key,
285
- )
286
- logger.debug("Updated task %s status to %s", task_run_id, status)
287
- except Exception as e:
288
- # Suppress warnings about interpreter shutdown
289
- if "interpreter shutdown" not in str(e):
290
- logger.warning("Failed to update task status: %s", e)
291
-
292
-
293
- def _fire_and_forget_status_update(
294
- task_run_id: str,
295
- status: str,
296
- job_id: str | None = None,
297
- error_message: str | None = None,
298
- trace_name: str | None = None,
299
- task_id: str | None = None,
300
- ) -> None:
301
- """Fire and forget status update - works in any context including Jupyter."""
302
- fire_and_forget(
303
- _update_task_status_async(task_run_id, status, job_id, error_message, trace_name, task_id),
304
- f"update task {task_run_id} status to {status}",
305
- )
306
-
307
-
308
- def _update_task_status_sync(
309
- task_run_id: str,
310
- status: str,
311
- job_id: str | None = None,
312
- error_message: str | None = None,
313
- trace_name: str | None = None,
314
- task_id: str | None = None,
315
- ) -> None:
316
- """Synchronous task status update."""
317
- if not settings.telemetry_enabled:
318
- return
319
-
320
- try:
321
- data: dict[str, Any] = {"status": status}
322
-
323
- # Resolve effective job_id from explicit param, OTel baggage, or current job context
324
- effective_job_id: str | None = job_id
325
- if not effective_job_id:
326
- bj = baggage.get_baggage("hud.job_id")
327
- if isinstance(bj, str) and bj:
328
- effective_job_id = bj
329
- if not effective_job_id:
330
- try:
331
- from hud.telemetry.job import get_current_job # Local import to avoid cycles
332
-
333
- current_job = get_current_job()
334
- if current_job:
335
- effective_job_id = current_job.id
336
- except Exception:
337
- effective_job_id = None
338
-
339
- if effective_job_id:
340
- data["job_id"] = effective_job_id
341
- if error_message:
342
- data["error_message"] = error_message
343
-
344
- # Build metadata with trace name and step counts
345
- metadata = {}
346
- if trace_name:
347
- metadata["trace_name"] = trace_name
348
-
349
- # Include all three step counts in metadata
350
- metadata["base_mcp_steps"] = get_base_mcp_steps()
351
- metadata["mcp_tool_steps"] = get_mcp_tool_steps()
352
- metadata["agent_steps"] = get_agent_steps()
353
-
354
- if metadata:
355
- data["metadata"] = metadata
356
-
357
- if task_id:
358
- data["task_id"] = task_id
359
-
360
- make_request_sync(
361
- method="POST",
362
- url=f"{settings.hud_telemetry_url}/trace/{task_run_id}/status",
363
- json=data,
364
- api_key=settings.api_key,
365
- )
366
- logger.debug("Updated task %s status to %s", task_run_id, status)
367
- except Exception as e:
368
- # Suppress warnings about interpreter shutdown
369
- if "interpreter shutdown" not in str(e):
370
- logger.warning("Failed to update task status: %s", e)
371
-
372
-
373
- def _print_trace_url(task_run_id: str) -> None:
374
- """Print the trace URL in a colorful box."""
375
- # Only print HUD URL if HUD telemetry is enabled and has API key
376
- if not (settings.telemetry_enabled and settings.api_key):
377
- return
378
-
379
- url = f"https://hud.so/trace/{task_run_id}"
380
- header = "🚀 See your agent live at:"
381
-
382
- # ANSI color codes
383
- DIM = "\033[90m" # Dim/Gray for border (visible on both light and dark terminals)
384
- GOLD = "\033[33m" # Gold/Yellow for URL
385
- RESET = "\033[0m"
386
- BOLD = "\033[1m"
387
-
388
- # Calculate box width based on the longest line
389
- box_width = max(len(url), len(header)) + 6
390
-
391
- # Box drawing characters
392
- top_border = "╔" + "═" * (box_width - 2) + "╗"
393
- bottom_border = "╚" + "═" * (box_width - 2) + "╝"
394
- divider = "╟" + "─" * (box_width - 2) + "╢"
395
-
396
- # Center the content
397
- header_padding = (box_width - len(header) - 2) // 2
398
- url_padding = (box_width - len(url) - 2) // 2
399
-
400
- # Print the box
401
- print(f"\n{DIM}{top_border}{RESET}") # noqa: T201
402
- print( # noqa: T201
403
- f"{DIM}║{RESET}{' ' * header_padding}{header}{' ' * (box_width - len(header) - header_padding - 3)}{DIM}║{RESET}" # noqa: E501
404
- )
405
- print(f"{DIM}{divider}{RESET}") # noqa: T201
406
- print( # noqa: T201
407
- f"{DIM}║{RESET}{' ' * url_padding}{BOLD}{GOLD}{url}{RESET}{' ' * (box_width - len(url) - url_padding - 2)}{DIM}║{RESET}" # noqa: E501
408
- )
409
- print(f"{DIM}{bottom_border}{RESET}\n") # noqa: T201
410
-
411
-
412
- def _print_trace_complete_url(task_run_id: str, error_occurred: bool = False) -> None:
413
- """Print the trace completion URL with appropriate messaging."""
414
- # Only print HUD URL if HUD telemetry is enabled and has API key
415
- if not (settings.telemetry_enabled and settings.api_key):
416
- return
417
-
418
- url = f"https://hud.so/trace/{task_run_id}"
419
-
420
- # ANSI color codes
421
- GREEN = "\033[92m"
422
- RED = "\033[91m"
423
- GOLD = "\033[33m"
424
- RESET = "\033[0m"
425
- DIM = "\033[2m"
426
- BOLD = "\033[1m"
427
-
428
- if error_occurred:
429
- print( # noqa: T201
430
- f"\n{RED}✗ Trace errored!{RESET} {DIM}More error details available at:{RESET} {BOLD}{GOLD}{url}{RESET}\n" # noqa: E501
431
- )
432
- else:
433
- print(f"\n{GREEN}✓ Trace complete!{RESET} {DIM}View at:{RESET} {BOLD}{GOLD}{url}{RESET}\n") # noqa: T201
434
-
435
-
436
- class trace:
437
- """Internal OpenTelemetry trace context manager.
438
-
439
- This is the implementation class. Users should use hud.trace() instead.
440
- """
441
-
442
- def __init__(
443
- self,
444
- task_run_id: str,
445
- is_root: bool = True,
446
- span_name: str = "hud.task",
447
- attributes: dict[str, Any] | None = None,
448
- job_id: str | None = None,
449
- task_id: str | None = None,
450
- ) -> None:
451
- self.task_run_id = task_run_id
452
- self.job_id = job_id
453
- self.task_id = task_id
454
- self.is_root = is_root
455
- self.span_name = span_name
456
- self.attributes = attributes or {}
457
- self._span: otel_trace.Span | None = None
458
- self._span_manager: Any | None = None
459
- self._otel_token: object | None = None
460
- self._task_run_token = None
461
- self._root_token = None
462
-
463
- def __enter__(self) -> str:
464
- """Enter the trace context and return the task_run_id."""
465
- # Set context variables
466
- self._task_run_token = set_current_task_run_id(self.task_run_id)
467
- self._root_token = is_root_trace_var.set(self.is_root)
468
-
469
- # Set OpenTelemetry baggage for propagation
470
- ctx = baggage.set_baggage(TASK_RUN_ID_KEY, self.task_run_id)
471
- ctx = baggage.set_baggage(IS_ROOT_TRACE_KEY, str(self.is_root), context=ctx)
472
- if self.job_id:
473
- ctx = baggage.set_baggage("hud.job_id", self.job_id, context=ctx)
474
- if self.task_id:
475
- ctx = baggage.set_baggage("hud.task_id", self.task_id, context=ctx)
476
- self._otel_token = context.attach(ctx)
477
-
478
- # Start a span as current
479
- tracer = otel_trace.get_tracer("hud-sdk")
480
- span_attrs = {
481
- "hud.task_run_id": self.task_run_id,
482
- "hud.is_root_trace": self.is_root,
483
- **self.attributes,
484
- }
485
- if self.job_id:
486
- span_attrs["hud.job_id"] = self.job_id
487
- if self.task_id:
488
- span_attrs["hud.task_id"] = self.task_id
489
-
490
- # Use start_as_current_span context manager
491
- self._span_manager = tracer.start_as_current_span(
492
- self.span_name,
493
- attributes=span_attrs,
494
- )
495
- self._span = self._span_manager.__enter__()
496
-
497
- # Update task status to running if root (only for HUD backend)
498
- if self.is_root and settings.telemetry_enabled and settings.api_key:
499
- _fire_and_forget_status_update(
500
- self.task_run_id,
501
- "running",
502
- job_id=self.job_id,
503
- trace_name=self.span_name,
504
- task_id=self.task_id,
505
- )
506
- # Print the nice trace URL box (only if not part of a job)
507
- if not self.job_id:
508
- _print_trace_url(self.task_run_id)
509
-
510
- logger.debug("Started HUD trace context for task_run_id=%s", self.task_run_id)
511
- return self.task_run_id
512
-
513
- def __exit__(
514
- self,
515
- exc_type: type[BaseException] | None,
516
- exc_val: BaseException | None,
517
- exc_tb: TracebackType | None,
518
- ) -> None:
519
- """Exit the trace context."""
520
- # Update task status if root (only for HUD backend)
521
- if self.is_root and settings.telemetry_enabled and settings.api_key:
522
- if exc_type is not None:
523
- # Use synchronous update to ensure it completes before process exit
524
- _update_task_status_sync(
525
- self.task_run_id,
526
- "error",
527
- job_id=self.job_id,
528
- error_message=str(exc_val),
529
- trace_name=self.span_name,
530
- task_id=self.task_id,
531
- )
532
- # Print error completion message (only if not part of a job)
533
- if not self.job_id:
534
- _print_trace_complete_url(self.task_run_id, error_occurred=True)
535
- else:
536
- # Use synchronous update to ensure it completes before process exit
537
- _update_task_status_sync(
538
- self.task_run_id,
539
- "completed",
540
- job_id=self.job_id,
541
- trace_name=self.span_name,
542
- task_id=self.task_id,
543
- )
544
- # Print success completion message (only if not part of a job)
545
- if not self.job_id:
546
- _print_trace_complete_url(self.task_run_id, error_occurred=False)
547
-
548
- # End the span
549
- if self._span and self._span_manager is not None:
550
- if exc_type is not None and exc_val is not None:
551
- self._span.record_exception(exc_val)
552
- self._span.set_status(Status(StatusCode.ERROR, str(exc_val)))
553
- else:
554
- self._span.set_status(Status(StatusCode.OK))
555
- self._span_manager.__exit__(exc_type, exc_val, exc_tb)
556
-
557
- # Detach OpenTelemetry context
558
- if self._otel_token is not None:
559
- try:
560
- context.detach(self._otel_token) # type: ignore[arg-type]
561
- except Exception:
562
- logger.warning("Failed to detach OpenTelemetry context")
563
-
564
- # Reset context variables
565
- if self._task_run_token is not None:
566
- current_task_run_id.reset(self._task_run_token) # type: ignore
567
- if self._root_token is not None:
568
- is_root_trace_var.reset(self._root_token) # type: ignore
569
-
570
- logger.debug("Ended HUD trace context for task_run_id=%s", self.task_run_id)