AWS Builder Center
AG-UI: A Practical Look at the Agent-to-User Protocol

AG-UI: A Practical Look at the Agent-to-User Protocol

AG-UI is an open protocol that standardises how AI agents communicate with user interfaces. After building a working demo, here's an assessment of what it does, what it doesn't, and when it works best.

Solutions Architect @ AWS

What AG-UI is

AG-UI defines a contract for streaming events from an agent backend to a frontend. It uses Server-Sent Events (SSE) or WebSocket to deliver typed messages like  TEXT_MESSAGE_CONTENT ,  TOOL_CALL_START , and  STATE_DELTA .
The core value proposition: interoperability. If your agent emits AG-UI events, any AG-UI-compatible frontend can consume them. Swap LangGraph for Strands, or React for Vue - the protocol stays the same.

The Event Types

AG-UI defines several event categories:
Lifecycle events:
  •  RUN_STARTED  /  RUN_FINISHED  - Marks execution boundaries
  •  RUN_ERROR  - Signals failures with error codes
Message streaming:
  •  TEXT_MESSAGE_START  /  TEXT_MESSAGE_CONTENT  /  TEXT_MESSAGE_END  - Token-by-token text delivery
Tool execution:
  •  TOOL_CALL_START  /  TOOL_CALL_ARGS  /  TOOL_CALL_END  - When the agent invokes tools
  •  TOOL_CALL_RESULT  - Tool output
State synchronisation:
  •  STATE_DELTA  /  STATE_SNAPSHOT  - Push structured data to the UI using JSON Patch operations

What AG-UI Doesn't Do

Let me be direct about the limitations:
No magic UI rendering. AG-UI gives you events. You still write all the code to render flight cards, weather widgets, or booking confirmations. The protocol doesn't know what a "flight" is.
Human-in-the-loop is just conversation. There's no special "pause and wait for approval" primitive. Your agent asks a question in text, the user replies, and you send another message. It's the same pattern as any chatbot.
State sync requires manual work. You decide what state to emit, when to emit it, and how to structure it. Then you write the UI code to interpret and render that state. AG-UI provides the transport, not the logic.
Session management is your problem. AG-UI has  threadId  and  runId  fields, but maintaining conversation history across messages is your responsibility. Each request is stateless unless you pass the full history.

When AG-UI Makes Sense

AG-UI is worth adopting when:
  1. You're building an ecosystem. Multiple agent backends need to talk to multiple frontends. The protocol provides a common contract.
  1. You want framework portability. Build with LangGraph today, migrate to CrewAI tomorrow. The frontend integration stays the same.
  1. You're deploying to AgentCore Runtime. AWS supports AG-UI as a first-class protocol, handling authentication and scaling.
  1. Teams need to agree on event shapes. Instead of bikeshedding JSON formats, adopt the standard.



Building an AG-UI Server

Here's a minimal working example using AWS Strands. Create  weather_agent.py :
import uvicorn
import httpx
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from ag_ui.core import RunAgentInput, StateDeltaEvent
from ag_ui.encoder import EventEncoder
from strands import Agent, tool
from strands.models.bedrock import BedrockModel

state_queue = []

@tool
def get_weather(city: str) -> dict:
"""Get current weather for a city using Open-Meteo API."""
# Geocode the city
geo = httpx.get(f"https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1").json()
if not geo.get("results"):
return {"error": f"City '{city}' not found"}
loc = geo["results"][0]
lat, lon = loc["latitude"], loc["longitude"]
# Fetch weather data
weather = httpx.get(
f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}"
f"&current=temperature_2m,weather_code,wind_speed_10m,relative_humidity_2m,cloud_cover"
f"&daily=temperature_2m_max,temperature_2m_min,weather_code&timezone=auto"
).json()
codes = {0: "Clear", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
45: "Foggy", 61: "Light rain", 63: "Rain", 65: "Heavy rain", 95: "Thunderstorm"}
current = weather["current"]
result = {
"city": loc["name"],
"country": loc.get("country", ""),
"temperature": current["temperature_2m"],
"condition": codes.get(current["weather_code"], "Unknown"),
"humidity": current["relative_humidity_2m"],
"wind": current["wind_speed_10m"],
"clouds": current.get("cloud_cover", 0),
}
# Queue state update for UI
state_queue.append({"type": "weather", "data": result})
return result

# Create agent
model = BedrockModel(model_id="us.anthropic.claude-sonnet-4-20250514", region_name="us-west-2")
agent = Agent(model=model, tools=[get_weather], system_prompt="You are a weather assistant. Be concise.")

app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

