Lendo AG-UI como um protocolo de fio, não um framework
Eu ficava reconstruindo o mesmo envelope SSE toda vez que escrevia uma UI de agente. AG-UI é a primeira tentativa séria que vi de padronizar esse envelope. Neste post eu desnudo o protocolo até seu formato de fio e reconstruo um endpoint mínimo em Spring WebFlux que fala o protocolo sem um SDK.
Construir uma UI de agente sempre termina do mesmo jeito para mim. Eu ligo Server-Sent Events do backend, invento mais um envelope para "pedaços de tokens vs chamadas de tool vs atualizações de estado", e então passo uma semana debugando as partes onde meu frontend e backend divergiam. AG-UI é a primeira tentativa séria que vi de padronizar esse envelope em vez de entregar mais uma biblioteca React.
Neste post eu trato AG-UI pelo que ele de fato é: um protocolo de fio. Olhei além dos SDKs oficiais e escrevi um endpoint Spring WebFlux mínimo que emite eventos AG-UI diretamente. A superfície que você precisa conhecer é pequena, e uma vez que o formato faz sentido o resto é só encanamento.
O event stream é o contrato
AG-UI é entregue como um protocolo aberto sob licença MIT mantido pela CopilotKit. Um cliente, normalmente um browser, envia um único HTTP POST para um endpoint de agente com um body RunAgentInput. O servidor responde com um stream de eventos tipados que termina em RUN_FINISHED ou RUN_ERROR. Na rede, esse stream é Server-Sent Events por default, um evento JSON por linha data:, mas a camada de abstração é agnóstica de transporte; WebSockets e frames binários são permitidos pela spec.
Os eventos caem em cinco famílias às quais eu sempre volto quando leio a documentação:
- Ciclo de vida:
RUN_STARTED,RUN_FINISHED,RUN_ERROR,STEP_STARTED,STEP_FINISHED - Mensagens de texto:
TEXT_MESSAGE_START,TEXT_MESSAGE_CONTENT,TEXT_MESSAGE_END - Chamadas de tool:
TOOL_CALL_START,TOOL_CALL_ARGS,TOOL_CALL_END - Estado:
STATE_SNAPSHOT,STATE_DELTA,MESSAGES_SNAPSHOT - Saídas de emergência:
CUSTOMeRAWpara qualquer coisa que não se encaixe
São dezesseis tipos de evento na versão que li. A contagem importa menos do que a regra em torno deles: eventos compartilhando um messageId ou toolCallId devem ser emitidos na ordem START → CONTENT/ARGS → END, e cada run deve ser delimitado por um RUN_STARTED e um RUN_FINISHED ou RUN_ERROR terminal. Qualquer frontend pode reconstruir uma UI coerente reproduzindo esses eventos em ordem.
Para estado, AG-UI usa um padrão snapshot-delta que vai parecer familiar se você já escreveu uma UI adjacente a CRDT. O primeiro STATE_SNAPSHOT é a verdade. Cada STATE_DELTA posterior é um JSON Patch (RFC 6902) aplicado em cima. Isso mantém o stream barato para conversas longas e permite o servidor reemitir um snapshot sempre que clientes saem de sincronia.
O ciclo de vida de um único run é mais fácil de ver como timeline do que como prosa:
Um endpoint AG-UI mínimo em Spring WebFlux
Para me convencer de que o protocolo era realmente tão pequeno, escrevi um serviço Spring Boot de um único arquivo que faz stream de eventos AG-UI válidos sem nenhuma biblioteca AG-UI. Isto é a coisa inteira:
// AgUiDemo.kt - start with: ./gradlew bootRun
package demo
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux
import java.time.Duration
import java.util.UUID
@SpringBootApplication
class AgUiDemo
fun main(args: Array<String>) { runApplication<AgUiDemo>(*args) }
data class RunAgentInput(
val threadId: String,
val messages: List<Map<String, Any>> = emptyList(),
)
@RestController
class AgentController(private val mapper: ObjectMapper) {
@PostMapping("/agent", produces = [TEXT_EVENT_STREAM_VALUE])
fun run(@RequestBody input: RunAgentInput): Flux<String> {
val runId = UUID.randomUUID().toString()
val messageId = UUID.randomUUID().toString()
val events = listOf(
mapOf("type" to "RUN_STARTED", "threadId" to input.threadId, "runId" to runId),
mapOf("type" to "TEXT_MESSAGE_START", "messageId" to messageId, "role" to "assistant"),
mapOf("type" to "TEXT_MESSAGE_CONTENT", "messageId" to messageId, "delta" to "Hello "),
mapOf("type" to "TEXT_MESSAGE_CONTENT", "messageId" to messageId, "delta" to "from AG-UI."),
mapOf("type" to "TEXT_MESSAGE_END", "messageId" to messageId),
mapOf("type" to "STATE_DELTA", "delta" to listOf(
mapOf("op" to "add", "path" to "/lastTurn", "value" to runId))),
mapOf("type" to "RUN_FINISHED", "threadId" to input.threadId, "runId" to runId),
)
return Flux.fromIterable(events)
.delayElements(Duration.ofMillis(60))
.map { "data: ${mapper.writeValueAsString(it)}\n\n" }
}
}Rode com ./gradlew bootRun em um projeto padrão Spring Boot 3.x que tem spring-boot-starter-webflux no classpath, então chame com:
curl -N -X POST localhost:8080/agent \
-H 'Content-Type: application/json' \
-d '{"threadId":"t1"}'Os eventos chegam no fio em ordem. Um cliente React CopilotKit apontado para essa URL renderiza a resposta em streaming exatamente como se tivesse vindo de uma integração completa LangGraph ou CrewAI.
Três detalhes nesse snippet valem uma pausa. Primeiro, produces = TEXT_EVENT_STREAM_VALUE é o que transforma um Flux<String> do Reactor em SSE; delayElements está ali só para eu poder ver o stream fluir no terminal. Segundo, STATE_DELTA carrega um array JSON Patch, não um diff simples; esse é o único detalhe em que errei na minha primeira tentativa, porque é fácil confundir com JSON Merge Patch (RFC 7396). Terceiro, o protocolo não exige campos SSE id ou event: — apenas data: com um payload JSON e uma linha em branco terminal. Agentes que dependem de nomes de evento SSE para roteamento estão fora da spec.
Onde ele se posiciona ao lado de MCP e A2A, e onde falha
AG-UI não é competidor do MCP. MCP (Model Context Protocol) padroniza a borda agent-to-tool com JSON-RPC; AG-UI padroniza a borda agent-to-frontend com um event stream. Protocolos A2A ficam na borda agent-to-agent. Em um sistema onde uma UI fala com um orquestrador que chama duas tools e outro agente, os três protocolos podem coexistir sem sobreposição.
Runtimes hospedados começaram a adotar o protocolo. O Amazon Bedrock AgentCore adicionou AG-UI ao lado das superfícies MCP e A2A já existentes em março de 2026, o que tornou a estratificação dos três protocolos visível em um único deployment gerenciado e me deu um motivo concreto para continuar tratando AG-UI como um contrato estável em vez de uma convenção passageira. O A2UI v0.9 do Google, anunciado algumas semanas antes, empilha um vocabulário de UI generativa em cima que o AG-UI pode carregar como eventos CUSTOM — então o protocolo continua estreito enquanto a descrição de UI sobe um nível.
Esse foco estreito também expõe arestas ásperas. Algumas das minhas notas:
- SSE é half-duplex. Input de usuário no meio do run ainda volta por uma chamada HTTP separada; a spec permite WebSockets, mas nenhum SDK de primeira parte os usa ainda, então fluxos bidirecionais como interrupção por voz ficam a seu cargo.
- Autenticação não tem opinião. A spec não prescreve headers de bearer token, scopes ou claims de tenant. Todo deployment de produção que olhei parafusa isso em cima.
- O schema de evento pode brigar com frameworks de agente. Schemas de tool carregando meta-campos
$schemajá causaram crashes de validação ao fazer bridge do Google ADK para Pydantic AI via AG-UI, um sintoma do protocolo passando payloads de tool totalmente tipados de ponta a ponta. - Não existem benchmarks publicados para throughput ou latência p99. O padrão snapshot-delta também torna estado de conversa grande caro se você emite um
STATE_SNAPSHOTnovo em cada reconexão. Tive que projetar meu próprio orçamento para esse caso.
Nada disso é impeditivo, mas são o formato do trabalho de engenharia que o AG-UI deixa no seu prato.
Quando eu usaria, e quando não
Use AG-UI quando você quer um contrato estável entre um frontend e um ou mais runtimes de agente que você pode trocar depois, e quando um feed de tokens em streaming, chamadas de tool e estado compartilhado são as três coisas que a UI precisa ver.
Pule quando uma resposta única, bloqueante, JSON-sobre-HTTP resolveria, quando você precisa de voz full-duplex ou colaboração a nível de cursor (escolha um protocolo WebSocket ou WebRTC diretamente), ou quando você já é dono das duas pontas do fio e o custo de um vocabulário de dezesseis eventos supera a portabilidade que você ganha.
Takeaways:
- AG-UI é um pequeno protocolo de fio, não um framework; as cinco famílias de evento e a regra de ordenação do ciclo de vida são quase toda a spec.
- O contrato HTTP é "POST um
RunAgentInput, receba um stream SSE de eventos tipados terminado porRUN_FINISHEDouRUN_ERROR". - Um stream AG-UI válido pode vir de qualquer backend; o exemplo Spring WebFlux acima tem menos de 40 linhas e não precisa de SDK.
- MCP, AG-UI e A2A cobrem cada um uma borda diferente de um sistema de agente e se compõem de forma limpa, e runtimes hospedados como o Bedrock AgentCore agora expõem os três lado a lado.
- Trate auth, backpressure e tamanho de snapshot como seu problema; a spec não vai decidir por você.
Leitura adicional: a lista canônica de eventos fica na documentação do AG-UI em concepts/events, e os SDKs de referência para TypeScript, Python e Kotlin ficam no repositório ag-ui-protocol/ag-ui no GitHub.
Curtindo? Talvez goste disso aqui.
Nada parecido — quer tentar outro ângulo?
Posts Relacionados
Expondo Agentes Spring AI via o Protocolo A2A: O Que a Interoperabilidade Realmente Te Dá
A integração A2A do lado servidor no Spring AI já está estável o suficiente para colocar em produção, mas o protocolo é mais útil em fronteiras organizacionais, não como substituto de RPC interno. Este post percorre o que de fato muda em uma codebase Spring AI, onde ainda existem arestas afiadas, e um framework prático de decisão entre A2A, MCP e REST puro.
JetBrains Tracy: Observabilidade Pragmática de IA para Kotlin
JetBrains Tracy é uma biblioteca Kotlin que conecta tracing ciente de LLM na sua aplicação em cima do OpenTelemetry. Este post percorre como eu integrei no serviço Spring Boot, as decisões de design que importam, e os modos de falha que times encontram quando chamadas de LLM se tornam o caminho mais quente do sistema.
A Espinha Dorsal Determinística: Por Que Sistemas de IA em Produção Estão Se Afastando de Agentes Totalmente Autônomos
Agentes totalmente autônomos são difíceis de limitar, difíceis de testar e caros de operar. Uma espinha dorsal determinística com etapas de agente estreitas devolve o controle de fluxo a você enquanto mantém a inteligência onde ela importa. Veja como projetar, testar e migrar nessa direção.