Surface inbound AG-UI CUSTOM events as metadata instead of dropping them - #694
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.
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.
This comment has been minimized.
This comment has been minimized.
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.
# Conflicts: # provider/aguiprovider/agui.go # provider/aguiprovider/agui_test.go
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by Go API Consistency Review Agent · sonnet46 · 37.7 AIC · ⌖ 5.83 AIC · ⊞ 5.7K
| 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 and payload shape diverge from the Python client
The Python AG-UI client (_event_converters.py → _handle_custom_event) surfaces custom events under the key "ag_ui_custom_event" (with an underscore between ag and ui), whereas this Go implementation uses "agui_custom_event". Callers consuming both SDKs will need to check different keys for the same concept.
The Python payload also includes thread_id, run_id, and raw_type alongside name/value:
return ChatResponseUpdate(
role="assistant",
contents=[],
additional_properties={
"thread_id": self.thread_id,
"run_id": self.run_id,
"ag_ui_custom_event": {
"name": event.get("name"),
"value": event.get("value"),
"raw_type": raw_event_type,
},
},
)Upstream Python reference: python/packages/ag-ui/agent_framework_ag_ui/_event_converters.py
Suggested fixes:
- Rename the key to
"ag_ui_custom_event"to match the Python SDK. - Add
thread_idandrun_idtoAdditionalPropertieson the sameResponseUpdate(the Gorun()loop already injectsagui_thread_id; consider threading it through to the accumulator for symmetry). - Consider exposing a
raw_typefield if the GoCustomEventtype carries equivalent information.
There was a problem hiding this comment.
PratikDhanave (@PratikDhanave) can you resolve the parity gap?
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by Go API Consistency Review Agent · sonnet46 · 42.9 AIC · ⌖ 4.34 AIC · ⊞ 6K
| "name": e.Name, | ||
| "value": e.Value, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Parity gap: custom event metadata key diverges from Python upstream
The Python _handle_custom_event uses "ag_ui_custom_event" as the AdditionalProperties key (_event_converters.py L272), while this PR uses "agui_custom_event" (missing underscore between ag and ui). Any caller normalizing across SDKs using the Python key convention will silently find nothing from the Go provider.
Suggested fix: rename to "ag_ui_custom_event".
| }}, nil | ||
| case *aguiEvents.MessagesSnapshotEvent: | ||
| return []*agent.ResponseUpdate{{ | ||
| Role: message.RoleAssistant, |
There was a problem hiding this comment.
Parity gap: missing thread_id, run_id, and raw_type fields
The Python upstream surfaces three additional fields alongside ag_ui_custom_event in the AdditionalProperties of the custom event update (_event_converters.py L269–276):
additional_properties={
"thread_id": self.thread_id,
"run_id": self.run_id,
"ag_ui_custom_event": {
"name": ...,
"value": ...,
"raw_type": raw_event_type, # distinguishes CUSTOM vs CUSTOM_EVENT
},
}The Go implementation omits thread_id, run_id, and the raw_type sub-field. thread_id and run_id are already surfaced on other updates (e.g. RunStartedEvent and MessagesSnapshotEvent), and raw_type lets callers distinguish the two CUSTOM wire variants. Consider adding at minimum raw_type inside agui_custom_event and the session IDs at the top level to stay aligned with the Python contract.
Cross-Repo Parity Review — PR #694Verdict: Parity issues found. The fix is directionally correct (custom events should not be dropped), but the Go implementation diverges from the Python reference in three observable ways. What was compared
.NET: Issues
See the inline comment on Reviewed upstream surface:
|
There was a problem hiding this comment.
Generated by Go API Consistency Review Agent · sonnet46 · 46.6 AIC · ⌖ 4.43 AIC · ⊞ 6K
| CreatedAt: eventTime(evt), | ||
| AdditionalProperties: map[string]any{ | ||
| "agui_custom_event": map[string]any{ | ||
| "name": e.Name, |
There was a problem hiding this comment.
Parity gap: missing context metadata and raw_type field
The Python reference implementation (_event_converters.py → _handle_custom_event) includes thread_id, run_id, and raw_type alongside name/value:
additional_properties={
"thread_id": self.thread_id,
"run_id": self.run_id,
"ag_ui_custom_event": {
"name": event.get("name"),
"value": event.get("value"),
"raw_type": raw_event_type, # "CUSTOM" or "CUSTOM_EVENT"
},
}This Go implementation differs in three ways:
-
Missing
agui_thread_id/agui_run_id: Every otherResponseUpdateemitted here (RunStarted, RunFinished, StateSnapshot, etc.) includesagui_thread_idandagui_run_idfor consumer correlation. The custom-event update omits them, breaking session correlation for callers that rely on these keys. -
Missing
raw_type: Python surfaces the wire-level event type string ("CUSTOM"or"CUSTOM_EVENT") so consumers can distinguish sub-variants. Go discards it. -
Key name divergence (
"agui_custom_event"vs Python"ag_ui_custom_event"): Theagui_prefix is consistent with Go's own convention, so this may be intentional — but it should be documented as an explicit SDK divergence so consumers know cross-language payloads are not interchangeable.
Suggested fix — align with the pattern used for other metadata updates:
case *aguiEvents.CustomEvent:
a.customSeq++
return []*agent.ResponseUpdate{{
Role: message.RoleAssistant,
MessageID: fmt.Sprintf("agui-custom-%d", a.customSeq),
CreatedAt: eventTime(evt),
AdditionalProperties: map[string]any{
"agui_thread_id": a.threadID, // thread in from run() closure or accumulator field
"agui_run_id": a.runID,
"agui_custom_event": map[string]any{
"name": e.Name,
"value": e.Value,
},
},
}}, nil
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.