Skip to content

Close the open AG-UI text message before emitting non-text events - #1056

Merged
Quim Muntal (qmuntal) merged 2 commits into
microsoft:mainfrom
PratikDhanaveFork:agui-close-text-before-nontext
Sep 15, 2026
Merged

Quim Muntal (qmuntal) merged 2 commits into
microsoft:mainfrom
PratikDhanaveFork:agui-close-text-before-nontext

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

Problem

In provider/aguiprovider/hosting_events.go, when a text message is open and the next content is non-text (e.g. a FunctionCallContent tool call), the loop closes any open reasoning message but not the text message:

if !textLike {
    if !closeReasoning() { return }
    // text message left open
}

currentMessageID stays set, so TEXT_MESSAGE_END is only emitted at the end of the run. The emitted order becomes TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, TOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END, TEXT_MESSAGE_END — the tool-call lifecycle is nested inside an unclosed text message. This violates the file's own invariant that AG-UI lifecycles are disjoint (reasoning is already explicitly closed before a text message starts).

Fix

Close the open text message (alongside reasoning) before emitting non-text-like events.

Test

TestHandler_TextThenToolCall_ClosesTextBeforeToolLifecycle emits a text update then a tool-call update and asserts the lifecycle order is TEXT_MESSAGE_START/CONTENT/END then TOOL_CALL_START/ARGS/END. Fails before the fix (TEXT_MESSAGE_END lands after the tool call), passes after.

When a text message was open and the next content was non-text (e.g. a
FunctionCallContent tool call), the loop closed any open reasoning message but
not the text message, so currentMessageID stayed set and TEXT_MESSAGE_END was
only emitted at the end of the run. The tool-call lifecycle (TOOL_CALL_START/
ARGS/END) was therefore nested inside an unclosed text message, violating the
file's invariant that AG-UI lifecycles are disjoint (reasoning is already
closed before text starts).

Close the open text message alongside reasoning before emitting non-text-like
events.
Copilot AI lite review requested due to automatic review settings September 13, 2026 10:51
@github-actions github-actions Bot added area:provider Changes files in the provider area area:provider/agui Changes files in the provider / agui area size:medium At most 100 changed lines across at most 5 files labels Sep 13, 2026

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.

🟢 Approval recommended

No unresolved review issues; the fix includes regression coverage.

Pull request overview

Closes open AG-UI text messages before emitting non-text events, ensuring tool-call lifecycles are not nested inside text messages.

Changes:

  • Close active text messages before non-text events.
  • Add regression coverage for text followed by a tool call.
File summaries
File Description
provider/aguiprovider/hosting_test.go Verifies correct text/tool-call event ordering.
provider/aguiprovider/hosting_events.go Closes text lifecycles before non-text events.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions github-actions Bot added kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure pending-auto-risk labels Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope: user-visible behavior (internal helper change, observable via emitted AG-UI SSE event ordering)

Changed Go contract: No exported API changed. updatesToAGUIEvents (unexported) now calls the existing unexported closeText() helper alongside closeReasoning() before emitting non-text-like events (e.g. tool calls), so an open TEXT_MESSAGE_* lifecycle is closed before a TOOL_CALL_* lifecycle begins, instead of leaving TEXT_MESSAGE_END deferred until end-of-run.

Upstream evidence reviewed:

  • python/packages/ag-ui/agent_framework_ag_ui/_run_common.py, _emit_content() (around line ~1195): closes the open reasoning block via _close_reasoning_block(flow) when content type switches away from text_reasoning, and _emit_tool_result_common() (~line 848-851) explicitly closes any open text message (TextMessageEndEvent(message_id=flow.message_id), then flow.message_id = None) before/around tool-call-adjacent processing, keeping TEXT_MESSAGE_* and TOOL_CALL_* segments disjoint rather than nested.
  • python/packages/ag-ui/tests/ag_ui/golden/test_scenario_backend_tools.py (lines ~58-68): golden lifecycle assertion explicitly shows TEXT_MESSAGE_STARTTOOL_CALL_START ... TOOL_CALL_ENDTEXT_MESSAGE_END → new TEXT_MESSAGE_START (synthetic tool-only message ends before the following text message opens), confirming Python's invariant that text and tool-call lifecycles never nest.
  • No equivalent .NET AG-UI event-conversion source was found in dotnet/src/ (only ASP.NET Core hosting/serialization plumbing exists there; no _run_common.py-equivalent event-stream converter), so .NET provides no comparable evidence either way.

Result: aligned. This fix brings the Go implementation's disjoint-lifecycle invariant in line with the upstream Python AG-UI event converter, which already closes an open text message before/around tool-call events. The change is confined to unexported helpers with no exported API impact; the added test (TestHandler_TextThenToolCall_ClosesTextBeforeToolLifecycle) matches the ordering upheld by Python's golden backend-tools scenario. No parity issues found.

Generated by Go API Consistency Review Agent · copilot · auto · 48.2 AIC · ⌖ 5.28 AIC · ⊞ 9.6K ·

@qmuntal
Quim Muntal (qmuntal) added this pull request to the merge queue Sep 15, 2026
Merged via the queue into microsoft:main with commit 08556bb Sep 15, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider/agui Changes files in the provider / agui area area:provider Changes files in the provider area kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure size:medium At most 100 changed lines across at most 5 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants