hud-python 0.3.5__py3-none-any.whl → 0.4.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.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +15 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +370 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +379 -0
  45. hud/clients/fastmcp.py +222 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -420
  87. hud/tools/computer/hud.py +376 -334
  88. hud/tools/computer/openai.py +295 -292
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.1.dist-info/METADATA +476 -0
  126. hud_python-0.4.1.dist-info/RECORD +132 -0
  127. hud_python-0.4.1.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.5.dist-info → hud_python-0.4.1.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.5.dist-info/METADATA +0 -284
  190. hud_python-0.3.5.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.5.dist-info → hud_python-0.4.1.dist-info}/WHEEL +0 -0
hud/telemetry/_trace.py DELETED
@@ -1,347 +0,0 @@
1
- from __future__ import annotations
2
-
3
- # ruff: noqa: T201
4
- import asyncio
5
- import logging
6
- import time
7
- import uuid
8
- from contextlib import contextmanager
9
- from functools import wraps
10
- from typing import (
11
- TYPE_CHECKING,
12
- Any,
13
- ParamSpec,
14
- TypeVar,
15
- )
16
-
17
- from hud.telemetry.context import (
18
- flush_buffer,
19
- get_current_task_run_id,
20
- is_root_trace,
21
- set_current_task_run_id,
22
- )
23
- from hud.telemetry.instrumentation.registry import registry
24
-
25
- if TYPE_CHECKING:
26
- from collections.abc import Generator
27
-
28
-
29
- logger = logging.getLogger("hud.telemetry")
30
- T = TypeVar("T")
31
- P = ParamSpec("P")
32
-
33
- # Track whether telemetry has been initialized
34
- _telemetry_initialized = False
35
-
36
-
37
- def init_telemetry() -> None:
38
- """Initialize telemetry instrumentors and ensure worker is started if telemetry is active."""
39
- global _telemetry_initialized
40
- if _telemetry_initialized:
41
- return
42
-
43
- registry.install_all()
44
- logger.info("Telemetry initialized.")
45
- _telemetry_initialized = True
46
-
47
-
48
- def _ensure_telemetry_initialized() -> None:
49
- """Ensure telemetry is initialized - called lazily by trace functions."""
50
- from hud.settings import settings
51
-
52
- if settings.telemetry_enabled and not _telemetry_initialized:
53
- init_telemetry()
54
-
55
-
56
- def _detect_agent_model() -> str | None:
57
- """
58
- Try to auto-detect agent model from parent frames.
59
- This is a best-effort approach and may not work in all cases.
60
- """
61
- import sys
62
-
63
- try:
64
- # Try different frame depths (2-3 typically covers most cases)
65
- for depth in range(2, 3):
66
- try:
67
- frame = sys._getframe(depth)
68
- # Check local variables for agent objects
69
- for var_value in frame.f_locals.values():
70
- # Look for objects with model_name attribute
71
- if hasattr(var_value, "model_name") and hasattr(var_value, "run"):
72
- # Likely an agent object
73
- model_name = getattr(var_value, "model_name", None)
74
- if model_name:
75
- logger.debug(
76
- "Found agent with model_name in frame %d: %s", depth, model_name
77
- )
78
- return str(model_name)
79
-
80
- # Also check self in case we're in a method
81
- if "self" in frame.f_locals:
82
- self_obj = frame.f_locals["self"]
83
- if hasattr(self_obj, "model_name"):
84
- model_name = getattr(self_obj, "model_name", None)
85
- if model_name:
86
- logger.debug(
87
- "Found agent model_name in self at frame %d: %s", depth, model_name
88
- )
89
- return str(model_name)
90
-
91
- except (ValueError, AttributeError):
92
- # Frame doesn't exist at this depth or other issues
93
- continue
94
-
95
- except Exception as e:
96
- logger.debug("Agent model detection failed: %s", e)
97
-
98
- return None
99
-
100
-
101
- def _print_trace_url(task_run_id: str) -> None:
102
- """Print the trace URL in a colorful box."""
103
- url = f"https://app.hud.so/trace/{task_run_id}"
104
- header = "🚀 See your agent live at:"
105
-
106
- # ANSI color codes
107
- DIM = "\033[90m" # Dim/Gray for border (visible on both light and dark terminals)
108
- GOLD = "\033[33m" # Gold/Yellow for URL
109
- RESET = "\033[0m"
110
- BOLD = "\033[1m"
111
-
112
- # Calculate box width based on the longest line
113
- box_width = max(len(url), len(header)) + 6
114
-
115
- # Box drawing characters
116
- top_border = "╔" + "═" * (box_width - 2) + "╗"
117
- bottom_border = "╚" + "═" * (box_width - 2) + "╝"
118
- divider = "╟" + "─" * (box_width - 2) + "╢"
119
-
120
- # Center the content
121
- header_padding = (box_width - len(header) - 2) // 2
122
- url_padding = (box_width - len(url) - 2) // 2
123
-
124
- # Print the box
125
- print(f"\n{DIM}{top_border}{RESET}")
126
- print(
127
- f"{DIM}║{RESET}{' ' * header_padding}{header}{' ' * (box_width - len(header) - header_padding - 3)}{DIM}║{RESET}" # noqa: E501
128
- )
129
- print(f"{DIM}{divider}{RESET}")
130
- print(
131
- f"{DIM}║{RESET}{' ' * url_padding}{BOLD}{GOLD}{url}{RESET}{' ' * (box_width - len(url) - url_padding - 2)}{DIM}║{RESET}" # noqa: E501
132
- )
133
- print(f"{DIM}{bottom_border}{RESET}\n")
134
-
135
-
136
- def _print_trace_complete_url(task_run_id: str) -> None:
137
- """Print the trace completion URL in a simple colorful format."""
138
- url = f"https://app.hud.so/trace/{task_run_id}"
139
-
140
- # ANSI color codes
141
- GREEN = "\033[92m"
142
- GOLD = "\033[33m"
143
- RESET = "\033[0m"
144
- DIM = "\033[2m"
145
- BOLD = "\033[1m"
146
-
147
- print(f"\n{GREEN}✓ Trace complete!{RESET} {DIM}View at:{RESET} {BOLD}{GOLD}{url}{RESET}\n")
148
-
149
-
150
- @contextmanager
151
- def trace_open(
152
- name: str | None = None,
153
- agent_model: str | None = None,
154
- run_id: str | None = None,
155
- attributes: dict[str, Any] | None = None,
156
- ) -> Generator[str, None, None]:
157
- """
158
- Context manager for tracing a block of code.
159
-
160
- Args:
161
- name: Optional name for this trace, will be added to attributes.
162
- attributes: Optional dictionary of attributes to associate with this trace
163
-
164
- Returns:
165
- The generated task run ID (UUID string) used for this trace
166
- """
167
- # Lazy initialization - only initialize telemetry when trace() is actually called
168
- _ensure_telemetry_initialized()
169
-
170
- task_run_id = run_id or str(uuid.uuid4())
171
-
172
- _print_trace_url(task_run_id)
173
-
174
- local_attributes = attributes.copy() if attributes is not None else {}
175
- if name is not None:
176
- local_attributes["trace_name"] = name
177
-
178
- # Auto-detect agent if not explicitly provided
179
- if agent_model is None:
180
- agent_model = _detect_agent_model()
181
-
182
- start_time = time.time()
183
- logger.debug("Starting trace %s (Name: %s)", task_run_id, name if name else "Unnamed")
184
-
185
- previous_task_id = get_current_task_run_id()
186
- was_root = is_root_trace.get()
187
-
188
- set_current_task_run_id(task_run_id)
189
- is_root = previous_task_id is None
190
- is_root_trace.set(is_root)
191
-
192
- # Update status to initializing for root traces
193
- if is_root:
194
- from hud.telemetry.exporter import (
195
- TaskRunStatus,
196
- submit_to_worker_loop,
197
- update_task_run_status,
198
- )
199
- from hud.telemetry.job import get_current_job_id
200
-
201
- # Include metadata in the initial status update
202
- initial_metadata = local_attributes.copy()
203
- initial_metadata["is_root_trace"] = is_root
204
- if agent_model:
205
- initial_metadata["agent_model"] = agent_model
206
-
207
- # Get job_id if we're in a job context
208
- job_id = get_current_job_id()
209
-
210
- coro = update_task_run_status(
211
- task_run_id, TaskRunStatus.INITIALIZING, metadata=initial_metadata, job_id=job_id
212
- )
213
- submit_to_worker_loop(coro)
214
- logger.debug("Updated task run %s status to INITIALIZING with metadata", task_run_id)
215
-
216
- error_occurred = False
217
- error_message = None
218
-
219
- try:
220
- yield task_run_id
221
- except Exception as e:
222
- error_occurred = True
223
- error_message = str(e)
224
- raise
225
- finally:
226
- end_time = time.time()
227
- duration = end_time - start_time
228
- local_attributes["duration_seconds"] = duration
229
- local_attributes["is_root_trace"] = is_root
230
-
231
- logger.debug("Finishing trace %s after %.2f seconds", task_run_id, duration)
232
-
233
- # Update status for root traces
234
- if is_root:
235
- from hud.telemetry.exporter import (
236
- TaskRunStatus,
237
- submit_to_worker_loop,
238
- update_task_run_status,
239
- )
240
-
241
- # Include final metadata with duration
242
- final_metadata = local_attributes.copy()
243
-
244
- if error_occurred:
245
- coro = update_task_run_status(
246
- task_run_id, TaskRunStatus.ERROR, error_message, metadata=final_metadata
247
- )
248
- logger.debug("Updated task run %s status to ERROR: %s", task_run_id, error_message)
249
- else:
250
- coro = update_task_run_status(
251
- task_run_id, TaskRunStatus.COMPLETED, metadata=final_metadata
252
- )
253
- logger.debug("Updated task run %s status to COMPLETED with metadata", task_run_id)
254
-
255
- # Wait for the status update to complete
256
- future = submit_to_worker_loop(coro)
257
- if future:
258
- try:
259
- # Wait up to 5 seconds for the status update
260
- import concurrent.futures
261
-
262
- future.result(timeout=5.0)
263
- logger.debug("Status update completed successfully")
264
- except concurrent.futures.TimeoutError:
265
- logger.warning("Timeout waiting for status update to complete")
266
- except Exception as e:
267
- logger.error("Error waiting for status update: %s", e)
268
-
269
- # Export any remaining records before flushing
270
- if is_root:
271
- from hud.telemetry.context import export_incremental
272
-
273
- export_incremental()
274
-
275
- # Always flush the buffer for the current task
276
- mcp_calls = flush_buffer(export=True)
277
- logger.debug("Flushed %d MCP calls for trace %s", len(mcp_calls), task_run_id)
278
-
279
- # Restore previous context
280
- set_current_task_run_id(previous_task_id)
281
- is_root_trace.set(was_root)
282
-
283
- # Log at the end
284
- if is_root:
285
- _print_trace_complete_url(task_run_id)
286
-
287
-
288
- @contextmanager
289
- def trace(
290
- name: str | None = None,
291
- agent_model: str | None = None,
292
- attributes: dict[str, Any] | None = None,
293
- ) -> Generator[str, None, None]:
294
- """
295
- Synchronous context manager that traces and blocks until telemetry is sent.
296
-
297
- This is the "worry-free" option when you want to ensure telemetry is
298
- sent immediately before continuing, rather than relying on background workers.
299
-
300
- Args:
301
- name: Optional name for this trace
302
- attributes: Optional attributes for the trace
303
-
304
- Returns:
305
- The generated task run ID (UUID string) used for this trace
306
- """
307
- with trace_open(name=name, agent_model=agent_model, attributes=attributes) as task_run_id:
308
- yield task_run_id
309
-
310
- # Ensure telemetry is flushed synchronously
311
- from hud import flush
312
-
313
- flush()
314
-
315
-
316
- def trace_decorator(
317
- name: str | None = None,
318
- agent_model: str | None = None,
319
- attributes: dict[str, Any] | None = None,
320
- ) -> Any:
321
- """
322
- Decorator for tracing functions.
323
-
324
- Can be used on both sync and async functions.
325
- """
326
-
327
- def decorator(func: Any) -> Any:
328
- if asyncio.iscoroutinefunction(func):
329
-
330
- @wraps(func)
331
- async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
332
- func_name = name or f"{func.__module__}.{func.__name__}"
333
- with trace_open(name=func_name, agent_model=agent_model, attributes=attributes):
334
- return await func(*args, **kwargs)
335
-
336
- return async_wrapper
337
- else:
338
-
339
- @wraps(func)
340
- def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
341
- func_name = name or f"{func.__module__}.{func.__name__}"
342
- with trace_open(name=func_name, agent_model=agent_model, attributes=attributes):
343
- return func(*args, **kwargs)
344
-
345
- return sync_wrapper
346
-
347
- return decorator
hud/telemetry/context.py DELETED
@@ -1,230 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import contextvars
4
- import logging
5
- from collections import defaultdict
6
- from datetime import datetime
7
- from typing import Any, TypeVar
8
-
9
- from hud.telemetry.mcp_models import (
10
- BaseMCPCall,
11
- MCPNotificationCall,
12
- MCPRequestCall,
13
- MCPResponseCall,
14
- StatusType,
15
- )
16
-
17
- logger = logging.getLogger("hud.telemetry")
18
-
19
- # Context variables for tracing
20
- current_task_run_id: contextvars.ContextVar[str | None] = contextvars.ContextVar(
21
- "current_task_run_id", default=None
22
- )
23
- # Global dictionary for buffering, keyed by task_run_id
24
- _GLOBAL_MCP_CALL_BUFFERS: defaultdict[str, list[BaseMCPCall]] = defaultdict(list)
25
- # Track the last exported index for each task_run_id
26
- _GLOBAL_EXPORT_INDICES: defaultdict[str, int] = defaultdict(int)
27
- # Track whether we've seen a non-init request for each task_run_id
28
- _GLOBAL_HAS_NON_INIT_REQUEST: defaultdict[str, bool] = defaultdict(bool)
29
- is_root_trace: contextvars.ContextVar[bool] = contextvars.ContextVar("is_root_trace", default=False)
30
-
31
- # Maximum buffer size before automatic flush
32
- MAX_BUFFER_SIZE = 100
33
-
34
- # Type variable for record factories
35
- T = TypeVar("T", bound=BaseMCPCall)
36
-
37
-
38
- def get_current_task_run_id() -> str | None:
39
- """Get the task_run_id for the current trace context."""
40
- return current_task_run_id.get()
41
-
42
-
43
- def set_current_task_run_id(task_run_id: str | None) -> None:
44
- """Set the task_run_id for the current trace context."""
45
- current_task_run_id.set(task_run_id)
46
-
47
-
48
- def buffer_mcp_call(record: BaseMCPCall | dict[str, Any]) -> None:
49
- """Buffer an MCP call record for the current trace."""
50
- task_run_id = get_current_task_run_id()
51
-
52
- if not task_run_id:
53
- logger.warning(
54
- "BUFFER_MCP_CALL: No task_run_id. Skipping buffer for %s", type(record).__name__
55
- )
56
- return
57
-
58
- # Ensure 'record' is a Pydantic model instance
59
- if isinstance(record, dict):
60
- try:
61
- record_model = BaseMCPCall.from_dict(record)
62
- record = record_model
63
- except Exception as e_conv:
64
- logger.exception("BUFFER_MCP_CALL: Failed to convert dict to BaseMCPCall: %s", e_conv)
65
- return
66
-
67
- _GLOBAL_MCP_CALL_BUFFERS[task_run_id].append(record)
68
- buffer_len = len(_GLOBAL_MCP_CALL_BUFFERS[task_run_id])
69
-
70
- if buffer_len >= MAX_BUFFER_SIZE:
71
- flush_buffer(export=True)
72
-
73
-
74
- def export_incremental() -> list[BaseMCPCall]:
75
- """
76
- Export only new MCP calls since last export without clearing the buffer.
77
-
78
- Returns:
79
- The list of newly exported MCP calls
80
- """
81
- task_run_id = get_current_task_run_id()
82
- if not task_run_id or not is_root_trace.get():
83
- return []
84
-
85
- buffer = _GLOBAL_MCP_CALL_BUFFERS.get(task_run_id, [])
86
- last_exported_idx = _GLOBAL_EXPORT_INDICES.get(task_run_id, 0)
87
-
88
- # Get only the new records since last export
89
- new_records = buffer[last_exported_idx:]
90
-
91
- if new_records:
92
- # Update the export index
93
- _GLOBAL_EXPORT_INDICES[task_run_id] = len(buffer)
94
-
95
- # Trigger export
96
- from hud.telemetry import exporter
97
- from hud.telemetry.exporter import submit_to_worker_loop
98
-
99
- # Get current trace attributes if available
100
- attributes = {"incremental": True}
101
-
102
- coro = exporter.export_telemetry(
103
- task_run_id=task_run_id,
104
- trace_attributes=attributes,
105
- mcp_calls=new_records.copy(), # Copy to avoid modification during export
106
- )
107
- submit_to_worker_loop(coro)
108
-
109
- logger.debug(
110
- "Incremental export: %d new MCP calls for trace %s", len(new_records), task_run_id
111
- )
112
-
113
- return new_records
114
-
115
-
116
- def flush_buffer(export: bool = False) -> list[BaseMCPCall]:
117
- """
118
- Clear the MCP calls buffer and return its contents.
119
-
120
- Args:
121
- export: Whether to trigger export of this buffer
122
-
123
- Returns:
124
- The list of buffered MCP calls
125
- """
126
- task_run_id = get_current_task_run_id()
127
- if not task_run_id:
128
- logger.warning("FLUSH_BUFFER: No current task_run_id. Cannot flush.")
129
- return []
130
-
131
- buffer_for_task = _GLOBAL_MCP_CALL_BUFFERS.pop(task_run_id, [])
132
- # Clean up export index when buffer is flushed
133
- _GLOBAL_EXPORT_INDICES.pop(task_run_id, None)
134
- # Clean up non-init request tracking
135
- _GLOBAL_HAS_NON_INIT_REQUEST.pop(task_run_id, None)
136
- return buffer_for_task
137
-
138
-
139
- def create_request_record(
140
- method: str, status: StatusType = StatusType.STARTED, **kwargs: Any
141
- ) -> MCPRequestCall:
142
- """Create and buffer a request record"""
143
- task_run_id = get_current_task_run_id()
144
- if not task_run_id:
145
- logger.warning("No active task_run_id, request record will not be created")
146
- raise ValueError("No active task_run_id")
147
-
148
- # Check if this is the first non-init request and update status
149
- if is_root_trace.get() and not _GLOBAL_HAS_NON_INIT_REQUEST[task_run_id]:
150
- # Common initialization method patterns
151
- init_methods = {"initialize", "session/new", "init", "setup", "connect"}
152
- method_lower = method.lower()
153
-
154
- # Check if this is NOT an initialization method
155
- if not any(init_pattern in method_lower for init_pattern in init_methods):
156
- _GLOBAL_HAS_NON_INIT_REQUEST[task_run_id] = True
157
-
158
- # Update status to running
159
- from hud.telemetry.exporter import (
160
- TaskRunStatus,
161
- submit_to_worker_loop,
162
- update_task_run_status,
163
- )
164
-
165
- coro = update_task_run_status(task_run_id, TaskRunStatus.RUNNING)
166
- submit_to_worker_loop(coro)
167
- logger.debug(
168
- "Updated task run %s status to RUNNING on first non-init request: %s",
169
- task_run_id,
170
- method,
171
- )
172
-
173
- record = MCPRequestCall(
174
- task_run_id=task_run_id,
175
- method=method,
176
- status=status,
177
- start_time=kwargs.pop("start_time", None) or datetime.now().timestamp(),
178
- **kwargs,
179
- )
180
- buffer_mcp_call(record)
181
- return record
182
-
183
-
184
- def create_response_record(
185
- method: str, related_request_id: str | int | None = None, is_error: bool = False, **kwargs: Any
186
- ) -> MCPResponseCall:
187
- """Create and buffer a response record"""
188
- task_run_id = get_current_task_run_id()
189
- if not task_run_id:
190
- logger.warning("No active task_run_id, response record will not be created")
191
- raise ValueError("No active task_run_id")
192
-
193
- # Default to COMPLETED status if not provided
194
- if "status" not in kwargs:
195
- kwargs["status"] = StatusType.COMPLETED
196
-
197
- record = MCPResponseCall(
198
- task_run_id=task_run_id,
199
- method=method,
200
- related_request_id=related_request_id,
201
- is_error=is_error,
202
- **kwargs,
203
- )
204
-
205
- buffer_mcp_call(record)
206
-
207
- # Trigger incremental export when we receive a response
208
- export_incremental()
209
-
210
- return record
211
-
212
-
213
- def create_notification_record(
214
- method: str, status: StatusType = StatusType.STARTED, **kwargs: Any
215
- ) -> MCPNotificationCall:
216
- """Create and buffer a notification record"""
217
- task_run_id = get_current_task_run_id()
218
- if not task_run_id:
219
- logger.warning("No active task_run_id, notification record will not be created")
220
- raise ValueError("No active task_run_id")
221
-
222
- record = MCPNotificationCall(
223
- task_run_id=task_run_id,
224
- method=method,
225
- status=status,
226
- start_time=kwargs.pop("start_time", None) or datetime.now().timestamp(),
227
- **kwargs,
228
- )
229
- buffer_mcp_call(record)
230
- return record