A proof of concept for implementing SSE (Server-Sent Events) + AG-UI end to end: a Kotlin/Spring Boot backend that streams AG-UI protocol events over SSE, and a Kotlin/Jetpack Compose Android app that consumes them and renders a live chat UI.
backend/ Spring Boot (WebFlux/WebMVC) server exposing SSE + AG-UI endpoints
frontend/app/ Android (Jetpack Compose) app consuming the endpoints via the AG-UI Kotlin client
This is a single Gradle build rooted at the repo root: settings.gradle.kts includes :backend and :frontend:app as subprojects, sharing one Gradle wrapper, version catalog (gradle/libs.versions.toml), and local.properties. Open the repo root in Android Studio — it imports as one project with both modules, each keeping its own run/debug configs (Spring Boot run config for :backend, Android emulator/device config for :frontend:app).
Kotlin + Spring Boot server (backend/) built with the com.ag-ui.community:kotlin-core library for AG-UI event types/serialization, and google-genai for the Gemini client used in the full example.
SimpleAgUiController.kt contains two minimal "hello world" examples:
| Endpoint | Method | Description |
|---|---|---|
/simple/hello-world |
GET |
A bare SSE stream (no AG-UI protocol) — emits two plain HELLO_WORLD events a couple of seconds apart. Since it's a GET endpoint, it can be opened directly in a browser to see raw SSE frames. |
/simple/ag-ui |
POST |
The smallest possible AG-UI flow over SSE: RunStartedEvent → TextMessageStartEvent → TextMessageContentEvent (x2) → TextMessageEndEvent → RunFinishedEvent, each AG-UI event serialized to JSON and wrapped as an SSE event named after its eventType. |
Kotlin isn't everyone's first language, so I asked Claude to generate equivalent, standalone implementations of the simplest example — the pure SSE hello-world endpoint above (not the AG-UI examples, which are protocol-specific) — in Go and PHP. The idea is that seeing the exact same two-event SSE stream written in three different languages makes it clearer that SSE is just plain text framing (id: / event: / data: / retry: + a blank line) over a kept-open HTTP connection, independent of any framework or protocol on top:
backend/ai-generated/go/main.go— stdlib-only, run withgo run main.go, serves onhttp://localhost:8081/hello-world.backend/ai-generated/php/index.php— stdlib-only, run withphp -S localhost:8082 index.php, serves onhttp://localhost:8082/.
Both are AI-generated, standalone references only — they aren't wired into the Spring Boot app or the Android frontend.
AgUiController.kt is the full example: it simulates a realistic agent turn using many of the event types in the AG-UI protocol (reasoning chunks, streamed text messages word-by-word, a custom event, run lifecycle events), driven by artificial delay()s to emulate real streaming latency.
| Endpoint | Method | Description |
|---|---|---|
/chat/stream |
POST |
Emits the same simulated event sequence as it's produced, event by event, as a live SSE stream. Consumes a RunAgentInput request body. |
/chat/buffered |
POST |
Runs the identical simulation but collects it into a List first (toList()) before emitting — used to compare a "streamed" vs. "buffered/classic" experience against the same backend logic. |
Simulated sequence emitted by buildEventFlow: RunStartedEvent → reasoning steps ("Loading", "Checking database", "Crunching numbers", "Almost there...") → a streamed text message echoing the user's input → another reasoning step → a CustomEvent (custom UI element) → a final reasoning step → a second streamed text message → RunFinishedEvent.
ReconnectionController.kt exists to exercise AG-UI/SSE reconnection behavior: a single long-running stream, useful for observing how a client reacts when a connection drops mid-run and it has to reconnect.
| Endpoint | Method | Description |
|---|---|---|
/reconnect/stream |
POST |
Emits RunStartedEvent, then 20 TextMessageChunkEvents (all on the same message) spaced 3 seconds apart (60s total), then RunFinishedEvent. Consumes a RunAgentInput request body, same as /chat/stream. |
The stream deliberately runs long enough to outlast typical client request/read timeouts (e.g. the Android app's StatefulAgUiAgent is configured with a 5s requestTimeout in MainViewModel.kt), so it can be used to trigger and observe reconnection.
./gradlew :backend:bootRunThe server starts on port 8080 by default (see spring.application.name=sse-server-poc in application.properties).
Native Android app (frontend/) built with Jetpack Compose, using the com.ag-ui.community:kotlin-client library (StatefulAgUiAgent) to connect to the backend and stream AG-UI events.
MainContent.ktwires up two independent chat panes side by side, each backed by its ownMainViewModelpointed at a different backend endpoint —chat/buffered("Classic (GET)") andchat/stream("Event stream (SSE)") — so both delivery styles can be compared live against identical simulated responses. A "Send both" action fires the same message at both, and "Clear messages" resets both panes.MainViewModel.ktowns aStatefulAgUiAgentfor a given URL, sends messages on a per-sessionthreadId, and measures/logs timing metrics for each run: time to first agent response, Time To First Token (TTFT), and total run duration — surfaced as system messages in the chat.ChatManager.ktfolds incoming AG-UI events (TextMessageStart/Content,ReasoningStart/Chunk/End,CustomEvent,MessagesSnapshotEvent, etc.) into an ordered, de-duplicated list ofChatItems driving the UI, including a synthetic "Loading" placeholder shown while waiting for the first event.ChatUI.ktrenders eachChatItem(user/assistant/system messages, reasoning, custom elements) in an auto-scrollingLazyColumnwith a text input to send new messages.
By default the app targets endpoints via the Android emulator's host loopback address (10.0.2.2), configured in MainContent.kt:
val bufferedViewModel = createViewModel("http://10.0.2.2:8080/chat/buffered")
val streamViewModel = createViewModel("http://10.0.2.2:8080/chat/stream")To use a custom backend (a different host/port, or a physical device instead of the emulator), change these URLs in MainContent.kt.
Open the repo root in Android Studio and run the :frontend:app module on an emulator or device, or from the command line:
./gradlew :frontend:app:installDebug- AG-UI documentation
- AG-UI event types
- AG-UI official SDKs
- Kotlin SDK — used by this project's backend and frontend
- Go SDK