Curated chat history ends on an unanswered function_call when the turn after AFC is invalid

Aberta
#3,314 0 comentários 0 reações 0 responsáveis Ver no GitHub

Mantenedores costumam responder em até 1 dia

@copybara-service já está trabalhando nisso.

Desde 8/11/2026.

  • #3315 de @copybara-service — aberto

Avaliação

Dificuldade
4/5
Tempo estimado
3-5 dias
Facilidade para iniciantes
48/100
Tipo de issue
Bug
Clareza
Razoavelmente clara
Status de atividade
Ativa
Stack de tecnologia
python

Direção de pesquisa

Read _BaseChat.__init__ and trace _extract_curated_history plus the four send_message variants that handle AFC. Compare their history truncation with record_history and the behavior discussed in #3290; start with tests/chats/test_send_message.py using the replay setup. Done means the curated history and next request handle an invalid turn after a function call consistently, with tests covering the exchange.

Escrita pelo modelo de indexação a partir do texto da issue.

Descrição

When AFC runs a tool in Chat.send_message and the next model turn is invalid, curated history keeps the function_call and drops the computed function_response.

Dropping the response matches the _BaseChat.__init__ docstring (invalid turns "and their associated inputs"). The call is my question: 2580638 ends a turn on one on purpose when the budget is spent, before any function runs. Here it ran. A hand-built history passed to chats.create curates to the same tail, and #3290 leaves both.

Environment details
  • Programming language: Python
  • OS: Linux x86_64
  • Language runtime version: CPython 3.12.14
  • Package version: google-genai 3.0.0, main at afb10e8 (2026-11-07), also #3290 head 3eaeefa
Steps to reproduce
  1. Run the script below. The stubbed generate_content returns a function_call, then an empty SAFETY candidate.
# afc_tail.py
from unittest import mock
from google import genai
from google.genai import types

def get_weather(city: str) -> dict:
    """Returns the weather for a city."""
    return {"c": 21}

def kinds(history):
    return [c.role + ":" + ("+".join(k for p in c.parts or [] for k in ("text", "function_call", "function_response")
                                     if getattr(p, k) is not None) or "empty") for c in history]

call = types.GenerateContentResponse(candidates=[types.Candidate(content=types.Content(
    role="model", parts=[types.Part.from_function_call(name="get_weather", args={"city": "Paris"})]))])
blocked = types.GenerateContentResponse(candidates=[types.Candidate(finish_reason="SAFETY")])
ok = types.GenerateContentResponse(candidates=[types.Candidate(content=types.Content(role="model", parts=[types.Part(text="ok")]))])

client = genai.Client(api_key="not-a-real-key")
chat = client.chats.create(model="gemini-3.8-flash", config=types.GenerateContentConfig(tools=[get_weather]))
sent = []
def fake(*, model, contents, config):
    sent.append(list(contents))
    return [call, blocked, ok][len(sent) - 1]
with mock.patch.object(chat._modules, "generate_content", side_effect=fake):
    chat.send_message("what is the weather in Paris")
    print("comprehensive:", kinds(chat.get_history()))
    print("curated:      ", kinds(chat.get_history(curated=True)))
    chat.send_message("try again")
    print("next request: ", kinds(sent[-1]))
$ python afc_tail.py      # main afb10e8; the same three lines at #3290 head 3eaeefa
comprehensive: ['user:text', 'model:function_call', 'user:function_response', 'model:empty']
curated:       ['user:text', 'model:function_call']
next request:  ['user:text', 'model:function_call', 'user:text']

With a trial truncation in sync send_message back to the curated length before the AFC loop:

$ python afc_tail.py      # main afb10e8 plus that truncation in Chat.send_message
curated:       []
next request:  ['user:text']
# not run: the live API, so I have not measured how the server answers that next request
# not run: tests/chats/test_send_message.py, which needs the replay setup

I would drop the whole exchange, as record_history already does with an invalid turn's input. The cost is that a retry may run the tool again. That needs the truncation in all four send_message variants, and _extract_curated_history popping through the call when it pops a function response. Is the tail intended once the function ran, or should the exchange go?

Linguagem predominante
Python
Estrelas
4.1k
Forks
1.1k
Merge médio
1d 20h
PRs com merge (30d)
99

Preparar o ambiente

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de googleapis/python-genai

Todas as issues de googleapis/python-genai

Issues semelhantes

Mais issues de Python

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.