Python: Fix duplicate arguments in declaration-only tool streaming#7110
Conversation
There was a problem hiding this comment.
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_callitems out of the post-processing streamedfunction_call_resultsupdate to prevent_process_updatefrom 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. |
026ca32 to
7def61f
Compare
|
Great catch, Copilot! I've addressed the concern about empty updates. The fix: Implementation:
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. |
…lls (microsoft#6973) When streaming a declaration-only function call, the _stream() loop yields function_call chunks from the inner stream (line ~2800), then later re-yields the same function_call items from function_call_results (line ~2847). The _process_update callback concatenates streaming arguments via +=, so duplicate yields cause the arguments to be concatenated twice: {"location":"Seattle"}{"location":"Seattle"} Filter out function_call type items from the streamed update, since they were already emitted by the inner stream. Only yield function_result and approval items, which are newly generated by _process_function_requests. Tests: both split-chunk and single-chunk streaming of declaration-only tools. Fixes microsoft#6973
7def61f to
c06f65d
Compare
| # Exclude function_call items from the streamed update: the inner stream already yielded | ||
| # them as they arrived, so re-yielding them would cause argument strings to be concatenated | ||
| # twice in _process_update (which uses +=) for declaration-only tools. | ||
| filtered_results = [r for r in function_call_results if r.type != "function_call"] |
There was a problem hiding this comment.
Could we preserve the finalized declaration-only call’s control metadata here? The earlier chunks contain the arguments, but user_input_request=True and id are added later to function_call_results. Filtering the finalized call entirely means streaming workflows never receive that signal, so AgentExecutor cannot emit request_info or pause for the client-side tool. Could the streaming path suppress only the duplicated arguments while retaining the finalized metadata?
Motivation & Context
When streaming declaration-only function calls, the final response contains duplicated arguments.
Fixes #6973
Description & Review Guide
What are the major changes?
_process_function_requests()in_tools.py(line 2403) to filter outfunction_calltype items from the streamed result updateWhat is the impact?
Root Cause: The
_stream()loop yieldsfunction_callchunks as they arrive, then later re-yields those same items fromfunction_call_results. The_process_update()callback concatenates arguments with+=, so duplicate yields cause duplication.Solution: Filter out
function_calltype items from the result'sfunction_call_resultsbefore yielding, since they were already emitted by the inner stream.Tests Added
test_declaration_only_tool_streaming_no_argument_duplication()- Verifies multi-chunk streamingtest_declaration_only_tool_streaming_single_chunk_no_duplication()- Verifies single-chunk streamingBoth tests pass ✅