code-puppy 0.0.124__py3-none-any.whl → 0.0.125__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.
@@ -0,0 +1,211 @@
1
+ """Agent manager for handling different agent configurations."""
2
+
3
+ import importlib
4
+ import pkgutil
5
+ import uuid
6
+ from typing import Dict, Optional, Type, Union
7
+
8
+ from code_puppy.config import get_value, set_config_value
9
+ from .base_agent import BaseAgent
10
+ from .json_agent import JSONAgent, discover_json_agents
11
+ from ..callbacks import on_agent_reload
12
+ from ..messaging import emit_warning
13
+
14
+ # Registry of available agents (Python classes and JSON file paths)
15
+ _AGENT_REGISTRY: Dict[str, Union[Type[BaseAgent], str]] = {}
16
+ _CURRENT_AGENT_CONFIG: Optional[BaseAgent] = None
17
+
18
+
19
+ def _discover_agents(message_group_id: Optional[str] = None):
20
+ """Dynamically discover all agent classes and JSON agents."""
21
+ # Always clear the registry to force refresh
22
+ _AGENT_REGISTRY.clear()
23
+
24
+ # 1. Discover Python agent classes in the agents package
25
+ import code_puppy.agents as agents_package
26
+
27
+ # Iterate through all modules in the agents package
28
+ for _, modname, _ in pkgutil.iter_modules(agents_package.__path__):
29
+ if modname.startswith("_") or modname in [
30
+ "base_agent",
31
+ "json_agent",
32
+ "agent_manager",
33
+ ]:
34
+ continue
35
+
36
+ try:
37
+ # Import the module
38
+ module = importlib.import_module(f"code_puppy.agents.{modname}")
39
+
40
+ # Look for BaseAgent subclasses
41
+ for attr_name in dir(module):
42
+ attr = getattr(module, attr_name)
43
+ if (
44
+ isinstance(attr, type)
45
+ and issubclass(attr, BaseAgent)
46
+ and attr not in [BaseAgent, JSONAgent]
47
+ ):
48
+ # Create an instance to get the name
49
+ agent_instance = attr()
50
+ _AGENT_REGISTRY[agent_instance.name] = attr
51
+
52
+ except Exception as e:
53
+ # Skip problematic modules
54
+ emit_warning(
55
+ f"Warning: Could not load agent module {modname}: {e}",
56
+ message_group=message_group_id,
57
+ )
58
+ continue
59
+
60
+ # 2. Discover JSON agents in user directory
61
+ try:
62
+ json_agents = discover_json_agents()
63
+
64
+ # Add JSON agents to registry (store file path instead of class)
65
+ for agent_name, json_path in json_agents.items():
66
+ _AGENT_REGISTRY[agent_name] = json_path
67
+
68
+ except Exception as e:
69
+ emit_warning(
70
+ f"Warning: Could not discover JSON agents: {e}",
71
+ message_group=message_group_id,
72
+ )
73
+
74
+
75
+ def get_available_agents() -> Dict[str, str]:
76
+ """Get a dictionary of available agents with their display names.
77
+
78
+ Returns:
79
+ Dict mapping agent names to display names.
80
+ """
81
+ # Generate a message group ID for this operation
82
+ message_group_id = str(uuid.uuid4())
83
+ _discover_agents(message_group_id=message_group_id)
84
+
85
+ agents = {}
86
+ for name, agent_ref in _AGENT_REGISTRY.items():
87
+ try:
88
+ if isinstance(agent_ref, str): # JSON agent (file path)
89
+ agent_instance = JSONAgent(agent_ref)
90
+ else: # Python agent (class)
91
+ agent_instance = agent_ref()
92
+ agents[name] = agent_instance.display_name
93
+ except Exception:
94
+ agents[name] = name.title() # Fallback
95
+
96
+ return agents
97
+
98
+
99
+ def get_current_agent_name() -> str:
100
+ """Get the name of the currently active agent.
101
+
102
+ Returns:
103
+ The name of the current agent, defaults to 'code-puppy'.
104
+ """
105
+ return get_value("current_agent") or "code-puppy"
106
+
107
+
108
+ def set_current_agent(agent_name: str) -> bool:
109
+ """Set the current agent by name.
110
+
111
+ Args:
112
+ agent_name: The name of the agent to set as current.
113
+
114
+ Returns:
115
+ True if the agent was set successfully, False if agent not found.
116
+ """
117
+ # Generate a message group ID for agent switching
118
+ message_group_id = str(uuid.uuid4())
119
+ _discover_agents(message_group_id=message_group_id)
120
+ # Clear the cached config when switching agents
121
+ global _CURRENT_AGENT_CONFIG
122
+ _CURRENT_AGENT_CONFIG = None
123
+ agent_obj = load_agent_config(agent_name)
124
+ on_agent_reload(agent_obj.id, agent_name)
125
+ set_config_value("current_agent", agent_name)
126
+ return True
127
+
128
+
129
+ def get_current_agent_config() -> BaseAgent:
130
+ """Get the current agent configuration.
131
+
132
+ Returns:
133
+ The current agent configuration instance.
134
+ """
135
+ global _CURRENT_AGENT_CONFIG
136
+
137
+ _CURRENT_AGENT_CONFIG = load_agent_config(get_current_agent_name())
138
+
139
+ return _CURRENT_AGENT_CONFIG
140
+
141
+
142
+ def load_agent_config(agent_name: str) -> BaseAgent:
143
+ """Load an agent configuration by name.
144
+
145
+ Args:
146
+ agent_name: The name of the agent to load.
147
+
148
+ Returns:
149
+ The agent configuration instance.
150
+
151
+ Raises:
152
+ ValueError: If the agent is not found.
153
+ """
154
+ # Generate a message group ID for agent loading
155
+ message_group_id = str(uuid.uuid4())
156
+ _discover_agents(message_group_id=message_group_id)
157
+
158
+ if agent_name not in _AGENT_REGISTRY:
159
+ # Fallback to code-puppy if agent not found
160
+ if "code-puppy" in _AGENT_REGISTRY:
161
+ agent_name = "code-puppy"
162
+ else:
163
+ raise ValueError(
164
+ f"Agent '{agent_name}' not found and no fallback available"
165
+ )
166
+
167
+ agent_ref = _AGENT_REGISTRY[agent_name]
168
+ if isinstance(agent_ref, str): # JSON agent (file path)
169
+ return JSONAgent(agent_ref)
170
+ else: # Python agent (class)
171
+ return agent_ref()
172
+
173
+
174
+ def get_agent_descriptions() -> Dict[str, str]:
175
+ """Get descriptions for all available agents.
176
+
177
+ Returns:
178
+ Dict mapping agent names to their descriptions.
179
+ """
180
+ # Generate a message group ID for this operation
181
+ message_group_id = str(uuid.uuid4())
182
+ _discover_agents(message_group_id=message_group_id)
183
+
184
+ descriptions = {}
185
+ for name, agent_ref in _AGENT_REGISTRY.items():
186
+ try:
187
+ if isinstance(agent_ref, str): # JSON agent (file path)
188
+ agent_instance = JSONAgent(agent_ref)
189
+ else: # Python agent (class)
190
+ agent_instance = agent_ref()
191
+ descriptions[name] = agent_instance.description
192
+ except Exception:
193
+ descriptions[name] = "No description available"
194
+
195
+ return descriptions
196
+
197
+
198
+ def clear_agent_cache():
199
+ """Clear the cached agent configuration to force reload."""
200
+ global _CURRENT_AGENT_CONFIG
201
+ _CURRENT_AGENT_CONFIG = None
202
+
203
+
204
+ def refresh_agents():
205
+ """Refresh the agent discovery to pick up newly created agents.
206
+
207
+ This clears the agent registry cache and forces a rediscovery of all agents.
208
+ """
209
+ # Generate a message group ID for agent refreshing
210
+ message_group_id = str(uuid.uuid4())
211
+ _discover_agents(message_group_id=message_group_id)
@@ -0,0 +1,60 @@
1
+ """Base agent configuration class for defining agent properties."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Dict, List, Optional
5
+ import uuid
6
+
7
+
8
+ class BaseAgent(ABC):
9
+ """Base class for all agent configurations."""
10
+
11
+ def __init__(self):
12
+ self.id = str(uuid.uuid4())
13
+
14
+ @property
15
+ @abstractmethod
16
+ def name(self) -> str:
17
+ """Unique identifier for the agent."""
18
+ pass
19
+
20
+ @property
21
+ @abstractmethod
22
+ def display_name(self) -> str:
23
+ """Human-readable name for the agent."""
24
+ pass
25
+
26
+ @property
27
+ @abstractmethod
28
+ def description(self) -> str:
29
+ """Brief description of what this agent does."""
30
+ pass
31
+
32
+ @abstractmethod
33
+ def get_system_prompt(self) -> str:
34
+ """Get the system prompt for this agent."""
35
+ pass
36
+
37
+ @abstractmethod
38
+ def get_available_tools(self) -> List[str]:
39
+ """Get list of tool names that this agent should have access to.
40
+
41
+ Returns:
42
+ List of tool names to register for this agent.
43
+ """
44
+ pass
45
+
46
+ def get_tools_config(self) -> Optional[Dict[str, Any]]:
47
+ """Get tool configuration for this agent.
48
+
49
+ Returns:
50
+ Dict with tool configuration, or None to use default tools.
51
+ """
52
+ return None
53
+
54
+ def get_user_prompt(self) -> Optional[str]:
55
+ """Get custom user prompt for this agent.
56
+
57
+ Returns:
58
+ Custom prompt string, or None to use default.
59
+ """
60
+ return None
@@ -0,0 +1,129 @@
1
+ """JSON-based agent configuration system."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
6
+
7
+ from .base_agent import BaseAgent
8
+
9
+
10
+ class JSONAgent(BaseAgent):
11
+ """Agent configured from a JSON file."""
12
+
13
+ def __init__(self, json_path: str):
14
+ """Initialize agent from JSON file.
15
+
16
+ Args:
17
+ json_path: Path to the JSON configuration file.
18
+ """
19
+ super().__init__()
20
+ self.json_path = json_path
21
+ self._config = self._load_config()
22
+ self._validate_config()
23
+
24
+ def _load_config(self) -> Dict:
25
+ """Load configuration from JSON file."""
26
+ try:
27
+ with open(self.json_path, "r", encoding="utf-8") as f:
28
+ return json.load(f)
29
+ except (json.JSONDecodeError, FileNotFoundError) as e:
30
+ raise ValueError(
31
+ f"Failed to load JSON agent config from {self.json_path}: {e}"
32
+ )
33
+
34
+ def _validate_config(self) -> None:
35
+ """Validate required fields in configuration."""
36
+ required_fields = ["name", "description", "system_prompt", "tools"]
37
+ for field in required_fields:
38
+ if field not in self._config:
39
+ raise ValueError(
40
+ f"Missing required field '{field}' in JSON agent config: {self.json_path}"
41
+ )
42
+
43
+ # Validate tools is a list
44
+ if not isinstance(self._config["tools"], list):
45
+ raise ValueError(
46
+ f"'tools' must be a list in JSON agent config: {self.json_path}"
47
+ )
48
+
49
+ # Validate system_prompt is string or list
50
+ system_prompt = self._config["system_prompt"]
51
+ if not isinstance(system_prompt, (str, list)):
52
+ raise ValueError(
53
+ f"'system_prompt' must be a string or list in JSON agent config: {self.json_path}"
54
+ )
55
+
56
+ @property
57
+ def name(self) -> str:
58
+ """Get agent name from JSON config."""
59
+ return self._config["name"]
60
+
61
+ @property
62
+ def display_name(self) -> str:
63
+ """Get display name from JSON config, fallback to name with emoji."""
64
+ return self._config.get("display_name", f"{self.name.title()} 🤖")
65
+
66
+ @property
67
+ def description(self) -> str:
68
+ """Get description from JSON config."""
69
+ return self._config["description"]
70
+
71
+ def get_system_prompt(self) -> str:
72
+ """Get system prompt from JSON config."""
73
+ system_prompt = self._config["system_prompt"]
74
+
75
+ # If it's a list, join with newlines
76
+ if isinstance(system_prompt, list):
77
+ return "\n".join(system_prompt)
78
+
79
+ return system_prompt
80
+
81
+ def get_available_tools(self) -> List[str]:
82
+ """Get available tools from JSON config."""
83
+ # Filter out any tools that don't exist in our registry
84
+ from code_puppy.tools import get_available_tool_names
85
+
86
+ available_tools = get_available_tool_names()
87
+
88
+ # Only return tools that are both requested and available
89
+ # Also filter out 'final_result' which is not in our registry
90
+ requested_tools = [
91
+ tool for tool in self._config["tools"] if tool in available_tools
92
+ ]
93
+
94
+ return requested_tools
95
+
96
+ def get_user_prompt(self) -> Optional[str]:
97
+ """Get custom user prompt from JSON config."""
98
+ return self._config.get("user_prompt")
99
+
100
+ def get_tools_config(self) -> Optional[Dict]:
101
+ """Get tool configuration from JSON config."""
102
+ return self._config.get("tools_config")
103
+
104
+
105
+ def discover_json_agents() -> Dict[str, str]:
106
+ """Discover JSON agent files in the user's agents directory.
107
+
108
+ Returns:
109
+ Dict mapping agent names to their JSON file paths.
110
+ """
111
+ from code_puppy.config import get_user_agents_directory
112
+
113
+ agents = {}
114
+ agents_dir = Path(get_user_agents_directory())
115
+
116
+ if not agents_dir.exists() or not agents_dir.is_dir():
117
+ return agents
118
+
119
+ # Find all .json files in the agents directory
120
+ for json_file in agents_dir.glob("*.json"):
121
+ try:
122
+ # Try to load and validate the agent
123
+ agent = JSONAgent(str(json_file))
124
+ agents[agent.name] = str(json_file)
125
+ except Exception:
126
+ # Skip invalid JSON agent files
127
+ continue
128
+
129
+ return agents
code_puppy/callbacks.py CHANGED
@@ -14,6 +14,7 @@ PhaseType = Literal[
14
14
  "run_shell_command",
15
15
  "load_model_config",
16
16
  "load_prompt",
17
+ "agent_reload",
17
18
  ]
