From 8a93e6437a096eedbe518911f2c3e576dfb3a006 Mon Sep 17 00:00:00 2001 From: joris Date: Thu, 23 Oct 2025 12:35:22 +0200 Subject: [PATCH] Feat: Implement ToolExecutor abstraction to allow you to swap out the way tools are being executed --- src/Interfaces/ToolExecutorInterface.php | 32 +++++++++ src/Tool/ToolExecutor.php | 87 ++++++++++++++++++++++++ src/Traits/HasToolCallingTrait.php | 46 ++++++++++--- 3 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 src/Interfaces/ToolExecutorInterface.php create mode 100644 src/Tool/ToolExecutor.php diff --git a/src/Interfaces/ToolExecutorInterface.php b/src/Interfaces/ToolExecutorInterface.php new file mode 100644 index 0000000..1eacb53 --- /dev/null +++ b/src/Interfaces/ToolExecutorInterface.php @@ -0,0 +1,32 @@ + $tools The tools to execute + * @param Agent $agent The Agent calling the tools + */ + public function executeTools(array $tools, Agent $agent): void; + + /** + * Execute a single tool. + * + * Handles the lifecycle of a tool execution including: + * - Notifying observers before execution + * - Adding the tool call to the context + * - Invoking the tool + * + * @param Tool $tool The tool instance to execute + * @param ToolCall $toolCall The original tool call request + * @param Agent $agent The Agent calling the tool + */ + public function executeTool(Tool $tool, ToolCall $toolCall, Agent $agent): void; +} diff --git a/src/Tool/ToolExecutor.php b/src/Tool/ToolExecutor.php new file mode 100644 index 0000000..2015188 --- /dev/null +++ b/src/Tool/ToolExecutor.php @@ -0,0 +1,87 @@ + $tools The tools to execute + * @param Agent $agent The Agent calling the tools + */ + public function executeTools(array $tools, Agent $agent): void + { + foreach ($tools as [$tool, $toolCall]) { + $this->executeTool($tool, $toolCall, $agent); + } + } + + /** + * Execute a single tool. + * + * Handles the lifecycle of a tool execution including: + * - Notifying observers before execution + * - Adding the tool call to the context + * - Invoking the tool + * + * @param Tool $tool The tool instance to execute + * @param ToolCall $toolCall The original tool call request + * @param Agent $agent The Agent calling the tool + */ + public function executeTool(Tool $tool, ToolCall $toolCall, Agent $agent): void + { + $context = $agent->orchestrator()->context; + + // Notify observers of the tool call + $context->observerInvoker()->agentOnToolCall($context, $agent, $tool, $toolCall); + $context->observerInvoker()->toolOnToolCall($context, $tool, $toolCall); + + // Add the tool call to the conversation context + $context->addMessage($toolCall); + + // Execute the tool and handle its result + $this->invokeTool($tool, $toolCall, $context, $agent); + } + + /** + * Invoke a tool and handle its result. + * + * Executes the tool, notifies observers of success or failure, + * and adds the tool output to the conversation context. + * + * @param Tool $tool The tool to invoke + * @param ToolCall $toolCall The original tool call + * @param RunContext $context The current run context + * @param Agent $agent The Agent calling the tool + */ + protected function invokeTool(Tool $tool, ToolCall $toolCall, RunContext $context, Agent $agent): void + { + $success = false; + + try { + // Execute the tool and get its result + $result = $tool(); + $context->observerInvoker()->toolOnSuccess($context, $tool, $toolCall, $result); + $success = true; + } catch (HandleToolException $e) { + // Handle tool execution errors + $result = $e->toPayload(); + $context->observerInvoker()->toolOnFailure($context, $tool, $toolCall, $result); + } + + // Notify observers after tool execution + $context->observerInvoker()->agentAfterToolCall($context, $agent, $tool, $toolCall, $result, $success); + + // Create and add tool output message to the context + $toolOutput = new ToolOutput((string)$result, $toolCall->id); + $context->addMessage($toolOutput); + } +} diff --git a/src/Traits/HasToolCallingTrait.php b/src/Traits/HasToolCallingTrait.php index 985778d..c4112ed 100644 --- a/src/Traits/HasToolCallingTrait.php +++ b/src/Traits/HasToolCallingTrait.php @@ -2,15 +2,18 @@ namespace Swis\Agents\Traits; +use Swis\Agents\Agent; use Swis\Agents\DynamicTool; use Swis\Agents\Exceptions\BuildToolException; use Swis\Agents\Exceptions\HandleToolException; use Swis\Agents\Handoff; use Swis\Agents\Helpers\ArrayHelper; use Swis\Agents\Helpers\ToolHelper; +use Swis\Agents\Interfaces\ToolExecutorInterface; use Swis\Agents\Orchestrator\RunContext; use Swis\Agents\Response\ToolCall; use Swis\Agents\Tool; +use Swis\Agents\Tool\ToolExecutor; use Swis\Agents\Tool\ToolOutput; use Throwable; @@ -26,6 +29,8 @@ */ trait HasToolCallingTrait { + protected ToolExecutorInterface $toolExecutor; + /** * Execute a list of tool calls. * @@ -36,24 +41,42 @@ trait HasToolCallingTrait */ public function executeTools(array $toolCalls): void { - foreach ($toolCalls as $toolCall) { - $tool = $this->buildTool($toolCall); + $tools = array_map(fn (ToolCall $toolCall) => [$this->buildTool($toolCall), $toolCall,], array_values($toolCalls)); - // Special handling for agent handoffs - if ($tool instanceof Handoff) { - $this->executeHandoff($tool, $toolCall); + // Special handling for agent handoffs + $handoffs = array_filter($tools, fn (array $tool) => $tool[0] instanceof Handoff); - // No further processing after handoff - return; - } + if (! empty($handoffs)) { + [$tool, $toolCall] = $handoffs[0]; - $this->executeTool($tool, $toolCall); + // No further processing after handoff + $this->executeHandoff($tool, $toolCall); + + return; } + $executor = $this->toolExecutor ?? $this->defaultToolExecutor(); + $executor->executeTools($tools, $this); + // After all tools are executed, give control back to the agent $this->invoke(); } + /** + * Use a different Tool Executor. + * + * This allows you to execute tools in parallel. + * + * @param ToolExecutorInterface $toolExecutor + * @return Agent + */ + public function withToolExecutor(ToolExecutorInterface $toolExecutor): self + { + $this->toolExecutor = $toolExecutor; + + return $this; + } + /** * Execute a single tool. * @@ -260,4 +283,9 @@ protected function invokeTool(Tool $tool, ToolCall $toolCall, RunContext $contex $toolOutput = new ToolOutput((string) $result, $toolCall->id); $context->addMessage($toolOutput); } + + protected function defaultToolExecutor(): ToolExecutorInterface + { + return new ToolExecutor(); + } }