Skip to content
astorlm
← Map

Level 6

Hooks

Events let you watch the loop. Hooks let you change it. A hook is a function of yours that the loop calls at a fixed point, and whatever it returns, the loop obeys.
1/26 Bandoneón folds:
  • user
  • assistant
  • tool_result
  • tool_result (error)
A company travel assistant. The loop is a closed circuit, and every hook point is a booth by the track. This run staffs two: one checks each tool call before it runs, the other checks each result before the model reads it.

EventBus

The problem

Your travel assistant works. Then the company adds a rule: no first-class tickets without a manager’s approval. And legal adds another: the passenger’s ID number must never reach the model.

Neither rule is about the model. The model can be told, but a prompt is a request, not a lock. Both rules are about what the loop does: which tool calls it runs, and what it puts into the history. If the loop gives you no way in, the only option left is to copy its code and edit it. That’s Ironclad, the sealed loop.

Events don’t help either. An event tells you that book_ticket is about to run. By the time your listener gets it, nothing you do there can stop it.

The solution

The loop calls your functions at fixed points of every turn, and uses what they return. Five points cover almost everything:

  • beforeTurn

    At the start of every turn.

    Check a budget, log the turn, stop a run that has gone on too long.

  • beforeProviderCall

    Right before the request goes to the model.

    Change what gets sent: trim old messages, add today’s date, hide a tool this turn.

  • beforeToolExecution

    After the model asks for a tool, before it runs.

    Let it through, refuse it (the model gets your reason instead), or answer with a canned result.

  • afterToolExecution

    After the tool runs, before the result joins the history.

    Rewrite what the model will read: hide personal data, shorten a huge output.

  • afterTurn

    Once the model’s reply and any tool results are in.

    Save progress, update a dashboard, count the cost.

The rule of thumb: events watch, hooks change. Use an event when you only want to know what happened. Use a hook when you need to decide what happens.

The cast

Same cast as always, on a model railway this time.

The circuit the loop
A closed ring of track that only runs one way. Every lap is a turn: past the Oracle, past the tools, round again.
Astor the loop's runner
Pumps the handcar around the circuit, with the bandoneón of messages on his back.
The booths hooks
One per hook point. An empty booth does nothing. A staffed one stops the handcar, checks what it carries, and can lower its barrier or stamp over the cargo. This run staffs two: beforeToolExecution and afterToolExecution.
The stands EventBus
Three spectators who write down everything that passes. They see it all, and can touch none of it.
The platforms tools
find_trains and book_ticket, on the far curve.

In the EventBus panel, the hook and code lines mark your own functions running: your hooks and your tools. astorlm doesn’t emit events for them. Notice where they fall: tool_execution_end comes after afterToolExecution, so it already carries the stamped text.

The code

With astorlm: Pass a hooks object to the agent. beforeToolExecution returns { authorize: false } to refuse a call, and afterToolExecution returns the text the model will read.

From scratch: The loop from level 2, with a call to each of the five hooks. A hook nobody set is just skipped.

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)

What to watch

  • Say why when you refuse. The refusal goes back to the model as an error result. "Blocked by policy: book tourist instead" gets you a tourist ticket. A bare "denied" gets you the same call again.
  • Hooks run on every call, so keep them fast. A hook that queries a database adds that delay to every tool and every turn.
  • A hook that throws takes the run down. The loop catches errors from your tools, not from your hooks. Wrap anything that can fail.
  • Don’t use a hook to watch. If you only log, listen to events. Keep hooks for the times you need to change something.