Skip to content

Python: Fix duplicate arguments in declaration-only tool streaming#7110

Open
kartikmadan11 wants to merge 2 commits into
microsoft:mainfrom
kartikmadan11:fix/6973-declaration-only-streaming-duplicate-args
Open

Python: Fix duplicate arguments in declaration-only tool streaming#7110
kartikmadan11 wants to merge 2 commits into
microsoft:mainfrom
kartikmadan11:fix/6973-declaration-only-streaming-duplicate-args

Conversation

@kartikmadan11

Copy link
Copy Markdown
Contributor

Motivation & Context

When streaming declaration-only function calls, the final response contains duplicated arguments.

Fixes #6973

Description & Review Guide

What are the major changes?

  • Modified _process_function_requests() in _tools.py (line 2403) to filter out function_call type items from the streamed result update
  • Added two test cases covering chunked and single-chunk streaming scenarios

What is the impact?

  • Eliminates argument duplication for declaration-only streaming tool calls
  • Preserves correct behavior for function_result and approval items

Root Cause: The _stream() loop yields function_call chunks as they arrive, then later re-yields those same items from function_call_results. The _process_update() callback concatenates arguments with +=, so duplicate yields cause duplication.

Solution: Filter out function_call type items from the result's function_call_results before yielding, since they were already emitted by the inner stream.

Tests Added

  • test_declaration_only_tool_streaming_no_argument_duplication() - Verifies multi-chunk streaming
  • test_declaration_only_tool_streaming_single_chunk_no_duplication() - Verifies single-chunk streaming

Both tests pass ✅

Copilot AI review requested due to automatic review settings July 14, 2026 13:04
@giles17 giles17 added the python Usage: [Issues, PRs], Target: Python label Jul 14, 2026

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

Fixes a Python streaming bug in the core function-invocation layer where declaration-only tool calls could end up with duplicated arguments in the final aggregated response due to the same function_call content being yielded twice during streaming.

Changes:

  • Filters function_call items out of the post-processing streamed function_call_results update to prevent _process_update from concatenating the same arguments twice.
  • Adds regression tests for both multi-chunk and single-chunk streaming declaration-only tool calls.

Reviewed changes

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

File Description
python/packages/core/agent_framework/_tools.py Adjusts streaming post-processing to avoid re-emitting function_call items that were already emitted by the inner stream.
python/packages/core/tests/core/test_function_invocation_logic.py Adds tests ensuring declaration-only tool streaming does not duplicate function-call arguments.

Comment thread python/packages/core/agent_framework/_tools.py Outdated
@kartikmadan11
kartikmadan11 force-pushed the fix/6973-declaration-only-streaming-duplicate-args branch from 026ca32 to 7def61f Compare July 14, 2026 14:37
@kartikmadan11

Copy link
Copy Markdown
Contributor Author

Great catch, Copilot! I've addressed the concern about empty updates.

The fix:
When the filtered function_call_results list is empty (after excluding already-streamed function_call items), we now clear update_role to suppress the update. This prevents emitting confusing empty ChatResponseUpdate events.

Implementation:

  • Filter out function_call items: filtered_results = [r for r in function_call_results if r.type != "function_call"]
  • If the filtered list is empty, set result["update_role"] = None
  • The existing yield check if role := result.get("update_role") will skip the empty update

This way declaration-only tools in streaming mode won't emit spurious empty updates—only the already-streamed chunks and any results/approvals will be emitted.

@kartikmadan11
kartikmadan11 force-pushed the fix/6973-declaration-only-streaming-duplicate-args branch from 7def61f to c06f65d Compare July 14, 2026 17:22
Comment thread python/packages/core/agent_framework/_tools.py Outdated
@kartikmadan11
kartikmadan11 force-pushed the fix/6973-declaration-only-streaming-duplicate-args branch from c06f65d to be7054d Compare July 16, 2026 13:21
…lls (microsoft#6973)

When streaming a declaration-only function call, the _stream() loop yields
function_call chunks from the inner stream, then _process_function_requests
re-emits the finalized call from function_call_results. _process_update merges
same-call_id function_call contents via +=, so re-emitting the full arguments
concatenated them twice: {"location":"Seattle"}{"location":"Seattle"}.

The finalized call gains its control metadata (user_input_request=True, id)
only during post-processing, so it must still reach the stream: AgentExecutor
relies on that signal to emit request_info and pause for client-side tools.

Fix: strip only the already-streamed arguments from function_call items in the
streamed update while preserving their metadata, instead of dropping the items
entirely. Also preserve id and user_input_request when merging function_call
contents in Content.__add__ so the aggregated response retains the signal.

Tests: split-chunk and single-chunk no-duplication, plus metadata preservation.

Fixes microsoft#6973
@kartikmadan11
kartikmadan11 force-pushed the fix/6973-declaration-only-streaming-duplicate-args branch from be7054d to 10aa390 Compare July 16, 2026 13:24
]

updates = []
async for update in chat_client_base.get_response( # type: ignore[call-overload] # pyrefly: ignore[no-matching-overload] # ty: ignore[no-matching-overload]

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.

we should use the get_final_response method on the stream object, to test this with

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, switched the test to use stream.get_final_response() and assert on the aggregated response. Thanks!

@github-actions

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _tools.py11708392%228–229, 406, 408, 421, 446–448, 456, 474, 488, 495, 502, 525, 527, 534, 542, 671, 705–707, 710–712, 714, 720, 771–773, 798, 824, 828, 866–868, 872, 894, 1045, 1057, 1064–1067, 1088, 1096, 1110–1112, 1462, 1554, 1582, 1604, 1612, 1670, 1717, 1724–1725, 1784, 1788, 1841, 1902–1903, 1914, 1986, 2000, 2003, 2016, 2019, 2042, 2049, 2059, 2063, 2140, 2193, 2215, 2271, 2350, 2560, 2626–2627, 2793–2794, 2885
   _types.py12269692%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, 1676–1678, 1963–1968, 1993, 2048, 2053, 2063, 2071, 2078–2082, 2100, 2173, 2186, 2191, 2304, 2327, 2588, 2612, 2711, 2892–2893, 2995, 3224, 3277, 3296, 3335, 3346, 3348–3352, 3354, 3357–3365, 3375, 3464, 3601, 3606, 3611, 3616, 3620, 3706–3708, 3737, 3825–3829
TOTAL44464523588% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
8986 33 💤 0 ❌ 0 🔥 2m 23s ⏱️

@eavanvalkenburg

Copy link
Copy Markdown
Member

@kartikmadan11 there are both open comments and failed checks, please have a look!

Rewrite the multi-chunk declaration-only streaming test to use
stream.get_final_response() instead of manually re-summing arguments
across streamed updates, per review feedback. Asserting against the
aggregated response mirrors the adjacent single-chunk test and matches
how consumers actually observe the result.

Also fixes the Test Typing Checks failure: the removed
`all_args += content.arguments or ""` concatenated a str with
`str | Mapping`, which zuban rejected.
@kartikmadan11

Copy link
Copy Markdown
Contributor Author

Both handled. Fixed the test per the review comment, which also clears the typing check failure. Rebased onto latest main so the merge gate should clear too.

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.

Python: [Bug]: Duplicate arguments in declaration-only function call when streamed

5 participants