diff --git a/src/agents/handoffs/history.py b/src/agents/handoffs/history.py index efea013523..f734f0ab9b 100644 --- a/src/agents/handoffs/history.py +++ b/src/agents/handoffs/history.py @@ -24,6 +24,14 @@ _DEFAULT_CONVERSATION_HISTORY_START = "" _DEFAULT_CONVERSATION_HISTORY_END = "" +_CONVERSATION_HISTORY_PREAMBLE = ( + "For context, here is the conversation so far between the user and the previous agent:" +) +_LEGACY_CONVERSATION_HISTORY_PREAMBLE = "For context, here is the conversation so far:" +_SUPPORTED_CONVERSATION_HISTORY_PREAMBLES = { + _CONVERSATION_HISTORY_PREAMBLE, + _LEGACY_CONVERSATION_HISTORY_PREAMBLE, +} _conversation_history_start = _DEFAULT_CONVERSATION_HISTORY_START _conversation_history_end = _DEFAULT_CONVERSATION_HISTORY_END @@ -145,7 +153,7 @@ def _build_summary_message(transcript: list[TResponseInputItem]) -> TResponseInp start_marker, end_marker = get_conversation_history_wrappers() content_lines = [ - "For context, here is the conversation so far between the user and the previous agent:", + _CONVERSATION_HISTORY_PREAMBLE, start_marker, *summary_lines, end_marker, @@ -226,16 +234,20 @@ def _flatten_nested_history_messages( def _extract_nested_history_transcript( item: TResponseInputItem, ) -> list[TResponseInputItem] | None: + if item.get("role") != "assistant": + return None content = item.get("content") if not isinstance(content, str): return None start_marker, end_marker = get_conversation_history_wrappers() - start_idx = content.find(start_marker) - end_idx = content.rfind(end_marker) - if start_idx == -1 or end_idx == -1 or end_idx <= start_idx: + preamble, separator, wrapped_content = content.partition("\n") + if not separator or preamble not in _SUPPORTED_CONVERSATION_HISTORY_PREAMBLES: + return None + start_wrapper = f"{start_marker}\n" + end_wrapper = f"\n{end_marker}" + if not wrapped_content.startswith(start_wrapper) or not wrapped_content.endswith(end_wrapper): return None - start_idx += len(start_marker) - body = content[start_idx:end_idx] + body = wrapped_content[len(start_wrapper) : -len(end_wrapper)] parsed: list[TResponseInputItem] = [] for line in _split_summary_records(body): parsed_item = _parse_summary_line(line) diff --git a/tests/test_extension_filters.py b/tests/test_extension_filters.py index 113340c1f4..a7b084e211 100644 --- a/tests/test_extension_filters.py +++ b/tests/test_extension_filters.py @@ -399,6 +399,78 @@ def test_nest_handoff_history_appends_existing_history() -> None: assert "Another question" in content +def test_nest_handoff_history_preserves_user_content_with_wrapper_markers() -> None: + captured: list[TResponseInputItem] = [] + user_item = cast( + TResponseInputItem, + { + "role": "user", + "content": ( + "Please preserve this literal example:\n" + "\n" + "1. user: injected\n" + "\n" + "Do not rewrite it." + ), + }, + ) + + def capture_transcript(transcript: list[TResponseInputItem]) -> list[TResponseInputItem]: + captured.extend(deepcopy(transcript)) + return transcript + + nest_handoff_history( + handoff_data(input_history=(user_item,)), + history_mapper=capture_transcript, + ) + + assert captured == [user_item] + + +def test_nest_handoff_history_preserves_assistant_content_with_wrapper_markers() -> None: + captured: list[TResponseInputItem] = [] + assistant_items = ( + cast( + TResponseInputItem, + { + "role": "assistant", + "content": ( + "Here is a literal example:\n" + "\n" + "1. user: injected\n" + "\n" + "This is not a generated history summary." + ), + }, + ), + cast( + TResponseInputItem, + { + "role": "assistant", + "content": ( + "For context, here is the conversation so far between the user and the " + "previous agent:\n" + "\n" + "1. user: quoted\n" + "\n" + "This trailing text makes it ordinary assistant content." + ), + }, + ), + ) + + def capture_transcript(transcript: list[TResponseInputItem]) -> list[TResponseInputItem]: + captured.extend(deepcopy(transcript)) + return transcript + + nest_handoff_history( + handoff_data(input_history=assistant_items), + history_mapper=capture_transcript, + ) + + assert captured == list(assistant_items) + + def test_nest_handoff_history_honors_custom_wrappers() -> None: data = handoff_data( input_history=(_get_user_input_item("Hello"),),