Your Agent Streams SSE. Your Frontend Still Needs a Message Contract.

Your agent already streams; the real frontend cost is inventing a message contract for runs, tools, tokens, and errors. AG-UI is that contract, already standardized on AgentCore Runtime.

Rick Hightower

Cover image for “Your Agent Streams SSE. Your Frontend Still Needs a Message Contract.” by Rick Hightower

AG-UI is the standardized event protocol AgentCore Runtime already serves over SSE and WebSocket. Swap one app class, translate your stream, stop inventing a private taxonomy.

You shipped streaming SSE, and now a frontend engineer has to invent how runs, tool calls, partial messages, and mid-stream errors look. Someone already wrote that contract.

In this article: You will learn what the AG-UI protocol buys you on Amazon Bedrock AgentCore Runtime, how serve_ag_ui and AGUIApp wire into a hosted DeepAgents loop, which lifecycle events the SDK actually confirms, and when plain SSE is still the right call. By the end you will know when to standardize the stream, when to open /ws, and why neither exempts you from the session idle clock.

Streaming is enough to build a UI, in the same sense that TCP is enough to build a website.

You yield tokens and tool starts from an async generator. The Runtime bridges them to SSE. A human watches the agent think. That is real progress. What you still do not have is a contract: how a client knows a run started, how a tool call differs from a token, what a partial message looks like before it is complete, and how an error lands mid-stream.

Every team invents those answers. Every team invents them differently. Then someone writes a second frontend.

AG-UI is that contract, standardized. AgentCore Runtime serves it over both SSE on POST /invocations and WebSocket on /ws, from a single entrypoint. This is a mechanical topic, and I am not going to pretend otherwise. Here is what it is and when to bother.

Ad-hoc SSE forces each frontend to invent run, tool, token, and error shapes; AG-UI standardizes the event taxonomy once.

The one-line form

If your agent already exposes an AG-UI-shaped .run() interface, the integration is a single call:

from bedrock_agentcore.runtime import serve_ag_ui
serve_ag_ui(agui_agent)

That is the whole story for agents that already speak the protocol. Most production loops do not, so you take the real form: host the adapter yourself.

The real form: AGUIApp as adapter

Swap BedrockAgentCoreApp for AGUIApp. Keep the container contract. Implement an entrypoint that accepts RunAgentInput, yields lifecycle events around your framework stream, and threads thread_id into the agent config so session continuity still works.

The annotated listing below is the full pattern. Orientation markers call out the host, the typed input, the run open/close events, and the middle translation you still own.

from ag_ui.core import RunAgentInput, RunStartedEvent, RunFinishedEvent
from bedrock_agentcore.runtime import AGUIApp
from langchain_aws import ChatBedrockConverse
from deepagents import create_deep_agent

llm = ChatBedrockConverse(model="us.anthropic.claude-sonnet-4-6")
agent = create_deep_agent(model=llm, tools=[], system_prompt="You are a market analyst.")

app = AGUIApp()  # ①

@app.entrypoint  # ②
async def my_agent(input_data: RunAgentInput):
    user_text = "".join(  # ③
        getattr(p, "text", "") for m in (input_data.messages or []) if m.role == "user"
        for p in (m.content or [])
    ) or str(input_data)

    yield RunStartedEvent(thread_id=input_data.thread_id, run_id=input_data.run_id)  # ④

    async for ev in agent.astream_events(  # ⑤
        {"messages": [("user", user_text)]},
        {"configurable": {"thread_id": input_data.thread_id}},
        version="v2",
    ):
        if ev["event"] == "on_chat_model_stream" and ev["data"]["chunk"].content:
            # Translate to AG-UI TextMessageStart/Delta/End here, per ag-ui-protocol.  # ⑥
            yield ev["data"]["chunk"].content

    yield RunFinishedEvent(thread_id=input_data.thread_id, run_id=input_data.run_id)  # ⑦

