In the previous article, we looked at human-in-the-loop agents. The main point was that approval is a system boundary. The model can request an action, but the application must decide whether that action is allowed to run.

That immediately creates a frontend problem.

A real agent UI cannot only show a chat transcript. It also has to show:

  • streamed text while the agent is still answering
  • tool calls before and after execution
  • long-running tool progress
  • approval requests with exact arguments
  • state updates that change the visible application
  • errors, retries, cancellations, and run status

AG-UI exists for this layer. It gives agent backends and frontends a protocol for exposing agent behavior as events instead of forcing everything through plain assistant text.

A simple rule is:

Use chat for language. Use events for agent behavior.

Plain chat is the wrong frontend boundary

A text-only chat box is fine for a simple assistant. The user sends a message. The assistant streams a response. The interaction ends.

That model breaks down once the agent can do real application work.

Imagine a deployment assistant:

User: Deploy release-2026-06-24 to production using CHG-1042.

The frontend should not wait silently until the final answer arrives. It should be able to show:

Run started
Checking staging status...
Tool call: CheckStagingStatusAsync(release-2026-06-24)
Tool result: staging healthy
Approval required: DeployToProductionAsync(...)
User approved
Deploying...
Deployment complete
Run finished

Those are not all assistant messages. Some are lifecycle events. Some are tool events. Some are approval requests. Some are state changes.

If you flatten all of that into text, the frontend loses structure. It cannot reliably render progress, approval forms, state panels, or tool-specific UI. It has to parse generated prose, which is exactly the kind of boundary we have avoided throughout this series.

What AG-UI adds

AG-UI is a protocol for agent user interfaces. In Agent Framework, the integration connects an agent backend to web clients over HTTP with Server-Sent Events.

The useful part is not only “streaming text”. We already had RunStreamingAsync for that.

The useful part is that the stream can carry richer agent events:

  • run lifecycle
  • text message chunks
  • tool calls
  • tool results
  • approval requests
  • state snapshots
  • state deltas
  • errors

That gives the frontend enough structure to render the agent as an application workflow instead of a typing indicator with a text box.

The Agent Framework AG-UI documentation currently describes support for several protocol features, including agentic chat, backend tool rendering, human-in-the-loop approval, generative UI, shared state, and predictive state updates. The protocol is still evolving, so treat implementation details as something to verify against the current package version before shipping.

The backend shape in .NET

For a .NET application, the basic backend shape is an ASP.NET Core endpoint.

Install the AG-UI hosting package:

dotnet add package Microsoft.Agents.AI.Hosting.AGUI.AspNetCore --prerelease

Then expose an Agent Framework agent through MapAGUI.

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.Extensions.AI;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddAGUI();

AzureOpenAIClient azureClient = new(
    new Uri(builder.Configuration["AZURE_OPENAI_ENDPOINT"]!),
    new DefaultAzureCredential());

IChatClient chatClient = azureClient.AsChatClient("gpt-5-mini");

AIAgent agent = chatClient.AsAIAgent(
    name: "deployment-agent",
    instructions: """
    You help operators inspect deployment state and prepare safe release actions.
    Explain tool usage clearly.
    Ask for approval before risky actions.
    """);

WebApplication app = builder.Build();

app.MapAGUI("/", agent);

await app.RunAsync();

AddAGUI registers the AG-UI services. MapAGUI exposes the agent through an HTTP endpoint and streams responses using Server-Sent Events.

That endpoint is now the frontend boundary. The browser does not need to know the provider SDK. It talks to an AG-UI endpoint.

Conceptually:

React or web client
-> HTTP POST with messages
-> ASP.NET Core MapAGUI endpoint
-> Agent Framework agent
-> model, tools, approvals, state
-> Server-Sent Events back to the client

A .NET client can consume it as an agent

Agent Framework also has an AG-UI client package:

dotnet add package Microsoft.Agents.AI.AGUI --prerelease
dotnet add package Microsoft.Agents.AI --prerelease

The client side can create an AGUIChatClient, convert it to an agent, and consume streaming updates.

using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;

string serverUrl =
    Environment.GetEnvironmentVariable("AGUI_SERVER_URL")
    ?? "http://localhost:8888";

using HttpClient httpClient = new();

AGUIChatClient aguiClient = new(httpClient, serverUrl);

AIAgent remoteAgent = aguiClient.AsAIAgent(
    name: "agui-client",
    description: "Client-side proxy for the AG-UI agent endpoint.");

AgentSession session = await remoteAgent.CreateSessionAsync();

List<ChatMessage> messages =
[
    new(ChatRole.User, "Check release-2026-06-24 and tell me whether it is ready.")
];

await foreach (AgentResponseUpdate update in remoteAgent.RunStreamingAsync(
                   messages,
                   session))
{
    foreach (AIContent content in update.Contents)
    {
        if (content is TextContent text)
        {
            Console.Write(text.Text);
        }
    }
}

This is useful for console demos or service-to-service tests. For a real web frontend, you might use a React client such as CopilotKit or your own AG-UI event consumer.

The important design point is the same either way:

Do not treat the endpoint as “an API that returns a string”. It is an event stream.

Stream text, but do not stop there

Streaming text is the minimum viable agent frontend feature.

It solves the waiting problem. The user sees output while the model is still generating.

But once tools are involved, text streaming alone is not enough. The frontend also needs to understand the run lifecycle.

At minimum, keep separate UI state for:

public enum AgentRunStatus
{
    Idle,
    Running,
    WaitingForApproval,
    RunningTool,
    Completed,
    Failed,
    Cancelled
}

Then update it from events rather than from generated text.

For example:

public sealed record AgentRunViewModel
{
    public AgentRunStatus Status { get; init; }
    public string? ConversationId { get; init; }
    public string? RunId { get; init; }
    public string AssistantText { get; init; } = string.Empty;
    public List<ToolCallViewModel> ToolCalls { get; init; } = [];
    public ApprovalRequestViewModel? PendingApproval { get; init; }
}

That separation matters.

The assistant text is content. The run status is application state. Tool calls are structured events. Approvals are interactive decisions.

If everything is appended to one markdown string, the frontend cannot behave like a product UI.

Show backend tool progress

In AG-UI backend tool rendering, tools are defined and executed on the server. The frontend receives tool call and tool result updates.

That is a good fit for actions that belong inside your application boundary:

  • search internal docs
  • check deployment status
  • calculate a quote
  • query inventory
  • create a support ticket
  • call an authorized backend service

A simple server-side tool looks like this:

using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;

[Description("Checks current deployment status for a release.")]
public static Task<DeploymentStatus> CheckDeploymentStatusAsync(
    [Description("Release identifier, for example release-2026-06-24.")]
    string releaseId,
    IServiceProvider services,
    CancellationToken cancellationToken)
{
    var deployments = services.GetRequiredService<IDeploymentService>();

    return deployments.CheckStatusAsync(
        releaseId,
        cancellationToken);
}

Register it with the agent:

AITool[] tools =
[
    AIFunctionFactory.Create(CheckDeploymentStatusAsync)
];

AIAgent agent = chatClient.AsAIAgent(
    name: "deployment-agent",
    instructions: "Use tools when deployment state is needed.",
    tools: tools,
    services: app.Services);

app.MapAGUI("/", agent);

On the client side, handle tool call and tool result content separately from text.

await foreach (AgentResponseUpdate update in remoteAgent.RunStreamingAsync(
                   messages,
                   session,
                   cancellationToken: cancellationToken))
{
    foreach (AIContent content in update.Contents)
    {
        switch (content)
        {
            case TextContent text:
                AppendAssistantText(text.Text);
                break;

            case FunctionCallContent call:
                MarkToolStarted(
                    call.CallId,
                    call.Name,
                    call.Arguments);
                break;

            case FunctionResultContent result:
                MarkToolFinished(
                    result.CallId,
                    result.Result,
                    result.Exception);
                break;
        }
    }
}

The actual UI can then show tool progress in a side panel, status bar, timeline, or inline expandable detail.

The point is not to expose every internal detail to the user. The point is to make the system understandable.

Good frontend text might be:

Checking staging deployment status...

Bad frontend text is:

The model is thinking.

The first tells the user what the system is doing. The second tells them nothing operationally useful.

Backend tools and frontend tools are different

AG-UI can also support frontend tools.

A frontend tool is registered on the client and executes in the client environment. The server can see the tool declaration and request the call, but the implementation runs on the client.

That is useful for client-local capabilities:

  • read browser-local preferences
  • access device location after user permission
  • inspect UI selection
  • copy something to clipboard
  • trigger a client-side view change
  • read state that only exists in the frontend

For example:

[Description("Gets the user's current client-side location after permission.")]
public static Task<string> GetUserLocationAsync(
    CancellationToken cancellationToken)
{
    // In a real frontend, this would call browser or device APIs.
    return Task.FromResult("Berlin, Germany");
}

AITool[] frontendTools =
[
    AIFunctionFactory.Create(GetUserLocationAsync)
];

AIAgent remoteAgent = aguiClient.AsAIAgent(
    name: "agui-client",
    description: "AG-UI client with frontend tools.",
    tools: frontendTools);

The server receives the frontend tool schema. The model can request the tool. The client executes it and returns the result.

Do not use frontend tools for privileged operations.

If the action changes server data, spends money, changes permissions, or calls internal systems, make it a backend tool with normal authentication, authorization, validation, and audit logging.

Frontend tools are for client capabilities. Backend tools are for application capabilities.

Approval requests need real UI

The previous article covered ApprovalRequiredAIFunction on the backend. AG-UI brings that approval request across the frontend boundary.

In the .NET pattern documented for Agent Framework, approval-required tools use ApprovalRequiredAIFunction. Middleware converts FunctionApprovalRequestContent into an AG-UI client tool call, commonly named request_approval. The client renders an approval UI and sends the decision back as a tool result. Middleware then converts that response back into FunctionApprovalResponseContent so the agent can continue.

Conceptually:

agent requests risky function
-> FunctionApprovalRequestContent
-> AG-UI client tool call: request_approval
-> frontend shows approval UI
-> user approves or rejects
-> frontend returns tool result
-> FunctionApprovalResponseContent
-> agent continues

The frontend must show the actual operation. Not just the model’s explanation.

For a deployment approval, show:

Tool: DeployToProductionAsync
Release: release-2026-06-24
Environment: production
Change ticket: CHG-1042
Requested by: lukas@example.com
Expires: 2026-06-24 15:30 UTC

A view model might look like this:

public sealed record ApprovalRequestViewModel
{
    public required string ApprovalId { get; init; }
    public required string FunctionName { get; init; }
    public required IReadOnlyDictionary<string, object?> Arguments { get; init; }
    public required string RequestedBy { get; init; }
    public DateTimeOffset ExpiresAt { get; init; }
    public string? AgentExplanation { get; init; }
}

The frontend should not approve vague text such as:

The agent wants to proceed.

It should approve a specific function call with specific arguments.

This is where AG-UI matters. Approval becomes an interactive event in the frontend, not a paragraph in the chat.

State updates are separate from messages

Some agent applications are not only conversations. They have visible application state.

Examples:

  • a trip plan
  • a deployment checklist
  • a draft support reply
  • a generated recipe
  • a remediation plan
  • a document outline
  • a shopping cart

The agent may update that state while it works.

AG-UI state management exists so the client and server can keep a synchronized view of application state. The Agent Framework docs describe state snapshots, bidirectional state synchronization, and predictive state updates.

For a deployment frontend, state might be:

public sealed class DeploymentReviewState
{
    public string ReleaseId { get; set; } = string.Empty;
    public string Environment { get; set; } = "production";
    public string CurrentStage { get; set; } = "not-started";
    public List<DeploymentCheck> Checks { get; set; } = [];
    public bool RequiresApproval { get; set; }
}

public sealed class DeploymentCheck
{
    public string Name { get; set; } = string.Empty;
    public string Status { get; set; } = "pending";
    public string? Details { get; set; }
}

Then the UI can render a real checklist instead of hoping the model produces a readable markdown list every time.

AG-UI state events let the frontend receive updates as structured data. The exact implementation depends on the client stack and package version. In the current .NET docs, one C# pattern emits DataContent with application/json; the AG-UI hosting layer converts that into state snapshot events.

Conceptually:

agent or middleware produces structured state
-> JSON state snapshot
-> AG-UI state event
-> frontend replaces or patches local state
-> UI rerenders structured panels

The key rule is:

Do not make the frontend parse state out of assistant text.

If the agent updates a checklist, send a checklist object. If it updates a plan, send a plan object. If it updates approval state, send approval state.

Predictive state updates need discipline

Predictive state updates are attractive because the frontend can update while the model is still generating tool arguments. That can make generative UI feel much faster.

But optimistic state is still speculative.

The model may change its arguments while streaming. The user may reject the approval. The tool may fail validation. The final backend state may differ from the predicted state.

So treat predictive updates as provisional.

For example:

public enum StateConfidence
{
    Predicted,
    Confirmed,
    Rejected
}

public sealed record UiStateEnvelope<T>(
    T Value,
    StateConfidence Confidence,
    string? SourceRunId);

A good UI can show predicted state differently from confirmed state. For example:

  • dim predicted checklist items
  • show “drafting…” on generated sections
  • require approval before applying risky state changes
  • rollback predicted changes if the tool call is rejected

This matters most when predictive state combines with human approval.

If the frontend shows a deployment plan while the model streams tool arguments, that does not mean the plan has been approved or executed. The UI needs separate states:

predicted
pending approval
approved
executed
failed

Do not let optimistic UI blur the boundary between “the model proposed this” and “the system did this”.

AG-UI does not replace product design

AG-UI gives you a protocol. It does not decide what your product should show.

You still need to design the interaction.

For each agent feature, decide:

  • What should be visible to the user?
  • What should stay in developer traces?
  • Which tool calls deserve inline status?
  • Which tool calls should be hidden behind a simple progress label?
  • Which actions require approval?
  • Which state updates are provisional?
  • Which errors need user action?
  • Which errors should only be logged?

For a deployment assistant, I would show:

  • current run status
  • deployment checklist state
  • meaningful tool progress
  • approval request details
  • final result

I would not show:

  • every token event
  • every internal message
  • raw prompt text
  • secrets or internal identifiers the user is not allowed to see
  • model reasoning traces

The frontend is a boundary too. It should expose useful system behavior without leaking internal implementation details.

A practical event handling shape

A frontend can treat the AG-UI stream as an event reducer.

The backend emits events. The frontend reduces those events into UI state.

For example:

public sealed record AgentFrontendState
{
    public AgentRunStatus RunStatus { get; init; } = AgentRunStatus.Idle;
    public string AssistantText { get; init; } = string.Empty;
    public List<ToolCallViewModel> ToolCalls { get; init; } = [];
    public ApprovalRequestViewModel? PendingApproval { get; init; }
    public DeploymentReviewState? DeploymentState { get; init; }
    public string? ErrorMessage { get; init; }
}

Then each event updates one part of state.

