Skip to content

tool executors #658

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open

tool executors #658

wants to merge 13 commits into from

Conversation

pgrayy
Copy link
Member

@pgrayy pgrayy commented Aug 11, 2025

Description

  • Allow users to decide whether tools should run concurrently or sequentially.
    • Currently, we execute all tools concurrently.
    • Sequential execution can however be useful (e.g., user wants agent to first use browser tool to navigate to a website and then snapshot tool to take a picture).

For more details, please see comments in file diff.

Usage

Sequential

from strands import Agent
from strands.tools.executors import SequentialToolExecutor

agent = Agent(tool_executor=SequentialToolExecutor(), tools=[screenshot_tool, email_tool])
agent("Please take a screenshot and then email the screenshot to my friend")

Assuming the model returns screenshot_tool and email_tool use requests, the SequentialToolExecutor will execute both sequentially in the order given.

Concurrent

from strands import Agent
from strands.tools.executors import ConcurrentToolExecutor

agent = Agent(tool_executor=ConcurrentToolExecutor(), tools=[weather_tool, time_tool])
agent("What is the weather and time in New York?")

Assuming the model returns weather_tool and time_tool use requests, the ConcurrentToolExecutor will execute both concurrently. Note, this is the default executor and so Agent(tools=[weather_tool, time_tool]) will result in the same behavior.

Related Issues

#614

Documentation PR

Will follow up

Type of Change

New feature

Testing

How have you tested the change? Verify that the changes do not break functionality or introduce warnings in consuming repositories: agents-docs, agents-tools, agents-cli

  • I ran hatch run prepare: Wrote new unit and integration tests

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Follow Up

Will document the following items in an issue tracker after PR approval:

  1. Add tool executors page to our docs.
  2. Simplify/reorganize the before and after tool execution logic.
    • Currently, the base Executor class implements _stream and _stream_with_trace methods to wrap the tool execution with additional logic that includes tracing, hooks, exception handling, etc. It is a bit cumbersome and would maybe make more sense to be placed in the AgentTool implementations.
  3. Finalize the execute method interface.
    • We should be able to remove tool_results. execute could instead work as a generator that yields results upon completion that the caller (inside event_loop.py) collects. For this to work however, we require strongly typed events.
    • See if we can remove invocation_state as this parameter is on a deprecation path.
  4. Expose Executor as a public interface after clean up.
  5. Accept custom thread pools for sync tool execution.

@@ -306,122 +304,6 @@ async def recurse_event_loop(agent: "Agent", invocation_state: dict[str, Any]) -
recursive_trace.end()


async def run_tool(agent: "Agent", tool_use: ToolUse, invocation_state: dict[str, Any]) -> ToolGenerator:
Copy link
Member Author

@pgrayy pgrayy Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved logic into strands.executors._executor.Executor._stream

@pgrayy pgrayy marked this pull request as ready for review August 11, 2025 22:24

async def acall() -> ToolResult:
# Pass kwargs as invocation_state
async for event in run_tool(self._agent, tool_use, kwargs):
Copy link
Member Author

@pgrayy pgrayy Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved run_tool logic into new ToolExecutor._stream method.

return wrapper

@_trace
async def stream(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic copied from the now removed run_tool method in event_loop.py.


validate_and_prepare_tools(message, tool_uses, tool_results, invalid_tool_use_ids)
tool_uses = [tool_use for tool_use in tool_uses if tool_use.get("toolUseId") not in invalid_tool_use_ids]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional nit: any reason this needs to be modified in place? I know we do it in the code base but I find it harder to follow

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could follow up on this (and a lot of code in this PR). For now though, I figured I would just try to copy and paste as much as possible rather than fully alter the existing logic to simplify the PR. In other words, this PR more so focuses on code organization and placement rather than the logic.

@pgrayy pgrayy marked this pull request as draft August 19, 2025 18:53
@@ -35,6 +35,8 @@
from ..session.session_manager import SessionManager
from ..telemetry.metrics import EventLoopMetrics
from ..telemetry.tracer import get_tracer, serialize
from ..tools.executors import ConcurrentToolExecutor
from ..tools.executors._executor import Executor as ToolExecutor
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per discussion, indicating that the base Executor class is private rather than experimental to discourage any use.


async def acall() -> ToolResult:
# Pass kwargs as invocation_state
async for event in run_tool(self._agent, tool_use, kwargs):
async for event in ToolExecutor._stream(self._agent, tool_use, tool_results, invocation_state):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per discussion, member methods of the executor classes are also marked private to discourage customer use. We want to first cement the interface before exposing.

"""Abstract base class for tool executors."""

@staticmethod
async def _stream(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic is copy and pasted from run_tool in event_loop.py.

tool_results.append(after_event.result)

@staticmethod
async def _stream_with_trace(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously I tried setting this up as a decorator. For more clarity, I went ahead and made it a separate method. Note, this logic is copied from tools/executor.py which is removed in this PR.

@pgrayy pgrayy marked this pull request as ready for review August 20, 2025 14:58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you update the PR with:

  • The new public APIs being exposed
  • Expected way that the customer would use the new apis?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add a section about the expected follow-ups that are decided on - if this is a stepping stone to the long-term solution

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added "Usage" and "Follow Up" sections to the overview.

@pgrayy pgrayy deployed to auto-approve August 22, 2025 21:50 — with GitHub Actions Active
@pgrayy pgrayy requested a review from zastrowm August 22, 2025 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants