←All posts

AG-UI Protocol Explained — Implementing the Standard Protocol Between Agents and Users with Microsoft Agent Framework

AG-UI (Agent-User Interaction Protocol) standardizes communication between agents and user-facing apps as a stream of typed events, so a frontend that implements it can work with any AG-UI backend. This article covers its architecture, events, middleware, and state synchronization, with implementation examples in Microsoft Agent Framework.

AG-UIAIProtocolMicrosoft Agent FrameworkPython

Introduction — The "Last Mile" Problem of AI Agents

LLMs can now call tools on their own, and multiple agents can coordinate on complex tasks. How agent output reaches the user gets much less attention.

Agents run for a long time, stream intermediate results, ask the user for approval, and update UI state. The one-request, one-response model of REST APIs and GraphQL doesn't handle this well. Today each agent framework has its own event format, so frontends write an adapter per framework and end up tightly coupled to it.

AG-UI (Agent-User Interaction Protocol) standardizes this communication between agents and users.

Three Agent Protocols

Three open protocols cover different connections around an AI agent.

ProtocolConnectionOrigin
MCP (Model Context Protocol)Agent ↔ Tools & DataAnthropic
A2A (Agent to Agent)Agent ↔ AgentGoogle
AG-UI (Agent-User Interaction)Agent ↔ User (UI)CopilotKit

MCP standardizes how agents connect to external tools and A2A standardizes how agents coordinate with each other. AG-UI standardizes how agents connect to user-facing applications. With all three, an agent's connections to tools and data, to other agents, and to the user can all go through standard protocols.

The World Without AG-UI

Without AG-UI, frontend developers repeat the following work for every framework or application.

  1. Framework-specific connection code: Use LangGraph? Write a LangGraph-specific client. Use CrewAI? Write CrewAI-specific code.
  2. Reimplementing streaming: Build text streaming, tool call display, and state synchronization for each framework.
  3. Reimplementing Human-in-the-Loop: Design a user approval flow for each application.

With AG-UI, a frontend implements one protocol and can talk to any agent backend that supports AG-UI. A backend that emits AG-UI events works with any frontend that implements AG-UI.


Design Principles of AG-UI

1. Event-Driven Communication

Agents emit events of standardized types while they run, and the client processes the stream in order. Unlike REST, where one response ends the exchange, events keep arriving until the run finishes.

2. Bidirectional Interaction

Agents can take input from users, so AI and humans can work together in one workflow.

3. Transport Agnostic

AG-UI does not dictate how events are delivered. SSE (Server-Sent Events), WebSockets, Webhooks — choose the transport that fits your architecture.

4. Flexible Event Structure

Events don't need to match AG-UI's format exactly; they only need to be AG-UI-compatible. Existing agent frameworks can therefore adapt their own event formats with small changes.


Architectural Overview

AG-UI uses a client-server architecture.

ComponentRole
ApplicationUser-facing app (chat UI, AI-powered app, etc.)
AG-UI ClientGeneric communication client like HttpAgent
Secure ProxyBackend service providing additional capabilities and acting as a secure proxy
AgentsAI agents processing requests and generating streaming responses

Protocol Layer

At the center of the protocol layer is a simple abstraction: run an agent and get back a stream of events.

// The core AG-UI abstraction
type RunAgent = () => Observable<BaseEvent>
 
class MyAgent extends AbstractAgent {
  run(input: RunAgentInput): RunAgent {
    const { threadId, runId } = input
    return () =>
      from([
        { type: EventType.RUN_STARTED, threadId, runId },
        {
          type: EventType.MESSAGES_SNAPSHOT,
          messages: [
            { id: "msg_1", role: "assistant", content: "Hello!" }
          ],
        },
        { type: EventType.RUN_FINISHED, threadId, runId },
      ])
  }
}

The run() method accepts RunAgentInput and returns a function that returns an Observable<BaseEvent>. All communication is expressed as a stream of BaseEvent.

Standard HTTP Client

AG-UI provides HttpAgent, a standard HTTP client that sends POST requests to any endpoint and receives a stream of BaseEvent.

import { HttpAgent, type AgentSubscriber } from "@ag-ui/client"
import { EventType } from "@ag-ui/core"
 
const agent = new HttpAgent({
  url: "https://your-agent-endpoint.com/agent",
  agentId: "my-agent",
  threadId: "conversation-123",
})
 
const subscriber: AgentSubscriber = {
  onEvent: ({ event }) => {
    switch(event.type) {
      case EventType.TEXT_MESSAGE_CONTENT:
        // Display text in UI
        break
      case EventType.TOOL_CALL_START:
        // Show tool call in progress
        break
    }
  },
  onRunFailed: ({ error }) => console.error("Agent error:", error),
  onRunFinalized: () => console.log("Run complete")
}
 
await agent.runAgent({
  tools: [...],
  context: [...]
}, subscriber)

HttpAgent supports two transports.

TransportCharacteristics
HTTP SSEText-based streaming. Easy to debug
HTTP Binary ProtocolHigh-performance, space-efficient binary serialization

The Event System — The Unit of Communication

All AG-UI communication happens through typed events, and every event inherits from BaseEvent.

interface BaseEvent {
  type: EventType       // Event type identifier
  timestamp?: number    // Event creation time (optional)
  rawEvent?: any        // Original event data if transformed (optional)
  metadata?: Record<string, any> // Extra event metadata (optional)
}

Events are classified into 8 categories by purpose.

CategoryPurposeExample Events
Lifecycle EventsMonitor agent run progressionRUN_STARTED, RUN_FINISHED, RUN_ERROR
Text Message EventsHandle streaming textual contentTEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, TEXT_MESSAGE_END
Tool Call EventsManage tool executionsTOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END, TOOL_CALL_RESULT
State Management EventsSynchronize state between agent and UISTATE_SNAPSHOT, STATE_DELTA, MESSAGES_SNAPSHOT
Activity EventsRepresent ongoing activity progressACTIVITY_SNAPSHOT, ACTIVITY_DELTA
Reasoning EventsVisualize reasoning processesREASONING_START, REASONING_MESSAGE_CONTENT, REASONING_END
Subagent EventsTrack nested agent executionSUBAGENT_STARTED, SUBAGENT_FINISHED, SUBAGENT_ERROR
Special EventsSupport custom functionalityRAW, CUSTOM

Lifecycle Events — Execution Boundaries

Lifecycle Events mark the start, intermediate steps, and end of a single agent run. A typical flow looks like this.

EventRequiredDescription
RUN_STARTEDAlwaysStart of execution. Uniquely identified by threadId and runId. Optional parentRunId supports branching/time travel
RUN_FINISHEDOn successSuccessful completion. Optional result field for output data
RUN_ERROROn errorExecution interrupted by error. Provides message and code
STEP_STARTEDOptionalStart of a subtask or phase. Enables fine-grained progress tracking
STEP_FINISHEDOptionalCompletion of a subtask. Used in pair with STEP_STARTED

RUN_STARTED and RUN_FINISHED (or RUN_ERROR) mark the boundaries of a run, and any number of step events can come between them.

Text Message Events — Streaming Text

Text messages are sent chunk by chunk as they are generated, without waiting for the whole message.

EventDescription
TEXT_MESSAGE_STARTInitializes a message. Sets messageId and role (developer, system, assistant, user, tool)
TEXT_MESSAGE_CONTENTText chunk. The text in the delta property should be appended to previous chunks
TEXT_MESSAGE_ENDMessage completion. UI removes loading indicators and finalizes rendering

The convenience event TEXT_MESSAGE_CHUNK expands to Start → Content → End automatically. Including messageId on the first chunk implicitly emits TEXT_MESSAGE_START, and TEXT_MESSAGE_END is emitted automatically when the stream switches to a new message ID or completes.

Tool Call Events — Visualizing Tool Calls

Tool calls are streamed the same way as text messages.

In AG-UI, tools defined in the frontend can be passed to the agent at run time. That makes Human-in-the-Loop workflows like this possible:

  1. Agent requests a tool call (TOOL_CALL_START → TOOL_CALL_ARGS → TOOL_CALL_END)
  2. Frontend executes the tool (may include user judgment)
  3. Result is returned to the agent as a tool-role message
  4. Agent continues reasoning with the result

When a tool runs on the backend, the agent sends its result to the client in a TOOL_CALL_RESULT event.

Tool calls also have a convenience event, TOOL_CALL_CHUNK, that corresponds to TEXT_MESSAGE_CHUNK. Including toolCallId and toolCallName on the first chunk implicitly emits TOOL_CALL_START, and TOOL_CALL_END is emitted automatically when the stream switches to a new toolCallId or completes.

State Management Events — Efficient State Synchronization

State management follows a snapshot-delta pattern: snapshots send the whole state, and deltas send only what changed.

EventDescription
STATE_SNAPSHOTComplete state snapshot. Sent initially or for resynchronization
STATE_DELTAIncremental update via JSON Patch (RFC 6902). Bandwidth-efficient
MESSAGES_SNAPSHOTComplete conversation message history. For chat initialization or resync

A snapshot brings the client's whole state in line, and changes between snapshots go out as deltas. The state stays correct while traffic stays low.

Event Flow Patterns

The events covered so far fall into three patterns, based on how they are sent.

PatternUse CaseExamples
Start-Content-EndStreaming contentTextMessage, ToolCall
Snapshot-DeltaState synchronizationSTATE_SNAPSHOT + STATE_DELTA
LifecycleExecution monitoringRUN_STARTED → RUN_FINISHED

Reasoning Events — Visualizing the Reasoning Process

Reasoning Events carry reasoning summaries or metadata provided by the model so the UI can show them. They are not a way to expose the model's private raw chain-of-thought to the user.

The REASONING_ENCRYPTED_VALUE event carries encrypted reasoning state across turns. The value is an opaque continuity token for the model or provider; the client only stores and forwards it and cannot read or render its contents. Reasoning can therefore continue across turns without exposing private chain-of-thought.

EventDescription
REASONING_STARTStart of reasoning process
REASONING_MESSAGE_STARTStart of reasoning message (visible portion)
REASONING_MESSAGE_CONTENTReasoning content chunk
REASONING_MESSAGE_ENDCompletion of reasoning message
REASONING_MESSAGE_CHUNKConvenience event auto-expanding Start/End
REASONING_ENDCompletion of reasoning process
REASONING_ENCRYPTED_VALUEOpaque encrypted reasoning continuity value

Middleware — Intercepting Event Streams

AG-UI middleware sits between the agent and whatever consumes its events. It can transform the event stream, filter it, or add information to it.

Middleware runs in the order it was added, and each one wraps the next. Events returned by the agent pass back through the middleware in reverse order before reaching the client.

→ middleware1
  → middleware2
    → middleware3
      → agent.run()
    ← events flow back through middleware3
  ← events flow back through middleware2
← events flow back through middleware1

Function-Based Middleware

For simple transformations, you can write middleware as a function.

import { MiddlewareFunction } from "@ag-ui/client"
import { EventType } from "@ag-ui/core"
 
// Middleware that prefixes text messages
const prefixMiddleware: MiddlewareFunction = (input, next) => {
  return next.run(input).pipe(
    map(event => {
      if (
        event.type === EventType.TEXT_MESSAGE_CHUNK ||
        event.type === EventType.TEXT_MESSAGE_CONTENT
      ) {
        return { ...event, delta: `[AI]: ${event.delta}` }
      }
      return event
    })
  )
}
 
agent.use(prefixMiddleware)

Class-Based Middleware

When the middleware needs its own state or configuration, implement it as a class.

import { Middleware } from "@ag-ui/client"
 
class MetricsMiddleware extends Middleware {
  private eventCount = 0
 
