feat(agent): enable parallel tool execution via pi-agent-core patch

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>
This commit is contained in:
Jiayuan Zhang 2026-02-15 18:47:53 +08:00
parent c012bff246
commit a254daff01
4 changed files with 110 additions and 20 deletions

View file

@ -44,6 +44,9 @@
"overrides": {
"@types/react": "catalog:",
"@types/react-dom": "catalog:"
},
"patchedDependencies": {
"@mariozechner/pi-agent-core@0.52.9": "patches/@mariozechner__pi-agent-core@0.52.9.patch"
}
},
"devDependencies": {

View file

@ -114,7 +114,7 @@ function extractRunLogResultDetails(result: unknown): Record<string, unknown> |
function formatRunLogToolSummary(tool: string, details: Record<string, unknown> | null): string | undefined {
if (!details) return undefined;
if (details.error) return `error: ${details.message || details.error}`;
if (details.error) return `error: ${details.code || details.message || details.error}`;
switch (tool) {
case "web_search": return `${details.count ?? 0} results`;
case "web_fetch": {
@ -780,17 +780,19 @@ export class Agent {
private handleRunLogEvent(event: AgentEvent) {
if (event.type === "tool_execution_start") {
const toolCallId = (event as any).toolCallId ?? "unknown";
const toolName = (event as any).toolName ?? "unknown";
this.toolStartTimes.set(toolName, Date.now());
this.toolStartTimes.set(toolCallId, Date.now());
this.runLog.log("tool_start", {
tool: toolName,
args: JSON.stringify((event as any).args ?? {}).slice(0, 500),
});
} else if (event.type === "tool_execution_end") {
const toolCallId = (event as any).toolCallId ?? "unknown";
const toolName = (event as any).toolName ?? "unknown";
const startTime = this.toolStartTimes.get(toolName);
const startTime = this.toolStartTimes.get(toolCallId);
const duration_ms = startTime ? Date.now() - startTime : undefined;
this.toolStartTimes.delete(toolName);
this.toolStartTimes.delete(toolCallId);
// Extract result metadata for run-log persistence (survives session compaction)
const result = (event as any).result;
@ -806,7 +808,7 @@ export class Agent {
result_summary: formatRunLogToolSummary(toolName, details),
};
if (details?.error) {
toolEndData.error_type = String(details.error);
toolEndData.error_type = details.code ? String(details.code) : String(details.error);
}
this.runLog.log("tool_end", toolEndData);
}

View file

@ -0,0 +1,80 @@
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 };

35
pnpm-lock.yaml generated
View file

@ -68,13 +68,18 @@ overrides:
'@types/react': ^19.2.0
'@types/react-dom': ^19.2.0
patchedDependencies:
'@mariozechner/pi-agent-core@0.52.9':
hash: befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00
path: patches/@mariozechner__pi-agent-core@0.52.9.patch
importers:
.:
dependencies:
'@mariozechner/pi-agent-core':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
version: 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
@ -591,7 +596,7 @@ importers:
dependencies:
'@mariozechner/pi-agent-core':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
version: 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
@ -690,7 +695,7 @@ importers:
devDependencies:
'@mariozechner/pi-agent-core':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
version: 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
@ -12498,7 +12503,7 @@ snapshots:
std-env: 3.10.0
yoctocolors: 2.1.2
'@mariozechner/pi-agent-core@0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)':
'@mariozechner/pi-agent-core@0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)':
dependencies:
'@mariozechner/pi-ai': 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
transitivePeerDependencies:
@ -12537,7 +12542,7 @@ snapshots:
'@mariozechner/pi-coding-agent@0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)':
dependencies:
'@mariozechner/jiti': 2.6.5
'@mariozechner/pi-agent-core': 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-agent-core': 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai': 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-tui': 0.52.9
'@silvia-odwyer/photon-node': 0.3.4
@ -15944,9 +15949,9 @@ snapshots:
'@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-expo: 1.0.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.6.1))
globals: 16.5.0
@ -15961,8 +15966,8 @@ snapshots:
'@next/eslint-plugin-next': 16.1.6
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
@ -15984,7 +15989,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@ -15995,18 +16000,18 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@ -16019,7 +16024,7 @@ snapshots:
- supports-color
- typescript
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@ -16030,7 +16035,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3