agui-dotnet-transport
GitHub负责 AG-UI .NET SDK 传输层编码与事件流协商,支持 Protobuf 和 SSE,确保 Native AOT 兼容及字节级一致性。
Trigger Scenarios
Install
npx skills add ag-ui-protocol/ag-ui --skill agui-dotnet-transport -g -y
SKILL.md
Frontmatter
{
"name": "agui-dotnet-transport",
"description": "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)."
}
AG-UI .NET Transport & Encoding
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.
Transport architecture
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, inAGUI.Client) advertises every registered formatter'sMediaTypeinAccept, then inspects the responseContent-Type, finds the firstCanReadformatter, and records it on the request. The body is left untouched for lazy streaming.AGUIResponseExtensions.ReadAGUIEventStreamAsyncreads that recorded formatter (falling back to SSE) and decodes. The SDK ships noIHttpClientFactoryintegration: a caller that wants protobuf wires the handler into its ownHttpClient, and constructsAGUIChatClientfromAGUIChatClientOptions.
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>()).
Wire format facts
- Protobuf media type:
application/vnd.ag-ui.event+proto(ProtobufEventStreamFormatter.ProtobufMediaType). - Framing: 4-byte big-endian
uint32length prefix + protobuf message bytes, per event — matches@ag-ui/encoderencodeProtobuf(dataView.setUint32(0, length, false)). SeeAGUIProtobuf.WriteFramed/ReadFramedAsync. Encode/Decode= single message, no length prefix (mirror TSproto.encode/proto.decode).- Dynamic payloads (state, args, results) use
google.protobuf.Value(Struct/ListValue/scalars) — neverAnyand never a JSON-string field.
The CRITICAL Native AOT rule
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.
- Number caveat:
Valueis double-only.long/decimalbeyond 2^53 lose precision on round trip. This is intentional — it matches the JS@ag-ui/protolimitation.
Schema-first extension
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:
- Extend the shared
.proto(coordinated across all SDKs — it is the cross-language contract). - Add a mapper case in
ProtoEventMapper(event oneof) /ProtoMessageMapper(messages), mirroringsdks/typescript/packages/proto/src/proto.tsreshaping 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.
How to verify
- Byte-parity against
@ag-ui/protovia the cross-language tests (seeagui-dotnet-integration-testsandsdks/dotnet/docs/cross-language-testing.md). The JSON compatibility fixtures intests/AGUI.Abstractions.UnitTests/Compatibility/guard SSE drift. - Multi-TFM AOT build:
dotnet buildfromsdks/dotnet/(targets net10/9/8/netstandard2.0/ net472; warnings are errors). UpdatePublicAPI.Unshipped.txtfor any public surface change.
❌ Anti-patterns
- Don't embed JSON-as-string inside a
Value. Map structured payloads recursively to Struct/ListValue/scalars viaProtoValueConverter. A string field breaks @ag-ui/proto parity. - Don't use
JsonFormatter/JsonParser/descriptor reflection. Not AOT-safe — hand-write the bridge over generated WellKnownTypes. - Don't copy or fork the
.proto. Reference the canonical TS schema from the.csprojso the codec can't drift from the wire contract. - Don't make
AGUI.Protobufdepend onAGUI.Clientor the hosting/server package. The codec stays transport-neutral; it references onlyAGUI.Abstractions+AGUI.Formatting. - Don't change framing or use little-endian. Length is big-endian
uint32; both SDKs depend on exact byte layout.
References
- Wire format & codec internals (framing, oneof mapping, Value bridge, negotiation parity): references/wire-format.md
Version History
- 1a78c27 Current 2026-07-24 20:27


