Skip to content

Nested handoff history flattening drops multiline and structured content #3319

Description

@Aphroq

Please read this first

  • Have you read the docs? Yes. This is about the SDK's nest_handoff_history behavior during chained handoffs.
  • Have you searched for related issues? Yes. No duplicate was found.

Describe the bug

When a nest_handoff_history summary is passed through another nested handoff, _extract_nested_history_transcript reconstructs the prior transcript by splitting the summary block into lines and parsing role: content text.

That parsing loses information:

  • Multiline string content is truncated to the first line.
  • Structured role content such as a list of input content parts is converted into a plain string.

Impact: chained handoffs and custom handoff_history_mapper implementations can receive a corrupted transcript. User content after the first newline is dropped, and structured input parts such as multimodal content descriptors no longer have their original shape.

Debug information

  • Agents SDK version: upstream main at bc3607ba
  • Python version: Python 3.12.1

Repro steps

Run this script against current upstream main:

from __future__ import annotations

import json

from agents.handoffs import HandoffInputData, nest_handoff_history
from agents.items import TResponseInputItem


def capture_second_pass(item: TResponseInputItem) -> list[TResponseInputItem]:
    captured: list[TResponseInputItem] = []

    def mapper(transcript: list[TResponseInputItem]) -> list[TResponseInputItem]:
        captured.extend(transcript)
        return transcript

    first = HandoffInputData(input_history=(item,), pre_handoff_items=(), new_items=())
    first_nested = nest_handoff_history(first)
    second = HandoffInputData(
        input_history=first_nested.input_history,
        pre_handoff_items=(),
        new_items=(),
    )
    nest_handoff_history(second, history_mapper=mapper)
    return captured


multiline = capture_second_pass(
    {"role": "user", "content": "first line\nsecond line"}
)
structured = capture_second_pass(
    {
        "role": "user",
        "content": [
            {"type": "input_text", "text": "look at this"},
            {"type": "input_image", "image_url": "https://example.com/image.png"},
        ],
    }
)

print("multiline:")
print(json.dumps(multiline, ensure_ascii=False, indent=2))
print("structured_content_type:", type(structured[0].get("content")).__name__)
print("structured:")
print(json.dumps(structured, ensure_ascii=False, indent=2))

Actual result:

multiline:
[
  {
    "role": "user",
    "content": "first line"
  }
]
structured_content_type: str
structured:
[
  {
    "role": "user",
    "content": "[{\"type\": \"input_text\", \"text\": \"look at this\"}, {\"type\": \"input_image\", \"image_url\": \"https://example.com/image.png\"}]"
  }
]

Expected behavior

Flattening an already nested handoff summary should preserve transcript item content across chained handoffs:

  • Multiline content should remain "first line\nsecond line".
  • Structured content should remain a list, not be converted to a string.
  • Legacy simple summary lines should continue to parse for existing saved histories.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions