Qué son las client-side functions de A2UI

Las client-side functions son el mecanismo de A2UI para que el agente delegue un cálculo concreto en código Dart que corre en el dispositivo, en vez de resolverlo el propio modelo. A2UI es el protocolo JSON con el que un agente genera interfaz Flutter en tiempo de ejecución, a través del paquete genui; sobre esa base, registras una función en el catálogo de GenUI (nombre, descripción, esquema de argumentos y tipo de retorno), el LLM la invoca dentro del payload como quien llama a una herramienta, y el resultado se evalúa de forma síncrona en el cliente y se pinta al instante. Para quien ya tiene una app en producción la traducción es directa: el precio de un carrito, el IVA, un formato de moneda o una conversión de unidades dejan de pasar por el modelo, con lo que eso supone en latencia, coste de tokens y consistencia.

Google lo publicó el 28 de agosto de 2026 como parte del paquete genui. Si ya sigues A2UI, esto no cambia el protocolo: añade una pieza que faltaba para llevarlo a producción sin que el modelo cargue con aritmética que nunca debería tocar.

De dónde viene el problema

En una GenUI el modelo decide qué se pinta. El riesgo aparece cuando también le dejas decidir cuánto vale algo. Un LLM que multiplica precio por cantidad falla de formas silenciosas: redondea distinto entre respuestas, se inventa un separador de miles, arrastra un céntimo. No es que el modelo sea malo con los números; es que cada cálculo consume tokens, añade latencia y produce un resultado que no puedes auditar contra tu backend.

Las client-side functions sacan ese trabajo del modelo. El agente sigue orquestando la conversación y la interfaz, pero cuando necesita un número exacto, llama a una función tuya. Tu Dart hace la cuenta, siempre igual, y devuelve el valor ya formateado.

Cómo funciona, en tres pasos

Declaración. Registras la función en el Catalog de GenUI. Ahí defines el nombre con el que el agente la referencia, una descripción que le dice cuándo usarla, el esquema de argumentos y el tipo de retorno. El PromptBuilder extrae esas declaraciones y las mete en el system prompt que ve el modelo, así que el agente sabe qué herramientas locales tiene sin que las cablees a mano.

Invocación. El modelo genera un payload A2UI con la llamada y sus argumentos, igual que generaría un widget.

Ejecución. El cliente Flutter evalúa la función de forma síncrona y renderiza el resultado. Sin ida y vuelta a la red, sin esperar al siguiente turno del modelo.

Una función se define extendiendo SynchronousClientFunction:

class CalculateCostFunction extends SynchronousClientFunction {
  @override
  String get name => 'calculate_cost';

  @override
  String get description =>
      'Calcula el coste de un ingrediente a partir de su id y la cantidad.';

  @override
  ClientFunctionReturnType get returnType => ClientFunctionReturnType.string;

  // El esquema de argumentos se construye con json_schema_builder.
  @override
  JsonSchema get argumentSchema => /* ingredient_id: string, quantity: number */;

  @override
  String executeSync(Map<String, Object?> args) {
    final id = args['ingredient_id'] as String;
    final qty = args['quantity'] as num;
    final price = _priceTable[id] ?? 0;
    return '\$${(price * qty).toStringAsFixed(2)}';
  }
}

Y se registra pasándola al catálogo:

final catalog = Catalog(
  widgets: [/* ... */],
  functions: [CalculateCostFunction()],
);

El ejemplo sale de Commis, la app de cocina que Google usa como muestra: CalculateCostFunction recibe ingredient_id y quantity y devuelve una cadena de moneda como $2.97. La app de demos está en el repo flutter/demos, si quieres verla entera.

El cálculo del ejemplo es trivial; lo que importa es la frontera que dibuja. Todo lo determinista queda de este lado, en Dart, testeable con un test unitario normal. El modelo se queda con lo que sabe hacer: entender la intención y componer la interfaz.

Qué ganas

Lo primero es coste. Cada operación aritmética que mueves al cliente es un puñado de tokens que dejas de pagar en cada turno. En una conversación con muchos cálculos pequeños, eso se nota en la factura mensual, no en el playground. Si estás peleando con el gasto de tokens en producción, esto va en la misma dirección que el resto de lo que se hace en FinOps de IA.

La latencia va detrás. El cálculo no espera a la red ni al siguiente token generado: se resuelve en el hilo de Dart y se pinta. Para una UI que reacciona a lo que el usuario toca, esa diferencia se percibe.

Y queda el determinismo, que es el que más se agradece a medio plazo. La misma entrada da la misma salida y la puedes cubrir con tests. Un cálculo que hace el modelo no lo pruebas: lo evalúas, que es otra disciplina y otro coste, como contamos en evals de agentes en producción. Con una client-side function vuelves al terreno del test unitario de siempre.

Cuándo NO usarlas

Aquí conviene frenar, porque el mecanismo tiene bordes afilados.

Son síncronas. executeSync es eso: sin await, sin red, sin leer de disco, sin nada que bloquee. Si tu "cálculo" necesita consultar tu API, no es una client-side function; es una tool de servidor. Forzarlo aquí te deja con un hilo bloqueado o con una función que miente sobre lo que puede hacer.

Corren en el dispositivo del usuario. El código y sus datos están al alcance de quien tenga el terminal. No pongas ahí una tabla de precios que sea secreto comercial, ni lógica de negocio que no quieras que se lea, ni nada que dependa de un permiso que se comprueba en cliente. Si el valor tiene que ser confiable de verdad (un total que se cobra, un descuento que se aplica), el servidor manda, y la función local es como mucho una previsualización.

El agente no debe fiarse a ciegas. Una client-side function es una entrada más al modelo, y todo lo que entra al modelo es dato, no verdad revelada. Si el resultado alimenta una decisión sensible, valídalo donde tengas autoridad para hacerlo.

La regla práctica: úsalas para cálculo determinista, local y sin efectos, como formateo, aritmética, conversiones o comprobaciones sobre datos que ya tienes en el cliente. Para cualquier cosa que toque red, secretos o dinero de verdad, la frontera correcta sigue siendo el backend.

Qué haríamos nosotros

Si ya tienes una GenUI con A2UI en marcha, adoptar client-side functions es de las decisiones fáciles: mueves el cálculo determinista al cliente y recuperas latencia, coste y tests sin tocar el protocolo. Escribir la función es lo de menos. El trabajo real está en dibujar la línea entre lo que puede vivir en el dispositivo y lo que no, que es la misma que ya deberías tener entre cliente y servidor. La novedad es que ahora el agente también la respeta.

Para quién sí: equipos que ya están en A2UI y tienen cálculos repetitivos y baratos que el modelo está resolviendo por inercia. Para quién no, todavía: quien está evaluando si adoptar GenUI. Esta feature no es motivo para dar el salto, es un refinamiento una vez dentro. Si estás en ese punto, primero conviene entender el protocolo y cuándo compensa, y eso lo desarrollamos en GenUI, A2UI y la interfaz que decide la IA.

En Dribba llevamos con Flutter desde 2011 y somos Flutter Partner oficial de Google. Si estás montando una interfaz generativa con agentes y quieres decidir con criterio qué corre en el modelo y qué en el dispositivo, hablamos.