|
9 | 9 | 2. Method-style for direct tool access: `agent.tool.tool_name(param1="value")` |
10 | 10 | """ |
11 | 11 |
|
12 | | -import asyncio |
13 | 12 | import json |
14 | 13 | import logging |
15 | 14 | import random |
16 | | -from concurrent.futures import ThreadPoolExecutor |
17 | 15 | from typing import ( |
18 | 16 | Any, |
19 | 17 | AsyncGenerator, |
|
31 | 29 | from pydantic import BaseModel |
32 | 30 |
|
33 | 31 | from .. import _identifier |
| 32 | +from .._async import run_async |
34 | 33 | from ..event_loop.event_loop import event_loop_cycle |
| 34 | +from ..experimental.tools import ToolProvider |
35 | 35 | from ..handlers.callback_handler import PrintingCallbackHandler, null_callback_handler |
36 | 36 | from ..hooks import ( |
37 | 37 | AfterInvocationEvent, |
@@ -160,12 +160,7 @@ async def acall() -> ToolResult: |
160 | 160 |
|
161 | 161 | return tool_results[0] |
162 | 162 |
|
163 | | - def tcall() -> ToolResult: |
164 | | - return asyncio.run(acall()) |
165 | | - |
166 | | - with ThreadPoolExecutor() as executor: |
167 | | - future = executor.submit(tcall) |
168 | | - tool_result = future.result() |
| 163 | + tool_result = run_async(acall) |
169 | 164 |
|
170 | 165 | if record_direct_tool_call is not None: |
171 | 166 | should_record_direct_tool_call = record_direct_tool_call |
@@ -208,7 +203,7 @@ def __init__( |
208 | 203 | self, |
209 | 204 | model: Union[Model, str, None] = None, |
210 | 205 | messages: Optional[Messages] = None, |
211 | | - tools: Optional[list[Union[str, dict[str, str], Any]]] = None, |
| 206 | + tools: Optional[list[Union[str, dict[str, str], ToolProvider, Any]]] = None, |
212 | 207 | system_prompt: Optional[str] = None, |
213 | 208 | callback_handler: Optional[ |
214 | 209 | Union[Callable[..., Any], _DefaultCallbackHandlerSentinel] |
@@ -240,7 +235,8 @@ def __init__( |
240 | 235 | - File paths (e.g., "/path/to/tool.py") |
241 | 236 | - Imported Python modules (e.g., from strands_tools import current_time) |
242 | 237 | - Dictionaries with name/path keys (e.g., {"name": "tool_name", "path": "/path/to/tool.py"}) |
243 | | - - Functions decorated with `@strands.tool` decorator. |
| 238 | + - Functions decorated with `@strands.tool` decorator |
| 239 | + - ToolProvider instances for managed tool collections |
244 | 240 |
|
245 | 241 | If provided, only these tools will be available. If None, all tools will be available. |
246 | 242 | system_prompt: System prompt to guide model behavior. |
@@ -333,6 +329,9 @@ def __init__( |
333 | 329 | else: |
334 | 330 | self.state = AgentState() |
335 | 331 |
|
| 332 | + # Track cleanup state |
| 333 | + self._cleanup_called = False |
| 334 | + |
336 | 335 | self.tool_caller = Agent.ToolCaller(self) |
337 | 336 |
|
338 | 337 | self.hooks = HookRegistry() |
@@ -399,13 +398,7 @@ def __call__(self, prompt: AgentInput = None, **kwargs: Any) -> AgentResult: |
399 | 398 | - metrics: Performance metrics from the event loop |
400 | 399 | - state: The final state of the event loop |
401 | 400 | """ |
402 | | - |
403 | | - def execute() -> AgentResult: |
404 | | - return asyncio.run(self.invoke_async(prompt, **kwargs)) |
405 | | - |
406 | | - with ThreadPoolExecutor() as executor: |
407 | | - future = executor.submit(execute) |
408 | | - return future.result() |
| 401 | + return run_async(lambda: self.invoke_async(prompt, **kwargs)) |
409 | 402 |
|
410 | 403 | async def invoke_async(self, prompt: AgentInput = None, **kwargs: Any) -> AgentResult: |
411 | 404 | """Process a natural language prompt through the agent's event loop. |
@@ -459,13 +452,7 @@ def structured_output(self, output_model: Type[T], prompt: AgentInput = None) -> |
459 | 452 | Raises: |
460 | 453 | ValueError: If no conversation history or prompt is provided. |
461 | 454 | """ |
462 | | - |
463 | | - def execute() -> T: |
464 | | - return asyncio.run(self.structured_output_async(output_model, prompt)) |
465 | | - |
466 | | - with ThreadPoolExecutor() as executor: |
467 | | - future = executor.submit(execute) |
468 | | - return future.result() |
| 455 | + return run_async(lambda: self.structured_output_async(output_model, prompt)) |
469 | 456 |
|
470 | 457 | async def structured_output_async(self, output_model: Type[T], prompt: AgentInput = None) -> T: |
471 | 458 | """This method allows you to get structured output from the agent. |
@@ -524,6 +511,69 @@ async def structured_output_async(self, output_model: Type[T], prompt: AgentInpu |
524 | 511 | finally: |
525 | 512 | self.hooks.invoke_callbacks(AfterInvocationEvent(agent=self)) |
526 | 513 |
|
| 514 | + def cleanup(self) -> None: |
| 515 | + """Clean up resources used by the agent. |
| 516 | +
|
| 517 | + This method cleans up all tool providers that require explicit cleanup, |
| 518 | + such as MCP clients. It should be called when the agent is no longer needed |
| 519 | + to ensure proper resource cleanup. |
| 520 | +
|
| 521 | + Note: This method uses a "belt and braces" approach with automatic cleanup |
| 522 | + through __del__ as a fallback, but explicit cleanup is recommended. |
| 523 | + """ |
| 524 | + run_async(self.cleanup_async) |
| 525 | + |
| 526 | + async def cleanup_async(self) -> None: |
| 527 | + """Asynchronously clean up resources used by the agent. |
| 528 | +
|
| 529 | + This method cleans up all tool providers that require explicit cleanup, |
| 530 | + such as MCP clients. It should be called when the agent is no longer needed |
| 531 | + to ensure proper resource cleanup. |
| 532 | +
|
| 533 | + Note: This method uses a "belt and braces" approach with automatic cleanup |
| 534 | + through __del__ as a fallback, but explicit cleanup is recommended. |
| 535 | + """ |
| 536 | + if self._cleanup_called: |
| 537 | + return |
| 538 | + |
| 539 | + logger.debug("agent_id=<%s> | cleaning up agent resources", self.agent_id) |
| 540 | + |
| 541 | + for provider in self.tool_registry.tool_providers: |
| 542 | + try: |
| 543 | + await provider.cleanup() |
| 544 | + logger.debug( |
| 545 | + "agent_id=<%s>, provider=<%s> | cleaned up tool provider", self.agent_id, type(provider).__name__ |
| 546 | + ) |
| 547 | + except Exception as e: |
| 548 | + logger.warning( |
| 549 | + "agent_id=<%s>, provider=<%s>, error=<%s> | failed to cleanup tool provider", |
| 550 | + self.agent_id, |
| 551 | + type(provider).__name__, |
| 552 | + e, |
| 553 | + ) |
| 554 | + |
| 555 | + self._cleanup_called = True |
| 556 | + logger.debug("agent_id=<%s> | agent cleanup complete", self.agent_id) |
| 557 | + |
| 558 | + def __del__(self) -> None: |
| 559 | + """Automatic cleanup when agent is garbage collected. |
| 560 | +
|
| 561 | + This serves as a fallback cleanup mechanism, but explicit cleanup() is preferred. |
| 562 | + """ |
| 563 | + try: |
| 564 | + if self._cleanup_called or not self.tool_registry.tool_providers: |
| 565 | + return |
| 566 | + |
| 567 | + logger.warning( |
| 568 | + "agent_id=<%s> | Agent cleanup called via __del__. " |
| 569 | + "Consider calling agent.cleanup() explicitly for better resource management.", |
| 570 | + self.agent_id, |
| 571 | + ) |
| 572 | + self.cleanup() |
| 573 | + except Exception as e: |
| 574 | + # Log exceptions during garbage collection cleanup for debugging |
| 575 | + logger.debug("agent_id=<%s>, error=<%s> | exception during __del__ cleanup", self.agent_id, e) |
| 576 | + |
527 | 577 | async def stream_async( |
528 | 578 | self, |
529 | 579 | prompt: AgentInput = None, |
|
0 commit comments