hud-python 0.4.1__py3-none-any.whl → 0.4.3__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 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- 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 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
hud/otel/instrumentation.py
CHANGED
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
"""MCP instrumentation support for HUD.
|
|
2
|
-
|
|
3
|
-
This module provides functions to enable MCP OpenTelemetry instrumentation
|
|
4
|
-
for automatic tracing of MCP protocol communication.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import logging
|
|
10
|
-
from typing import TYPE_CHECKING, Any
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from collections.abc import AsyncGenerator, Callable
|
|
14
|
-
|
|
15
|
-
from opentelemetry.trace import TracerProvider
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
LIFECYCLE_TOOLS = {"setup", "evaluate"}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def install_mcp_instrumentation(provider: TracerProvider) -> None:
|
|
23
|
-
"""Enable community MCP OpenTelemetry instrumentation if present.
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
provider: The TracerProvider to use for instrumentation
|
|
27
|
-
"""
|
|
28
|
-
import logging
|
|
29
|
-
|
|
30
|
-
logger = logging.getLogger(__name__)
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
from opentelemetry.instrumentation.mcp.instrumentation import (
|
|
34
|
-
McpInstrumentor,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
# First, patch the instrumentation to handle 3-value transports correctly
|
|
38
|
-
_patch_mcp_instrumentation()
|
|
39
|
-
|
|
40
|
-
McpInstrumentor().instrument(tracer_provider=provider)
|
|
41
|
-
logger.debug("MCP instrumentation installed with fastmcp compatibility patch")
|
|
42
|
-
except ImportError:
|
|
43
|
-
logger.debug("opentelemetry-instrumentation-mcp not available, skipping")
|
|
44
|
-
except Exception as exc:
|
|
45
|
-
logger.warning("Failed to install MCP instrumentation: %s", exc)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _patch_mcp_instrumentation() -> None:
|
|
49
|
-
"""Patch MCP instrumentation to handle 3-value transport yields correctly."""
|
|
50
|
-
from contextlib import asynccontextmanager
|
|
51
|
-
|
|
52
|
-
try:
|
|
53
|
-
from opentelemetry.instrumentation.mcp.instrumentation import McpInstrumentor
|
|
54
|
-
|
|
55
|
-
def patched_transport_wrapper(self: Any, tracer: Any) -> Callable[..., Any]:
|
|
56
|
-
@asynccontextmanager
|
|
57
|
-
async def traced_method(
|
|
58
|
-
wrapped: Callable[..., Any], instance: Any, args: Any, kwargs: Any
|
|
59
|
-
) -> AsyncGenerator[Any, None]:
|
|
60
|
-
async with wrapped(*args, **kwargs) as result:
|
|
61
|
-
# Check if we got a tuple with 3 values
|
|
62
|
-
if isinstance(result, tuple) and len(result) == 3:
|
|
63
|
-
read_stream, write_stream, third_value = result
|
|
64
|
-
# Import here to avoid circular imports
|
|
65
|
-
from opentelemetry.instrumentation.mcp.instrumentation import (
|
|
66
|
-
InstrumentedStreamReader,
|
|
67
|
-
InstrumentedStreamWriter,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
yield (
|
|
71
|
-
InstrumentedStreamReader(read_stream, tracer),
|
|
72
|
-
InstrumentedStreamWriter(write_stream, tracer),
|
|
73
|
-
third_value,
|
|
74
|
-
)
|
|
75
|
-
else:
|
|
76
|
-
# Fall back to 2-value case
|
|
77
|
-
read_stream, write_stream = result
|
|
78
|
-
from opentelemetry.instrumentation.mcp.instrumentation import (
|
|
79
|
-
InstrumentedStreamReader,
|
|
80
|
-
InstrumentedStreamWriter,
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
yield (
|
|
84
|
-
InstrumentedStreamReader(read_stream, tracer),
|
|
85
|
-
InstrumentedStreamWriter(write_stream, tracer),
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
return traced_method
|
|
89
|
-
|
|
90
|
-
# Apply the patch
|
|
91
|
-
McpInstrumentor._transport_wrapper = patched_transport_wrapper
|
|
92
|
-
|
|
93
|
-
except Exception as e:
|
|
94
|
-
import logging
|
|
95
|
-
|
|
96
|
-
logger = logging.getLogger(__name__)
|
|
97
|
-
logger.warning("Failed to patch MCP instrumentation: %s", e)
|
|
1
|
+
"""MCP instrumentation support for HUD.
|
|
2
|
+
|
|
3
|
+
This module provides functions to enable MCP OpenTelemetry instrumentation
|
|
4
|
+
for automatic tracing of MCP protocol communication.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import AsyncGenerator, Callable
|
|
14
|
+
|
|
15
|
+
from opentelemetry.trace import TracerProvider
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
LIFECYCLE_TOOLS = {"setup", "evaluate"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def install_mcp_instrumentation(provider: TracerProvider) -> None:
|
|
23
|
+
"""Enable community MCP OpenTelemetry instrumentation if present.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
provider: The TracerProvider to use for instrumentation
|
|
27
|
+
"""
|
|
28
|
+
import logging
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from opentelemetry.instrumentation.mcp.instrumentation import (
|
|
34
|
+
McpInstrumentor,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# First, patch the instrumentation to handle 3-value transports correctly
|
|
38
|
+
_patch_mcp_instrumentation()
|
|
39
|
+
|
|
40
|
+
McpInstrumentor().instrument(tracer_provider=provider)
|
|
41
|
+
logger.debug("MCP instrumentation installed with fastmcp compatibility patch")
|
|
42
|
+
except ImportError:
|
|
43
|
+
logger.debug("opentelemetry-instrumentation-mcp not available, skipping")
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
logger.warning("Failed to install MCP instrumentation: %s", exc)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _patch_mcp_instrumentation() -> None:
|
|
49
|
+
"""Patch MCP instrumentation to handle 3-value transport yields correctly."""
|
|
50
|
+
from contextlib import asynccontextmanager
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
from opentelemetry.instrumentation.mcp.instrumentation import McpInstrumentor
|
|
54
|
+
|
|
55
|
+
def patched_transport_wrapper(self: Any, tracer: Any) -> Callable[..., Any]:
|
|
56
|
+
@asynccontextmanager
|
|
57
|
+
async def traced_method(
|
|
58
|
+
wrapped: Callable[..., Any], instance: Any, args: Any, kwargs: Any
|
|
59
|
+
) -> AsyncGenerator[Any, None]:
|
|
60
|
+
async with wrapped(*args, **kwargs) as result:
|
|
61
|
+
# Check if we got a tuple with 3 values
|
|
62
|
+
if isinstance(result, tuple) and len(result) == 3:
|
|
63
|
+
read_stream, write_stream, third_value = result
|
|
64
|
+
# Import here to avoid circular imports
|
|
65
|
+
from opentelemetry.instrumentation.mcp.instrumentation import (
|
|
66
|
+
InstrumentedStreamReader,
|
|
67
|
+
InstrumentedStreamWriter,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
yield (
|
|
71
|
+
InstrumentedStreamReader(read_stream, tracer),
|
|
72
|
+
InstrumentedStreamWriter(write_stream, tracer),
|
|
73
|
+
third_value,
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
# Fall back to 2-value case
|
|
77
|
+
read_stream, write_stream = result
|
|
78
|
+
from opentelemetry.instrumentation.mcp.instrumentation import (
|
|
79
|
+
InstrumentedStreamReader,
|
|
80
|
+
InstrumentedStreamWriter,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
yield (
|
|
84
|
+
InstrumentedStreamReader(read_stream, tracer),
|
|
85
|
+
InstrumentedStreamWriter(write_stream, tracer),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return traced_method
|
|
89
|
+
|
|
90
|
+
# Apply the patch
|
|
91
|
+
McpInstrumentor._transport_wrapper = patched_transport_wrapper
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
import logging
|
|
95
|
+
|
|
96
|
+
logger = logging.getLogger(__name__)
|
|
97
|
+
logger.warning("Failed to patch MCP instrumentation: %s", e)
|
hud/otel/processors.py
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from opentelemetry import baggage
|
|
7
|
-
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
|
|
8
|
-
|
|
9
|
-
from .context import (
|
|
10
|
-
get_agent_steps,
|
|
11
|
-
get_base_mcp_steps,
|
|
12
|
-
get_mcp_tool_steps,
|
|
13
|
-
increment_agent_steps,
|
|
14
|
-
increment_base_mcp_steps,
|
|
15
|
-
increment_mcp_tool_steps,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class HudEnrichmentProcessor(SpanProcessor):
|
|
22
|
-
"""Span processor that enriches every span with HUD-specific context.
|
|
23
|
-
|
|
24
|
-
• Adds ``hud.task_run_id`` attribute if available.
|
|
25
|
-
• Adds ``hud.job_id`` attribute if available in baggage.
|
|
26
|
-
• Adds ``hud.step_count`` attribute if available in baggage.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(self) -> None:
|
|
30
|
-
# No state, everything comes from context vars
|
|
31
|
-
super().__init__()
|
|
32
|
-
|
|
33
|
-
# --- callback hooks -------------------------------------------------
|
|
34
|
-
def on_start(self, span: Span, parent_context: Any) -> None: # type: ignore[override]
|
|
35
|
-
try:
|
|
36
|
-
# Get task_run_id from baggage in parent context
|
|
37
|
-
run_id = baggage.get_baggage("hud.task_run_id", context=parent_context)
|
|
38
|
-
if run_id and span.is_recording():
|
|
39
|
-
span.set_attribute("hud.task_run_id", str(run_id))
|
|
40
|
-
|
|
41
|
-
# Get job_id from baggage if available
|
|
42
|
-
job_id = baggage.get_baggage("hud.job_id", context=parent_context)
|
|
43
|
-
if job_id and span.is_recording():
|
|
44
|
-
span.set_attribute("hud.job_id", str(job_id))
|
|
45
|
-
|
|
46
|
-
# Check what type of step this is and increment appropriate counters
|
|
47
|
-
if span.is_recording():
|
|
48
|
-
step_type = self._get_step_type(span)
|
|
49
|
-
|
|
50
|
-
if step_type == "agent":
|
|
51
|
-
# Increment agent steps
|
|
52
|
-
new_agent_count = increment_agent_steps()
|
|
53
|
-
span.set_attribute("hud.agent_steps", new_agent_count)
|
|
54
|
-
logger.debug("Incremented agent steps to %d", new_agent_count)
|
|
55
|
-
|
|
56
|
-
elif step_type == "base_mcp":
|
|
57
|
-
# Increment base MCP steps
|
|
58
|
-
new_base_count = increment_base_mcp_steps()
|
|
59
|
-
span.set_attribute("hud.base_mcp_steps", new_base_count)
|
|
60
|
-
logger.debug("Incremented base MCP steps to %d", new_base_count)
|
|
61
|
-
|
|
62
|
-
elif step_type == "mcp_tool":
|
|
63
|
-
# Increment both base MCP and MCP tool steps
|
|
64
|
-
new_base_count = increment_base_mcp_steps()
|
|
65
|
-
new_tool_count = increment_mcp_tool_steps()
|
|
66
|
-
span.set_attribute("hud.base_mcp_steps", new_base_count)
|
|
67
|
-
span.set_attribute("hud.mcp_tool_steps", new_tool_count)
|
|
68
|
-
logger.debug(
|
|
69
|
-
"Incremented MCP steps to base=%d, tool=%d", new_base_count, new_tool_count
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
# Always set all current step counts on the span
|
|
73
|
-
span.set_attribute("hud.base_mcp_steps", get_base_mcp_steps())
|
|
74
|
-
span.set_attribute("hud.mcp_tool_steps", get_mcp_tool_steps())
|
|
75
|
-
span.set_attribute("hud.agent_steps", get_agent_steps())
|
|
76
|
-
|
|
77
|
-
except Exception as exc: # defensive; never fail the tracer
|
|
78
|
-
logger.debug("HudEnrichmentProcessor.on_start error: %s", exc, exc_info=False)
|
|
79
|
-
|
|
80
|
-
def _get_step_type(self, span: Span) -> str | None:
|
|
81
|
-
"""Determine what type of step this span represents.
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
'base_mcp' for any MCP span
|
|
85
|
-
'mcp_tool' for MCP tool calls (tools/call.mcp)
|
|
86
|
-
'agent' for agent spans
|
|
87
|
-
None if not a step
|
|
88
|
-
"""
|
|
89
|
-
# Check span attributes
|
|
90
|
-
attrs = span.attributes or {}
|
|
91
|
-
span_name = span.name
|
|
92
|
-
|
|
93
|
-
# Check for agent steps (instrumented with span_type="agent")
|
|
94
|
-
if attrs.get("category") == "agent":
|
|
95
|
-
return "agent"
|
|
96
|
-
|
|
97
|
-
# Check span name pattern for MCP calls
|
|
98
|
-
if span_name:
|
|
99
|
-
# tools/call.mcp is an mcp_tool step
|
|
100
|
-
if span_name == "tools/call.mcp":
|
|
101
|
-
return "mcp_tool"
|
|
102
|
-
|
|
103
|
-
# Any other .mcp suffixed span is a base MCP step
|
|
104
|
-
elif span_name.endswith(".mcp"):
|
|
105
|
-
return "base_mcp"
|
|
106
|
-
|
|
107
|
-
return None
|
|
108
|
-
|
|
109
|
-
def on_end(self, span: ReadableSpan) -> None:
|
|
110
|
-
# Nothing to do enrichment is on_start only
|
|
111
|
-
pass
|
|
112
|
-
|
|
113
|
-
# Required to fully implement abstract base, but we don't batch spans
|
|
114
|
-
def shutdown(self) -> None: # type: ignore[override]
|
|
115
|
-
pass
|
|
116
|
-
|
|
117
|
-
def force_flush(self, timeout_millis: int | None = None) -> bool: # type: ignore[override]
|
|
118
|
-
return True
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from opentelemetry import baggage
|
|
7
|
+
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
|
|
8
|
+
|
|
9
|
+
from .context import (
|
|
10
|
+
get_agent_steps,
|
|
11
|
+
get_base_mcp_steps,
|
|
12
|
+
get_mcp_tool_steps,
|
|
13
|
+
increment_agent_steps,
|
|
14
|
+
increment_base_mcp_steps,
|
|
15
|
+
increment_mcp_tool_steps,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HudEnrichmentProcessor(SpanProcessor):
|
|
22
|
+
"""Span processor that enriches every span with HUD-specific context.
|
|
23
|
+
|
|
24
|
+
• Adds ``hud.task_run_id`` attribute if available.
|
|
25
|
+
• Adds ``hud.job_id`` attribute if available in baggage.
|
|
26
|
+
• Adds ``hud.step_count`` attribute if available in baggage.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
# No state, everything comes from context vars
|
|
31
|
+
super().__init__()
|
|
32
|
+
|
|
33
|
+
# --- callback hooks -------------------------------------------------
|
|
34
|
+
def on_start(self, span: Span, parent_context: Any) -> None: # type: ignore[override]
|
|
35
|
+
try:
|
|
36
|
+
# Get task_run_id from baggage in parent context
|
|
37
|
+
run_id = baggage.get_baggage("hud.task_run_id", context=parent_context)
|
|
38
|
+
if run_id and span.is_recording():
|
|
39
|
+
span.set_attribute("hud.task_run_id", str(run_id))
|
|
40
|
+
|
|
41
|
+
# Get job_id from baggage if available
|
|
42
|
+
job_id = baggage.get_baggage("hud.job_id", context=parent_context)
|
|
43
|
+
if job_id and span.is_recording():
|
|
44
|
+
span.set_attribute("hud.job_id", str(job_id))
|
|
45
|
+
|
|
46
|
+
# Check what type of step this is and increment appropriate counters
|
|
47
|
+
if span.is_recording():
|
|
48
|
+
step_type = self._get_step_type(span)
|
|
49
|
+
|
|
50
|
+
if step_type == "agent":
|
|
51
|
+
# Increment agent steps
|
|
52
|
+
new_agent_count = increment_agent_steps()
|
|
53
|
+
span.set_attribute("hud.agent_steps", new_agent_count)
|
|
54
|
+
logger.debug("Incremented agent steps to %d", new_agent_count)
|
|
55
|
+
|
|
56
|
+
elif step_type == "base_mcp":
|
|
57
|
+
# Increment base MCP steps
|
|
58
|
+
new_base_count = increment_base_mcp_steps()
|
|
59
|
+
span.set_attribute("hud.base_mcp_steps", new_base_count)
|
|
60
|
+
logger.debug("Incremented base MCP steps to %d", new_base_count)
|
|
61
|
+
|
|
62
|
+
elif step_type == "mcp_tool":
|
|
63
|
+
# Increment both base MCP and MCP tool steps
|
|
64
|
+
new_base_count = increment_base_mcp_steps()
|
|
65
|
+
new_tool_count = increment_mcp_tool_steps()
|
|
66
|
+
span.set_attribute("hud.base_mcp_steps", new_base_count)
|
|
67
|
+
span.set_attribute("hud.mcp_tool_steps", new_tool_count)
|
|
68
|
+
logger.debug(
|
|
69
|
+
"Incremented MCP steps to base=%d, tool=%d", new_base_count, new_tool_count
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Always set all current step counts on the span
|
|
73
|
+
span.set_attribute("hud.base_mcp_steps", get_base_mcp_steps())
|
|
74
|
+
span.set_attribute("hud.mcp_tool_steps", get_mcp_tool_steps())
|
|
75
|
+
span.set_attribute("hud.agent_steps", get_agent_steps())
|
|
76
|
+
|
|
77
|
+
except Exception as exc: # defensive; never fail the tracer
|
|
78
|
+
logger.debug("HudEnrichmentProcessor.on_start error: %s", exc, exc_info=False)
|
|
79
|
+
|
|
80
|
+
def _get_step_type(self, span: Span) -> str | None:
|
|
81
|
+
"""Determine what type of step this span represents.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
'base_mcp' for any MCP span
|
|
85
|
+
'mcp_tool' for MCP tool calls (tools/call.mcp)
|
|
86
|
+
'agent' for agent spans
|
|
87
|
+
None if not a step
|
|
88
|
+
"""
|
|
89
|
+
# Check span attributes
|
|
90
|
+
attrs = span.attributes or {}
|
|
91
|
+
span_name = span.name
|
|
92
|
+
|
|
93
|
+
# Check for agent steps (instrumented with span_type="agent")
|
|
94
|
+
if attrs.get("category") == "agent":
|
|
95
|
+
return "agent"
|
|
96
|
+
|
|
97
|
+
# Check span name pattern for MCP calls
|
|
98
|
+
if span_name:
|
|
99
|
+
# tools/call.mcp is an mcp_tool step
|
|
100
|
+
if span_name == "tools/call.mcp":
|
|
101
|
+
return "mcp_tool"
|
|
102
|
+
|
|
103
|
+
# Any other .mcp suffixed span is a base MCP step
|
|
104
|
+
elif span_name.endswith(".mcp"):
|
|
105
|
+
return "base_mcp"
|
|
106
|
+
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
110
|
+
# Nothing to do enrichment is on_start only
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
# Required to fully implement abstract base, but we don't batch spans
|
|
114
|
+
def shutdown(self) -> None: # type: ignore[override]
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
def force_flush(self, timeout_millis: int | None = None) -> bool: # type: ignore[override]
|
|
118
|
+
return True
|
hud/otel/tests/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"""Tests for OpenTelemetry integration."""
|
|
1
|
+
"""Tests for OpenTelemetry integration."""
|