Nested handoff history: speaker names in summary records break Chat Completions agents when a handoff_history_mapper forwards the transcript

Abierto Apto para principiantes
#5,595 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
85/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
python

Línea de trabajo

Lee src/agents/models/chatcmpl_converter.py, especialmente Converter.maybe_easy_input_message, y compara los campos de mensaje aceptados con el campo name restaurado por _parse_summary_line en src/agents/handoffs/history.py. Añade una prueba de regresión para un elemento anidado del historial de handoff con un nombre de hablante reenviado a un modelo de Chat Completions mediante handoff_history_mapper. El trabajo estará terminado cuando la reproducción funcione y pasen las pruebas existentes del converter.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Please read this first
  • Have you read the docs? Agents SDK docs Yes
  • Have you searched for related issues? Yes. Related but different: #4230 (keeps empty turns in nested history), #3319 and #3814 (other nested-history parsing issues).
Describe the bug

With RunConfig(nest_handoff_history=True), conversation history is carried in <CONVERSATION HISTORY> summary records, and the next handoff parses those records back into input items. The record format includes an optional speaker name, role (name): content: the summary writer emits it for items that have a name (_format_transcript_item_legacy in src/agents/handoffs/history.py), and _parse_summary_line restores it as a name key, e.g. {'role': 'user', 'name': 'Dana', 'content': '...'}.

When a RunConfig.handoff_history_mapper returns the normalized transcript to the next agent (the documented way to "return the exact list of input items to forward to the next agent"), and that agent uses a Chat Completions model, the run fails before the request is sent:

UserError: Unhandled item type or structure: {'role': 'user', 'name': 'Dana', 'content': 'Hi, I need my subscription history.'}

The same conversation without speaker names works. The cause is that Converter.maybe_easy_input_message (src/agents/models/chatcmpl_converter.py) only accepts the keys content, role, type and phase, so the items produced by the SDK's own parser are rejected, even though Chat Completions messages support a name field.

Debug information
  • Agents SDK version: v0.24.0 (also reproduced on main at 27c75c5)
  • Related library versions (optional, e.g. any-llm, litellm, or pydantic): openai 3.29.0, pydantic 2.12.3
  • Python version: 3.12.13
  • Operating system: Linux (aarch64)
  • Model and model provider: Chat Completions (OpenAIChatCompletionsModel); the repro stubs the HTTP transport
  • Does the issue reproduce with the latest Agents SDK release? Yes (v0.24.0, and main at 27c75c5)
  • Does the issue occur consistently or intermittently? Consistently
without speaker names: Your plan changed in May.
Traceback (most recent call last):
  ...
  File "04_named_history_chat_completions.py", line 40, in run
    result = await Runner.run(agents(), [{"role": "assistant", "content": summary},
  File "agents/run.py", line 352, in run
    return await runner.run(
  File "agents/run.py", line 610, in run
    return await self._run_impl(starting_agent, input, **kwargs)
  File "agents/run.py", line 1909, in _run_impl
    turn_result = await run_single_turn(
  File "agents/run_internal/run_loop.py", line 2883, in run_single_turn
    new_response = await get_new_response(
  File "agents/run_internal/run_loop.py", line 3046, in get_new_response
    new_response = await get_response_with_retry(
  File "agents/run_internal/model_retry.py", line 612, in get_response_with_retry
    response = await _await_model_attempt(get_response(), timeout)
  File "agents/run_internal/model_retry.py", line 296, in _await_model_attempt
    return await awaitable
  File "agents/models/openai_chatcompletions.py", line 249, in get_response
    response = await self._fetch_response(
  File "agents/models/openai_chatcompletions.py", line 621, in _fetch_response
    converted_messages = Converter.items_to_messages(
  File "agents/models/chatcmpl_converter.py", line 1026, in items_to_messages
    raise UserError(f"Unhandled item type or structure: {item}")
agents.exceptions.UserError: Unhandled item type or structure: {'role': 'user', 'name': 'Dana', 'content': 'Hi, I need my subscription history.'}
Repro steps

Self-contained; the model is a stubbed Chat Completions HTTP transport, so no API key or network is needed.

import asyncio

import httpx
from openai import AsyncOpenAI

from agents import Agent, OpenAIChatCompletionsModel, RunConfig, Runner, get_conversation_history_wrappers, handoff
from agents import set_tracing_disabled

set_tracing_disabled(True)


def chat_model(*messages):
    """A Chat Completions model whose HTTP endpoint returns the given assistant messages in order."""
    replies = iter(messages)

    def respond(request):
        return httpx.Response(200, json={"id": "chatcmpl-1", "object": "chat.completion", "created": 0, "model": "gpt-4.1",
                                         "choices": [{"index": 0, "finish_reason": "stop", "message": next(replies)}]})

    client = AsyncOpenAI(api_key="test", http_client=httpx.AsyncClient(transport=httpx.MockTransport(respond)))
    return OpenAIChatCompletionsModel("gpt-4.1", client)


def agents():
    billing = Agent(name="Billing", model=chat_model({"role": "assistant", "content": "Your plan changed in May."}))
    return Agent(name="Triage", handoffs=[handoff(billing)], model=chat_model(
        {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "type": "function", "function": {
            "name": "transfer_to_billing", "arguments": "{}"}}]}))


def earlier(user, assistant):
    """An earlier conversation carried over in the nested-history summary format."""
    start, end = get_conversation_history_wrappers()
    return ("For context, here is the conversation so far between the user and the previous agent:\n"
            f"{start}\n1. {user}: Hi, I need my subscription history.\n2. {assistant}: Sure, let me look into it.\n{end}")


async def run(summary):
    config = RunConfig(nest_handoff_history=True, handoff_history_mapper=lambda transcript: transcript)
    result = await Runner.run(agents(), [{"role": "assistant", "content": summary},
                                         {"role": "user", "content": "When did my plan change?"}], run_config=config)
    return result.final_output


async def main():
    print("without speaker names:", await run(earlier("user", "assistant")))
    print("with speaker names:   ", await run(earlier("user (Dana)", "assistant (Support)")))


asyncio.run(main())
Expected behavior

Items recovered from nested-history summaries can be sent to any supported model: either the Chat Completions converter maps name to the message's name field, or the parser keeps the speaker name in a form the converters accept. Both lines of the script print Your plan changed in May.

Lenguaje dominante
Python
Estrellas
30.5k
Forks
5k
Merge medio
16 h 15 min
PR fusionados (30 d)
154

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de openai/openai-agents-python

Todos los issues de openai/openai-agents-python

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.