( step: ToolCallStepContext, response: LLMChatResponse, )
| 121 | } |
| 122 | |
| 123 | export async function runToolCallBatch( |
| 124 | step: ToolCallStepContext, |
| 125 | response: LLMChatResponse, |
| 126 | ): Promise<ToolCallBatchResult> { |
| 127 | if (response.toolCalls.length === 0) return { stopTurn: false }; |
| 128 | const batchStep: ToolCallBatchContext = { ...step, toolCalls: response.toolCalls }; |
| 129 | const calls = response.toolCalls.map((toolCall) => preflightToolCall(step, toolCall)); |
| 130 | const scheduler = new ToolScheduler<PendingToolResult>(); |
| 131 | const pendingResults: Array<Promise<PendingToolResult>> = []; |
| 132 | let stopTurn = false; |
| 133 | |
| 134 | try { |
| 135 | for (let index = 0; index < calls.length; index += 1) { |
| 136 | const call = calls[index]!; |
| 137 | const prepared = await prepareToolCall(batchStep, call); |
| 138 | pendingResults.push(scheduler.add(prepared.task)); |
| 139 | |
| 140 | if (prepared.stopBatchAfterThis === true) { |
| 141 | stopTurn = true; |
| 142 | for (const skippedCall of calls.slice(index + 1)) { |
| 143 | const skippedTask = await prepareSkippedToolCall(batchStep, skippedCall); |
| 144 | pendingResults.push(scheduler.add(skippedTask)); |
| 145 | } |
| 146 | break; |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | // Tool tasks may finish out of order; terminal results are still emitted in |
| 151 | // provider order. Await all tasks so each recorded `tool.call` gets a |
| 152 | // paired `tool.result`; the caller checks abort before writing `step.end`. |
| 153 | for (const pendingResult of pendingResults) { |
| 154 | const result = await finalizePendingToolResult(batchStep, await pendingResult); |
| 155 | if (result.stopTurn === true) stopTurn = true; |
| 156 | await step.dispatchEvent({ |
| 157 | type: 'tool.result', |
| 158 | parentUuid: result.toolCall.id, |
| 159 | toolCallId: result.toolCall.id, |
| 160 | result: result.result, |
| 161 | }); |
| 162 | } |
| 163 | } finally { |
| 164 | // Preparation or result dispatch can throw after execution has started. |
| 165 | // Always settle spawned tasks before the caller continues so rejected |
| 166 | // execute promises cannot surface as detached unhandled rejections. |
| 167 | await Promise.allSettled(pendingResults); |
| 168 | } |
| 169 | return { stopTurn }; |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Provider-order validation pass. It does not run hooks, spawn tools, or write |
no test coverage detected