Skip to content

Surface inbound AG-UI CUSTOM events as metadata instead of dropping them - #694

Open
PratikDhanave (PratikDhanave) wants to merge 5 commits into
microsoft:mainfrom
PratikDhanaveFork:surface-agui-custom-events
Open

Surface inbound AG-UI CUSTOM events as metadata instead of dropping them#694
PratikDhanave (PratikDhanave) wants to merge 5 commits into
microsoft:mainfrom
PratikDhanaveFork:surface-agui-custom-events

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

What

The AG-UI event accumulator in provider/aguiprovider/agui.go silently dropped inbound CUSTOM events. (*toolCallAccumulator).onEvent had no case for *events.CustomEvent, so a decoded CUSTOM frame fell straight through to the default: arm and returned nil, 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 (no Contents) with the event surfaced under AdditionalProperties["agui_custom_event"] as {name, value}. The existing run() loop already lazily initializes AdditionalProperties and injects agui_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_SurfacesCustomEventAsMetadata in agui_test.go: an httptest SSE server emits NewCustomEvent("predictive_state", WithValue(map[string]any{"foo":"bar"})), the agent runs, and the test asserts one collected message carries AdditionalProperties["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/..., and go test ./provider/aguiprovider/... all pass.

Copilot AI review requested due to automatic review settings July 24, 2026 01:55
@PratikDhanave
PratikDhanave (PratikDhanave) requested a review from a team as a code owner July 24, 2026 01:55

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 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.CustomEvent in (*toolCallAccumulator).onEvent by emitting a metadata-only ResponseUpdate containing AdditionalProperties["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.

Comment thread provider/aguiprovider/agui.go
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the parity-approved Go API consistency review found no parity issues label Jul 24, 2026
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.
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added public-api-change Pull Request changes public APIs and removed parity-approved Go API consistency review found no parity issues labels Jul 24, 2026
@github-actions

This comment has been minimized.

@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.

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.allowed list 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{

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.

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,

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.

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

This comment has been minimized.

@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.

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{

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.

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:

  1. Rename the key to "ag_ui_custom_event" to match the Python SDK.
  2. Add thread_id and run_id to AdditionalProperties on the same ResponseUpdate (the Go run() loop already injects agui_thread_id; consider threading it through to the accumulator for symmetry).
  3. Consider exposing a raw_type field if the Go CustomEvent type carries equivalent information.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PratikDhanave (@PratikDhanave) can you resolve the parity gap?

@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:large At most 300 changed lines across at most 10 files pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026
@github-actions

This comment has been minimized.

@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.

Generated by Go API Consistency Review Agent · sonnet46 · 42.9 AIC · ⌖ 4.34 AIC · ⊞ 6K

"name": e.Name,
"value": e.Value,
},
},

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.

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,

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.

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.

@github-actions github-actions Bot added failed-auto-risk Automatic risk classification was inconclusive or failed and removed pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026
@github-actions github-actions Bot added the pending-auto-risk Automatic risk classification is in progress label Aug 22, 2026
@github-actions github-actions Bot added risk:medium Contained production impact requiring normal review depth and removed failed-auto-risk Automatic risk classification was inconclusive or failed pending-auto-risk Automatic risk classification is in progress labels Aug 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Cross-Repo Parity Review — PR #694

Verdict: 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

Go (this PR) Python (_event_converters.py → _handle_custom_event)
Metadata key agui_custom_event ag_ui_custom_event
Session context ❌ no agui_thread_id / agui_run_id thread_id + run_id in same additional_properties
Raw event type ❌ not included raw_type ("CUSTOM" or "CUSTOM_EVENT")

.NET: Microsoft.Agents.AI.AGUI currently contains only a README; the .NET client-side custom-event path has not yet shipped, so .NET is not a constraint here.

Issues

  1. Missing agui_thread_id / agui_run_id — every other ResponseUpdate from this accumulator carries these keys so consumers can correlate updates back to the running session. The custom-event update is the only one that omits them. This is a regression relative to the existing API contract exposed by the Go SDK itself, independent of cross-language parity.

  2. Missing raw_type — the AG-UI wire protocol permits both CUSTOM and CUSTOM_EVENT event types; Python surfaces the raw type so callers can distinguish future sub-variants. Go discards it.

  3. Key name (agui_custom_event vs ag_ui_custom_event) — the agui_ prefix is consistent with Go's existing convention (agui_thread_id, agui_run_id), so this divergence may be intentional. If so, it should be documented explicitly so cross-language consumers know the payloads differ.

See the inline comment on agui.go line 521 for a suggested fix.


Reviewed upstream surface: python/packages/ag-ui/agent_framework_ag_ui/_event_converters.py (commit on main)

Generated by Go API Consistency Review Agent · sonnet46 · 46.6 AIC · ⌖ 4.43 AIC · ⊞ 6K ·

@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.

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,

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.

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:

  1. Missing agui_thread_id / agui_run_id: Every other ResponseUpdate emitted here (RunStarted, RunFinished, StateSnapshot, etc.) includes agui_thread_id and agui_run_id for consumer correlation. The custom-event update omits them, breaking session correlation for callers that rely on these keys.

  2. Missing raw_type: Python surfaces the wire-level event type string ("CUSTOM" or "CUSTOM_EVENT") so consumers can distinguish sub-variants. Go discards it.

  3. Key name divergence ("agui_custom_event" vs Python "ag_ui_custom_event"): The agui_ 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

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 public-api-change Pull Request changes public APIs risk:medium Contained production impact requiring normal review depth size:large At most 300 changed lines across at most 10 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants