Tema
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:
- SDK — referencia —
UrAgentClientycreateChatStorecompletos, con todos los tipos. - Contexto de cliente — pasar datos de tu app a las tools.
- Limitaciones — qué no hace todavía y por qué.
/llms.txt— mapa del sitio en el formato llms.txt, pensado para que cualquier agente lo cargue de un vistazo antes de tocar código.
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.