@app.post("/invocations")
async def invocations(input_data: dict, request: Request):
encoder = EventEncoder(accept=request.headers.get("accept"))
async def generate():
global state_queue
state_queue = []
from ag_ui_strands import StrandsAgent
agui = StrandsAgent(agent=agent, name="weather", description="Weather assistant")
async for event in agui.run(RunAgentInput(**input_data)):
yield encoder.encode(event)
# Emit state deltas after tool results
if hasattr(event, 'type') and 'TOOL_CALL_RESULT' in str(event.type):
for s in state_queue:
yield encoder.encode(StateDeltaEvent(
delta=[{"op": "add", "path": f"/{s['type']}", "value": s['data']}]
))
state_queue = []
return StreamingResponse(generate(), media_type=encoder.get_content_type())

@app.get("/ping")
async def ping():
return JSONResponse({"status": "Healthy"})

if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080)
Install dependencies and run:
pip install fastapi uvicorn ag-ui-strands httpx
python weather_agent.py
Test with curl:
curl -N -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{
"threadId": "test-123",
"runId": "run-456",
"messages": [{"id": "msg-1", "role": "user", "content": "Weather in London?"}],
"state": {},
"tools": [],
"context": [],
"forwardedProps": {}
}'
You'll see SSE events stream back:
data: {"type":"RUN_STARTED","threadId":"test-123","runId":"run-456"}
data: {"type":"TEXT_MESSAGE_START","messageId":"msg-789","role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg-789","delta":"Let me check"}
data: {"type":"TOOL_CALL_START","toolCallId":"tool-001","toolCallName":"get_weather"}
data: {"type":"TOOL_CALL_RESULT","toolCallId":"tool-001","content":"{\"city\":\"London\"...}"}
data: {"type":"STATE_DELTA","delta":[{"op":"add","path":"/weather","value":{...}}]}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg-789","delta":"London is currently 12°C..."}
data: {"type":"RUN_FINISHED","threadId":"test-123","runId":"run-456"}

Deploying to AgentCore Runtime

AgentCore Runtime handles authentication, session isolation, and scaling. Deploy with:
pip install bedrock-agentcore-starter-toolkit

# Configure for AG-UI protocol
agentcore configure -e weather_agent.py --protocol AGUI

# Deploy
agentcore deploy
You'll receive an ARN like:
arn:aws:bedrock-agentcore:us-west-2:123456789:runtime/weather_agent-xyz123
Invoke the deployed agent:
import asyncio
import json
import os
from urllib.parse import quote
from uuid import uuid4
import httpx
from httpx_sse import aconnect_sse

async def invoke_agent(message: str):
agent_arn = os.environ.get('AGENT_ARN')
bearer_token = os.environ.get('BEARER_TOKEN')
escaped_arn = quote(agent_arn, safe='')
url = f"https://bedrock-agentcore.us-west-2.amazonaws.com/runtimes/{escaped_arn}/invocations?qualifier=DEFAULT"
headers = {
"Authorization": f"Bearer {bearer_token}",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": str(uuid4()),
}
payload = {
"threadId": str(uuid4()),
"runId": str(uuid4()),
"messages": [{"id": str(uuid4()), "role": "user", "content": message}],
"state": {},
"tools": [],
"context": [],
"forwardedProps": {},
}

async with httpx.AsyncClient(timeout=300) as client:
async with aconnect_sse(client, "POST", url, headers=headers, json=payload) as sse:
async for event in sse.aiter_sse():
data = json.loads(event.data)
if data.get("type") == "TEXT_MESSAGE_CONTENT":
print(data.get("delta", ""), end="", flush=True)
elif data.get("type") == "STATE_DELTA":
print(f"\n[State update: {data.get('delta')}]")

asyncio.run(invoke_agent("What's the weather in Tokyo?"))

The Bigger Picture

AG-UI sits alongside other emerging protocols:
  • MCP (Model Context Protocol) - Connects agents to tools and data sources
  • A2A (Agent-to-Agent) - Handles coordination between multiple agents
  • AG-UI - Handles agent-to-user interaction
These are complementary layers. MCP gives your agent capabilities. A2A lets agents collaborate. AG-UI surfaces the results to humans.

Conclusion

AG-UI is a sensible protocol for standardising agent-to-UI communication. It's not magic - you still write the rendering logic, manage state, and handle conversation flow. But if you're building for an ecosystem where interoperability matters, or deploying to infrastructure that supports it natively, the protocol provides a solid foundation.
For single-purpose applications, evaluate whether the abstraction layer is worth the complexity. Sometimes raw SSE with custom JSON is simpler and sufficient.

Resources

Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article