Skip to content

Python: [Bug]: Gemini streaming loses the final answer after a tool call #9490

Closed

Description

Observed Behavior

With stream=True, GeminiChatClient creates every ChatResponseUpdate without a role (RawGeminiChatClient._process_chunk in python/packages/gemini/agent_framework_gemini/_chat_client.py).

When the model calls a tool, the function-invocation layer adds the tool result as a role="tool" update. The text update that Gemini streams next has no role, so it is appended to that tool message instead of starting a new assistant message. The final answer therefore ends up inside a tool message.

When the history is sent back to Gemini, the request builder keeps only function_result content from tool messages, so the answer is dropped without a warning. On the next turn Gemini never sees what it answered before.

The non-streaming path already builds its message with role="assistant", and the Anthropic, Bedrock, Mistral and Ollama clients set role="assistant" on every streamed update.

Expected Behavior

Streaming gives the same messages as non-streaming: assistant (function_call), tool (function_result), assistant (text). On the next turn the answer is sent back to Gemini as a model turn.

Steps to Reproduce

  1. Create an Agent with GeminiChatClient and a function tool.
  2. Run it with stream=True on a prompt where Gemini calls the tool and then answers.
  3. Look at the messages in the final response: the answer is inside the tool message.
  4. Ask a follow-up question in the same session: the request sent to Gemini has no model turn with the earlier answer.

Minimal Reproduction

The script uses a mocked Gemini SDK client that returns real google.genai response objects, so it needs no API key.

import asyncio
from unittest.mock import MagicMock

from agent_framework import Agent, tool
from agent_framework.gemini import GeminiChatClient
from google.genai import types


@tool
def get_weather(city: str) -> str:
    """Get the weather for a city."""
    return f"Sunny in {city}"


async def gemini_stream(part: types.Part):
    yield types.GenerateContentResponse(
        candidates=[types.Candidate(content=types.Content(role="model", parts=[part]), finish_reason="STOP")],
        model_version="gemini-2.5-flash",
    )


async def main() -> None:
    # Scripted Gemini replies: a tool call, the answer, then the reply to the follow-up question.
    replies = [
        gemini_stream(types.Part(function_call=types.FunctionCall(id="call-1", name="get_weather", args={"city": "Paris"}))),
        gemini_stream(types.Part(text="It is sunny in Paris.")),
        gemini_stream(types.Part(text="You asked about Paris.")),
    ]
    requests: list[list[types.Content]] = []

    async def generate_content_stream(*, contents: list[types.Content], **kwargs):
        requests.append(contents)
        return replies.pop(0)

    genai_client = MagicMock()
    genai_client._api_client.vertexai = False
    genai_client._api_client._http_options.base_url = "https://generativelanguage.googleapis.com/"
    genai_client.aio.models.generate_content_stream = generate_content_stream

    agent = Agent(client=GeminiChatClient(client=genai_client, model="gemini-2.5-flash"), tools=[get_weather])
    session = agent.create_session()

    stream = agent.run("What's the weather in Paris?", session=session, stream=True)
    async for _ in stream:
        pass
    response = await stream.get_final_response()
    print("Turn 1 messages:")
    for message in response.messages:
        print(" ", message.role, [content.type for content in message.contents])

    stream = agent.run("Which city did I ask about?", session=session, stream=True)
    async for _ in stream:
        pass
    print("Turn 2 request sent to Gemini:")
    for content in requests[-1]:
        print(" ", content.role, [part.text or (part.function_call and "function_call") or "function_response" for part in content.parts])


asyncio.run(main())

Output on main:

Turn 1 messages:
  assistant ['function_call']
  tool ['function_result', 'text']
Turn 2 request sent to Gemini:
  user ["What's the weather in Paris?"]
  model ['function_call']
  user ['function_response']
  user ['Which city did I ask about?']

The answer "It is sunny in Paris." is inside the tool message in turn 1, and it is missing from the turn 2 request.

Error Messages and Stack Traces

No response

Package Versions

agent-framework-core: 1.22.0, agent-framework-gemini: 1.0.0b261015 (source checkout of main at 44fe223), google-genai: 2.27.0

Python Version

Python 3.13

Operating System

Linux

Regression

No

Additional Context

Acknowledgements

  • I searched existing issues and did not find a duplicate.
  • I personally verified this behavior and the reproduction details are authentic.
  • I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 18, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Oct 18, 2026
  3. github-actions commented on Oct 18, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    python/packages/gemini/agent_framework_gemini/_chat_client.py::RawGeminiChatClient._process_chunk emits updates without an assistant role; after a tool result, subsequent streamed text aggregates into the tool message and _prepare_gemini_messages omits it from replay. Minimal repro: mock consecutive Gemini streams containing a function call and final text, finalize get_response(..., stream=True), then submit a follow-up using final.messages; the regression test observes ['assistant', 'tool'] instead of ['assistant', 'tool', 'assistant'] and no replayed model text.

    • Failing test: python/packages/gemini/tests/test_gemini_client.py::test_streaming_answer_after_tool_call_stays_in_history
    • Files examined: python/packages/gemini/agent_framework_gemini/_chat_client.py, python/packages/gemini/tests/test_gemini_client.py, python/packages/gemini/pyproject.toml, docs/specs/004-python-function-calling-loop.md
    • Tests run: python/packages/gemini/tests/test_gemini_client.py::test_streaming_get_final_response, python/packages/gemini/tests/test_gemini_client.py::test_streaming_answer_after_tool_call_stays_in_history
    • Reported version: agent-framework-gemini 1.0.0b261015
    • Current version: agent-framework-gemini 1.0.0b261015
  4. added
    agentsUsage: [Issues, PRs], Target: Single agent
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions