Skip to content

SDK — referencia ​

Superficie pública completa de @nykk/ur-agent-client. No hay nada más: no existe connect() ni onMessage. La única credencial es el token del usuario de getUserToken, para un agente que identifica a su usuario.

UrAgentClient ​

ts
new UrAgentClient(options: UrAgentClientOptions)

UrAgentClientOptions ​

CampoTipoDescripción
agentstringEl id (UUID) del agente — es el que trae el snippet del panel. También se acepta su identifier, pero es un slug derivado del nombre y cambia al renombrar el agente.
apiBase?stringOrigen del backend ur. Por defecto, window.location.origin; fuera del navegador (SSR, tests) no hay window y queda vacío, así que ahí pásalo siempre explícito.
channel?string'js_sdk' (por defecto) o 'widget'. Se comprueba contra el estado y los orígenes de ese canal, y elige qué devuelve getConfig() (solo widget trae apariencia). Una UI propia, aunque sea un widget a medida, va por js_sdk. Cualquier otro valor lo rechaza el backend con 400.
subjectId?stringReanuda un hilo concreto en vez del persistido por defecto. Nunca se escribe en storage. Hasta 256 caracteres. Quien conozca un subjectId puede leer ese hilo y escribir en él, porque el canal público no pide nada más: que no se pueda adivinar (un UUID aleatorio), nunca el id o el email de tu usuario. Con un agente que identifica a su usuario, la conversación es además de ese usuario, y otro con el mismo subjectId cae en otra.
storage?UrAgentClientStoragePersistencia por agente de tres claves: el subjectId por defecto (ur-widget-subject-<agente>), la hora del último mensaje enviado con él (ur-widget-activity-<agente>, la que mira expireIfIdle()) y el identificador anti-abuso que el SDK manda como X-Widget-Fingerprint (ur-widget-fingerprint-<agente>), con el que el backend cuenta los límites de mensajes por hora aunque el visitante empiece conversación nueva. Por defecto, localStorage. Pasa tu propia implementación (getItem/setItem) en entornos sin localStorage (SSR, ciertos WebViews); si no persiste de verdad entre cargas (un Map en memoria, por ejemplo), cada recarga estrena conversación y reinicia ese contador.
context?Record<string, unknown>Datos por turno para las tools del agente, agrupados por el nombre de la instancia de tool que los consume (no por el de la plantilla). Ver Contexto de cliente.
locale?string | readonly string[]Los idiomas del visitante, el preferido primero, como etiquetas BCP 47 ('eu', 'es-ES'): normalmente navigator.languages, o el selector de idioma de tu web. Va en start() y en sendMessage() (nunca en getConfig() ni en loadHistory()) para que el mensaje automático llegue en ese idioma si el agente lo tiene: cada etiqueta se prueba tal cual y luego por su idioma base (eu-ES → eu). Sin coincidencia, o sin la opción, el servidor mira la cabecera Accept-Language del navegador y, si tampoco, usa el texto por defecto del agente. Lo que no es una etiqueta se ignora, sin error. Llegó después de la 0.2.9: si los tipos del SDK instalado no tienen locale, súbelo.
getUserToken?() => string | null | undefined | Promise<string | null | undefined>El token del usuario de tu aplicación, para un agente que lo identifica: un JWT que firma tu backend, nunca el navegador. Se llama antes de cada petición a /chat, /history y /start (nunca en /config), y lo que devuelve, sin espacios en los extremos, va como Authorization: Bearer <token>. Vacío, null o undefined: la petición va anónima, que vale si el agente tiene el usuario como opcional y da 401 si lo exige. Ante un 401 token_expired se vuelve a llamar y la petición se repite una vez, si el token nuevo es distinto y no vacío. Se llama en cada petición: si firmar te cuesta, guarda el token en caché en tu lado. Cambiar de token (iniciar o cerrar sesión) es cambiar de conversación.
getAuthHeaders?() => Record<string,string> | Promise<Record<string,string>>Obsoleta: usa getUserToken. Cabeceras extra que se resuelven una vez por petición y se fusionan en /chat, /history y /start. El backend ya no lee X-Widget-Auth. Un Authorization devuelto aquí sí llega y se verifica como token del usuario, pero sin el reintento de getUserToken; y si getUserToken da un token, el suyo sustituye a este.

Propiedades ​

  • subjectId: string (getter) — la identidad de conversación en uso.
  • isNewSubject: boolean (readonly) — true si el subjectId se acaba de generar (no hay nada que restaurar vía loadHistory()).

Métodos ​

  • onToken(handler: (delta: string) => void): () => void — delta de texto del turno en curso. Devuelve una función para des-registrar el handler.

  • onNotice(handler: (content: string) => void): () => void — el mensaje automático único que un agente puede tener configurado (una bienvenida, un aviso legal): el primer mensaje de la conversación. Dispara como máximo una vez por (agente, subjectId, channel): desde start(), antes de que el visitante escriba, o —si no se llamó o no llegó a hacerse— con el primer sendMessage(), antes de su primer onToken. Si no te suscribes, el texto no se pierde: loadHistory() lo devuelve igual, marcado con isAutoMessage: true.

  • onToolEvent(handler: (event: UrAgentClientToolEvent) => void): () => void — actividad de tools del agente. Solo se emite en el chat de pruebas del panel; en una integración real (widget/js_sdk) nunca llega, así que no lo trates como parte del contrato.

  • onUsage(handler: (usage: UrAgentClientUsage) => void): () => void — tokens/coste del turno. Igual que onToolEvent, solo en el chat de pruebas del panel.

  • onConversationEnd(handler: (content: string) => void): () => void — la conversación ha agotado el máximo de turnos del agente. Recibe el mensaje Límite de conversación del catálogo, para pintarlo después de la respuesta. Llega antes que onDone. Desde ahí, quita o deshabilita la caja de texto: el backend descarta todo lo que se envíe en esa conversación y vuelve a contestar solo con este evento (sin onToken delante). Si en la misma respuesta llegara texto después, se descarta sin llamar a onToken. Al recargar, loadHistory() lo refleja con conversationEnded: true.

  • onDone(handler: (threadId: string) => void): () => void — el turno ha terminado.

  • onError(handler: (message: string, info?: UrAgentClientErrorInfo) => void): () => void — fallo de red o del backend. Dos orígenes distintos, y el texto no es intercambiable:

    • Si el turno ya había arrancado y falla en el backend, llega un mensaje de catálogo (nunca la excepción real: es un canal público).
    • Si la petición no llega a abrir el stream, el mensaje lo genera el propio SDK y es técnico y en inglés: Chat request failed (<status>) para una respuesta HTTP no ok (404 canal deshabilitado o agente inexistente, 403 origen no permitido, 429 rate limit, 400 un subjectId de más de 256 caracteres, 413 una petición de más de 1 MB, 401 el token del usuario identificado no pasa), o el mensaje del error de red (Failed to fetch…).

    Trátalo como un código de diagnóstico, no como un texto de cara al usuario: registra message y pinta tu propia copia. info solo llega con una respuesta HTTP de error: su status y, en un 401, code (token_missing, token_invalid o token_expired), la señal para pedirle al usuario que vuelva a iniciar sesión.

  • setContext(context: Record<string, unknown> | undefined): void — sustituye el contexto de cliente. Surte efecto en el siguiente sendMessage().

  • abort(): void — cancela un sendMessage en curso sin disparar onError (tampoco onDone: no se emite ningún evento).

    abort() + createChatStore

    Como no llega ni onDone ni onError, un store de createChatStore se queda en status: 'sending' y sus send() posteriores son no-op. Para desbloquearlo hay que llamar a reset(), que empieza conversación nueva y limpia los mensajes. Si necesitas un botón de «parar» que conserve el hilo, usa el cliente directamente y acumula tú los mensajes.

  • start(): Promise<void> — tu UI está enseñando el chat: en una conversación que aún no existe, el backend la abre con el mensaje automático del agente y este llega por onNotice, antes de que el visitante escriba. Llámalo al abrir el chat (al montarlo, si siempre está a la vista), después de loadHistory() y solo si no había historial; y otra vez tras resetConversation() si el chat sigue a la vista. No lo llames al cargar la página si el chat va detrás de un botón: abriría una conversación por visita. Sobre una conversación que ya existe, o con un agente sin mensaje automático, no hace nada. Nunca lanza ni dispara onError: si falla, el mensaje llega con el primer sendMessage(). Dos llamadas a la vez comparten petición, y un sendMessage() enviado mientras está en vuelo lo espera. El mensaje llega en el idioma del visitante si el agente lo tiene (la opción locale, si no Accept-Language), y la conversación guarda ese texto: loadHistory() devuelve el mismo.

  • resetConversation(): string — aborta cualquier envío en curso, genera y persiste un subjectId nuevo (hilo nuevo de verdad en el backend), y lo devuelve. Si el agente tiene límite de turnos (getConfig().maxTurns), no lo ofrezcas como botón: una conversación nueva empieza la cuenta de cero.

  • expireIfIdle(hours: number | null | undefined): boolean — si no se ha enviado ningún mensaje en las últimas hours horas, olvida la conversación (resetConversation()) y devuelve true: no hay historial que cargar. Pásale getConfig().conversationTtlHours antes de loadHistory(). No hace nada sin plazo, con un subjectId explícito, sin storage o si aún no consta ningún envío (una conversación sin mensajes, o anterior a esta versión del SDK: cuenta desde el siguiente).

  • getConfig(): Promise<UrAgentClientConfig> — los límites de conversación del agente (maxTurns, conversationTtlHours), si identifica a su usuario (authMode) y, solo en el canal widget, el título y los colores de su Apariencia; en js_sdk, style llega vacío y sin title. Va siempre sin el token de getUserToken. Lanza si la respuesta no es ok (Config request failed (<status>), 404 si el canal está apagado). Si falla, sigue sin límites.

  • loadHistory(): Promise<UrAgentClientHistory> — turnos previos de este subjectId, si hay. threadId: null y messages: [] si no hay nada que restaurar (no es un error). A diferencia de sendMessage, este sí lanza si la petición falla: un UrAgentClientHttpError con status y, en un 401, code.

  • sendMessage(text: string): Promise<void> — envía el mensaje y procesa el streaming invocando los handlers registrados arriba. Resuelve cuando el stream termina; no hace falta esperar nada más para saber que el turno acabó — usa onDone/onError para eso. Nunca lanza: los fallos van a onError.

La política de reintentos la pones tú

sendMessage() y loadHistory() hacen un intento por llamada, con una excepción: si el servidor responde que el token del usuario ha caducado (401 token_expired), el SDK le pide otro a getUserToken y repite esa petición una vez, solo si el token nuevo es distinto y no vacío. Fuera de eso no reintenta por su cuenta, a propósito: cuántas veces, con qué espera y ante qué errores depende de tu aplicación, y un reintento automático de un mensaje de chat rara vez es lo que quiere el usuario. Envuélvelos en el nivel de tu framework si lo necesitas.

Tipos ​

ts
interface UrAgentClientStorage {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
}

interface UrAgentClientTokenUsage {
  inputTokens: number;
  outputTokens: number;
}

interface UrAgentClientUsageCost {
  inputCost: number;
  outputCost: number;
  totalCost: number;
}

interface UrAgentClientUsage extends UrAgentClientTokenUsage {
  cost: UrAgentClientUsageCost | null;   // null si el modelo no tiene precios cargados
}

interface UrAgentClientHistoryMessage {
  role: 'user' | 'bot';
  content: string;
  time: string | null;
  usage?: UrAgentClientTokenUsage;                             // solo chat de pruebas del panel
  toolCalls?: unknown;                                         // solo chat de pruebas del panel
  isAutoMessage?: boolean;
  isSummaryMarker?: boolean;   // resumen interno del hilo: no lo pintes
  isSummarized?: boolean;      // mensaje ya cubierto por un resumen anterior
  isConversationEnd?: boolean; // el mensaje de cierre: siempre el último, con conversationEnded
}

interface UrAgentClientHistory {
  threadId: string | null;
  messages: UrAgentClientHistoryMessage[];
  conversationEnded?: boolean;   // se agotaron los turnos: deja la caja de texto cerrada
  usage?: UrAgentClientUsage;    // solo chat de pruebas del panel
  modelName?: string;            // solo chat de pruebas del panel
}

