Skip to content

A Workflow LlmAgent node with two pending tool confirmations does not keep waiting when only one is answered; the next node runs too early #7684

Description

@yifanxiong272

🔴 Required Information

Describe the Bug:
In a Workflow, an LlmAgent node (agent) is followed by a FunctionNode (report). In one turn, the model calls a tool that has require_confirmation=True twice, so the run pauses with two adk_request_confirmation calls. The application confirms them one per message, for example as a user approves them one at a time. After the first confirmation, the node should keep waiting for the second. It does not:

  • With a plain Runner(node=workflow), the agent goes on to its model. The model gets the first tool's result and the placeholder {'error': 'This tool call requires confirmation, please approve or reject.'} for the second call. report runs on that answer and the workflow finishes. The second confirmation then runs the tool and the model again, but report does not run again, so the confirmed result never reaches the workflow.
  • With ResumabilityConfig(is_resumable=True), the agent correctly waits. However, report already runs after the first confirmation, with None as its input, and runs a second time after the second confirmation.

Confirming both calls in one message works in both setups.

Steps to Reproduce:

  1. pip install google-adk==2.12.0
  2. Save the script under "Minimal Reproduction Code" as repro.py. No model or API key is needed; the model is a scripted BaseLlm.
  3. Run python repro.py, then python repro.py --resumable. Add --all to answer both confirmations in one message.

Expected Behavior:
Both setups print what --all prints: the model is consulted again only once both calls are answered, and report runs once, on that answer:

google-adk 2.12.0
model call 0 got: []
model call 1 got: [{'result': 'acted 1'}, {'result': 'acted 2'}]
report node ran with: ["done: [{'result': 'acted 1'}, {'result': 'acted 2'}]"]

Observed Behavior:
python repro.py:

google-adk 2.12.0
model call 0 got: []
model call 1 got: [{'result': 'acted 1'}, {'error': 'This tool call requires confirmation, please approve or reject.'}]
model call 2 got: [{'result': 'acted 1'}, {'result': 'acted 2'}]
report node ran with: ["done: [{'result': 'acted 1'}, {'error': 'This tool call requires confirmation, please approve or reject.'}]"]

python repro.py --resumable:

google-adk 2.12.0
model call 0 got: []
model call 1 got: [{'result': 'acted 1'}, {'result': 'acted 2'}]
report node ran with: [None, "done: [{'result': 'acted 1'}, {'result': 'acted 2'}]"]

Environment Details:

  • ADK Library Version (pip show google-adk): 2.12.0; also 2.11.0 and main at 471341f
  • Desktop OS: macOS 15.7.3
  • Python Version (python -V): Python 3.13.9

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: N/A (the reproduction uses a scripted BaseLlm)

🟡 Optional Information

Regression:
Not that I can tell: 2.11.0, 2.12.0 and main behave the same.

Logs:
See "Observed Behavior".

Screenshots / Video:
N/A

Additional Context:
The tool confirmation docs do not say that the confirmations of one model turn must be answered together. The code supports answering them one at a time in several places (main at 471341f):

  • _pause_left_calls_unanswered in flows/llm_flows/core/_resume.py:119-123 says: "a partially answered pause keeps waiting".
  • TestHITLConfirmationFlowWithParallelAgentAndResumableApp confirms two pending calls in two messages.
  • The workflow reruns a node whose interrupts are only partly resolved (workflow/utils/_replay_interceptor.py:113-121).

The problem appears to be in that rerun. It passes the node only the resolved answers and drops the unresolved interrupt ids (_replay_interceptor.py:117-119; only the else branch keeps interrupts = unresolved), so the node has to raise the open ones again itself. An LlmAgent node does not do that:

  • Not resumable: decide_resume checks for a partially answered pause only when invocation_context.is_resumable (flows/llm_flows/core/_resume.py:343-349), so the flow calls the model with the placeholder still in place of the second result.
  • Resumable: the same check returns PAUSE, but the step ends without any event. The node run has no output and no interrupt, so the workflow treats it as completed with None and starts report.

In both setups, the second confirmation's request is still answered by _RequestConfirmationLlmRequestProcessor, which reruns the node after the workflow has moved on.

Minimal Reproduction Code:

"""An LlmAgent node of a Workflow calls a tool that requires confirmation twice in one model turn.
The application confirms the two pending `adk_request_confirmation` calls one per message."""
import asyncio
import sys
from importlib.metadata import version

