Surface inbound AG-UI CUSTOM events as metadata instead of dropping them#694
Surface inbound AG-UI CUSTOM events as metadata instead of dropping them#694PratikDhanave wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request updates the AG-UI provider’s event-to-response mapping so inbound CUSTOM AG-UI events are no longer silently dropped, and become observable to callers as response/message metadata.
Changes:
- Handle
*aguiEvents.CustomEventin(*toolCallAccumulator).onEventby emitting a metadata-onlyResponseUpdatecontainingAdditionalProperties["agui_custom_event"]. - Add an integration-style SSE test to verify a CUSTOM event is surfaced into collected response messages.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| provider/aguiprovider/agui.go | Adds a CustomEvent case that converts inbound CUSTOM frames into a metadata-only assistant update. |
| provider/aguiprovider/agui_test.go | Adds a test that streams a CUSTOM event and asserts it is surfaced via AdditionalProperties["agui_custom_event"]. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| case *aguiEvents.CustomEvent: | ||
| return []*agent.ResponseUpdate{{ | ||
| Role: message.RoleAssistant, | ||
| CreatedAt: eventTime(evt), | ||
| AdditionalProperties: map[string]any{ | ||
| "agui_custom_event": map[string]any{ | ||
| "name": e.Name, | ||
| "value": e.Value, | ||
| }, | ||
| }, | ||
| }}, nil |
There was a problem hiding this comment.
Good catch — fixed in 77bdb0a: each custom event now gets a unique synthetic MessageID (agui-custom-<n>) so it forms its own message instead of being merged into the last assistant message. This stops multiple custom events from overwriting each other under the shared agui_custom_event key via maps.Copy, and keeps them off unrelated assistant content. Added a black-box test that fails without the fix.
This comment has been minimized.
This comment has been minimized.
801b2a1 to
ce9dff7
Compare
This comment has been minimized.
This comment has been minimized.
The AG-UI event accumulator dropped decoded CUSTOM events: onEvent had no case for *events.CustomEvent, so every CUSTOM frame fell through to the default arm and was silently discarded. Emit a metadata-only assistant ResponseUpdate carrying the event name and value under AdditionalProperties["agui_custom_event"], matching how the .NET and Python AG-UI clients expose custom events to callers rather than swallowing them.
Custom events previously emitted a ResponseUpdate with no MessageID, so they were merged into the last assistant message. Because Response.Update uses maps.Copy for AdditionalProperties, multiple custom events in a stream overwrote each other under the shared agui_custom_event key and attached to unrelated assistant content. Assign each custom event a unique synthetic MessageID so it forms its own message and is preserved.
77bdb0a to
77bed09
Compare
This comment has been minimized.
This comment has been minimized.
Cross-repo parity review — PR #694Scope: Upstream reference: Parity issues found1.
|
| SDK | Key used |
|---|---|
| Go (this PR) | "agui_custom_event" |
| Python | "ag_ui_custom_event" |
Python uses ag_ui_ (matching the "AG-UI" protocol) while Go uses agui_. Callers that introspect AdditionalProperties cross-SDK will need different key spellings. The Go key should be "ag_ui_custom_event" to stay consistent.
2. thread_id / run_id not included in Go custom-event response
Python's _handle_custom_event always populates thread_id and run_id in additional_properties alongside the custom-event payload (these are tracked state on the converter). The Go implementation omits them, so consumers of the custom-event update lose correlation context they get from the Python SDK.
3. raw_type field not present in Go
Python records the original event type string (raw_event_type) as "raw_type" in the custom event's metadata object. This allows callers to distinguish between CUSTOM and CUSTOM_EVENT spellings at runtime. The Go path handles only *aguiEvents.CustomEvent (a single typed variant) so there may not be an equivalent distinction, but if there is, it should be surfaced for parity.
Upstream Python surface: microsoft/agent-framework → python/packages/ag-ui/agent_framework_ag_ui/_event_converters.py → _handle_custom_event
Recommend aligning the Go key name to "ag_ui_custom_event" and adding thread_id/run_id correlation fields to match Python semantics before merging.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
awmgmcpg
To allow these domains, add them to the
network.allowedlist in your workflow frontmatter:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
Generated by Go API Consistency Review Agent · 45 AIC · ⌖ 4.53 AIC · ⊞ 5.9K · ◷
There was a problem hiding this comment.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
awmgmcpg
To allow these domains, add them to the
network.allowedlist in your workflow frontmatter:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
Generated by Go API Consistency Review Agent · 45 AIC · ⌖ 4.53 AIC · ⊞ 5.9K
| MessageID: fmt.Sprintf("agui-custom-%d", a.customSeq), | ||
| CreatedAt: eventTime(evt), | ||
| AdditionalProperties: map[string]any{ | ||
| "agui_custom_event": map[string]any{ |
There was a problem hiding this comment.
Parity issue — key name diverges from Python SDK
The upstream Python _handle_custom_event uses "ag_ui_custom_event" (with underscore after ag) as the additional_properties key. This PR uses "agui_custom_event" (no underscore). Callers inspecting AdditionalProperties cross-SDK will need different spellings.
Suggested rename to "ag_ui_custom_event" to match microsoft/agent-framework → _event_converters.py.
| CreatedAt: eventTime(evt), | ||
| AdditionalProperties: map[string]any{ | ||
| "agui_custom_event": map[string]any{ | ||
| "name": e.Name, |
There was a problem hiding this comment.
Parity issue — missing thread_id/run_id correlation fields
The Python _handle_custom_event includes thread_id and run_id in the additional_properties alongside the custom-event payload (they are tracked state in the converter). Go omits them, so consumers of this update lose run-correlation context that Python callers receive.
Consider adding the AG-UI thread/run IDs here to keep observability parity with the Python SDK.
What
The AG-UI event accumulator in
provider/aguiprovider/agui.gosilently dropped inboundCUSTOMevents.(*toolCallAccumulator).onEventhad no case for*events.CustomEvent, so a decoded CUSTOM frame fell straight through to thedefault:arm and returnednil, nil— the event never reached the caller even though the decoder handled the CUSTOM wire type without error.This adds a case that emits a single metadata-only assistant
ResponseUpdate(noContents) with the event surfaced underAdditionalProperties["agui_custom_event"]as{name, value}. The existingrun()loop already lazily initializesAdditionalPropertiesand injectsagui_thread_id, so the update flows through unchanged.Why
CUSTOM is a first-class AG-UI event type used for application-specific signals (e.g.
predictive_state). The .NET and Python AG-UI clients expose custom events to callers rather than discarding them; dropping them here is a cross-SDK parity gap. Surfacing them as message metadata keeps them out of the assistant text/content stream while still making them observable to consumers.How tested
Added
TestAGUIAgentRun_SurfacesCustomEventAsMetadatainagui_test.go: anhttptestSSE server emitsNewCustomEvent("predictive_state", WithValue(map[string]any{"foo":"bar"})), the agent runs, and the test asserts one collected message carriesAdditionalProperties["agui_custom_event"]with the expected name and value map. The test fails before the fix (no such update is emitted) and passes after.go build ./...,go vet ./provider/aguiprovider/..., andgo test ./provider/aguiprovider/...all pass.