Conexão · Interrompida

Algo não carregou

Parte desta página não chegou até você. Recarregue para tentar novamente — se persistir, verifique sua conexão.

Pular para o conteúdo principal
AI7 min de leitura

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.

Todos os Posts
2/4

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: CUSTOM e RAW para 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:

kotlin
// 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:

bash
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 $schema já 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_SNAPSHOT novo 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 por RUN_FINISHED ou RUN_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.

Continue lendo

Curtindo? Talvez goste disso aqui.

Nada parecido — quer tentar outro ângulo?

Isso foi útil?

Deixe uma avaliação ou uma nota rápida — me ajuda a melhorar.