Skip to content

Python: [BREAKING]: Emit TOOL_CALL events for workflow participant tool calls in AG-UI#7039

Merged
moonbox3 merged 3 commits into
microsoft:mainfrom
moonbox3:6994-fix
Jul 10, 2026
Merged

Python: [BREAKING]: Emit TOOL_CALL events for workflow participant tool calls in AG-UI#7039
moonbox3 merged 3 commits into
microsoft:mainfrom
moonbox3:6994-fix

Conversation

@moonbox3

@moonbox3 moonbox3 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

The Workflows with AG-UI docs list TOOL_CALL_START/TOOL_CALL_ARGS/TOOL_CALL_END as standard events emitted when an agent invokes a tool inside a workflow, and the worked example shows a handoff participant emitting them. In practice the workflow code path never emitted them for participant tool calls: the only TOOL_CALL events came from the request_info interrupt mechanism, and every participant tool call was wrapped in an informational CustomEvent(name="workflow_output") instead. The single-agent path emits the documented events for the identical tool, so anything downstream that observes tool calls by name (for example tool-call-interception middleware) could see calls from a standalone agent but structurally could not see the same call made by a workflow participant.

Root cause: _workflow_payload_to_contents only converted a streaming AgentResponseUpdate into chat content when the update carried an assistant role. Providers stream function-call chunks with no role and function-result chunks with the tool role, so both were rejected and fell through to the custom event. Because the shared emitters only produce TOOL_CALL_END when the function result flows through them, the rejected result also explains the missing END: one gate, all three missing event kinds.

Description & Review Guide

  • What are the major changes?
    • _workflow_payload_to_contents is now content-aware for streaming updates: function_call, function_result, and function_approval_request content passes through to the shared AG-UI content emitters regardless of the update's role. Text content keeps the existing assistant-role requirement unchanged.
    • Unit tests cover the gate matrix (role-less function call passes, tool-role function result passes, role-less approval request passes, non-assistant text still rejected, assistant text still passes).
    • An integration test drives the workflow run stream with a scripted participant tool call and asserts the full TOOL_CALL_START -> TOOL_CALL_ARGS -> TOOL_CALL_RESULT -> TOOL_CALL_END sequence with the correct tool call name.
    • A regression test asserts exactly one TOOL_CALL_START per call id when a workflow emits both streaming updates and a final response containing the same conversation; no production guard was needed because the final-response conversion takes only the latest assistant message's contents.
  • What is the impact of these changes?
    • Workflow participant tool calls (including handoff function calls) now emit the documented TOOL_CALL events through the same shared emitters as the single-agent path. Those function-content updates no longer appear as workflow_output custom events, so a tool call is represented exactly once in the stream. request_info interrupt events, text emission, and non-chat workflow outputs are unchanged; existing workflow golden scenarios do not exercise participant tool calls, so no snapshots changed.
    • Breaking for consumers of the previous (undocumented) stream shape: clients that detect participant tool calls by parsing CustomEvent(name="workflow_output") payloads must migrate to the standard TOOL_CALL events, which is the shape the docs have described all along.
  • What do you want reviewers to focus on?
    • The role-gate semantics in _workflow_payload_to_contents: function-typed content intentionally bypasses the role check while text intentionally does not, to keep this fix scoped to the reported behavior.

Related Issue

Fixes #6994

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically. This is a breaking change: the [BREAKING] title prefix and label are applied.

moonbox3 added 2 commits July 10, 2026 15:11
Decisions:
- Pass function call, function result, and approval request content from streaming agent updates regardless of role.
- Preserve the assistant-role gate for text and reuse the shared AG-UI content emitters without dual custom-event emission.

Files changed:
- packages/ag-ui/agent_framework_ag_ui/_workflow_run.py
- packages/ag-ui/tests/ag_ui/test_workflow_run.py

Verification:
- uv run poe test -P ag-ui
- uv run poe pyright -P ag-ui
- uv run poe test-typing -P ag-ui
- uv run poe syntax -P ag-ui -C

Notes:
- Existing workflow golden scenarios do not exercise participant tool calls, so no snapshot changed.
- No blockers.
Decisions:
- Assert the workflow stream emits one TOOL_CALL_START when a streamed call is also present in final conversation history.
- Keep production flow unchanged because latest-assistant final-response conversion prevents duplication.

Files changed:
- packages/ag-ui/tests/ag_ui/test_workflow_run.py

Verification:
- uv run pytest packages/ag-ui/tests/ag_ui/test_workflow_run.py -k 'participant_tool_call or repeat_tool_call' -q
- uv run poe test -P ag-ui
- uv run poe pyright -P ag-ui
- uv run poe test-typing -P ag-ui
- uv run poe syntax -P ag-ui -C
- git diff --check

Notes:
- No blockers; no call-id guard was required.
Copilot AI review requested due to automatic review settings July 10, 2026 06:20
@moonbox3 moonbox3 added python Usage: [Issues, PRs], Target: Python ag-ui Usage: [Issues, PRs], Target: AG-UI protocol integration labels Jul 10, 2026
@moonbox3 moonbox3 changed the title Python: Emit TOOL_CALL events for workflow participant tool calls in AG-UI Python: [BREAKING]: Emit TOOL_CALL events for workflow participant tool calls in AG-UI Jul 10, 2026
@moonbox3 moonbox3 added the breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible label Jul 10, 2026
@github-actions github-actions Bot changed the title Python: [BREAKING]: Emit TOOL_CALL events for workflow participant tool calls in AG-UI Python: [BREAKING] [BREAKING]: Emit TOOL_CALL events for workflow participant tool calls in AG-UI Jul 10, 2026
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/ag-ui/agent_framework_ag_ui
   _workflow_run.py6667488%78, 92, 94, 96, 99, 226–229, 278, 289, 294, 319, 355–358, 386, 391, 419, 429, 440, 445, 448, 461, 471, 474, 479, 482, 497–499, 504, 506–507, 511, 513, 530, 536–537, 547–548, 552–553, 577–578, 611, 619, 660, 680, 696, 711, 807–821, 853–854, 875–876, 947, 987, 1006