  run(input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> {
    const startTime = Date.now()
    return this.runNext(input, next).pipe(
      tap(event => {
        this.eventCount++
        this.metricsService.recordEvent(event.type)
      }),
      finalize(() => {
        this.metricsService.recordDuration(Date.now() - startTime)
      })
    )
  }
}

Built-in Middleware

Built-in middleware includes FilterToolCallsMiddleware, which filters tool calls with an allow list or a block list.

import { FilterToolCallsMiddleware } from "@ag-ui/client"
 
// Allow list
const filter = new FilterToolCallsMiddleware({
  allowedToolCalls: ["search", "calculate"]
})
 
// Block list
const blocker = new FilterToolCallsMiddleware({
  disallowedToolCalls: ["delete", "modify"]
})
 
agent.use(filter)

Agent Capabilities

AG-UI agents are implemented by extending AbstractAgent and can have the following capabilities.

Bidirectional Communication

Agents exchange events with the frontend in both directions through the event stream. This covers real-time streaming of responses, immediate feedback, progress display for long-running work, and passing structured data back and forth.

Multi-Agent Collaboration

Agents can delegate tasks to other agents. You can build workflows across several agents and pass state and context between them. When a different agent takes over, the frontend can handle it without knowing about the switch.

Human-in-the-Loop

Agents can ask the user for input on specific decisions, pause execution, and resume after human feedback. This lets you build workflows that combine AI speed with human judgment.

Conversational Memory

Agents keep the full history of conversation messages and use past exchanges in later responses. The message history is synchronized between client and server.


Implementing AG-UI with Microsoft Agent Framework

The rest of the article implements the events and features described so far with Microsoft Agent Framework.

The agent-framework-ag-ui Package

Microsoft Agent Framework ships AG-UI support as the first-party agent-framework-ag-ui package, which converts Agent Framework events into AG-UI events.

pip install agent-framework-ag-ui

Architecture

The package consists of the following components.

ComponentRole
AgentFrameworkAgentLightweight wrapper making Agent Framework agents AG-UI compatible
OrchestratorsHandle different execution flows (default, human-in-the-loop, etc.)
AgentFrameworkEventBridgeConverts Agent Framework events to AG-UI events
Message AdaptersBidirectional conversion between AG-UI and Agent Framework message formats
FastAPI EndpointStreaming HTTP endpoint with Server-Sent Events (SSE)

Feature 1: Agentic Chat — Basic Conversational Agent

Agentic Chat is a basic conversational agent with streaming chat and tool calling.

# simple_agent.py
from typing import Any
from agent_framework import Agent, SupportsChatGetResponse
 
 
def simple_agent(client: SupportsChatGetResponse[Any]) -> Agent[Any]:
    """Create a simple chat agent."""
    return Agent[Any](
        name="simple_chat_agent",
        instructions="You are a helpful assistant. Be concise and friendly.",
        client=client,
    )

An Agent Framework Agent runs with just a name, instructions, and a chat client. To expose it as an AG-UI endpoint, use add_agent_framework_fastapi_endpoint.

# server.py
from fastapi import FastAPI
from agent_framework.openai import OpenAIChatCompletionClient
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
from agents.simple_agent import simple_agent
 
app = FastAPI(title="AG-UI Demo Server")
 
# Create chat client
chat_client = OpenAIChatCompletionClient(model="gpt-4o")
 
# Register as AG-UI endpoint (local, single user)
add_agent_framework_fastapi_endpoint(
    app, simple_agent(chat_client), "/agentic_chat", single_user=True
)

Registration requires a way to set the authorization scope: either authorization_scope_resolver or single_user=True. With neither, it raises ValueError (agent-framework _endpoint.py). The examples in this article assume a local trial, so they use single_user=True; the security section covers services shared by several users.

add_agent_framework_fastapi_endpoint does the following:

  1. Registers a POST endpoint on the FastAPI route
  2. Receives RunAgentInput from the AG-UI client
  3. Runs the Agent Framework agent
  4. Converts events to AG-UI format through AgentFrameworkEventBridge
  5. Returns them as streaming SSE (Server-Sent Events)

A request to this endpoint returns AG-UI events such as the following:

→ RUN_STARTED(threadId, runId)
  → TEXT_MESSAGE_START(messageId, role="assistant")
    → TEXT_MESSAGE_CONTENT(messageId, delta="Hel")
    → TEXT_MESSAGE_CONTENT(messageId, delta="lo")
    → TEXT_MESSAGE_CONTENT(messageId, delta="!")
  → TEXT_MESSAGE_END(messageId)
→ RUN_FINISHED(threadId, runId)

Feature 2: Backend Tool Rendering — Tool Execution and Result Streaming

With Backend Tool Rendering, tools run on the backend and their results stream to the client.

# weather_agent.py
from typing import Any
from agent_framework import Agent, SupportsChatGetResponse, tool
 
 
@tool
def get_weather(location: str) -> dict[str, Any]:
    """Get current weather for a location.
 
    Args:
        location: The city to get weather for.
 
    Returns:
        Dictionary with temperature, conditions, humidity, etc.
    """
    weather_data = {
        "tokyo": {
            "temperature": 22,
            "conditions": "sunny",
            "humidity": 55,
            "wind_speed": 8,
        },
        "osaka": {
            "temperature": 24,
            "conditions": "partly cloudy",
            "humidity": 60,
            "wind_speed": 6,
        },
    }
    location_lower = location.lower()
    if location_lower in weather_data:
        return weather_data[location_lower]
    return {"temperature": 21, "conditions": "unknown", "humidity": 50, "wind_speed": 10}
 
 
@tool
def get_forecast(location: str, days: int = 3) -> str:
    """Get weather forecast for a location.
 
    Args:
        location: The city to get forecast for.
        days: Number of days to forecast (default: 3).
 
    Returns:
        Forecast information string.
    """
    forecast: list[str] = []
    for day in range(1, min(days, 7) + 1):
        forecast.append(f"Day {day}: Partly cloudy, {20 + day}°C")
    return f"{days}-day forecast for {location}:\n" + "\n".join(forecast)
 
 
def weather_agent(client: SupportsChatGetResponse[Any]) -> Agent[Any]:
    """Create a weather agent."""
    return Agent[Any](
        name="weather_agent",
        instructions=(
            "You are a helpful weather assistant. "
            "Use the get_weather and get_forecast functions to help users. "
            "Always provide friendly and informative responses."
        ),
        client=client,
        tools=[get_weather, get_forecast],
    )

Adding the @tool decorator to a function makes Agent Framework generate the tool schema and pass it to the LLM. When the agent calls a tool, these AG-UI events flow:

→ RUN_STARTED
  → TOOL_CALL_START(toolCallId, toolCallName="get_weather")
    → TOOL_CALL_ARGS(toolCallId, delta='{"location":')
    → TOOL_CALL_ARGS(toolCallId, delta=' "Tokyo"}')
  → TOOL_CALL_END(toolCallId)
  → TOOL_CALL_RESULT(toolCallId, content='{"temperature": 22, ...}')
  → TEXT_MESSAGE_START(messageId, role="assistant")
    → TEXT_MESSAGE_CONTENT(messageId, delta="The current weather in Tokyo is...")
  → TEXT_MESSAGE_END(messageId)
→ RUN_FINISHED

The frontend can show an in-progress indicator such as a loading spinner when it receives TOOL_CALL_START, and show the result on TOOL_CALL_RESULT. Because these are standardized events, this display logic doesn't depend on the backend framework.

Feature 3: Human-in-the-Loop — User Approval Flow

With Human-in-the-Loop, the agent can ask for user approval before running a tool.

# human_in_the_loop_agent.py
from typing import Any
from agent_framework import Agent, SupportsChatGetResponse, tool
 
 
@tool(approval_mode="always_require")
def delete_file(filename: str) -> str:
    """Delete a file (requires approval).
 
    Args:
        filename: Name of the file to delete.
 
    Returns:
        Deletion result message.
    """
    return f"File '{filename}' has been deleted."
 
 
@tool(approval_mode="always_require")
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email (requires approval).
 
    Args:
        to: Recipient address.
        subject: Email subject.
        body: Email body.
 
    Returns:
        Send result message.
    """
    return f"Email sent to {to} with subject '{subject}'."
 
 
def human_in_the_loop_agent(
    client: SupportsChatGetResponse[Any],
) -> Agent[Any]:
    """Create a Human-in-the-Loop agent."""
    return Agent[Any](
        name="hitl_agent",
        instructions=(
            "You are a helpful assistant that can delete files and send emails. "
            "Always confirm with the user before performing these actions."
        ),
        client=client,
        tools=[delete_file, send_email],
    )

Tools declared with @tool(approval_mode="always_require") always ask for user approval before they run. The event flow looks like this:

When a tool needs approval, the run ends with RUN_FINISHED, and its outcome carries an interrupt for the pending approval. The frontend resumes the same thread with the approval or rejection in resume; the tool runs if approved and does not run if rejected (Microsoft Learn: Human-in-the-Loop with AG-UI). Interrupts and resumes are AG-UI protocol types, so a frontend can implement a generic approval UI once and reuse it with other AG-UI agents.

Feature 4: Shared State — Bidirectional State Synchronization

Shared State synchronizes structured state between the agent and the frontend in both directions.

# recipe_agent.py
from typing import Any
from agent_framework import Agent, SupportsChatGetResponse, tool
from agent_framework.ag_ui import AgentFrameworkAgent
 
 
@tool
def update_recipe(recipe_data: dict[str, Any]) -> str:
    """Update recipe information.
 
    Args:
        recipe_data: Recipe info (name, ingredients, etc.)
 
    Returns:
        Update result message.
    """
    return f"Recipe '{recipe_data.get('name', 'Unknown')}' has been updated."
 
 
def recipe_agent(client: SupportsChatGetResponse[Any]) -> AgentFrameworkAgent:
    """Create a recipe management agent."""
    agent = Agent[Any](
        name="recipe_agent",
        instructions=(
            "You are a recipe assistant. Help users create and modify recipes. "
            "Use the update_recipe tool to save changes."
        ),
        client=client,
        tools=[update_recipe],
    )
 
    # Define state schema
    state_schema = {
        "recipe": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "ingredients": {"type": "array"},
                "instructions": {"type": "array"},
            },
        }
    }
 
    return AgentFrameworkAgent(
        agent=agent,
        state_schema=state_schema,
        name="RecipeAgent",
        description="A recipe management agent with shared state",
    )

The state_schema defines the structure of state shared with the frontend. When the agent calls update_recipe, the state is updated and STATE_DELTA events are sent to the frontend.

Feature 5: Predictive State Updates — Optimistic State Updates

Predictive State Updates write tool arguments into state in real time, while the LLM is still generating them.

# document_writer_agent.py
from typing import Any
from agent_framework import Agent, SupportsChatGetResponse, tool
from agent_framework.ag_ui import AgentFrameworkAgent
 
 
@tool
def write_document(title: str, content: str) -> str:
    """Create a document.
 
    Args:
        title: Document title.
        content: Document content.
 
    Returns:
        Creation result message.
    """
    return f"Document '{title}' has been created."
 
 
def document_writer_agent(
    client: SupportsChatGetResponse[Any],
) -> AgentFrameworkAgent:
    """Create a document writer agent."""
    agent = Agent[Any](
        name="document_writer",
        instructions="You are a document writer. Create documents based on user requests.",
        client=client,
        tools=[write_document],
    )
 
    # Predictive State configuration
    predict_state_config = {
        "current_title": {"tool": "write_document", "tool_argument": "title"},
        "current_content": {"tool": "write_document", "tool_argument": "content"},
    }
 
    return AgentFrameworkAgent(
        agent=agent,
        state_schema={
            "current_title": {"type": "string"},
            "current_content": {"type": "string"},
        },
        predict_state_config=predict_state_config,
        require_confirmation=True,
    )

predict_state_config defines which argument of which tool maps to which state field. As the LLM streams tool arguments, each chunk is written to the matching state field as it arrives.

→ RUN_STARTED
  → TOOL_CALL_START(write_document)
    → TOOL_CALL_ARGS(delta='{"title": "AG-')
      → STATE_DELTA(op: replace, path: /current_title, value: "AG-")
    → TOOL_CALL_ARGS(delta='UI Guide"')
      → STATE_DELTA(op: replace, path: /current_title, value: "AG-UI Guide")
    → TOOL_CALL_ARGS(delta=', "content": "AG-UI is...')
      → STATE_DELTA(op: replace, path: /current_content, value: "AG-UI is...")
  → TOOL_CALL_END
→ RUN_FINISHED

Users can watch the document being written without waiting for the tool to finish.

Feature 6: Workflow AG-UI Integration

Agent Framework's workflows (graph-based multi-step execution engines) can also be exposed as AG-UI endpoints.

from fastapi import FastAPI
from agent_framework import WorkflowBuilder, WorkflowContext, executor
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
 
 
@executor(id="start")
async def start(message: str, ctx: WorkflowContext) -> None:
    """Start step of the workflow."""
    await ctx.yield_output(f"Workflow received: {message}")
 
 
@executor(id="analyze")
async def analyze(data: str, ctx: WorkflowContext) -> None:
    """Analysis step."""
    result = f"Analysis of '{data}': positive sentiment detected."
    await ctx.yield_output(result)
 
 
workflow = (
    WorkflowBuilder(start_executor=start)
    .add_executor(analyze)
    .add_edge("start", "analyze")
    .build()
)
 
app = FastAPI()
add_agent_framework_fastapi_endpoint(app, workflow, "/workflow", single_user=True)

Workflow events (run/step/activity/tool/custom) are automatically converted to the corresponding AG-UI events.

Combining into One Server

Here is what it looks like to put the agents from Features 1–5 on a single server.

"""AG-UI Demo Server — All features integrated"""
import os
from fastapi import FastAPI
from agent_framework.openai import OpenAIChatCompletionClient
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
 
# Import agents
from agents.simple_agent import simple_agent
from agents.weather_agent import weather_agent
from agents.human_in_the_loop_agent import human_in_the_loop_agent
from agents.recipe_agent import recipe_agent
from agents.document_writer_agent import document_writer_agent
 
app = FastAPI(title="AG-UI Demo Server")
 
# Shared chat client
chat_client = OpenAIChatCompletionClient(
    model=os.getenv("OPENAI_CHAT_MODEL", "gpt-4o"),
    api_key=os.getenv("OPENAI_API_KEY"),
)
 
# Register endpoints for each feature
add_agent_framework_fastapi_endpoint(
    app, simple_agent(chat_client), "/agentic_chat",
    single_user=True,
)
add_agent_framework_fastapi_endpoint(
    app, weather_agent(chat_client), "/backend_tool_rendering",
    single_user=True,
)
add_agent_framework_fastapi_endpoint(
    app, human_in_the_loop_agent(chat_client), "/human_in_the_loop",
    single_user=True,
)
add_agent_framework_fastapi_endpoint(
    app, recipe_agent(chat_client), "/shared_state",
    single_user=True,
)
add_agent_framework_fastapi_endpoint(
    app, document_writer_agent(chat_client), "/predictive_state_updates",
    single_user=True,
)
 
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8888)

Connecting from an AG-UI Client

To connect to an AG-UI server from Python, use AGUIChatClient.

import asyncio
from agent_framework.ag_ui import AGUIChatClient
 
 
async def main():
    async with AGUIChatClient(endpoint="http://localhost:8888/agentic_chat") as client:
        # Streaming response
        async for update in client.get_response("Hello!", stream=True):
            for content in update.contents:
                if content.type == "text" and content.text:
                    print(content.text, end="", flush=True)
        print()
 
 
asyncio.run(main())

AGUIChatClient supports streaming and non-streaming responses, hybrid tool execution that combines client-side and server-side tools, and automatic thread management.


Security Considerations

AG-UI endpoints do not enforce authentication by default. In production, add authentication with FastAPI's dependency injection.

import os
from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security import APIKeyHeader
from agent_framework import Agent
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
 
# API key authentication setup
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
EXPECTED_API_KEY = os.environ.get("AG_UI_API_KEY")
 
 
async def verify_api_key(
    api_key: str | None = Security(API_KEY_HEADER),
) -> None:
    """Verify the API key in request headers."""
    if not api_key or api_key != EXPECTED_API_KEY:
        raise HTTPException(status_code=401, detail="Invalid or missing API key")
 
 
app = FastAPI()
 
# Assume agent was created in a previous section
# agent = simple_agent(chat_client)
 
# Register endpoint WITH authentication
# Callers sharing one API key are treated as a single user
add_agent_framework_fastapi_endpoint(
    app,
    agent,
    "/",
    dependencies=[Depends(verify_api_key)],
    single_user=True,
)

The dependencies parameter accepts any FastAPI dependency, so instead of an API key you can plug in OAuth 2.0, JWT, or Azure AD / Entra ID authentication, or add rate limiting. These dependencies only authenticate; they do not set the authorization scope. To keep conversation and approval state separate per user, pass an authorization_scope_resolver that returns a scope for the authenticated user instead of single_user=True.


Integration with CopilotKit

CopilotKit is a framework for adding AI copilot features to React applications and a well-known client with native AG-UI support.

CopilotKit's useCopilotAction hook gives a concise way to define AG-UI frontend tools.

import { useCopilotAction } from "@copilotkit/react-core"
 
// Define a tool on the frontend
useCopilotAction({
  name: "confirmAction",
  description: "Ask user to confirm an action",
  parameters: [
    { name: "action", type: "string", description: "Action to confirm" },
    { name: "importance", type: "string", enum: ["low", "medium", "high"] },
  ],
  handler: async ({ action, importance }) => {
    // Show confirmation dialog to user
    const confirmed = await showConfirmDialog(action, importance)
    return confirmed ? "approved" : "rejected"
  },
})

The tool defined here is passed to the agent, and when the agent calls it, the frontend handler runs. AG-UI standardizes this round trip, so no code specific to the backend framework is needed.


Building Blocks Provided by AG-UI

The official AG-UI documentation lists the following building blocks.

Building BlockDescriptionStatus
Streaming ChatReal-time token and event streamingSupported
MultimodalityFile, image, audio attachmentsSupported
Generative UI (Static)Output rendered as typed componentsSupported
Generative UI (Declarative)Declarative UI languageSupported
Shared StateRead-only/read-write shared stateSupported
Thinking StepsIntermediate reasoning visualizationSupported
Frontend Tool CallsFrontend-executed tool handoffSupported
Backend Tool RenderingBackend tool output visualizationSupported
Interrupts (HITL)Pause, approve, retrySupported
Sub-agentsNested delegationSupported
Agent SteeringReal-time user input for guiding agentSupported
Tool Output StreamingReal-time tool result streamingSupported
Custom EventsCustom data exchange beyond protocolSupported

Update (as of August 2026): the table above still matches the current official documentation. The surrounding ecosystem has changed in these ways.

  • Composing with generative UI specs. AG-UI supports generative UI specifications such as Google's A2UI and MCP Apps. A2UI and MCP Apps specify how an agent describes a UI widget. AG-UI is the protocol for how a frontend and an agent backend talk to each other. They work at different layers, so you can use them together.
  • Microsoft Agent Framework's status. It is listed as a Supported first-party integration, so the implementation approach in this article still applies.
  • Clients beyond web apps. AG-UI specifies an event stream, not a rendering target, so terminals, mobile apps, and chat platforms such as Slack and Microsoft Teams can also act as clients.

Supported Integrations

AG-UI was born from CopilotKit, and these agent frameworks now support it.

Agent Frameworks

FrameworkStatus
LangGraphSupported
CrewAISupported
Microsoft Agent FrameworkSupported
Google ADKSupported
AWS Strands AgentsSupported
MastraSupported
Pydantic AISupported
AgnoSupported
LlamaIndexSupported
AG2Supported
AWS Bedrock AgentCoreSupported

SDKs

At the time of writing, community-led SDKs for Kotlin, Go, Dart, Java, and Rust were available alongside TypeScript, and .NET and Nim SDKs were in progress. As of August 2026, SDKs for Kotlin, Go, Dart, Java, Rust, Ruby, C++, and .NET, including community contributions, are listed as Supported.


Conclusion — The Significance of AG-UI

AG-UI is a protocol that standardizes communication over the "last mile" between agents and users.

Complementary Nature of the Three Protocols

MCP handles the agent's connection to tools and data, A2A handles its connection to other agents, and AG-UI handles its connection to the user. With all three, every connection between an agent and the outside world can go through an open standard protocol.

Why Use AG-UI

  1. Framework-agnostic: One protocol connects to every backend that supports AG-UI, including LangGraph, CrewAI, Microsoft Agent Framework, and Google ADK.
  2. Real-time: Text, tool calls, and state changes arrive as events one by one, so the UI can show what the agent is doing while it runs.
  3. Standardized Human-in-the-Loop: User approval flows are defined as part of the standard protocol.
  4. State synchronization: Snapshots plus deltas keep agent and frontend state in sync in both directions with little traffic.
  5. Extensibility: You can extend it with middleware, custom events, and custom orchestrators.

Microsoft Agent Framework supports these features first-party through the agent-framework-ag-ui package. A single call to add_agent_framework_fastapi_endpoint exposes an existing agent as an AG-UI-compatible endpoint.