
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.
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:
- You're building an ecosystem. Multiple agent backends need to talk to multiple frontends. The protocol provides a common contract.
- You want framework portability. Build with LangGraph today, migrate to CrewAI tomorrow. The frontend integration stays the same.
- You're deploying to AgentCore Runtime. AWS supports AG-UI as a first-class protocol, handling authentication and scaling.
- 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 uvicornimport httpxfrom fastapi import FastAPI, Requestfrom fastapi.responses import StreamingResponse, JSONResponsefrom fastapi.middleware.cors import CORSMiddlewarefrom ag_ui.core import RunAgentInput, StateDeltaEventfrom ag_ui.encoder import EventEncoderfrom strands import Agent, toolfrom strands.models.bedrock import BedrockModel
state_queue = []
@tooldef 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"¤t=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 agentmodel = 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 httpxpython weather_agent.pyTest 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 protocolagentcore configure -e weather_agent.py --protocol AGUI
# Deployagentcore deployYou'll receive an ARN like:
arn:aws:bedrock-agentcore:us-west-2:123456789:runtime/weather_agent-xyz123Invoke the deployed agent:
import asyncioimport jsonimport osfrom urllib.parse import quotefrom uuid import uuid4import httpxfrom 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
- AG-UI Dojo - Interactive examples
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article