mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fix doubled tool_call arguments in MESSAGES_SNAPSHOT when streaming (#4200)
* fix: prevent doubled tool_call arguments in MESSAGES_SNAPSHOT
When streaming with client-side tools, some providers send a full-
arguments replay after the streaming deltas complete. The `_emit_tool_call`
function unconditionally appends every arguments delta to the internal
`flow.tool_calls_by_id` tracking dictionary via `+=`. When the replay
contains the exact same complete arguments string that was already
accumulated from prior deltas, the arguments get doubled (e.g.,
`{"todoText":"buy groceries"}{"todoText":"buy groceries"}`).
This causes `MESSAGES_SNAPSHOT` events to contain invalid doubled JSON in
`tool_calls[].function.arguments`, breaking any client or middleware that
relies on snapshots for state reconstruction.
The fix adds a guard (mirroring the existing duplicate guard in
`_emit_text`) that detects when the incoming delta exactly equals the
already-accumulated arguments string, indicating a full-arguments replay
rather than an incremental delta. In this case the append is skipped,
preventing the doubling.
The `ToolCallArgsEvent` deltas are still emitted correctly for real-time
streaming — only the internal snapshot accumulator is guarded.
Fixes #4194
* fix: move duplicate check before event emission + add test
Address Copilot review feedback:
1. Move duplicate full-arguments replay detection BEFORE emitting
ToolCallArgsEvent, for consistency with _emit_text() which returns
early without emitting any events on replay detection.
2. Add test_emit_tool_call_skips_duplicate_full_arguments_replay() to
verify the duplicate detection behavior for tool call arguments,
matching the existing test pattern for text content.
This commit is contained in:
@@ -195,6 +195,23 @@ def _emit_tool_call(
|
||||
delta = (
|
||||
content.arguments if isinstance(content.arguments, str) else json.dumps(make_json_safe(content.arguments))
|
||||
)
|
||||
|
||||
if tool_call_id in flow.tool_calls_by_id:
|
||||
accumulated = flow.tool_calls_by_id[tool_call_id]["function"]["arguments"]
|
||||
# Guard against full-argument replay: if the accumulated arguments
|
||||
# already equal the incoming delta, this is a non-delta replay of
|
||||
# the complete arguments string (some providers send the full
|
||||
# arguments again after streaming deltas). Skip the event emission
|
||||
# and accumulation to prevent doubling in MESSAGES_SNAPSHOT.
|
||||
# This mirrors the early-return behaviour of _emit_text().
|
||||
# (Fixes #4194)
|
||||
if accumulated and delta == accumulated:
|
||||
logger.debug(
|
||||
"Skipping duplicate full-arguments replay for tool_call_id=%s",
|
||||
tool_call_id,
|
||||
)
|
||||
return events
|
||||
|
||||
events.append(ToolCallArgsEvent(tool_call_id=tool_call_id, delta=delta))
|
||||
|
||||
if tool_call_id in flow.tool_calls_by_id:
|
||||
|
||||
Reference in New Issue
Block a user