Tema
Contexto de cliente
Tu aplicación puede mandar datos en cada mensaje para que las tools del agente los usen: la página en la que está el usuario, un número de pedido, el idioma de la interfaz.
El modelo no ve estos valores directamente: ur no los concatena al prompt en ningún momento. Solo llegan a las tools que los hayan declarado, y cada una decide qué hacer con ellos. Ojo: si una tool devuelve el valor en su respuesta, el modelo sí lo leerá —como leería cualquier otro resultado de tool—, y ese texto no ha pasado por las comprobaciones de seguridad del agente, que solo miran el mensaje. Pásalo antes por screenClientText (ver Escribir una tool que lo consuma).
No es una credencial
Nadie verifica estos valores, tampoco en un agente que identifica a su usuario: cualquiera puede mandar los que quiera con un curl. Los datos que llegan son bien formados, no fiables.
Úsalos para decir qué mirar, nunca qué puede ver quien pregunta. Cualquier búsqueda que dependa de un valor del contexto tiene que cruzarse con una identidad verificada: WHERE id = ctx.orderId AND owner = <sujeto verificado>. Si el agente identifica a su usuario, ese sujeto es el sub de su token, que la tool lee con getEndUser; quién es el usuario va ahí, nunca en el contexto.
La forma: agrupado por tool
El contexto no es un saco plano. Va agrupado por el nombre de la instancia de tool que lo va a consumir:
json
{
"facturacion": { "orderId": "A-1024" },
"visitor_context": { "page": "/precios", "locale": "es-ES" }
}Así dos tools distintas pueden querer las dos una clave page sin pisarse, y cada una recibe únicamente su bloque.
Enviarlo
ts
const client = new UrAgentClient({
agent: 'soporte',
apiBase: 'https://<tu-backend-ur>',
context: {
visitor_context: { page: '/precios', locale: 'es-ES' },
},
});
// Al navegar, sin recrear el cliente (que perdería la conversación):
client.setContext({ visitor_context: { page: '/contacto', locale: 'es-ES' } });html
<script
src="https://<tu-backend-ur>/widget.js"
data-agent="soporte"
data-context='{"visitor_context":{"page":"/precios"}}'
></script>js
document.querySelector('ur-agent-chat').context = {
visitor_context: { page: '/contacto' },
};Qué claves acepta tu agente
No se configura en ninguna parte: lo declaran las tools del agente. Un agente sin ninguna tool que declare contexto no acepta ninguna clave. Y si la tool lleva su propio interruptor de contexto —rag_retrieve lo lleva, y viene apagado— con él apagado tampoco: la instancia no aparece en el contrato, así que lo que mandes bajo su nombre se descarta y sale en X-Ur-Context-Dropped.
Para saber cuáles son las de un agente concreto te lo dice el panel, en la ficha del canal: lista las claves aceptadas con su tipo, si son obligatorias y para qué sirven, y te deja escribir un ejemplo en JSON que valida contra ese contrato y baja al snippet — al data-context del <script> en Chat embebible, y a la opción context en Integración a medida. Cópialo de ahí en vez de adivinarlo. El ejemplo no se guarda: en producción cada integración manda los valores que calcule en cada momento.
Los cambios tardan hasta 30 segundos
El contrato se cachea. Si acabas de vincular o desvincular una tool, puede tardar medio minuto en reflejarse en lo que el endpoint acepta.
Qué pasa si mandas algo que no encaja
Aquí hay dos comportamientos muy distintos, y conviene no confundirlos.
Si el context entero no es válido → 400. Solo pasa si no es un objeto JSON o si ocupa más de 4 KB serializado:
json
{ "error": "context must be a JSON object no larger than 4KB" }Si una clave concreta no encaja → se descarta en silencio y el mensaje sigue. No hay error. Los nombres descartados vuelven en la cabecera de respuesta X-Ur-Context-Dropped, para que los veas en las devtools.
Se descarta porque un 400 convertiría un endpoint público en un oráculo para averiguar qué tools tiene un agente, y rompería integraciones vivas en cuanto alguien desvincule una tool en el panel.
Un campo malo tira el bloque entero
La validación es por namespace, no por campo. Si mandas {"facturacion": {"orderId": 1024}} y el contrato declara orderId como texto, se descarta facturacion entero — la tool no recibe nada de ese bloque. Lo mismo si falta un campo obligatorio.
Una clave de más que el contrato no declare es la excepción: no rompe nada, se elimina en silencio y el resto del bloque se entrega igual.
Es la causa número uno de "le paso el contexto y la tool no lo ve". Mira X-Ur-Context-Dropped en la respuesta: si aparece ahí el namespace, es esto.
Acotar la búsqueda en tus documentos
La tool rag_retrieve puede aceptar contexto para restringir a qué documentos mira. Viene desactivado por defecto, y hay dos interruptores que tienen que estar encendidos:
- El de la instancia de la tool (panel → Tools → la instancia → Contexto de cliente).
- El maestro del agente (panel → Agentes → el agente → Tools → Aceptar contexto de cliente), encendido de fábrica. Apaga el contexto de todas las tools del agente de golpe, sin tocar instancias que puedan estar compartidas con otros agentes.
Lo que llega es el AND de los dos: con cualquiera apagado, el contexto se descarta.
Con ambos activos, acepta tres claves, todas opcionales:
| Clave | Límite | Qué hace | Cómo combina |
|---|---|---|---|
paths | 25 rutas, 1024 caracteres | Rutas exactas | Suma con pathPrefixes |
pathPrefixes | 25 prefijos, 1024 caracteres | Toda una sección, p. ej. docs/facturacion | Suma con paths |
tags | 10 etiquetas, 128 caracteres | Etiquetas del proveedor de ficheros | Filtra encima de las rutas |
En una fórmula: (paths ∪ pathPrefixes) ∩ tags. Las dos claves de ruta nombran lo mismo, así que entra todo lo que case con alguna de las dos — mandar las dos amplía el ámbito respecto a mandar solo una, no lo acota más. Las etiquetas son otra dimensión y sí acotan: combinadas con rutas, solo entra lo que además lleve alguna de ellas; solas, acotan por sí mismas.
ts
client.setContext({
documentacion: {
paths: ['legal/contrato-marco.pdf'], // este fichero...
pathPrefixes: ['docs/facturacion'], // ...y toda esta sección
tags: ['publico'], // ...pero solo lo etiquetado así
},
});Con varias bases de conocimiento, nómbralas
Una tool puede estar enganchada a varias. Cuando lo está, el ámbito va por base de conocimiento, con su identificador como primera clave:
ts
client.setContext({
documentacion: {
'docs-publica': { pathPrefixes: ['docs/facturacion'] },
'manuales': { pathPrefixes: ['producto-x/'], tags: ['publico'] },
},
});Con una sola base, las claves van directamente y no hace falta nombrarla.
Un bloque suelto con varias bases no acota nada
Y se descarta entero, a propósito. El mismo pathPrefixes: ['docs/'] aplicado a dos corpus con estructuras distintas acota bien uno y vacía el otro, que es la peor forma de fallar: parece que funciona. Como no hay forma de adivinar cuál de los dos querías, no se aplica a ninguno.
Una base que no nombres se busca entera: el silencio es «sin preferencia», no «nada». Acotar a cero hay que pedirlo.
El contexto pone el techo; el modelo solo puede estrechar
Dentro del contexto las rutas suman y las etiquetas filtran, pero frente al modelo el ámbito resultante es un techo: si el contexto limita a docs/facturacion, el modelo puede buscar en esa sección o en una parte de ella, y no puede ampliarla pidiendo otras rutas o etiquetas. No existe forma de que el contexto abra documentos que el agente no tuviera ya — precisamente porque el valor es falsificable.
La contención es una consulta a la base de datos y un filtro en el índice vectorial, no una instrucción en el prompt, así que tampoco se puede saltar por prompt injection.
No hace falta escribirlo a mano
En el panel, la sección Contexto de cliente de la instancia de la tool tiene un botón Examinar. Abre el corpus indexado de cada base de conocimiento, navegable, y al marcar una carpeta o un fichero escribe por ti el contexto que haría falta: una carpeta sale como pathPrefixes, un fichero como paths. El recuento de arriba —«entran N de M»— se recalcula con cada marca, así que se ve si te has pasado de filtro mientras lo montas.
También te dice qué rutas has escrito que no existen. Los dos fallos más comunes —una barra inicial de más y una etiqueta que no está en esa fuente— son silenciosos en producción: el agente contesta que no hay documentación.
Las barras iniciales se normalizan, así que puedes pasar location.pathname tal cual aunque el corpus esté indexado sin ella.
Si el interruptor de la instancia está apagado, el namespace no forma parte del contrato, así que se descarta como cualquier clave desconocida y sí aparece en X-Ur-Context-Dropped. Si el apagado es el maestro del agente, pasa lo mismo con todos sus namespaces a la vez, y la respuesta lleva además X-Ur-Context-Disabled: agent — que es lo que distingue «este agente lo tiene apagado» de «esa tool no declara contexto», dos situaciones que se arreglan en pantallas distintas.
Las dos capas
Lo que acabas de ver es el contexto de ejecución: lo envía tu aplicación en cada turno y nadie lo verifica, así que cualquiera puede mandar otro, o ninguno.
Debajo hay una segunda capa, el ámbito cerrado de la tool. Lo escribe un admin en el panel (Tools → la instancia → Contexto de cliente), tiene exactamente la misma forma, y se aplica siempre — también con el interruptor de contexto de cliente apagado. Lo efectivo es la intersección: el de ejecución solo puede estrechar dentro del cerrado, nunca salirse.
| Quién lo escribe | Cuándo se aplica | ¿Puede falsificarse? | |
|---|---|---|---|
| Ámbito cerrado | Un admin, en el panel | Siempre | No |
| Contexto de ejecución | Tu aplicación, por turno | Si el interruptor está activo | Sí |
Si lo que necesitas es que un agente no pueda salir de un conjunto de documentos pase lo que pase, la capa que buscas es la cerrada. El contexto de ejecución sirve para enfocar, no para contener.
Dónde no funciona
En el canal demo (el "Probar agente" del panel) el contexto no llega a ninguna tool: se sigue comprobando que sea un objeto JSON y que no pase de 4 KB, pero se descarta después. Ese canal se salta las comprobaciones de canal y origen, así que aceptar contexto ahí sería la vía de escape. Para probar el contexto, hazlo desde tu integración real.
Escribir una tool que lo consuma
Si desarrollas tus propias tools sobre @nykk/ur-core, una plantilla declara su contrato con contextSchema (un esquema zod) y lo lee dentro del callback con getClientContext:
ts
import { tool } from '@langchain/core/tools';
import {
getClientContext,
screenClientText,
type ToolInstanceConfig,
type ToolTemplate,
} from '@nykk/ur-core';
import { z } from 'zod';
const visitorContextSchema = z.object({
page: z.string().max(512).describe('Dónde está el visitante, p. ej. "/precios"'),
locale: z.string().max(16).optional(),
});
export const visitorContextTool: ToolTemplate = {
id: 'visitor_context',
name: 'Visitor Context',
description: 'Dónde está el visitante ahora mismo en tu aplicación.',
scope: 'app',
singleton: true,
// Declararlo aquí es lo que hace que el agente acepte un bloque `context.<instancia>`.
contextSchema: visitorContextSchema,
functions: [
{
suffix: '',
description: 'Dónde está el visitante ahora mismo en tu aplicación.',
// Los argumentos que elige el MODELO — distintos del contexto, que lo pone tu app.
parametersSchema: z.object({}),
factory: (instance: ToolInstanceConfig) =>
tool(
async (_input, runConfig) => {
const ctx = getClientContext<z.infer<typeof visitorContextSchema>>(
runConfig,
instance.name,
);
// `ctx` es `undefined` si no llegó nada o si no validó.
if (!ctx) {
return 'La aplicación no ha dicho dónde está el visitante.';
}
// Lo que devuelves lo lee el modelo: pasa antes por los checks de seguridad del agente.
if (!(await screenClientText(runConfig, instance.name, ctx.page))) {
return 'La aplicación no ha dicho dónde está el visitante.';
}
return `El visitante está en ${JSON.stringify(ctx.page)}.`;
},
{ name: instance.name, description: instance.description, schema: z.object({}) },
),
},
],
};Se registra al arrancar, pasándoselo a createApp({ tools: [visitorContextTool] }).
Los esquemas son de zod 4, la misma versión que usa @nykk/ur-core: declara "zod": "4.6.1" en el package.json de tu app. Desde core 0.7 los tipos de ToolTemplate son de zod 4, y una tool con esquemas de zod 3 no compila.
Lo que devuelves lo lee el modelo
El mensaje del usuario pasa por las comprobaciones de seguridad del agente (palabras prohibidas, entropía, firewall semántico) antes de llegar al modelo. El contexto no, porque casi siempre sirve solo para decidir qué buscar. En cuanto una tool lo copia en su respuesta, como hace esta con page, cualquiera puede meter con un curl texto en el prompt que nadie ha revisado.
screenClientText le pasa ese texto al mismo pipeline. Si lo bloquea, devuelve false y la intercepción queda en el registro del agente con el motivo «Contexto de cliente (…)». El turno sigue: la tool descarta el valor y responde como si no hubiera llegado. Fuera de un turno de agente devuelve siempre false, otra razón para que la tool sepa responder sin el valor. La excepción es «Probar tool», en el panel: ahí lo deja pasar, porque el texto lo escribe el admin.
Lo que solo usas para buscar (un orderId en un WHERE) no hace falta pasarlo.
La tool tiene que funcionar sin contexto
getClientContext devuelve undefined cuando el cliente no mandó nada, cuando la plantilla no declara contrato, o cuando lo que mandó no validó. Una tool que dé por hecho que el contexto está ahí se romperá en cuanto alguien la llame desde otro canal.
El ejemplo de arriba es completo: guárdalo en tu app (por ejemplo en src/tools/visitor-context.ts) y pásalo a createApp.