Repository navigation
Curated history keeps half a user turn when the model turn is invalid #3051
Copy link
Copy link
Open
Labels
priority: p2Moderately-important priority. Fix may not be included in next release.Moderately-important priority. Fix may not be included in next release.status:awaiting user responsetype: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Description
Activity
- addedpriority: p2Moderately-important priority. Fix may not be included in next release.Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.Error or flaw in code with unintended results or allowing sub-optimal usage patterns.and removedpriority: p2Moderately-important priority. Fix may not be included in next release.Moderately-important priority. Fix may not be included in next release.
on Oct 5, 2026 Hi @chrikrah, thanks for all the details! Could you share the exact code snippet of your receive loop? We'd love to try reproducing this on our end using the exact logic you have set up.
Thanks- added 2 commits that reference this issue
on Oct 9, 2026 @Venkaiahbabuneelam, there is no receive loop. The history is built by hand and passed to
chats.create, and no request is sent, so a placeholder key works:from google import genai from google.genai import types hist = [ types.Content(role="user", parts=[types.Part(text="what is the weather")]), types.Content(role="user", parts=[ types.Part.from_function_response(name="get_weather", response={"c": 21})]), types.Content(role="model", parts=[]), # invalid model turn, e.g. a blocked response ] client = genai.Client(api_key="not-a-real-key") # no request is sent chat = client.chats.create(model="gemini-2.0-flash", history=hist) print("curated:", [(c.role, len(c.parts)) for c in chat.get_history(curated=True)]) print("comprehensive:", [(c.role, len(c.parts)) for c in chat.get_history()])
$ python -I repro.py curated: [('user', 1)] comprehensive: [('user', 1), ('user', 1), ('model', 0)] # google-genai 2.29.0 from PyPI, CPython 3.12.3, Linux x86_64, run 2026-10-10The question stays and its function response goes. One wider case: with a model
function_callturn before the response, curated history ends on that unanswered call, so walking back over the user run (#3092) does not cover it. Does the script print the same for you?
Metadata
Metadata
Assignees
Labels
priority: p2Moderately-important priority. Fix may not be included in next release.Moderately-important priority. Fix may not be included in next release.status:awaiting user responsetype: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Title:
_extract_curated_historydrops only the last user content when a model turn is invalidAn invalid model turn makes
_extract_curated_historycallcurated_history.pop()once (chats.py:107-108). A user turn of two or moreContentobjects went in one at a time, so onepopremoves part of it. The rest stays in the curated history with no model answer behind it.The way in is
chats.create(history=...), the only caller of that function (chats.py:136): a caller persisting raw exchanges stores one content per function response, and a blocked model turn as an empty content.At
618f0aa, through the public constructor:The question survives and the tool result behind it disappears, so the next
send_messagesends a user turn the model already failed on, minus its evidence.Two of those tests bracket the gap.
test_history_with_consecutive_valid_user_inputs(test_get_history.py:152) makes back-to-back user contents supported input, andtest_history_with_invalid_turns(:401) sets the intent that the whole rejected user turn goes. Onepopmeets that intent only where the turn is a single content.There is no CODEOWNERS here and every merge on GitHub is the Copybara import, so @jaycee-li, I am going by your last commit on this file. Should the pop walk back over the whole user run? I can write the diff out here if that helps the internal fix.