From b4440ad87fdee56a79ead171d86caa92dc679326 Mon Sep 17 00:00:00 2001 From: "L. Elaine Dazzio" Date: Fri, 10 Apr 2026 16:07:52 -0400 Subject: [PATCH 1/5] feat: add finish_reason support to AgentResponse and AgentResponseUpdate Add finish_reason field to AgentResponse and AgentResponseUpdate classes, propagate it through _process_update() and map_chat_to_agent_update(), and add comprehensive unit tests. Fixes #4622 --- .../core/tests/core/test_finish_reason.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 python/packages/core/tests/core/test_finish_reason.py diff --git a/python/packages/core/tests/core/test_finish_reason.py b/python/packages/core/tests/core/test_finish_reason.py new file mode 100644 index 0000000000..1c01215f22 --- /dev/null +++ b/python/packages/core/tests/core/test_finish_reason.py @@ -0,0 +1,100 @@ +from agent_framework import ( + AgentResponse, + AgentResponseUpdate, + ChatResponseUpdate, + Content, + Message, +) +from agent_framework._types import _process_update, map_chat_to_agent_update + + +def test_agent_response_init_with_finish_reason() -> None: + """Test that AgentResponse correctly initializes and stores finish_reason.""" + response = AgentResponse( + messages=[Message("assistant", [Content.from_text("test")])], + finish_reason="stop", + ) + assert response.finish_reason == "stop" + + +def test_agent_response_update_init_with_finish_reason() -> None: + """Test that AgentResponseUpdate correctly initializes and stores finish_reason.""" + update = AgentResponseUpdate( + contents=[Content.from_text("test")], + role="assistant", + finish_reason="stop", + ) + assert update.finish_reason == "stop" + + +def test_map_chat_to_agent_update_forwards_finish_reason() -> None: + """Test that mapping a ChatResponseUpdate with finish_reason forwards it.""" + chat_update = ChatResponseUpdate( + contents=[Content.from_text("test")], + finish_reason="length", + ) + agent_update = map_chat_to_agent_update(chat_update, agent_name="test_agent") + + assert agent_update.finish_reason == "length" + assert agent_update.author_name == "test_agent" + + +def test_process_update_propagates_finish_reason_to_agent_response() -> None: + """Test that _process_update correctly updates an AgentResponse from an AgentResponseUpdate.""" + response = AgentResponse(messages=[Message("assistant", [Content.from_text("test")])]) + update = AgentResponseUpdate( + contents=[Content.from_text("more text")], + role="assistant", + finish_reason="stop", + ) + + # Process the update + _process_update(response, update) + + assert response.finish_reason == "stop" + + +def test_process_update_does_not_overwrite_with_none() -> None: + """Test that _process_update does not overwrite an existing finish_reason with None.""" + response = AgentResponse( + messages=[Message("assistant", [Content.from_text("test")])], + finish_reason="length", + ) + update = AgentResponseUpdate( + contents=[Content.from_text("more text")], + role="assistant", + finish_reason=None, + ) + + # Process the update + _process_update(response, update) + + assert response.finish_reason == "length" + + +def test_agent_response_serialization_includes_finish_reason() -> None: + """Test that AgentResponse serializes correctly, including finish_reason.""" + response = AgentResponse( + messages=[Message("assistant", [Content.from_text("test")])], + response_id="test_123", + finish_reason="stop", + ) + + # Serialize using the framework's API and verify finish_reason is included. + data = response.to_dict() + assert "finish_reason" in data + assert data["finish_reason"] == "stop" + + +def test_agent_response_update_serialization_includes_finish_reason() -> None: + """Test that AgentResponseUpdate serializes correctly, including finish_reason.""" + update = AgentResponseUpdate( + contents=[Content.from_text("test")], + role="assistant", + response_id="test_456", + finish_reason="tool_calls", + ) + + data = update.to_dict() + assert "finish_reason" in data + assert data["finish_reason"] == "tool_calls" From c2ecdb2e8b0c243a2415d58c14f6da9ad0758093 Mon Sep 17 00:00:00 2001 From: LEDazzio01 <170764058+LEDazzio01@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:35:48 -0400 Subject: [PATCH 2/5] feat: add finish_reason to AgentResponse and AgentResponseUpdate --- python/packages/core/agent_framework/_types.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 584c1f0110..db32026e7d 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -1877,8 +1877,11 @@ def _process_update(response: ChatResponse | AgentResponse, update: ChatResponse response.conversation_id = update.conversation_id if update.finish_reason is not None: response.finish_reason = update.finish_reason - if update.model is not None: - response.model = update.model + if update.model_id is not None: + response.model_id = update.model_id + if isinstance(response, AgentResponse) and isinstance(update, AgentResponseUpdate): + if update.finish_reason is not None: + response.finish_reason = update.finish_reason response.continuation_token = update.continuation_token @@ -2435,6 +2438,7 @@ def __init__( response_id: str | None = None, agent_id: str | None = None, created_at: CreatedAtT | None = None, + finish_reason: FinishReasonLiteral | FinishReason | None = None, usage_details: UsageDetails | None = None, value: ResponseModelT | None = None, response_format: StructuredResponseFormat = None, @@ -2476,6 +2480,7 @@ def __init__( self.response_id = response_id self.agent_id = agent_id self.created_at = created_at + self.finish_reason = finish_reason self.usage_details = usage_details self._value: ResponseModelT | None = value self._response_format: type[BaseModel] | Mapping[str, Any] | None = response_format @@ -2688,6 +2693,7 @@ def __init__( response_id: str | None = None, message_id: str | None = None, created_at: CreatedAtT | None = None, + finish_reason: FinishReasonLiteral | FinishReason | None = None, continuation_token: ContinuationToken | None = None, additional_properties: dict[str, Any] | None = None, raw_representation: Any | None = None, @@ -2729,6 +2735,7 @@ def __init__( self.response_id = response_id self.message_id = message_id self.created_at = created_at + self.finish_reason = finish_reason self.continuation_token = continuation_token self.additional_properties = _restore_compaction_annotation_in_additional_properties( additional_properties, @@ -2761,6 +2768,7 @@ def map_chat_to_agent_update(update: ChatResponseUpdate, agent_name: str | None) response_id=update.response_id, message_id=update.message_id, created_at=update.created_at, + finish_reason=update.finish_reason, continuation_token=update.continuation_token, additional_properties=update.additional_properties, raw_representation=update, From b25e90bab0f36e7809faf50e12fd3627018fd505 Mon Sep 17 00:00:00 2001 From: "L. Elaine Dazzio" Date: Fri, 10 Apr 2026 17:02:04 -0400 Subject: [PATCH 3/5] style: add copyright header to test_finish_reason.py --- python/packages/core/tests/core/test_finish_reason.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/packages/core/tests/core/test_finish_reason.py b/python/packages/core/tests/core/test_finish_reason.py index 1c01215f22..67a3dd7ded 100644 --- a/python/packages/core/tests/core/test_finish_reason.py +++ b/python/packages/core/tests/core/test_finish_reason.py @@ -1,3 +1,5 @@ +# Copyright (c) Microsoft. All rights reserved. + from agent_framework import ( AgentResponse, AgentResponseUpdate, From 0a235ea524e5f397f5b3064d136f4c5ef80afe16 Mon Sep 17 00:00:00 2001 From: LEDazzio01 <170764058+LEDazzio01@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:02:43 -0400 Subject: [PATCH 4/5] docs: add finish_reason to AgentResponse and AgentResponseUpdate docstrings --- python/packages/core/agent_framework/_types.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index db32026e7d..db132b3b2e 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -2454,6 +2454,9 @@ def __init__( agent_id: The identifier of the agent that produced this response. Useful in multi-agent scenarios to track which agent generated the response. created_at: A timestamp for the chat response. + finish_reason: The reason the model stopped generating. Common values include + ``"stop"`` (natural completion), ``"length"`` (token limit), and + ``"tool_calls"`` (the model invoked a tool). usage_details: The usage details for the chat response. value: The structured output of the agent run response, if applicable. response_format: Optional response format for the agent response. @@ -2709,6 +2712,9 @@ def __init__( response_id: Optional ID of the response of which this update is a part. message_id: Optional ID of the message of which this update is a part. created_at: Optional timestamp for the chat response update. + finish_reason: The reason the model stopped generating. Common values include + ``"stop"`` (natural completion), ``"length"`` (token limit), and + ``"tool_calls"`` (the model invoked a tool). continuation_token: Optional token for resuming a long-running background operation. When present, indicates the operation is still in progress. additional_properties: Optional additional properties associated with the chat response update. From e19fc57157621da25c418eb2734e7a5f7e8b979b Mon Sep 17 00:00:00 2001 From: LEDazzio01 Date: Wed, 15 Apr 2026 18:33:31 -0400 Subject: [PATCH 5/5] refactor: move finish_reason tests into test_types.py per review feedback Move all finish_reason test cases from the separate test_finish_reason.py file into test_types.py as requested by eavanvalkenburg. Tests are placed in a new '# region finish_reason' section at the end of the file. --- .../core/tests/core/test_finish_reason.py | 102 ------------------ python/packages/core/tests/core/test_types.py | 100 +++++++++++++++++ 2 files changed, 100 insertions(+), 102 deletions(-) delete mode 100644 python/packages/core/tests/core/test_finish_reason.py diff --git a/python/packages/core/tests/core/test_finish_reason.py b/python/packages/core/tests/core/test_finish_reason.py deleted file mode 100644 index 67a3dd7ded..0000000000 --- a/python/packages/core/tests/core/test_finish_reason.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from agent_framework import ( - AgentResponse, - AgentResponseUpdate, - ChatResponseUpdate, - Content, - Message, -) -from agent_framework._types import _process_update, map_chat_to_agent_update - - -def test_agent_response_init_with_finish_reason() -> None: - """Test that AgentResponse correctly initializes and stores finish_reason.""" - response = AgentResponse( - messages=[Message("assistant", [Content.from_text("test")])], - finish_reason="stop", - ) - assert response.finish_reason == "stop" - - -def test_agent_response_update_init_with_finish_reason() -> None: - """Test that AgentResponseUpdate correctly initializes and stores finish_reason.""" - update = AgentResponseUpdate( - contents=[Content.from_text("test")], - role="assistant", - finish_reason="stop", - ) - assert update.finish_reason == "stop" - - -def test_map_chat_to_agent_update_forwards_finish_reason() -> None: - """Test that mapping a ChatResponseUpdate with finish_reason forwards it.""" - chat_update = ChatResponseUpdate( - contents=[Content.from_text("test")], - finish_reason="length", - ) - agent_update = map_chat_to_agent_update(chat_update, agent_name="test_agent") - - assert agent_update.finish_reason == "length" - assert agent_update.author_name == "test_agent" - - -def test_process_update_propagates_finish_reason_to_agent_response() -> None: - """Test that _process_update correctly updates an AgentResponse from an AgentResponseUpdate.""" - response = AgentResponse(messages=[Message("assistant", [Content.from_text("test")])]) - update = AgentResponseUpdate( - contents=[Content.from_text("more text")], - role="assistant", - finish_reason="stop", - ) - - # Process the update - _process_update(response, update) - - assert response.finish_reason == "stop" - - -def test_process_update_does_not_overwrite_with_none() -> None: - """Test that _process_update does not overwrite an existing finish_reason with None.""" - response = AgentResponse( - messages=[Message("assistant", [Content.from_text("test")])], - finish_reason="length", - ) - update = AgentResponseUpdate( - contents=[Content.from_text("more text")], - role="assistant", - finish_reason=None, - ) - - # Process the update - _process_update(response, update) - - assert response.finish_reason == "length" - - -def test_agent_response_serialization_includes_finish_reason() -> None: - """Test that AgentResponse serializes correctly, including finish_reason.""" - response = AgentResponse( - messages=[Message("assistant", [Content.from_text("test")])], - response_id="test_123", - finish_reason="stop", - ) - - # Serialize using the framework's API and verify finish_reason is included. - data = response.to_dict() - assert "finish_reason" in data - assert data["finish_reason"] == "stop" - - -def test_agent_response_update_serialization_includes_finish_reason() -> None: - """Test that AgentResponseUpdate serializes correctly, including finish_reason.""" - update = AgentResponseUpdate( - contents=[Content.from_text("test")], - role="assistant", - response_id="test_456", - finish_reason="tool_calls", - ) - - data = update.to_dict() - assert "finish_reason" in data - assert data["finish_reason"] == "tool_calls" diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index cf945dae0e..4298563209 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -40,8 +40,10 @@ _get_data_bytes_as_str, _parse_content_list, _parse_structured_response_value, + _process_update, _validate_uri, add_usage_details, + map_chat_to_agent_update, validate_tool_mode, ) from agent_framework.exceptions import AdditionItemMismatch, ContentError @@ -4179,3 +4181,101 @@ def test_prepend_instructions_custom_role(): # endregion + + +# region finish_reason + + +def test_agent_response_init_with_finish_reason() -> None: + """Test that AgentResponse correctly initializes and stores finish_reason.""" + response = AgentResponse( + messages=[Message("assistant", [Content.from_text("test")])], + finish_reason="stop", + ) + assert response.finish_reason == "stop" + + +def test_agent_response_update_init_with_finish_reason() -> None: + """Test that AgentResponseUpdate correctly initializes and stores finish_reason.""" + update = AgentResponseUpdate( + contents=[Content.from_text("test")], + role="assistant", + finish_reason="stop", + ) + assert update.finish_reason == "stop" + + +def test_map_chat_to_agent_update_forwards_finish_reason() -> None: + """Test that mapping a ChatResponseUpdate with finish_reason forwards it.""" + chat_update = ChatResponseUpdate( + contents=[Content.from_text("test")], + finish_reason="length", + ) + agent_update = map_chat_to_agent_update(chat_update, agent_name="test_agent") + + assert agent_update.finish_reason == "length" + assert agent_update.author_name == "test_agent" + + +def test_process_update_propagates_finish_reason_to_agent_response() -> None: + """Test that _process_update correctly updates an AgentResponse from an AgentResponseUpdate.""" + response = AgentResponse(messages=[Message("assistant", [Content.from_text("test")])]) + update = AgentResponseUpdate( + contents=[Content.from_text("more text")], + role="assistant", + finish_reason="stop", + ) + + # Process the update + _process_update(response, update) + + assert response.finish_reason == "stop" + + +def test_process_update_does_not_overwrite_with_none() -> None: + """Test that _process_update does not overwrite an existing finish_reason with None.""" + response = AgentResponse( + messages=[Message("assistant", [Content.from_text("test")])], + finish_reason="length", + ) + update = AgentResponseUpdate( + contents=[Content.from_text("more text")], + role="assistant", + finish_reason=None, + ) + + # Process the update + _process_update(response, update) + + assert response.finish_reason == "length" + + +def test_agent_response_serialization_includes_finish_reason() -> None: + """Test that AgentResponse serializes correctly, including finish_reason.""" + response = AgentResponse( + messages=[Message("assistant", [Content.from_text("test")])], + response_id="test_123", + finish_reason="stop", + ) + + # Serialize using the framework's API and verify finish_reason is included. + data = response.to_dict() + assert "finish_reason" in data + assert data["finish_reason"] == "stop" + + +def test_agent_response_update_serialization_includes_finish_reason() -> None: + """Test that AgentResponseUpdate serializes correctly, including finish_reason.""" + update = AgentResponseUpdate( + contents=[Content.from_text("test")], + role="assistant", + response_id="test_456", + finish_reason="tool_calls", + ) + + data = update.to_dict() + assert "finish_reason" in data + assert data["finish_reason"] == "tool_calls" + + +# endregion