When a tool requests OAuth credentials at execution time (tool-level auth), the resulting adk_request_credential event does not terminate the invocation. The agent loop continues to the next LLM call. This contrasts with toolset-level auth, where the same EUC event terminates the invocation cleanly.
In _postprocess_handle_function_calls_async (base_llm_flow.py:1111), the auth event is yielded at line 1124 and the function_response_event at line 1133. After _run_one_step_async returns, run_async (lines 803-816) checks last_event.is_final_response(). last_event is the function_response, not the EUC. The function_response is not a final response because is_final_response (event.py:91-98) returns False whenever the event carries function responses. The agent loop continues.
In _resolve_toolset_auth (base_llm_flow.py:113-191), the auth event is yielded at line 184 and invocation_context.end_invocation = True is set at line 191. _run_one_step_async returns early at line 831 and run_async breaks. Both paths emit the same adk_request_credential shape with long_running_tool_ids set, so the divergence is in invocation termination, not in the event itself.
The primary user-visible consequence is that the agent reasons one more turn after surfacing the authorization request. The next LLM call produces a follow-up message that emits before the user can complete the OAuth flow, often a model-generated explanation that authorization is still missing. This message appears on top of or alongside the authorization request itself.
A secondary consequence affects deployments that use a session service with marker-based concurrency control, such as DatabaseSessionService. The trailing turn's writes continue past the EUC and can overlap with a separately-scheduled post-auth resume task that loads the session in parallel. The resume task's session snapshot ends up stale relative to the trailing writes, and its first append fails with the marker-mismatch ValueError from database_session_service.py. We have observed this with DatabaseSessionService. The in-memory service does not implement that check. We have not verified the Agent Engine session service.
The TODO at base_llm_flow.py:842-845 acknowledges this case ("we should find a way to pause when the long running tool call is followed by more than one text responses"). The is_resumable branch at lines 838-851 returns early when a long-running call is detected in recent events, but only when invocation_context.is_resumable is set, which is not the default. Setting end_invocation = True when _postprocess_handle_function_calls_async yields an auth event would make tool-level auth terminate symmetrically with toolset-level auth, without depending on the resumable codepath.
Reproduction
Self-contained repro at https://github.com/doughayden/adk-issue-examples/tree/main/04-tool_level_auth_continuation. It applies the workaround for #5327 (get_auth_config = lambda: None) at runtime to land on the tool-level auth path, then sends a prompt that triggers a tool call. The same script accepts a --apply-fix flag that monkey-patches the proposed fix.
Without the fix:
👤 User: What's the weather in San Francisco?
🌤️ Weather Assistant event stream:
[function_call] get_weather by WeatherAssistant
[auth_event] adk_request_credential by WeatherAssistant
[function_response] get_weather by WeatherAssistant
[post_euc_text] WeatherAssistant: "I'm sorry, I cannot retrieve the weather for San Francisco at the moment. It ..."
Event counts:
function_calls: 1
auth_events: 1
function_responses: 1
text_events: 1
post_euc_text_events: 1
✅ Bug reproduced: 1 text event(s) after the EUC (agent loop continued past adk_request_credential).
With the fix:
👤 User: What's the weather in San Francisco?
🌤️ Weather Assistant event stream:
[function_call] get_weather by WeatherAssistant
[auth_event] adk_request_credential by WeatherAssistant
[function_response] get_weather by WeatherAssistant
Event counts:
function_calls: 1
auth_events: 1
function_responses: 1
text_events: 0
post_euc_text_events: 0
✅ Fix verified: no LLM events after the EUC.
Versions
Observed in google-adk==1.30.0. Verified that base_llm_flow.py is byte-identical on v1.32.0 and on main at the time of writing, so the bug is present on current released versions.
Related
The same yield site at lines 1126-1130 also produces tool_confirmation_event for HITL confirmations with the same long_running_tool_ids shape and the same termination gap. The accompanying PR scopes to auth_event only; the HITL case is a likely sibling that would benefit from a similar fix.
When a tool requests OAuth credentials at execution time (tool-level auth), the resulting
adk_request_credentialevent does not terminate the invocation. The agent loop continues to the next LLM call. This contrasts with toolset-level auth, where the same EUC event terminates the invocation cleanly.In
_postprocess_handle_function_calls_async(base_llm_flow.py:1111), the auth event is yielded at line 1124 and the function_response_event at line 1133. After_run_one_step_asyncreturns,run_async(lines 803-816) checkslast_event.is_final_response(). last_event is the function_response, not the EUC. The function_response is not a final response becauseis_final_response(event.py:91-98) returns False whenever the event carries function responses. The agent loop continues.In
_resolve_toolset_auth(base_llm_flow.py:113-191), the auth event is yielded at line 184 andinvocation_context.end_invocation = Trueis set at line 191._run_one_step_asyncreturns early at line 831 andrun_asyncbreaks. Both paths emit the sameadk_request_credentialshape withlong_running_tool_idsset, so the divergence is in invocation termination, not in the event itself.The primary user-visible consequence is that the agent reasons one more turn after surfacing the authorization request. The next LLM call produces a follow-up message that emits before the user can complete the OAuth flow, often a model-generated explanation that authorization is still missing. This message appears on top of or alongside the authorization request itself.
A secondary consequence affects deployments that use a session service with marker-based concurrency control, such as
DatabaseSessionService. The trailing turn's writes continue past the EUC and can overlap with a separately-scheduled post-auth resume task that loads the session in parallel. The resume task's session snapshot ends up stale relative to the trailing writes, and its first append fails with the marker-mismatchValueErrorfromdatabase_session_service.py. We have observed this withDatabaseSessionService. The in-memory service does not implement that check. We have not verified the Agent Engine session service.The TODO at base_llm_flow.py:842-845 acknowledges this case ("we should find a way to pause when the long running tool call is followed by more than one text responses"). The
is_resumablebranch at lines 838-851 returns early when a long-running call is detected in recent events, but only wheninvocation_context.is_resumableis set, which is not the default. Settingend_invocation = Truewhen_postprocess_handle_function_calls_asyncyields an auth event would make tool-level auth terminate symmetrically with toolset-level auth, without depending on the resumable codepath.Reproduction
Self-contained repro at https://github.com/doughayden/adk-issue-examples/tree/main/04-tool_level_auth_continuation. It applies the workaround for #5327 (
get_auth_config = lambda: None) at runtime to land on the tool-level auth path, then sends a prompt that triggers a tool call. The same script accepts a--apply-fixflag that monkey-patches the proposed fix.Without the fix:
With the fix:
Versions
Observed in
google-adk==1.30.0. Verified thatbase_llm_flow.pyis byte-identical onv1.32.0and onmainat the time of writing, so the bug is present on current released versions.Related
The same yield site at lines 1126-1130 also produces
tool_confirmation_eventfor HITL confirmations with the samelong_running_tool_idsshape and the same termination gap. The accompanying PR scopes toauth_eventonly; the HITL case is a likely sibling that would benefit from a similar fix.