18
19
  CallbackFunc = Callable[..., Any]
19
20
 
@@ -28,6 +29,7 @@ _callbacks: Dict[PhaseType, List[CallbackFunc]] = {
28
29
  "run_shell_command": [],
29
30
  "load_model_config": [],
30
31
  "load_prompt": [],
32
+ "agent_reload": [],
31
33
  }
32
34
 
33
35
  logger = logging.getLogger(__name__)
@@ -166,5 +168,9 @@ def on_run_shell_command(*args, **kwargs) -> Any:
166
168
  return _trigger_callbacks_sync("run_shell_command", *args, **kwargs)
167
169
 
168
170
 
171
+ def on_agent_reload(*args, **kwargs) -> Any:
172
+ return _trigger_callbacks_sync("agent_reload", *args, **kwargs)
173
+
174
+
169
175
  def on_load_prompt():
170
176
  return _trigger_callbacks_sync("load_prompt")
@@ -13,7 +13,7 @@ COMMANDS_HELP = """
13
13
  [bold magenta]Commands Help[/bold magenta]
14
14
  /help, /h Show this help message
15
15
  /cd <dir> Change directory or show directories
16
-
16
+ /agent <name> Switch to a different agent or show available agents
17
17
  /exit, /quit Exit interactive mode
18
18
  /generate-pr-description [@dir] Generate comprehensive PR description
19
19
  /m <model> Set active model
@@ -141,6 +141,7 @@ def handle_command(command: str):
141
141
  get_compaction_threshold,
142
142
  get_yolo_mode,
143
143
  )
144
+ from code_puppy.agents import get_current_agent_config
144
145
 
145
146
  from code_puppy.config import get_compaction_strategy
146
147
 
@@ -152,10 +153,14 @@ def handle_command(command: str):
152
153
  compaction_threshold = get_compaction_threshold()
153
154
  compaction_strategy = get_compaction_strategy()
154
155
 
156
+ # Get current agent info
157
+ current_agent = get_current_agent_config()
158
+
155
159
  status_msg = f"""[bold magenta]🐶 Puppy Status[/bold magenta]
