docs/design/2026-07-11-tool-call-preparing-events.md
Qwen Code currently emits a tool call only after the provider has finished streaming its arguments. For tools with large or complex inputs, generating those arguments can take much longer than executing the tool itself. ACP clients therefore show no activity during the expensive part and users can mistake the turn for a stalled request.
The provider streams already expose stable tool identity before the arguments are complete:
id and name in content_block_start for a tool_use
block, then sends argument fragments as input_json_delta.id and function.name in the
first choice.delta.tool_calls item, then append argument fragments.Qwen Code deliberately waits for content_block_stop or finish_reason
before constructing a Gemini-compatible functionCall. That execution safety
property must remain unchanged.
Let ACP clients render a tool card while the model is still preparing tool arguments, with this lifecycle:
preparing -> in_progress -> completed | failed
The early event contains only the stable tool-call ID and tool name. It never contains partial arguments and never starts tool execution.
This change supports the two provider paths used by the integrating client:
Other providers keep their current behavior. Because preparation metadata is
optional, they naturally degrade to the existing
in_progress -> completed | failed lifecycle.
The change does not alter:
functionCall or functionResponse construction;Associate transient tool preparation metadata with each
GenerateContentResponse through a module-local WeakMap:
interface ToolCallPreparation {
callId: string;
toolName: string;
}
Provider adapters store this metadata against the top-level response chunk.
It is neither an enumerable response property nor a Gemini Part, so it is
not serialized and Gemini history assembly continues to see only text,
thought, and complete functionCall parts. Shared helpers provide typed store
and read operations, avoiding provider-specific casts in ACP.
In AnthropicContentGenerator.processStream(), when
content_block_start(tool_use) contains a non-empty id and name, yield an
otherwise empty Gemini response chunk carrying one preparation entry.
Continue accumulating input_json_delta unchanged. At content_block_stop,
emit the existing complete functionCall with parsed arguments. No argument
data is exposed before that point.
In convertOpenAIChunkToGemini(), observe each
choice.delta.tool_calls item after passing it to the existing stream-local
tool-call parser. When a stable non-empty ID and name are available for the
first time, attach one preparation entry to the current response chunk.
Deduplicate by tool-call ID within the request context. Continue emitting the
complete functionCall only when finish_reason is present. Providers that do
not expose both identity fields early simply keep the existing behavior.
ACP Session reads preparation metadata before collecting complete
functionCalls. For each new preparation it emits the standard ACP
tool_call frame with:
{
status: 'pending',
rawInput: {},
_meta: {
phase: 'preparing',
toolName,
// existing provenance metadata remains present
},
}
The existing execution path later emits the same toolCallId with
status: 'in_progress' and the complete arguments. Existing result emission
then finishes the card as completed or failed.
TodoWrite keeps its current special handling and does not emit a tool card.
Preparation emission uses the same filtering rule, so it cannot create a card
that the execution path intentionally suppresses.
Each active ACP model stream tracks preparations until the stream completes and
hands its parsed calls to tool execution. When an attempt is abandoned by
retry, model fallback, user cancellation, or stream error, ACP emits a terminal
tool_call_update for each remaining entry:
{
status: 'failed',
content: [],
_meta: {
phase: 'preparing',
preparationDiscarded: true,
toolName,
},
}
preparationDiscarded means the model attempt was abandoned before a parsed
tool request reached execution. It is not a tool execution failure. The integrating
client should remove this transient card rather than render a failed tool.
Using a protocol-valid terminal status ensures older clients do not retain an
indefinitely pending card.
RETRY now clears complete functionCalls collected from the abandoned
attempt, matching the existing MODEL_FALLBACK behavior across all four ACP
stream paths. This prevents a parsed call from the failed attempt from being
executed together with calls from the replacement attempt.
When a complete functionCall with the same ID arrives and the stream finishes
normally, ACP hands it to the existing execution path without a discarded
update. If the stream fails after parsing the call but before execution, the
preparation is still discarded. Normal tool errors therefore continue through
the existing result path and are never marked as discarded.
GeminiChat and history builders ignore the optional top-level metadata and
continue persisting only candidate content.functionCall IDs, preserving ACP update correlation when a provider reuses
an ID from history.Turn, TUI, and non-interactive JSON consumers keep their current
behavior because no new Gemini Part or server event is introduced.content_block_start(tool_use) yields preparation metadata
before any input_json_delta and before the final functionCall.finish_reason, with
unchanged parsed arguments.pending with _meta.phase = 'preparing' and
no partial input.in_progress with
complete arguments._meta.preparationDiscarded = true.TodoWrite remains suppressed.Run the focused provider and ACP suites from their package directories, then run repository build, typecheck, and lint before completion. The implementation rebased on v0.19.9 has been verified with: