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

Đang mở Phù hợp với người mới
#5,595 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
2/5
Thời gian dự kiến
1-3 giờ
Mức phù hợp với người mới
85/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
python
Lĩnh vực
backend-api-design

Hướng nghiên cứu

Đọc src/agents/models/chatcmpl_converter.py, đặc biệt là Converter.maybe_easy_input_message, và so sánh các trường thông điệp được chấp nhận với trường name được khôi phục bởi _parse_summary_line trong src/agents/handoffs/history.py. Thêm một kiểm thử hồi quy cho mục lịch sử handoff lồng nhau có tên người nói được chuyển tiếp qua handoff_history_mapper đến một mô hình Chat Completions. Hoàn tất khi có thể tái hiện lỗi thành công và các kiểm thử converter hiện có đều đạt.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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.

Ngôn ngữ chính
Python
Star
30.5k
Fork
5k
Merge trung bình
16 giờ 15 phút
Pull request đã merge (30 ngày)
154

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của openai/openai-agents-python

Tất cả issue của openai/openai-agents-python

Issue tương tự

Thêm issue về Python

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.