From 19dea20e6d8892aae857d5e340871a643ffc59e0 Mon Sep 17 00:00:00 2001 From: ken <39673849+SuperKenVery@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:07:12 +0800 Subject: [PATCH 1/6] Preserve reasoning blocks with OpenRouter --- .../core/agent_framework/openai/_chat_client.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index 940858b670..807a57ef8b 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -234,7 +234,13 @@ def _parse_response_from_openai(self, response: ChatCompletion, chat_options: Ch contents.append(text_content) if parsed_tool_calls := [tool for tool in self._parse_tool_calls_from_openai(choice)]: contents.extend(parsed_tool_calls) - messages.append(ChatMessage(role="assistant", contents=contents)) + messages.append( + ChatMessage( + role="assistant", + contents=contents, + additional_properties={"reasoning_details": getattr(choice.message, "reasoning_details", None)}, + ) + ) return ChatResponse( response_id=response.id, created_at=datetime.fromtimestamp(response.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), @@ -394,6 +400,10 @@ def _prepare_message_for_openai(self, message: ChatMessage) -> list[dict[str, An } if message.author_name and message.role != Role.TOOL: args["name"] = message.author_name + if "reasoning_details" in message.additional_properties and ( + details := message.additional_properties["reasoning_details"] + ): + args["reasoning_details"] = details match content: case FunctionCallContent(): if all_messages and "tool_calls" in all_messages[-1]: From eddc718ccb27cba9400b1d96c5981e51498362f8 Mon Sep 17 00:00:00 2001 From: ken <39673849+SuperKenVery@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:36:39 +0800 Subject: [PATCH 2/6] Put encrypted reasoning in TextReasoningContent --- python/packages/core/agent_framework/_types.py | 5 ++++- .../packages/core/agent_framework/openai/_chat_client.py | 7 ++++++- .../packages/ollama/agent_framework_ollama/_chat_client.py | 3 ++- .../agents/ollama/ollama_agent_reasoning.py | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index ab68382a83..92b0ee8b6b 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -789,8 +789,9 @@ class TextReasoningContent(BaseContent): def __init__( self, - text: str, + text: str | None, *, + protected_data: str | None = None, additional_properties: dict[str, Any] | None = None, raw_representation: Any | None = None, annotations: Sequence[Annotations | MutableMapping[str, Any]] | None = None, @@ -802,6 +803,7 @@ def __init__( text: The text content represented by this instance. Keyword Args: + protected_data: Optional protected reasoning data that needs to be sent back. additional_properties: Optional additional properties associated with the content. raw_representation: Optional raw representation of the content. annotations: Optional annotations associated with the content. @@ -814,6 +816,7 @@ def __init__( **kwargs, ) self.text = text + self.protected_data = protected_data self.type: Literal["text_reasoning"] = "text_reasoning" def __add__(self, other: "TextReasoningContent") -> "TextReasoningContent": diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index 807a57ef8b..dbe5a9be70 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -16,6 +16,8 @@ from openai.types.chat.chat_completion_message_custom_tool_call import ChatCompletionMessageCustomToolCall from pydantic import ValidationError +from agent_framework import TextReasoningContent + from .._clients import BaseChatClient from .._logging import get_logger from .._middleware import use_chat_middleware @@ -234,11 +236,12 @@ def _parse_response_from_openai(self, response: ChatCompletion, chat_options: Ch contents.append(text_content) if parsed_tool_calls := [tool for tool in self._parse_tool_calls_from_openai(choice)]: contents.extend(parsed_tool_calls) + if reasoning_details := getattr(choice.message, "reasoning_details", None): + contents.append(TextReasoningContent(None, protected_data=json.dumps(reasoning_details))) messages.append( ChatMessage( role="assistant", contents=contents, - additional_properties={"reasoning_details": getattr(choice.message, "reasoning_details", None)}, ) ) return ChatResponse( @@ -415,6 +418,8 @@ def _prepare_message_for_openai(self, message: ChatMessage) -> list[dict[str, An args["tool_call_id"] = content.call_id if content.result is not None: args["content"] = prepare_function_call_results(content.result) + case TextReasoningContent(protected_data=protected_data) if protected_data is not None: + all_messages[-1]["reasoning_details"] = json.loads(protected_data) case _: if "content" not in args: args["content"] = [] diff --git a/python/packages/ollama/agent_framework_ollama/_chat_client.py b/python/packages/ollama/agent_framework_ollama/_chat_client.py index d8856143b8..f047a5d4b3 100644 --- a/python/packages/ollama/agent_framework_ollama/_chat_client.py +++ b/python/packages/ollama/agent_framework_ollama/_chat_client.py @@ -239,7 +239,8 @@ def _format_user_message(self, message: ChatMessage) -> list[OllamaMessage]: def _format_assistant_message(self, message: ChatMessage) -> list[OllamaMessage]: text_content = message.text - reasoning_contents = "".join(c.text for c in message.contents if isinstance(c, TextReasoningContent)) + # Ollama shouldn't have encrypted reasoning, so we just process text. + reasoning_contents = "".join((c.text or "") for c in message.contents if isinstance(c, TextReasoningContent)) assistant_message = OllamaMessage(role="assistant", content=text_content, thinking=reasoning_contents) diff --git a/python/samples/getting_started/agents/ollama/ollama_agent_reasoning.py b/python/samples/getting_started/agents/ollama/ollama_agent_reasoning.py index e123ca04b5..e0ce24bb85 100644 --- a/python/samples/getting_started/agents/ollama/ollama_agent_reasoning.py +++ b/python/samples/getting_started/agents/ollama/ollama_agent_reasoning.py @@ -30,7 +30,7 @@ async def reasoning_example() -> None: print(f"User: {query}") # Enable Reasoning on per request level result = await agent.run(query) - reasoning = "".join(c.text for c in result.messages[-1].contents if isinstance(c, TextReasoningContent)) + reasoning = "".join((c.text or "") for c in result.messages[-1].contents if isinstance(c, TextReasoningContent)) print(f"Reasoning: {reasoning}") print(f"Answer: {result}\n") From 341f990d5a7bfec681cc25811da179e802a6c35a Mon Sep 17 00:00:00 2001 From: ken <39673849+SuperKenVery@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:39:57 +0800 Subject: [PATCH 3/6] Remove unneccessary change --- .../packages/core/agent_framework/openai/_chat_client.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index dbe5a9be70..b47555c235 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -238,12 +238,7 @@ def _parse_response_from_openai(self, response: ChatCompletion, chat_options: Ch contents.extend(parsed_tool_calls) if reasoning_details := getattr(choice.message, "reasoning_details", None): contents.append(TextReasoningContent(None, protected_data=json.dumps(reasoning_details))) - messages.append( - ChatMessage( - role="assistant", - contents=contents, - ) - ) + messages.append(ChatMessage(role="assistant", contents=contents)) return ChatResponse( response_id=response.id, created_at=datetime.fromtimestamp(response.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), From 009296565c0ea424812492d8401be997df8c5787 Mon Sep 17 00:00:00 2001 From: ken <39673849+SuperKenVery@users.noreply.github.com> Date: Fri, 19 Dec 2025 18:05:04 +0800 Subject: [PATCH 4/6] Fix docs --- python/packages/core/agent_framework/_types.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 92b0ee8b6b..fe697121e8 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -803,7 +803,16 @@ def __init__( text: The text content represented by this instance. Keyword Args: - protected_data: Optional protected reasoning data that needs to be sent back. + protected_data: This property is used to store data from a provider that should be roundtripped back to the + provider but that is not intended for human consumption. It is often encrypted or otherwise redacted + information that is only intended to be sent back to the provider and not displayed to the user. It's + possible for a TextReasoningContent to contain only `protected_data` and have an empty `text` property. + This data also may be associated with the corresponding `text`, acting as a validation signature for it. + + Note that whereas `text` can be provider agnostic, `protected_data` is provider-specific, and is likely + to only be understood by the provider that created it. The data is often represented as a more complex + object, so it should be serialized to a string before storing so that the whole object is easily + serializable without loss. additional_properties: Optional additional properties associated with the content. raw_representation: Optional raw representation of the content. annotations: Optional annotations associated with the content. From 49db8f2f8177e6bf423252f373eecad6c129b46d Mon Sep 17 00:00:00 2001 From: ken <39673849+SuperKenVery@users.noreply.github.com> Date: Fri, 19 Dec 2025 18:05:41 +0800 Subject: [PATCH 5/6] Support streaming --- python/packages/core/agent_framework/_types.py | 10 ++++++++++ .../core/agent_framework/openai/_chat_client.py | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index fe697121e8..72b69e251a 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -858,6 +858,10 @@ def __add__(self, other: "TextReasoningContent") -> "TextReasoningContent": else: annotations = self.annotations + other.annotations + # Replace protected data. + # Discussion: https://github.com/microsoft/agent-framework/pull/2950#discussion_r2634345613 + protected_data = other.protected_data or self.protected_data + # Create new instance using from_dict for proper deserialization result_dict = { "text": self.text + other.text, @@ -865,6 +869,7 @@ def __add__(self, other: "TextReasoningContent") -> "TextReasoningContent": "annotations": [ann.to_dict(exclude_none=False) for ann in annotations] if annotations else None, "additional_properties": {**(self.additional_properties or {}), **(other.additional_properties or {})}, "raw_representation": raw_representation, + "protected_data": protected_data, } return TextReasoningContent.from_dict(result_dict) @@ -900,6 +905,11 @@ def __iadd__(self, other: "TextReasoningContent") -> Self: self.raw_representation if isinstance(self.raw_representation, list) else [self.raw_representation] ) + (other.raw_representation if isinstance(other.raw_representation, list) else [other.raw_representation]) + # Replace protected data. + # Discussion: https://github.com/microsoft/agent-framework/pull/2950#discussion_r2634345613 + if other.protected_data is not None: + self.protected_data = other.protected_data + # Merge annotations if other.annotations: if self.annotations is None: diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index b47555c235..b7cac3ba20 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -16,8 +16,6 @@ from openai.types.chat.chat_completion_message_custom_tool_call import ChatCompletionMessageCustomToolCall from pydantic import ValidationError -from agent_framework import TextReasoningContent - from .._clients import BaseChatClient from .._logging import get_logger from .._middleware import use_chat_middleware @@ -36,6 +34,7 @@ FunctionResultContent, Role, TextContent, + TextReasoningContent, UriContent, UsageContent, UsageDetails, @@ -275,6 +274,8 @@ def _parse_response_update_from_openai( if text_content := self._parse_text_from_openai(choice): contents.append(text_content) + if reasoning_details := getattr(choice.delta, "reasoning_details", None): + contents.append(TextReasoningContent(None, protected_data=json.dumps(reasoning_details))) return ChatResponseUpdate( created_at=datetime.fromtimestamp(chunk.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), contents=contents, From dba5467d646bfc3473d3ada47a4e2c7de2ffaa69 Mon Sep 17 00:00:00 2001 From: ken <39673849+SuperKenVery@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:31:21 +0800 Subject: [PATCH 6/6] Fix handling None in TextReasoningContent.text --- python/packages/core/agent_framework/_types.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 72b69e251a..637794b1ca 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -864,7 +864,7 @@ def __add__(self, other: "TextReasoningContent") -> "TextReasoningContent": # Create new instance using from_dict for proper deserialization result_dict = { - "text": self.text + other.text, + "text": (self.text or "") + (other.text or "") if self.text is not None or other.text is not None else None, "type": "text_reasoning", "annotations": [ann.to_dict(exclude_none=False) for ann in annotations] if annotations else None, "additional_properties": {**(self.additional_properties or {}), **(other.additional_properties or {})}, @@ -886,7 +886,9 @@ def __iadd__(self, other: "TextReasoningContent") -> Self: raise TypeError("Incompatible type") # Concatenate text - self.text += other.text + if self.text is not None or other.text is not None: + self.text = (self.text or "") + (other.text or "") + # if both are None, should keep as None # Merge additional properties (self takes precedence) if self.additional_properties is None: