From ag-ui-dotnet
Bootstraps an AG-UI .NET streaming-chat client and server. Installs NuGet packages, configures AGUIChatClient or a hosted endpoint, and runs multi-turn conversations with stateless or session-persisted agents.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ag-ui-dotnet:agui-dotnet-streaming-chatThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Goal: install the packages, stand up an AG-UI endpoint, connect a client, and stream a multi-turn conversation. **You are done when you run the client and it prints a streamed reply.**
Goal: install the packages, stand up an AG-UI endpoint, connect a client, and stream a multi-turn conversation. You are done when you run the client and it prints a streamed reply.
How you keep a conversation going depends on who owns the history:
AGUIChatClient is a Microsoft.Extensions.AI.IChatClient, so you consume it with the ordinary GetStreamingResponseAsync(messages, options, ct) API.
Thread id. Every conversation has one — it is the stable identifier for the whole exchange. Keep it stable across turns by reusing the same ChatOptions instance: AGUIChatClient pins the resolved thread id onto it after the first turn. (The SDK never surfaces it as ConversationId — update.ConversationId is always null by design, issue #4869 — so don't use ConversationId to track a conversation.)
Client app:
dotnet add package AGUI.Client
Server app:
dotnet add package AGUI.Server # RunAgentInput adaptation + event-stream conversion
dotnet add package AGUI.Formatting # SseEventStreamFormatter (writes the SSE wire format)
dotnet add package installs the latest stable version. To discover versions or pin one:
dotnet package search AGUI.Client --exact-match # show the latest version on nuget.org
dotnet add package AGUI.Client --prerelease # install latest, including prereleases
dotnet add package AGUI.Client --version <x.y.z> # pin a specific version
AGUI.Abstractions (protocol types such as RunAgentInput and RunStartedEvent) comes in transitively with both. On the server you also use Microsoft.Extensions.AI (AddChatClient, IChatClient) plus a chat-client provider of your choice (OpenAI, Azure OpenAI, Ollama, …).
Construct it from an AGUIChatClientOptions (here via the (HttpClient, endpoint) options constructor):
using AGUI.Client;
using Microsoft.Extensions.AI;
using var httpClient = new HttpClient();
IChatClient client = new AGUIChatClient(new(httpClient, "http://localhost:5001"));
await foreach (var update in client.GetStreamingResponseAsync(
new List<ChatMessage> { new(ChatRole.User, "Hello") }))
{
Console.Write(update.Text); // text deltas stream in as they arrive
}
You own the HttpClient. The transport defaults to Server-Sent Events.
Host the endpoint yourself: receive RunAgentInput, adapt it to Microsoft.Extensions.AI, stream from any IChatClient, convert to AG-UI events, and write them as SSE.
using AGUI.Abstractions;
using AGUI.Formatting;
using AGUI.Server;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Options;
using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;
var builder = WebApplication.CreateBuilder(args);
// Register your chat client. Any Microsoft.Extensions.AI provider works
// (OpenAI, Ollama, a local model, …); Azure OpenAI is shown here.
builder.Services.AddChatClient(
new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsIChatClient());
// SseEventStreamFormatter is stateless — register it once as a singleton.
builder.Services.AddSingleton<IAGUIEventStreamFormatter, SseEventStreamFormatter>();
var app = builder.Build();
app.MapPost("/", async (
[FromBody] RunAgentInput input,
IChatClient chatClient,
IAGUIEventStreamFormatter formatter,
IOptions<JsonOptions> jsonOptions,
HttpContext http,
CancellationToken ct) =>
{
var jso = jsonOptions.Value.SerializerOptions;
// 1) RunAgentInput -> messages + ChatOptions
var ctx = input.ToChatRequestContext(jso);
// 2) Stream from the model
var updates = chatClient.GetStreamingResponseAsync(ctx.Messages, ctx.ChatOptions, ct);
// 3) ChatResponseUpdate stream -> AG-UI event stream
var events = updates.AsAGUIEventStreamAsync(ctx, ct);
// 4) Write the events using the injected (singleton) formatter
http.Response.ContentType = formatter.MediaType;
http.Response.Headers.CacheControl = "no-cache";
await formatter.WriteAsync(events, http.Response.Body, ct);
});
app.Run();
That is the whole server contract. If you later expose server-executed tools, add .UseFunctionInvocation() to the chat-client registration — not needed for plain chat.
A stateless server has no memory, so the client keeps the running List<ChatMessage>: append the assistant reply, then add the next user turn. Reuse the same ChatOptions so the thread id stays stable across the conversation.
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var options = new ChatOptions();
await SendAsync(messages, options);
messages.Add(new ChatMessage(ChatRole.User, "How are you?"));
await SendAsync(messages, options);
async Task SendAsync(List<ChatMessage> history, ChatOptions options)
{
var updates = new List<ChatResponseUpdate>();
await foreach (var update in client.GetStreamingResponseAsync(history, options))
{
Console.Write(update.Text); // stream the reply as it arrives
updates.Add(update);
}
Console.WriteLine();
history.AddMessages(updates.ToChatResponse()); // carry the reply into the next turn
}
AGUIChatClient sends the full message list every turn by design — that is how a stateless server sees prior context.
dotnet run in the server project (listening on, e.g., :5001).update.Text chunks, not one blob at the end).curl -N -X POST http://localhost:5001/ -H "Content-Type: application/json" \
-d '{"threadId":"t1","runId":"r1","messages":[{"id":"m1","role":"user","content":"Hello"}]}'
You should see data: {...} SSE frames: RUN_STARTED … TEXT_MESSAGE_CONTENT … RUN_FINISHED.
4. Multi-turn keeps context: the second answer reflects the first exchange.
Use this when the server holds the conversation (a session store keyed by thread id). The server recalls history from the thread id, so the client just needs to keep that thread id stable — reusing the same ChatOptions (above) is usually enough. Two extra tools for when you need them:
Pin a known thread id (resuming a session created earlier): set it as ConversationId — AGUIChatClient maps it inward to the AG-UI thread id.
var options = new ChatOptions { ConversationId = threadId };
Read the thread id / run id for a turn — they ride on the RUN_STARTED event, exposed as the update's RawRepresentation:
using AGUI.Abstractions;
string? threadId = null, runId = null;
await foreach (var u in client.GetStreamingResponseAsync(messages, options))
{
if (u.RawRepresentation is RunStartedEvent started)
{
threadId = started.ThreadId;
runId = started.RunId;
}
Console.Write(u.Text);
}
Advanced — set wire-level fields with no ChatOptions equivalent. For example ParentRunId, to chain one run onto the previous. Supply it via RawRepresentationFactory; messages still flow through the normal call argument, and to send only the new turn you simply pass a shorter list:
var options = new ChatOptions
{
RawRepresentationFactory = _ => new RunAgentInput { ParentRunId = runId },
};
await foreach (var update in client.GetStreamingResponseAsync(newMessages, options))
{
Console.Write(update.Text);
}
Send only the new messages this way only when the server truly owns history; otherwise prefer the full-history stateless pattern, which works against any AG-UI server.
ChatOptions across different conversations. The thread id pins onto the options instance, so a second, unrelated conversation inherits the first one's thread id — and against a hosted server the two get merged into one session. Use a fresh ChatOptions per conversation; reuse it only across turns of the same one.GetResponseAsync when you want streaming. It returns only the final aggregated response — no incremental text, and content-less updates (lifecycle and state events) do not survive the aggregation. Use GetStreamingResponseAsync.npx claudepluginhub ag-ui-protocol/ag-ui --plugin ag-ui-dotnetExposes client-side tools to an AG-UI agent via the .NET SDK: C# functions running in the client app (GPS, local state, device APIs) that the model can call and get results back automatically.
Creates and manages persistent AI agents in .NET using Azure SDK for threads, messages, runs, tools, function calling, file search, and code interpreter.
Implements the AG-UI protocol for agent-to-frontend communication via SSE events. Covers event types, state sync, tool calls, and human-in-the-loop flows for custom agent backends.