05Garantías obligatorias

Sección 05

Garantías obligatorias

Seis garantías que hoy aporta la plataforma Asixto y que su sistema debe aportar en su lugar. No son buenas prácticas genéricas: cada una corresponde a una forma concreta en que un agente conversacional rompe datos reales. Todas son comprobaciones bloqueantes de la certificación.

Aplican a las herramientas de escritura. Las de solo lectura solo deben cumplir la 5 y la 6.


1 · Idempotencia en toda escritura

El Gateway envía com.asixto/idempotencyKey en el _meta de la llamada (no en los argumentos) con una clave determinista por operación lógica: se deriva de la empresa, el nombre de la herramienta y la firma de los argumentos, así que un doble intento del modelo la repite idéntica. Es el mismo mecanismo que usan las herramientas nativas del agente.

Su servidor debe persistirla y, si la vuelve a recibir, devolver el mismo resultado sin volver a ejecutar.

El orden importa, y es contraintuitivo: la consulta de la clave va ANTES de las guardas de propiedad y de estado terminal. Si va después, pasa esto:

Diagrama
1er intento → cancela la cita → éxito
2º  intento → la guarda ve «ya cancelada» → VALIDATION

…y el agente le dice al cliente que su cancelación falló, cuando en realidad funcionó. El estado cambió por su propia primera ejecución, así que no es una transición inválida: es el mismo trabajo pedido dos veces. Guarde solo los éxitos: un fallo transitorio debe poder reintentarse.

Python
async def cancelar_cita(cita_id: str, documento: str, *, meta: dict) -> dict:
    key = meta["com.asixto/idempotencyKey"]

    # 0 · PRIMERO la clave, antes de cualquier guarda.
    if (previo := await store.get(key)) is not None:
        return previo

    cita = await mi_backend.buscar_cita(cita_id)
    if cita is None:
        return error("NOT_FOUND", "No encontré esa cita.")

    # 1 · Propiedad.
    if cita.titular != documento:
        return error("PERMISSION", "Esa cita no está a nombre de quien me habla.")

    # 2 · Estado terminal.
    if cita.estado != "agendada":
        return error("VALIDATION", f"Esa cita ya está {cita.estado}.")

    resultado = await mi_backend.cancelar(cita_id)
    await store.set(key, resultado, ttl=7 * 24 * 3600)   # solo los éxitos
    return resultado

Requisitos del almacén: TTL de al menos 7 días y escritura atómica (SET NX o índice único), no get seguido de set sin transacción.

Dos precisiones que evitan un error caro:

  • La clave incluye la conversación, así que dos clientes finales distintos que pidan lo mismo con los mismos datos no comparten clave. Si su implementación deduplica por su cuenta usando solo los argumentos, sí las mezclaría: use la clave que le llega, no una propia.
  • Dentro de la misma conversación, la misma operación con los mismos argumentos es la misma operación. Si el cliente quiere de verdad dos citas idénticas, algún argumento tendrá que distinguirlas; si no, la segunda le devolverá el resultado de la primera. En la práctica es el comportamiento correcto: casi siempre que eso ocurre es un doble intento del modelo.

Por qué. Hay dos causas reales de repetición, no hipótesis:

  1. El modelo puede emitir la misma llamada dos veces en un mismo turno.
  2. El transporte ya no reanuda flujos cortados: una respuesta interrumpida se reintenta como petición nueva.

Si falta: casos, citas o cotizaciones duplicados.


2 · Propiedad del registro

Toda operación sobre una entidad valida que pertenece al contacto de la conversación, no solo que el identificador existe.

Diagrama
❌  cancelar_cita(cita_id)          → busca la cita y la cancela
✅  cancelar_cita(cita_id)          → busca la cita, verifica que su titular
                                      es el contacto en curso, y solo entonces cancela

Un punto del diseño que conviene tener claro: el Gateway no le envía una identidad autenticada del usuario final. La conversación ocurre en WhatsApp, en un chat web o por teléfono; ahí no hay sesión ni token de usuario. Quien dice ser el cliente lo afirma hablando.

Consecuencia práctica, y es responsabilidad suya:

  1. Exponga una herramienta de identificación (documento, teléfono o correo) y haga que las operaciones sensibles exijan ese identificador como parámetro.
  2. Valide que la entidad objetivo pertenece a ese identificador. Con com.asixto/conversationId puede además detectar si el identificador cambió a mitad de conversación, que es una señal de alarma que vale la pena registrar.
  3. Para operaciones de alto impacto, no se conforme con el identificador: exija un dato que solo el titular conoce, o un código que su sistema envíe por su propio canal.

