hud-python 0.3.5__py3-none-any.whl → 0.4.0__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.
- hud/__init__.py +22 -89
- hud/agents/__init__.py +17 -0
- hud/agents/art.py +101 -0
- hud/agents/base.py +599 -0
- hud/{mcp → agents}/claude.py +373 -321
- hud/{mcp → agents}/langchain.py +250 -250
- hud/agents/misc/__init__.py +7 -0
- hud/{agent → agents}/misc/response_agent.py +80 -80
- hud/{mcp → agents}/openai.py +352 -334
- hud/agents/openai_chat_generic.py +154 -0
- hud/{mcp → agents}/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -0
- hud/agents/tests/test_claude.py +324 -0
- hud/{mcp → agents}/tests/test_client.py +363 -324
- hud/{mcp → agents}/tests/test_openai.py +237 -238
- hud/cli/__init__.py +617 -0
- hud/cli/__main__.py +8 -0
- hud/cli/analyze.py +371 -0
- hud/cli/analyze_metadata.py +230 -0
- hud/cli/build.py +427 -0
- hud/cli/clone.py +185 -0
- hud/cli/cursor.py +92 -0
- hud/cli/debug.py +392 -0
- hud/cli/docker_utils.py +83 -0
- hud/cli/init.py +281 -0
- hud/cli/interactive.py +353 -0
- hud/cli/mcp_server.py +756 -0
- hud/cli/pull.py +336 -0
- hud/cli/push.py +379 -0
- hud/cli/remote_runner.py +311 -0
- hud/cli/runner.py +160 -0
- hud/cli/tests/__init__.py +3 -0
- hud/cli/tests/test_analyze.py +284 -0
- hud/cli/tests/test_cli_init.py +265 -0
- hud/cli/tests/test_cli_main.py +27 -0
- hud/cli/tests/test_clone.py +142 -0
- hud/cli/tests/test_cursor.py +253 -0
- hud/cli/tests/test_debug.py +453 -0
- hud/cli/tests/test_mcp_server.py +139 -0
- hud/cli/tests/test_utils.py +388 -0
- hud/cli/utils.py +263 -0
- hud/clients/README.md +143 -0
- hud/clients/__init__.py +16 -0
- hud/clients/base.py +354 -0
- hud/clients/fastmcp.py +202 -0
- hud/clients/mcp_use.py +278 -0
- hud/clients/tests/__init__.py +1 -0
- hud/clients/tests/test_client_integration.py +111 -0
- hud/clients/tests/test_fastmcp.py +342 -0
- hud/clients/tests/test_protocol.py +188 -0
- hud/clients/utils/__init__.py +1 -0
- hud/clients/utils/retry_transport.py +160 -0
- hud/datasets.py +322 -192
- hud/misc/__init__.py +1 -0
- hud/{agent → misc}/claude_plays_pokemon.py +292 -283
- hud/otel/__init__.py +35 -0
- hud/otel/collector.py +142 -0
- hud/otel/config.py +164 -0
- hud/otel/context.py +536 -0
- hud/otel/exporters.py +366 -0
- hud/otel/instrumentation.py +97 -0
- hud/otel/processors.py +118 -0
- hud/otel/tests/__init__.py +1 -0
- hud/otel/tests/test_processors.py +197 -0
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -0
- hud/server/helper/__init__.py +5 -0
- hud/server/low_level.py +132 -0
- hud/server/server.py +166 -0
- hud/server/tests/__init__.py +3 -0
- hud/settings.py +73 -79
- hud/shared/__init__.py +5 -0
- hud/{exceptions.py → shared/exceptions.py} +180 -180
- hud/{server → shared}/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -0
- hud/{server → shared}/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -30
- hud/telemetry/instrument.py +379 -0
- hud/telemetry/job.py +309 -141
- hud/telemetry/replay.py +74 -0
- hud/telemetry/trace.py +83 -0
- hud/tools/__init__.py +33 -34
- hud/tools/base.py +365 -65
- hud/tools/bash.py +161 -137
- hud/tools/computer/__init__.py +15 -13
- hud/tools/computer/anthropic.py +437 -420
- hud/tools/computer/hud.py +376 -334
- hud/tools/computer/openai.py +295 -292
- hud/tools/computer/settings.py +82 -0
- hud/tools/edit.py +314 -290
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -532
- hud/tools/executors/pyautogui.py +621 -619
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -503
- hud/tools/{playwright_tool.py → playwright.py} +412 -379
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -0
- hud/tools/tests/test_bash.py +158 -152
- hud/tools/tests/test_bash_extended.py +197 -0
- hud/tools/tests/test_computer.py +425 -52
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -240
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -157
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -0
- hud/tools/utils.py +50 -50
- hud/types.py +136 -89
- hud/utils/__init__.py +10 -16
- hud/utils/async_utils.py +65 -0
- hud/utils/design.py +168 -0
- hud/utils/mcp.py +55 -0
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -0
- hud/utils/tests/test_init.py +17 -21
- hud/utils/tests/test_progress.py +261 -225
- hud/utils/tests/test_telemetry.py +82 -37
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- hud_python-0.4.0.dist-info/METADATA +474 -0
- hud_python-0.4.0.dist-info/RECORD +132 -0
- hud_python-0.4.0.dist-info/entry_points.txt +3 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
- hud/adapters/__init__.py +0 -8
- hud/adapters/claude/__init__.py +0 -5
- hud/adapters/claude/adapter.py +0 -180
- hud/adapters/claude/tests/__init__.py +0 -1
- hud/adapters/claude/tests/test_adapter.py +0 -519
- hud/adapters/common/__init__.py +0 -6
- hud/adapters/common/adapter.py +0 -178
- hud/adapters/common/tests/test_adapter.py +0 -289
- hud/adapters/common/types.py +0 -446
- hud/adapters/operator/__init__.py +0 -5
- hud/adapters/operator/adapter.py +0 -108
- hud/adapters/operator/tests/__init__.py +0 -1
- hud/adapters/operator/tests/test_adapter.py +0 -370
- hud/agent/__init__.py +0 -19
- hud/agent/base.py +0 -126
- hud/agent/claude.py +0 -271
- hud/agent/langchain.py +0 -215
- hud/agent/misc/__init__.py +0 -3
- hud/agent/operator.py +0 -268
- hud/agent/tests/__init__.py +0 -1
- hud/agent/tests/test_base.py +0 -202
- hud/env/__init__.py +0 -11
- hud/env/client.py +0 -35
- hud/env/docker_client.py +0 -349
- hud/env/environment.py +0 -446
- hud/env/local_docker_client.py +0 -358
- hud/env/remote_client.py +0 -212
- hud/env/remote_docker_client.py +0 -292
- hud/gym.py +0 -130
- hud/job.py +0 -773
- hud/mcp/__init__.py +0 -17
- hud/mcp/base.py +0 -631
- hud/mcp/client.py +0 -312
- hud/mcp/tests/test_base.py +0 -512
- hud/mcp/tests/test_claude.py +0 -294
- hud/task.py +0 -149
- hud/taskset.py +0 -237
- hud/telemetry/_trace.py +0 -347
- hud/telemetry/context.py +0 -230
- hud/telemetry/exporter.py +0 -575
- hud/telemetry/instrumentation/__init__.py +0 -3
- hud/telemetry/instrumentation/mcp.py +0 -259
- hud/telemetry/instrumentation/registry.py +0 -59
- hud/telemetry/mcp_models.py +0 -270
- hud/telemetry/tests/__init__.py +0 -1
- hud/telemetry/tests/test_context.py +0 -210
- hud/telemetry/tests/test_trace.py +0 -312
- hud/tools/helper/README.md +0 -56
- hud/tools/helper/__init__.py +0 -9
- hud/tools/helper/mcp_server.py +0 -78
- hud/tools/helper/server_initialization.py +0 -115
- hud/tools/helper/utils.py +0 -58
- hud/trajectory.py +0 -94
- hud/utils/agent.py +0 -37
- hud/utils/common.py +0 -256
- hud/utils/config.py +0 -120
- hud/utils/deprecation.py +0 -115
- hud/utils/misc.py +0 -53
- hud/utils/tests/test_common.py +0 -277
- hud/utils/tests/test_config.py +0 -129
- hud_python-0.3.5.dist-info/METADATA +0 -284
- hud_python-0.3.5.dist-info/RECORD +0 -120
- /hud/{adapters/common → shared}/tests/__init__.py +0 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.0.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
|