992. Method-style for direct tool access: `agent.tool.tool_name(param1="value")`
1010"""
1111
12- import asyncio
1312import json
1413import logging
1514import random
1615import warnings
17- from concurrent .futures import ThreadPoolExecutor
1816from typing import (
17+ TYPE_CHECKING ,
1918 Any ,
2019 AsyncGenerator ,
2120 AsyncIterator ,
3231from pydantic import BaseModel
3332
3433from .. import _identifier
34+ from .._async import run_async
3535from ..event_loop .event_loop import event_loop_cycle
36+
37+ if TYPE_CHECKING :
38+ from ..experimental .tools import ToolProvider
3639from ..handlers .callback_handler import PrintingCallbackHandler , null_callback_handler
3740from ..hooks import (
3841 AfterInvocationEvent ,
@@ -167,12 +170,7 @@ async def acall() -> ToolResult:
167170
168171 return tool_results [0 ]
169172
170- def tcall () -> ToolResult :
171- return asyncio .run (acall ())
172-
173- with ThreadPoolExecutor () as executor :
174- future = executor .submit (tcall )
175- tool_result = future .result ()
173+ tool_result = run_async (acall )
176174
177175 if record_direct_tool_call is not None :
178176 should_record_direct_tool_call = record_direct_tool_call
@@ -215,7 +213,7 @@ def __init__(
215213 self ,
216214 model : Union [Model , str , None ] = None ,
217215 messages : Optional [Messages ] = None ,
218- tools : Optional [list [Union [str , dict [str , str ], Any ]]] = None ,
216+ tools : Optional [list [Union [str , dict [str , str ], "ToolProvider" , Any ]]] = None ,
219217 system_prompt : Optional [str ] = None ,
220218 structured_output_model : Optional [Type [BaseModel ]] = None ,
221219 callback_handler : Optional [
@@ -248,6 +246,7 @@ def __init__(
248246 - File paths (e.g., "/path/to/tool.py")
249247 - Imported Python modules (e.g., from strands_tools import current_time)
250248 - Dictionaries with name/path keys (e.g., {"name": "tool_name", "path": "/path/to/tool.py"})
249+ - ToolProvider instances for managed tool collections
251250 - Functions decorated with `@strands.tool` decorator.
252251
253252 If provided, only these tools will be available. If None, all tools will be available.
@@ -423,17 +422,11 @@ def __call__(
423422 - state: The final state of the event loop
424423 - structured_output: Parsed structured output when structured_output_model was specified
425424 """
426-
427- def execute () -> AgentResult :
428- return asyncio .run (
429- self .invoke_async (
430- prompt , invocation_state = invocation_state , structured_output_model = structured_output_model , ** kwargs
431- )
425+ return run_async (
426+ lambda : self .invoke_async (
427+ prompt , invocation_state = invocation_state , structured_output_model = structured_output_model , ** kwargs
432428 )
433-
434- with ThreadPoolExecutor () as executor :
435- future = executor .submit (execute )
436- return future .result ()
429+ )
437430
438431 async def invoke_async (
439432 self ,
@@ -506,12 +499,7 @@ def structured_output(self, output_model: Type[T], prompt: AgentInput = None) ->
506499 stacklevel = 2 ,
507500 )
508501
509- def execute () -> T :
510- return asyncio .run (self .structured_output_async (output_model , prompt ))
511-
512- with ThreadPoolExecutor () as executor :
513- future = executor .submit (execute )
514- return future .result ()
502+ return run_async (lambda : self .structured_output_async (output_model , prompt ))
515503
516504 async def structured_output_async (self , output_model : Type [T ], prompt : AgentInput = None ) -> T :
517505 """This method allows you to get structured output from the agent.
@@ -529,6 +517,7 @@ async def structured_output_async(self, output_model: Type[T], prompt: AgentInpu
529517
530518 Raises:
531519 ValueError: If no conversation history or prompt is provided.
520+ -
532521 """
533522 if self ._interrupt_state .activated :
534523 raise RuntimeError ("cannot call structured output during interrupt" )
@@ -583,6 +572,25 @@ async def structured_output_async(self, output_model: Type[T], prompt: AgentInpu
583572 finally :
584573 self .hooks .invoke_callbacks (AfterInvocationEvent (agent = self ))
585574
575+ def cleanup (self ) -> None :
576+ """Clean up resources used by the agent.
577+
578+ This method cleans up all tool providers that require explicit cleanup,
579+ such as MCP clients. It should be called when the agent is no longer needed
580+ to ensure proper resource cleanup.
581+
582+ Note: This method uses a "belt and braces" approach with automatic cleanup
583+ through finalizers as a fallback, but explicit cleanup is recommended.
584+ """
585+ self .tool_registry .cleanup ()
586+
587+ def __del__ (self ) -> None :
588+ """Clean up resources when agent is garbage collected."""
589+ # __del__ is called even when an exception is thrown in the constructor,
590+ # so there is no guarantee tool_registry was set..
591+ if hasattr (self , "tool_registry" ):
592+ self .tool_registry .cleanup ()
593+
586594 async def stream_async (
587595 self ,
588596 prompt : AgentInput = None ,
0 commit comments