156
160
 
157
161
  [bold]puppy_name:[/bold] [cyan]{puppy_name}[/cyan]
158
162
  [bold]owner_name:[/bold] [cyan]{owner_name}[/cyan]
163
+ [bold]current_agent:[/bold] [magenta]{current_agent.display_name}[/magenta]
159
164
  [bold]model:[/bold] [green]{model}[/green]
160
165
  [bold]YOLO_MODE:[/bold] {"[red]ON[/red]" if yolo_mode else "[yellow]off[/yellow]"}
161
166
  [bold]protected_tokens:[/bold] [cyan]{protected_tokens:,}[/cyan] recent tokens preserved
@@ -207,6 +212,91 @@ def handle_command(command: str):
207
212
  emit_info(markdown_content)
208
213
  return True
209
214
 
215
+ if command.startswith("/agent"):
216
+ # Handle agent switching
217
+ from code_puppy.agents import (
218
+ get_available_agents,
219
+ get_current_agent_config,
220
+ set_current_agent,
221
+ get_agent_descriptions,
222
+ )
223
+ from code_puppy.agent import get_code_generation_agent
224
+
225
+ tokens = command.split()
226
+
227
+ if len(tokens) == 1:
228
+ # Show current agent and available agents
229
+ current_agent = get_current_agent_config()
230
+ available_agents = get_available_agents()
231
+ descriptions = get_agent_descriptions()
232
+
233
+ # Generate a group ID for all messages in this command
234
+ import uuid
235
+
236
+ group_id = str(uuid.uuid4())
237
+
238
+ emit_info(
239
+ f"[bold green]Current Agent:[/bold green] {current_agent.display_name}",
240
+ message_group=group_id,
241
+ )
242
+ emit_info(
243
+ f"[dim]{current_agent.description}[/dim]\n", message_group=group_id
244
+ )
245
+
246
+ emit_info(
247
+ "[bold magenta]Available Agents:[/bold magenta]", message_group=group_id
248
+ )
249
+ for name, display_name in available_agents.items():
250
+ description = descriptions.get(name, "No description")
251
+ current_marker = (
252
+ " [green]← current[/green]" if name == current_agent.name else ""
253
+ )
254
+ emit_info(
255
+ f" [cyan]{name:<12}[/cyan] {display_name}{current_marker}",
256
+ message_group=group_id,
257
+ )
258
+ emit_info(f" [dim]{description}[/dim]", message_group=group_id)
259
+
260
+ emit_info(
261
+ "\n[yellow]Usage:[/yellow] /agent <agent-name>", message_group=group_id
262
+ )
263
+ return True
264
+
265
+ elif len(tokens) == 2:
266
+ agent_name = tokens[1].lower()
267
+
268
+ # Generate a group ID for all messages in this command
269
+ import uuid
270
+
271
+ group_id = str(uuid.uuid4())
272
+
273
+ if set_current_agent(agent_name):
274
+ # Reload the agent with new configuration
275
+ get_code_generation_agent(force_reload=True)
276
+ new_agent = get_current_agent_config()
277
+ emit_success(
278
+ f"Switched to agent: {new_agent.display_name}",
279
+ message_group=group_id,
280
+ )
281
+ emit_info(f"[dim]{new_agent.description}[/dim]", message_group=group_id)
282
+ return True
283
+ else:
284
+ # Generate a group ID for all messages in this command
285
+ import uuid
286
+
287
+ group_id = str(uuid.uuid4())
288
+
289
+ available_agents = get_available_agents()
290
+ emit_error(f"Agent '{agent_name}' not found", message_group=group_id)
291
+ emit_warning(
292
+ f"Available agents: {', '.join(available_agents.keys())}",
293
+ message_group=group_id,
294
+ )
295
+ return True
296
+ else:
297
+ emit_warning("Usage: /agent [agent-name]")
298
+ return True
299
+
210
300
  if command.startswith("/m"):
