Replace sequential for+await tool dispatch with Promise.allSettled for parallel execution. All tool_execution_start events emit immediately, tools run concurrently, results are processed in original order. Also fix run-log toolStartTimes to key by toolCallId instead of toolName to prevent collisions with parallel same-name tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
80 lines
3.3 KiB
Diff
80 lines
3.3 KiB
Diff
diff --git a/dist/agent-loop.js b/dist/agent-loop.js
|
|
index b26d45753809da14df6409354e8c537684b9acd7..931399cc12bafea940fe99ab74ae288d71506e52 100644
|
|
--- a/dist/agent-loop.js
|
|
+++ b/dist/agent-loop.js
|
|
@@ -206,17 +206,18 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
|
|
*/
|
|
async function executeToolCalls(tools, assistantMessage, signal, stream, getSteeringMessages) {
|
|
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
|
|
- const results = [];
|
|
- let steeringMessages;
|
|
- for (let index = 0; index < toolCalls.length; index++) {
|
|
- const toolCall = toolCalls[index];
|
|
- const tool = tools?.find((t) => t.name === toolCall.name);
|
|
+ // Emit all tool_execution_start events immediately
|
|
+ for (const toolCall of toolCalls) {
|
|
stream.push({
|
|
type: "tool_execution_start",
|
|
toolCallId: toolCall.id,
|
|
toolName: toolCall.name,
|
|
args: toolCall.arguments,
|
|
});
|
|
+ }
|
|
+ // Execute all tools in parallel
|
|
+ const settled = await Promise.allSettled(toolCalls.map(async (toolCall) => {
|
|
+ const tool = tools?.find((t) => t.name === toolCall.name);
|
|
let result;
|
|
let isError = false;
|
|
try {
|
|
@@ -240,6 +241,27 @@ async function executeToolCalls(tools, assistantMessage, signal, stream, getStee
|
|
};
|
|
isError = true;
|
|
}
|
|
+ return { result, isError };
|
|
+ }));
|
|
+ // Process results IN ORIGINAL ORDER (critical for LLM context)
|
|
+ const results = [];
|
|
+ let steeringMessages;
|
|
+ for (let i = 0; i < settled.length; i++) {
|
|
+ const entry = settled[i];
|
|
+ const toolCall = toolCalls[i];
|
|
+ let result;
|
|
+ let isError;
|
|
+ if (entry.status === "fulfilled") {
|
|
+ result = entry.value.result;
|
|
+ isError = entry.value.isError;
|
|
+ }
|
|
+ else {
|
|
+ result = {
|
|
+ content: [{ type: "text", text: entry.reason instanceof Error ? entry.reason.message : String(entry.reason) }],
|
|
+ details: {},
|
|
+ };
|
|
+ isError = true;
|
|
+ }
|
|
stream.push({
|
|
type: "tool_execution_end",
|
|
toolCallId: toolCall.id,
|
|
@@ -259,17 +281,12 @@ async function executeToolCalls(tools, assistantMessage, signal, stream, getStee
|
|
results.push(toolResultMessage);
|
|
stream.push({ type: "message_start", message: toolResultMessage });
|
|
stream.push({ type: "message_end", message: toolResultMessage });
|
|
- // Check for steering messages - skip remaining tools if user interrupted
|
|
- if (getSteeringMessages) {
|
|
- const steering = await getSteeringMessages();
|
|
- if (steering.length > 0) {
|
|
- steeringMessages = steering;
|
|
- const remainingCalls = toolCalls.slice(index + 1);
|
|
- for (const skipped of remainingCalls) {
|
|
- results.push(skipToolCall(skipped, stream));
|
|
- }
|
|
- break;
|
|
- }
|
|
+ }
|
|
+ // Check steering messages once after all tools complete
|
|
+ if (getSteeringMessages) {
|
|
+ const steering = await getSteeringMessages();
|
|
+ if (steering.length > 0) {
|
|
+ steeringMessages = steering;
|
|
}
|
|
}
|
|
return { toolResults: results, steeringMessages };
|