Repository navigation
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
- Create an
AgentwithGeminiChatClientand a function tool. - Run it with
stream=Trueon a prompt where Gemini calls the tool and then answers. - Look at the messages in the final response: the answer is inside the
toolmessage. - Ask a follow-up question in the same session: the request sent to Gemini has no
modelturn 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
- Also reproduced on Windows. The role has been missing from streamed updates since
GeminiChatClientwas added in Python: Add GeminiChatClient #4847. Without streaming, the same agent returnsassistant,tool,assistantas expected. - This is the same problem Python: [Bug]: Anthropic Streaming agent fails with HTTP 400 on consecutive tool calls: tool_use blocks can only be in assistant messages #4328 reported for Anthropic, which Python: fix(anthropic): set role='assistant' on message_start streaming update #4329 fixed by setting
role="assistant"on the streamed update. - Suggested fix: pass
role="assistant"to theChatResponseUpdatebuilt in_process_chunk. Since this is a one-line correction (the same fix as Python: fix(anthropic): set role='assistant' on message_start streaming update #4329), I'm opening a PR with it and regression tests right after this issue. - AI assistance: I used Claude Code as an assistant to find this bug and to draft this report. I reviewed this report myself and checked the reproduction and its output above.
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.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
github-actions commentedon Oct 18, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
python/packages/gemini/agent_framework_gemini/_chat_client.py::RawGeminiChatClient._process_chunkemits updates without an assistant role; after a tool result, subsequent streamed text aggregates into the tool message and_prepare_gemini_messagesomits it from replay. Minimal repro: mock consecutive Gemini streams containing a function call and final text, finalizeget_response(..., stream=True), then submit a follow-up usingfinal.messages; the regression test observes['assistant', 'tool']instead of['assistant', 'tool', 'assistant']and no replayed model text.python/packages/gemini/tests/test_gemini_client.py::test_streaming_answer_after_tool_call_stays_in_historyagent-framework-gemini 1.0.0b261015agent-framework-gemini 1.0.0b261015