solace-agent-mesh 1.4.4__py3-none-any.whl → 1.4.5__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 solace-agent-mesh might be problematic. Click here for more details.

Files changed (66) hide show
  1. solace_agent_mesh/agent/adk/setup.py +545 -470
  2. solace_agent_mesh/agent/sac/app.py +35 -15
  3. solace_agent_mesh/assets/docs/404.html +3 -3
  4. solace_agent_mesh/assets/docs/assets/images/sam-enterprise-credentials-b269f095349473118b2b33bdfcc40122.png +0 -0
  5. solace_agent_mesh/assets/docs/assets/js/ae0e903d.7c73bc4f.js +1 -0
  6. solace_agent_mesh/assets/docs/assets/js/{f284c35a.cad4dbf2.js → f284c35a.2b2f5048.js} +1 -1
  7. solace_agent_mesh/assets/docs/assets/js/{main.03fb0598.js → main.2b4fe82a.js} +2 -2
  8. solace_agent_mesh/assets/docs/assets/js/{runtime~main.4ee39c6f.js → runtime~main.c0805958.js} +1 -1
  9. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +27 -11
  10. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +3 -3
  11. solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
  12. solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +3 -3
  13. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +3 -3
  14. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +3 -3
  15. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +3 -3
  16. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +3 -3
  17. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +3 -3
  18. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +3 -3
  19. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +3 -3
  20. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +3 -3
  21. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +3 -3
  22. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +3 -3
  23. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +3 -3
  24. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +3 -3
  25. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  26. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +3 -3
  27. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +3 -3
  28. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +3 -3
  29. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +3 -3
  30. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +3 -3
  31. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +3 -3
  32. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +3 -3
  33. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +3 -3
  34. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +3 -3
  35. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +3 -3
  36. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +3 -3
  37. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +3 -3
  38. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +3 -3
  39. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +3 -3
  40. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +3 -3
  41. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +3 -3
  42. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +3 -3
  43. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +3 -3
  44. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +3 -3
  45. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +3 -3
  46. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +3 -3
  47. solace_agent_mesh/assets/docs/img/sam-enterprise-credentials.png +0 -0
  48. solace_agent_mesh/assets/docs/lunr-index-1758644347760.json +1 -0
  49. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  50. solace_agent_mesh/assets/docs/search-doc-1758644347760.json +1 -0
  51. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  52. solace_agent_mesh/cli/__init__.py +1 -1
  53. solace_agent_mesh/cli/commands/eval_cmd.py +3 -3
  54. solace_agent_mesh/client/webui/frontend/static/assets/main-B67MsY-v.js +339 -0
  55. solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
  56. solace_agent_mesh/common/utils/type_utils.py +28 -0
  57. {solace_agent_mesh-1.4.4.dist-info → solace_agent_mesh-1.4.5.dist-info}/METADATA +3 -3
  58. {solace_agent_mesh-1.4.4.dist-info → solace_agent_mesh-1.4.5.dist-info}/RECORD +62 -59
  59. solace_agent_mesh/assets/docs/assets/js/ae0e903d.ac3b9419.js +0 -1
  60. solace_agent_mesh/assets/docs/lunr-index-1758293998763.json +0 -1
  61. solace_agent_mesh/assets/docs/search-doc-1758293998763.json +0 -1
  62. solace_agent_mesh/client/webui/frontend/static/assets/main-Cv2k8j3R.js +0 -339
  63. /solace_agent_mesh/assets/docs/assets/js/{main.03fb0598.js.LICENSE.txt → main.2b4fe82a.js.LICENSE.txt} +0 -0
  64. {solace_agent_mesh-1.4.4.dist-info → solace_agent_mesh-1.4.5.dist-info}/WHEEL +0 -0
  65. {solace_agent_mesh-1.4.4.dist-info → solace_agent_mesh-1.4.5.dist-info}/entry_points.txt +0 -0
  66. {solace_agent_mesh-1.4.4.dist-info → solace_agent_mesh-1.4.5.dist-info}/licenses/LICENSE +0 -0
@@ -7,6 +7,7 @@ import functools
7
7
  import inspect
8
8
  from solace_ai_connector.common.log import log
9
9
  from solace_ai_connector.common.utils import import_module
10
+ from ...common.utils.type_utils import is_subclass_by_name
10
11
 
11
12
  from .app_llm_agent import AppLlmAgent
12
13
  from .tool_wrapper import ADKToolWrapper
@@ -31,20 +32,30 @@ if TYPE_CHECKING:
31
32
  from ..tools.registry import tool_registry
32
33
  from ..tools.tool_definition import BuiltinTool
33
34
  from ..tools.dynamic_tool import DynamicTool, DynamicToolProvider
34
- from ..tools.tool_config_types import AnyToolConfig
35
+ from ..tools.tool_config_types import (
36
+ AnyToolConfig,
37
+ BuiltinToolConfig,
38
+ BuiltinGroupToolConfig,
39
+ McpToolConfig,
40
+ PythonToolConfig,
41
+ )
35
42
 
36
43
 
37
44
  from ...agent.adk import callbacks as adk_callbacks
38
45
  from ...agent.adk.models.lite_llm import LiteLlm
39
46
 
40
47
 
48
+ # Define a clear return type for all tool-loading helpers
49
+ ToolLoadingResult = Tuple[List[Union[BaseTool, Callable]], List[BuiltinTool], List[Callable]]
50
+
51
+
41
52
  def _find_dynamic_tool_class(module) -> Optional[type]:
42
53
  """Finds a single non-abstract DynamicTool subclass in a module."""
43
54
  found_classes = []
44
55
  for name, obj in inspect.getmembers(module, inspect.isclass):
45
56
  if (
46
- issubclass(obj, DynamicTool)
47
- and obj is not DynamicTool
57
+ is_subclass_by_name(obj, "DynamicTool")
58
+ and not is_subclass_by_name(obj, "DynamicToolProvider")
48
59
  and not inspect.isabstract(obj)
49
60
  ):
50
61
  found_classes.append(obj)
