Skip to content

Interactions API in single_turn sub-agent result dropped as orphan ("Dropping function responses with no matching function call")聽#7349

Description

@Dumeng

馃敶 Required Information

Please ensure all items in this section are completed to allow for efficient
triaging. Requests without complete information may be rejected / deprioritized.
If an item is not applicable to you - please mark it as N/A

Describe the Bug:
With Gemini(use_interactions_api=True), when a chat agent delegates to a mode="single_turn" sub-agent, the parent's next request drops the delegation's function_response.

ADK logs:

WARNING google_adk...tools._rearranger: Dropping function responses with no matching function call: ['call_root_1']

The Interactions delta then carries only the sub-agent's quoted final text. The server-side interaction is left with an unanswered function_call, and anything an after_tool_callback attached to the result is lost.

Steps to Reproduce:

import asyncio
from collections.abc import AsyncGenerator

from google.adk.agents import Agent
from google.adk.models.google_llm import Gemini
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.adk.runners import InMemoryRunner
from google.genai import types
from pydantic import PrivateAttr


class Scripted(Gemini):
    use_interactions_api: bool = True
    _script: list[LlmResponse] = PrivateAttr(default_factory=list)
    _requests: list[LlmRequest] = PrivateAttr(default_factory=list)

    async def generate_content_async(
        self, llm_request: LlmRequest, stream: bool = False
    ) -> AsyncGenerator[LlmResponse, None]:
        self._requests.append(llm_request.model_copy(deep=True))
        yield self._script.pop(0)


def turn(iid: str, part: types.Part) -> LlmResponse:
    return LlmResponse(content=types.Content(role="model", parts=[part]), interaction_id=iid)


async def main() -> None:
    child_model = Scripted(model="gemini-3-flash")
    child_model._script = [turn("c1", types.Part(text="child answer"))]
    child = Agent(name="child", description="d", mode="single_turn", model=child_model)

    root_model = Scripted(model="gemini-3-flash")
    root_model._script = [
        turn("r1", types.Part(function_call=types.FunctionCall(
            id="call_root_1", name="child", args={"request": "do it"}))),
        turn("r2", types.Part(text="done")),
    ]
    root = Agent(name="root", model=root_model, sub_agents=[child])

    runner = InMemoryRunner(agent=root, app_name="repro")
    session = await runner.session_service.create_session(app_name="repro", user_id="u")
    async for _ in runner.run_async(
        user_id="u", session_id=session.id,
        new_message=types.Content(role="user", parts=[types.Part(text="hi")]),
    ):
        pass

    # Expected: [..., function_call(call_root_1), function_response(call_root_1)]
    # Actual:   [user: "For context: ... [child] said: child answer"]
    for c in root_model._requests[1].contents:
        print(c.role, [p.function_response or p.function_call or p.text for p in c.parts])


asyncio.run(main())

Environment Details:

  • ADK Library Version (pip show google-adk): 2.10.0
  • Desktop OS: Linux
  • Python Version (python -V): 3.14.5

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: gemini-3.8-flash

馃煛 Optional Information

Providing this information greatly speeds up the resolution process.

Regression:
N/A

Logs:
See above

Screenshots / Video:
N/A

Additional Context:
Add any other context about the problem here.

Minimal Reproduction Code:
See above

How often has this issue occurred?:

Always (100%)

Potential root cause

  1. _SingleTurnAgentTool.run_async runs the child with tool_context.run_node(..., override_branch="<agent>@<fc_id>") but no override_isolation_scope. So the child's events (including its synthetic author="user" input event) are unscoped and visible to the parent. They land in session.events between the parent's function_call and its function_response.
  2. On the Interactions path, _get_current_turn_contents scans backward for the turn start. It only keeps scanning past a function_response whose author is user (_is_submitted_tool_result). The parent's result is authored by the parent agent, so the scan stops at the child's last event.
  3. The parent's function_call falls outside the slice, so drop_orphaned_function_responses removes the result.

Task mode doesn't hit this, because _dispatch_task_fc passes override_isolation_scope=fc.id and _synthesize_task_fr_event authors the result as user.

Suggested fix (either)

  • In _get_current_turn_contents, add every event's function_response ids to unmatched_response_ids, not only user-submitted ones. That's what the invariant in the comment above the loop requires ("the slice must reach back far enough to include the call it answers"). Verified locally: the slice becomes [user, function_call, function_response] and the Interactions delta is exactly the function_response.
  • Or run _SingleTurnAgentTool children under override_isolation_scope=fc_id, as task mode does.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions