Agent host: client tools execute off the stream-mapper ready rather than the SDK invocation
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
### Summary
Client tools are executed off the stream-mapper's `chat/toolCallReady`, independently of whether the SDK has invoked that tool through its in-process MCP server. Three separate defects fall out of that single seam, each currently proposed as its own patch.
Filing this per @kycutler's request on #330684, so the root cause can be triaged as one design question rather than three unrelated fixes.
### Mechanism
When a client-contributed tool call reaches `ToolCallStatus.Running`, the host raises a `ToolClientExecution` input request ([agentSideEffects.ts:566-575](https://github.com/microsoft/vscode/blob/main/src/vs/platform/agentHost/node/agentSideEffects.ts#L566-L575)) and the workbench executes the tool. That happens in parallel with, and not ordered against, the SDK's own invocation of the same tool. The ready is emitted by the stream mapper at `content_block_stop` with `confirmed: "not-needed"`.
Because the trigger is the streamed ready rather than the SDK invocation, execution can begin before the arguments are known to be complete, more than once, and before anything is waiting for the result.
### The three defects
| Defect | What goes wrong | PR |
|---|---|---|
| Double invocation | The call is readied twice, by the stream mapper and again by the permission flow, so the tool runs twice | #330683 |
| Empty arguments | The disconnect path synthesises a ready with no `toolInput`, so the tool is invoked with `{}` and throws during preparation | #330684 |
| Dropped result | Execution finishes before the SDK registers its handler, so `respond` discards the result and the turn heartbeats forever | #330730 |
Each is individually reproducible, and each PR carries a test that fails without the fix.
### Why this looks structural
If client tools executed only on the SDK-driven invocation, with the stream-mapper ready used for rendering only, all three become impossible by construction: one authoritative trigger, never without real input, and handler registration ordered before execution.
I did not propose that as a PR because I cannot tell whether the stream-mapper ready is deliberately actionable. Starting a tool as soon as its arguments finish streaming, rather than waiting for the SDK round trip, is a plausible latency win, and the permission flow may already depend on the current ordering.
@kycutler suggested validating arguments against the tool schema, which would catch the undefined page id in #330684 at the boundary rather than in each tool. That seems right for the empty-argument class specifically. It would not by itself address the double invocation or the dropped result, since both are ordering problems rather than payload problems.
### Environment
Reproduced against a dev container (remote agent host) on `1.133.x`, using the Claude provider with auto approvals.
*AI disclosure: this issue was written with the assistance of AI.*
### Public patches and patcher scripts
[Public patch catalog and patcher scripts](https://github.com/RyanEwen/vscode-patches/blob/main/CATALOG.md) · [Source patch index](https://github.com/RyanEwen/vscode-patches/blob/main/SOURCE-PATCHES.md). The [public collection](https://github.com/RyanEwen/vscode-patches) includes the maintained patchers, rollback instructions, regression scripts, and historical snapshots. Build restrictions and exact installer coverage are documented there.
Contributor guide
Assessment
This issue has not been assessed yet.