Skip to content

LongRunningFunctionTool resume fails: unresolved pause check + streaming ID mismatch #5064

Closed

Description

@tottenjordan

Description

LongRunningFunctionTool resume is broken when using streaming (stream=True, the default). After a user sends a functionResponse to resume a paused tool, the agent immediately returns without calling the LLM — effectively ending the run instead of continuing.

There are two distinct bugs that compound to cause this:

Bug 1: _run_one_step_async doesn't check if a pause has been resolved

In base_llm_flow.py around line 792, the pause check looks at the last two events:

if (
    invocation_context.is_resumable
    and events
    and len(events) > 1
    and (
        invocation_context.should_pause_invocation(events[-1])
        or invocation_context.should_pause_invocation(events[-2])
    )
):
    return

When a functionResponse is appended to the session (resolving the pause), events[-2] is still the original event with long_running_tool_ids set. should_pause_invocation(events[-2]) returns True, so the method returns early — even though the pause was already resolved by the functionResponse in events[-1].

Expected behavior: The check should verify whether the paused tool IDs have corresponding functionResponse entries elsewhere in the event list. If all paused IDs are resolved, execution should continue.

Bug 2: Streaming causes function call ID mismatch between partial and stored events

_finalize_model_response_event (line 80-113) creates a new Event via Event.model_validate() and then calls populate_client_function_call_id, which assigns fresh adk-* UUIDs each time it's called.

With streaming enabled, _finalize_model_response_event is called for every chunk — partial events and the final non-partial event each get different adk-* IDs. However:

  • Partial events are sent via SSE to the client but are not stored in the session (_should_append_event returns False for partial events)
  • Non-partial events are stored in the session

If the frontend captures the functionCall.id from a partial SSE event (which arrives first and has longRunningToolIds set), that ID won't match the ID stored in the session's non-partial event. When the client sends a functionResponse with the partial event's ID, find_event_by_function_call_id fails with:

ValueError: Function call event not found for function response id: adk-XXXX

Expected behavior: Function call IDs should be stable across partial and non-partial events for the same LLM response, OR only non-partial events should have longRunningToolIds set.

Steps to Reproduce

  1. Create an agent with LongRunningFunctionTool and ResumabilityConfig(is_resumable=True)
  2. Use streaming (the default)
  3. Trigger the long-running tool — agent pauses correctly
  4. Send a functionResponse to resume
  5. Bug 1: Agent returns immediately without continuing (if you work around Bug 2)
  6. Bug 2: ValueError: Function call event not found because the ID from the SSE partial event doesn't match the stored non-partial event

Workarounds

For Bug 1: Patch _run_one_step_async to collect all functionResponse IDs across events and check if paused tool IDs have been resolved:

if (
    invocation_context.is_resumable
    and events
    and len(events) > 1
):
    all_response_ids = set()
    for evt in events:
        for fr in evt.get_function_responses():
            if fr.id:
                all_response_ids.add(fr.id)

    has_unresolved_pause = False
    for check_evt in (events[-1], events[-2]):
        if invocation_context.should_pause_invocation(check_evt):
            paused_ids = check_evt.long_running_tool_ids or set()
            if paused_ids - all_response_ids:
                has_unresolved_pause = True
                break

    if has_unresolved_pause:
        return

For Bug 2: On the client side, only capture longRunningToolIds from non-partial events (event.partial !== true). Partial events have unstable IDs that won't match what's stored in the session.

Environment

  • google-adk version: 1.27.3
  • Python: 3.13
  • Model: gemini-2.5-flash (streaming enabled)
  • Using: LongRunningFunctionTool, ResumabilityConfig, AgentTool (sub-agents)

Related Issues

