Skip to content

Integrar con un agente de IA

Esta página está pensada para dártela a leer a un agente de codificación (Claude Code, Cursor, Copilot, Windsurf…) cuando quieras que él mismo integre el chat en tu app. No es una guía para humanos — es contexto denso y autocontenido para que el agente no tenga que ir a buscar el resto del sitio ni adivinar una API que no existe.

Cópiale a tu agente el bloque de abajo entero (edita los <placeholders> primero si ya conoces los valores, o déjalos para que te los pregunte).

El prompt

text
Eres un agente de codificación ayudando a integrar un cliente de chat personalizado contra un
agente de la plataforma `ur`. Sigue estos pasos.

1. Instala el SDK headless (sin dependencias de runtime, sin UI propia):

   npm install @nykk/ur-agent-client

   (pnpm add / yarn add son equivalentes).

2. Necesitas dos datos — pídemelos si no los tienes ya:
   - `agent`: el `identifier` (o el `id`) del agente `ur` a integrar.
   - `apiBase`: el origen del backend `ur` (ej. `https://ur.miempresa.com`). Si el cliente se sirve
     desde el mismo origen que el backend, puedes omitirlo (usa `window.location.origin` por
     defecto).
   El canal `js_sdk` de ese agente debe estar habilitado — lo está por defecto salvo que alguien lo
   haya desactivado explícitamente en el panel.

3. Esta es la API pública completa del SDK. No hay más superficie que esta — no inventes métodos,
   eventos u opciones que no aparezcan aquí (no existe `connect()`, no existe autenticación, no
   existe `onMessage`):

   class UrAgentClient {
     constructor(options: {
       agent: string;
       apiBase?: string;
       channel?: string;   // por defecto 'js_sdk' — no lo cambies salvo que sepas por qué
       subjectId?: string; // reanuda un hilo concreto en vez del persistido por defecto
       storage?: { getItem(key: string): string | null; setItem(key: string, value: string): void };
       // ^ persistencia del subjectId por defecto; por defecto usa localStorage
       context?: Record<string, unknown>; // datos por turno para las tools, agrupados por tool
     });

     readonly subjectId: string;
     readonly isNewSubject: boolean; // true si no hay nada que restaurar vía loadHistory()

     onToken(fn: (delta: string) => void): () => void;   // delta de texto del turno en curso
     onNotice(fn: (content: string) => void): () => void; // aviso automático único del agente, si tiene uno configurado
     onDone(fn: (threadId: string) => void): () => void;
     onError(fn: (message: string) => void): () => void;  // mensaje siempre "de catálogo", nunca la excepción real

     setContext(context: Record<string, unknown> | undefined): void; // surte efecto en el siguiente sendMessage()
     abort(): void;                 // cancela un sendMessage() en curso, sin disparar onError
     resetConversation(): string;   // nuevo subjectId, conversación nueva de verdad en el backend
     loadHistory(): Promise<{
       threadId: string | null;
       messages: { role: 'user' | 'bot'; content: string; time: string | null; isAutoMessage?: boolean }[];
     }>;
     sendMessage(text: string): Promise<void>; // resuelve cuando el streaming termina; nunca lanza
   }

   function createChatStore(client: UrAgentClient): {
     getState(): {
       messages: {
         id: string;
         role: 'user' | 'bot';
         content: string;
         time: string | null;
         isAutoMessage?: boolean; // aviso automático del agente
         isError?: boolean;       // bubble sintético creado al recibir onError
         pending?: boolean;       // el bubble del bot sigue acumulando tokens
       }[];
       status: 'idle' | 'loading-history' | 'sending' | 'error';
       error: string | null;
     };
     subscribe(listener: () => void): () => void; // NO llama al listener al suscribirte — lee getState() tú mismo justo después si necesitas el valor inicial
     loadHistory(): Promise<void>; // sustituye messages por el historial previo
     send(text: string): Promise<void>;          // no-op mientras status === 'sending'
     reset(): void;                               // limpia messages y llama a resetConversation()
   };

4. Patrón recomendado — funciona igual en cualquier framework, así que constrúyelo así salvo que
   te pida explícitamente usar `UrAgentClient` directamente:

   import { UrAgentClient, createChatStore } from '@nykk/ur-agent-client';

   const client = new UrAgentClient({ agent: '<agent>', apiBase: '<apiBase>' });
   const store = createChatStore(client);

   if (!client.isNewSubject) {
     await store.loadHistory(); // restaura la conversación previa de este navegador
   }

   // UI: renderiza store.getState().messages, un input de texto, y al enviar: store.send(texto)
   // Botón "nueva conversación": store.reset()

5. Conecta `store` a la reactividad de mi framework usando exactamente su contrato
   `getState()`/`subscribe()` — no reimplementes el acumulado de tokens a mano, ya lo hace
   `createChatStore`. Dime qué framework uso (React, Angular, Vue, Svelte, o ninguno/vanilla) si no
   lo sabes por el resto del proyecto, y adapta el store con el primitivo de estado idiomático de
   ese framework (signal, ref, store, useState, o un simple render manual).

6. Las respuestas del agente vienen en Markdown. Renderízalo (p. ej. `marked` + `DOMPurify`) en vez
   de pintarlas como texto plano.

7. Detalles que no debes pasar por alto:
   - Este canal no tiene autenticación — no añadas cabeceras `Authorization`/`Bearer` aquí. Es un
     `subjectId` generado por el propio navegador y persistido en `localStorage`.
   - `onToolEvent`/`onUsage` (en `UrAgentClient`, no expuestos por `createChatStore`) solo disparan
     en el canal interno `demo` — no los uses ni los esperes en una integración real.
   - `context` envía datos por turno a las tools del agente, agrupados por nombre de tool; las
     claves que ninguna tool declare se descartan (vuelven en `X-Ur-Context-Dropped`). Un solo campo
     inválido descarta el bloque entero de esa tool. No es una credencial: el canal es anónimo y
     cualquiera puede falsificarlo.
   - `getAuthHeaders` existe en las options de `UrAgentClient` pero el backend todavía no hace nada
     con él — no lo presentes como funcional en la UI que construyas.
   - Un mensaje bloqueado por seguridad llega como una respuesta normal (tokens + fin), no como
     error. No intentes detectarlo.
   - El estilo/CSS es tu elección completa; el SDK no renderiza nada ni impone clases.

Recetas por framework

Lo que sigue es la traducción de "conecta store a la reactividad de mi framework" (paso 5 del prompt) para los casos más comunes. Los dos primeros son exactamente el código de los ejemplos completos de este monorepo — el resto son el mismo patrón aplicado al framework correspondiente.

React

Igual que use-ur-agent-chat.ts del ejemplo React: useSyncExternalStore es literalmente el contrato getState()/subscribe() que ya expone el store, sin adaptador intermedio.

tsx
import { useState, useSyncExternalStore } from 'react';
import { createChatStore, UrAgentClient } from '@nykk/ur-agent-client';

function useUrAgentChat() {
  const [{ client, store }] = useState(() => {
    const client = new UrAgentClient({ agent: '<agent>', apiBase: '<apiBase>' });
    return { client, store: createChatStore(client) };
  });

  const state = useSyncExternalStore(store.subscribe, store.getState);
  // en un useEffect: if (!client.isNewSubject) void store.loadHistory();

  return { state, send: store.send, reset: store.reset };
}

Angular

Igual que agent-chat.service.ts del ejemplo Angular: un signal que se reescribe dentro de subscribe().

ts
import { Injectable, OnDestroy, signal } from '@angular/core';
import { createChatStore, UrAgentClient, type UrAgentChatState } from '@nykk/ur-agent-client';

@Injectable({ providedIn: 'root' })
export class AgentChatService implements OnDestroy {
  private readonly client = new UrAgentClient({ agent: '<agent>', apiBase: '<apiBase>' });
  private readonly store = createChatStore(this.client);
  private readonly unsubscribe = this.store.subscribe(() => this.state.set(this.store.getState()));

  readonly state = signal<UrAgentChatState>(this.store.getState());

  send(text: string): void { void this.store.send(text); }
  reset(): void { this.store.reset(); }
  ngOnDestroy(): void { this.unsubscribe(); this.client.abort(); }
}

Vue

Mismo patrón con un shallowRef (el estado se sustituye entero en cada cambio, nunca se muta) y onScopeDispose para desuscribirse.

ts
import { onScopeDispose, shallowRef } from 'vue';
import { createChatStore, UrAgentClient } from '@nykk/ur-agent-client';

export function useUrAgentChat() {
  const client = new UrAgentClient({ agent: '<agent>', apiBase: '<apiBase>' });
  const store = createChatStore(client);
  const state = shallowRef(store.getState());

  const unsubscribe = store.subscribe(() => (state.value = store.getState()));
  onScopeDispose(() => { unsubscribe(); client.abort(); });
  if (!client.isNewSubject) void store.loadHistory();

  return { state, send: store.send, reset: store.reset };
}

Svelte

createChatStore no llama al listener al suscribirte (a diferencia del contrato de store nativo de Svelte, que sí lo hace) — un readable con un start que lee getState() una vez y luego reenvía cada cambio cierra esa diferencia:

ts
import { readable } from 'svelte/store';
import { createChatStore, UrAgentClient } from '@nykk/ur-agent-client';

const client = new UrAgentClient({ agent: '<agent>', apiBase: '<apiBase>' });
const store = createChatStore(client);

export const chatState = readable(store.getState(), (set) => {
  set(store.getState());
  if (!client.isNewSubject) void store.loadHistory();
  return store.subscribe(() => set(store.getState()));
});
export const send = store.send;
export const reset = store.reset;

Sin framework (vanilla)

El caso base del que derivan todos los anteriores: subscribe + una función de render.

ts
import { createChatStore, UrAgentClient } from '@nykk/ur-agent-client';

const client = new UrAgentClient({ agent: '<agent>', apiBase: '<apiBase>' });
const store = createChatStore(client);

function render() {
  const { messages, status } = store.getState();
  // pinta `messages` en el DOM que tengas
}

store.subscribe(render);
render();
if (!client.isNewSubject) void store.loadHistory();

// al enviar: void store.send(inputEl.value);
// botón reset: store.reset();

Si tu agente necesita más detalle

Este prompt cubre el 90% de una integración típica de chat. Para lo que se quedó fuera a propósito, dirige a tu agente a:

Y si lo que quiere integrar no es un chat:

  • API HTTP — inferencias y RAGs desde un backend, con token Bearer.
  • Agente por HTTP — un turno completo sin streaming.