SKILL.md
SKILL.mdBrowse 2 files
1,592 tokens
6,390 bytes
Token encoding: o200k_base
Snapshot cd06f5d
1---2name: agui-dotnet-transport3description: >4 Add or modify a wire transport / event-stream encoding in the AG-UI .NET SDK — the protobuf5 codec, the SSE format, content negotiation, the JsonElement-to-protobuf Value bridge, or a brand6 new encoding — while preserving Native AOT compatibility and byte-level wire compatibility with7 @ag-ui/proto. USE FOR: working on AGUI.Formatting / AGUI.Protobuf, IAGUIEventStreamFormatter,8 transport content negotiation, the JsonElement-to-google.protobuf.Value bridge, SSE or protobuf9 framing, server formatter registration / AGUIResults.Events negotiation. DO NOT USE FOR: adding a new wire event10 TYPE (use agui-dotnet-wire-types), writing tests (use agui-dotnet-integration-tests).11---12 13# AG-UI .NET Transport & Encoding14 15Encodes the non-obvious design constraints of the AG-UI .NET transport layer, discovered while16building protobuf support. Apply when adding/changing an encoding or the negotiation that selects17one. Read `references/wire-format.md` before touching the protobuf codec or framing.18 19## Transport architecture20 21One bidirectional abstraction, `IAGUIEventStreamFormatter` (in `AGUI.Formatting`), serves every22transport and both directions:23 24| Member | Role |25|--------|------|26| `MediaType` | Advertised in client `Accept` and written as server `Content-Type`. Registration order = preference. |27| `CanRead(contentType)` | Client picks the decoder for the response `Content-Type`. |28| `ReadAsync(body, ct)` | Decode body → `IAsyncEnumerable<BaseEvent>`. |29| `WriteAsync(events, output, ct)` | Encode events → body. |30 31`SseEventStreamFormatter` (`text/event-stream`) is the always-available default;32`ProtobufEventStreamFormatter` (`ProtobufEventStreamFormatter.ProtobufMediaType`) is opt-in.33 34**Client negotiation** = `DelegatingHandler` + decode helper:35- `AGUIEventStreamHandler` (public, in `AGUI.Client`) advertises every registered formatter's36 `MediaType` in `Accept`, then inspects the response `Content-Type`, finds the first `CanRead`37 formatter, and records it on the request. The body is left untouched for lazy streaming.38- `AGUIResponseExtensions.ReadAGUIEventStreamAsync` reads that recorded formatter (falling back to39 SSE) and decodes. The SDK ships no `IHttpClientFactory` integration: a caller that wants protobuf40 wires the handler into its own `HttpClient`, and constructs `AGUIChatClient` from41 `AGUIChatClientOptions`.42 43**Server negotiation** = `AGUIResults.Events` (samples `AGUI.Samples.Shared`): collects registered44`IAGUIEventStreamFormatter` services (+ built-in SSE), then picks protobuf **only when its media45type is explicitly present** in `Accept` with non-zero quality, else SSE for `text/event-stream`/46wildcard/absent, else `406`. Mirrors `preferredMediaTypes(accept, [proto])` in `@ag-ui/encoder`.47A server opts in by registering the formatter (for example,48`services.AddSingleton<IAGUIEventStreamFormatter, ProtobufEventStreamFormatter>()`).49 50## Wire format facts51 52- Protobuf media type: `application/vnd.ag-ui.event+proto` (`ProtobufEventStreamFormatter.ProtobufMediaType`).53- Framing: **4-byte big-endian `uint32` length prefix + protobuf message bytes**, per event —54 matches `@ag-ui/encoder` `encodeProtobuf` (`dataView.setUint32(0, length, false)`). See55 `AGUIProtobuf.WriteFramed` / `ReadFramedAsync`.56- `Encode`/`Decode` = single message, no length prefix (mirror TS `proto.encode`/`proto.decode`).57- Dynamic payloads (state, args, results) use `google.protobuf.Value` (Struct/ListValue/scalars) —58 **never `Any` and never a JSON-string field**.59 60## The CRITICAL Native AOT rule61 62Implement the `JsonElement` <-> `google.protobuf.Value` bridge **by hand over the generated63WellKnownTypes** (`ProtoValueConverter`). **NEVER** use Google.Protobuf's reflection-based64`JsonFormatter`/`JsonParser` or any descriptor reflection API — they are not trim/AOT safe and the65package multi-targets `net10/9/8/netstandard2.0/net472`.66 67- **Number caveat**: `Value` is double-only. `long`/`decimal` beyond 2^53 lose precision on round68 trip. This is intentional — it matches the JS `@ag-ui/proto` limitation.69 70## Schema-first extension71 72The `.proto` schema is **canonical** and lives in the TS package. `AGUI.Protobuf.csproj`73`<Protobuf>` references `sdks/typescript/packages/proto/src/proto/*.proto` directly74(`csharp_namespace = AGUI.ProtocolBuffers`, generated types `Access="Internal"`) — **do not fork or75copy it**. To add a wire-representable event:76 771. Extend the shared `.proto` (coordinated across all SDKs — it is the cross-language contract).782. Add a mapper case in `ProtoEventMapper` (event oneof) / `ProtoMessageMapper` (messages),79 mirroring `sdks/typescript/packages/proto/src/proto.ts` reshaping verbatim.80 81Subset coverage is intentional: .NET-only events that have no wire representation throw82`NotSupportedException` from the mapper's `default` case. Don't invent a wire shape unilaterally.83 84## How to verify85 86- **Byte-parity** against `@ag-ui/proto` via the cross-language tests (see87 `agui-dotnet-integration-tests` and `sdks/dotnet/docs/cross-language-testing.md`). The JSON88 compatibility fixtures in `tests/AGUI.Abstractions.UnitTests/Compatibility/` guard SSE drift.89- **Multi-TFM AOT build**: `dotnet build` from `sdks/dotnet/` (targets net10/9/8/netstandard2.0/90 net472; warnings are errors). Update `PublicAPI.Unshipped.txt` for any public surface change.91 92## ❌ Anti-patterns93 941. **Don't embed JSON-as-string inside a `Value`.** Map structured payloads recursively to95 Struct/ListValue/scalars via `ProtoValueConverter`. A string field breaks @ag-ui/proto parity.962. **Don't use `JsonFormatter`/`JsonParser`/descriptor reflection.** Not AOT-safe — hand-write the97 bridge over generated WellKnownTypes.983. **Don't copy or fork the `.proto`.** Reference the canonical TS schema from the `.csproj` so the99 codec can't drift from the wire contract.1004. **Don't make `AGUI.Protobuf` depend on `AGUI.Client` or the hosting/server package.** The codec101 stays transport-neutral; it references only `AGUI.Abstractions` + `AGUI.Formatting`.1025. **Don't change framing or use little-endian.** Length is big-endian `uint32`; both SDKs depend103 on exact byte layout.104 105## References106 107- **Wire format & codec internals** (framing, oneof mapping, Value bridge, negotiation parity):108 [references/wire-format.md](references/wire-format.md)109 Discovery context
Discovered by repository scan. No exact path reference found in the snapshot’s root CLAUDE.md.