Repository navigation
LongRunningFunctionTool resume fails: unresolved pause check + streaming ID mismatch #5064
Description
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])
)
):
returnWhen 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_eventreturnsFalsefor 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
- Create an agent with
LongRunningFunctionToolandResumabilityConfig(is_resumable=True) - Use streaming (the default)
- Trigger the long-running tool — agent pauses correctly
- Send a
functionResponseto resume - Bug 1: Agent returns immediately without continuing (if you work around Bug 2)
- Bug 2:
ValueError: Function call event not foundbecause 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:
returnFor 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-adkversion: 1.27.3- Python: 3.13
- Model: gemini-2.5-flash (streaming enabled)
- Using:
LongRunningFunctionTool,ResumabilityConfig,AgentTool(sub-agents)
Related Issues
- Long-Running Tool FunctionResponse Routing Bypasses Custom Agent Orchestration #3986 — Long-Running Tool FunctionResponse routing
- LongRunningFunctionTool + A2A Protocol Task Completion Issue #4145 — LongRunningFunctionTool + A2A task completion
Activity
zeel2104 commentedon Mar 30, 2026
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
surajksharma07 commentedon Apr 1, 2026
@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!
tottenjordan commentedon Apr 1, 2026
@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 onInvocationContextis a clean fix. Collecting allfunctionResponseIDs across events and checking them againstlong_running_tool_idsis exactly the right approach — much better than the brittleevents[-2:]window. Using it in bothllm_agent.pyandbase_llm_flow.pycovers 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 beforepopulate_client_function_call_idis called — this is the server-side fix we need. On our end, we've been working around this client-side by only capturinglongRunningToolIdsfrom 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 ourinteractive_creativeagent, which has 3 sequentialLongRunningFunctionToolcheckpoints with streaming enabled. Will report back with results and leave a review on #5072.tottenjordan commentedon Apr 1, 2026
Tested PR #5072 (
zeel2104/adk-python@fix-5064-long-running-resume) against our setup — both bugs are fixed:has_unresolved_long_running_tool_callscorrectly identifies resolved pauses.Left an approving review on #5072. Once it merges and lands in a release, we'll bump our
google-adkversion. Our client-side workarounds (partial event filtering, session fallback inspection) will stay as defense-in-depth.surajksharma07 commentedon Apr 4, 2026
@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.
surajksharma07 commentedon Apr 4, 2026
@xuanyang15 Please have a look into it once.
xuanyang15 commentedon Jul 23, 2026
Both reported bugs are fixed on the latest code (v2.5.0), though via a different
change than PR #5072 (which was never merged):
checks whether the paused call's id is resolved by a functionResponse before
pausing. Covered by tests/unittests/agents/test_llm_agent_interruptions.py.
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).