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
- Create an agent with
LongRunningFunctionTool and ResumabilityConfig(is_resumable=True)
- Use streaming (the default)
- Trigger the long-running tool — agent pauses correctly
- Send a
functionResponse to resume
- Bug 1: Agent returns immediately without continuing (if you work around Bug 2)
- 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
Description
LongRunningFunctionToolresume is broken when using streaming (stream=True, the default). After a user sends afunctionResponseto 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_asyncdoesn't check if a pause has been resolvedIn
base_llm_flow.pyaround line 792, the pause check looks at the last two events:When a
functionResponseis appended to the session (resolving the pause),events[-2]is still the original event withlong_running_tool_idsset.should_pause_invocation(events[-2])returnsTrue, so the method returns early — even though the pause was already resolved by thefunctionResponseinevents[-1].Expected behavior: The check should verify whether the paused tool IDs have corresponding
functionResponseentries 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 newEventviaEvent.model_validate()and then callspopulate_client_function_call_id, which assigns freshadk-*UUIDs each time it's called.With streaming enabled,
_finalize_model_response_eventis called for every chunk — partial events and the final non-partial event each get differentadk-*IDs. However:_should_append_eventreturnsFalsefor partial events)If the frontend captures the
functionCall.idfrom a partial SSE event (which arrives first and haslongRunningToolIdsset), that ID won't match the ID stored in the session's non-partial event. When the client sends afunctionResponsewith the partial event's ID,find_event_by_function_call_idfails with: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
longRunningToolIdsset.Steps to Reproduce
LongRunningFunctionToolandResumabilityConfig(is_resumable=True)functionResponseto resumeValueError: Function call event not foundbecause the ID from the SSE partial event doesn't match the stored non-partial eventWorkarounds
For Bug 1: Patch
_run_one_step_asyncto collect allfunctionResponseIDs across events and check if paused tool IDs have been resolved:For Bug 2: On the client side, only capture
longRunningToolIdsfrom 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.3LongRunningFunctionTool,ResumabilityConfig,AgentTool(sub-agents)Related Issues