From 28a8dbf9ed21bca285dfc5026ebb22e0896ca137 Mon Sep 17 00:00:00 2001 From: Copilot Date: Mon, 23 Mar 2026 17:18:37 +0000 Subject: [PATCH 1/5] Fix _add_text_reasoning_content dropping id during coalescing (#4852) Preserve the id field (rs_* identifier) when coalescing text_reasoning Content objects by passing id=self.id or other.id to the Content constructor. This fixes the encrypted reasoning round-trip where the missing id prevented _prepare_content_for_openai from including it in the serialized reasoning item. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../packages/core/agent_framework/_types.py | 1 + python/packages/core/tests/core/test_types.py | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index a4e3a57330..1f73f770bd 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -1390,6 +1390,7 @@ def _add_text_reasoning_content(self, other: Content) -> Content: return Content( "text_reasoning", + id=self.id or other.id, text=combined_text, protected_data=protected_data, annotations=_combine_annotations(self.annotations, other.annotations), diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index 1cde898787..b4609a55db 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -1526,6 +1526,44 @@ def test_text_reasoning_content_iadd_coverage(): assert t1.text == "Thinking 1 Thinking 2" + +def test_text_reasoning_content_add_preserves_id(): + """Test that coalescing text_reasoning Content preserves the id field.""" + + t1 = Content.from_text_reasoning(id="rs_abc123", text="Thinking part 1") + t2 = Content.from_text_reasoning(id="rs_abc123", text=" part 2") + + result = t1 + t2 + assert result.text == "Thinking part 1 part 2" + assert result.id == "rs_abc123" + + +def test_text_reasoning_content_add_id_fallback_to_other(): + """Test that coalescing falls back to other's id when self has no id.""" + + t1 = Content.from_text_reasoning(text="Thinking part 1") + t2 = Content.from_text_reasoning(id="rs_abc123", text=" part 2") + + result = t1 + t2 + assert result.id == "rs_abc123" + + +def test_text_reasoning_content_add_preserves_id_with_encrypted_content(): + """Test that id and encrypted_content both survive coalescing for round-trip.""" + + t1 = Content.from_text_reasoning( + id="rs_abc123", + text="Thinking", + additional_properties={"encrypted_content": "enc_blob_data"}, + ) + t2 = Content.from_text_reasoning(id="rs_abc123", text=" more") + + result = t1 + t2 + assert result.text == "Thinking more" + assert result.id == "rs_abc123" + assert result.additional_properties.get("encrypted_content") == "enc_blob_data" + + def test_comprehensive_to_dict_exclude_options(): """Test to_dict methods with various exclude options for better coverage.""" From b01ac96f5f116aca5f5364ab78189efca48438bf Mon Sep 17 00:00:00 2001 From: Copilot Date: Mon, 23 Mar 2026 17:21:45 +0000 Subject: [PATCH 2/5] Python: Fix `_add_text_reasoning_content` to preserve `id` during coalescing Fixes #4852 --- python/packages/core/tests/core/test_types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index b4609a55db..b3c13920dc 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -1526,7 +1526,6 @@ def test_text_reasoning_content_iadd_coverage(): assert t1.text == "Thinking 1 Thinking 2" - def test_text_reasoning_content_add_preserves_id(): """Test that coalescing text_reasoning Content preserves the id field.""" From 1a7c4e01ac14b1cbad4b13e91e6d8dc2ea102904 Mon Sep 17 00:00:00 2001 From: Copilot Date: Mon, 23 Mar 2026 17:27:32 +0000 Subject: [PATCH 3/5] Raise AdditionItemMismatch on conflicting text_reasoning ids (#4852) Detect when both operands have different non-empty ids during text_reasoning Content coalescing and raise AdditionItemMismatch instead of silently keeping one. This prevents mis-associating encrypted_content during round-trips. Also adds tests for conflicting ids and the neither-has-id edge case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../packages/core/agent_framework/_types.py | 7 +++++- python/packages/core/tests/core/test_types.py | 23 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 1f73f770bd..0ba54d4ba0 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -1380,6 +1380,11 @@ def _add_text_content(self, other: Content) -> Content: def _add_text_reasoning_content(self, other: Content) -> Content: """Add two TextReasoningContent instances.""" + # Ensure we do not silently merge contents with conflicting ids + if self.id and other.id and self.id != other.id: + raise AdditionItemMismatch(f"Cannot add text_reasoning content with different ids: {self.id!r} != {other.id!r}") + combined_id = self.id or other.id + # Concatenate text, handling None values self_text = self.text or "" # type: ignore[attr-defined] other_text = other.text or "" # type: ignore[attr-defined] @@ -1390,7 +1395,7 @@ def _add_text_reasoning_content(self, other: Content) -> Content: return Content( "text_reasoning", - id=self.id or other.id, + id=combined_id, text=combined_text, protected_data=protected_data, annotations=_combine_annotations(self.annotations, other.annotations), diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index b3c13920dc..374064aa91 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -43,7 +43,7 @@ add_usage_details, validate_tool_mode, ) -from agent_framework.exceptions import ContentError +from agent_framework.exceptions import AdditionItemMismatch, ContentError @fixture @@ -1563,6 +1563,27 @@ def test_text_reasoning_content_add_preserves_id_with_encrypted_content(): assert result.additional_properties.get("encrypted_content") == "enc_blob_data" +def test_text_reasoning_content_add_conflicting_ids_raises(): + """Test that coalescing text_reasoning Content with different ids raises AdditionItemMismatch.""" + + t1 = Content.from_text_reasoning(id="rs_abc123", text="Thinking part 1") + t2 = Content.from_text_reasoning(id="rs_xyz789", text=" part 2") + + with pytest.raises(AdditionItemMismatch, match="different ids"): + t1 + t2 + + +def test_text_reasoning_content_add_neither_has_id(): + """Test that coalescing text_reasoning Content when neither has an id results in None id.""" + + t1 = Content.from_text_reasoning(text="Thinking part 1") + t2 = Content.from_text_reasoning(text=" part 2") + + result = t1 + t2 + assert result.text == "Thinking part 1 part 2" + assert result.id is None + + def test_comprehensive_to_dict_exclude_options(): """Test to_dict methods with various exclude options for better coverage.""" From 61f398a587c2ad24aa57fbf063c3f623db01eeb6 Mon Sep 17 00:00:00 2001 From: Copilot Date: Mon, 23 Mar 2026 17:29:01 +0000 Subject: [PATCH 4/5] Address review feedback for #4852: Python: [Bug]: Content._add_text_reasoning_content drops id during coalescing, breaking encrypted reasoning round-trip --- python/packages/core/agent_framework/_types.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 0ba54d4ba0..74ffefdf89 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -1382,7 +1382,9 @@ def _add_text_reasoning_content(self, other: Content) -> Content: """Add two TextReasoningContent instances.""" # Ensure we do not silently merge contents with conflicting ids if self.id and other.id and self.id != other.id: - raise AdditionItemMismatch(f"Cannot add text_reasoning content with different ids: {self.id!r} != {other.id!r}") + raise AdditionItemMismatch( + f"Cannot add text_reasoning content with different ids: {self.id!r} != {other.id!r}" + ) combined_id = self.id or other.id # Concatenate text, handling None values From c5d1d632a879f7328b1092154ffef2d71a1f1d2e Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Tue, 31 Mar 2026 14:46:42 -0700 Subject: [PATCH 5/5] test fix --- .../packages/core/agent_framework/_types.py | 7 +++++- python/packages/core/tests/core/test_types.py | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index afa1e4941f..6d6bc58068 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -1888,7 +1888,12 @@ def _coalesce_text_content(contents: list[Content], type_str: Literal["text", "t if first_new_content is None: first_new_content = deepcopy(content) else: - first_new_content += content + try: + first_new_content += content + except AdditionItemMismatch: + # Different IDs means a new logical segment; flush the current one + coalesced_contents.append(first_new_content) + first_new_content = deepcopy(content) else: # skip this content, it is not of the right type # so write the existing one to the list and start a new one, diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index 016e91fd09..9d1b39f29e 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -1584,6 +1584,30 @@ def test_text_reasoning_content_add_neither_has_id(): assert result.id is None +def test_coalesce_text_reasoning_with_different_ids(): + """Test that _coalesce_text_content keeps separate text_reasoning items when IDs differ. + + Regression test: streaming responses can produce multiple text_reasoning + segments with distinct IDs. These must not be merged into one. + """ + from agent_framework._types import _coalesce_text_content + + contents = [ + Content.from_text_reasoning(id="rs_aaa", text="Thinking A1"), + Content.from_text_reasoning(id="rs_aaa", text=" A2"), + Content.from_text_reasoning(id="rs_bbb", text="Thinking B1"), + Content.from_text_reasoning(id="rs_bbb", text=" B2"), + ] + + _coalesce_text_content(contents, "text_reasoning") + + assert len(contents) == 2 + assert contents[0].id == "rs_aaa" + assert contents[0].text == "Thinking A1 A2" + assert contents[1].id == "rs_bbb" + assert contents[1].text == "Thinking B1 B2" + + def test_comprehensive_to_dict_exclude_options(): """Test to_dict methods with various exclude options for better coverage."""