interface UrAgentClientConfig {
  style: { color?: string; colorText?: string; colorBot?: string };   // '#rrggbb'; vacío en js_sdk
  title?: string;                  // solo en el canal widget
  maxTurns?: number;               // máximo de turnos por conversación; sin él, no hay límite
  conversationTtlHours?: number;   // horas sin actividad para olvidarla; pásalo a expireIfIdle()
  authMode?: 'optional' | 'required'; // el agente identifica al usuario: pasa getUserToken
}

interface UrAgentClientErrorInfo {
  status?: number;   // el código HTTP de la respuesta
  code?: string;     // solo en un 401: token_missing | token_invalid | token_expired
}

class UrAgentClientHttpError extends Error {   // lo que rechaza loadHistory() con una respuesta no ok
  readonly status: number;
  readonly code: string | undefined;           // como UrAgentClientErrorInfo.code
}

interface UrAgentClientToolEvent {
  name: string;
  status: 'start' | 'end';
  args?: Record<string, unknown>;
}

Si el agente tiene activado el resumen de conversaciones largas, el historial incluye un mensaje role: 'bot' con isSummaryMarker: true cuyo contenido es ese resumen interno: fíltralo antes de pintar, o tu usuario verá un resumen de su propia conversación como si se lo hubiera dicho el agente.

createChatStore ​

ts
function createChatStore(client: UrAgentClient): UrAgentChatStore

Capa opcional de acumulación de mensajes sobre un UrAgentClient ya creado. Framework-agnóstico — un simple getState()/subscribe(), exactamente la forma que espera useSyncExternalStore en React (ver el ejemplo React) y trivial de reflejar en un signal de Angular (ver el ejemplo Angular). Se suscribe internamente a onToken/onNotice/onConversationEnd/onDone/onError del cliente que le pasas — sigues pudiendo registrar tus propios handlers sobre ese mismo cliente para lo que el store no cubre (onToolEvent/onUsage, que solo llegan en el chat de pruebas del panel).

UrAgentChatStore ​

  • getState(): UrAgentChatState
  • subscribe(listener: () => void): () => void — no llama al listener al suscribirte; lee getState() justo después si necesitas el valor inicial.
  • loadHistory(): Promise<void> — sustituye messages por el historial previo, ya sin el resumen interno (isSummaryMarker): lo que queda se puede pintar tal cual. También fija conversationEnded según el servidor. Al contrario que client.loadHistory(), no lanza: un fallo deja status: 'error' y el texto en error (y en un 401, el motivo en errorCode), sin tocar messages. Llamar como mucho una vez por montaje, normalmente solo si !client.isNewSubject.
  • start(): Promise<void> — llámalo al enseñar el chat, después de loadHistory(): si no hay mensajes, pide el mensaje automático del agente (client.start()), que entra como primera burbuja con isAutoMessage. Con mensajes ya, o con conversationEnded, no hace nada. No lanza.
  • send(text: string): Promise<void> — añade el mensaje del usuario y procesa el streaming de la respuesta. No-op mientras status === 'sending' y con conversationEnded. Si hay un start() en vuelo, lo espera antes de añadir el mensaje, para que la bienvenida quede encima.
  • reset(): void — limpia messages y conversationEnded, y llama a client.resetConversation(). Con el chat a la vista, llama a start() después.

UrAgentChatState ​

ts
interface UrAgentChatMessage {
  id: string;
  role: 'user' | 'bot';
  content: string;
  time: string | null;
  isAutoMessage?: boolean;     // el aviso automático del agente
  isError?: boolean;           // bubble sintético creado por el store al recibir `onError`
  pending?: boolean;           // el bubble del bot sigue acumulando tokens
  isConversationEnd?: boolean; // el mensaje de cierre al agotarse los turnos: dale estilo propio
}

interface UrAgentChatState {
  messages: UrAgentChatMessage[];
  status: 'idle' | 'loading-history' | 'sending' | 'error';
  error: string | null;
  errorCode?: string;          // con `error`, en un 401: token_missing | token_invalid | token_expired
  conversationEnded: boolean;  // se agotaron los turnos: deshabilita la caja de texto
}

Si tu cliente no es JavaScript ​

Este paquete es la vía soportada para integrar un chat en el navegador, junto con el widget. Fuera de JavaScript no hay vía soportada para hablar con un agente: el protocolo de /embed/* que usa el paquete no es un contrato público.