Describe the bug
When a model turn contains a confirmation-gated function call (FunctionTool(require_confirmation=True)) in parallel with ungated function calls, the ungated siblings' results are lost across the confirmation pause. After the user approves, the resume processor (flows/llm_flows/request_confirmation.py) re-executes only the confirmed tool, leaving the sibling function_call in session history with no matching function_response. Gemini, given that dangling call, returns an empty final message — while the approved (destructive) tool HAS executed. The end user sees no answer and may retry an action that already succeeded.
To Reproduce
- An
LlmAgent with two tools: delete_record built with require_confirmation=True, and list_records (ungated).
- Prompt so the model emits both calls in one turn, e.g. "Delete record X, and after deleting, list the remaining records." Gemini frequently parallelizes this into one response with both
function_call parts.
- The run pauses with
adk_request_confirmation. Observations at this point:
- The ungated
list_records does execute during the pause (its side effects are visible), but the merged function-response event — carrying the sibling's real result plus the gated tool's "This tool call requires confirmation" placeholder — is never persisted to the session. When the caller stops consuming the event stream at the confirmation event (the natural thing for a client that must wait for user input, e.g. any AG-UI/SSE bridge), the response event that _postprocess_handle_function_calls_async yields after generate_request_confirmation_event is never reached (GeneratorExit).
- Resume with
ToolConfirmation(confirmed=True).
_RequestConfirmationLlmRequestProcessor._resolve_confirmation_targets re-executes only the confirmed delete_record. Session events now contain function_call list_records with no function_response.
- The follow-up LLM call returns empty content (
{"text": ""}).
Observed session event sequence (from a Postgres session service, ADK 2.7.0):
model: [text, function_call delete_record, thought_signature, function_call list_records, text]
model: [function_call adk_request_confirmation] (long_running_tool_ids set)
user: [function_response adk_request_confirmation {confirmed: true}]
model: [function_response delete_record {...success...}] <- only the confirmed tool
model: [text ""] <- empty final reply
Expected behavior
Either (a) the pause-time function-response event (sibling results + confirmation placeholder) is persisted before/with the confirmation-request event so history stays complete across the pause, or (b) the resume path re-executes (or synthesizes responses for) unresolved sibling calls from the same turn — so the post-approval continuation never sees a dangling function_call.
Screenshots
N/A (event sequence above).
Desktop:
- OS: macOS 26 / Linux (Docker python:3.12-slim)
- Python version: 3.12
- ADK version: 2.7.0 (also inspected 2.5.0 — same structure)
Model Information:
- Are you using LiteLLM: No
- Which model is being used: gemini via Vertex AI
Additional context
Single-call confirmation flows (pause → approve/reject → resume) work correctly in 2.7.0, including the 2.6.x fixes for consumed-confirmation re-validation. The failure is specific to the parallel gated+ungated combination. Downstream we mitigate with an after_model_callback that drops ungated siblings from mixed turns before execution (the model re-issues them after the decision), but a framework-level fix would let parallel calls keep their results.
Describe the bug
When a model turn contains a confirmation-gated function call (
FunctionTool(require_confirmation=True)) in parallel with ungated function calls, the ungated siblings' results are lost across the confirmation pause. After the user approves, the resume processor (flows/llm_flows/request_confirmation.py) re-executes only the confirmed tool, leaving the siblingfunction_callin session history with no matchingfunction_response. Gemini, given that dangling call, returns an empty final message — while the approved (destructive) tool HAS executed. The end user sees no answer and may retry an action that already succeeded.To Reproduce
LlmAgentwith two tools:delete_recordbuilt withrequire_confirmation=True, andlist_records(ungated).function_callparts.adk_request_confirmation. Observations at this point:list_recordsdoes execute during the pause (its side effects are visible), but the merged function-response event — carrying the sibling's real result plus the gated tool's"This tool call requires confirmation"placeholder — is never persisted to the session. When the caller stops consuming the event stream at the confirmation event (the natural thing for a client that must wait for user input, e.g. any AG-UI/SSE bridge), the response event that_postprocess_handle_function_calls_asyncyields aftergenerate_request_confirmation_eventis never reached (GeneratorExit).ToolConfirmation(confirmed=True)._RequestConfirmationLlmRequestProcessor._resolve_confirmation_targetsre-executes only the confirmeddelete_record. Session events now containfunction_call list_recordswith nofunction_response.{"text": ""}).Observed session event sequence (from a Postgres session service, ADK 2.7.0):
Expected behavior
Either (a) the pause-time function-response event (sibling results + confirmation placeholder) is persisted before/with the confirmation-request event so history stays complete across the pause, or (b) the resume path re-executes (or synthesizes responses for) unresolved sibling calls from the same turn — so the post-approval continuation never sees a dangling
function_call.Screenshots
N/A (event sequence above).
Desktop:
Model Information:
Additional context
Single-call confirmation flows (pause → approve/reject → resume) work correctly in 2.7.0, including the 2.6.x fixes for consumed-confirmation re-validation. The failure is specific to the parallel gated+ungated combination. Downstream we mitigate with an
after_model_callbackthat drops ungated siblings from mixed turns before execution (the model re-issues them after the decision), but a framework-level fix would let parallel calls keep their results.