TOTAL44119526888% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
8848 33 💤 0 ❌ 0 🔥 2m 19s ⏱️

@github-actions github-actions Bot 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.

Automated Code Review

Reviewers: 5 | Confidence: 92%

✓ Correctness

The PR correctly modifies _workflow_payload_to_contents to let function-typed content (function_call, function_result, function_approval_request) pass through to the shared AG-UI emitters regardless of the update's role, while keeping the existing assistant-role requirement for text content. The implementation is clean: when role is 'assistant' all contents pass, otherwise only function-typed contents are extracted (returning None if none exist). The deduplication with final responses works because _latest_assistant_contents only takes the last assistant message, which typically contains text rather than repeating earlier tool calls. No correctness issues found.

✓ Security Reliability

The change widens the content gate in _workflow_payload_to_contents to allow function-typed content (function_call, function_result, function_approval_request) through regardless of the update's role, while keeping the assistant-role requirement for text content. The implementation is safe: Content.type is a constrained Literal type, the _FUNCTION_CONTENT_TYPES set contains only valid content type strings, downstream _emit_content properly dispatches each type, and the deduplication logic (via _latest_assistant_contents extracting only the last assistant message) prevents duplicate TOOL_CALL events from final responses. No injection risks, resource leaks, or unhandled failure modes identified.

✓ Test Coverage

Test coverage for the new behavior is solid. Unit tests cover all four content-type branches in the updated _workflow_payload_to_contents gate (function_call/no-role, function_result/tool-role, approval_request/no-role, assistant-text). The integration test validates the full TOOL_CALL event lifecycle through an actual Agent with tool execution, and the regression test confirms deduplication when a final response echoes streamed history. One minor gap: the behavior of mixed content (text + function_call) in a single non-assistant AgentResponseUpdate is not explicitly tested—only the function content would be returned while text is dropped.

✓ Failure Modes

The change to _workflow_payload_to_contents correctly gates function-typed content through regardless of role while preserving the assistant-role requirement for text. The role=None path correctly falls through to the function content filter (str(None) becomes "None" which != "assistant"), deduplication of repeated tool calls is handled by the existing flow.tool_call_id guard in _emit_tool_call (line 516 of _run_common.py), and the _latest_assistant_contents helper ensures final AgentResponse payloads only extract the last assistant message (avoiding re-emission of earlier function_call content). No silent failures, lost errors, or missing cleanup paths introduced.

✓ Design Approach

The new role-bypass fixes participant tool call emission, but it also broadens the workflow path to admit streamed function_approval_request content without wiring the corresponding resume bookkeeping. That leaves one concrete workflow input shape able to emit an approval interrupt in turn 1 that the workflow cannot recognize or resume in turn 2.


Automated review by moonbox3's agents

Comment thread python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py Outdated

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

This PR fixes the Python AG-UI workflow runner so that tool calls made by workflow participants emit the standard AG-UI TOOL_CALL_* event lifecycle (instead of being wrapped as CUSTOM workflow_output), aligning workflow participant behavior with the single-agent path.

Changes:

  • Update _workflow_payload_to_contents to pass through function-typed streaming AgentResponseUpdate content even when the update has no role, while keeping the existing assistant-only gate for text.
  • Add unit tests that validate the role/content-type gating matrix for _workflow_payload_to_contents.
  • Add a workflow-stream test that drives a participant tool call and asserts the emitted TOOL_CALL_* events and absence of workflow_output wrapping.

Reviewed changes

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

File Description
python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py Adjusts workflow output payload → AG-UI content conversion so function-typed streaming updates are emitted via shared tool-call emitters even when role metadata is missing.
python/packages/ag-ui/tests/ag_ui/test_workflow_run.py Adds unit + integration tests to validate the new role/content gating and the resulting tool-call event stream behavior.

Comment thread python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py Outdated
Comment thread python/packages/ag-ui/tests/ag_ui/test_workflow_run.py
- Exclude approval request content from the role bypass. Workflow approvals
  resume through request_info pending state, so an approval interrupt emitted
  from streamed content would have no pending request to resume against.
- Admit mcp_server_tool_call and mcp_server_tool_result so provider-hosted MCP
  tool calls from workflow participants emit standard tool call events.
- Add unit tests for MCP passthrough, approval exclusion, and mixed
  text-plus-tool content in non-assistant updates.
@moonbox3 moonbox3 changed the title Python: [BREAKING] [BREAKING]: Emit TOOL_CALL events for workflow participant tool calls in AG-UI Python: [BREAKING]: Emit TOOL_CALL events for workflow participant tool calls in AG-UI Jul 10, 2026
@moonbox3 moonbox3 self-assigned this Jul 10, 2026
@moonbox3 moonbox3 enabled auto-merge July 10, 2026 06:41
@moonbox3 moonbox3 added this pull request to the merge queue Jul 10, 2026
Merged via the queue into microsoft:main with commit 87af313 Jul 10, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ag-ui Usage: [Issues, PRs], Target: AG-UI protocol integration breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Workflow participant tool calls emit as CustomEvent, not TOOL_CALL_START/ARGS, despite documented behavior

4 participants