tools, el modelo de @botmesh/ai puede llamar a funciones
de tu bot (buscar en el catálogo, consultar un pedido, crear una reserva)
mediante el tool / function calling nativo de cada proveedor. Declaras cada
herramienta una vez; el plugin la traduce para Anthropic, OpenAI, Gemini u
Ollama, ejecuta un bucle de varios pasos y le devuelve los resultados al modelo
hasta que escribe la respuesta final.
Declarar herramientas
tools es un AITool; la clave es el nombre que ve el modelo.
defineTool<TArgs>(tool) es un helper identidad: solo infiere el tipo de los
argumentos del handler para que el editor autocomplete args.
Esquemas
- JSON Schema: se envía tal cual al modelo y se valida con un subconjunto
propio (
type,required,properties,enum,const,items,additionalProperties: false,minimum/maximumy sus formas exclusivas,minLength/maxLength,minItems/maxItems). - Validadores con
safeParse(zod y similares): los argumentos se validan consafeParse()y el handler recibe eldataya parseado. El esquema para el modelo sale detoJSONSchema()si el validador lo tiene (zod v4); si no, pasajsonSchemaa mano.
enum para conjuntos cerrados
como las claves de producto: en una prueba real el modelo pasó
"torta de chocolate" en vez de la clave torta_chocolate, y solo el enum
lo frena antes de preguntarle al usuario.
El contexto del handler
El segundo argumento dehandler es un AIToolContext:
El bucle de herramientas
Cada turno ejecuta un bucle: llama al modelo con las definiciones; mientras pida herramientas, valida los argumentos, pide confirmación si hace falta, ejecuta cada handler con su timeout y le devuelve los resultados.maxSteps(por defecto5) limita las llamadas al modelo por turno. Las herramientas pedidas en el último paso permitido no se ejecutan (el modelo nunca vería su resultado): el turno respondemaxStepsMessagey emiteloop.max_steps.- Varias llamadas en el mismo paso se ejecutan una tras otra, en orden.
- Los errores van al modelo, nunca al usuario: un handler que lanza, argumentos inválidos, una herramienta desconocida, un timeout o una confirmación rechazada se convierten en resultados fallidos, para que el modelo se recupere o lo explique.
- Timeouts:
toolTimeoutMs(por defecto15000,0lo desactiva) se aplica a todas las herramientas salvo a las que fijan su propiotimeoutMs. Al vencer se aborta elsignaldel handler. Cada llamada al modelo mantiene eltimeoutMsdel plugin. - El historial guardado sigue siendo texto plano usuario/asistente: los turnos intermedios de herramientas no se persisten.
Confirmación humana
Una herramienta conconfirm: true solo se ejecuta cuando una persona la
aprueba.
En el autoresponder el plugin le pregunta al usuario. La pregunta
(confirmMessage, o una legible por defecto, ver más abajo) se hace dentro de
una conversación del runtime, así que el handler termina y libera la cola FIFO
del chat: la respuesta del usuario llega a la conversación y el bucle
continúa. Nunca bloquea el chat.
La respuesta se clasifica con classifyConfirmation():
- Un sí claro (
sí,ok,dale,sí, por favor…) ejecuta la herramienta. - Un no claro (
no,no, gracias,cancelar…) la rechaza y se le dice al modelo que no reintente. - Cualquier otra cosa, incluido un sí que pide cambios (
sí pero cambia a 5 unidades), no ejecuta la herramienta ni se descarta: el modelo recibe{ "executed": false, "reason": "user_replied", "userReply": "…" }para volver a llamarla con los argumentos corregidos (el usuario confirma de nuevo) o responder la pregunta.
DEFAULT_YES) son si, s, yes, y, ok, okay, confirmo,
confirmar, dale, claro, de acuerdo, acepto, 1, vale, listo,
perfecto, adelante, hazlo, correcto, esta bien, sure, yeah y
yep; las de “no” (DEFAULT_NO) son no, n, nop, nope, nel,
cancelar, cancela, cancelo, cancel, negativo, mejor no,
para nada, tampoco, no gracias, 2 y 0. Cámbialas con
toolConfirm.yes / toolConfirm.no; las dos listas están congeladas, así que extiende
una copia (yes: [...DEFAULT_YES, 'arre']).
- Sin doble confirmación: cuando el turno tiene confirmador, el prompt de
sistema lleva una nota localizada (
ai.tools.confirmNote) con las herramientasconfirm: true, que le pide al modelo no preguntar “¿te lo registro?” sino llamar a la herramienta en cuanto el usuario exprese la intención.toolConfirm.systemNotela cambia por tu texto o la apaga (false). toolConfirm.timeout(por defecto'2m') limita la espera. Un timeout o/resetcancela el turno en silencio;/resettambién lo cancela después de la aprobación, así que el turno viejo no responde ni escribe en el historial recién borrado.- Los turnos de un mismo chat se serializan: un mensaje nuevo espera a que termine un turno desacoplado. Una confirmación nunca reemplaza una conversación que el usuario haya abierto entretanto; el turno se cancela y no responde encima de ese flujo.
Botones Sí/No
En los canales que muestran botones nativos (capabilities.buttons: teclados
inline de Telegram, botones de respuesta de WhatsApp Cloud API, Discord, quick
replies de Messenger e Instagram…) la pregunta sale como mensaje de botones
con etiquetas localizadas (Sí / No, Yes / No en inglés; claves
ai.tools.button.yes / ai.tools.button.no) y el mismo resumen, sin la
coletilla de respuesta escrita (ai.tools.confirmButtons:
Voy a: {tool}{args}\n¿Confirmas?).
- Pulsar un botón resuelve la confirmación igual que escribir sí o no, y la
interacción se reconoce (
answerCallbackQueryen Telegram, el ack de 3 segundos de Discord). Las respuestas escritas siguen funcionando junto a los botones, también una respuesta con cambios, que vuelve al modelo. - Ya respondida (o caducada / cancelada), los botones se quitan editando el
mensaje a texto con la elección (
→ Sí) donde el provider puede editar y pinta botones nativos (Telegram, Discord). - Una pulsación sobre una confirmación que ya no está pendiente (respondida,
sustituida por otra más nueva, caducada,
/reset) se reconoce y se ignora antes de enrutar nada: cada confirmación tiene sus propios ids de botón, así que una pulsación vieja nunca responde una confirmación más nueva ni una pregunta posterior de una conversación. - Canales que pintan botones nativos pero no editan mensajes (WhatsApp Cloud
API, Messenger, Instagram, Gupshup, TikTok): los botones se quedan en
pantalla. Ahí, una pulsación sobre una confirmación que ya no está pendiente
recibe una respuesta corta localizada (
ai.tools.confirmExpired: “Esa confirmación ya no está activa.” / “That confirmation is no longer active.”);toolConfirm.expiredNotice: falsela descarta en silencio. Un chat en handoff con un humano nunca recibe este aviso (la pulsación solo se reconoce). Opcionalmente,toolConfirm.answeredNotice: trueresponde tambiénai.tools.confirmAnswered(“Respuesta recibida: Sí.” / “Answer received: Yes.”) cuando una pulsación responde; va apagado por defecto porque esos canales ya muestran la elección como mensaje del usuario. Ninguno de los dos avisos sale donde los botones se quitan editando ni donde la confirmación es un menú de texto numerado (Baileys, Evolution, WPPConnect, Twilio, email, GoHighLevel): ahí una respuesta posterior es solo texto. - Los canales sin botones nativos (WhatsApp vía Baileys, Evolution, WPPConnect,
Twilio, SMS, email, GoHighLevel…) mantienen la pregunta escrita con la
coletilla
(sí/no); ahí una respuesta posterior es texto normal y no hay aviso de confirmación caducada. toolConfirm.buttons: falsepide siempre la respuesta escrita. ElconfirmMessagede una herramienta se usa tal cual en los dos modos. Unbot.locale()que cambiaai.tools.confirmse reutiliza para los botones si no traduces tambiénai.tools.confirmButtons.
ai.tools.confirm con
el title de la herramienta y una línea corta por argumento; un único
argumento lista muestra una línea por elemento, y los elementos con cantidad
se leen como cantidad × producto:
ctx.lang es es (por ejemplo
con lang: 'es' en el bot). Por defecto sale en inglés (About to: Create order … Do you confirm? (yes/no)), como todos los textos internos (ver
i18n). Un chat en un idioma sin textos
internos (por ejemplo fr) recibe el idioma por defecto del bot si es es o
en, y si no, inglés. Cámbialo por idioma con
bot.locale('es', { ai: { tools: { confirm: '¿Ejecuto {tool}?{args}' } } })
({tool} es el título y {args} un salto de línea más las líneas • , o
vacío), o usa confirmMessage en la herramienta.
Valores legibles y totales
Claves de catálogo comopan_de_bono le sirven al modelo, no al usuario. Dos
opciones de la herramienta mejoran la pregunta por defecto sin escribirla tú:
labelstraduce valores de argumentos a etiquetas humanas, por clave de argumento (también dentro de los ítems de una lista). Los valores sin etiqueta se muestran tal cual; el modelo y el handler siguen recibiendo las claves crudas.confirmLines(args)devuelve las líneas que se muestran en lugar de los argumentos, dentro del mismo envoltorio localizado. Úsala para mostrar lo que calculó tu código (precios, total), así el usuario nunca confirma un número que se inventó el modelo.confirmMessagesigue mandando sobre ambas.
argLines(args, ctx, labels) y
summarizeValue(valor, ctx, depth, labels) están exportadas.
Dentro de tu propia conversación pasa confirmWith(c). Tampoco bloquea la
cola y, en un canal con botones nativos, pregunta con los mismos botones Sí/No
que el autoresponder (misma semántica: las respuestas escritas siguen
funcionando, la pulsación se reconoce, los botones se quitan editando donde se
puede y una pulsación posterior se reconoce y se ignora en vez de responder tu
siguiente c.ask()). En los demás canales, o con buttons: false, pregunta un
sí/no escrito con c.ask(). Los botones van al chat del ctx que pasas a
replyInChat (o de la opción ctx).
confirmWith(c, { yes, no, timeout, buttons, ctx, answeredNotice }) clasifica
la respuesta igual que el
autoresponder: 'approved', 'denied' o { reply }. Un error
de control de la conversación (timeout, cancelación, conversación
reemplazada) sale del bucle como siempre, para que lo gestione el runtime.
Sin confirmador (por ejemplo un ctx.tools.ai.ask() suelto), las
herramientas confirm: true se deniegan y se le dice al modelo que no había
confirmación humana disponible.
También puedes escribir tu propio ToolConfirmer: una función async que
recibe { tool, callId, args, description, title, prompt, previous? } y
resuelve 'approved', 'denied', { reply: string } (la respuesta libre del
usuario, que se le pasa al modelo) o 'cancelled'. 'cancelled' aborta el
turno entero: el bucle lanza AIToolCancelledError (con el nombre en tool).
Efectos secundarios repetidos
A veces el modelo vuelve a llamar por su cuenta a una herramienta con efectos (por ejemplo para “corregir” el total de un pedido después de que el usuario dijo “no, nada más”). El cliente recuerda por chat las ejecuciones correctas de las herramientasconfirm: true y dedupe (en memoria, 30 minutos; /reset y
ctx.tools.ai.clearToolHistory(chatKey) las olvidan) y las usa de tres formas:
- Aviso en la confirmación: si una herramienta
confirm: trueya se ejecutó en el chat con los mismos argumentos, la pregunta empieza con una línea localizada (ai.tools.confirmRepeat):Ojo: ya hice «Crear pedido» hace un momento (resultado: …). Si confirmas, se hará otra vez.El confirmador la recibe también comoprevious. Una llamada con argumentos distintos (cambiar 3 → 2 unidades) es una acción nueva: sin aviso niprevious. El resumen del resultado omite los campos demasiado anidados para leerse (listas de objetos). - Prompt de sistema: los turnos siguientes listan las acciones ya hechas
(
ai.tools.doneNote) y le piden al modelo no repetirlas salvo que el usuario lo pida explícitamente. dedupe(opcional por herramienta): una llamada con los mismos argumentos validados que una ejecución correcta dentro de la ventana no se ejecuta ni se confirma; el modelo recibe{ "executed": false, "reason": "duplicate", "previous": { "result", "secondsAgo" } }.dedupe: 'args'usa una ventana de 10 minutos;dedupe: { windowMs: 60_000 }fija la tuya. También sirve para herramientas sinconfirm.
error no vacío (por ejemplo
{ error: 'No existe el pedido P-123' }) no hizo nada, así que no se recuerda:
ni aviso de repetición, ni línea de “ya ejecutadas”, ni bloqueo de dedupe.
toolConfirm.systemNote: false también apaga la lista del prompt de sistema.
Buenas prácticas para herramientas con efectos
Estas reglas salen de pruebas en vivo con modelos reales en WhatsApp. Sin ellas, el modelo calcula mal el total de un pedido en texto libre, le muestra claves de catálogo al cliente o dice que “reemplazó” un pedido sin tener ninguna herramienta para hacerlo.- Nunca dejes que el modelo calcule dinero. Precios, subtotales y totales
se calculan en tu código: ofrece una herramienta de cotización (sin
confirm, sin efectos) para el “¿cuánto cuesta…?”, vuelve a calcular el total en el handler que guarda el pedido y muéstralo en la confirmación conconfirmLines(oconfirmMessage). Devuelve el total calculado en el resultado para que el modelo lo repita en vez de calcularlo. - Valida los valores de catálogo con
enum. Claves de producto, tamaños, sedes…: los argumentos se validan antes de la confirmación, así que un producto inventado vuelve al modelo como error y el usuario nunca confirma un pedido imposible. Acompaña las claves conlabels(o nombres enconfirmLines) para que el usuario vea “Pan de bono” y nopan_de_bono. - Cada herramienta de crear trae las de cambiar, cancelar y listar. Si el
modelo solo puede crear, “corrige” un pedido creando otro o dice que cambió
algo que no podía cambiar. Una herramienta de listado (
mis_pedidos) le da los ids para cambiar o cancelar. - Usa
dedupeen las de crear (dedupe: 'args'), para que un pedido idéntico no se registre dos veces seguidas cuando el modelo vuelve a llamar la herramienta por su cuenta. - Devuelve un
{ error }explícito para ids desconocidos ({ error: 'No existe un pedido activo P-123. Consulta mis_pedidos.' }), nunca unokonullsilencioso. El modelo lo lee y se lo dice al usuario; la ejecución no se recuerda como hecha. - Mantén
confirm: trueen todo lo que tenga efectos (crear, cambiar, cancelar, pagar, reservar). Las de solo lectura (menú, cotizar, listar) no lo necesitan. - Apóyate en la nota de sistema, no la pelees. Con un confirmador, el
prompt de sistema le dice al modelo qué herramientas confirma el sistema,
que no pregunte “¿te lo registro?” él mismo, que no mencione el proceso de
confirmación y que nunca diga que hizo, cambió, reemplazó o canceló algo que
ningún resultado de herramienta confirmó (y que lo diga con claridad cuando
no tiene herramienta para lo que le piden). Déjala activa
(
toolConfirm.systemNote) salvo que la reemplaces por un texto equivalente.
ai-full de create-botmesh trae las mismas
herramientas, con los pedidos guardados en ctx.user.
Hay una versión completa y ejecutable (en inglés) en el
ejemplo del asistente de panadería (examples/bakery-ai). Corre el
mismo bot en WhatsApp con Baileys (QR) o con Twilio (webhook + @botmesh/server)
e incluye configuración tipada por variables de entorno y tests con MockAIProvider.
Eventos de auditoría
Cada paso del bucle emite unAIToolEvent, tanto a onToolEvent como en el
bot como 'ai.tool':
Cada evento lleva
at (ms epoch), step (empieza en 1) y, cuando aplica,
tool, callId, chatKey, args, durationMs y error.
Proveedores
Las llamadas en streaming (
stream(), streamReplyInChat()) nunca envían
herramientas.
Usar herramientas desde tu código
Contools configuradas, ctx.tools.ai.ask() y ctx.tools.ai.replyInChat()
ejecutan el bucle solos. Ambos aceptan { ctx, confirm, signal, chatKey }.
Para control total, runTools() devuelve el resultado del bucle:
runToolLoop() es el mismo bucle sin AIClient: le pasas las herramientas,
una función generate y los mensajes.
Testing
MockAIProvider puede simular llamadas a herramientas: devuelve
{ toolCalls } desde reply para que el “modelo” llame a una herramienta, y
texto plano para terminar. Construye un AIClient con tus herramientas y
comprueba el resultado:
mock.calls guarda cada petición que envió el cliente (con sus messages y
tools), así que también puedes comprobar qué vio el modelo.
Siguientes pasos
- Plugin de IA — proveedores y la referencia completa de
aiPlugin - Memoria y RAG — hechos por usuario y respuestas con tus documentos
- Flujos conversacionales — el runtime que aloja las confirmaciones
- Ejemplo: asistente de panadería — estas prácticas en un bot de WhatsApp ejecutable (Baileys o Twilio)