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

Files changed (64) hide show
  1. hud/__init__.py +14 -5
  2. hud/env/docker_client.py +1 -1
  3. hud/env/environment.py +10 -7
  4. hud/env/local_docker_client.py +1 -1
  5. hud/env/remote_client.py +1 -1
  6. hud/env/remote_docker_client.py +2 -2
  7. hud/exceptions.py +2 -1
  8. hud/mcp_agent/__init__.py +15 -0
  9. hud/mcp_agent/base.py +723 -0
  10. hud/mcp_agent/claude.py +316 -0
  11. hud/mcp_agent/langchain.py +231 -0
  12. hud/mcp_agent/openai.py +318 -0
  13. hud/mcp_agent/tests/__init__.py +1 -0
  14. hud/mcp_agent/tests/test_base.py +437 -0
  15. hud/settings.py +14 -2
  16. hud/task.py +4 -0
  17. hud/telemetry/__init__.py +11 -7
  18. hud/telemetry/_trace.py +82 -71
  19. hud/telemetry/context.py +9 -27
  20. hud/telemetry/exporter.py +6 -5
  21. hud/telemetry/instrumentation/mcp.py +174 -410
  22. hud/telemetry/mcp_models.py +13 -74
  23. hud/telemetry/tests/test_context.py +9 -6
  24. hud/telemetry/tests/test_trace.py +92 -61
  25. hud/tools/__init__.py +21 -0
  26. hud/tools/base.py +65 -0
  27. hud/tools/bash.py +137 -0
  28. hud/tools/computer/__init__.py +13 -0
  29. hud/tools/computer/anthropic.py +411 -0
  30. hud/tools/computer/hud.py +315 -0
  31. hud/tools/computer/openai.py +283 -0
  32. hud/tools/edit.py +290 -0
  33. hud/tools/executors/__init__.py +13 -0
  34. hud/tools/executors/base.py +331 -0
  35. hud/tools/executors/pyautogui.py +585 -0
  36. hud/tools/executors/tests/__init__.py +1 -0
  37. hud/tools/executors/tests/test_base_executor.py +338 -0
  38. hud/tools/executors/tests/test_pyautogui_executor.py +162 -0
  39. hud/tools/executors/xdo.py +503 -0
  40. hud/tools/helper/README.md +56 -0
  41. hud/tools/helper/__init__.py +9 -0
  42. hud/tools/helper/mcp_server.py +78 -0
  43. hud/tools/helper/server_initialization.py +115 -0
  44. hud/tools/helper/utils.py +58 -0
  45. hud/tools/playwright_tool.py +373 -0
  46. hud/tools/tests/__init__.py +3 -0
  47. hud/tools/tests/test_bash.py +152 -0
  48. hud/tools/tests/test_computer.py +52 -0
  49. hud/tools/tests/test_computer_actions.py +34 -0
  50. hud/tools/tests/test_edit.py +233 -0
  51. hud/tools/tests/test_init.py +27 -0
  52. hud/tools/tests/test_playwright_tool.py +183 -0
  53. hud/tools/tests/test_tools.py +154 -0
  54. hud/tools/tests/test_utils.py +156 -0
  55. hud/tools/utils.py +50 -0
  56. hud/types.py +10 -1
  57. hud/utils/tests/test_init.py +21 -0
  58. hud/utils/tests/test_version.py +1 -1
  59. hud/version.py +1 -1
  60. {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/METADATA +9 -6
  61. hud_python-0.3.0.dist-info/RECORD +124 -0
  62. hud_python-0.2.10.dist-info/RECORD +0 -85
  63. {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/WHEEL +0 -0
  64. {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,494 +2,258 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import time
5
- from contextlib import asynccontextmanager
6
5
  from typing import TYPE_CHECKING, Any
7
6
 
8
7
  if TYPE_CHECKING:
9
- from collections.abc import AsyncGenerator, Awaitable, Callable
8
+ from collections.abc import Awaitable, Callable
10
9
 
11
- # from dataclasses import dataclass # Likely unused now
12
- from mcp.shared.message import SessionMessage # type: ignore
13
-
14
- # MCP type imports for type checking and attribute access safety
15
- from mcp.types import ( # type: ignore
16
- JSONRPCError,
17
- JSONRPCNotification,
18
- JSONRPCRequest,
19
- JSONRPCResponse,
20
- )
21
- from wrapt import register_post_import_hook, wrap_function_wrapper
10
+ from wrapt import wrap_function_wrapper
22
11
 
23
12
  from hud.telemetry.context import (
24
- buffer_mcp_call,
25
13
  create_notification_record,
26
14
  create_request_record,
27
15
  create_response_record,
28
16
  get_current_task_run_id,
29
17
  )
30
- from hud.telemetry.mcp_models import DirectionType, MCPCallType, MCPManualTestCall, StatusType
18
+ from hud.telemetry.mcp_models import DirectionType, MCPCallType, StatusType
31
19
 
32
20
  logger = logging.getLogger(__name__)
33
21
 
34
22
 
35
23
  class MCPInstrumentor:
36
24
  """
37
- Instrumentor for MCP calls.
25
+ Context-aware instrumentor for MCP calls.
26
+ Only instruments MCP methods when there's an active trace context.
38
27
  """
39
28
 
40
29
  def __init__(self) -> None:
41
30
  self._installed = False
42
31
 
43
32
  def install(self) -> None:
44
- """Install instrumentation for MCP"""
45
- logger.debug("MCPInstrumentor: install() called")
33
+ """Install instrumentation for MCP - but only activate when trace context exists."""
34
+ logger.debug("MCPInstrumentor: install() called (context-aware mode)")
46
35
  if self._installed:
47
36
  logger.debug("MCP instrumentation already installed")
48
37
  return
49
38
 
50
- # Updated hooks: removed _transport_wrapper and _base_session_init_wrapper related hooks
51
- # if they become no-ops. For now, let's assume _transport_wrapper might still be used,
52
- # but simplified. _base_session_init_wrapper hook is removed.
53
- hooks = [
54
- ("mcp.client.sse", "sse_client", self._transport_wrapper),
55
- ("mcp.server.sse", "SseServerTransport.connect_sse", self._transport_wrapper),
56
- ("mcp.client.stdio", "stdio_client", self._transport_wrapper),
57
- ("mcp.server.stdio", "stdio_server", self._transport_wrapper),
58
- ("mcp.shared.session", "BaseSession._receive_loop", self._receive_loop_wrapper),
59
- ]
60
-
61
- def create_hook(module: str, func_name: str, wrapper: Any) -> Any:
62
- return lambda _: wrap_function_wrapper(module, func_name, wrapper)
63
-
64
- for module, func_name, wrapper in hooks:
65
- logger.debug(
66
- "MCPInstrumentor: Registering post-import hook for %s.%s", module, func_name
67
- )
68
- register_post_import_hook(
69
- create_hook(module, func_name, wrapper),
70
- module,
71
- )
72
-
73
- # Removed hook for BaseSession.__init__ (it was for _base_session_init_wrapper)
74
-
75
- logger.debug(
76
- "MCPInstrumentor: Attempting immediate instrumentation of already imported modules."
77
- )
78
- for module, func_name, wrapper in hooks:
79
- try:
80
- mod = __import__(module, fromlist=[func_name.split(".")[0]])
81
- target_obj = mod
82
- parts = func_name.split(".")
83
- for i, part in enumerate(parts):
84
- if hasattr(target_obj, part):
85
- if i == len(parts) - 1:
86
- target_obj = getattr(target_obj, part)
87
- else:
88
- target_obj = getattr(target_obj, part)
89
- else:
90
- target_obj = None
91
- break
92
-
93
- if target_obj and callable(target_obj):
94
- logger.debug("MCPInstrumentor: Wrapping %s.%s", module, func_name)
95
- wrap_function_wrapper(module, func_name, wrapper)
96
- else:
97
- logger.debug(
98
- "Function %s not found in %s during immediate instrumentation attempt",
99
- func_name,
100
- module,
101
- )
102
- except ImportError:
103
- logger.debug("Module %s not imported yet, post-import hook will cover it", module)
104
- except Exception as e:
105
- logger.warning("Failed to immediately wrap %s.%s: %s", module, func_name, e)
106
-
107
39
  try:
108
- # Import only for testing availability, don't store reference
109
- __import__("mcp.shared.session")
40
+ # Import and wrap the main session methods
41
+ import mcp.shared.session # noqa: F401
42
+
110
43
  wrap_function_wrapper(
111
44
  "mcp.shared.session",
112
45
  "BaseSession.send_request",
113
- self._send_request_telemetry_wrapper,
46
+ self._context_aware_send_request_wrapper,
114
47
  )
115
48
  wrap_function_wrapper(
116
49
  "mcp.shared.session",
117
50
  "BaseSession.send_notification",
118
- self._send_notification_telemetry_wrapper,
51
+ self._context_aware_send_notification_wrapper,
119
52
  )
120
- logger.debug("Successfully wrapped BaseSession methods for telemetry")
53
+
54
+ logger.debug("Successfully wrapped BaseSession methods for context-aware telemetry")
121
55
  except ImportError:
122
- logger.debug("mcp.shared.session not imported yet, post-import hook will cover it")
56
+ logger.debug("mcp.shared.session not available yet")
123
57
  except Exception as e:
124
58
  logger.warning("Failed to wrap BaseSession methods: %s", e)
125
59
 
126
60
  self._installed = True
127
- logger.debug("MCP instrumentation installed (simplified)")
128
-
129
- @asynccontextmanager
130
- async def _transport_wrapper(
131
- self, wrapped: Callable[..., Any], instance: Any, args: Any, kwargs: Any
132
- ) -> AsyncGenerator[Any, None]:
133
- """Wrap transport functions. Simplified: passes through original streams."""
134
- original_func_name = f"{wrapped.__module__}.{wrapped.__name__}"
135
- logger.debug(
136
- "MCPInstrumentor: _transport_wrapper called for %s (passthrough)", original_func_name
137
- )
61
+ logger.debug("MCP instrumentation installed (context-aware mode)")
138
62
 
139
- # No OTel context or HUD Task ID manipulation here anymore for transport layer itself.
140
- # Higher level wrappers (_send_request, _receive_loop) will handle HUD Task ID.
141
-
142
- async with wrapped(*args, **kwargs) as (read_stream, write_stream):
143
- logger.debug("_transport_wrapper: Yielding original streams for %s", original_func_name)
144
- yield read_stream, write_stream # Pass original streams directly
145
- logger.debug(
146
- "_transport_wrapper: Exited original stream context for %s", original_func_name
147
- )
148
-
149
- async def _receive_loop_wrapper(
63
+ async def _context_aware_send_request_wrapper(
150
64
  self,
151
65
  wrapped: Callable[[Any, Any, Any, Any], Awaitable[Any]],
152
66
  instance: Any,
153
67
  args: Any,
154
68
  kwargs: Any,
155
69
  ) -> Any:
156
- """
157
- Wrap _receive_loop to capture responses and handled incoming messages.
158
- """
159
- logger.debug("MCPInstrumentor: _receive_loop_wrapper called")
160
-
161
- orig_handle_incoming = instance._handle_incoming
162
-
163
- async def handle_incoming_with_telemetry(req_or_msg: Any) -> Any:
164
- start_time = time.time()
165
- hud_task_run_id = get_current_task_run_id()
166
- method_name = "unknown_method"
167
- message_id = None
168
- is_response_or_error_flag = False
169
- call_type_override = MCPCallType.HANDLE_INCOMING
170
-
171
- actual_message_root = None
172
- # SessionMessage
173
- if hasattr(req_or_msg, "message") and hasattr(req_or_msg.message, "root"):
174
- actual_message_root = req_or_msg.message.root
175
- elif hasattr(req_or_msg, "root"): # e.g. RequestResponder
176
- actual_message_root = req_or_msg.root
177
- elif isinstance(
178
- req_or_msg, JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError
179
- ):
180
- actual_message_root = req_or_msg
181
-
182
- if actual_message_root:
183
- if isinstance(actual_message_root, JSONRPCRequest):
184
- method_name = actual_message_root.method
185
- message_id = actual_message_root.id
186
- # call_type_override can remain HANDLE_INCOMING or be more specific if desired
187
- elif isinstance(actual_message_root, JSONRPCNotification):
188
- method_name = actual_message_root.method
189
- message_id = None # Notifications don't have IDs
190
- # call_type_override can remain HANDLE_INCOMING
191
- elif isinstance(actual_message_root, JSONRPCResponse | JSONRPCError):
192
- # This case implies _handle_incoming is processing a response/error directly
193
- # (e.g. an error encountered while trying to send/route a response previously)
194
- message_id = actual_message_root.id
195
- method_name = f"internal_response_handling_for_id_{message_id}"
196
- is_response_or_error_flag = True
197
- # Treat as receiving a response internally
198
- call_type_override = MCPCallType.RECEIVE_RESPONSE
199
- else:
200
- # Fallback for other types, if any, that might appear here
201
- if hasattr(actual_message_root, "method"):
202
- method_name = actual_message_root.method
203
- if hasattr(actual_message_root, "id"):
204
- message_id = actual_message_root.id
205
-
206
- record_data_base = {
207
- "method": method_name,
208
- "direction": DirectionType.RECEIVED,
209
- "call_type": call_type_override,
210
- "message_id": message_id,
211
- "is_response_or_error": is_response_or_error_flag, # For MCPRequestCall model
212
- }
213
-
214
- try:
215
- if hud_task_run_id:
216
- create_request_record(
217
- **record_data_base,
218
- status=StatusType.STARTED,
219
- start_time=start_time,
220
- # request_data might be populated if needed for HANDLE_INCOMING
221
- )
222
-
223
- result = await orig_handle_incoming(req_or_msg)
224
-
225
- if hud_task_run_id:
226
- create_request_record(
227
- **record_data_base,
228
- status=StatusType.COMPLETED,
229
- start_time=start_time,
230
- end_time=time.time(),
231
- duration=time.time() - start_time,
232
- # result_data might be populated if needed
233
- )
234
- return result
235
- except Exception as e:
236
- if hud_task_run_id:
237
- create_request_record(
238
- **record_data_base,
239
- status=StatusType.ERROR,
240
- start_time=start_time,
241
- end_time=time.time(),
242
- duration=time.time() - start_time,
243
- error=str(e),
244
- error_type=type(e).__name__,
245
- )
246
- raise
247
-
248
- instance._handle_incoming = handle_incoming_with_telemetry
249
-
250
- try:
251
- logger.debug("MCPInstrumentor: Entering instrumented _receive_loop section")
252
- # The original logic for distinguishing request/notification from response for
253
- # telemetry:
254
- # Ensure streams are context managed
255
- async with instance._read_stream, instance._write_stream:
256
- async for message in instance._read_stream:
257
- hud_task_run_id = get_current_task_run_id() # Get ID for each message
258
-
259
- if isinstance(message, Exception):
260
- await instance._handle_incoming(message) # Will be wrapped
261
- continue
262
-
263
- # Ensure we are dealing with SessionMessage
264
- if (
265
- not isinstance(message, SessionMessage)
266
- or not hasattr(message, "message")
267
- or not hasattr(message.message, "root")
268
- ):
269
- logger.warning(
270
- "Unexpected message type in _receive_loop: %s", type(message)
271
- )
272
- await instance._handle_incoming(
273
- RuntimeError(f"Unknown message type: {message}")
274
- )
275
- continue
276
-
277
- msg_root = message.message.root
278
-
279
- if isinstance(msg_root, JSONRPCRequest | JSONRPCNotification):
280
- # Let the (wrapped) _handle_incoming deal with these and record telemetry
281
- await instance._handle_incoming(message)
282
- elif isinstance(msg_root, JSONRPCResponse | JSONRPCError):
283
- # This is a direct response/error, record it here
284
- logger.debug(
285
- "MCPInstrumentor: Capturing direct response/error ID: %s", msg_root.id
286
- )
287
- if hud_task_run_id:
288
- is_error = isinstance(msg_root, JSONRPCError)
289
- error_message = None
290
- error_code = None
291
- if is_error and hasattr(msg_root, "error"):
292
- error_message = getattr(msg_root.error, "message", None)
293
- error_code = str(getattr(msg_root.error, "code", None))
294
-
295
- create_response_record(
296
- method=f"response_to_id_{msg_root.id}", # Consistent method naming
297
- related_request_id=msg_root.id,
298
- response_id=msg_root.id,
299
- is_error=is_error,
300
- response_data=msg_root.model_dump(exclude_none=True),
301
- error=error_message,
302
- error_type=error_code,
303
- direction=DirectionType.RECEIVED,
304
- call_type=MCPCallType.RECEIVE_RESPONSE,
305
- timestamp=time.time(), # Add timestamp here for accuracy
306
- )
307
-
308
- # Original logic to pass response to waiting stream
309
- stream = instance._response_streams.pop(msg_root.id, None)
310
- if stream:
311
- await stream.send(msg_root)
312
- else:
313
- # This case should ideally be handled by _handle_incoming if it's an
314
- # unsolicited response
315
- logger.warning(
316
- "Received response/error with unknown request ID %s and no "
317
- "response stream.",
318
- msg_root.id,
319
- )
320
- # Potentially pass to _handle_incoming if that's desired for unroutable
321
- # responses
322
- await instance._handle_incoming(message) # Let wrapped handler decide
323
- else:
324
- logger.warning(
325
- "Unknown message root type in _receive_loop: %s", type(msg_root)
326
- )
327
- await instance._handle_incoming(message) # Let wrapped handler decide
70
+ """Context-aware send request wrapper."""
71
+ hud_task_run_id = get_current_task_run_id()
328
72
 
329
- logger.debug("MCPInstrumentor: Exiting instrumented _receive_loop section")
330
- except Exception as e:
331
- logger.error("Error in instrumented receive loop: %s, falling back to original", e)
332
- instance._handle_incoming = orig_handle_incoming
333
- # Re-raising to ensure original behavior if full loop fails, or call wrapped directly
334
- # For simplicity now, we just log and let finally restore.
335
- raise
336
- finally:
337
- instance._handle_incoming = orig_handle_incoming
338
- logger.debug("MCPInstrumentor: Restored original _handle_incoming")
73
+ if not hud_task_run_id:
74
+ # No active trace - pass through without instrumentation
75
+ return await wrapped(*args, **kwargs)
339
76
 
340
- async def _send_request_telemetry_wrapper(
341
- self,
342
- wrapped: Callable[[Any, Any, Any, Any], Awaitable[Any]],
343
- instance: Any,
344
- args: Any,
345
- kwargs: Any,
346
- ) -> Any:
347
77
  start_time = time.time()
348
- hud_task_run_id = get_current_task_run_id()
78
+ request = args[0] if args else None
79
+
80
+ # Extract method name from the request
349
81
  method_name = "unknown_method"
350
- request_obj_data = None
351
- request_id = None
352
-
353
- if args and len(args) > 0:
354
- request_session_msg = args[0] # Assuming SessionMessage
355
- if hasattr(request_session_msg, "message") and hasattr(
356
- request_session_msg.message, "root"
357
- ):
358
- actual_request = request_session_msg.message.root
359
- if hasattr(actual_request, "method"):
360
- method_name = actual_request.method
361
- if hasattr(actual_request, "id"):
362
- request_id = actual_request.id
363
- if hasattr(actual_request, "model_dump"):
364
- try:
365
- request_obj_data = actual_request.model_dump(exclude_none=True)
366
- except Exception as e:
367
- logger.warning(
368
- "Could not dump request data for %s: %s",
369
- method_name,
370
- e,
371
- )
82
+ message_id = None
83
+ request_data = None
372
84
 
373
- record_data_base = {
374
- "method": method_name,
375
- "direction": DirectionType.SENT,
376
- "call_type": MCPCallType.SEND_REQUEST, # More specific type
377
- "request_id": request_id,
378
- "message_id": request_id,
379
- "request_data": request_obj_data,
380
- }
85
+ if request:
86
+ try:
87
+ # Get model dump for Pydantic models
88
+ if hasattr(request, "model_dump"):
89
+ request_data = request.model_dump(mode="json", exclude_none=True)
90
+ # Extract method from the dump
91
+ if isinstance(request_data, dict) and "method" in request_data:
92
+ method_name = request_data["method"]
93
+ # Fallback to direct method attribute
94
+ elif hasattr(request, "method"):
95
+ method_name = str(request.method)
96
+ request_data = {"method": method_name}
97
+ except Exception as e:
98
+ logger.debug("Failed to extract method: %s", e)
99
+
100
+ # Create the request record
101
+ create_request_record(
102
+ method=method_name,
103
+ message_id=message_id,
104
+ call_type=MCPCallType.SEND_REQUEST,
105
+ direction=DirectionType.SENT,
106
+ status=StatusType.STARTED,
107
+ start_time=start_time,
108
+ request_data=request_data,
109
+ )
381
110
 
382
111
  try:
383
- if hud_task_run_id:
384
- create_request_record(
385
- **record_data_base, status=StatusType.STARTED, start_time=start_time
386
- )
112
+ # Call the wrapped method and get the result
113
+ result = await wrapped(*args, **kwargs)
114
+
115
+ # Get the actual message_id from the session after sending
116
+ if hasattr(instance, "_request_id"):
117
+ # Current request ID minus 1 (since it was incremented)
118
+ message_id = getattr(instance, "_request_id", 1) - 1
119
+
120
+ # Update request record with completion
121
+ create_request_record(
122
+ method=method_name,
123
+ message_id=message_id,
124
+ call_type=MCPCallType.SEND_REQUEST,
125
+ direction=DirectionType.SENT,
126
+ status=StatusType.COMPLETED,
127
+ start_time=start_time,
128
+ end_time=time.time(),
129
+ duration=time.time() - start_time,
130
+ request_data=request_data,
131
+ )
387
132
 
388
- result = await wrapped(*args, **kwargs) # result here is usually None for send_request
389
-
390
- if hud_task_run_id:
391
- # For send_request, the 'result' is the response future, not the response itself.
392
- # Completion means the request was successfully sent to the transport.
393
- # The actual response is captured by _receive_loop_wrapper.
394
- create_request_record(
395
- **record_data_base,
396
- status=StatusType.COMPLETED, # Meaning: successfully dispatched
397
- start_time=start_time,
398
- end_time=time.time(),
399
- duration=time.time() - start_time,
133
+ # Capture the response
134
+ if result is not None:
135
+ response_data = None
136
+ try:
137
+ if hasattr(result, "model_dump"):
138
+ response_data = result.model_dump(mode="json", exclude_none=True)
139
+ else:
140
+ response_data = {"_type": type(result).__name__}
141
+ except Exception as e:
142
+ logger.debug("Failed to serialize response data: %s", e)
143
+ response_data = {"_type": type(result).__name__, "_error": str(e)}
144
+
145
+ create_response_record(
146
+ method=method_name,
147
+ related_request_id=message_id,
148
+ is_error=False,
149
+ message_id=message_id,
150
+ direction=DirectionType.RECEIVED,
151
+ call_type=MCPCallType.RECEIVE_RESPONSE,
152
+ response_data=response_data,
153
+ timestamp=time.time(),
400
154
  )
155
+
401
156
  return result
157
+
402
158
  except Exception as e:
403
- if hud_task_run_id:
404
- create_request_record(
405
- **record_data_base,
406
- status=StatusType.ERROR,
407
- start_time=start_time,
408
- end_time=time.time(),
409
- duration=time.time() - start_time,
410
- error=str(e),
411
- error_type=type(e).__name__,
412
- )
159
+ # Log the error
160
+ create_request_record(
161
+ method=method_name,
162
+ message_id=message_id,
163
+ call_type=MCPCallType.SEND_REQUEST,
164
+ direction=DirectionType.SENT,
165
+ status=StatusType.ERROR,
166
+ start_time=start_time,
167
+ end_time=time.time(),
168
+ duration=time.time() - start_time,
169
+ error=str(e),
170
+ error_type=type(e).__name__,
171
+ request_data=request_data,
172
+ )
173
+
174
+ # Also record error response
175
+ create_response_record(
176
+ method=method_name,
177
+ related_request_id=message_id,
178
+ is_error=True,
179
+ message_id=message_id,
180
+ direction=DirectionType.RECEIVED,
181
+ call_type=MCPCallType.RECEIVE_RESPONSE,
182
+ status=StatusType.ERROR,
183
+ error=str(e),
184
+ error_type=type(e).__name__,
185
+ timestamp=time.time(),
186
+ )
187
+
413
188
  raise
414
189
 
415
- async def _send_notification_telemetry_wrapper(
190
+ async def _context_aware_send_notification_wrapper(
416
191
  self,
417
192
  wrapped: Callable[[Any, Any, Any, Any], Awaitable[Any]],
418
193
  instance: Any,
419
194
  args: Any,
420
195
  kwargs: Any,
421
196
  ) -> Any:
422
- start_time = time.time()
197
+ """Context-aware send notification wrapper."""
423
198
  hud_task_run_id = get_current_task_run_id()
199
+
200
+ if not hud_task_run_id:
201
+ # No active trace - pass through without instrumentation
202
+ return await wrapped(*args, **kwargs)
203
+
204
+ start_time = time.time()
205
+ notification = args[0] if args else None
206
+
207
+ # Extract method name
424
208
  method_name = "unknown_method"
425
- notification_obj_data = None
426
-
427
- if args and len(args) > 0:
428
- notification_session_msg = args[0] # Assuming SessionMessage
429
- if hasattr(notification_session_msg, "message") and hasattr(
430
- notification_session_msg.message, "root"
431
- ):
432
- actual_notification = notification_session_msg.message.root
433
- if hasattr(actual_notification, "method"):
434
- method_name = actual_notification.method
435
- if hasattr(actual_notification, "model_dump"):
436
- try:
437
- notification_obj_data = actual_notification.model_dump(exclude_none=True)
438
- except Exception as e:
439
- logger.warning(
440
- "Could not dump notification data for %s: %s",
441
- method_name,
442
- e,
443
- )
209
+ notification_data = None
210
+
211
+ if notification:
212
+ try:
213
+ # Get model dump for Pydantic models
214
+ if hasattr(notification, "model_dump"):
215
+ notification_data = notification.model_dump(mode="json", exclude_none=True)
216
+ # Extract method from the dump
217
+ if isinstance(notification_data, dict) and "method" in notification_data:
218
+ method_name = notification_data["method"]
219
+ # Fallback to direct method attribute
220
+ elif hasattr(notification, "method"):
221
+ method_name = str(notification.method)
222
+ notification_data = {"method": method_name}
223
+ except Exception as e:
224
+ logger.debug("Failed to extract notification method: %s", e)
444
225
 
445
226
  record_data_base = {
446
227
  "method": method_name,
228
+ "message_id": None, # Notifications don't have IDs
229
+ "call_type": MCPCallType.SEND_NOTIFICATION,
447
230
  "direction": DirectionType.SENT,
448
- "call_type": MCPCallType.SEND_NOTIFICATION, # More specific type
449
- "notification_data": notification_obj_data,
231
+ "notification_data": notification_data,
450
232
  }
451
233
 
452
234
  try:
453
- if hud_task_run_id:
454
- create_notification_record(
455
- **record_data_base, status=StatusType.STARTED, start_time=start_time
456
- )
235
+ create_notification_record(
236
+ **record_data_base, status=StatusType.STARTED, start_time=start_time
237
+ )
457
238
 
458
- result = await wrapped(*args, **kwargs) # Usually None
239
+ result = await wrapped(*args, **kwargs)
459
240
 
460
- if hud_task_run_id:
461
- create_notification_record(
462
- **record_data_base,
463
- status=StatusType.COMPLETED,
464
- start_time=start_time,
465
- end_time=time.time(),
466
- duration=time.time() - start_time,
467
- )
241
+ create_notification_record(
242
+ **record_data_base,
243
+ status=StatusType.COMPLETED,
244
+ start_time=start_time,
245
+ end_time=time.time(),
246
+ duration=time.time() - start_time,
247
+ )
468
248
  return result
469
249
  except Exception as e:
470
- if hud_task_run_id:
471
- create_notification_record(
472
- **record_data_base,
473
- status=StatusType.ERROR,
474
- start_time=start_time,
475
- end_time=time.time(),
476
- duration=time.time() - start_time,
477
- error=str(e),
478
- error_type=type(e).__name__,
479
- )
250
+ create_notification_record(
251
+ **record_data_base,
252
+ status=StatusType.ERROR,
253
+ start_time=start_time,
254
+ end_time=time.time(),
255
+ duration=time.time() - start_time,
256
+ error=str(e),
257
+ error_type=type(e).__name__,
258
+ )
480
259
  raise
481
-
482
- def record_manual_test(self, **kwargs: Any) -> bool:
483
- """Record a manual test telemetry entry"""
484
- hud_task_run_id = get_current_task_run_id()
485
- if not hud_task_run_id:
486
- logger.warning("Manual test not recorded: No active task_run_id")
487
- return False
488
- try:
489
- record = MCPManualTestCall.create(task_run_id=hud_task_run_id, **kwargs)
490
- buffer_mcp_call(record) # buffer_mcp_call handles Pydantic model directly
491
- logger.debug("Manual test recorded: %s", record.model_dump(exclude_none=True))
492
- return True
493
- except Exception as e:
494
- logger.warning("Manual test not recorded: %s", e)
495
- return False