① AGUIApp is the AG-UI host. It replaces BedrockAgentCoreApp while keeping the same container contract.

② The entrypoint accepts RunAgentInput, the protocol's typed request shape for messages, thread, and run ids.

③ User text is flattened from the protocol message list so the agent still receives a plain string prompt.

④ RunStartedEvent opens the run lifecycle so clients can render "run began" before any tokens arrive.

⑤ astream_events is the data-plane loop; thread_id from the protocol input is wired into the DeepAgents config.

⑥ Token chunks are the adapter middle: map model stream events to AG-UI text-message events per ag-ui-protocol.

⑦ RunFinishedEvent closes the run lifecycle with the same thread and run ids the client opened.

Note: The full extracted listing at code/agent-core/appendix-a-ag-ui-frontend/listings/01-agui-app-entrypoint.py shows the complete program.

Everything else about the container stays the same: ARM64, bind 0.0.0.0:8080, /ping, and a per-session microVM. You changed the message surface, not the Runtime contract.

AGUIApp replaces BedrockAgentCoreApp; ARM64, port 8080, /ping, and per-session microVMs stay the same.

What is verified, and what is yours

Treat this honestly as an adapter. RunAgentInput, RunStartedEvent, RunFinishedEvent, RunErrorEvent, and EventEncoder are confirmed in the SDK. The text-message streaming classes (TextMessageStart, TextMessageDelta, TextMessageEnd) come from the ag-ui-protocol package. Read that package's API rather than trusting tutorial names. Lifecycle events are verified; the translation in the middle is yours.

A client hits Runtime; AGUIApp opens the run, streams text events from astream_events, then emits RunFinishedEvent.

Notice thread_id again. input_data.thread_id goes straight into the DeepAgents config. If a stateful thing in this stack works, an ID got threaded correctly. AG-UI is just another direction that identifier arrives from: the protocol input, not only the Runtime session header.

When to bother

Use plain SSE when your client is your own code, the rendering is simple, and you control both ends. Homegrown yields are fine, and they are less to learn. Most internal tools land here.

Use AG-UI when more than one client renders your agent; when a frontend team owns the UI and you want to hand them a spec instead of a changelog; or when you want off-the-shelf AG-UI components rather than writing a renderer.

Use the WebSocket when the human talks back mid-run. That is the real discriminator. SSE is one-directional. An approval flow, a mid-run correction, or a "no, not that competitor" is a client-to-agent message, and that wants /ws.

Choose plain SSE for one simple client, AG-UI for multi-client product surfaces, and WebSocket when humans reply mid-run, then checkpoint if they are slow.

One warning before you build on /ws. A WebSocket held open while a human decides whether to approve something burns the session's 15-minute idle clock while a person has lunch. AG-UI gives you the message contract; it does not give you an exemption from the lifecycle. If the human might be slow, checkpoint and re-invoke rather than holding the connection.

Holding /ws through a long human pause risks the 15-minute idle clock; checkpoint and re-invoke instead of blocking.

Do this today

  • Decide whether you have one client you control or a product surface that will grow a second frontend. That binary chooses plain SSE versus AG-UI.
  • If you adopt AG-UI, swap to AGUIApp (or serve_ag_ui if you already have a .run() surface) and keep the same ARM64 container contract.
  • Implement RunStartedEvent / RunFinishedEvent around your existing astream_events loop, and wire input_data.thread_id into the framework config.
  • Read ag-ui-protocol for TextMessageStart / Delta / End and map token chunks explicitly; do not invent parallel event names.
  • For mid-run human input, prefer /ws plus checkpoint-and-reinvoke over holding a socket through a slow approval.

Stop inventing a private event taxonomy

AG-UI is a protocol for a problem you will otherwise solve badly, twice. It costs you one class swap and an event-translation function. It buys you a contract your frontend team can build against without asking you what shape the tool calls come out in.

If your agent has one client and you wrote it, skip this. If your agent is a product, do not invent your own event taxonomy.