Repository navigation
Python: [Bug]: AG-UI starts each tool call twice when an agent calls two declaration-only tools in one response #9638
Description
Observed Behavior
When the model calls two declaration-only (client-side) tools in one response, the AG-UI endpoint starts each tool call twice. The client receives TOOL_CALL_START twice for each call id, and the thread snapshot records each call twice. The next run then fails, because the provider rejects the duplicate call ids.
A single client-side tool call per response is not affected.
Expected Behavior
Each tool call is started once and recorded once in the snapshot, however many declaration-only calls the response contains.
Steps to Reproduce
- Register two declaration-only tools on an agent served through the AG-UI endpoint.
- Send a message that makes the model call both tools in one response.
- Observe two
TOOL_CALL_STARTevents per call id, and each call twice in the thread snapshot. - Send the tool results back: the provider rejects the run because the call ids repeat.
Minimal Reproduction
from ag_ui.core import ToolCallStartEvent
from agent_framework import Content
from agent_framework_ag_ui._run_common import FlowState, _emit_content
flow = FlowState()
streamed = [
Content.from_function_call(call_id="call_a", name="get_weather", arguments='{"city": "NYC"}'),
Content.from_function_call(call_id="call_b", name="get_time", arguments='{"zone": "UTC"}'),
]
# The function loop hands declaration-only calls back as caller-input pauses after they stream.
replayed = [
Content.from_function_call(call_id="call_a", name="get_weather"),
Content.from_function_call(call_id="call_b", name="get_time"),
]
for pause in replayed:
pause.user_input_request = True
events = [e for c in streamed + replayed for e in _emit_content(c, flow)]
print([e.tool_call_id for e in events if isinstance(e, ToolCallStartEvent)])
print([call["id"] for call in flow.pending_tool_calls])Output on main (a8e284c) |
|
|---|---|
| Expected | ['call_a', 'call_b'] and ['call_a', 'call_b'] |
| Actual | ['call_a', 'call_b', 'call_a', 'call_b'] and ['call_a', 'call_b', 'call_a', 'call_b'] |
Error Messages and Stack Traces
tool_use ids must be unique
(Anthropic's 400 response on the run after the duplicated snapshot.)
Package Versions
agent-framework-core: 1.20.0, agent-framework-ag-ui: 1.5.0, agent-framework-anthropic: 1.0.0b261002
Python Version
Python 3.13.16
Operating System
Linux
Regression
Unknown
Additional Context
Cause.
- After the calls stream, the function loop returns each declaration-only call again as a caller-input pause (
_as_user_input_pause,python/packages/core/agent_framework/_tools.py:2482, used at:2510and:2758). - The AG-UI stream passes those copies to
_emit_contentlike any other content (python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py:3893), which routes them to_emit_tool_call. _emit_tool_calltreats a call as new when its id differs from the last call id (python/packages/ag-ui/agent_framework_ag_ui/_run_common.py:733):if content.name and tool_call_id != flow.tool_call_id:. The stream is A, B, then the replay A, B. Replayed A differs from the last id (B), so A starts again; replayed B then differs from A, so B starts again. Each restart also appends a second entry toflow.pending_tool_calls, which the snapshot is built from.
Proposed fix. Start a call only when its id is not yet registered in this run: if content.name and tool_call_id not in flow.tool_calls_by_id:. A replayed call then takes the existing continuation branch. Every writer of flow.tool_calls_by_id emits TOOL_CALL_START when it registers the id, so a registered id has always been started. I have this change and a regression test ready, and will open a PR once the direction is agreed.
Platform. Observed in the server's Debian 13 (aarch64) container. The cause is platform-independent: the reproduction gives the same output on macOS.
Workaround. We wrap _emit_tool_call so a call id already in flow.tool_calls_by_id is made the current call before the original function runs.
AI assistance. This report was drafted with AI assistance. I verified the reproduction and the analysis.
Acknowledgements
- I searched existing issues and did not find a duplicate.
- I personally verified this behavior and the reproduction details are authentic.
- I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
github-actions commentedon Oct 23, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
The bug occurs in
python/packages/ag-ui/agent_framework_ag_ui/_run_common.py::_emit_tool_callaround line 733 when previously registered call IDs are replayed non-consecutively. Minimal repro: pass function-call contents with IDs A, B, then caller-input pause copies A, B through_emit_content; both emitted start IDs andflow.pending_tool_callsbecome A, B, A, B.python/packages/ag-ui/tests/ag_ui/test_run_common.py::test_replayed_declaration_only_tool_calls_are_started_once1.5.01.7.0