Skip to content

Tool-level auth continues invocation past the EUC event #5637

@doughayden

Description

@doughayden

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.

Metadata

Metadata

Labels

core[Component] This issue is related to the core interface and implementation

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions