Parallel ungated tool results are lost across a tool-confirmation pause, yielding an empty model reply after approval
- Langage dominant
- Python
- Étoiles
- 21.5k
- Forks
- 4k
- Merge moyen
- 1 j 14 h
- PR mergées (30 j)
- 37
Description
**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**
1. An `LlmAgent` with two tools: `delete_record` built with `require_confirmation=True`, and `list_records` (ungated).
2. 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.
3. 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`).
4. Resume with `ToolConfirmation(confirmed=True)`.
5. `_RequestConfirmationLlmRequestProcessor._resolve_confirmation_targets` re-executes only the confirmed `delete_record`. Session events now contain `function_call list_records` with no `function_response`.
6. 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.
Guide de contribution
Ouvrir le guide de contribution
Évaluation
Cette issue n'a pas encore été évaluée.