connectonion 0.5.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. connectonion/__init__.py +78 -0
  2. connectonion/address.py +320 -0
  3. connectonion/agent.py +450 -0
  4. connectonion/announce.py +84 -0
  5. connectonion/asgi.py +287 -0
  6. connectonion/auto_debug_exception.py +181 -0
  7. connectonion/cli/__init__.py +3 -0
  8. connectonion/cli/browser_agent/__init__.py +5 -0
  9. connectonion/cli/browser_agent/browser.py +243 -0
  10. connectonion/cli/browser_agent/prompt.md +107 -0
  11. connectonion/cli/commands/__init__.py +1 -0
  12. connectonion/cli/commands/auth_commands.py +527 -0
  13. connectonion/cli/commands/browser_commands.py +27 -0
  14. connectonion/cli/commands/create.py +511 -0
  15. connectonion/cli/commands/deploy_commands.py +220 -0
  16. connectonion/cli/commands/doctor_commands.py +173 -0
  17. connectonion/cli/commands/init.py +469 -0
  18. connectonion/cli/commands/project_cmd_lib.py +828 -0
  19. connectonion/cli/commands/reset_commands.py +149 -0
  20. connectonion/cli/commands/status_commands.py +168 -0
  21. connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
  22. connectonion/cli/docs/connectonion.md +1256 -0
  23. connectonion/cli/docs.md +123 -0
  24. connectonion/cli/main.py +148 -0
  25. connectonion/cli/templates/meta-agent/README.md +287 -0
  26. connectonion/cli/templates/meta-agent/agent.py +196 -0
  27. connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
  28. connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
  29. connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
  30. connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
  31. connectonion/cli/templates/minimal/README.md +56 -0
  32. connectonion/cli/templates/minimal/agent.py +40 -0
  33. connectonion/cli/templates/playwright/README.md +118 -0
  34. connectonion/cli/templates/playwright/agent.py +336 -0
  35. connectonion/cli/templates/playwright/prompt.md +102 -0
  36. connectonion/cli/templates/playwright/requirements.txt +3 -0
  37. connectonion/cli/templates/web-research/agent.py +122 -0
  38. connectonion/connect.py +128 -0
  39. connectonion/console.py +539 -0
  40. connectonion/debug_agent/__init__.py +13 -0
  41. connectonion/debug_agent/agent.py +45 -0
  42. connectonion/debug_agent/prompts/debug_assistant.md +72 -0
  43. connectonion/debug_agent/runtime_inspector.py +406 -0
  44. connectonion/debug_explainer/__init__.py +10 -0
  45. connectonion/debug_explainer/explain_agent.py +114 -0
  46. connectonion/debug_explainer/explain_context.py +263 -0
  47. connectonion/debug_explainer/explainer_prompt.md +29 -0
  48. connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
  49. connectonion/debugger_ui.py +1039 -0
  50. connectonion/decorators.py +208 -0
  51. connectonion/events.py +248 -0
  52. connectonion/execution_analyzer/__init__.py +9 -0
  53. connectonion/execution_analyzer/execution_analysis.py +93 -0
  54. connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
  55. connectonion/host.py +579 -0
  56. connectonion/interactive_debugger.py +342 -0
  57. connectonion/llm.py +801 -0
  58. connectonion/llm_do.py +307 -0
  59. connectonion/logger.py +300 -0
  60. connectonion/prompt_files/__init__.py +1 -0
  61. connectonion/prompt_files/analyze_contact.md +62 -0
  62. connectonion/prompt_files/eval_expected.md +12 -0
  63. connectonion/prompt_files/react_evaluate.md +11 -0
  64. connectonion/prompt_files/react_plan.md +16 -0
  65. connectonion/prompt_files/reflect.md +22 -0
  66. connectonion/prompts.py +144 -0
  67. connectonion/relay.py +200 -0
  68. connectonion/static/docs.html +688 -0
  69. connectonion/tool_executor.py +279 -0
  70. connectonion/tool_factory.py +186 -0
  71. connectonion/tool_registry.py +105 -0
  72. connectonion/trust.py +166 -0
  73. connectonion/trust_agents.py +71 -0
  74. connectonion/trust_functions.py +88 -0
  75. connectonion/tui/__init__.py +57 -0
  76. connectonion/tui/divider.py +39 -0
  77. connectonion/tui/dropdown.py +251 -0
  78. connectonion/tui/footer.py +31 -0
  79. connectonion/tui/fuzzy.py +56 -0
  80. connectonion/tui/input.py +278 -0
  81. connectonion/tui/keys.py +35 -0
  82. connectonion/tui/pick.py +130 -0
  83. connectonion/tui/providers.py +155 -0
  84. connectonion/tui/status_bar.py +163 -0
  85. connectonion/usage.py +161 -0
  86. connectonion/useful_events_handlers/__init__.py +16 -0
  87. connectonion/useful_events_handlers/reflect.py +116 -0
  88. connectonion/useful_plugins/__init__.py +20 -0
  89. connectonion/useful_plugins/calendar_plugin.py +163 -0
  90. connectonion/useful_plugins/eval.py +139 -0
  91. connectonion/useful_plugins/gmail_plugin.py +162 -0
  92. connectonion/useful_plugins/image_result_formatter.py +127 -0
  93. connectonion/useful_plugins/re_act.py +78 -0
  94. connectonion/useful_plugins/shell_approval.py +159 -0
  95. connectonion/useful_tools/__init__.py +44 -0
  96. connectonion/useful_tools/diff_writer.py +192 -0
  97. connectonion/useful_tools/get_emails.py +183 -0
  98. connectonion/useful_tools/gmail.py +1596 -0
  99. connectonion/useful_tools/google_calendar.py +613 -0
  100. connectonion/useful_tools/memory.py +380 -0
  101. connectonion/useful_tools/microsoft_calendar.py +604 -0
  102. connectonion/useful_tools/outlook.py +488 -0
  103. connectonion/useful_tools/send_email.py +205 -0
  104. connectonion/useful_tools/shell.py +97 -0
  105. connectonion/useful_tools/slash_command.py +201 -0
  106. connectonion/useful_tools/terminal.py +285 -0
  107. connectonion/useful_tools/todo_list.py +241 -0
  108. connectonion/useful_tools/web_fetch.py +216 -0
  109. connectonion/xray.py +467 -0
  110. connectonion-0.5.8.dist-info/METADATA +741 -0
  111. connectonion-0.5.8.dist-info/RECORD +113 -0
  112. connectonion-0.5.8.dist-info/WHEEL +4 -0
  113. connectonion-0.5.8.dist-info/entry_points.txt +3 -0