from google.adk.agents import LlmAgent
from google.adk.apps.app import App, ResumabilityConfig
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_response import LlmResponse
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools import FunctionTool
from google.adk.workflow import START, FunctionNode, Workflow
from google.genai import types

RESUMABLE = "--resumable" in sys.argv
ALL_AT_ONCE = "--all" in sys.argv
consultations, reports = [], []


class Model(BaseLlm):
    model: str = "scripted"

    async def generate_content_async(self, llm_request, stream=False):
        results = [part.function_response.response for content in llm_request.contents
                   for part in content.parts or [] if part.function_response]
        consultations.append(results)
        if results:
            parts = [types.Part(text=f"done: {results}")]
        else:
            parts = [types.Part(function_call=types.FunctionCall(id=f"call-{n}", name="act", args={"n": n}))
                     for n in (1, 2)]
        yield LlmResponse(content=types.Content(role="model", parts=parts))


def act(n: int) -> str:
    return f"acted {n}"


def report(node_input):
    reports.append(node_input)
    return node_input


agent = LlmAgent(name="agent", model=Model(), tools=[FunctionTool(act, require_confirmation=True)])
workflow = Workflow(name="root", edges=[(START, agent), (agent, FunctionNode(func=report, name="report"))])


async def main():
    sessions = InMemorySessionService()
    if RESUMABLE:
        runner = Runner(app=App(name="app", root_agent=workflow,
                                resumability_config=ResumabilityConfig(is_resumable=True)), session_service=sessions)
    else:
        runner = Runner(app_name="app", node=workflow, session_service=sessions)
    session = await sessions.create_session(app_name="app", user_id="u")
    message, invocation = types.Content(role="user", parts=[types.Part(text="go")]), None
    pending = []
    for _ in range(4):
        async for event in runner.run_async(user_id="u", session_id=session.id, new_message=message,
                                            invocation_id=invocation if RESUMABLE else None):
            invocation = event.invocation_id
            for part in event.content.parts or [] if event.content else []:
                if part.function_call and part.function_call.id in (event.long_running_tool_ids or ()):
                    pending.append(part.function_call)
        if not pending:
            break
        answered = pending if ALL_AT_ONCE else pending[:1]
        pending = pending[len(answered):]
        message = types.Content(role="user", parts=[types.Part(function_response=types.FunctionResponse(
            id=call.id, name=call.name, response={"confirmed": True})) for call in answered])
    print("google-adk", version("google-adk"))
    for index, results in enumerate(consultations):
        print(f"model call {index} got: {results}")
    print("report node ran with:", reports)


asyncio.run(main())

How often has this issue occurred?:

  • Always (100%)

Activity

  1. widechaos commented on Oct 22, 2026

    @widechaos

    I reproduced both paths on current main (466648bb) with scripted models and network access blocked. In 16 cases covering confirmation order and approval/rejection, all eight one-at-a-time cases let the downstream node run early; all eight batch-confirmation controls passed. The local input-restoration candidate for #7650 leaves these results unchanged.

    May I work on a fix scoped to partial tool confirmations in a Workflow LlmAgent? I would preserve the existing first-answer behavior for general rerun nodes and the non-resumable flow controls. I have not changed the shared resume logic; please let me know if someone is already working on this.

  2. llalitkumarrr commented on Oct 27, 2026

    @llalitkumarrr
    Collaborator

    Hello @yifanxiong272,

    Could you please take a look at #7762 and share your feedback on whether this resolves your issue?

  3. added
    request clarification[Status] The maintainer need clarification or more information from the author
    on Oct 27, 2026
  4. yifanxiong272 commented on Oct 27, 2026

    @yifanxiong272
    Author

    Thanks @llalitkumarrr, #7762 fixes it for me.

    I ran the reproduction from the issue on #7762 (051348d) and on its base (2084efb, 2.13.0):

    • Confirming the two calls one per message, as in the issue: on the base, the first confirmation already resumes the model with [{'result': 'acted 1'}, {'error': 'This tool call requires confirmation, please approve or reject.'}], and the report node runs with that answer. With the PR, the node keeps waiting until both are confirmed, the model is consulted once with [{'result': 'acted 1'}, {'result': 'acted 2'}], and the report node runs once with it.
    • With ResumabilityConfig(is_resumable=True), the base runs the report node twice, first with None and then with the final answer. With the PR, it runs once with the final answer.
    • Confirming both calls in one message behaves as before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

request clarification[Status] The maintainer need clarification or more information from the authortools[Component] This issue is related to tools

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions