Modern Agent Engineering

Del chat a la UI de agentes: ChatKit, A2UI y superficies de interacción estructuradas

La UI de agentes se está convirtiendo en un stack propio. Aquí verás cómo encajan ChatKit, A2UI y MCP Apps, dónde se queda corto el chat convencional y cómo diseñar superficies de interacción estructuradas que realmente ayuden a los usuarios a trabajar.

28 min de lectura

TL;DR

  • El chat nos llevó a la era de los agentes, pero por sí solo es un espacio de trabajo poco eficaz para revisar, aprobar, comparar, filtrar y editar en varios pasos.
  • El stack se está dividiendo en tres capas: contenedores de chat como ChatKit, protocolos de UI declarativa como A2UI y runtimes de host como MCP Apps.
  • ChatKit es la forma más rápida de incorporar chat con agentes en tu propio producto si quieres una interfaz pulida, streaming, hilos, archivos adjuntos y widgets sin armar todo desde cero.
  • A2UI es una buena opción cuando los agentes son remotos, no son de confianza, operan en múltiples plataformas o entre organizaciones y necesitan describir la UI de forma segura como datos en lugar de enviar código ejecutable.
  • MCP Apps estandariza superficies de aplicación interactivas dentro de hosts de agentes como Claude, ChatGPT, Goose y VS Code, combinando llamadas a herramientas con recursos de UI aislados en un sandbox.
  • Para aplicaciones web propias, una opción predeterminada muy práctica es: useChat en el frontend, streamText en el backend y partes de herramienta tipadas o partes de datos personalizadas cuando el usuario necesite una superficie real en lugar de otro párrafo.
  • El patrón de diseño ganador no es «reemplaza tu producto con chat». Es «usa el chat para expresar la intención y narrar lo que sucede, y luego lleva al usuario a una superficie estructurada cuando el trabajo se vuelva visual, requiera manejar estado o dependa de aprobaciones».

Qué aprenderás aquí

  • Por qué el chat convencional deja de ser suficiente para workflows de agentes exigentes
  • En qué se diferencian ChatKit, A2UI y MCP Apps a nivel arquitectónico
  • Cuándo elegir cada capa
  • Qué significan realmente las «superficies de interacción estructuradas» en términos de producto
  • Cómo implementar dos patrones prácticos de superficies estructuradas con AI SDK
  • Reglas de diseño para construir interfaces nativas para agentes sin crear un caos confuso

Por qué el chat por sí solo deja de ser suficiente

El repositorio ya tiene «El giro hacia el chat», y ese cambio es real. Cada vez más, los usuarios esperan pedirle resultados al software en lugar de navegar por menús.

Pero el chat tiene un límite evidente: es excelente para captar intenciones y dar explicaciones, y bastante incómodo para el trabajo que requiere inspección, manipulación o confirmación.

El texto es un medio poco adecuado para:

  • revisar un plan de despliegue con varias alertas de riesgo
  • filtrar una tabla de 200 resultados
  • aprobar una cláusula de un contrato y rechazar otra
  • comparar dos consultas candidatas lado a lado
  • ajustar un mapa, gráfico o línea de tiempo de forma interactiva
  • recorrer un asistente de configuración con opciones dependientes

El anuncio de MCP Apps de January 26, 2026 lo explica bien: el texto puede resumir el resultado de una herramienta, pero los usuarios a menudo quieren ordenar, filtrar, profundizar, previsualizar o aprobar sin convertir cada pequeña acción en otro prompt.

Ese es el cambio arquitectónico. La UI de agentes no consiste solo en «mejores burbujas de chat». Es un stack que permite pasar entre:

  • la intención expresada en una conversación
  • el estado estructurado
  • las superficies de trabajo visibles
  • la ejecución de herramientas
  • la aprobación humana

Un mejor modelo mental

Piensa en la UI de agentes como tres capas que colaboran:

  1. El contenedor de conversación capta la intención, transmite el progreso y mantiene al usuario orientado.
  2. La superficie de interacción estructurada permite al usuario inspeccionar, editar, comparar o aprobar algo sin reducirlo todo a prosa.
  3. La capa de herramientas y estado convierte esas interacciones en acciones reales en tus sistemas.

En otras palabras, el chat se convierte en el plano de control, mientras que las superficies estructuradas se convierten en el espacio de trabajo.

Chat-only flow

User -> Prompt -> Agent -> Text answer -> Another prompt -> Another text answer

Agent UI flow

User -> Chat intent
     -> Agent/tool reasoning
     -> Structured surface (table/form/chart/review card)
     -> User action
     -> Updated model context + tool call
     -> Confirmed result

Una vez que ves esa separación, ChatKit, A2UI y MCP Apps dejan de parecer competidores. Viven en distintas capas de un mismo stack emergente.

Capa 1: ChatKit como contenedor integrado del agente

OpenAI ChatKit es la opción más lista para usar de este stack. La documentación lo describe como un framework para construir experiencias de chat de alta calidad impulsadas por IA, con streaming, archivos adjuntos, gestión de hilos, anotaciones de fuentes y widgets interactivos enriquecidos integrados.

Esto importa porque la mayoría de los equipos no quiere inventar desde cero los elementos básicos de UX de un chat. Quieren:

  • un contenedor de conversación listo para producción
  • una forma rápida de integrar chat con agentes en su aplicación
  • soporte para herramientas, workflows y widgets
  • margen para personalizar la UI de modo que siga sintiéndose nativa de su producto

El detalle arquitectónico interesante es que ChatKit admite dos modos:

  • un backend alojado por OpenAI para avanzar lo más rápido posible
  • un backend autoalojado que usa el SDK de Python si quieres el máximo control sobre workflows, almacenamiento, inferencia y rutas de datos

Esto hace de ChatKit una opción pragmática cuando:

  • controlas el contenedor de la aplicación
  • quieres incorporar chat con agentes en tu producto ahora
  • te importa más la velocidad de entrega que la portabilidad entre hosts
  • tu patrón principal de interacción sigue siendo conversacional, con widgets como UI complementaria

Donde ChatKit destaca:

  • copilotos integrados en productos SaaS
  • paneles de soporte y operaciones
  • herramientas internas que necesitan archivos adjuntos, hilos y workflows guiados
  • productos donde el agente es una funcionalidad principal, no un plugin externo

Donde ChatKit es menos fuerte:

  • casos en los que la UI debe trasladarse entre hosts que no controlas
  • casos en los que agentes remotos necesitan describir una UI nativa sin integración mediante iframes
  • entornos de clientes muy heterogéneos donde la web, los dispositivos móviles y el escritorio deben renderizar de forma distinta el mismo payload estructurado

Por eso, lo mejor es entender ChatKit como la capa de contenedor: la forma más rápida de incorporar una experiencia de conversación con agentes pulida en tu propia aplicación.

Capa 2: A2UI como protocolo de UI declarativa

El December 15, 2025, Google presentó A2UI como un proyecto abierto para interfaces impulsadas por agentes.

El planteamiento del problema es importante. Si el agente vive dentro de tu aplicación, puede manipular la UI directamente. Pero en muchos sistemas reales, el agente que realiza el trabajo es remoto:

  • se ejecuta en segundo plano
  • vive en otro servidor
  • pertenece a una organización asociada
  • no debería tener permiso para ejecutar código de UI arbitrario en tu cliente

Google lo plantea de forma precisa: la industria necesitaba algo «seguro como los datos, pero expresivo como el código».

Eso es lo que A2UI intenta hacer.

En lugar de enviar HTML o JavaScript desde el agente, A2UI envía un payload estructurado que hace referencia a componentes de confianza de un catálogo del lado del cliente. El cliente conserva el control sobre los estilos, el renderizado y la seguridad. El agente describe la estructura y los datos.

Según la publicación de lanzamiento, A2UI se diseñó en torno a tres ideas:

  • La seguridad primero: el agente solo puede solicitar componentes previamente aprobados de un catálogo de confianza
  • Actualizaciones incrementales: el formato facilita el renderizado progresivo a medida que llega nuevo estado
  • Portabilidad entre frameworks: el mismo payload puede asociarse a React, web components, Flutter, SwiftUI u otra tecnología

Esto hace que A2UI resulte atractivo cuando:

  • tu agente y tu UI están separados por un límite de confianza
  • quieres que la misma respuesta del agente se renderice de forma nativa en múltiples plataformas cliente
  • te importa el renderizado declarativo en lugar de la ejecución remota de código
  • esperas que los agentes generen formularios, tarjetas, gráficos o flujos a medida sobre la marcha

También cambia cómo pensamos en la «UI agéntica». El agente ya no es quien renderiza. Se convierte en un planificador que emite una intención de UI estructurada.

Aquí hay un ejemplo simplificado de lo que esto permite:

{
  "component": "ApprovalCard",
  "props": {
    "title": "Rotate production API keys",
    "summary": "3 services will restart during the next maintenance window.",
    "riskLevel": "medium",
    "actions": [
      { "id": "approve", "label": "Approve" },
      { "id": "request_changes", "label": "Request changes" }
    ]
  }
}

El cliente decide cómo se ve realmente ApprovalCard. El agente decide que una tarjeta de aprobación es la superficie adecuada para ese momento.

Esa separación es poderosa.

También significa que A2UI no es una plataforma de aplicaciones completa por sí sola. Todavía necesitas:

  • un catálogo de componentes
  • bindings de renderizado
  • reglas de sincronización de estado
  • lógica de aprobación y permisos
  • una capa de transporte u orquestación alrededor

Por eso, es mejor entender A2UI como la capa de protocolo para UI generada por agentes, especialmente a través de límites de confianza.

Capa 3: MCP Apps y el runtime del host

Si ChatKit es el contenedor y A2UI es el protocolo declarativo, MCP Apps representa el patrón de runtime para ofrecer superficies interactivas dentro de hosts de agentes.

Aquí es donde el ecosistema se vuelve especialmente interesante.

La publicación oficial de MCP Apps describe herramientas que devuelven recursos de UI en lugar de texto plano. Una herramienta puede incluir _meta.ui.resourceUri; el host obtiene el recurso de UI, lo renderiza en un iframe aislado en un sandbox y habilita la comunicación bidireccional entre la UI y el host mediante JSON-RPC sobre postMessage.

Esto nos da una arquitectura de host concreta:

User
  -> Agent host (ChatGPT / Claude / VS Code / Goose)
  -> MCP tool call
  -> UI resource returned by server
  -> Sandboxed app surface rendered in-conversation
  -> User clicks / edits / filters
  -> App sends structured events back to host
  -> Host updates model context and optionally calls more tools

Lo importante no es solo que aparezca un widget. Es que el widget se convierte en un participante del ciclo del agente.

El anuncio de MCP Apps de January 26, 2026 destaca precisamente los tipos de superficies que se benefician de este modelo:

  • paneles
  • asistentes de configuración
  • revisión de documentos
  • monitoreo en tiempo real
  • otras interfaces con estado que resultan incómodas en un chat puro

La misma publicación también señala que el patrón se basa en el trabajo de MCP-UI y del OpenAI Apps SDK, y luego lo estandariza entre múltiples hosts.

Al January 26, 2026, OpenAI’s Help Center describes developer mode and MCP apps in ChatGPT as supporting interactive UI plus full MCP actions, with admin controls and RBAC on Business, Enterprise, and Edu plans.

Esta capa de host/runtime es la opción adecuada cuando:

  • quieres que tu experiencia se ejecute dentro de hosts de agentes externos
  • quieres que los resultados de herramientas se abran como superficies de aplicación interactivas
  • te importa más la distribución entre clientes que controlar todo el contenedor
  • tu producto se está convirtiendo en «algo que un agente puede usar con una persona en el ciclo»

Es menos ideal cuando:

  • necesitas un control exacto al píxel de toda la experiencia de usuario
  • tu producto necesita sentirse como un único contenedor de aplicación estrechamente integrado
  • no puedes depender del soporte del host ni de cómo evolucione el comportamiento del sandbox con el tiempo

Una opción predeterminada práctica para aplicaciones propias

Si estás desarrollando dentro de tu propia aplicación web este trimestre, la configuración práctica más clara suele ser:

  • ChatKit cuando quieres el contenedor pulido más rápido de implementar
  • AI SDK UI cuando quieres un renderizado flexible en el frontend y control preciso sobre superficies personalizadas
  • partes de herramienta tipadas cuando el modelo debe solicitar una interacción específica
  • partes de datos personalizadas cuando el servidor debe transmitir un artefacto estructurado a la conversación

Volví a revisar la documentación vigente de AI SDK UI el March 30, 2026 antes de escribir los ejemplos siguientes. Las ideas clave que confirma la documentación son:

  • useChat es la capa de estado y streaming del frontend
  • las partes de herramienta son la opción adecuada para acciones y aprobaciones
  • las partes de datos personalizadas son la opción adecuada para tarjetas, paneles de estado, artefactos y UI que se actualiza progresivamente
  • la UI generativa en AI SDK es, en esencia, «resultado de herramienta -> componente de UI tipado»

Esto te da un modelo mental muy práctico:

User message
  -> useChat()
  -> /api/chat
  -> streamText(...)
  -> tool parts and/or data parts
  -> React components render inside the conversation
  -> user clicks, confirms, edits, or filters
  -> next tool call or next model step

El beneficio es simple: dejas de tratar la respuesta del asistente como «una sola cadena» y empiezas a tratarla como un flujo de partes de interacción tipadas.

Astro: contenedor estático, isla de agente con streaming y MCP del lado del servidor

Astro es un entorno especialmente adecuado para este patrón porque sus opciones predeterminadas ya coinciden con la estructura de una buena UI de agentes:

  • la mayor parte de la página debe permanecer estática y ser rápida
  • solo la superficie del agente debe hidratarse
  • los secretos y el acceso a herramientas del lado del servidor deben permanecer fuera del cliente
  • las secciones dinámicas o personalizadas deben aislarse en lugar de convertir toda la aplicación en una SPA renderizada en el cliente

La documentación oficial refuerza esta arquitectura:

  • el modelo de islas de Astro indica que los componentes interactivos deben hidratarse de forma independiente con client:*
  • la documentación de endpoints de Astro indica que los endpoints del servidor pueden devolver un Response y, en modo static, deben excluirse del prerenderizado con export const prerender = false
  • la documentación de renderizado bajo demanda de Astro indica que las rutas renderizadas en el servidor requieren un adaptador y señala que output: 'server' hace que las rutas sean dinámicas de forma predeterminada
  • la documentación de islas de servidor de Astro muestra cómo diferir componentes lentos o personalizados renderizados en el servidor con server:defer

Esto conduce a una arquitectura de Astro muy práctica para funcionalidades de agentes:

Static Astro page
  -> React agent island (client:visible or client:load)
  -> Astro API route (/api/chat)
  -> AI SDK streamText(...) on the server
  -> MCP clients / DB / internal APIs on the server only
  -> typed tool parts and data parts back to the island

Patrón común de Astro 1: página estática + isla de chat con carga diferida

Mantén estática la página que rodea al panel e hidrata solo el panel del agente.

---
// src/pages/copilot.astro
import BaseLayout from "@/layouts/BaseLayout.astro";
import AgentPanel from "@/components/AgentPanel.tsx";
---

<BaseLayout
  title="Release Copilot"
  description="Review deploy plans, approvals, and rollout status."
>
  <section class="prose">
    <h1>Release Copilot</h1>
    <p>
      The docs, checklist, and rollout notes can stay static. Only the agent
      workspace needs to hydrate.
    </p>
  </section>

  <AgentPanel client:visible />
</BaseLayout>

Por qué esta es una buena opción predeterminada:

  • tu SEO y el primer renderizado siguen siendo excelentes
  • el código del agente se carga solo cuando hace falta
  • la UI del agente se siente como una isla enfocada en su función en lugar de apoderarse de toda la página

Si el asistente es la interacción principal de la página, usa client:load. Si es secundario, client:visible o client:idle suele ser una mejor alternativa.

Patrón común de Astro 2: ruta de API de Astro como gateway del agente

Este es el lugar más claro para integrar AI SDK, el contexto de autenticación, clientes MCP y APIs internas.

// src/pages/api/chat.ts
import type { APIRoute } from "astro";
import { openai } from "@ai-sdk/openai";
import { convertToModelMessages, streamText, type UIMessage } from "ai";

// Needed when the overall site is static or hybrid and this route must stay live.
export const prerender = false;

export const POST: APIRoute = async ({ request, locals, cookies }) => {
  const { messages }: { messages: UIMessage[] } = await request.json();

  // Read auth/session context here, not in the browser.
  const session = cookies.get("session")?.value;
  const tenantId = locals.tenantId;

  const result = streamText({
    model: openai("gpt-4.1-mini"),
    system: `You are a release copilot for tenant ${tenantId}. Never suggest actions outside the user's scope.`,
    messages: await convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse();
};

En la práctica, esta ruta se convierte en tu punto de control para:

  • autenticación y RBAC
  • claves de proveedores
  • conexiones MCP
  • consultas a la base de datos
  • registros de auditoría
  • preparación de la solicitud antes de que el modelo vea cualquier cosa

Si tu aplicación Astro usa output: 'server', normalmente no necesitas prerender = false en cada ruta. Si sigue siendo mayormente estática, agrega un adaptador y excluye del prerenderizado solo las rutas activas que necesitan streaming.

Patrón común de Astro 3: mantén MCP en el servidor, no en la isla

Si tu agente usa servidores MCP, la isla de React solo debería comunicarse con tu endpoint de Astro. Deja que el endpoint controle las conexiones MCP y la exposición de herramientas.

Browser island
  -> /api/chat
  -> server-side tool wrapper
  -> MCP server(s)
  -> tool result
  -> AI SDK UI message stream
  -> browser island

Este suele ser el patrón adecuado porque te da un único lugar para controlar:

  • a qué servidores MCP se puede acceder
  • qué herramientas están disponibles para cada rol
  • qué llamadas a herramientas necesitan aprobación
  • cuánto del resultado sin procesar de las herramientas llega al modelo o a la UI

También significa que puedes intercambiar:

  • un servidor MCP local
  • un servidor MCP remoto
  • una llamada directa a un servicio interno

sin reescribir la isla del frontend.

Patrón común de Astro 4: usa islas de servidor para paneles laterales personalizados

El chat en sí pertenece a una isla de cliente, pero la UI que lo rodea a menudo puede seguir renderizándose en el servidor.

Ejemplos:

  • ejecuciones recientes del agente
  • resúmenes de hilos específicos del usuario
  • cuotas asociadas a la cuenta
  • cantidad de elementos en la cola de aprobación
---
// src/pages/copilot.astro
import BaseLayout from "@/layouts/BaseLayout.astro";
import AgentPanel from "@/components/AgentPanel.tsx";
import AgentSidebar from "@/components/AgentSidebar.astro";
---

<BaseLayout title="Ops Copilot">
  <div class="grid gap-6 lg:grid-cols-[1fr_320px]">
    <AgentPanel client:load />

    <AgentSidebar server:defer>
      <div slot="fallback">Loading recent runs…</div>
    </AgentSidebar>
  </div>
</BaseLayout>

Esto mantiene aislado el trabajo de servidor costoso o personalizado en lugar de obligar a toda la página a seguir una ruta de ejecución más pesada.

Ejemplo de Astro + AI SDK: UI de isla que llama a una ruta de Astro

// src/components/AgentPanel.tsx
"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";

export default function AgentPanel() {
  const { messages, sendMessage, status } = useChat({
    transport: new DefaultChatTransport({
      api: "/api/chat",
    }),
  });

  return (
    <section className="rounded-2xl border p-4">
      <button
        onClick={() =>
          sendMessage({
            text: "Review release 2026.03.30-1 and tell me if we should deploy.",
          })
        }
      >
        Run review
      </button>

      <p>Status: {status}</p>

      {messages.map((message) => (
        <article key={message.id}>
          {message.parts.map((part, index) =>
            part.type === "text" ? <p key={index}>{part.text}</p> : null,
          )}
        </article>
      ))}
    </section>
  );
}

Este es el patrón de Astro de extremo a extremo más simple:

  • la página de Astro proporciona el contenedor
  • la isla de React proporciona la UI del agente
  • la ruta de API de Astro proporciona el backend con streaming
  • AI SDK proporciona el protocolo de mensajes
  • MCP y las herramientas sensibles permanecen en el servidor

La regla práctica específica de Astro

Si estás construyendo una UI de agentes en Astro, usa como opción predeterminada:

  1. primero una página estática
  2. una isla de cliente enfocada en la superficie del agente
  3. un endpoint de servidor para streaming
  4. MCP solo en el servidor
  5. islas de servidor solo para secciones cercanas personalizadas o dinámicas

Esto te da gran parte de los beneficios de una UI nativa para agentes sin renunciar a la mayor ventaja de Astro: enviar muy poco JavaScript fuera de la parte de la página que realmente lo necesita.

Ejemplo 1: transmitir una tarjeta de revisión al chat

Este es el patrón que elegiría cuando el asistente debe:

  • explicar algo en lenguaje natural
  • adjuntar un artefacto estructurado al hilo
  • permitir que el usuario inspeccione el estado visible antes de actuar

Piensa en planes de lanzamiento, resúmenes de revisión de contratos, líneas de tiempo de incidentes o listas de verificación de incorporación.

Backend: transmitir una parte de datos persistente junto con texto del asistente

// ai/types.ts
import { UIMessage } from "ai";

export type MyUIMessage = UIMessage<
  never,
  {
    "deploy-plan": {
      releaseId: string;
      risk: "low" | "medium" | "high";
      services: string[];
      status: "draft" | "approved";
    };
  }
>;

// app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import {
  convertToModelMessages,
  createUIMessageStream,
  createUIMessageStreamResponse,
  streamText,
} from "ai";
import type { MyUIMessage } from "@/ai/types";

export async function POST(req: Request) {
  const { messages }: { messages: MyUIMessage[] } = await req.json();

  const stream = createUIMessageStream<MyUIMessage>({
    async execute({ writer }) {
      writer.write({
        type: "data-deploy-plan",
        id: "release-2026-03-30-1",
        data: {
          releaseId: "2026.03.30-1",
          risk: "medium",
          services: ["api", "worker", "web"],
          status: "draft",
        },
      });

      const result = streamText({
        model: openai("gpt-4.1-mini"),
        system:
          "Explain the rollout in plain English and tell the user to inspect the deploy card before approving.",
        messages: await convertToModelMessages(messages),
      });

      writer.merge(result.toUIMessageStream());
    },
  });

  return createUIMessageStreamResponse({ stream });
}

Frontend: renderizar la tarjeta directamente desde message.parts

"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import type { MyUIMessage } from "@/ai/types";

export default function ReleaseCopilot() {
  const { messages, sendMessage } = useChat<MyUIMessage>({
    transport: new DefaultChatTransport({
      api: "/api/chat",
    }),
  });

  return (
    <>
      <button
        onClick={() =>
          sendMessage({ text: "Review release 2026.03.30-1 before we deploy." })
        }
      >
        Review release
      </button>

      {messages.map((message) => (
        <article key={message.id}>
          {message.parts.map((part, index) => {
            if (part.type === "text") {
              return <p key={index}>{part.text}</p>;
            }

            if (part.type === "data-deploy-plan") {
              return (
                <section key={part.id ?? index} className="rounded-xl border p-4">
                  <h3>Release {part.data.releaseId}</h3>
                  <p>Risk: {part.data.risk}</p>
                  <p>Services: {part.data.services.join(", ")}</p>
                  <p>Status: {part.data.status}</p>
                </section>
              );
            }

            return null;
          })}
        </article>
      ))}
    </>
  );
}

Por qué este patrón es útil:

  • la tarjeta es parte de la conversación, no un modal desconectado
  • el asistente puede narrar lo que importa mientras la UI conserva el estado exacto
  • la reconciliación de partes de datos permite actualizar la misma tarjeta con el tiempo en lugar de agregar texto que genera ruido

En esencia, esto se acerca mucho a la idea de A2UI: el modelo decide que debe existir un artefacto de «plan de despliegue», pero tu aplicación sigue controlando el renderizado real.

Ejemplo 2: pausar para recibir aprobación humana sin salir del hilo

Este es el patrón que usaría para acciones de alto impacto:

  • desplegar en producción
  • rotar credenciales
  • enviar un mensaje a un cliente
  • ejecutar una migración destructiva

El asistente no debería limitarse a decir «¿Debo continuar?» en texto plano. Debería presentar una interacción de aprobación explícita y luego continuar automáticamente cuando el resultado esté disponible.

Backend: exponer una herramienta de confirmación

// app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { convertToModelMessages, streamText, type UIMessage } from "ai";
import { z } from "zod";

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: openai("gpt-4.1"),
    system:
      "Before any production action, call askForConfirmation. Do not proceed until the user has decided.",
    messages: await convertToModelMessages(messages),
    tools: {
      askForConfirmation: {
        description: "Ask the user for confirmation.",
        inputSchema: z.object({
          message: z
            .string()
            .describe("The message to ask for confirmation."),
        }),
      },
    },
  });

  return result.toUIMessageStreamResponse();
}

Frontend: renderizar la herramienta de confirmación como un paso real en la UI

"use client";

import { useChat } from "@ai-sdk/react";
import {
  DefaultChatTransport,
  lastAssistantMessageIsCompleteWithToolCalls,
} from "ai";

export default function ApprovalChat() {
  const { messages, sendMessage, addToolOutput } = useChat({
    transport: new DefaultChatTransport({
      api: "/api/chat",
    }),
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
  });

  return (
    <>
      <button
        onClick={() =>
          sendMessage({ text: "Deploy release 2026.03.30-1 to production." })
        }
      >
        Start deploy
      </button>

      {messages.map((message) => (
        <article key={message.id}>
          {message.parts.map((part, index) => {
            if (part.type === "text") {
              return <p key={index}>{part.text}</p>;
            }

            if (part.type === "tool-askForConfirmation") {
              if (part.state === "input-available") {
                return (
                  <section
                    key={part.toolCallId}
                    className="rounded-xl border border-amber-300 bg-amber-50 p-4"
                  >
                    <p>{part.input.message}</p>
                    <div className="mt-3 flex gap-2">
                      <button
                        onClick={() =>
                          addToolOutput({
                            tool: "askForConfirmation",
                            toolCallId: part.toolCallId,
                            output: "Yes, approved.",
                          })
                        }
                      >
                        Approve
                      </button>

                      <button
                        onClick={() =>
                          addToolOutput({
                            tool: "askForConfirmation",
                            toolCallId: part.toolCallId,
                            output: "No, blocked.",
                          })
                        }
                      >
                        Block
                      </button>
                    </div>
                  </section>
                );
              }

              if (part.state === "output-available") {
                return <p key={part.toolCallId}>Decision: {part.output}</p>;
              }
            }

            return null;
          })}
        </article>
      ))}
    </>
  );
}

Por qué este patrón es útil:

  • la aprobación es visible y auditable
  • el usuario actúa sobre una superficie delimitada, no sobre un párrafo ambiguo
  • sendAutomaticallyWhen permite que la conversación continúe una vez que existan todos los resultados de las herramientas
  • el ciclo de tu agente ahora tiene un punto de control humano explícito en lugar de una frase vaga como «por favor confirma»

Esto es mucho mejor que obligar al usuario a escribir «sí, aprueba el despliegue canario para la versión 2026.03.30-1» en el chat.

Cómo encajan estas piezas

Esta es la forma más clara que he encontrado de entender el stack:

  • ChatKit es un framework de producto para conversaciones integradas con agentes
  • A2UI es un lenguaje declarativo para UI especificada por agentes
  • MCP Apps es un patrón de runtime estándar para superficies interactivas dentro de clientes host

O, dicho más directamente:

  • ChatKit te ayuda a construir una aplicación nativa para agentes
  • A2UI ayuda a los agentes remotos a describir UI nativa de forma segura
  • MCP Apps ayuda a las herramientas a ofrecer UI interactiva dentro de hosts de agentes compartidos

Estos enfoques pueden superponerse.

Un producto real podría:

  • usar ChatKit dentro de su propia aplicación SaaS
  • permitir que subagentes remotos emitan descripciones estructuradas de UI al estilo de A2UI
  • exponer determinados workflows como MCP Apps para usuarios que prefieren ChatGPT o Claude como su host principal

Por eso la «UI de agentes» se está convirtiendo en un stack propio. La antigua división del frontend entre «API + aplicación web» ya no es suficiente. Ahora necesitamos diseñar la relación entre:

  • el modelo
  • el host
  • la superficie estructurada
  • el sistema de herramientas
  • el punto de aprobación humana

Reglas de diseño para superficies de interacción estructuradas

Si estás construyendo este tipo de sistema, algunas reglas ya parecen tener vigencia a largo plazo.

1. Usa el chat para expresar intenciones, no para cada microacción

El chat es excelente para:

  • preguntar
  • aclarar
  • narrar
  • resumir

Es malo para:

  • comparaciones densas
  • filtrado repetido
  • controles de aprobación
  • edición de múltiples campos

Pasa a una superficie en cuanto el usuario necesite manipular el estado en lugar de hablar sobre él.

2. Prefiere el estado visible al estado implícito

Si el agente propone un cambio, el usuario debe ver:

  • qué va a suceder
  • en qué datos se basa la decisión
  • qué está aprobando
  • cuál es el camino para revertir el cambio

Por eso importan las tarjetas, tablas, diffs y formularios. Hacen explícito el razonamiento del agente de una forma que se puede inspeccionar.

3. Mantén el renderizado bajo control del host y con capacidades limitadas

Aquí es donde A2UI y MCP Apps son especialmente valiosos. El agente no debería poder ejecutar código de UI arbitrario solo porque quiere una interacción más rica. Tu cliente o host debe decidir:

  • qué componentes son de confianza
  • qué recursos pueden renderizarse
  • qué acciones requieren confirmación
  • qué llamadas a herramientas se permiten desde la superficie

4. Trata las acciones del usuario como eventos estructurados

«El usuario hizo clic en aprobar» es mejor que una respuesta posterior vaga en lenguaje natural.

Cuanto más importante sea la acción, más debería parecerse a un evento con campos explícitos, capacidad de auditoría y verificaciones de permisos.

5. Diseña alternativas de respaldo

No todos los hosts admitirán todas las superficies para siempre. Construye tus flujos para que se adapten de forma gradual:

  • widget si está disponible
  • explicación en texto plano si no lo está
  • aprobación con un clic si está disponible
  • solicitud de confirmación explícita si no lo está

6. Separa la narración de la ejecución

Uno de los patrones más claros de UX para agentes es:

  • el chat explica lo que está sucediendo
  • la superficie captura la decisión
  • la herramienta realiza la acción

Cuando todo eso se mezcla, los usuarios pierden la confianza rápidamente.

Flujos de arquitectura en ASCII

1. Patrón de aplicación integrada

Your SaaS app
  -> ChatKit shell
  -> Agent runtime
  -> Internal tools / APIs
  -> Widget inside chat
  -> User approves or edits
  -> Tool executes action

2. Patrón de agente remoto

User app
  -> Host/orchestrator
  -> Remote agent
  -> A2UI payload
  -> Native client renderer
  -> Structured user event
  -> Agent/tool follow-up

3. Patrón de aplicación entre hosts

User in ChatGPT / Claude / VS Code
  -> Host calls MCP server
  -> Tool returns UI resource
  -> Host renders sandboxed app
  -> User interacts in conversation
  -> Host updates model context
  -> More tools run if needed

Construir o adoptar: una guía práctica para decidir

Elige ChatKit cuando:

  • controlas toda la experiencia del producto
  • quieres la ruta más rápida hacia una UI pulida de chat con agentes
  • la conversación es el centro de la UX
  • los widgets complementan la conversación en lugar de reemplazarla

Elige A2UI cuando:

  • tu agente es remoto u opera entre organizaciones
  • el renderizado nativo importa más que la ejecución compartida en iframes
  • la seguridad y la portabilidad son más importantes que la libertad para escribir código de UI
  • múltiples plataformas cliente necesitan interpretar la misma respuesta estructurada

Elige MCP Apps / Apps SDK cuando:

  • tus usuarios trabajan cada vez más dentro de hosts de agentes
  • quieres que los resultados de herramientas se conviertan en superficies interactivas
  • quieres distribuir en múltiples hosts
  • tu workflow se beneficia de aplicaciones dentro de la conversación, no solo de respuestas de texto

Elige un enfoque híbrido cuando:

  • tienes tu propio contenedor de producto y también quieres distribuirlo en hosts externos
  • algunos workflows son conversacionales mientras que otros claramente necesitan superficies de revisión estructuradas
  • tu roadmap incluye tanto copilotos integrados como integraciones para agentes

El verdadero cambio

La tendencia importante no es que «el chat está reemplazando la UI». El cambio más profundo es que los agentes están adquiriendo la capacidad de interactuar directamente con partes de la UI.

Esto significa que ahora diseñamos interfaces para tres participantes a la vez:

  • el usuario humano
  • el agente
  • el runtime del host que media la confianza, los permisos y el renderizado

Los equipos que hagan esto bien dejarán de tratar la UX de agentes como una burbuja de chat flotante. Construirán sistemas donde la conversación, las superficies estructuradas y la ejecución de herramientas se refuercen mutuamente.

Eso es lo que convierte a la UI de agentes en un stack propio.

Fuentes