211
301
  # Try setting model and show confirmation
212
302
  new_input = update_model_in_input(command)
code_puppy/config.py CHANGED
@@ -9,6 +9,7 @@ MCP_SERVERS_FILE = os.path.join(CONFIG_DIR, "mcp_servers.json")
9
9
  COMMAND_HISTORY_FILE = os.path.join(CONFIG_DIR, "command_history.txt")
10
10
  MODELS_FILE = os.path.join(CONFIG_DIR, "models.json")
11
11
  EXTRA_MODELS_FILE = os.path.join(CONFIG_DIR, "extra_models.json")
12
+ AGENTS_DIR = os.path.join(CONFIG_DIR, "agents")
12
13
 
13
14
  DEFAULT_SECTION = "puppy"
14
15
  REQUIRED_KEYS = ["puppy_name", "owner_name"]
@@ -303,6 +304,17 @@ def normalize_command_history():
303
304
  direct_console.print(f"[bold red]{error_msg}[/bold red]")
304
305
 
305
306
 
307
+ def get_user_agents_directory() -> str:
308
+ """Get the user's agents directory path.
309
+
310
+ Returns:
311
+ Path to the user's Code Puppy agents directory.
312
+ """
313
+ # Ensure the agents directory exists
314
+ os.makedirs(AGENTS_DIR, exist_ok=True)
315
+ return AGENTS_DIR
316
+
317
+
306
318
  def initialize_command_history_file():
