Hooks

Run your own code at every step of the agent loop to guard, steer or watch it.

Hooks let your code take part in a run: block a risky tool call, add context before the model is asked, redact a tool result, or send the agent back to work when it stops too early. Observers watch every recorded event without being able to change anything.

Both come from extension(), the one way to package reusable behavior: instructions, tools, hooks and observers together.

Write an extension

const guard = extension({
  name: "guard",
  instructions: "Only email people at example.com.",
  hooks: {
    beforeTool: async (call) => {
      const to = call.input["to"];
      if (call.name === "send_email" && typeof to === "string" && !to.endsWith("@example.com"))
        return { decision: "deny", reason: "external recipients are not allowed" };
      return { decision: "allow" };
    },
    sessionStart: async () => [`Today is ${new Date().toDateString()}.`],
  },
  on: {
    tool_result: async (event) => {
      console.log("tool finished:", event.type);
    },
  },
  hookTimeoutMs: 2000,
});

const assistant = agent({
  model: scriptedModel({
    responses: [use("send_email", { to: "[email protected]", body: "hi" }, "c1"), say("I can't email that address.")],
  }),
  tools: [sendEmail],
  extensions: [guard],
  permissions: { allow: ["send_email"] },
});

In Python, the decision types come from threads.hooks.types (Allow, Deny, Ask, Proceed, ...) and the event types from threads.log.

When the model tries to email [email protected], the hook denies the call and the email is never sent. The model gets a denied tool result and answers accordingly.

Options

namestringrequired

Unique per agent: lowercase letters, digits and _. The extension's tools are named <name>__<tool>.

instructionsstring

Added to the system prompt after the agent's own instructions, in the order extensions are listed.

toolsTool[]

Tools this extension brings. In Python they receive no deps.

hooksHooks

Any of the hooks below.

onRecord<string, handler>

Observers keyed by event type, or "*" for every event.

setup() => Promise<void>

Runs once before the first run. If it throws, setup fails with a ConfigError.

hookTimeoutMsnumberdefault 5000

Time limit for each hook call. hook_timeout_ms in Python.

Hook points

Every hook is optional, awaited and time-limited. Each gets the run context last (ctx: thread id, principal and, in TypeScript, your deps; Python hooks get deps as None).

TypeScriptPythonWhenReturns
sessionStartsession_startA run starts, resumes or forks, or history was summarizedText to add as context
sessionEndsession_endA run endsNothing
beforeInputbefore_inputBefore new user input is acceptedallow (optional injections) or deny
beforeModelbefore_modelBefore each model requestproceed (optional injections) or deny
afterModelafter_modelAfter each model responseproceed, deny, guide (with text) or retry (with reason)
beforeToolbefore_toolBefore every tool call is decided, even one the policy deniesallow, deny or ask (optional rule)
permissionRequestpermission_requestA call would need approvalallow, deny or ask
permissionDeniedpermission_deniedA call was deniedNothing
afterToolafter_toolAfter a tool call ranNotes to record in the log
beforeToolResultbefore_tool_resultBefore the model sees the result of a call that ranproceed, redact (with spans) or deny
afterToolBatchafter_tool_batchAfter all calls of one response ranText to add as context
beforeCompactbefore_compactBefore older history is summarizedproceed, deny or guide
afterCompactafter_compactAfter history was summarizedText to add as context
onStopon_stopThe agent is about to finish its turnstop, or continue with a reason to keep it working
onStopFailureon_stop_failureA run ended with an errorNothing
subagentStartsubagent_startBefore a subagent startsallow or deny
subagentStopsubagent_stopA subagent finishedstop, or continue with a reason to send it back
beforeModelSwitchbefore_model_switchBefore an automatic model switch: a fallback, or the revert at the next input. setModel doesn't run itallow or deny
afterModelSwitchafter_model_switchAfter the model changedNothing
notificationnotificationThe run parked or scheduled a retry (TypeScript also: a budget ran out, a subagent finished)Nothing

A deny always carries a reason, recorded in the log. When several extensions define beforeTool, the strictest answer wins; TypeScript stops at the first deny, while Python calls every extension. redact spans are UTF-8 byte offsets into the result's first text part (the preview when it has no content); an empty list, an empty span, or a span outside that text or splitting a character fails the hook and clears the whole result. For a denied tool call, TypeScript shows the reason to the model as the result; Python currently shows denied by policy. Text a hook adds is shown to the model as context from the hook.

When a hook fails

A hook fails when it throws, times out or returns something that hook can't return.

  • Hooks that decide (the ones that return allow/deny/proceed/stop and similar): a failure denies. A broken guard never lets something through.
  • Hooks that add context (sessionStart, afterToolBatch, afterCompact): a failure stops that step. A failed sessionStart refuses the run's input.
  • Hooks that return nothing: a failure is recorded and ignored.

Every decision a hook makes is recorded in the thread's log, next to the call it was about.

Observers

Observers in on get each event after it is written to the log, in order, in the background. They never slow the run down and can't change it. Each observer keeps its place in the log: if a handler throws or the process stops, delivery picks up from the first event it hadn't finished, on the next event or the next run.

Use observers for metrics, notifications and syncing to other systems. Use hooks when you need to change what happens.

Hooks and extensions are trusted code you run on your host, not a sandbox. The hooks an agent has are fixed when its thread starts; the agent can't add or change them.

Edit on GitHub

On this page