Tema
SDK — quickstart
@nykk/ur-agent-client es un SDK headless, sin dependencias de runtime: no renderiza nada. Habla por SSE con el backend y te entrega los eventos; la UI la pones tú, en el framework que quieras o en ninguno.
Es la misma pieza que usa por debajo el widget. Si el widget no encaja con tu stack —necesitas tu propio diseño, o integrarlo en una pantalla que ya existe— esto es lo que habla con el backend.
1. Instala el paquete
bash
npm install @nykk/ur-agent-clientCero dependencias de runtime — usa solo fetch, crypto.randomUUID, localStorage y AbortController, todas ya disponibles en cualquier navegador moderno.
Una salvedad de entorno, no de navegador: crypto.randomUUID solo existe en contexto seguro (https, o localhost/127.0.0.1). Si pruebas contra un staging por http:// con IP o nombre de host, el constructor falla con crypto.randomUUID is not a function. Sirve cualquiera de las dos salidas: pon TLS delante, o entra por localhost.
Es un paquete público en npmjs: no necesitas .npmrc ni token para instalarlo. Si tu proyecto ya mapea el scope @nykk a un registro privado, lo recibirás de ahí igualmente; es el mismo paquete.
2. Necesitas identificar al agente
En el panel, abre el agente que quieras exponer y ve a la sección Canales. La fila «Integración a medida con tu propia interfaz» (@nykk/ur-agent-client) es la del SDK — internamente el canal se llama js_sdk, que es el nombre que verás en la URL y en los filtros de conversaciones. Comprueba que su interruptor, a la izquierda de la fila, sigue encendido: lo está por defecto al crear el agente, salvo que alguien lo haya apagado (si lo enciendes, guarda el agente). Luego ábrela. Ver Canales.
Esa misma pantalla trae, ya rellenos con los valores de tu agente, un bloque de configuración y un snippet completo con botón de copiar: es la vía recomendada para no teclear nada a mano.
3. El snippet canónico
Es el mismo esqueleto que genera el panel, allí ya relleno con el id y la URL de tu instalación (y, si el agente tiene tools que acepten contexto de cliente, con el bloque context incluido):
ts
import { UrAgentClient } from '@nykk/ur-agent-client';
const client = new UrAgentClient({
agent: '<id-del-agente>',
apiBase: '<origen-de-tu-backend-ur>',
});
client.onToken((delta) => {
/* añade `delta` al mensaje del bot que estás construyendo */
});
client.onNotice((text) => {
/* el mensaje automático del agente (una bienvenida, un aviso legal): su primer mensaje */
});
client.onConversationEnd((text) => {
/* se acabaron los turnos: muestra `text` y quita o deshabilita la caja de texto */
});
client.onDone((threadId) => {
/* el turno ha terminado */
});
client.onError((message) => {
/* muéstralo como error */
});
await client.sendMessage('Hola');Usa el id, no el identifier
Las dos referencias valen, pero el identifier es un slug derivado del nombre: si alguien renombra el agente en el panel, el slug cambia y tu integración empieza a recibir 404. El id (UUID) no cambia nunca, y es el que trae ya puesto el snippet del panel. Cambiar de una referencia a la otra más adelante tampoco es gratis: la conversación guardada en el navegador va asociada a la referencia que usaste, así que todos los visitantes empezarían de cero.
Esto es prácticamente todo lo que el widget oficial hace por debajo: el resto de su código es shadow DOM y renderizado.
Título y colores
Van en tu código: tu UI es tuya, y el canal Integración a medida no tiene Apariencia en el panel (esa es del widget). Si quieres poder cambiarlos sin desplegar, sácalos a tu propia configuración, como hace el widget con sus data-color*.
client.getConfig() sigue sirviendo para los límites de conversación del agente (maxTurns, conversationTtlHours; ver abajo). En js_sdk trae style vacío y sin title.
4. Restaurar una conversación al recargar
ts
const config = await client.getConfig().catch(() => null);
// Si el agente olvida las conversaciones inactivas, esto estrena una nueva cuando toca.
const expired = client.expireIfIdle(config?.conversationTtlHours);
if (!client.isNewSubject && !expired) {
const history = await client.loadHistory();
// history.messages: { role: 'user' | 'bot', content, time, isAutoMessage?, isConversationEnd? }[]
// history.conversationEnded: true si ya se agotaron los turnos → deja la caja cerrada
}isNewSubject es true solo la primera vez que este navegador habla con este agente (la identidad de conversación se persiste en localStorage) — no hay nada que restaurar en ese caso.
5. Que el agente hable primero
Si el agente tiene mensaje automático, puede abrir él la conversación: llama a start() cuando tu UI enseñe el chat, después de restaurar, y solo si no había nada que restaurar:
ts
// al abrir el panel del chat (o al montarlo, si siempre está a la vista)
if (history.messages.length === 0) {
await client.start(); // el mensaje llega por onNotice, antes de que el visitante escriba
}- Sobre una conversación que ya existe, o con un agente sin mensaje automático, no hace nada.
- No lo llames al cargar la página si el chat va detrás de un botón: abriría una conversación por cada visita que nunca lo mira.
- Nunca falla hacia fuera (ni lanza ni dispara
onError). Si no llega a hacerse, el mensaje sale igualmente con la primera respuesta, como hacen los canales que solo responden. - Un
sendMessage()enviado mientras está en vuelo lo espera, para que la bienvenida quede encima.
Las conversaciones que se abren así y en las que nadie escribe no cuentan en el panel: ni en el dashboard ni en el listado de conversaciones (salvo que marques «Incluir chats sin mensajes»).
En el idioma del visitante
Si el agente tiene el mensaje en varios idiomas, pásale al cliente los del visitante con locale:
ts
const client = new UrAgentClient({
agent: '<id-del-agente>',
apiBase: '<origen-de-tu-backend-ur>',
locale: navigator.languages, // o el idioma que el visitante eligió en tu web: 'eu'
});El servidor elige la versión: la primera de esos idiomas que tenga el agente (eu-ES también vale para eu) y, si no hay ninguna, el texto por defecto. Sin locale, mira la cabecera Accept-Language del navegador, así que una integración que no lo pasa también recibe su idioma. locale llegó después de la 0.2.9 del SDK: si tus tipos no lo tienen, sube @nykk/ur-agent-client.
6. Empezar una conversación nueva
ts
client.resetConversation(); // aborta cualquier envío en curso y crea una conversación nueva
await client.start(); // si el chat sigue a la vista: la nueva también abre con la bienvenidaSi el agente tiene límite de turnos (config.maxTurns), no ofrezcas este botón: una conversación nueva empieza la cuenta de cero.
7. Si no quieres gestionar tú el acumulado de mensajes
Todo lo anterior (burbuja del usuario, ir acumulando los onToken en la burbuja del bot en curso, etc.) es exactamente el trabajo que hace createChatStore. Ver la referencia y los dos ejemplos completos en Ejemplos.
Fin de conversación
Si el agente tiene un máximo de turnos por conversación, la respuesta al último llega como siempre y, detrás, onConversationEnd(texto) con el mensaje Límite de conversación del catálogo (antes de onDone). A partir de ahí:
- Quita o deshabilita la caja de texto. El backend descarta cualquier mensaje más en esa conversación, sin llamar al modelo, y contesta otra vez solo con
onConversationEnd. - Si después de ese aviso llegara texto en la misma respuesta, el cliente lo descarta y no llama a
onToken. - Al recargar,
loadHistory()devuelveconversationEnded: truey el mismo texto como último mensaje, marcadoisConversationEnd: true.
Con createChatStore no tienes que hacer nada de esto a mano: el estado trae conversationEnded, el mensaje de cierre viene marcado isConversationEnd (dale el estilo que quieras) y send() deja de hacer nada.
ts
const state = store.getState();
textarea.disabled = state.conversationEnded;
resetButton.hidden = !!config?.maxTurns;Qué cuenta como turno y qué pasa al cambiar el límite: Seguridad y límites.
Lo que sigue
- La superficie completa → SDK — referencia.
- Pasarle datos de tu app a las tools → Contexto de cliente.
- Que el agente sepa quién es el usuario de tu aplicación (
getUserToken) → Usuarios identificados. - Límites, filtros y el modelo de confianza del canal → Seguridad y límites.
- Delegar la integración en un agente de IA → Integrar con un agente de IA.