Python: [BREAKING]: Emit TOOL_CALL events for workflow participant tool calls in AG-UI#7039
Conversation
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.
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Automated Code Review
Reviewers: 5 | Confidence: 92%
✓ Correctness
The PR correctly modifies
_workflow_payload_to_contentsto 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_contentsonly 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_contentsgate (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_requestcontent 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
There was a problem hiding this comment.
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_contentsto pass through function-typed streamingAgentResponseUpdatecontent even when the update has norole, 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 ofworkflow_outputwrapping.
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. |
- 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.
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_contentsonly converted a streamingAgentResponseUpdateinto 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
_workflow_payload_to_contentsis 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.workflow_outputcustom 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.CustomEvent(name="workflow_output")payloads must migrate to the standard TOOL_CALL events, which is the shape the docs have described all along._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
breaking changelabel (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.