Activity

  1. zeel2104 commented on Mar 30, 2026

    @zeel2104

    I’ve opened a PR with a fix for both reported issues: resume now only stays paused for unresolved long-running tool calls, and streaming function call IDs are preserved across partial/final events so resume routing stays stable.
    Validated locally with targeted unit tests: 49 passed across the touched test files, plus focused regressions for unresolved-pause handling and function-call ID preservation

  2. added
    tools[Component] This issue is related to tools
    on Mar 31, 2026
  3. added theissue type on Mar 31, 2026
  4. surajksharma07 commented on Apr 1, 2026

    @surajksharma07
    Collaborator

    @tottenjordan the streaming ID mismatch and the early return on resume are clearly present in the current code.

    For a workaround: try tracking a stable ID dict across streaming chunks in _finalize_model_response_event, and for the pause check, collect all functionResponse IDs first and only pause if the long_running_tool_ids are still unresolved after that.

    Could you apply those two changes and confirm the behavior on your end?

    PR #5072 addresses exactly this, so once you've tested it and it holds up for your setup, a review or confirmation over there would really help move it forward!

  5. tottenjordan commented on Apr 1, 2026

    @tottenjordan
    Author

    @surajksharma07 Thanks for the pointer to #5072 — I took a detailed look at the PR and the approach is solid.

    Bug 1 (unresolved pause check): The new has_unresolved_long_running_tool_calls() method on InvocationContext is a clean fix. Collecting all functionResponse IDs across events and checking them against long_running_tool_ids is exactly the right approach — much better than the brittle events[-2:] window. Using it in both llm_agent.py and base_llm_flow.py covers both entry points.

    Bug 2 (streaming ID stability): preserve_existing_function_call_ids() carrying forward IDs from the previous streaming event to the finalized event before populate_client_function_call_id is called — this is the server-side fix we need. On our end, we've been working around this client-side by only capturing longRunningToolIds from non-partial events (!event.partial), but the server-side fix in #5072 is the proper solution.

    I'll install the PR branch locally (zeel2104/adk-python@fix-5064-long-running-resume) and test it end-to-end against our interactive_creative agent, which has 3 sequential LongRunningFunctionTool checkpoints with streaming enabled. Will report back with results and leave a review on #5072.

  6. tottenjordan commented on Apr 1, 2026

    @tottenjordan
    Author

    Tested PR #5072 (zeel2104/adk-python@fix-5064-long-running-resume) against our setup — both bugs are fixed:

    • Bug 1: Resume no longer returns early. has_unresolved_long_running_tool_calls correctly identifies resolved pauses.
    • Bug 2: Function call IDs are stable across partial/final streaming events. No more ID mismatch errors.
    • All 3 upstream unit tests pass, 125 downstream tests pass with no regressions.

    Left an approving review on #5072. Once it merges and lands in a release, we'll bump our google-adk version. Our client-side workarounds (partial event filtering, session fallback inspection) will stay as defense-in-depth.

  7. surajksharma07 commented on Apr 4, 2026

    @surajksharma07
    Collaborator

    @tottenjordan Thanks for putting it through such a thorough test — 3 sequential checkpoints with streaming is exactly the kind of real-world scenario that matters for something like this.

    Your approving review on #5072 is really helpful, we'll track it through to merge and get the fix into the future releases.
    Will close this out once it ships!

    Team is looking more into it.

  8. added
    needs review[Status] The PR/issue is awaiting review from the maintainer
    on Apr 4, 2026
  9. surajksharma07 commented on Apr 4, 2026

    @surajksharma07
    Collaborator

    @xuanyang15 Please have a look into it once.

  10. removed their assignment
    on Apr 15, 2026
  11. added
    spam[Status] Issues suspected of having comments which are spam
    on Jul 21, 2026
  12. deleted a comment from adk-bot on Jul 21, 2026
  13. removed
    spam[Status] Issues suspected of having comments which are spam
    on Jul 21, 2026
  14. xuanyang15 commented on Jul 23, 2026

    @xuanyang15
    Collaborator

    Both reported bugs are fixed on the latest code (v2.5.0), though via a different
    change than PR #5072 (which was never merged):

    • Bug 1 (resume returns early): resolved in base_llm_flow.py:990-1005, which now
      checks whether the paused call's id is resolved by a functionResponse before
      pausing. Covered by tests/unittests/agents/test_llm_agent_interruptions.py.
    • Bug 2 (streaming FC id mismatch): resolved in StreamingResponseAggregator,
      which generates a stable FC id and carries it across partial/final events;
      populate_client_function_call_id now preserves existing ids. Covered by
      tests/unittests/utils/test_streaming_utils.py (asserts partial_fc.id == final_fc.id).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

needs review[Status] The PR/issue is awaiting review from the maintainertools[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