Skip to content

Python: fix(core): treat a missing text delta as empty when adding text content - #8699

Merged
Eduard van Valkenburg (eavanvalkenburg) merged 3 commits into
microsoft:mainfrom
anishmehta24:fix/text-content-none-concat
Sep 29, 2026
Merged

Eduard van Valkenburg (eavanvalkenburg) merged 3 commits into
microsoft:mainfrom
anishmehta24:fix/text-content-none-concat

Conversation

@anishmehta24

Copy link
Copy Markdown
Contributor

Motivation and Context

text is optional on a text content — Content("text") and Content.from_dict({"type": "text"}) both produce one with text=None, and test_chat_response_value_handles_text_content_without_text already covers that shape existing. But _add_text_content concatenates the attributes directly:

text=self.text + other.text,  # type: ignore[attr-defined, operator]

so adding one raises TypeError: can only concatenate str (not "NoneType") to str.

That's reachable from the public API rather than just from +. _coalesce_text_content merges consecutive text contents with + during _finalize_response, so a single text part carrying no delta (annotations or metadata only) takes down the whole response:

ChatResponse.from_updates([
    ChatResponseUpdate(role="assistant", contents=[Content("text", text="Hello ")]),
    ChatResponseUpdate(role="assistant", contents=[Content("text")]),
    ChatResponseUpdate(role="assistant", contents=[Content("text", text="world")]),
])
# TypeError: can only concatenate str (not "NoneType") to str

The same stream built out of text_reasoning parts returns "Hello world" and always has, because _add_text_reasoning_content handles this case deliberately — there's a comment there about Anthropic emitting a thinking block with thinking="". The two content types just disagreed about what a missing value means.

Description

_add_text_content now follows the same rule as its reasoning counterpart: a missing delta counts as empty, and None survives only when neither side has text. That keeps None (no text) distinguishable from "" (an empty one), which matters for the same replay reason as in the reasoning path.

One line of behaviour change, no signature change. The # type: ignore[operator] comment is gone too, since the operands are strings now.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • I didn't break anyone 😄

Tests in packages/core/tests/core/test_types.py:

  • test_text_content_add_handles_missing_text — adding in either direction
  • test_text_content_add_preserves_none_and_empty_string — None + None stays None, "" + None stays ""
  • test_chat_response_from_updates_coalesces_text_update_without_text — the reachable path, asserting text and text_reasoning now behave the same

All three fail on main. tests/core/test_types.py passes in full, and the whole tests/core directory is green. ruff check and ruff format clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WyCUJcDPnCnQMmuDwsnDmD

…xt content

`text` is optional on a text content, but `_add_text_content` concatenated
`self.text + other.text` directly, so a part with no text raised
`TypeError: can only concatenate str (not "NoneType") to str`.

This is reachable from the public API: `_coalesce_text_content` merges
consecutive text contents with `+`, so one such part aborts the whole
response.

    ChatResponse.from_updates([
        ChatResponseUpdate(role="assistant", contents=[Content("text", text="Hello ")]),
        ChatResponseUpdate(role="assistant", contents=[Content("text")]),
        ChatResponseUpdate(role="assistant", contents=[Content("text", text="world")]),
    ])                                                 # TypeError

The same stream built from `text_reasoning` parts already worked, because
`_add_text_reasoning_content` does handle it. This makes the two agree:
a missing delta counts as empty, and `None` survives only when neither side
has text, so it stays distinguishable from a real empty string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyCUJcDPnCnQMmuDwsnDmD

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Sep 23, 2026
Comment thread python/packages/core/agent_framework/_types.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anish Mehta (@anishmehta24) Reviewed the current head and found no blocking issues.

Merged via the queue into microsoft:main with commit 0911ad6 Sep 29, 2026
44 checks passed

This branch was successfully deployed

1 active deployment
github-app-auth — 29a9c595 Deployed Sep 29, 2026 by anishmehta24 via add_label #23917
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants