From afcbd02a7a8ed724393baefbda9a7c97bcf1b432 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:02:09 -0800 Subject: [PATCH 1/5] Added internal kwargs filtering for chat clients --- .../packages/core/agent_framework/_clients.py | 25 ++- .../packages/core/tests/core/test_clients.py | 147 +++++++++++++++++- 2 files changed, 168 insertions(+), 4 deletions(-) diff --git a/python/packages/core/agent_framework/_clients.py b/python/packages/core/agent_framework/_clients.py index 68d9d0312f..7f21ae7eb0 100644 --- a/python/packages/core/agent_framework/_clients.py +++ b/python/packages/core/agent_framework/_clients.py @@ -66,6 +66,27 @@ logger = get_logger() +# Internal kwargs that should never be passed to client implementations. +_INTERNAL_KWARGS: frozenset[str] = frozenset({"thread", "middleware"}) + + +def _filter_internal_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]: + """Filter out internal framework kwargs before passing to client implementations. + + Filters: + - Any kwarg starting with '_' (internal objects like _function_middleware_pipeline) + - 'thread': Thread object used by agent framework + - 'middleware': Middleware list passed through decorator chain + + Args: + kwargs: The kwargs dictionary to filter. + + Returns: + A new dict with internal framework kwargs removed. + """ + return {k: v for k, v in kwargs.items() if not k.startswith("_") and k not in _INTERNAL_KWARGS} + + __all__ = [ "BaseChatClient", "ChatClientProtocol", @@ -376,7 +397,7 @@ async def get_response( return await self._inner_get_response( messages=prepare_messages(messages), options=await validate_chat_options(dict(options) if options else {}), - **kwargs, + **_filter_internal_kwargs(kwargs), ) async def get_streaming_response( @@ -399,7 +420,7 @@ async def get_streaming_response( async for update in self._inner_get_streaming_response( messages=prepare_messages(messages), options=await validate_chat_options(dict(options) if options else {}), - **kwargs, + **_filter_internal_kwargs(kwargs), ): yield update diff --git a/python/packages/core/tests/core/test_clients.py b/python/packages/core/tests/core/test_clients.py index 67ecd54a8d..3dfb4501a0 100644 --- a/python/packages/core/tests/core/test_clients.py +++ b/python/packages/core/tests/core/test_clients.py @@ -7,8 +7,12 @@ BaseChatClient, ChatClientProtocol, ChatMessage, + ChatResponse, + ChatResponseUpdate, Role, ) +from agent_framework._clients import _filter_internal_kwargs +from agent_framework._types import prepend_instructions_to_messages def test_chat_client_type(chat_client: ChatClientProtocol): @@ -57,8 +61,6 @@ async def test_chat_client_instructions_handling(chat_client_base: ChatClientPro assert messages[0].role == Role.USER assert messages[0].text == "hello" - from agent_framework._types import prepend_instructions_to_messages - appended_messages = prepend_instructions_to_messages( [ChatMessage(role=Role.USER, text="hello")], instructions, @@ -68,3 +70,144 @@ async def test_chat_client_instructions_handling(chat_client_base: ChatClientPro assert appended_messages[0].text == "You are a helpful assistant." assert appended_messages[1].role == Role.USER assert appended_messages[1].text == "hello" + + +# region Internal kwargs filtering tests + + +class TestFilterInternalKwargs: + """Tests for _filter_internal_kwargs function.""" + + def test_filters_underscore_prefixed_kwargs(self): + """Kwargs starting with underscore should be filtered out.""" + kwargs = { + "_function_middleware_pipeline": object(), + "_chat_middleware_pipeline": object(), + "_internal": "value", + "normal_kwarg": "kept", + } + result = _filter_internal_kwargs(kwargs) + assert "_function_middleware_pipeline" not in result + assert "_chat_middleware_pipeline" not in result + assert "_internal" not in result + assert result["normal_kwarg"] == "kept" + + def test_filters_thread_kwarg(self): + """The 'thread' kwarg should be filtered out.""" + kwargs = {"thread": object(), "other": "value"} + result = _filter_internal_kwargs(kwargs) + assert "thread" not in result + assert result["other"] == "value" + + def test_filters_middleware_kwarg(self): + """The 'middleware' kwarg should be filtered out.""" + kwargs = {"middleware": [object()], "other": "value"} + result = _filter_internal_kwargs(kwargs) + assert "middleware" not in result + assert result["other"] == "value" + + def test_preserves_conversation_id(self): + """The 'conversation_id' kwarg should NOT be filtered (used by Azure AI).""" + kwargs = {"conversation_id": "test-id", "other": "value"} + result = _filter_internal_kwargs(kwargs) + assert result["conversation_id"] == "test-id" + assert result["other"] == "value" + + def test_preserves_other_kwargs(self): + """Regular kwargs should be preserved.""" + kwargs = { + "temperature": 0.7, + "max_tokens": 100, + "custom_param": "value", + } + result = _filter_internal_kwargs(kwargs) + assert result == kwargs + + def test_filters_multiple_internal_kwargs(self): + """Multiple internal kwargs should all be filtered.""" + kwargs = { + "_function_middleware_pipeline": object(), + "thread": object(), + "middleware": [object()], + "temperature": 0.7, + "conversation_id": "test-id", + } + result = _filter_internal_kwargs(kwargs) + assert "_function_middleware_pipeline" not in result + assert "thread" not in result + assert "middleware" not in result + assert result["temperature"] == 0.7 + assert result["conversation_id"] == "test-id" + + def test_empty_kwargs(self): + """Empty kwargs should return empty dict.""" + result = _filter_internal_kwargs({}) + assert result == {} + + +async def test_get_response_filters_internal_kwargs(chat_client_base: ChatClientProtocol): + """Verify that get_response filters internal kwargs before calling _inner_get_response.""" + with patch.object(chat_client_base, "_inner_get_response") as mock_inner: + mock_inner.return_value = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) + + # Call with internal kwargs that should be filtered + # Note: _function_middleware_pipeline is handled by the @use_function_invocation decorator, + # not the base class filtering, so we don't test it here. + await chat_client_base.get_response( + "hello", + thread=object(), + middleware=[object()], + _custom_internal="filtered", # Underscore-prefixed should be filtered + conversation_id="test-id", # Should NOT be filtered + custom_param="value", # Should NOT be filtered + ) + + mock_inner.assert_called_once() + _, kwargs = mock_inner.call_args + + # Internal kwargs should be filtered + assert "thread" not in kwargs + assert "middleware" not in kwargs + assert "_custom_internal" not in kwargs + + # These should be preserved + assert kwargs["conversation_id"] == "test-id" + assert kwargs["custom_param"] == "value" + + +async def test_get_streaming_response_filters_internal_kwargs(chat_client_base: ChatClientProtocol): + """Verify that get_streaming_response filters internal kwargs before calling _inner_get_streaming_response.""" + with patch.object(chat_client_base, "_inner_get_streaming_response") as mock_inner: + + async def mock_generator(*args, **kwargs): + yield ChatResponseUpdate(text="response") + + mock_inner.return_value = mock_generator() + + # Call with internal kwargs that should be filtered + # Note: _function_middleware_pipeline is handled by the @use_function_invocation decorator, + # not the base class filtering, so we don't test it here. + async for _ in chat_client_base.get_streaming_response( + "hello", + thread=object(), + middleware=[object()], + _custom_internal="filtered", # Underscore-prefixed should be filtered + conversation_id="test-id", # Should NOT be filtered + custom_param="value", # Should NOT be filtered + ): + pass + + mock_inner.assert_called_once() + _, kwargs = mock_inner.call_args + + # Internal kwargs should be filtered + assert "thread" not in kwargs + assert "middleware" not in kwargs + assert "_custom_internal" not in kwargs + + # These should be preserved + assert kwargs["conversation_id"] == "test-id" + assert kwargs["custom_param"] == "value" + + +# endregion From 4734d5015fadb113167319f42098c06cd7b0c795 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:30:01 -0800 Subject: [PATCH 2/5] Small updates --- .../agent_framework_anthropic/_chat_client.py | 8 +- .../anthropic/tests/test_anthropic_client.py | 30 ++++ .../packages/core/agent_framework/_clients.py | 25 +-- .../packages/core/tests/core/test_clients.py | 144 ------------------ 4 files changed, 39 insertions(+), 168 deletions(-) diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 630b92ca02..a4b5d7fb82 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -442,7 +442,13 @@ def _prepare_options( # Add the structured outputs beta flag run_options["betas"].add(STRUCTURED_OUTPUTS_BETA_FLAG) - run_options.update(kwargs) + # Filter out framework kwargs that should not be passed to the Anthropic API. + # This includes underscore-prefixed internal objects (like _function_middleware_pipeline) + # and framework kwargs like 'thread' and 'middleware'. + filtered_kwargs = { + k: v for k, v in kwargs.items() if not k.startswith("_") and k not in {"thread", "middleware"} + } + run_options.update(filtered_kwargs) return run_options def _prepare_betas(self, options: dict[str, Any]) -> set[str]: diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 6b06843b73..15551af082 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -481,6 +481,36 @@ async def test_prepare_options_with_top_p(mock_anthropic_client: MagicMock) -> N assert run_options["top_p"] == 0.9 +async def test_prepare_options_filters_internal_kwargs(mock_anthropic_client: MagicMock) -> None: + """Test _prepare_options filters internal framework kwargs. + + Internal kwargs like _function_middleware_pipeline, thread, and middleware + should be filtered out before being passed to the Anthropic API. + """ + chat_client = create_test_anthropic_client(mock_anthropic_client) + + messages = [ChatMessage(role=Role.USER, text="Hello")] + chat_options: ChatOptions = {} + + # Simulate internal kwargs that get passed through the middleware pipeline + internal_kwargs = { + "_function_middleware_pipeline": object(), + "_chat_middleware_pipeline": object(), + "_any_underscore_prefixed": object(), + "thread": object(), + "middleware": [object()], + } + + run_options = chat_client._prepare_options(messages, chat_options, **internal_kwargs) + + # Internal kwargs should be filtered out + assert "_function_middleware_pipeline" not in run_options + assert "_chat_middleware_pipeline" not in run_options + assert "_any_underscore_prefixed" not in run_options + assert "thread" not in run_options + assert "middleware" not in run_options + + # Response Processing Tests diff --git a/python/packages/core/agent_framework/_clients.py b/python/packages/core/agent_framework/_clients.py index 7f21ae7eb0..68d9d0312f 100644 --- a/python/packages/core/agent_framework/_clients.py +++ b/python/packages/core/agent_framework/_clients.py @@ -66,27 +66,6 @@ logger = get_logger() -# Internal kwargs that should never be passed to client implementations. -_INTERNAL_KWARGS: frozenset[str] = frozenset({"thread", "middleware"}) - - -def _filter_internal_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]: - """Filter out internal framework kwargs before passing to client implementations. - - Filters: - - Any kwarg starting with '_' (internal objects like _function_middleware_pipeline) - - 'thread': Thread object used by agent framework - - 'middleware': Middleware list passed through decorator chain - - Args: - kwargs: The kwargs dictionary to filter. - - Returns: - A new dict with internal framework kwargs removed. - """ - return {k: v for k, v in kwargs.items() if not k.startswith("_") and k not in _INTERNAL_KWARGS} - - __all__ = [ "BaseChatClient", "ChatClientProtocol", @@ -397,7 +376,7 @@ async def get_response( return await self._inner_get_response( messages=prepare_messages(messages), options=await validate_chat_options(dict(options) if options else {}), - **_filter_internal_kwargs(kwargs), + **kwargs, ) async def get_streaming_response( @@ -420,7 +399,7 @@ async def get_streaming_response( async for update in self._inner_get_streaming_response( messages=prepare_messages(messages), options=await validate_chat_options(dict(options) if options else {}), - **_filter_internal_kwargs(kwargs), + **kwargs, ): yield update diff --git a/python/packages/core/tests/core/test_clients.py b/python/packages/core/tests/core/test_clients.py index 3dfb4501a0..d3e3aa298c 100644 --- a/python/packages/core/tests/core/test_clients.py +++ b/python/packages/core/tests/core/test_clients.py @@ -7,11 +7,8 @@ BaseChatClient, ChatClientProtocol, ChatMessage, - ChatResponse, - ChatResponseUpdate, Role, ) -from agent_framework._clients import _filter_internal_kwargs from agent_framework._types import prepend_instructions_to_messages @@ -70,144 +67,3 @@ async def test_chat_client_instructions_handling(chat_client_base: ChatClientPro assert appended_messages[0].text == "You are a helpful assistant." assert appended_messages[1].role == Role.USER assert appended_messages[1].text == "hello" - - -# region Internal kwargs filtering tests - - -class TestFilterInternalKwargs: - """Tests for _filter_internal_kwargs function.""" - - def test_filters_underscore_prefixed_kwargs(self): - """Kwargs starting with underscore should be filtered out.""" - kwargs = { - "_function_middleware_pipeline": object(), - "_chat_middleware_pipeline": object(), - "_internal": "value", - "normal_kwarg": "kept", - } - result = _filter_internal_kwargs(kwargs) - assert "_function_middleware_pipeline" not in result - assert "_chat_middleware_pipeline" not in result - assert "_internal" not in result - assert result["normal_kwarg"] == "kept" - - def test_filters_thread_kwarg(self): - """The 'thread' kwarg should be filtered out.""" - kwargs = {"thread": object(), "other": "value"} - result = _filter_internal_kwargs(kwargs) - assert "thread" not in result - assert result["other"] == "value" - - def test_filters_middleware_kwarg(self): - """The 'middleware' kwarg should be filtered out.""" - kwargs = {"middleware": [object()], "other": "value"} - result = _filter_internal_kwargs(kwargs) - assert "middleware" not in result - assert result["other"] == "value" - - def test_preserves_conversation_id(self): - """The 'conversation_id' kwarg should NOT be filtered (used by Azure AI).""" - kwargs = {"conversation_id": "test-id", "other": "value"} - result = _filter_internal_kwargs(kwargs) - assert result["conversation_id"] == "test-id" - assert result["other"] == "value" - - def test_preserves_other_kwargs(self): - """Regular kwargs should be preserved.""" - kwargs = { - "temperature": 0.7, - "max_tokens": 100, - "custom_param": "value", - } - result = _filter_internal_kwargs(kwargs) - assert result == kwargs - - def test_filters_multiple_internal_kwargs(self): - """Multiple internal kwargs should all be filtered.""" - kwargs = { - "_function_middleware_pipeline": object(), - "thread": object(), - "middleware": [object()], - "temperature": 0.7, - "conversation_id": "test-id", - } - result = _filter_internal_kwargs(kwargs) - assert "_function_middleware_pipeline" not in result - assert "thread" not in result - assert "middleware" not in result - assert result["temperature"] == 0.7 - assert result["conversation_id"] == "test-id" - - def test_empty_kwargs(self): - """Empty kwargs should return empty dict.""" - result = _filter_internal_kwargs({}) - assert result == {} - - -async def test_get_response_filters_internal_kwargs(chat_client_base: ChatClientProtocol): - """Verify that get_response filters internal kwargs before calling _inner_get_response.""" - with patch.object(chat_client_base, "_inner_get_response") as mock_inner: - mock_inner.return_value = ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="response")]) - - # Call with internal kwargs that should be filtered - # Note: _function_middleware_pipeline is handled by the @use_function_invocation decorator, - # not the base class filtering, so we don't test it here. - await chat_client_base.get_response( - "hello", - thread=object(), - middleware=[object()], - _custom_internal="filtered", # Underscore-prefixed should be filtered - conversation_id="test-id", # Should NOT be filtered - custom_param="value", # Should NOT be filtered - ) - - mock_inner.assert_called_once() - _, kwargs = mock_inner.call_args - - # Internal kwargs should be filtered - assert "thread" not in kwargs - assert "middleware" not in kwargs - assert "_custom_internal" not in kwargs - - # These should be preserved - assert kwargs["conversation_id"] == "test-id" - assert kwargs["custom_param"] == "value" - - -async def test_get_streaming_response_filters_internal_kwargs(chat_client_base: ChatClientProtocol): - """Verify that get_streaming_response filters internal kwargs before calling _inner_get_streaming_response.""" - with patch.object(chat_client_base, "_inner_get_streaming_response") as mock_inner: - - async def mock_generator(*args, **kwargs): - yield ChatResponseUpdate(text="response") - - mock_inner.return_value = mock_generator() - - # Call with internal kwargs that should be filtered - # Note: _function_middleware_pipeline is handled by the @use_function_invocation decorator, - # not the base class filtering, so we don't test it here. - async for _ in chat_client_base.get_streaming_response( - "hello", - thread=object(), - middleware=[object()], - _custom_internal="filtered", # Underscore-prefixed should be filtered - conversation_id="test-id", # Should NOT be filtered - custom_param="value", # Should NOT be filtered - ): - pass - - mock_inner.assert_called_once() - _, kwargs = mock_inner.call_args - - # Internal kwargs should be filtered - assert "thread" not in kwargs - assert "middleware" not in kwargs - assert "_custom_internal" not in kwargs - - # These should be preserved - assert kwargs["conversation_id"] == "test-id" - assert kwargs["custom_param"] == "value" - - -# endregion From 648e43d61fff691db90a7b10353270c14ce3238e Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:48:36 -0800 Subject: [PATCH 3/5] Reverted changes --- python/packages/core/tests/core/test_clients.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/packages/core/tests/core/test_clients.py b/python/packages/core/tests/core/test_clients.py index d3e3aa298c..67ecd54a8d 100644 --- a/python/packages/core/tests/core/test_clients.py +++ b/python/packages/core/tests/core/test_clients.py @@ -9,7 +9,6 @@ ChatMessage, Role, ) -from agent_framework._types import prepend_instructions_to_messages def test_chat_client_type(chat_client: ChatClientProtocol): @@ -58,6 +57,8 @@ async def test_chat_client_instructions_handling(chat_client_base: ChatClientPro assert messages[0].role == Role.USER assert messages[0].text == "hello" + from agent_framework._types import prepend_instructions_to_messages + appended_messages = prepend_instructions_to_messages( [ChatMessage(role=Role.USER, text="hello")], instructions, From 1cac69cc3d74f639ecba7a1278a9db219df64161 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:23:58 -0800 Subject: [PATCH 4/5] Small fix --- python/packages/anthropic/tests/test_anthropic_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 15551af082..2e74d98582 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -489,7 +489,7 @@ async def test_prepare_options_filters_internal_kwargs(mock_anthropic_client: Ma """ chat_client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role=Role.USER, text="Hello")] + messages = [ChatMessage(role="user", text="Hello")] chat_options: ChatOptions = {} # Simulate internal kwargs that get passed through the middleware pipeline From 219d79f3f4a7dcd01b8becd0ca0cba436416cc6b Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:27:25 -0800 Subject: [PATCH 5/5] Fixed test --- python/packages/core/tests/workflow/test_sub_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/packages/core/tests/workflow/test_sub_workflow.py b/python/packages/core/tests/workflow/test_sub_workflow.py index c413190a24..33333d2906 100644 --- a/python/packages/core/tests/workflow/test_sub_workflow.py +++ b/python/packages/core/tests/workflow/test_sub_workflow.py @@ -599,7 +599,7 @@ async def test_sub_workflow_checkpoint_restore_no_duplicate_requests() -> None: # Get checkpoint checkpoints = await storage.list_checkpoints(workflow1.id) - checkpoint_id = max(checkpoints, key=lambda cp: cp.timestamp).checkpoint_id + checkpoint_id = max(checkpoints, key=lambda cp: cp.iteration_count).checkpoint_id # Step 2: Resume workflow from checkpoint workflow2 = _build_checkpoint_test_workflow(storage)