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

未关闭 适合新手
#5,595 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
2/5
预计耗时
1-3 小时
新手友好度
85/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
python

调研方向

阅读 src/agents/models/chatcmpl_converter.py,尤其是 Converter.maybe_easy_input_message,并比较它接受的消息字段与 src/agents/handoffs/history.py 中由 _parse_summary_line 恢复的 name 字段。为一个嵌套的 handoff 历史项添加回归测试,其中说话者名称通过 handoff_history_mapper 转发给 Chat Completions 模型。复现成功且现有 converter 测试通过后即为完成。

由索引模型根据 Issue 内容生成。

描述

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.

主要语言
Python
星标
30.5k
派生
5k
平均合并
16 小时 15 分钟
30 天内合并 PR
154

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

openai/openai-agents-python 的其他 Issue

查看 openai/openai-agents-python 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。