Repository navigation
Description
Describe the bug
An empty tool result becomes the literal string "None" after the prompt round trip that every ChatCompletionClientBase invocation performs.
history = ChatHistory()
history.add_tool_message("", tool_call_id="call_1", function_name="my-plugin-do")
rendered = history.to_prompt()
parsed = ChatHistory.from_rendered_prompt(rendered)Actual:
original tool message:
result field: ''
to_dict(): {'role': 'tool', 'content': '', 'tool_call_id': 'call_1'}
after to_prompt() -> from_rendered_prompt():
result field: None
to_dict(): {'role': 'tool', 'content': 'None', 'tool_call_id': 'call_1'}
rendered template: <chat_history>...<message role="tool"><function_result id="call_1" /></message></chat_history>
The empty element parses to result=None, and str(None) is what gets sent to the model. The same happens through the Jinja2 messages helper ({{ messages(chat_history) }}) and through KernelFunctionFromPrompt, which performs this round trip for every chat completion. JSON serialize() / restore_chat_history() preserves "", so only the XML prompt path is affected.
Root cause: FunctionResultContent.from_element sets result=element.text (python/semantic_kernel/contents/function_result_content.py:119), which is None for an empty element; ChatMessageContent._parse_items then stringifies it (python/semantic_kernel/contents/chat_message_content.py:335-336).
To Reproduce
Empty tool results are supported since #14472 (ChatHistory.add_tool_message("")). Run from python/:
uv run python repro_tool_result_roundtrip.pyExpected behavior
An empty result stays "" through to_prompt() / from_rendered_prompt(); to_dict() should keep content='', not 'None'. (Alternatively, rendering could emit an explicit empty text so parsing round-trips.)
Platform
- Language: Python
- Source: main branch at
4bb64ee(2026-10-28), semantic-kernel 1.46.0 - AI model: n/a (deterministic round trip, no model call)
- OS: Windows 11
Additional context
- Searched open and closed issues/PRs for
from_rendered_prompt,FunctionResultContent, and empty tool results. Related but distinct: Python: Empty tool results lose correlation and fail serialization #14359 (closed, fixed the direct add path via Python: fix(python): preserve empty tool results in chat history #14472), open Python: Handle empty chat message function results #14375 (factoryfrom_function_call_content_and_result) and open fix: ChatHistory: preserve empty tool results with tool_call_id #14480 (same add path). None covers theto_element/from_elementround trip. - Suggested fix: normalize a missing element text to
""infrom_element(or always emit a text node), plus a round-trip test with an empty result.
Reported with AI assistance (analysis and reproduction script); the reporter ran the reproduction and verified the output above.
Activity
[-]Bug: An empty tool result becomes the string 'None' after ChatHistory.to_prompt() / from_rendered_prompt()[/-][+]Python: Bug: An empty tool result becomes the string 'None' after ChatHistory.to_prompt() / from_rendered_prompt()[/+]BlueX888 commentedon Oct 29, 2026
I plan to implement this if the direction is right: normalize a missing
<function_result>element text to""inFunctionResultContent.from_element(or always emit a text node into_element), plus regression coverage for the empty-result round trip and the Jinjamessageshelper. The reproduction above is deterministic and needs no model calls.Could this issue be assigned to me (哈基米 (@BlueX888))? Happy to adjust the approach first if you'd prefer a different fix location.
Python: keep empty tool results through the prompt round trip (micros…