Skip to content

Python: [Bug]: AG-UI starts each tool call twice when an agent calls two declaration-only tools in one response #9638

Open

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

  1. Register two declaration-only tools on an agent served through the AG-UI endpoint.
  2. Send a message that makes the model call both tools in one response.
  3. Observe two TOOL_CALL_START events per call id, and each call twice in the thread snapshot.
  4. 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.

  1. 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 :2510 and :2758).
  2. The AG-UI stream passes those copies to _emit_content like any other content (python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py:3893), which routes them to _emit_tool_call.
  3. _emit_tool_call treats 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 to flow.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.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 23, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Oct 23, 2026
  3. github-actions commented on Oct 23, 2026

    @github-actions
    Contributor

    🤖 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_call around 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 and flow.pending_tool_calls become A, B, A, B.

    • Failing test: python/packages/ag-ui/tests/ag_ui/test_run_common.py::test_replayed_declaration_only_tool_calls_are_started_once
    • Files examined: python/packages/core/agent_framework/_tools.py, python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py, python/packages/ag-ui/agent_framework_ag_ui/_run_common.py, python/packages/ag-ui/tests/ag_ui/test_run_common.py, python/packages/ag-ui/pyproject.toml
    • Tests run: python/packages/ag-ui/tests/ag_ui/test_run_common.py (79 passed baseline), python/packages/ag-ui/tests/ag_ui/test_run_common.py::test_replayed_declaration_only_tool_calls_are_started_once (failed)
    • Reported version: 1.5.0
    • Current version: 1.7.0
  4. added
    ag-uiUsage: [Issues, PRs], Target: AG-UI protocol integration
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

ag-uiUsage: [Issues, PRs], Target: AG-UI protocol integrationpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions