Saltar al contenido
astorlm
Idioma: Español
← Mapa

Nivel 6

Hooks

Los eventos te dejan mirar el bucle. Los hooks te dejan cambiarlo. Un hook es una función tuya que el bucle llama en un punto fijo, y lo que devuelva, el bucle lo obedece.
1/26 Pliegues del bandoneón:
  • user
  • assistant
  • tool_result
  • tool_result (error)
Un asistente de viajes de empresa. El bucle es un circuito cerrado, y cada punto de hook es una cabina junto a la vía. Esta ejecución tiene dos con personal: una revisa cada llamada a herramienta antes de que se ejecute, la otra revisa cada resultado antes de que lo lea el modelo.

EventBus

El problema

Tu asistente de viajes funciona. Entonces la empresa agrega una regla: nada de pasajes en primera clase sin la aprobación de un gerente. Y el área legal agrega otra: el número de documento del pasajero nunca debe llegar al modelo.

Ninguna de las dos reglas es sobre el modelo. Al modelo se le puede decir, pero un prompt es un pedido, no un candado. Las dos reglas son sobre lo que hace el bucle: qué llamadas a herramientas ejecuta y qué pone en el historial. Si el bucle no te da forma de entrar, la única opción que queda es copiar su código y editarlo. Ese es Ironclad, el bucle sellado.

Los eventos tampoco ayudan. Un evento te avisa que book_ticket está por ejecutarse. Para cuando tu listener lo recibe, nada de lo que hagas ahí puede detenerlo.

La solución

El bucle llama a tus funciones en puntos fijos de cada turno, y usa lo que devuelven. Cinco puntos cubren casi todo:

  • beforeTurn

    Al principio de cada turno.

    Revisar un presupuesto, registrar el turno, detener una ejecución que ya duró demasiado.

  • beforeProviderCall

    Justo antes de que el pedido salga hacia el modelo.

    Cambiar lo que se envía: recortar mensajes viejos, agregar la fecha de hoy, ocultar una herramienta en este turno.

  • beforeToolExecution

    Después de que el modelo pide una herramienta, antes de que se ejecute.

    Dejarla pasar, rechazarla (el modelo recibe tu motivo en su lugar) o responder con un resultado predefinido.

  • afterToolExecution

    Después de que la herramienta se ejecuta, antes de que el resultado entre en el historial.

    Reescribir lo que va a leer el modelo: ocultar datos personales, acortar una salida enorme.

  • afterTurn

    Cuando ya llegaron la respuesta del modelo y los resultados de herramientas.

    Guardar el progreso, actualizar un panel, contar el costo.

La regla práctica: los eventos miran, los hooks cambian. Usa un evento cuando solo quieres saber qué pasó. Usa un hook cuando necesitas decidir qué pasa.

El elenco

El mismo elenco de siempre, esta vez en una maqueta de tren.

El circuito el bucle
Un anillo cerrado de vías que solo va en un sentido. Cada vuelta es un turno: pasa por el Oráculo, pasa por las herramientas, y otra vez.
Astor quien recorre el bucle
Bombea el carrito de mano por el circuito, con el bandoneón de mensajes a la espalda.
Las cabinas hooks
Una por punto de hook. Una cabina vacía no hace nada. Una con personal detiene el carrito, revisa lo que lleva, y puede bajar su barrera o tapar la carga con un sello. Esta ejecución tiene dos con personal: beforeToolExecution y afterToolExecution.
La tribuna EventBus
Tres espectadores que anotan todo lo que pasa. Lo ven todo, y no pueden tocar nada.
Los andenes herramientas
find_trains y book_ticket, en la curva del fondo.

En el panel EventBus, las líneas hook y code marcan tus propias funciones en ejecución: tus hooks y tus herramientas. astorlm no emite eventos para ellas. Fíjate dónde caen: tool_execution_end llega después de afterToolExecution, así que ya trae el texto sellado.

El código

Con astorlm: Pásale al agente un objeto hooks. beforeToolExecution devuelve { authorize: false } para rechazar una llamada, y afterToolExecution devuelve el texto que va a leer el modelo.

Desde cero: El bucle del nivel 2, con una llamada a cada uno de los cinco hooks. Un hook que nadie definió simplemente se omite.

import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'

const findTrains = tool({
  name: 'find_trains',
  description: 'List the trains to a destination on a date, with the fare for each class.',
  schema: z.object({ to: z.string(), date: z.string() }),
  execute: async ({ to, date }) => searchTimetable(to, date), // your code
})

const bookTicket = tool({
  name: 'book_ticket',
  description: 'Book one seat on a train for the employee who is asking.',
  schema: z.object({ train: z.number().int(), seat_class: z.enum(['first', 'tourist']) }),
  execute: async ({ train, seat_class }) => reserveSeat(train, seat_class), // your code
})

const agent = await createLocalAgent({
  // Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
  provider: new OpenAIProvider({
    baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
    model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
    apiKey: 'YOUR_API_KEY', // local servers usually ignore it
  }),
  tools: [findTrains, bookTicket],
  maxTurns: 10,
  hooks: {
    // The first booth: runs before every tool call, and decides whether it runs at all.
    beforeToolExecution: async ({ toolName, input }) => {
      const { seat_class } = input as { seat_class?: string }
      if (toolName === 'book_ticket' && seat_class === 'first') {
        // The tool never runs. The model reads this text as an error result instead.
        return { authorize: false, mockResult: 'Blocked by policy: first class needs a manager’s approval. Book tourist instead.' }
      }
      return { authorize: true }
    },
    // The second booth: runs after every tool call. What you return is what the model reads.
    afterToolExecution: async ({ output }) => output.replace(/DNI [\d.]+/g, 'DNI ***'),
  },
})

// Events only watch. By the time this fires, the hook has already stamped over the DNI.
agent.on('tool-end', ({ name, output, isError }) => console.log(name, isError ? 'refused:' : 'ok:', output))

const last = await agent.run('Book me the most comfortable seat to Mar del Plata on Friday.')
console.log(last.content)

Qué vigilar

  • Di por qué cuando rechazas. El rechazo vuelve al modelo como un resultado con error. "Bloqueado por política: reserva turista en su lugar" te consigue un pasaje en turista. Un "denegado" a secas te devuelve la misma llamada otra vez.
  • Los hooks corren en cada llamada, así que mantenlos rápidos. Un hook que consulta una base de datos suma esa demora a cada herramienta y a cada turno.
  • Un hook que lanza una excepción tira abajo la ejecución. El bucle atrapa los errores de tus herramientas, no los de tus hooks. Envuelve todo lo que pueda fallar.
  • No uses un hook para mirar. Si solo registras, escucha eventos. Guarda los hooks para cuando necesitas cambiar algo.