Pular para o conteúdo
astorlm
Idioma: Português
← Mapa

Nível 6

Hooks

Eventos deixam você observar o loop. Hooks deixam você mudá-lo. Um hook é uma função sua que o loop chama num ponto fixo, e o que quer que ela devolva, o loop obedece.
1/26 Dobras do bandoneón:
  • user
  • assistant
  • tool_result
  • tool_result (erro)
Um assistente de viagens corporativo. O loop é um circuito fechado, e cada ponto de hook é uma cabine ao lado do trilho. Esta execução tem duas com gente: uma confere cada chamada de ferramenta antes de ela rodar, a outra confere cada resultado antes de o modelo ler.

EventBus

O problema

O seu assistente de viagens funciona. Aí a empresa adiciona uma regra: nada de passagens de primeira classe sem a aprovação de um gestor. E o jurídico adiciona outra: o número de documento do passageiro nunca pode chegar ao modelo.

Nenhuma das duas regras é sobre o modelo. Dá para avisar o modelo, mas um prompt é um pedido, não uma tranca. As duas regras são sobre o que o loop faz: quais chamadas de ferramenta ele executa e o que ele coloca no histórico. Se o loop não te dá um jeito de entrar, a única opção que sobra é copiar o código dele e editar. Esse é o Ironclad, o loop lacrado.

Eventos também não ajudam. Um evento te avisa que book_ticket está prestes a rodar. Quando o seu listener o recebe, nada do que você fizer ali consegue impedir.

A solução

O loop chama as suas funções em pontos fixos de cada turno, e usa o que elas devolvem. Cinco pontos cobrem quase tudo:

  • beforeTurn

    No início de cada turno.

    Conferir um orçamento, registrar o turno, parar uma execução que já durou demais.

  • beforeProviderCall

    Logo antes de a requisição ir para o modelo.

    Mudar o que é enviado: cortar mensagens antigas, adicionar a data de hoje, esconder uma ferramenta neste turno.

  • beforeToolExecution

    Depois que o modelo pede uma ferramenta, antes de ela rodar.

    Deixar passar, recusar (o modelo recebe o seu motivo no lugar) ou responder com um resultado pronto.

  • afterToolExecution

    Depois que a ferramenta roda, antes de o resultado entrar no histórico.

    Reescrever o que o modelo vai ler: esconder dados pessoais, encurtar uma saída enorme.

  • afterTurn

    Quando a resposta do modelo e os resultados de ferramentas já chegaram.

    Salvar o progresso, atualizar um painel, contar o custo.

A regra prática: eventos observam, hooks mudam. Use um evento quando você só quer saber o que aconteceu. Use um hook quando precisa decidir o que acontece.

O elenco

O mesmo elenco de sempre, desta vez num ferromodelo.

O circuito o loop
Um anel fechado de trilhos que só anda num sentido. Cada volta é um turno: passa pelo Oráculo, passa pelas ferramentas, e de novo.
Astor quem percorre o loop
Bombeia o trole pelo circuito, com o bandoneón de mensagens nas costas.
As cabines hooks
Uma por ponto de hook. Uma cabine vazia não faz nada. Uma com gente para o trole, confere o que ele leva e pode abaixar a cancela ou carimbar por cima da carga. Esta execução tem duas com gente: beforeToolExecution e afterToolExecution.
A arquibancada EventBus
Três espectadores que anotam tudo o que passa. Eles veem tudo, e não podem tocar em nada.
As plataformas ferramentas
find_trains e book_ticket, na curva do fundo.

No painel EventBus, as linhas hook e code marcam as suas próprias funções rodando: os seus hooks e as suas ferramentas. O astorlm não emite eventos para elas. Repare onde elas caem: tool_execution_end vem depois de afterToolExecution, então já carrega o texto carimbado.

O código

Com astorlm: Passe um objeto hooks para o agente. beforeToolExecution devolve { authorize: false } para recusar uma chamada, e afterToolExecution devolve o texto que o modelo vai ler.

Do zero: O loop do nível 2, com uma chamada a cada um dos cinco hooks. Um hook que ninguém definiu é simplesmente pulado.

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)

O que observar

  • Diga por que ao recusar. A recusa volta para o modelo como um resultado com erro. "Bloqueado pela política: reserve turística no lugar" te rende uma passagem turística. Um "negado" seco te devolve a mesma chamada outra vez.
  • Hooks rodam em toda chamada, então mantenha-os rápidos. Um hook que consulta um banco de dados soma esse atraso a cada ferramenta e a cada turno.
  • Um hook que lança uma exceção derruba a execução. O loop captura erros das suas ferramentas, não dos seus hooks. Embrulhe tudo o que pode falhar.
  • Não use um hook para observar. Se você só registra, escute eventos. Guarde os hooks para quando precisar mudar alguma coisa.