Add or modify a wire transport / event-stream encoding in the AG-UI .NET SDK — the protobuf codec, the SSE format, content negotiation, the JsonElement-to-protobuf Value bridge, or a brand new encoding — while preserving Native AOT compatibility and byte-level wire compatibility with @ag-ui/proto. USE FOR: working on AGUI.Formatting / AGUI.Protobuf, IAGUIEventStreamFormatter, transport content negotiation, the JsonElement-to-google.protobuf.Value bridge, SSE or protobuf framing, server formatter registration / AGUIResults.Events negotiation. DO NOT USE FOR: adding a new wire event TYPE (use agui-dotnet-wire-types), writing tests (use agui-dotnet-integration-tests).
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Encodes the non-obvious design constraints of the AG-UI .NET transport layer, discovered while
building protobuf support. Apply when adding/changing an encoding or the negotiation that selects
one. Read references/wire-format.md before touching the protobuf codec or framing.
One bidirectional abstraction, IAGUIEventStreamFormatter (in AGUI.Formatting), serves every
transport and both directions:
| Member | Role |
|---|---|
MediaType | Advertised in client Accept and written as server Content-Type. Registration order = preference. |
CanRead(contentType) | Client picks the decoder for the response Content-Type. |
ReadAsync(body, ct) | Decode body → IAsyncEnumerable<BaseEvent>. |
WriteAsync(events, output, ct) | Encode events → body. |
SseEventStreamFormatter (text/event-stream) is the always-available default;
ProtobufEventStreamFormatter (ProtobufEventStreamFormatter.ProtobufMediaType) is opt-in.
Client negotiation = DelegatingHandler + decode helper:
AGUIEventStreamHandler (public, in AGUI.Client) advertises every registered formatter's
MediaType in Accept, then inspects the response Content-Type, finds the first CanRead
formatter, and records it on the request. The body is left untouched for lazy streaming.AGUIResponseExtensions.ReadAGUIEventStreamAsync reads that recorded formatter (falling back to
SSE) and decodes. The SDK ships no IHttpClientFactory integration: a caller that wants protobuf
wires the handler into its own HttpClient, and constructs AGUIChatClient from
AGUIChatClientOptions.Server negotiation = AGUIResults.Events (samples AGUI.Samples.Shared): collects registered
IAGUIEventStreamFormatter services (+ built-in SSE), then picks protobuf only when its media
type is explicitly present in Accept with non-zero quality, else SSE for text/event-stream/
wildcard/absent, else 406. Mirrors preferredMediaTypes(accept, [proto]) in @ag-ui/encoder.
A server opts in by registering the formatter (for example,
services.AddSingleton<IAGUIEventStreamFormatter, ProtobufEventStreamFormatter>()).
application/vnd.ag-ui.event+proto (ProtobufEventStreamFormatter.ProtobufMediaType).uint32 length prefix + protobuf message bytes, per event —
matches @ag-ui/encoder encodeProtobuf (dataView.setUint32(0, length, false)). See
AGUIProtobuf.WriteFramed / ReadFramedAsync.Encode/Decode = single message, no length prefix (mirror TS proto.encode/proto.decode).google.protobuf.Value (Struct/ListValue/scalars) —
never Any and never a JSON-string field.Implement the JsonElement <-> google.protobuf.Value bridge by hand over the generated
WellKnownTypes (ProtoValueConverter). NEVER use Google.Protobuf's reflection-based
JsonFormatter/JsonParser or any descriptor reflection API — they are not trim/AOT safe and the
package multi-targets net10/9/8/netstandard2.0/net472.
Value is double-only. long/decimal beyond 2^53 lose precision on round
trip. This is intentional — it matches the JS @ag-ui/proto limitation.The .proto schema is canonical and lives in the TS package. AGUI.Protobuf.csproj
<Protobuf> references sdks/typescript/packages/proto/src/proto/*.proto directly
(csharp_namespace = AGUI.ProtocolBuffers, generated types Access="Internal") — do not fork or
copy it. To add a wire-representable event:
.proto (coordinated across all SDKs — it is the cross-language contract).ProtoEventMapper (event oneof) / ProtoMessageMapper (messages),
mirroring sdks/typescript/packages/proto/src/proto.ts reshaping verbatim.Subset coverage is intentional: .NET-only events that have no wire representation throw
NotSupportedException from the mapper's default case. Don't invent a wire shape unilaterally.
@ag-ui/proto via the cross-language tests (see
agui-dotnet-integration-tests and sdks/dotnet/docs/cross-language-testing.md). The JSON
compatibility fixtures in tests/AGUI.Abstractions.UnitTests/Compatibility/ guard SSE drift.dotnet build from sdks/dotnet/ (targets net10/9/8/netstandard2.0/
net472; warnings are errors). Update PublicAPI.Unshipped.txt for any public surface change.Value. Map structured payloads recursively to
Struct/ListValue/scalars via ProtoValueConverter. A string field breaks @ag-ui/proto parity.JsonFormatter/JsonParser/descriptor reflection. Not AOT-safe — hand-write the
bridge over generated WellKnownTypes..proto. Reference the canonical TS schema from the .csproj so the
codec can't drift from the wire contract.AGUI.Protobuf depend on AGUI.Client or the hosting/server package. The codec
stays transport-neutral; it references only AGUI.Abstractions + AGUI.Formatting.uint32; both SDKs depend
on exact byte layout.34c3e0c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.