code-puppy 0.0.373__py3-none-any.whl → 0.0.374__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 (26) hide show
  1. code_puppy/agents/agent_creator_agent.py +49 -1
  2. code_puppy/agents/agent_helios.py +122 -0
  3. code_puppy/agents/agent_manager.py +26 -2
  4. code_puppy/agents/json_agent.py +30 -7
  5. code_puppy/command_line/colors_menu.py +2 -0
  6. code_puppy/command_line/command_handler.py +1 -0
  7. code_puppy/command_line/config_commands.py +3 -1
  8. code_puppy/command_line/uc_menu.py +890 -0
  9. code_puppy/config.py +29 -0
  10. code_puppy/messaging/messages.py +18 -0
  11. code_puppy/messaging/rich_renderer.py +35 -0
  12. code_puppy/messaging/subagent_console.py +0 -1
  13. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  14. code_puppy/plugins/universal_constructor/models.py +138 -0
  15. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  16. code_puppy/plugins/universal_constructor/registry.py +304 -0
  17. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  18. code_puppy/tools/__init__.py +138 -1
  19. code_puppy/tools/universal_constructor.py +889 -0
  20. {code_puppy-0.0.373.dist-info → code_puppy-0.0.374.dist-info}/METADATA +1 -1
  21. {code_puppy-0.0.373.dist-info → code_puppy-0.0.374.dist-info}/RECORD +26 -18
  22. {code_puppy-0.0.373.data → code_puppy-0.0.374.data}/data/code_puppy/models.json +0 -0
  23. {code_puppy-0.0.373.data → code_puppy-0.0.374.data}/data/code_puppy/models_dev_api.json +0 -0
  24. {code_puppy-0.0.373.dist-info → code_puppy-0.0.374.dist-info}/WHEEL +0 -0
  25. {code_puppy-0.0.373.dist-info → code_puppy-0.0.374.dist-info}/entry_points.txt +0 -0
  26. {code_puppy-0.0.373.dist-info → code_puppy-0.0.374.dist-info}/licenses/LICENSE +0 -0
code_puppy/config.py CHANGED
@@ -101,6 +101,9 @@ PACK_AGENT_NAMES = frozenset(
101
101
  ]
102
102
  )
103
103
 
104
+ # Agents that require Universal Constructor to be enabled
105
+ UC_AGENT_NAMES = frozenset(["helios"])
106
+
104
107
 
105
108
  def get_pack_agents_enabled() -> bool:
