Curated history keeps half a user turn when the model turn is invalid

Open Beginner friendly
#3,051 3 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@Venkaiahbabuneelam is already working on this.

Since Oct 5, 2026.

  • #3092 by @DavidLeeGarrett — closed without merging
  • #3290 by @copybara-service — open

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
62/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
api

Research direction

Start at _extract_curated_history in chats.py, specifically the single curated_history.pop() at lines 107-108, reached through chats.create(history=...) at line 136. Read google/genai/tests/chats/test_get_history.py, especially test_history_with_consecutive_valid_user_inputs (line 152) and test_history_with_invalid_turns (line 401), which bracket the intended behavior. Reproduce with the multi-content user turn plus empty model turn shown in the issue. Done means an invalid model turn drops the whole preceding user run, not one content, and the get-history suite passes.

Written by the indexing model from the issue text.

Description

priority: p2 type: bug

Title: _extract_curated_history drops only the last user content when a model turn is invalid

An invalid model turn makes _extract_curated_history call curated_history.pop() once (chats.py:107-108). A user turn of two or more Content objects went in one at a time, so one pop removes 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:

$ /var/tmp/p9bL16/genai/.venv/bin/python probe_hist2.py
import: /var/tmp/p9bL16/genai/google/genai/__init__.py
curated: [('user', 1)]
comprehensive: [('user', 1), ('user', 1), ('model', 0)]
hist = [
    C(role="user", parts=[P(text="what is the weather")]),
    C(role="user", parts=[P.from_function_response(name="get_weather", response={"c": 21})]),
    C(role="model", parts=[]),          # invalid model turn
]
chat = client.chats.create(model="gemini-2.0-flash", history=hist)

The question survives and the tool result behind it disappears, so the next send_message sends a user turn the model already failed on, minus its evidence.

$ python -m pytest google/genai/tests/chats/test_get_history.py -q
17 passed, 1 warning in 0.06s
# not run: the replay suites, which need recorded sessions

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, and test_history_with_invalid_turns (:401) sets the intent that the whole rejected user turn goes. One pop meets 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.

Dominant language
Python
Stars
4.1k
Forks
1.1k
Avg merge
1d 20h
Merged PRs (30d)
90

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from googleapis/python-genai

All issues in googleapis/python-genai

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.