connectonion/xray.py ADDED
@@ -0,0 +1,467 @@
1
+ """
2
+ Purpose: Provide runtime debugging context and visual trace for AI agent tool execution
3
+ LLM-Note:
4
+ Dependencies: imports from [inspect, builtins, typing] | imported by [tool_executor.py, __init__.py] | tested by [tests/test_xray_class.py, tests/test_xray_without_decorator.py, tests/test_xray_auto_trace.py]
5
+ Data flow: receives from tool_executor → inject_xray_context(agent, user_prompt, messages, iteration, previous_tools) → stores in builtins.xray global → tool accesses xray.agent, xray.task, etc. → tool calls xray.trace() to display formatted execution history → clear_xray_context() after execution
6
+ State/Effects: modifies builtins namespace by injecting global 'xray' object | stores thread-local context in XrayDecorator instance (_agent, _user_prompt, _messages, _iteration, _previous_tools) | clears context after tool execution | no file I/O or persistence
7
+ Integration: exposes @xray decorator, xray global object with .agent, .task, .user_prompt, .messages, .iteration, .previous_tools properties, .trace() method | inject_xray_context(), clear_xray_context(), is_xray_enabled() helper functions | tool_executor checks __xray_enabled__ attribute to auto-print Rich tables
8
+ Performance: lightweight context storage | trace() uses stack inspection to find agent instance | smart value formatting with truncation for strings (400 chars), lists, dicts, DataFrames, Images
9
+ Errors: trace() handles missing agent gracefully with helpful messages | handles missing current_session | handles empty execution history
10
+
11
+ ConnectOnion XRay Debugging Tool
12
+
13
+ This module provides the @xray decorator and xray context for debugging AI agent tools.
14
+ See everything your agent is thinking during tool execution.
15
+
16
+ Usage:
17
+ from connectonion.xray import xray
18
+
19
+ @xray
20
+ def my_tool(query: str):
21
+ print(xray.agent.name) # Access agent context
22
+ print(xray.task) # Access user prompt
23
+ xray.trace() # Display execution trace
24
+ return result
25
+ """
26
+
27
+ import inspect
28
+ import builtins
29
+ from typing import Any, Callable, Optional
30
+
31
+
32
+ class XrayDecorator:
33
+ """
34
+ Simple xray decorator that provides context access and auto-tracing.
35
+
36
+ Usage:
37
+ @xray # Auto-print trace after execution
38
+ @xray(trace=False) # No auto-trace
39
+ @xray(rich=False) # Simple text output
40
+
41
+ def my_tool(query: str):
42
+ print(xray.agent.name) # Access agent context
43
+ print(xray.task) # Access user prompt
44
+ xray.trace() # Manual trace display
45
+ return result
46
+ """
47
+
48
+ def __init__(self):
49
+ """Initialize with empty context."""
50
+ # Store context directly (no wrapper class needed)
51
+ self._agent = None
52
+ self._user_prompt = None
53
+ self._messages = []
54
+ self._iteration = None
55
+ self._previous_tools = []
56
+
57
+ # Make available globally as 'xray' for easy access
58
+ builtins.xray = self
59
+
60
+ def __call__(self, func: Optional[Callable] = None, *, trace: bool = True, rich: bool = True) -> Any:
61
+ """
62
+ Decorator that marks functions for auto-tracing.
63
+
64
+ @xray # Auto-print trace after execution (Rich format)
65
+ @xray(trace=False) # No auto-trace
66
+ @xray(rich=False) # Simple text output
67
+
68
+ The actual tracing logic is handled by tool_executor.py which checks
69
+ the __xray_enabled__ and __xray_rich__ attributes.
70
+
71
+ Args:
72
+ func: Function to decorate (optional)
73
+ trace: Enable automatic tracing (default: True)
74
+ rich: Use Rich formatting for trace output (default: True)
75
+
76
+ Returns:
77
+ Decorated function (no wrapper) with __xray_enabled__ attribute
78
+ """
79
+ def decorator(f):
80
+ # Mark the function with xray settings
81
+ f.__xray_enabled__ = trace
82
+ f.__xray_rich__ = rich
83
+ return f
84
+
85
+ # Handle different call patterns
86
+ if func is None:
87
+ # Called with parentheses: @xray() or @xray(trace=False)
88
+ return decorator
89
+ else:
90
+ # Called without parentheses: @xray
91
+ return decorator(func)
92
+
93
+ # -------------------------------------------------------------------------
94
+ # Properties for accessing context data
95
+ # -------------------------------------------------------------------------
96
+
97
+ @property
98
+ def agent(self):
99
+ """The Agent instance that called this tool."""
100
+ return self._agent
101
+
102
+ @property
103
+ def task(self):
104
+ """The original user prompt/task (alias for user_prompt)."""
105
+ return self._user_prompt
106
+
107
+ @property
108
+ def user_prompt(self):
109
+ """The original user prompt string from agent.input()."""
110
+ return self._user_prompt
111
+
112
+ @property
113
+ def messages(self):
114
+ """Complete conversation history (the prompt)."""
115
+ return self._messages
116
+
117
+ @property
118
+ def iteration(self):
119
+ """Current iteration number in the agent loop."""
120
+ return self._iteration
121
+
122
+ @property
123
+ def previous_tools(self):
124
+ """List of tools called in previous iterations."""
125
+ return self._previous_tools
126
+
127
+ def _update(self, agent, user_prompt, messages, iteration, previous_tools):
128
+ """Internal: Update context (called by tool_executor before tool runs)."""
129
+ self._agent = agent
130
+ self._user_prompt = user_prompt
131
+ self._messages = messages
132
+ self._iteration = iteration
133
+ self._previous_tools = previous_tools
134
+
135
+ def _clear(self):
136
+ """Internal: Clear context after tool execution."""
137
+ self._agent = None
138
+ self._user_prompt = None
139
+ self._messages = []
140
+ self._iteration = None
141
+ self._previous_tools = []
142
+
143
+ def __repr__(self):
144
+ """Provide helpful representation for debugging."""
145
+ if not self._agent:
146
+ return "<xray (no active context)>"
147
+
148
+ agent_name = self._agent.name if self._agent else 'None'
149
+ prompt_preview = (self._user_prompt[:50] + '...') if self._user_prompt and len(self._user_prompt) > 50 else self._user_prompt
150
+
151
+ lines = [
152
+ f"<xray active>",
153
+ f" agent: '{agent_name}'",
154
+ f" task: '{prompt_preview}'",
155
+ f" iteration: {self._iteration}",
156
+ f" messages: {len(self._messages)} items",
157
+ ]
158
+
159
+ if self._previous_tools:
160
+ lines.append(f" previous_tools: {self._previous_tools}")
161
+
162
+ return '\n'.join(lines)
163
+
164
+ def trace(self):
165
+ """
166
+ Display a visual trace of tool execution flow.
167
+
168
+ Uses stack inspection to find the agent instance, so it works
169
+ from anywhere in the call stack (inside tools, breakpoints, etc.)
170
+
171
+ Usage:
172
+ # Within a tool or anywhere in the call stack:
173
+ xray.trace() # Shows current execution flow
174
+
175
+ # In debugging session with breakpoint:
176
+ >>> xray.trace()
177
+ Task: "Analyze customer feedback and generate report"
178
+
179
+ [1] • 89ms analyze_document(text="Dear customer, Thank you for...")
180
+ IN → text: <string: 15,234 chars> "Dear customer, Thank you for..."
181
+ OUT ← {sentiment: "positive", topics: ["refund", "satisfaction"]}
182
+
183
+ [2] • 340ms process_image(image=<...>, enhance=true)
184
+ IN → image: <Image: JPEG 1920x1080, 2.3MB>
185
+ IN → enhance: true
186
+ OUT ← <Image: JPEG 1920x1080, 1.8MB, enhanced>
187
+
188
+ Total: 429ms • 2 steps • 1 iterations
189
+
190
+ Visual Format:
191
+ - Step numbers in brackets: [1], [2], etc.
192
+ - Timing shown after bullet (•) or ERROR/pending indicator
193
+ - Function signature shows first 2 params inline, rest as "..."
194
+ - IN → shows input parameters (one per line)
195
+ - OUT ← shows return values
196
+ - ERR ✗ shows errors
197
+ - Smart truncation for long strings, images, DataFrames
198
+ """
199
+ # Use stack inspection to find agent instance
200
+ target_agent = None
201
+ for frame_info in inspect.stack():
202
+ frame_locals = frame_info.frame.f_locals
203
+
204
+ # Look for 'agent' in local variables
205
+ if 'agent' in frame_locals:
206
+ potential_agent = frame_locals['agent']
207
+ # Check if it has current_session (duck typing for Agent)
208
+ if hasattr(potential_agent, 'current_session'):
209
+ target_agent = potential_agent
210
+ break
211
+
212
+ # Also check 'self' in case we're in an agent method
213
+ if 'self' in frame_locals:
214
+ potential_agent = frame_locals['self']
215
+ if hasattr(potential_agent, 'current_session'):
216
+ target_agent = potential_agent
217
+ break
218
+
219
+ if not target_agent:
220
+ print("xray.trace() could not find agent in call stack.")
221
+ print("Make sure you're calling this from within a tool or agent method.")
222
+ return
223
+
224
+ if not target_agent.current_session:
225
+ print("No active session found on agent.")
226
+ print("Make sure the agent has been run with .input() first.")
227
+ return
228
+
229
+ # Get trace data from agent session
230
+ execution_history = [
231
+ entry for entry in target_agent.current_session.get('trace', [])
232
+ if entry.get('type') == 'tool_execution'
233
+ ]
234
+ user_prompt = target_agent.current_session.get('user_prompt', '')
235
+
236
+ if not execution_history:
237
+ print("No tool execution history available.")
238
+ print("Make sure the agent has executed some tools first.")
239
+ return
240
+
241
+ # Display the prompt that was executed
242
+ if user_prompt:
243
+ print(f'User Prompt: "{user_prompt}"')
244
+ print()
245
+
246
+ # Display each tool execution with visual formatting
247
+ for i, entry in enumerate(execution_history, 1):
248
+ # Format timing with appropriate precision (timing is in milliseconds)
249
+ timing = entry.get('timing', 0)
250
+ if timing >= 1000:
251
+ timing_str = f"{timing/1000:.1f}s" # Show seconds for long operations
252
+ elif timing >= 1:
253
+ timing_str = f"{timing:.0f}ms" # Whole milliseconds
254
+ else:
255
+ timing_str = f"{timing:.2f}ms" # Sub-millisecond precision
256
+
257
+ # Format function call
258
+ func_name = entry.get('tool_name', 'unknown')
259
+ # Check both 'arguments' (new format) and 'parameters' (old format)
260
+ params = entry.get('arguments', entry.get('parameters', {}))
261
+
262
+ # Build parameter preview for function signature
263
+ # Shows first 2 params inline to keep the main line readable
264
+ param_preview = []
265
+ for k, v in list(params.items())[:2]: # Show first 2 params in signature
266
+ param_preview.append(f"{k}={self._format_value_preview(v)}")
267
+ if len(params) > 2:
268
+ param_preview.append("...") # Indicate more params exist
269
+
270
+ func_call = f"{func_name}({', '.join(param_preview)})"
271
+
272
+ # Status indicators for visual clarity
273
+ status = entry.get('status', 'success')
274
+ if status == 'error':
275
+ prefix = "ERROR" # Clearly mark errors
276
+ elif status == 'pending':
277
+ timing_str = "..." # Show operation in progress
278
+ prefix = "..."
279
+ else:
280
+ prefix = "•" # Success indicator
281
+
282
+ # Print main execution line with aligned columns
283
+ print(f"[{i}] {prefix} {timing_str:<6} {func_call}")
284
+
285
+ # Print input parameters (one per line for readability)
286
+ for param_name, param_value in params.items():
287
+ formatted_value = self._format_value_full(param_value)
288
+ print(f" IN → {param_name}: {formatted_value}")
289
+
290
+ # Print result or error based on status
291
+ if status == 'error':
292
+ error = entry.get('error', 'Unknown error')
293
+ print(f" ERR ✗ {error}")
294
+ elif status == 'pending':
295
+ print(f" ⋯ pending")
296
+ else:
297
+ result = entry.get('result')
298
+ formatted_result = self._format_value_full(result)
299
+ print(f" OUT ← {formatted_result}")
300
+
301
+ # Add spacing between entries for readability
302
+ if i < len(execution_history):
303
+ print()
304
+
305
+ # Summary line with total execution statistics
306
+ total_time = sum(e.get('timing', 0) for e in execution_history if e.get('timing'))
307
+ iterations = target_agent.current_session.get('iteration', 1)
308
+
309
+ # Format total time with same rules as individual timings
310
+ if total_time >= 1000:
311
+ total_str = f"{total_time/1000:.1f}s"
312
+ elif total_time >= 1:
313
+ total_str = f"{total_time:.0f}ms"
314
+ else:
315
+ total_str = f"{total_time:.2f}ms"
316
+
317
+ print(f"\nTotal: {total_str} • {len(execution_history)} steps • {iterations} iterations")
318
+
319
+ def _format_value_preview(self, value):
320
+ """
321
+ Format a value for compact display in function signature.
322
+
323
+ Used in the main execution line to show parameter values inline
324
+ without taking too much horizontal space.
325
+
326
+ Args:
327
+ value: Any parameter value to format
328
+
329
+ Returns:
330
+ Compact string representation (max ~50 chars)
331
+ """
332
+ if value is None:
333
+ return "None"
334
+ elif isinstance(value, str):
335
+ if len(value) > 50:
336
+ return f'"{value[:50]}..."'
337
+ return repr(value)
338
+ elif isinstance(value, (int, float, bool)):
339
+ return str(value)
340
+ elif isinstance(value, dict):
341
+ return "{...}" # Just indicate it's a dict
342
+ elif isinstance(value, list):
343
+ return "[...]" # Just indicate it's a list
344
+ else:
345
+ return "..." # Unknown type
346
+
347
+ def _format_value_full(self, value):
348
+ """
349
+ Format a value for full display with smart truncation.
350
+
351
+ Used in the detailed parameter/result lines. Provides more detail
352
+ than preview format while still keeping output manageable.
353
+
354
+ Truncation strategies:
355
+ - Strings: Show first 400 chars (~4 sentences) with total length
356
+ - Lists: Show item count if > 3 items
357
+ - Dicts: Show first 3 keys if large
358
+ - DataFrames: Show dimensions (rows × columns)
359
+ - Images: Show format, dimensions, and estimated size
360
+
361
+ Args:
362
+ value: Any value to format for display
363
+
364
+ Returns:
365
+ Formatted string with smart truncation applied
366
+ """
367
+ if value is None:
368
+ return "None"
369
+ elif isinstance(value, str):
370
+ # Show up to ~4 sentences worth of text (roughly 400 chars)
371
+ if len(value) > 400:
372
+ preview = value[:400].replace('\n', ' ')
373
+ return f'<string: {len(value):,} chars> "{preview}..."'
374
+ return repr(value)
375
+ elif isinstance(value, (int, float)):
376
+ return str(value)
377
+ elif isinstance(value, bool):
378
+ return str(value)
379
+ elif isinstance(value, dict):
380
+ # Show compact dict representation
381
+ if len(str(value)) <= 80:
382
+ return str(value)
383
+ # Show keys for large dicts
384
+ keys = list(value.keys())[:3]
385
+ key_str = ", ".join(f"{k}: ..." for k in keys)
386
+ if len(value) > 3:
387
+ key_str += f", ... ({len(value)-3} more)"
388
+ return f"{{{key_str}}}"
389
+ elif isinstance(value, list):
390
+ if len(value) == 0:
391
+ return "[]"
392
+ elif len(value) <= 3 and len(str(value)) <= 80:
393
+ return str(value)
394
+ else:
395
+ return f"[{len(value)} items]"
396
+ elif hasattr(value, '__class__'):
397
+ # Handle custom objects
398
+ class_name = value.__class__.__name__
399
+
400
+ # Special handling for common ML/data objects
401
+ if 'DataFrame' in class_name:
402
+ # Try to get shape info
403
+ if hasattr(value, 'shape'):
404
+ rows, cols = value.shape
405
+ return f"<DataFrame: {rows:,} rows × {cols} columns>"
406
+ return f"<{class_name}>"
407
+ elif 'Image' in class_name or 'PIL' in str(type(value)):
408
+ # Handle image objects
409
+ if hasattr(value, 'size'):
410
+ w, h = value.size
411
+ format_str = getattr(value, 'format', 'Unknown')
412
+ # Estimate size (rough)
413
+ size_mb = (w * h * 3) / (1024 * 1024)
414
+ return f"<Image: {format_str} {w}x{h}, {size_mb:.1f}MB>"
415
+ return f"<{class_name}>"
416
+ else:
417
+ return f"<{class_name} object>"
418
+ else:
419
+ return str(type(value).__name__)
420
+
421
+
422
+ # Create the global xray instance
423
+ xray = XrayDecorator()
424
+
425
+
426
+ # =============================================================================
427
+ # Helper Functions for Tool Executor Integration
428
+ # =============================================================================
429
+
430
+ def inject_xray_context(agent, user_prompt: str, messages: list,
431
+ iteration: int, previous_tools: list) -> None:
432
+ """
433
+ Inject debugging context before tool execution.
434
+
435
+ This is called internally by tool_executor before running a tool,
436
+ making xray.agent, xray.task, etc. available inside the tool.
437
+
438
+ Args:
439
+ agent: The Agent instance
440
+ user_prompt: Original user prompt string from agent.input()
441
+ messages: Conversation history
442
+ iteration: Current iteration number
443
+ previous_tools: List of previously called tool names
444
+ """
445
+ xray._update(agent, user_prompt, messages, iteration, previous_tools)
446
+
447
+
448
+ def clear_xray_context() -> None:
449
+ """
450
+ Clear debugging context after tool execution.
451
+
452
+ This is called internally by tool_executor to prevent context leakage.
453
+ """
454
+ xray._clear()
455
+
456
+
457
+ def is_xray_enabled(func: Callable) -> bool:
458
+ """
459
+ Check if a function has the @xray decorator.
460
+
461
+ Args:
462
+ func: Function to check
463
+
464
+ Returns:
465
+ True if function is decorated with @xray
466
+ """
467
+ return getattr(func, '__xray_enabled__', False)