@@ -49,9 +49,9 @@ protected function callTools(array $tools, array $toolCalls): array
4949 */
5050 protected function callToolsAndYieldEvents (array $ tools , array $ toolCalls , string $ messageId , array &$ toolResults ): Generator
5151 {
52- $ groupedToolCalls = $ this ->groupToolCallsByConcurrency ($ tools , $ toolCalls );
52+ $ resolvedCalls = $ this ->resolveToolCalls ($ tools , $ toolCalls );
5353
54- $ executionResults = $ this ->executeToolsWithConcurrency ( $ tools , $ groupedToolCalls , $ messageId );
54+ $ executionResults = $ this ->executeToolCalls ( $ resolvedCalls , $ messageId );
5555
5656 foreach (array_keys ($ toolCalls ) as $ index ) {
5757 $ result = $ executionResults [$ index ];
@@ -65,26 +65,36 @@ protected function callToolsAndYieldEvents(array $tools, array $toolCalls, strin
6565 }
6666
6767 /**
68+ * Resolve tool calls to Tool+ToolCall pairs, grouping by concurrency.
69+ * Each tool is resolved exactly once.
70+ *
71+ * Unresolvable tools (not found, duplicates) are stored with the caught
72+ * exception so executeResolvedToolCall can produce a proper error result.
73+ *
6874 * @param Tool[] $tools
6975 * @param ToolCall[] $toolCalls
70- * @return array{concurrent: array<int, ToolCall>, sequential: array<int, ToolCall>}
76+ * @return array{concurrent: array<int, array{tool: Tool, toolCall: ToolCall} >, sequential: array<int, array{tool: ?Tool, toolCall: ToolCall, error?: PrismException} >}
7177 */
72- protected function groupToolCallsByConcurrency (array $ tools , array $ toolCalls ): array
78+ protected function resolveToolCalls (array $ tools , array $ toolCalls ): array
7379 {
7480 $ concurrent = [];
7581 $ sequential = [];
7682
7783 foreach ($ toolCalls as $ index => $ toolCall ) {
7884 try {
7985 $ tool = $ this ->resolveTool ($ toolCall ->name , $ tools );
86+ } catch (PrismException $ e ) {
87+ $ sequential [$ index ] = ['tool ' => null , 'toolCall ' => $ toolCall , 'error ' => $ e ];
88+
89+ continue ;
90+ }
91+
92+ $ pair = ['tool ' => $ tool , 'toolCall ' => $ toolCall ];
8093
81- if ($ tool ->isConcurrent ()) {
82- $ concurrent [$ index ] = $ toolCall ;
83- } else {
84- $ sequential [$ index ] = $ toolCall ;
85- }
86- } catch (PrismException ) {
87- $ sequential [$ index ] = $ toolCall ;
94+ if ($ tool ->isConcurrent ()) {
95+ $ concurrent [$ index ] = $ pair ;
96+ } else {
97+ $ sequential [$ index ] = $ pair ;
8898 }
8999 }
90100
@@ -95,43 +105,53 @@ protected function groupToolCallsByConcurrency(array $tools, array $toolCalls):
95105 }
96106
97107 /**
98- * @param Tool[] $tools
99- * @param array{concurrent: array<int, ToolCall>, sequential: array<int, ToolCall>} $groupedToolCalls
108+ * @param array{concurrent: array<int, array{tool: Tool, toolCall: ToolCall}>, sequential: array<int, array{tool: ?Tool, toolCall: ToolCall, error?: PrismException}>} $resolvedCalls
100109 * @return array<int, array{toolResult: ToolResult, events: array<int, ToolResultEvent|ArtifactEvent>}>
101110 */
102- protected function executeToolsWithConcurrency (array $ tools , array $ groupedToolCalls , string $ messageId ): array
111+ protected function executeToolCalls (array $ resolvedCalls , string $ messageId ): array
103112 {
104113 $ results = [];
105114
106- $ concurrentClosures = [];
107-
108- foreach ($ groupedToolCalls ['concurrent ' ] as $ index => $ toolCall ) {
109- $ concurrentClosures [$ index ] = fn () => $ this -> executeToolCall ( $ tools , $ toolCall , $ messageId );
110- }
115+ if ( $ resolvedCalls [ ' concurrent ' ] !== []) {
116+ $ closures = [];
117+ foreach ($ resolvedCalls ['concurrent ' ] as $ index => $ pair ) {
118+ $ closures [$ index ] = static fn (): array => self :: executeResolvedToolCall ( $ pair [ ' tool ' ] , $ pair [ ' toolCall ' ] , $ messageId );
119+ }
111120
112- if ($ concurrentClosures !== []) {
113- foreach (Concurrency::run ($ concurrentClosures ) as $ index => $ result ) {
121+ foreach (Concurrency::run ($ closures ) as $ index => $ result ) {
114122 $ results [$ index ] = $ result ;
115123 }
116124 }
117125
118- foreach ($ groupedToolCalls ['sequential ' ] as $ index => $ toolCall ) {
119- $ results [$ index ] = $ this -> executeToolCall ( $ tools , $ toolCall , $ messageId );
126+ foreach ($ resolvedCalls ['sequential ' ] as $ index => $ pair ) {
127+ $ results [$ index ] = self :: executeResolvedToolCall ( $ pair [ ' tool ' ] , $ pair [ ' toolCall ' ] , $ messageId, $ pair [ ' error ' ] ?? null );
120128 }
121129
122130 return $ results ;
123131 }
124132
125133 /**
126- * @param Tool[] $tools
134+ * Execute a tool call without capturing $this — safe for serialization
135+ * by Laravel's ProcessDriver which uses SerializableClosure.
136+ *
137+ * When $error is provided (tool resolution failed), skips execution and
138+ * returns a failed result directly.
139+ *
127140 * @return array{toolResult: ToolResult, events: array<int, ToolResultEvent|ArtifactEvent>}
128141 */
129- protected function executeToolCall ( array $ tools , ToolCall $ toolCall , string $ messageId ): array
142+ protected static function executeResolvedToolCall (? Tool $ tool , ToolCall $ toolCall , string $ messageId, ? PrismException $ error = null ): array
130143 {
131144 $ events = [];
132145
133146 try {
134- $ tool = $ this ->resolveTool ($ toolCall ->name , $ tools );
147+ if ($ error instanceof PrismException) {
148+ throw $ error ;
149+ }
150+
151+ if (! $ tool instanceof Tool) {
152+ throw new PrismException ("Tool [ {$ toolCall ->name }] could not be resolved " );
153+ }
154+
135155 $ output = call_user_func_array (
136156 $ tool ->handle (...),
137157 $ toolCall ->arguments ()
@@ -224,6 +244,8 @@ protected function executeToolCall(array $tools, ToolCall $toolCall, string $mes
224244
225245 /**
226246 * @param Tool[] $tools
247+ *
248+ * @throws PrismException
227249 */
228250 protected function resolveTool (string $ name , array $ tools ): Tool
229251 {
0 commit comments