Si no coincide, devuelva error_type: "PERMISSION" (no un 403 crudo) para que el agente lo explique y escale.

Si falta: un usuario cancela la cita de otro o corrige datos ajenos mencionando un identificador plausible. Con un modelo de lenguaje de intermediario, un identificador plausible es fácil de producir.


3 · Guardas de estado terminal

Rechace transiciones inválidas con un error de negocio claro:

EntidadEstado terminalRechazar
Citacancelada, atendidamodificar, cancelar de nuevo
Oportunidadcerrada ganada o perdidamover etapa, reabrir
Cotizaciónaceptada, rechazada, vencidavolver a resolver, reenviar
Casocerradoreabrir sin caso nuevo

Si falta: el agente insiste, el usuario recibe una confirmación falsa y su sistema queda con estados imposibles.


3.1 · No confíe en el orden dentro de un turno

El modelo puede pedir dos herramientas en el mismo turno (por ejemplo, consultar franjas y crear la cita) y el orden en que las recibe su servidor no está garantizado: el runtime solo garantiza precedencia en un caso concreto de agenda, no en general.

Consecuencia: toda mutación revalida sus propios identificadores contra su sistema en el momento de ejecutarse. No asuma que la lectura que resolvió el id ya ocurrió, ni que el estado que devolvió sigue vigente. Si el id no resuelve o el estado cambió, rechace con VALIDATION (página 06) en vez de operar sobre algo que ya no existe.


4 · El dato crítico lo decide su sistema

Fechas, montos, existencias e identificadores se resuelven y validan en su lado. Si el agente propone algo inconsistente, rechácelo con un error entendible en vez de aceptarlo.

DatoRegla
Fecha y horaValide contra su disponibilidad real, al nivel de instante, no de día: una franja de hoy cuya hora ya pasó es pasada. Rechace lo ocupado aunque venga solicitado.
MontoRecalcule desde su catálogo y su lista de precios. Nunca acepte el total que le manden.
ExistenciaVerifique en el momento de la operación, no en el de la consulta.
Identificador de entidadResuelva contra lo que el cliente ya vio en la conversación. Si es ambiguo, devuelva error de ambigüedad en vez de elegir.

Por qué. Es el fallo más costoso de todos: un modelo interpreta «el próximo martes» y acierta casi siempre. Un acierto «casi siempre» no es suficiente para agendar ni para cobrar.


5 · Salida mínima y saneada

Devuelva solo lo necesario para responder. Prohibido: identificadores internos, claves primarias, datos de terceros, campos administrativos, trazas, tokens, rutas internas.

El resultado entra al contexto del modelo, queda en la traza de la conversación y, en una llamada telefónica, puede terminar leído en voz alta.

Límite duro: 3.000 caracteres por respuesta (ahí recorta el Gateway) y el limit que reciba como tope de elementos (página 07).


6 · Error con el tipo canónico

Todo error lleva error_type (uno de los seis valores canónicos) y un user_message apto para el usuario final. Forma exacta y tabla de los seis en la página 06.

La distinción que más importa: un dato ausente y un sistema no disponible son eventos distintos. Si los colapsa, el agente dirá «no tenemos eso» cuando la causa real es que su sistema no responde, y lo dirá con la misma seguridad con la que dice todo lo demás.

Y el caso que más veces se implementa mal: una búsqueda sin resultados no es un error. Es un éxito con lista vacía.


Cómo se verifica

GarantíaComprobación de la certificación
1Dos llamadas con la misma clave en _meta: el efecto debe ocurrir una sola vez
2Llamada con un identificador que no pertenece al contacto: PERMISSION
3Transición sobre una entidad en estado terminal: VALIDATION con el estado actual
3.1Mutación con un identificador que la lectura del mismo turno no devolvió: rechazada
4Franja ocupada y monto alterado: deben rechazarse
5Inspección de la respuesta: sin identificadores internos ni datos de terceros, ≤ 3.000 caracteres
6Provocar tres casos distinguibles: sin resultados (éxito), dato inexistente (NOT_FOUND) y dependencia caída (SYSTEM)

Las comprobaciones que escriben se ejecutan solo contra el entorno de pruebas que usted declara.