public static AgentFrontendState Reduce(
    AgentFrontendState state,
    AIContent content)
{
    return content switch
    {
        TextContent text =>
            state with
            {
                AssistantText = state.AssistantText + text.Text
            },

        FunctionCallContent call =>
            state with
            {
                RunStatus = AgentRunStatus.RunningTool,
                ToolCalls = AddToolCall(state.ToolCalls, call)
            },

        FunctionResultContent result =>
            state with
            {
                ToolCalls = CompleteToolCall(state.ToolCalls, result)
            },

        FunctionApprovalRequestContent approval =>
            state with
            {
                RunStatus = AgentRunStatus.WaitingForApproval,
                PendingApproval = ToApprovalViewModel(approval)
            },

        DataContent data when data.MediaType == "application/json" =>
            state with
            {
                DeploymentState = ParseDeploymentState(data)
            },

        _ => state
    };
}

This example uses Agent Framework content types because it fits the .NET client surface. A browser client may work with raw AG-UI protocol event names instead. The design is the same:

Events update structured UI state. Text is only one part of that state.

Cancellation belongs in the frontend contract

Agent frontends also need cancellation.

Users close tabs. They navigate away. They reject an approval. They realize the prompt was wrong.

The frontend should be able to cancel the current run and the backend should pass cancellation through the model call and tool calls.

In .NET, keep the CancellationToken flowing:

await foreach (AgentResponseUpdate update in remoteAgent.RunStreamingAsync(
                   messages,
                   session,
                   cancellationToken: cancellationToken))
{
    Render(update);
}

For backend tools:

public static Task<DeploymentStatus> CheckDeploymentStatusAsync(
    string releaseId,
    IServiceProvider services,
    CancellationToken cancellationToken)
{
    var deployments = services.GetRequiredService<IDeploymentService>();
    return deployments.CheckStatusAsync(releaseId, cancellationToken);
}

If the browser disconnects from the SSE stream, the server should not blindly keep doing expensive work unless the product explicitly requires background continuation.

This is especially important with tools. A cancelled text generation is one thing. A cancelled deployment workflow is another. Make the cancellation behavior explicit per operation.

When I would use AG-UI

Use AG-UI when:

  • the frontend needs streaming text
  • users should see tool progress
  • approval requests need real UI
  • agent state should update visible components
  • frontend tools need to run in the client
  • backend tools should expose progress and results
  • multiple clients should talk to the same agent endpoint shape
  • you want a protocol boundary instead of custom ad-hoc streaming JSON

This is especially useful when the agent is part of an application, not just a chat demo.

Examples:

  • deployment assistant with approval and checklist state
  • support copilot that drafts replies and creates tickets
  • document review UI with extracted findings and reviewer actions
  • planning assistant that updates a structured itinerary
  • operations assistant that streams tool status while querying systems

When I would not use AG-UI

Do not add AG-UI just because an agent exists.

It may be unnecessary when:

  • the application only needs a simple text response
  • there are no tools, approvals, or shared state
  • a normal backend endpoint already gives enough structure
  • the UI is internal and short-lived
  • you do not want to take a dependency on a still-evolving protocol
  • the frontend team cannot support event-driven state handling yet

For a background job, webhook processor, batch summarizer, or simple API endpoint, direct Agent Framework usage may be cleaner.

Use RunAsync or RunStreamingAsync directly when the frontend does not need the extra protocol semantics.

Conclusion

Agent frontends need more than a plain chat box once agents start doing application work.

Streaming text solves only one part of the experience. The frontend also needs structured events for tool progress, approval requests, state updates, errors, cancellation, and run lifecycle.

AG-UI gives Agent Framework applications a protocol boundary for that. On the backend, MapAGUI exposes an agent as an HTTP and SSE endpoint. On the client, AG-UI events can become structured UI state instead of being flattened into assistant text.

The design I would carry forward is:

  • render text as text
  • render tool calls as tool progress
  • render approvals as approval forms
  • render state as structured UI
  • keep frontend tools separate from backend tools
  • treat predictive state as provisional
  • keep cancellation and errors explicit

That turns the frontend from a passive transcript into part of the agent system.

In the next post, I will move from frontend behavior to production visibility. Once the UI can show streaming, tools, approvals, and state, the backend also needs traces that explain what happened across model calls, tool execution, AG-UI events, and user decisions.

Further reading