hud-python 0.4.45__py3-none-any.whl → 0.5.13__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 (282) hide show
  1. hud/__init__.py +27 -7
  2. hud/agents/__init__.py +70 -5
  3. hud/agents/base.py +238 -500
  4. hud/agents/claude.py +236 -247
  5. hud/agents/gateway.py +42 -0
  6. hud/agents/gemini.py +264 -0
  7. hud/agents/gemini_cua.py +324 -0
  8. hud/agents/grounded_openai.py +98 -100
  9. hud/agents/misc/integration_test_agent.py +51 -20
  10. hud/agents/misc/response_agent.py +48 -36
  11. hud/agents/openai.py +282 -296
  12. hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
  13. hud/agents/operator.py +199 -0
  14. hud/agents/resolver.py +70 -0
  15. hud/agents/tests/conftest.py +133 -0
  16. hud/agents/tests/test_base.py +300 -622
  17. hud/agents/tests/test_base_runtime.py +233 -0
  18. hud/agents/tests/test_claude.py +381 -214
  19. hud/agents/tests/test_client.py +9 -10
  20. hud/agents/tests/test_gemini.py +369 -0
  21. hud/agents/tests/test_grounded_openai_agent.py +65 -50
  22. hud/agents/tests/test_openai.py +377 -140
  23. hud/agents/tests/test_operator.py +362 -0
  24. hud/agents/tests/test_resolver.py +192 -0
  25. hud/agents/tests/test_run_eval.py +179 -0
  26. hud/agents/types.py +148 -0
  27. hud/cli/__init__.py +493 -546
  28. hud/cli/analyze.py +43 -5
  29. hud/cli/build.py +699 -113
  30. hud/cli/debug.py +8 -5
  31. hud/cli/dev.py +889 -732
  32. hud/cli/eval.py +793 -667
  33. hud/cli/flows/dev.py +167 -0
  34. hud/cli/flows/init.py +191 -0
  35. hud/cli/flows/tasks.py +153 -56
  36. hud/cli/flows/templates.py +151 -0
  37. hud/cli/flows/tests/__init__.py +1 -0
  38. hud/cli/flows/tests/test_dev.py +126 -0
  39. hud/cli/init.py +60 -58
  40. hud/cli/pull.py +1 -1
  41. hud/cli/push.py +38 -13
  42. hud/cli/rft.py +311 -0
  43. hud/cli/rft_status.py +145 -0
  44. hud/cli/tests/test_analyze.py +5 -5
  45. hud/cli/tests/test_analyze_metadata.py +3 -2
  46. hud/cli/tests/test_analyze_module.py +120 -0
  47. hud/cli/tests/test_build.py +110 -8
  48. hud/cli/tests/test_build_failure.py +41 -0
  49. hud/cli/tests/test_build_module.py +50 -0
  50. hud/cli/tests/test_cli_init.py +6 -1
  51. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  52. hud/cli/tests/test_cli_root.py +140 -0
  53. hud/cli/tests/test_convert.py +361 -0
  54. hud/cli/tests/test_debug.py +12 -10
  55. hud/cli/tests/test_dev.py +197 -0
  56. hud/cli/tests/test_eval.py +251 -0
  57. hud/cli/tests/test_eval_bedrock.py +51 -0
  58. hud/cli/tests/test_init.py +124 -0
  59. hud/cli/tests/test_main_module.py +11 -5
  60. hud/cli/tests/test_mcp_server.py +12 -100
  61. hud/cli/tests/test_push.py +1 -1
  62. hud/cli/tests/test_push_happy.py +74 -0
  63. hud/cli/tests/test_push_wrapper.py +23 -0
  64. hud/cli/tests/test_registry.py +1 -1
  65. hud/cli/tests/test_utils.py +1 -1
  66. hud/cli/{rl → utils}/celebrate.py +14 -12
  67. hud/cli/utils/config.py +18 -1
  68. hud/cli/utils/docker.py +130 -4
  69. hud/cli/utils/env_check.py +9 -9
  70. hud/cli/utils/git.py +136 -0
  71. hud/cli/utils/interactive.py +39 -5
  72. hud/cli/utils/metadata.py +70 -1
  73. hud/cli/utils/runner.py +1 -1
  74. hud/cli/utils/server.py +2 -2
  75. hud/cli/utils/source_hash.py +3 -3
  76. hud/cli/utils/tasks.py +4 -1
  77. hud/cli/utils/tests/__init__.py +0 -0
  78. hud/cli/utils/tests/test_config.py +58 -0
  79. hud/cli/utils/tests/test_docker.py +93 -0
  80. hud/cli/utils/tests/test_docker_hints.py +71 -0
  81. hud/cli/utils/tests/test_env_check.py +74 -0
  82. hud/cli/utils/tests/test_environment.py +42 -0
  83. hud/cli/utils/tests/test_git.py +142 -0
  84. hud/cli/utils/tests/test_interactive_module.py +60 -0
  85. hud/cli/utils/tests/test_local_runner.py +50 -0
  86. hud/cli/utils/tests/test_logging_utils.py +23 -0
  87. hud/cli/utils/tests/test_metadata.py +49 -0
  88. hud/cli/utils/tests/test_package_runner.py +35 -0
  89. hud/cli/utils/tests/test_registry_utils.py +49 -0
  90. hud/cli/utils/tests/test_remote_runner.py +25 -0
  91. hud/cli/utils/tests/test_runner_modules.py +52 -0
  92. hud/cli/utils/tests/test_source_hash.py +36 -0
  93. hud/cli/utils/tests/test_tasks.py +80 -0
  94. hud/cli/utils/version_check.py +258 -0
  95. hud/cli/{rl → utils}/viewer.py +2 -2
  96. hud/clients/README.md +12 -11
  97. hud/clients/__init__.py +4 -3
  98. hud/clients/base.py +166 -26
  99. hud/clients/environment.py +51 -0
  100. hud/clients/fastmcp.py +13 -6
  101. hud/clients/mcp_use.py +45 -15
  102. hud/clients/tests/test_analyze_scenarios.py +206 -0
  103. hud/clients/tests/test_protocol.py +9 -3
  104. hud/datasets/__init__.py +23 -20
  105. hud/datasets/loader.py +326 -0
  106. hud/datasets/runner.py +198 -105
  107. hud/datasets/tests/__init__.py +0 -0
  108. hud/datasets/tests/test_loader.py +221 -0
  109. hud/datasets/tests/test_utils.py +315 -0
  110. hud/datasets/utils.py +270 -90
  111. hud/environment/__init__.py +52 -0
  112. hud/environment/connection.py +258 -0
  113. hud/environment/connectors/__init__.py +33 -0
  114. hud/environment/connectors/base.py +68 -0
  115. hud/environment/connectors/local.py +177 -0
  116. hud/environment/connectors/mcp_config.py +137 -0
  117. hud/environment/connectors/openai.py +101 -0
  118. hud/environment/connectors/remote.py +172 -0
  119. hud/environment/environment.py +835 -0
  120. hud/environment/integrations/__init__.py +45 -0
  121. hud/environment/integrations/adk.py +67 -0
  122. hud/environment/integrations/anthropic.py +196 -0
  123. hud/environment/integrations/gemini.py +92 -0
  124. hud/environment/integrations/langchain.py +82 -0
  125. hud/environment/integrations/llamaindex.py +68 -0
  126. hud/environment/integrations/openai.py +238 -0
  127. hud/environment/mock.py +306 -0
  128. hud/environment/router.py +263 -0
  129. hud/environment/scenarios.py +620 -0
  130. hud/environment/tests/__init__.py +1 -0
  131. hud/environment/tests/test_connection.py +317 -0
  132. hud/environment/tests/test_connectors.py +205 -0
  133. hud/environment/tests/test_environment.py +593 -0
  134. hud/environment/tests/test_integrations.py +257 -0
  135. hud/environment/tests/test_local_connectors.py +242 -0
  136. hud/environment/tests/test_scenarios.py +1086 -0
  137. hud/environment/tests/test_tools.py +208 -0
  138. hud/environment/types.py +23 -0
  139. hud/environment/utils/__init__.py +35 -0
  140. hud/environment/utils/formats.py +215 -0
  141. hud/environment/utils/schema.py +171 -0
  142. hud/environment/utils/tool_wrappers.py +113 -0
  143. hud/eval/__init__.py +67 -0
  144. hud/eval/context.py +727 -0
  145. hud/eval/display.py +299 -0
  146. hud/eval/instrument.py +187 -0
  147. hud/eval/manager.py +533 -0
  148. hud/eval/parallel.py +268 -0
  149. hud/eval/task.py +372 -0
  150. hud/eval/tests/__init__.py +1 -0
  151. hud/eval/tests/test_context.py +178 -0
  152. hud/eval/tests/test_eval.py +210 -0
  153. hud/eval/tests/test_manager.py +152 -0
  154. hud/eval/tests/test_parallel.py +168 -0
  155. hud/eval/tests/test_task.py +291 -0
  156. hud/eval/types.py +65 -0
  157. hud/eval/utils.py +194 -0
  158. hud/patches/__init__.py +19 -0
  159. hud/patches/mcp_patches.py +308 -0
  160. hud/patches/warnings.py +54 -0
  161. hud/samples/browser.py +4 -4
  162. hud/server/__init__.py +2 -1
  163. hud/server/low_level.py +2 -1
  164. hud/server/router.py +164 -0
  165. hud/server/server.py +567 -80
  166. hud/server/tests/test_mcp_server_integration.py +11 -11
  167. hud/server/tests/test_mcp_server_more.py +1 -1
  168. hud/server/tests/test_server_extra.py +2 -0
  169. hud/settings.py +45 -3
  170. hud/shared/exceptions.py +36 -10
  171. hud/shared/hints.py +26 -1
  172. hud/shared/requests.py +15 -3
  173. hud/shared/tests/test_exceptions.py +40 -31
  174. hud/shared/tests/test_hints.py +167 -0
  175. hud/telemetry/__init__.py +20 -19
  176. hud/telemetry/exporter.py +201 -0
  177. hud/telemetry/instrument.py +165 -253
  178. hud/telemetry/tests/test_eval_telemetry.py +356 -0
  179. hud/telemetry/tests/test_exporter.py +258 -0
  180. hud/telemetry/tests/test_instrument.py +401 -0
  181. hud/tools/__init__.py +18 -2
  182. hud/tools/agent.py +223 -0
  183. hud/tools/apply_patch.py +639 -0
  184. hud/tools/base.py +54 -4
  185. hud/tools/bash.py +2 -2
  186. hud/tools/computer/__init__.py +36 -3
  187. hud/tools/computer/anthropic.py +2 -2
  188. hud/tools/computer/gemini.py +385 -0
  189. hud/tools/computer/hud.py +23 -6
  190. hud/tools/computer/openai.py +20 -21
  191. hud/tools/computer/qwen.py +434 -0
  192. hud/tools/computer/settings.py +37 -0
  193. hud/tools/edit.py +3 -7
  194. hud/tools/executors/base.py +4 -2
  195. hud/tools/executors/pyautogui.py +1 -1
  196. hud/tools/grounding/grounded_tool.py +13 -18
  197. hud/tools/grounding/grounder.py +10 -31
  198. hud/tools/grounding/tests/test_grounded_tool.py +26 -44
  199. hud/tools/jupyter.py +330 -0
  200. hud/tools/playwright.py +18 -3
  201. hud/tools/shell.py +308 -0
  202. hud/tools/tests/test_agent_tool.py +355 -0
  203. hud/tools/tests/test_apply_patch.py +718 -0
  204. hud/tools/tests/test_computer.py +4 -9
  205. hud/tools/tests/test_computer_actions.py +24 -2
  206. hud/tools/tests/test_jupyter_tool.py +181 -0
  207. hud/tools/tests/test_shell.py +596 -0
  208. hud/tools/tests/test_submit.py +85 -0
  209. hud/tools/tests/test_types.py +193 -0
  210. hud/tools/types.py +21 -1
  211. hud/types.py +194 -56
  212. hud/utils/__init__.py +2 -0
  213. hud/utils/env.py +67 -0
  214. hud/utils/hud_console.py +89 -18
  215. hud/utils/mcp.py +15 -58
  216. hud/utils/strict_schema.py +162 -0
  217. hud/utils/tests/test_init.py +1 -2
  218. hud/utils/tests/test_mcp.py +1 -28
  219. hud/utils/tests/test_pretty_errors.py +186 -0
  220. hud/utils/tests/test_tool_shorthand.py +154 -0
  221. hud/utils/tests/test_version.py +1 -1
  222. hud/utils/types.py +20 -0
  223. hud/version.py +1 -1
  224. hud_python-0.5.13.dist-info/METADATA +264 -0
  225. hud_python-0.5.13.dist-info/RECORD +305 -0
  226. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
  227. hud/agents/langchain.py +0 -261
  228. hud/agents/lite_llm.py +0 -72
  229. hud/cli/rl/__init__.py +0 -180
  230. hud/cli/rl/config.py +0 -101
  231. hud/cli/rl/display.py +0 -133
  232. hud/cli/rl/gpu.py +0 -63
  233. hud/cli/rl/gpu_utils.py +0 -321
  234. hud/cli/rl/local_runner.py +0 -595
  235. hud/cli/rl/presets.py +0 -96
  236. hud/cli/rl/remote_runner.py +0 -463
  237. hud/cli/rl/rl_api.py +0 -150
  238. hud/cli/rl/vllm.py +0 -177
  239. hud/cli/rl/wait_utils.py +0 -89
  240. hud/datasets/parallel.py +0 -687
  241. hud/misc/__init__.py +0 -1
  242. hud/misc/claude_plays_pokemon.py +0 -292
  243. hud/otel/__init__.py +0 -35
  244. hud/otel/collector.py +0 -142
  245. hud/otel/config.py +0 -181
  246. hud/otel/context.py +0 -570
  247. hud/otel/exporters.py +0 -369
  248. hud/otel/instrumentation.py +0 -135
  249. hud/otel/processors.py +0 -121
  250. hud/otel/tests/__init__.py +0 -1
  251. hud/otel/tests/test_processors.py +0 -197
  252. hud/rl/README.md +0 -30
  253. hud/rl/__init__.py +0 -1
  254. hud/rl/actor.py +0 -176
  255. hud/rl/buffer.py +0 -405
  256. hud/rl/chat_template.jinja +0 -101
  257. hud/rl/config.py +0 -192
  258. hud/rl/distributed.py +0 -132
  259. hud/rl/learner.py +0 -637
  260. hud/rl/tests/__init__.py +0 -1
  261. hud/rl/tests/test_learner.py +0 -186
  262. hud/rl/train.py +0 -382
  263. hud/rl/types.py +0 -101
  264. hud/rl/utils/start_vllm_server.sh +0 -30
  265. hud/rl/utils.py +0 -524
  266. hud/rl/vllm_adapter.py +0 -143
  267. hud/telemetry/job.py +0 -352
  268. hud/telemetry/replay.py +0 -74
  269. hud/telemetry/tests/test_replay.py +0 -40
  270. hud/telemetry/tests/test_trace.py +0 -63
  271. hud/telemetry/trace.py +0 -158
  272. hud/utils/agent_factories.py +0 -86
  273. hud/utils/async_utils.py +0 -65
  274. hud/utils/group_eval.py +0 -223
  275. hud/utils/progress.py +0 -149
  276. hud/utils/tasks.py +0 -127
  277. hud/utils/tests/test_async_utils.py +0 -173
  278. hud/utils/tests/test_progress.py +0 -261
  279. hud_python-0.4.45.dist-info/METADATA +0 -552
  280. hud_python-0.4.45.dist-info/RECORD +0 -228
  281. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
  282. {hud_python-0.4.45.dist-info → hud_python-0.5.13.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)