Skip to content

Python: fix: preserve function-call name when merging streaming deltas#6809

Open
Osamaali313 wants to merge 3 commits into
microsoft:mainfrom
Osamaali313:fix/function-call-name-merge
Open

Python: fix: preserve function-call name when merging streaming deltas#6809
Osamaali313 wants to merge 3 commits into
microsoft:mainfrom
Osamaali313:fix/function-call-name-merge

Conversation

@Osamaali313

Copy link
Copy Markdown

Problem

Content._add_function_call_content (the + operator for function_call content, used by ChatResponse.from_updates / AgentResponse.from_updates to merge streaming deltas) builds the merged function name like this:

name=getattr(self, "name", getattr(other, "name", None)),

Content.__init__ always assigns self.name (defaulting to None), so the attribute is never missing. getattr(self, "name", <fallback>) therefore always returns self.name — even when it is None — and the <fallback> (other.name) is never consulted.

As a result, when two function_call contents are merged and only the second carries the name, the name is silently dropped. This happens for any streaming path where the function name arrives in a later delta than the first chunk:

a = Content.from_function_call(call_id="1", name=None, arguments='{"a":')
b = Content.from_function_call(call_id="1", name="get_weather", arguments="1}")

(a + b).name   # -> None          ❌  (name dropped)
(b + a).name   # -> "get_weather" ✅  (works only when self has the name)

The asymmetry confirms it's an oversight: the sibling fields in the same return use the correct "either side" pattern — exception=getattr(self, "exception", None) or getattr(other, "exception", None) on the very next line, and _add_text_reasoning_content uses self.id or other.id.

Fix

Mirror the adjacent exception field:

name=getattr(self, "name", None) or getattr(other, "name", None),

After the fix, the name is preserved regardless of which side carries it, both-None stays None, and the normal both-set case is unchanged (self wins).

Tests

Extended test_function_call_content_add_merging_and_errors to cover the late-name (a + b and b + a) and both-None cases. Verified RED→GREEN: the new assertions fail on main and pass with the fix; the full tests/core/test_types.py suite stays green (267 passed).

Copilot AI review requested due to automatic review settings June 29, 2026 20:19

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.

@moonbox3 moonbox3 added the python Usage: [Issues, PRs], Target: Python label Jun 29, 2026
@github-actions github-actions Bot changed the title fix: preserve function-call name when merging streaming deltas Python: fix: preserve function-call name when merging streaming deltas Jun 29, 2026
`Content._add_function_call_content` built the merged name with
`getattr(self, "name", getattr(other, "name", None))`. Because
`Content.__init__` always sets `self.name` (defaulting to `None`), the
attribute is never missing, so the `getattr` default is never consulted
and `other.name` is ignored. When two function_call contents are merged
and only the second carries the name -- e.g. a streaming delta where the
function name arrives after the first chunk -- the name was silently
dropped.

Use the same "either side" pattern already used for the sibling
`exception` field on the next line: `getattr(self, "name", None) or
getattr(other, "name", None)`. Extend the existing merge test to cover
the late-name and both-None cases.
@Osamaali313 Osamaali313 force-pushed the fix/function-call-name-merge branch from 4eac647 to 6dc4250 Compare July 1, 2026 17:39
Comment thread python/packages/core/tests/core/test_types.py Outdated
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _types.py12269792%61, 70–71, 125, 130, 149, 151, 155, 159, 161, 163, 165, 183, 187, 213, 235, 240, 245, 249, 279, 705–706, 887–888, 1346, 1421, 1456, 1476, 1486, 1538, 1672–1674, 1959–1964, 1989, 2044, 2049, 2059, 2067, 2074–2078, 2096, 2169, 2182, 2187, 2300, 2323, 2584, 2608, 2707, 2888–2889, 2991, 3220, 3273, 3292, 3331, 3342, 3344–3348, 3350, 3353–3361, 3371, 3460, 3597, 3602, 3607, 3612, 3616, 3702–3704, 3733, 3821–3825
TOTAL44153526588% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
8856 33 💤 0 ❌ 0 🔥 2m 19s ⏱️

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread python/packages/core/tests/core/test_types.py Outdated
Comment thread python/packages/core/tests/core/test_types.py Outdated
Per review (pyright `reportArgumentType`): `Content.from_function_call`
annotates `name: str`, so passing `name=None` to model a streaming delta
with no name yet tripped the typing gate. Build those nameless deltas
with the `Content("function_call", ...)` constructor instead (its `name`
param is `str | None`) — the factory just wraps that same constructor, so
the runtime objects and the merge assertions are unchanged.
@Osamaali313

Copy link
Copy Markdown
Author

Good catch — fixed in d82c14f. I went with the second option you offered (construct the nameless delta via Content("function_call", ...) rather than the factory), so the public from_function_call(name: str) signature stays strict and the pyright reportArgumentType gate clears. The factory just wraps that same constructor, so the merged objects and assertions are unchanged (test still passes).

I chose this over widening from_function_call to name: str | None because a nameless call is really a streaming-delta intermediate rather than something a caller should mint through the public factory — and production already treats name as optionally-absent on that path (e.g. the content.name guards in _compaction.py / _evaluation.py). But if you'd prefer to make nameless deltas first-class and widen the factory signature instead, I'm happy to switch — just say the word.

@moonbox3 moonbox3 enabled auto-merge July 13, 2026 02:00
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