307
319
  """Create the command history file if it doesn't exist.
308
320
  Handles migration from the old history file location for backward compatibility.
code_puppy/main.py CHANGED
@@ -373,8 +373,13 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
373
373
 
374
374
  while True:
375
375
  from code_puppy.messaging import emit_info
376
+ from code_puppy.agents.agent_manager import get_current_agent_config
376
377
 
377
- emit_info("[bold blue]Enter your coding task:[/bold blue]")
378
+ # Get the custom prompt from the current agent, or use default
379
+ current_agent = get_current_agent_config()
380
+ user_prompt = current_agent.get_user_prompt() or "Enter your coding task:"
381
+
382
+ emit_info(f"[bold blue]{user_prompt}[/bold blue]")
378
383
 
379
384
  try:
380
385
  # Use prompt_toolkit for enhanced input with path completion
@@ -1,10 +1,63 @@
1
- from code_puppy.tools.command_runner import register_command_runner_tools
2
- from code_puppy.tools.file_modifications import register_file_modifications_tools
3
- from code_puppy.tools.file_operations import register_file_operations_tools
1
+ from code_puppy.messaging import emit_warning
2
+ from code_puppy.tools.command_runner import (
3
+ register_agent_run_shell_command,
4
+ register_agent_share_your_reasoning,
5
+ )
6
+ from code_puppy.tools.file_modifications import register_edit_file, register_delete_file
7
+ from code_puppy.tools.file_operations import (
8
+ register_list_files,
9
+ register_read_file,
10
+ register_grep,
11
+ )
12
+
13
+
14
+ # Map of tool names to their individual registration functions
15
+ TOOL_REGISTRY = {
16
+ # File Operations
17
+ "list_files": register_list_files,
18
+ "read_file": register_read_file,
19
+ "grep": register_grep,
20
+ # File Modifications
21
+ "edit_file": register_edit_file,
22
+ "delete_file": register_delete_file,
23
+ # Command Runner
24
+ "agent_run_shell_command": register_agent_run_shell_command,
25
+ "agent_share_your_reasoning": register_agent_share_your_reasoning,
26
+ }
27
+
28
+
29
+ def register_tools_for_agent(agent, tool_names: list[str]):
30
+ """Register specific tools for an agent based on tool names.
31
+
32
+ Args:
33
+ agent: The agent to register tools to.
34
+ tool_names: List of tool names to register.
35
+ """
36
+ for tool_name in tool_names:
37
+ if tool_name not in TOOL_REGISTRY:
38
+ # Skip unknown tools with a warning instead of failing
39
+ emit_warning(f"Warning: Unknown tool '{tool_name}' requested, skipping...")
40
+ continue
41
+
42
+ # Register the individual tool
43
+ register_func = TOOL_REGISTRY[tool_name]
44
+ register_func(agent)
4
45
 
5
46
 
6
47
  def register_all_tools(agent):
7
- """Register all available tools to the provided agent."""
8
- register_file_operations_tools(agent)
9
- register_file_modifications_tools(agent)
10
- register_command_runner_tools(agent)
48
+ """Register all available tools to the provided agent.
49
+
50
+ Args:
51
+ agent: The agent to register tools to.
52
+ """
53
+ all_tools = list(TOOL_REGISTRY.keys())
54
+ register_tools_for_agent(agent, all_tools)
55
+
56
+
57
+ def get_available_tool_names() -> list[str]:
58
+ """Get list of all available tool names.
59
+
60
+ Returns:
61
+ List of all tool names that can be registered.
62
+ """
63
+ return list(TOOL_REGISTRY.keys())