106
109
  """Return True if pack agents are enabled (default False).
@@ -117,6 +120,30 @@ def get_pack_agents_enabled() -> bool:
117
120
  return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
118
121
 
119
122
 
123
+ def get_universal_constructor_enabled() -> bool:
124
+ """Return True if the Universal Constructor is enabled (default True).
125
+
126
+ The Universal Constructor allows agents to dynamically create, manage,
127
+ and execute custom tools at runtime. When enabled, agents can extend
128
+ their capabilities by writing Python code that becomes callable tools.
129
+
130
+ When False, the universal_constructor tool is not registered with agents.
131
+ """
132
+ cfg_val = get_value("enable_universal_constructor")
133
+ if cfg_val is None:
134
+ return True # Enabled by default
135
+ return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
136
+
137
+
138
+ def set_universal_constructor_enabled(enabled: bool) -> None:
139
+ """Enable or disable the Universal Constructor.
140
+
141
+ Args:
142
+ enabled: True to enable, False to disable
143
+ """
144
+ set_value("enable_universal_constructor", "true" if enabled else "false")
145
+
146
+
120
147
  DEFAULT_SECTION = "puppy"
121
148
  REQUIRED_KEYS = ["puppy_name", "owner_name"]
122
149
 
@@ -260,6 +287,8 @@ def get_config_keys():
260
287
  default_keys.append("enable_dbos")
261
288
  # Add pack agents control key
262
289
  default_keys.append("enable_pack_agents")
290
+ # Add universal constructor control key
291
+ default_keys.append("enable_universal_constructor")
263
292
  # Add cancel agent key configuration
264
293
  default_keys.append("cancel_agent_key")
265
294
  # Add banner color keys
@@ -317,6 +317,21 @@ class SubAgentStatusMessage(BaseMessage):
317
317
  )
318
318
 
319
319
 
320
+ class UniversalConstructorMessage(BaseMessage):
321
+ """Result of a universal_constructor operation."""
322
+
323
+ category: MessageCategory = MessageCategory.TOOL_OUTPUT
324
+ action: str = Field(
325
+ description="The UC action performed (list/call/create/update/info)"
326
+ )
327
+ tool_name: Optional[str] = Field(
328
+ default=None, description="Tool name if applicable"
329
+ )
330
+ success: bool = Field(description="Whether the operation succeeded")
331
+ summary: str = Field(description="Brief summary of the result")
332
+ details: Optional[str] = Field(default=None, description="Additional details")
333
+
334
+
320
335
  # =============================================================================
321
336
  # User Interaction Messages (Agent → User)
322
337
  # =============================================================================
@@ -443,6 +458,7 @@ AnyMessage = Union[
443
458
  SubAgentInvocationMessage,
444
459
  SubAgentResponseMessage,
445
460
  SubAgentStatusMessage,
461
+ UniversalConstructorMessage,
446
462
  UserInputRequest,
447
463
  ConfirmationRequest,
448
464
  SelectionRequest,
@@ -485,6 +501,8 @@ __all__ = [
485
501
  "SubAgentInvocationMessage",
486
502
  "SubAgentResponseMessage",
487
503
  "SubAgentStatusMessage",
504
+ # Universal Constructor
505
+ "UniversalConstructorMessage",
488
506
  # User interaction
489
507
  "UserInputRequest",
490
508
  "ConfirmationRequest",
@@ -48,6 +48,7 @@ from .messages import (
48
48
  SubAgentInvocationMessage,
49
49
  SubAgentResponseMessage,
50
50
  TextMessage,
51
+ UniversalConstructorMessage,
51
52
  UserInputRequest,
52
53
  VersionCheckMessage,
53
54
  )
@@ -287,6 +288,8 @@ class RichConsoleRenderer:
287
288
  elif isinstance(message, SubAgentResponseMessage):
288
289
  # Skip rendering - we now display sub-agent responses via display_non_streamed_result
289
290
  pass
291
+ elif isinstance(message, UniversalConstructorMessage):
292
+ self._render_universal_constructor(message)
290
293
  elif isinstance(message, UserInputRequest):
291
294
  # Can't handle async user input in sync context - skip
292
295
  self._console.print("[dim]User input requested (requires async)[/dim]")
@@ -775,6 +778,38 @@ class RichConsoleRenderer:
775
778
  f"({msg.message_count} messages)[/dim]"
776
779
  )
777
780
 
781
+ def _render_universal_constructor(self, msg: UniversalConstructorMessage) -> None:
782
+ """Render universal_constructor tool output with banner."""
783
+ # Skip for sub-agents unless verbose mode
784
+ if self._should_suppress_subagent_output():
785
+ return
786
+
787
+ # Format banner
788
+ banner = self._format_banner("universal_constructor", "UNIVERSAL CONSTRUCTOR")
789
+
790
+ # Build the header line with action and optional tool name
791
+ # Escape user-controlled strings to prevent Rich markup injection
792
+ header_parts = [f"\n{banner} 🔧 [bold cyan]{msg.action.upper()}[/bold cyan]"]
793
+ if msg.tool_name:
794
+ safe_tool_name = escape_rich_markup(msg.tool_name)
795
+ header_parts.append(f" [dim]tool=[/dim][bold]{safe_tool_name}[/bold]")
796
+ self._console.print("".join(header_parts))
797
+
798
+ # Status indicator
799
+ safe_summary = escape_rich_markup(msg.summary) if msg.summary else ""
800
+ if msg.success:
801
+ self._console.print(f"[green]✓[/green] {safe_summary}")
802
+ else:
803
+ self._console.print(f"[red]✗[/red] {safe_summary}")
804
+
805
+ # Show details if present
806
+ if msg.details:
807
+ safe_details = escape_rich_markup(msg.details)
808
+ self._console.print(f"[dim]{safe_details}[/dim]")
809
+
810
+ # Trailing newline for spinner separation
811
+ self._console.print()
812
+
778
813
  # =========================================================================
779
814
  # User Interaction
780
815
  # =========================================================================
@@ -24,7 +24,6 @@ from rich.text import Text
24
24
 
25
25
  from code_puppy.messaging.messages import SubAgentStatusMessage
26
26
 
27
-
28
27
  # =============================================================================
29
28
  # Status Configuration
30
29
  # =============================================================================
@@ -0,0 +1,13 @@
1
+ """Universal Constructor - Dynamic tool creation and management plugin.
2
+
3
+ This plugin enables users to create, manage, and deploy custom tools
4
+ that extend Code Puppy's capabilities. Tools are stored in the user's
5
+ config directory and can be organized into namespaces via subdirectories.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ # User tools directory - where user-created UC tools live
11
+ USER_UC_DIR = Path.home() / ".code_puppy" / "plugins" / "universal_constructor"
12
+
13
+ __all__ = ["USER_UC_DIR"]
@@ -0,0 +1,138 @@
1
+ """Pydantic models for Universal Constructor tools and responses.
2
+
3
+ This module defines the data structures used throughout the UC plugin
4
+ for representing tool metadata, tool information, and operation responses.
5
+ """
6
+
7
+ from typing import Any, List, Optional
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class ToolMeta(BaseModel):
13
+ """Metadata for a UC tool.
14
+
15
+ This is the structure expected in the TOOL_META dictionary
16
+ at the top of each tool file.
17
+ """
18
+
19
+ name: str = Field(..., description="Human-readable tool name")
20
+ namespace: str = Field(
21
+ default="", description="Namespace for the tool (from subdirectory path)"
22
+ )
23
+ description: str = Field(..., description="What the tool does")
24
+ enabled: bool = Field(default=True, description="Whether the tool is active")
25
+ version: str = Field(default="1.0.0", description="Semantic version of the tool")
26
+ author: str = Field(default="", description="Tool author or creator")
27
+ created_at: Optional[str] = Field(
28
+ default=None, description="When the tool was created (ISO format string)"
29
+ )
30
+
31
+ model_config = {"extra": "allow"} # Allow additional metadata fields
32
+
33
+
34
+ class UCToolInfo(BaseModel):
35
+ """Full information about a UC tool.
36
+
37
+ Combines metadata with runtime information like function signature
38
+ and source file location.
39
+ """
40
+
41
+ meta: ToolMeta = Field(..., description="Tool metadata")
42
+ signature: str = Field(..., description="Function signature string")
43
+ source_path: str = Field(..., description="Path to the tool source file")
44
+ function_name: str = Field(default="", description="Name of the callable function")
45
+ docstring: Optional[str] = Field(default=None, description="Function docstring")
46
+
47
+ model_config = {"arbitrary_types_allowed": True}
48
+
49
+ @property
50
+ def full_name(self) -> str:
51
+ """Get the fully qualified tool name including namespace."""
52
+ if self.meta.namespace:
53
+ return f"{self.meta.namespace}.{self.meta.name}"
54
+ return self.meta.name
55
+
56
+
57
+ # Response models for UC operations
58
+
59
+
60
+ class UCListOutput(BaseModel):
61
+ """Response model for listing UC tools."""
62
+
63
+ tools: List[UCToolInfo] = Field(
64
+ default_factory=list, description="List of available tools"
65
+ )
66
+ total_count: int = Field(default=0, description="Total number of tools")
67
+ enabled_count: int = Field(default=0, description="Number of enabled tools")
68
+ error: Optional[str] = Field(default=None, description="Error message if any")
69
+
70
+ model_config = {"arbitrary_types_allowed": True}
71
+
72
+
73
+ class UCCallOutput(BaseModel):
74
+ """Response model for calling a UC tool."""
75
+
76
+ success: bool = Field(..., description="Whether the call succeeded")
77
+ tool_name: str = Field(..., description="Name of the tool that was called")
78
+ result: Any = Field(default=None, description="Return value from the tool")
79
+ error: Optional[str] = Field(default=None, description="Error message if failed")
80
+ execution_time: Optional[float] = Field(
81
+ default=None, description="Execution time in seconds"
82
+ )
83
+ source_preview: Optional[str] = Field(
84
+ default=None, description="Preview of the tool's source code that was executed"
85
+ )
86
+
87
+
88
+ class UCCreateOutput(BaseModel):
89
+ """Response model for creating a UC tool."""
90
+
91
+ success: bool = Field(..., description="Whether creation succeeded")
92
+ tool_name: str = Field(default="", description="Name of the created tool")
93
+ source_path: Optional[str] = Field(
94
+ default=None, description="Path where tool was saved"
95
+ )
96
+ preview: Optional[str] = Field(
97
+ default=None, description="Preview of the first 10 lines of source code"
98
+ )
99
+ error: Optional[str] = Field(default=None, description="Error message if failed")
100
+ validation_warnings: List[str] = Field(
101
+ default_factory=list, description="Non-fatal validation warnings"
102
+ )
103
+
104
+ model_config = {"arbitrary_types_allowed": True}
105
+
106
+
107
+ class UCUpdateOutput(BaseModel):
108
+ """Response model for updating a UC tool."""
109
+
110
+ success: bool = Field(..., description="Whether update succeeded")
111
+ tool_name: str = Field(default="", description="Name of the updated tool")
112
+ source_path: Optional[str] = Field(
113
+ default=None, description="Path to the updated tool"
114
+ )
115
+ preview: Optional[str] = Field(
116
+ default=None, description="Preview of the first 10 lines of updated source code"
117
+ )
118
+ error: Optional[str] = Field(default=None, description="Error message if failed")
119
+ changes_applied: List[str] = Field(
120
+ default_factory=list, description="List of changes that were applied"
121
+ )
122
+
123
+ model_config = {"arbitrary_types_allowed": True}
124
+
125
+
126
+ class UCInfoOutput(BaseModel):
127
+ """Response model for getting info about a specific UC tool."""
128
+
129
+ success: bool = Field(..., description="Whether lookup succeeded")
130
+ tool: Optional[UCToolInfo] = Field(
131
+ default=None, description="Tool information if found"
132
+ )
133
+ source_code: Optional[str] = Field(
134
+ default=None, description="Source code of the tool"
135
+ )
136
+ error: Optional[str] = Field(default=None, description="Error message if failed")
137
+
138
+ model_config = {"arbitrary_types_allowed": True}
@@ -0,0 +1,47 @@
1
+ """Callback registration for the Universal Constructor plugin.
2
+
3
+ This module registers callbacks to integrate UC with the rest of
4
+ Code Puppy. It ensures the plugin is properly loaded and initialized.
5
+ """
6
+
7
+ import logging
8
+
9
+ from code_puppy.callbacks import register_callback
10
+
11
+ from . import USER_UC_DIR
12
+ from .registry import get_registry
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def _on_startup() -> None:
18
+ """Initialize UC plugin on application startup."""
19
+ from code_puppy.config import get_universal_constructor_enabled
20
+
21
+ # Skip initialization if UC is disabled
22
+ if not get_universal_constructor_enabled():
23
+ logger.debug("Universal Constructor is disabled, skipping initialization")
24
+ return
25
+
26
+ logger.debug("Universal Constructor plugin initializing...")
27
+
28
+ # Ensure the user tools directory exists
29
+ USER_UC_DIR.mkdir(parents=True, exist_ok=True)
30
+
31
+ # Do an initial scan of tools (lazy - will happen on first access)
32
+ registry = get_registry()
33
+ logger.debug(f"UC registry initialized, tools dir: {registry._tools_dir}")
34
+
35
+ # Log plugin info at startup
36
+ tools = registry.list_tools(include_disabled=True)
37
+ enabled = [t for t in tools if t.meta.enabled]
38
+ logger.debug(
39
+ f"UC plugin loaded: {len(enabled)}/{len(tools)} tools enabled "
40
+ f"from {USER_UC_DIR}"
41
+ )
42
+
43
+
44
+ # Register startup callback
45
+ register_callback("startup", _on_startup)
46
+
47
+ logger.debug("Universal Constructor plugin callbacks registered")
@@ -0,0 +1,304 @@
1
+ """UC Tool Registry - discovers and manages user-created tools.
2
+
3
+ This module provides the core registry that scans the user's UC directory,
4
+ loads tool metadata, extracts function signatures, and provides access
5
+ to enabled tools for the LLM.
6
+ """
7
+
8
+ import importlib.util
9
+ import inspect
10
+ import logging
11
+ import sys
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from types import ModuleType
15
+ from typing import Callable, Dict, List, Optional, Tuple
16
+
17
+ from . import USER_UC_DIR
18
+ from .models import ToolMeta, UCToolInfo
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class UCRegistry:
24
+ """Registry for discovering and managing UC tools.
25
+
26
+ Scans the user's UC directory recursively, loading tool metadata
27
+ and providing access to enabled tools. Supports namespacing via
28
+ subdirectories (e.g., api/weather.py → "api.weather").
29
+ """
30
+
31
+ def __init__(self, tools_dir: Optional[Path] = None):
32
+ """Initialize the registry.
33
+
34
+ Args:
35
+ tools_dir: Directory to scan for tools. Defaults to USER_UC_DIR.
36
+ """
37
+ self._tools_dir = tools_dir or USER_UC_DIR
38
+ self._tools: Dict[str, UCToolInfo] = {}
39
+ self._modules: Dict[str, ModuleType] = {}
40
+ self._last_scan: Optional[datetime] = None
41
+
42
+ def ensure_tools_dir(self) -> Path:
43
+ """Ensure the tools directory exists.
44
+
45
+ Returns:
46
+ Path to the tools directory.
47
+ """
48
+ self._tools_dir.mkdir(parents=True, exist_ok=True)
49
+ return self._tools_dir
50
+
51
+ def scan(self) -> int:
52
+ """Scan the tools directory and load all tools.
53
+
54
+ Returns:
55
+ Number of tools found.
56
+ """
57
+ self._tools.clear()
58
+ self._modules.clear()
59
+
60
+ if not self._tools_dir.exists():
61
+ logger.debug(f"Tools directory does not exist: {self._tools_dir}")
62
+ return 0
63
+
64
+ # Find all Python files recursively
65
+ tool_files = list(self._tools_dir.rglob("*.py"))
66
+
67
+ for tool_file in tool_files:
68
+ # Skip __init__.py and hidden files
69
+ if tool_file.name.startswith("_") or tool_file.name.startswith("."):
70
+ continue
71
+
72
+ try:
73
+ tool_info = self._load_tool_file(tool_file)
74
+ if tool_info:
75
+ self._tools[tool_info.full_name] = tool_info
76
+ logger.debug(f"Loaded tool: {tool_info.full_name}")
77
+ except Exception as e:
78
+ logger.warning(f"Failed to load tool from {tool_file}: {e}")
79
+
80
+ self._last_scan = datetime.now()
81
+ logger.info(f"Scanned {len(self._tools)} tools from {self._tools_dir}")
82
+ return len(self._tools)
83
+
84
+ def _load_tool_file(self, file_path: Path) -> Optional[UCToolInfo]:
85
+ """Load a tool from a Python file.
86
+
87
+ Args:
88
+ file_path: Path to the Python file.
89
+
90
+ Returns:
91
+ UCToolInfo if valid tool, None otherwise.
92
+ """
93
+ # Calculate namespace from relative path
94
+ try:
95
+ rel_path = file_path.relative_to(self._tools_dir)
96
+ namespace_parts = list(rel_path.parent.parts)
97
+ namespace = ".".join(namespace_parts) if namespace_parts else ""
98
+ except ValueError:
99
+ namespace = ""
100
+
101
+ # Load the module
102
+ module = self._load_module(file_path)
103
+ if module is None:
104
+ return None
105
+
106
+ # Check for TOOL_META
107
+ if not hasattr(module, "TOOL_META"):
108
+ logger.debug(f"No TOOL_META found in {file_path}")
109
+ return None
110
+
111
+ raw_meta = dict(
112
+ getattr(module, "TOOL_META")
113
+ ) # Copy to avoid mutating module constant
114
+ if not isinstance(raw_meta, dict):
115
+ logger.warning(f"TOOL_META is not a dict in {file_path}")
116
+ return None
117
+
118
+ # Set namespace from directory structure
119
+ raw_meta["namespace"] = namespace
120
+
121
+ # Parse metadata
122
+ try:
123
+ meta = ToolMeta(**raw_meta)
124
+ except Exception as e:
125
+ logger.warning(f"Invalid TOOL_META in {file_path}: {e}")
126
+ return None
127
+
128
+ # Find the callable function
129
+ func, func_name = self._find_tool_function(module, meta.name)
130
+ if func is None:
131
+ logger.warning(f"No callable function found in {file_path}")
132
+ return None
133
+
134
+ # Extract signature
135
+ try:
136
+ sig = inspect.signature(func)
137
+ signature_str = f"{func_name}{sig}"
138
+ except (ValueError, TypeError):
139
+ signature_str = f"{func_name}(...)"
140
+
141
+ # Extract docstring
142
+ docstring = inspect.getdoc(func)
143
+
144
+ # Store module reference for later calls
145
+ full_name = f"{namespace}.{meta.name}" if namespace else meta.name
146
+ self._modules[full_name] = module
147
+
148
+ return UCToolInfo(
149
+ meta=meta,
150
+ signature=signature_str,
151
+ source_path=str(file_path),
152
+ function_name=func_name,
153
+ docstring=docstring,
154
+ )
155
+
156
+ def _load_module(self, file_path: Path) -> Optional[ModuleType]:
157
+ """Load a Python module from a file path.
158
+
159
+ Args:
160
+ file_path: Path to the Python file.
161
+
162
+ Returns:
163
+ Loaded module or None if failed.
164
+ """
165
+ module_name = f"uc_tool_{file_path.stem}_{hash(str(file_path))}"
166
+
167
+ try:
168
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
169
+ if spec is None or spec.loader is None:
170
+ return None
171
+
172
+ module = importlib.util.module_from_spec(spec)
173
+ sys.modules[module_name] = module
174
+ spec.loader.exec_module(module)
175
+ return module
176
+ except Exception:
177
+ return None
178
+
179
+ def _find_tool_function(
180
+ self, module: ModuleType, tool_name: str
181
+ ) -> Tuple[Optional[Callable], str]:
182
+ """Find the callable function in a tool module.
183
+
184
+ Looks for:
185
+ 1. A function with the same name as the tool
186
+ 2. A function named 'run'
187
+ 3. A function named 'execute'
188
+ 4. Any public function (not starting with _)
189
+
190
+ Args:
191
+ module: The loaded module.
192
+ tool_name: The tool name from metadata.
193
+
194
+ Returns:
195
+ Tuple of (function, function_name) or (None, "") if not found.
196
+ """
197
+ # Priority order for finding the function
198
+ candidates = [tool_name, "run", "execute"]
199
+
200
+ for name in candidates:
201
+ if hasattr(module, name):
202
+ func = getattr(module, name)
203
+ if callable(func) and not isinstance(func, type):
204
+ return func, name
205
+
206
+ # Fall back to first public callable
207
+ for name in dir(module):
208
+ if name.startswith("_"):
209
+ continue
210
+ obj = getattr(module, name)
211
+ if callable(obj) and not isinstance(obj, type):
212
+ return obj, name
213
+
214
+ return None, ""
215
+
216
+ def list_tools(self, include_disabled: bool = False) -> List[UCToolInfo]:
217
+ """List all discovered tools.
218
+
219
+ Args:
220
+ include_disabled: Whether to include disabled tools.
221
+
222
+ Returns:
223
+ List of tool info objects.
224
+ """
225
+ if not self._tools:
226
+ self.scan()
227
+
228
+ tools = list(self._tools.values())
229
+ if not include_disabled:
230
+ tools = [t for t in tools if t.meta.enabled]
231
+
232
+ return sorted(tools, key=lambda t: t.full_name)
233
+
234
+ def get_tool(self, name: str) -> Optional[UCToolInfo]:
235
+ """Get a specific tool by name.
236
+
237
+ Args:
238
+ name: Full tool name (including namespace).
239
+
240
+ Returns:
241
+ Tool info or None if not found.
242
+ """
243
+ if not self._tools:
244
+ self.scan()
245
+
246
+ return self._tools.get(name)
247
+
248
+ def get_tool_function(self, name: str) -> Optional[Callable]:
249
+ """Get the callable function for a tool.
250
+
251
+ Args:
252
+ name: Full tool name (including namespace).
253
+
254
+ Returns:
255
+ Callable function or None if not found.
256
+ """
257
+ tool = self.get_tool(name)
258
+ if tool is None:
259
+ return None
260
+
261
+ module = self._modules.get(name)
262
+ if module is None:
263
+ return None
264
+
265
+ func, _ = self._find_tool_function(module, tool.meta.name)
266
+ return func
267
+
268
+ def load_tool_module(self, name: str) -> Optional[ModuleType]:
269
+ """Get the loaded module for a tool.
270
+
271
+ Args:
272
+ name: Full tool name (including namespace).
273
+
274
+ Returns:
275
+ Module or None if not found.
276
+ """
277
+ if not self._tools:
278
+ self.scan()
279
+
280
+ return self._modules.get(name)
281
+
282
+ def reload(self) -> int:
283
+ """Force a rescan of all tools.
284
+
285
+ Returns:
286
+ Number of tools found.
287
+ """
288
+ return self.scan()
289
+
290
+
291
+ # Global registry instance
292
+ _registry: Optional[UCRegistry] = None
293
+
294
+
295
+ def get_registry() -> UCRegistry:
296
+ """Get the global UC registry instance.
297
+
298
+ Returns:
299
+ The global UCRegistry instance.
300
+ """
301
+ global _registry
302
+ if _registry is None:
303
+ _registry = UCRegistry()
304
+ return _registry