@@ -137,10 +148,8 @@ def _find_dynamic_tool_provider_class(module) -> Optional[type]:
137
148
  """Finds a single non-abstract DynamicToolProvider subclass in a module."""
138
149
  found_classes = []
139
150
  for name, obj in inspect.getmembers(module, inspect.isclass):
140
- if (
141
- issubclass(obj, DynamicToolProvider)
142
- and obj is not DynamicToolProvider
143
- and not inspect.isabstract(obj)
151
+ if is_subclass_by_name(obj, "DynamicToolProvider") and not inspect.isabstract(
152
+ obj
144
153
  ):
145
154
  found_classes.append(obj)
146
155
  if len(found_classes) > 1:
@@ -151,512 +160,445 @@ def _find_dynamic_tool_provider_class(module) -> Optional[type]:
151
160
  return found_classes[0] if found_classes else None
152
161
 
153
162
 
154
- async def load_adk_tools(
155
- component,
156
- ) -> Tuple[List[Union[BaseTool, Callable]], List[BuiltinTool], List[Callable]]:
163
+ def _check_and_register_tool_name(name: str, source: str, loaded_tool_names: Set[str]):
164
+ """Checks for duplicate tool names and raises ValueError if found."""
165
+ if name in loaded_tool_names:
166
+ raise ValueError(
167
+ f"Configuration Error: Duplicate tool name '{name}' found from source '{source}'. "
168
+ "This name is already in use. Please resolve the conflict by renaming or "
169
+ "disabling one of the tools in your agent's configuration."
170
+ )
171
+ loaded_tool_names.add(name)
172
+
173
+
174
+ async def _create_python_tool_lifecycle_hooks(
175
+ component: "SamAgentComponent",
176
+ tool_config_model: "PythonToolConfig",
177
+ loaded_python_tools: List[Union[BaseTool, Callable]],
178
+ ) -> List[Callable]:
157
179
  """
158
- Loads all configured tools for the agent.
159
- - Explicitly configured tools (Python, MCP, ADK Built-ins) from YAML.
160
- - SAM Built-in tools (Artifact, Data, etc.) from the tool registry,
161
- filtered by agent configuration.
180
+ Executes init hooks and collects cleanup hooks for a Python tool.
181
+ Handles both YAML-defined hooks and class-based init/cleanup methods.
182
+ Returns cleanup hooks in LIFO order.
183
+ """
184
+ module_name = tool_config_model.component_module
185
+ base_path = tool_config_model.component_base_path
186
+ cleanup_hooks = []
187
+
188
+ # 1. YAML Init (runs first)
189
+ await _execute_lifecycle_hook(
190
+ component,
191
+ tool_config_model.init_function,
192
+ module_name,
193
+ base_path,
194
+ tool_config_model,
195
+ )
162
196
 
163
- Args:
164
- component: The SamAgentComponent instance.
197
+ # 2. DynamicTool/Provider Init (runs second)
198
+ for tool_instance in loaded_python_tools:
199
+ if is_subclass_by_name(type(tool_instance), "DynamicTool"):
200
+ log.info(
201
+ "%s Executing .init() method for DynamicTool '%s'.",
202
+ component.log_identifier,
203
+ tool_instance.tool_name,
204
+ )
205
+ await tool_instance.init(component, tool_config_model)
206
+
207
+ # 3. Collect Cleanup Hooks (in reverse order of init)
208
+ # Class-based cleanup hook (will be executed first)
209
+ for tool_instance in loaded_python_tools:
210
+ if is_subclass_by_name(type(tool_instance), "DynamicTool"):
211
+ cleanup_hooks.append(
212
+ functools.partial(
213
+ tool_instance.cleanup, component, tool_config_model
214
+ )
215
+ )
165
216
 
166
- Returns:
167
- A tuple containing:
168
- - A list of loaded tool callables/instances for the ADK agent.
169
- - A list of enabled BuiltinTool definition objects for prompt generation.
170
- - A list of awaitable cleanup functions for the tools.
217
+ # YAML-based cleanup hook (will be executed second)
218
+ yaml_cleanup_partial = _create_cleanup_partial(
219
+ component,
220
+ tool_config_model.cleanup_function,
221
+ module_name,
222
+ base_path,
223
+ tool_config_model,
224
+ )
225
+ if yaml_cleanup_partial:
226
+ cleanup_hooks.append(yaml_cleanup_partial)
171
227
 
172
- Raises:
173
- ImportError: If a configured tool or its dependencies cannot be loaded.
228
+ # Return in LIFO order relative to init
229
+ return list(reversed(cleanup_hooks))
230
+
231
+
232
+ def _load_python_class_based_tool(
233
+ module: Any,
234
+ tool_config: Dict,
235
+ component: "SamAgentComponent",
236
+ ) -> List[DynamicTool]:
174
237
  """
175
- loaded_tools: List[Union[BaseTool, Callable]] = []
176
- enabled_builtin_tools: List[BuiltinTool] = []
177
- loaded_tool_names: Set[str] = set()
178
- cleanup_hooks: List[Callable] = []
179
- tools_config = component.get_config("tools", [])
238
+ Loads a class-based tool, which can be a single DynamicTool or a
239
+ DynamicToolProvider that generates multiple tools.
240
+ """
241
+ from pydantic import BaseModel, ValidationError
180
242
 
181
- from pydantic import TypeAdapter, BaseModel, ValidationError
243
+ specific_tool_config = tool_config.get("tool_config")
244
+ dynamic_tools: List[DynamicTool] = []
245
+ module_name = module.__name__
182
246
 
183
- any_tool_adapter = TypeAdapter(AnyToolConfig)
247
+ # Determine the class to load
248
+ tool_class = None
249
+ class_name = tool_config.get("class_name")
250
+ if class_name:
251
+ tool_class = getattr(module, class_name)
252
+ else:
253
+ # Auto-discover: provider first, then single tool
254
+ tool_class = _find_dynamic_tool_provider_class(module)
255
+ if not tool_class:
256
+ tool_class = _find_dynamic_tool_class(module)
184
257
 
185
- def _check_and_register_tool_name(name: str, source: str):
186
- """Checks for duplicate tool names and raises ValueError if found."""
187
- if name in loaded_tool_names:
188
- raise ValueError(
189
- f"Configuration Error: Duplicate tool name '{name}' found from source '{source}'. "
190
- "This name is already in use. Please resolve the conflict by renaming or "
191
- "disabling one of the tools in your agent's configuration."
192
- )
193
- loaded_tool_names.add(name)
258
+ if not tool_class:
259
+ raise TypeError(
260
+ f"Module '{module_name}' does not contain a 'function_name' or 'class_name' to load, "
261
+ "and no DynamicTool or DynamicToolProvider subclass could be auto-discovered."
262
+ )
194
263
 
195
- if not tools_config:
264
+ # Check for a Pydantic model declaration on the tool class
265
+ config_model: Optional[Type["BaseModel"]] = getattr(
266
+ tool_class, "config_model", None
267
+ )
268
+ validated_config: Union[dict, "BaseModel"] = specific_tool_config
269
+
270
+ if config_model:
271
+ log.debug(
272
+ "%s Found config_model '%s' for tool class '%s'. Validating...",
273
+ component.log_identifier,
274
+ config_model.__name__,
275
+ tool_class.__name__,
276
+ )
277
+ try:
278
+ # Validate the raw dict and get a Pydantic model instance
279
+ validated_config = config_model.model_validate(specific_tool_config or {})
280
+ log.debug(
281
+ "%s Successfully validated tool_config for '%s'.",
282
+ component.log_identifier,
283
+ tool_class.__name__,
284
+ )
285
+ except ValidationError as e:
286
+ # Provide a clear error message and raise
287
+ error_msg = (
288
+ f"Configuration error for tool '{tool_class.__name__}' from module '{module_name}'. "
289
+ f"The provided 'tool_config' in your YAML is invalid:\n{e}"
290
+ )
291
+ log.error("%s %s", component.log_identifier, error_msg)
292
+ raise ValueError(error_msg) from e
293
+
294
+ # Instantiate tools from the class
295
+ if is_subclass_by_name(tool_class, "DynamicToolProvider"):
296
+ provider_instance = tool_class()
297
+ dynamic_tools = provider_instance.get_all_tools_for_framework(
298
+ tool_config=validated_config
299
+ )
196
300
  log.info(
197
- "%s No explicit tools configured in 'tools' list.", component.log_identifier
301
+ "%s Loaded %d tools from DynamicToolProvider '%s' in %s",
302
+ component.log_identifier,
303
+ len(dynamic_tools),
304
+ tool_class.__name__,
305
+ module_name,
198
306
  )
307
+ elif is_subclass_by_name(tool_class, "DynamicTool"):
308
+ tool_instance = tool_class(tool_config=validated_config)
309
+ dynamic_tools = [tool_instance]
199
310
  else:
200
- log.debug(
201
- "%s Processing %d tool configurations: %s",
202
- component.log_identifier,
203
- len(tools_config),
204
- [tc.get("tool_type") for tc in tools_config],
311
+ raise TypeError(
312
+ f"Class '{tool_class.__name__}' in module '{module_name}' is not a valid "
313
+ "DynamicTool or DynamicToolProvider subclass."
205
314
  )
315
+
316
+ # Post-process all generated tools
317
+ for tool in dynamic_tools:
318
+ tool.origin = "dynamic"
319
+ declaration = tool._get_declaration()
320
+ if not declaration:
321
+ log.warning(
322
+ "Dynamic tool '%s' from module '%s' did not generate a valid declaration. Skipping.",
323
+ tool.__class__.__name__,
324
+ module_name,
325
+ )
326
+ continue
206
327
  log.info(
207
- "%s Loading tools from 'tools' list configuration...",
328
+ "%s Loaded dynamic tool: %s from %s",
208
329
  component.log_identifier,
330
+ declaration.name,
331
+ module_name,
209
332
  )
210
- for tool_config in tools_config:
211
- newly_loaded_tools = []
212
- try:
213
- tool_config_model = any_tool_adapter.validate_python(tool_config)
214
- tool_type = tool_config_model.tool_type.lower()
215
333
 
216
- if tool_type == "python":
217
- module_name = tool_config.get("component_module")
218
- base_path = tool_config.get("component_base_path")
219
- if not module_name:
220
- raise ValueError(
221
- "'component_module' is required for python tools."
222
- )
223
- module = import_module(module_name, base_path=base_path)
224
-
225
- # Case 1: Simple function-based tool
226
- if "function_name" in tool_config:
227
- function_name = tool_config.get("function_name")
228
- tool_name = tool_config.get("tool_name")
229
- tool_description = tool_config.get("tool_description")
230
-
231
- func = getattr(module, function_name)
232
- if not callable(func):
233
- raise TypeError(
234
- f"'{function_name}' in module '{module_name}' is not callable."
235
- )
334
+ return dynamic_tools
236
335
 
237
- specific_tool_config = tool_config.get("tool_config")
238
- tool_callable = ADKToolWrapper(
239
- func,
240
- specific_tool_config,
241
- function_name,
242
- origin="python",
243
- raw_string_args=tool_config.get("raw_string_args", []),
244
- )
245
336
 
246
- if tool_name:
247
- function_name = tool_name
248
- tool_callable.__name__ = tool_name
337
+ async def _load_python_tool(component: "SamAgentComponent", tool_config: Dict) -> ToolLoadingResult:
338
+ from pydantic import TypeAdapter
249
339
 
250
- if tool_description:
251
- tool_callable.__doc__ = tool_description
340
+ python_tool_adapter = TypeAdapter(PythonToolConfig)
341
+ tool_config_model = python_tool_adapter.validate_python(tool_config)
252
342
 
253
- _check_and_register_tool_name(
254
- function_name, f"python:{module_name}"
255
- )
256
- loaded_tools.append(tool_callable)
257
- newly_loaded_tools.append(tool_callable)
258
- log.info(
259
- "%s Loaded Python tool: %s from %s.",
260
- component.log_identifier,
261
- function_name,
262
- module_name,
263
- )
264
- # Case 2: Advanced class-based dynamic tool or provider
265
- else:
266
- specific_tool_config = tool_config.get("tool_config")
267
- dynamic_tools = []
268
-
269
- # Determine the class to load
270
- tool_class = None
271
- class_name = tool_config.get("class_name")
272
- if class_name:
273
- tool_class = getattr(module, class_name)
274
- else:
275
- # Auto-discover: provider first, then single tool
276
- tool_class = _find_dynamic_tool_provider_class(module)
277
- if not tool_class:
278
- tool_class = _find_dynamic_tool_class(module)
279
-
280
- if not tool_class:
281
- raise TypeError(
282
- f"Module '{module_name}' does not contain a 'function_name' or 'class_name' to load, "
283
- "and no DynamicTool or DynamicToolProvider subclass could be auto-discovered."
284
- )
343
+ module_name = tool_config_model.component_module
344
+ base_path = tool_config_model.component_base_path
345
+ if not module_name:
346
+ raise ValueError("'component_module' is required for python tools.")
347
+ module = import_module(module_name, base_path=base_path)
285
348
 
286
- # Check for a Pydantic model declaration on the tool class
287
- config_model: Optional[Type["BaseModel"]] = getattr(
288
- tool_class, "config_model", None
289
- )
290
- validated_config: Union[dict, "BaseModel"] = specific_tool_config
349
+ loaded_python_tools: List[Union[BaseTool, Callable]] = []
291
350
 
292
- if config_model:
293
- log.debug(
294
- "%s Found config_model '%s' for tool class '%s'. Validating...",
295
- component.log_identifier,
296
- config_model.__name__,
297
- tool_class.__name__,
298
- )
299
- try:
300
- # Validate the raw dict and get a Pydantic model instance
301
- validated_config = config_model.model_validate(
302
- specific_tool_config or {}
303
- )
304
- log.debug(
305
- "%s Successfully validated tool_config for '%s'.",
306
- component.log_identifier,
307
- tool_class.__name__,
308
- )
309
- except ValidationError as e:
310
- # Provide a clear error message and raise
311
- error_msg = (
312
- f"Configuration error for tool '{tool_class.__name__}' from module '{module_name}'. "
313
- f"The provided 'tool_config' in your YAML is invalid:\n{e}"
314
- )
315
- log.error("%s %s", component.log_identifier, error_msg)
316
- raise ValueError(error_msg) from e
317
-
318
- # Instantiate tools from the class
319
- if issubclass(tool_class, DynamicToolProvider):
320
- provider_instance = tool_class()
321
- dynamic_tools = (
322
- provider_instance.get_all_tools_for_framework(
323
- tool_config=validated_config
324
- )
325
- )
326
- log.info(
327
- "%s Loaded %d tools from DynamicToolProvider '%s' in %s",
328
- component.log_identifier,
329
- len(dynamic_tools),
330
- tool_class.__name__,
331
- module_name,
332
- )
333
- elif issubclass(tool_class, DynamicTool):
334
- tool_instance = tool_class(tool_config=validated_config)
335
- dynamic_tools = [tool_instance]
336
- else:
337
- raise TypeError(
338
- f"Class '{tool_class.__name__}' in module '{module_name}' is not a valid "
339
- "DynamicTool or DynamicToolProvider subclass."
340
- )
351
+ # Case 1: Simple function-based tool
352
+ if tool_config_model.function_name:
353
+ func = getattr(module, tool_config_model.function_name)
354
+ if not callable(func):
355
+ raise TypeError(
356
+ f"'{tool_config_model.function_name}' in module '{module_name}' is not callable."
357
+ )
341
358
 
342
- # Process all generated tools
343
- for tool in dynamic_tools:
344
- tool.origin = "dynamic"
345
- declaration = tool._get_declaration()
346
- if not declaration:
347
- log.warning(
348
- "Dynamic tool '%s' from module '%s' did not generate a valid declaration. Skipping.",
349
- tool.__class__.__name__,
350
- module_name,
351
- )
352
- continue
359
+ tool_callable = ADKToolWrapper(
360
+ func,
361
+ tool_config_model.tool_config,
362
+ tool_config_model.function_name,
363
+ origin="python",
364
+ raw_string_args=tool_config_model.raw_string_args,
365
+ )
353
366
 
354
- _check_and_register_tool_name(
355
- declaration.name, f"dynamic:{module_name}"
356
- )
357
- loaded_tools.append(tool)
358
- newly_loaded_tools.append(tool)
359
- log.info(
360
- "%s Loaded dynamic tool: %s from %s",
361
- component.log_identifier,
362
- declaration.name,
363
- module_name,
364
- )
367
+ if tool_config_model.tool_name:
368
+ tool_callable.__name__ = tool_config_model.tool_name
369
+ if tool_config_model.tool_description:
370
+ tool_callable.__doc__ = tool_config_model.tool_description
365
371
 
366
- # --- Lifecycle Hook Execution for Python Tools ---
367
- module_name = tool_config_model.component_module
368
- base_path = tool_config_model.component_base_path
369
-
370
- # 1. YAML Init
371
- await _execute_lifecycle_hook(
372
- component,
373
- tool_config_model.init_function,
374
- module_name,
375
- base_path,
376
- tool_config_model,
377
- )
372
+ loaded_python_tools.append(tool_callable)
373
+ log.info(
374
+ "%s Loaded Python tool: %s from %s.",
375
+ component.log_identifier,
376
+ tool_callable.__name__,
377
+ module_name,
378
+ )
379
+ # Case 2: Advanced class-based dynamic tool or provider
380
+ else:
381
+ dynamic_tools = _load_python_class_based_tool(module, tool_config, component)
382
+ loaded_python_tools.extend(dynamic_tools)
378
383
 
379
- # 2. DynamicTool Init
380
- for tool_instance in newly_loaded_tools:
381
- if isinstance(tool_instance, DynamicTool):
382
- log.info(
383
- "%s Executing .init() method for DynamicTool '%s'.",
384
- component.log_identifier,
385
- tool_instance.tool_name,
386
- )
387
- await tool_instance.init(component, tool_config_model)
388
-
389
- # 3. Collect Cleanup Hooks (LIFO order)
390
- yaml_cleanup_partial = _create_cleanup_partial(
391
- component,
392
- tool_config_model.cleanup_function,
393
- module_name,
394
- base_path,
395
- tool_config_model,
396
- )
384
+ # --- Lifecycle Hook Execution for all Python Tools ---
385
+ cleanup_hooks = await _create_python_tool_lifecycle_hooks(
386
+ component, tool_config_model, loaded_python_tools
387
+ )
397
388
 
398
- dynamic_cleanup_partials = []
399
- for tool_instance in newly_loaded_tools:
400
- if isinstance(tool_instance, DynamicTool):
401
- dynamic_cleanup_partials.append(
402
- functools.partial(
403
- tool_instance.cleanup, component, tool_config_model
404
- )
405
- )
389
+ return loaded_python_tools, [], cleanup_hooks
406
390
 
407
- # Add to main list, prepending to get LIFO execution
408
- if yaml_cleanup_partial:
409
- cleanup_hooks.insert(0, yaml_cleanup_partial)
391
+ async def _load_builtin_tool(component: "SamAgentComponent", tool_config: Dict) -> ToolLoadingResult:
392
+ """Loads a single built-in tool from the SAM or ADK tool registry."""
393
+ from pydantic import TypeAdapter
410
394
 
411
- for partial_func in reversed(dynamic_cleanup_partials):
412
- cleanup_hooks.insert(0, partial_func)
395
+ builtin_tool_adapter = TypeAdapter(BuiltinToolConfig)
396
+ tool_config_model = builtin_tool_adapter.validate_python(tool_config)
413
397
 
414
- elif tool_type == "builtin":
415
- tool_name = tool_config.get("tool_name")
416
- if not tool_name:
417
- raise ValueError("'tool_name' required for builtin tool.")
418
-
419
- _check_and_register_tool_name(tool_name, "builtin")
420
-
421
- sam_tool_def = tool_registry.get_tool_by_name(tool_name)
422
- if sam_tool_def:
423
- specific_tool_config = tool_config.get("tool_config")
424
- tool_callable = ADKToolWrapper(
425
- sam_tool_def.implementation,
426
- specific_tool_config,
427
- sam_tool_def.name,
428
- origin="builtin",
429
- raw_string_args=sam_tool_def.raw_string_args,
430
- )
431
- loaded_tools.append(tool_callable)
432
- newly_loaded_tools.append(tool_callable)
433
- enabled_builtin_tools.append(sam_tool_def)
434
- log.info(
435
- "%s Loaded SAM built-in tool: %s",
436
- component.log_identifier,
437
- sam_tool_def.name,
438
- )
439
- continue
440
-
441
- adk_tool = getattr(adk_tools_module, tool_name, None)
442
- if adk_tool and isinstance(adk_tool, (BaseTool, Callable)):
443
- adk_tool.origin = "adk_builtin"
444
- loaded_tools.append(adk_tool)
445
- log.info(
446
- "%s Loaded ADK built-in tool: %s",
447
- component.log_identifier,
448
- tool_name,
449
- )
450
- continue
398
+ tool_name = tool_config_model.tool_name
399
+ if not tool_name:
400
+ raise ValueError("'tool_name' required for builtin tool.")
451
401
 
452
- raise ValueError(
453
- f"Built-in tool '{tool_name}' not found in SAM or ADK registry."
454
- )
402
+ # Check SAM registry first
403
+ sam_tool_def = tool_registry.get_tool_by_name(tool_name)
404
+ if sam_tool_def:
405
+ tool_callable = ADKToolWrapper(
406
+ sam_tool_def.implementation,
407
+ tool_config_model.tool_config,
408
+ sam_tool_def.name,
409
+ origin="builtin",
410
+ raw_string_args=sam_tool_def.raw_string_args,
411
+ )
412
+ log.info(
413
+ "%s Loaded SAM built-in tool: %s",
414
+ component.log_identifier,
415
+ sam_tool_def.name,
416
+ )
417
+ return [tool_callable], [sam_tool_def], []
455
418
 
456
- elif tool_type == "builtin-group":
457
- group_name = tool_config.get("group_name")
458
- if not group_name:
459
- raise ValueError("'group_name' required for builtin-group.")
460
-
461
- tools_in_group = tool_registry.get_tools_by_category(group_name)
462
- if not tools_in_group:
463
- log.warning("No tools found for built-in group: %s", group_name)
464
- continue
465
-
466
- initializers_to_run: Dict[Callable, Dict] = {}
467
- for tool_def in tools_in_group:
468
- if (
469
- tool_def.initializer
470
- and tool_def.initializer not in initializers_to_run
471
- ):
472
- initializers_to_run[tool_def.initializer] = tool_config.get(
473
- "config", {}
474
- )
419
+ # Fallback to ADK built-in tools module
420
+ adk_tool = getattr(adk_tools_module, tool_name, None)
421
+ if adk_tool and isinstance(adk_tool, (BaseTool, Callable)):
422
+ adk_tool.origin = "adk_builtin"
423
+ log.info(
424
+ "%s Loaded ADK built-in tool: %s",
425
+ component.log_identifier,
426
+ tool_name,
427
+ )
428
+ return [adk_tool], [], []
475
429
 
476
- for init_func, init_config in initializers_to_run.items():
477
- try:
478
- log.info(
479
- "%s Running initializer '%s' for tool group '%s'.",
480
- component.log_identifier,
481
- init_func.__name__,
482
- group_name,
483
- )
484
- init_func(component, init_config)
485
- log.info(
486
- "%s Successfully executed initializer '%s' for tool group '%s'.",
487
- component.log_identifier,
488
- init_func.__name__,
489
- group_name,
490
- )
491
- except Exception as e:
492
- log.exception(
493
- "%s Failed to run initializer '%s' for tool group '%s': %s",
494
- component.log_identifier,
495
- init_func.__name__,
496
- group_name,
497
- e,
498
- )
499
- raise e
430
+ raise ValueError(
431
+ f"Built-in tool '{tool_name}' not found in SAM or ADK registry."
432
+ )
500
433
 
501
- group_tool_count = 0
502
- for tool_def in tools_in_group:
503
- _check_and_register_tool_name(
504
- tool_def.name, f"builtin-group:{group_name}"
505
- )
506
- specific_tool_config = tool_config.get("tool_configs", {}).get(
507
- tool_def.name
508
- )
509
- tool_callable = ADKToolWrapper(
510
- tool_def.implementation,
511
- specific_tool_config,
512
- tool_def.name,
513
- origin="builtin",
514
- raw_string_args=tool_def.raw_string_args,
515
- )
516
- loaded_tools.append(tool_callable)
517
- newly_loaded_tools.append(tool_callable)
518
- enabled_builtin_tools.append(tool_def)
519
- group_tool_count += 1
520
- log.info(
521
- "Loaded %d tools from built-in group: %s",
522
- group_tool_count,
523
- group_name,
524
- )
434
+ async def _load_builtin_group_tool(component: "SamAgentComponent", tool_config: Dict) -> ToolLoadingResult:
435
+ """Loads a group of built-in tools by category from the SAM tool registry."""
436
+ from pydantic import TypeAdapter
525
437
 
526
- elif tool_type == "mcp":
527
- tool_name = tool_config.get("tool_name")
528
- if not tool_name:
529
- log.info(
530
- "%s No specific 'tool_name' for MCP tool, will load all tools from server unless tool_filter is specified in MCPToolset itself.",
531
- component.log_identifier,
532
- )
438
+ group_tool_adapter = TypeAdapter(BuiltinGroupToolConfig)
439
+ tool_config_model = group_tool_adapter.validate_python(tool_config)
533
440
 
534
- connection_params_config = tool_config.get("connection_params")
535
- if not connection_params_config:
536
- raise ValueError("'connection_params' required for mcp tool.")
537
-
538
- connection_type = connection_params_config.get("type", "").lower()
539
- connection_args = {
540
- k: v for k, v in connection_params_config.items() if k != "type"
541
- }
542
- connection_args["timeout"] = connection_args.get("timeout", 30)
543
-
544
- environment_variables = tool_config.get("environment_variables")
545
- env_param = {}
546
- if connection_type == "stdio" and environment_variables:
547
- if isinstance(environment_variables, dict):
548
- env_param = environment_variables
549
- log.debug(
550
- "%s Found environment_variables for stdio MCP tool.",
551
- component.log_identifier,
552
- )
553
- else:
554
- log.warning(
555
- "%s 'environment_variables' provided for stdio MCP tool but it is not a dictionary. Ignoring.",
556
- component.log_identifier,
557
- )
441
+ group_name = tool_config_model.group_name
442
+ if not group_name:
443
+ raise ValueError("'group_name' required for builtin-group.")
558
444
 
559
- if connection_type == "stdio":
560
- cmd_arg = connection_args.get("command")
561
- args_list = connection_args.get("args", [])
562
- if isinstance(cmd_arg, list):
563
- command_str = " ".join(cmd_arg)
564
- elif isinstance(cmd_arg, str):
565
- command_str = cmd_arg
566
- else:
567
- raise ValueError(
568
- f"MCP tool 'command' parameter must be a string or a list of strings, got {type(cmd_arg)}"
569
- )
570
- if not isinstance(args_list, list):
571
- raise ValueError(
572
- f"MCP tool 'args' parameter must be a list, got {type(args_list)}"
573
- )
574
- final_connection_args = {
575
- k: v
576
- for k, v in connection_args.items()
577
- if k not in ["command", "args", "timeout"]
578
- }
579
- connection_params = StdioConnectionParams(
580
- server_params=StdioServerParameters(
581
- command=command_str,
582
- args=args_list,
583
- **final_connection_args,
584
- env=env_param if env_param else None,
585
- ),
586
- timeout=connection_args.get("timeout"),
587
- )
445
+ tools_in_group = tool_registry.get_tools_by_category(group_name)
446
+ if not tools_in_group:
447
+ log.warning("No tools found for built-in group: %s", group_name)
448
+ return [], [], []
588
449
 
589
- elif connection_type == "sse":
590
- connection_params = SseServerParams(**connection_args)
591
- else:
592
- raise ValueError(
593
- f"Unsupported MCP connection type: {connection_type}"
594
- )
450
+ # Run initializers for the group
451
+ initializers_to_run: Dict[Callable, Dict] = {}
452
+ for tool_def in tools_in_group:
453
+ if (
454
+ tool_def.initializer
455
+ and tool_def.initializer not in initializers_to_run
456
+ ):
457
+ initializers_to_run[tool_def.initializer] = tool_config_model.tool_config
595
458
 
596
- tool_filter_list = [tool_name] if tool_name else None
597
- if tool_filter_list:
598
- log.info(
599
- "%s MCP tool config specifies tool_name: '%s'. Applying as tool_filter.",
600
- component.log_identifier,
601
- tool_name,
602
- )
459
+ for init_func, init_config in initializers_to_run.items():
460
+ try:
461
+ log.info(
462
+ "%s Running initializer '%s' for tool group '%s'.",
463
+ component.log_identifier,
464
+ init_func.__name__,
465
+ group_name,
466
+ )
467
+ init_func(component, init_config)
468
+ log.info(
469
+ "%s Successfully executed initializer '%s' for tool group '%s'.",
470
+ component.log_identifier,
471
+ init_func.__name__,
472
+ group_name,
473
+ )
474
+ except Exception as e:
475
+ log.exception(
476
+ "%s Failed to run initializer '%s' for tool group '%s': %s",
477
+ component.log_identifier,
478
+ init_func.__name__,
479
+ group_name,
480
+ e,
481
+ )
482
+ raise e
603
483
 
604
- mcp_toolset_instance = EmbedResolvingMCPToolset(
605
- connection_params=connection_params,
606
- tool_filter=tool_filter_list,
607
- tool_config=tool_config,
608
- )
609
- mcp_toolset_instance.origin = "mcp"
610
-
611
- # Check for duplicates from the MCP server
612
- try:
613
- mcp_tools = await mcp_toolset_instance.get_tools()
614
- log.debug(
615
- "%s Successfully discovered %d tools from MCP server: %s",
616
- component.log_identifier,
617
- len(mcp_tools),
618
- [tool.name for tool in mcp_tools],
619
- )
620
- for mcp_tool in mcp_tools:
621
- log.debug(
622
- "%s Registering MCP tool: %s",
623
- component.log_identifier,
624
- mcp_tool.name,
625
- )
626
- _check_and_register_tool_name(mcp_tool.name, "mcp")
627
- except Exception as e:
628
- log.error(
629
- "%s Failed to discover tools from MCP server: %s",
630
- component.log_identifier,
631
- str(e),
632
- )
633
- raise
484
+ loaded_tools: List[Union[BaseTool, Callable]] = []
485
+ enabled_builtin_tools: List[BuiltinTool] = []
486
+ for tool_def in tools_in_group:
487
+ specific_tool_config = tool_config_model.tool_config.get(tool_def.name)
488
+ tool_callable = ADKToolWrapper(
489
+ tool_def.implementation,
490
+ specific_tool_config,
491
+ tool_def.name,
492
+ origin="builtin",
493
+ raw_string_args=tool_def.raw_string_args,
494
+ )
495
+ loaded_tools.append(tool_callable)
496
+ enabled_builtin_tools.append(tool_def)
634
497
 
635
- loaded_tools.append(mcp_toolset_instance)
636
- newly_loaded_tools.append(mcp_toolset_instance)
637
- log.info(
638
- "%s Initialized MCPToolset (filter: %s) for server: %s",
639
- component.log_identifier,
640
- (tool_filter_list if tool_filter_list else "none (all tools)"),
641
- connection_params,
642
- )
498
+ log.info(
499
+ "Loaded %d tools from built-in group: %s",
500
+ len(loaded_tools),
501
+ group_name,
502
+ )
503
+ return loaded_tools, enabled_builtin_tools, []
504
+
505
+ async def _load_mcp_tool(component: "SamAgentComponent", tool_config: Dict) -> ToolLoadingResult:
506
+ """Loads an MCP toolset based on connection parameters."""
507
+ from pydantic import TypeAdapter
508
+
509
+ mcp_tool_adapter = TypeAdapter(McpToolConfig)
510
+ tool_config_model = mcp_tool_adapter.validate_python(tool_config)
511
+
512
+ connection_params_config = tool_config_model.connection_params
513
+ if not connection_params_config:
514
+ raise ValueError("'connection_params' required for mcp tool.")
515
+
516
+ connection_type = connection_params_config.get("type", "").lower()
517
+ connection_args = {
518
+ k: v for k, v in connection_params_config.items() if k != "type"
519
+ }
520
+ connection_args["timeout"] = connection_args.get("timeout", 30)
521
+
522
+ environment_variables = tool_config_model.environment_variables
523
+ env_param = {}
524
+ if connection_type == "stdio" and environment_variables:
525
+ if isinstance(environment_variables, dict):
526
+ env_param = environment_variables
527
+ log.debug(
528
+ "%s Found environment_variables for stdio MCP tool.",
529
+ component.log_identifier,
530
+ )
531
+ else:
532
+ log.warning(
533
+ "%s 'environment_variables' provided for stdio MCP tool but it is not a dictionary. Ignoring.",
534
+ component.log_identifier,
535
+ )
643
536
 
644
- else:
645
- log.warning(
646
- "%s Unknown tool type '%s' in config: %s",
647
- component.log_identifier,
648
- tool_type,
649
- tool_config,
650
- )
537
+ if connection_type == "stdio":
538
+ cmd_arg = connection_args.get("command")
539
+ args_list = connection_args.get("args", [])
540
+ if isinstance(cmd_arg, list):
541
+ command_str = " ".join(cmd_arg)
542
+ elif isinstance(cmd_arg, str):
543
+ command_str = cmd_arg
544
+ else:
545
+ raise ValueError(
546
+ f"MCP tool 'command' parameter must be a string or a list of strings, got {type(cmd_arg)}"
547
+ )
548
+ if not isinstance(args_list, list):
549
+ raise ValueError(
550
+ f"MCP tool 'args' parameter must be a list, got {type(args_list)}"
551
+ )
552
+ final_connection_args = {
553
+ k: v
554
+ for k, v in connection_args.items()
555
+ if k not in ["command", "args", "timeout"]
556
+ }
557
+ connection_params = StdioConnectionParams(
558
+ server_params=StdioServerParameters(
559
+ command=command_str,
560
+ args=args_list,
561
+ **final_connection_args,
562
+ env=env_param if env_param else None,
563
+ ),
564
+ timeout=connection_args.get("timeout"),
565
+ )
651
566
 
652
- except Exception as e:
653
- log.error(
654
- "%s Failed to load tool config %s: %s",
655
- component.log_identifier,
656
- tool_config,
657
- e,
658
- )
659
- raise e
567
+ elif connection_type == "sse":
568
+ connection_params = SseServerParams(**connection_args)
569
+ else:
570
+ raise ValueError(f"Unsupported MCP connection type: {connection_type}")
571
+
572
+ tool_filter_list = (
573
+ [tool_config_model.tool_name] if tool_config_model.tool_name else None
574
+ )
575
+ if tool_filter_list:
576
+ log.info(
577
+ "%s MCP tool config specifies tool_name: '%s'. Applying as tool_filter.",
578
+ component.log_identifier,
579
+ tool_config_model.tool_name,
580
+ )
581
+
582
+ mcp_toolset_instance = EmbedResolvingMCPToolset(
583
+ connection_params=connection_params,
584
+ tool_filter=tool_filter_list,
585
+ tool_config=tool_config,
586
+ )
587
+ mcp_toolset_instance.origin = "mcp"
588
+
589
+ log.info(
590
+ "%s Initialized MCPToolset (filter: %s) for server: %s",
591
+ component.log_identifier,
592
+ (tool_filter_list if tool_filter_list else "none (all tools)"),
593
+ connection_params,
594
+ )
595
+
596
+ return [mcp_toolset_instance], [], []
597
+
598
+ def _load_internal_tools(component: "SamAgentComponent", loaded_tool_names: Set[str]) -> ToolLoadingResult:
599
+ """Loads internal framework tools that are not explicitly configured by the user."""
600
+ loaded_tools: List[Union[BaseTool, Callable]] = []
601
+ enabled_builtin_tools: List[BuiltinTool] = []
660
602
 
661
603
  internal_tool_names = ["_notify_artifact_save"]
662
604
  if component.get_config("enable_auto_continuation", True):
@@ -664,7 +606,7 @@ async def load_adk_tools(
664
606
 
665
607
  for tool_name in internal_tool_names:
666
608
  try:
667
- _check_and_register_tool_name(tool_name, "internal")
609
+ _check_and_register_tool_name(tool_name, "internal", loaded_tool_names)
668
610
  except ValueError:
669
611
  log.debug(
670
612
  "%s Internal tool '%s' was already loaded explicitly. Skipping implicit load.",
@@ -699,6 +641,139 @@ async def load_adk_tools(
699
641
  tool_name,
700
642
  )
701
643
 
644
+ return loaded_tools, enabled_builtin_tools, []
645
+
646
+
647
+ async def load_adk_tools(
648
+ component,
649
+ ) -> Tuple[List[Union[BaseTool, Callable]], List[BuiltinTool], List[Callable]]:
650
+ """
651
+ Loads all configured tools for the agent.
652
+ - Explicitly configured tools (Python, MCP, ADK Built-ins) from YAML.
653
+ - SAM Built-in tools (Artifact, Data, etc.) from the tool registry,
654
+ filtered by agent configuration.
655
+
656
+ Args:
657
+ component: The SamAgentComponent instance.
658
+
659
+ Returns:
660
+ A tuple containing:
661
+ - A list of loaded tool callables/instances for the ADK agent.
662
+ - A list of enabled BuiltinTool definition objects for prompt generation.
663
+ - A list of awaitable cleanup functions for the tools.
664
+
665
+ Raises:
666
+ ImportError: If a configured tool or its dependencies cannot be loaded.
667
+ """
668
+ loaded_tools: List[Union[BaseTool, Callable]] = []
669
+ enabled_builtin_tools: List[BuiltinTool] = []
670
+ loaded_tool_names: Set[str] = set()
671
+ cleanup_hooks: List[Callable] = []
672
+ tools_config = component.get_config("tools", [])
673
+
674
+ from pydantic import TypeAdapter, ValidationError
675
+
676
+ any_tool_adapter = TypeAdapter(AnyToolConfig)
677
+
678
+ if not tools_config:
679
+ log.info(
680
+ "%s No explicit tools configured in 'tools' list.", component.log_identifier
681
+ )
682
+ else:
683
+ log.info(
684
+ "%s Loading %d tool(s) from 'tools' list configuration...",
685
+ component.log_identifier,
686
+ len(tools_config),
687
+ )
688
+ for tool_config in tools_config:
689
+ try:
690
+ tool_config_model = any_tool_adapter.validate_python(tool_config)
691
+ tool_type = tool_config_model.tool_type.lower()
692
+
693
+ new_tools, new_builtins, new_cleanups = [], [], []
694
+
695
+ if tool_type == "python":
696
+ (
697
+ new_tools,
698
+ new_builtins,
699
+ new_cleanups,
700
+ ) = await _load_python_tool(component, tool_config)
701
+ elif tool_type == "builtin":
702
+ (
703
+ new_tools,
704
+ new_builtins,
705
+ new_cleanups,
706
+ ) = await _load_builtin_tool(component, tool_config)
707
+ elif tool_type == "builtin-group":
708
+ (
709
+ new_tools,
710
+ new_builtins,
711
+ new_cleanups,
712
+ ) = await _load_builtin_group_tool(component, tool_config)
713
+ elif tool_type == "mcp":
714
+ (
715
+ new_tools,
716
+ new_builtins,
717
+ new_cleanups,
718
+ ) = await _load_mcp_tool(component, tool_config)
719
+ else:
720
+ log.warning(
721
+ "%s Unknown tool type '%s' in config: %s",
722
+ component.log_identifier,
723
+ tool_type,
724
+ tool_config,
725
+ )
726
+
727
+ # Centralized name checking and result aggregation
728
+ for tool in new_tools:
729
+ if isinstance(tool, EmbedResolvingMCPToolset):
730
+ # Special handling for MCPToolset which can load multiple tools
731
+ try:
732
+ mcp_tools = await tool.get_tools()
733
+ for mcp_tool in mcp_tools:
734
+ _check_and_register_tool_name(
735
+ mcp_tool.name, "mcp", loaded_tool_names
736
+ )
737
+ except Exception as e:
738
+ log.error(
739
+ "%s Failed to discover tools from MCP server for name registration: %s",
740
+ component.log_identifier,
741
+ str(e),
742
+ )
743
+ raise
744
+ else:
745
+ tool_name = getattr(
746
+ tool, "name", getattr(tool, "__name__", None)
747
+ )
748
+ if tool_name:
749
+ _check_and_register_tool_name(
750
+ tool_name, tool_type, loaded_tool_names
751
+ )
752
+
753
+ loaded_tools.extend(new_tools)
754
+ enabled_builtin_tools.extend(new_builtins)
755
+ # Prepend cleanup hooks to maintain LIFO execution order
756
+ cleanup_hooks = new_cleanups + cleanup_hooks
757
+
758
+ except Exception as e:
759
+ log.error(
760
+ "%s Failed to load tool config %s: %s",
761
+ component.log_identifier,
762
+ tool_config,
763
+ e,
764
+ )
765
+ raise e
766
+
767
+ # Load internal framework tools
768
+ (
769
+ internal_tools,
770
+ internal_builtins,
771
+ internal_cleanups,
772
+ ) = _load_internal_tools(component, loaded_tool_names)
773
+ loaded_tools.extend(internal_tools)
774
+ enabled_builtin_tools.extend(internal_builtins)
775
+ cleanup_hooks.extend(internal_cleanups)
776
+
702
777
  log.info(
703
778
  "%s Finished loading tools. Total tools for ADK: %d. Total SAM built-ins for prompt: %d. Total cleanup hooks: %d. Peer tools added dynamically.",
704
779
  component.log_identifier,