From 21c9a5656274ac5b0840d44159550c6b0a3a8afb Mon Sep 17 00:00:00 2001 From: venti <1308199824@qq.com> Date: Sat, 30 May 2026 15:22:23 +0800 Subject: [PATCH 1/2] Fix auto function calling stripping explicit null arguments (fixes #5934) --- python/packages/core/agent_framework/_tools.py | 6 +++--- python/packages/core/tests/core/test_tools.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 93722a8987..1605c5443d 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -636,7 +636,7 @@ async def invoke( parsed_arguments = dict(arguments) if self.input_model is not None and not self._schema_supplied: parsed_arguments = self.input_model.model_validate(parsed_arguments).model_dump( - exclude_none=True + exclude_none=False ) elif isinstance(arguments, BaseModel): if ( @@ -645,7 +645,7 @@ async def invoke( and not isinstance(arguments, self.input_model) ): raise TypeError(f"Expected {self.input_model.__name__}, got {type(arguments).__name__}") - parsed_arguments = arguments.model_dump(exclude_none=True) + parsed_arguments = arguments.model_dump(exclude_none=False) else: raise TypeError( f"Expected mapping-like arguments for tool '{self.name}', got {type(arguments).__name__}" @@ -1492,7 +1492,7 @@ async def _auto_invoke_function( runtime_kwargs["session"] = invocation_session try: if not cast(bool, getattr(tool, "_schema_supplied", False)) and tool.input_model is not None: - args = tool.input_model.model_validate(parsed_args).model_dump(exclude_none=True) + args = tool.input_model.model_validate(parsed_args).model_dump(exclude_none=False) else: args = dict(parsed_args) args = _validate_arguments_against_schema( diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index b3762bf4ef..df161a056d 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. +import asyncio from typing import Annotated, Any, Literal, get_args, get_origin from unittest.mock import Mock @@ -1462,4 +1463,20 @@ def test_skip_parsing_is_singleton() -> None: assert repr(SKIP_PARSING) == "SKIP_PARSING" +def test_invoke_preserves_explicit_none_arguments() -> None: + """Optional parameters explicitly set to None must not be stripped before invocation.""" + + @tool + def greet(name: str, greeting: str | None = None) -> str: + return f"{greeting or 'Hello'}, {name}!" + + result = asyncio.run(greet.invoke(arguments={"name": "World", "greeting": None})) + assert isinstance(result, list) + assert result[0].text == "Hello, World!" + + result = asyncio.run(greet.invoke(arguments={"name": "World"})) + assert isinstance(result, list) + assert result[0].text == "Hello, World!" + + # endregion From 61e1617dbc49b499c30c8ef674b9f66dd340156a Mon Sep 17 00:00:00 2001 From: venti <1308199824@qq.com> Date: Sat, 30 May 2026 15:26:35 +0800 Subject: [PATCH 2/2] fix: coalesce code_interpreter_tool_call chunks with same call_id in finalize_response Closes #5793 During streaming, each code_interpreter_call_code.delta event creates a separate code_interpreter_tool_call content item with one text chunk. The final response then accumulates these as multiple items rather than coalescing them into a single item with the complete aggregated text. When stored by a history provider (e.g. CosmosHistoryProvider), each chunk becomes a separate content item, producing thousands of redundant entries for a single code interpreter invocation. Fix: add _coalesce_code_interpreter_content in _finalize_response that merges code_interpreter_tool_call items sharing the same call_id into one item with all inputs concatenated. --- .../packages/core/agent_framework/_types.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index ce47f5fc8e..048551752b 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -1973,11 +1973,35 @@ def _coalesce_text_content(contents: list[Content], type_str: Literal["text", "t contents.extend(coalesced_contents) +def _coalesce_code_interpreter_content(contents: list[Content]) -> None: + """Coalesce code_interpreter_tool_call items with the same call_id into a single item.""" + if not contents: + return + seen: dict[str, Content] = {} + coalesced: list[Content] = [] + for content in contents: + if content.type == "code_interpreter_tool_call" and content.call_id: + if content.call_id in seen: + existing = seen[content.call_id] + if content.inputs: + if existing.inputs is None: + existing.inputs = [] + existing.inputs.extend(content.inputs) + else: + seen[content.call_id] = content + coalesced.append(content) + else: + coalesced.append(content) + contents.clear() + contents.extend(coalesced) + + def _finalize_response(response: ChatResponse | AgentResponse) -> None: """Finalizes the response by performing any necessary post-processing.""" for msg in response.messages: _coalesce_text_content(msg.contents, "text") _coalesce_text_content(msg.contents, "text_reasoning") + _coalesce_code_interpreter_content(msg.contents) # region ContinuationToken