Skip to content

Hooks

Monitor, transform, and control agent execution with three hook categories:

  • Observers - Read-only logging/metrics (run in parallel)
  • Interceptors - Synchronous transformations (run in sequence)
  • Controllers - Async lifecycle control (can short-circuit)

HookPresets provide ready-to-use hook configurations for common monitoring and debugging tasks.

import { HookPresets } from 'llmist';
// Basic logging
.withHooks(HookPresets.logging())
// Verbose logging with parameters/results
.withHooks(HookPresets.logging({ verbose: true }))
// Full monitoring suite (logging + timing + tokens + errors)
.withHooks(HookPresets.monitoring())
// Combine presets
.withHooks(HookPresets.merge(
HookPresets.logging(),
HookPresets.timing(),
HookPresets.tokenTracking(),
))
PresetDescription
logging(options?)Logs LLM calls and gadget execution
timing()Measures execution time
tokenTracking()Tracks cumulative token usage
errorLogging()Logs detailed error information
silent()No output (for testing)
monitoring(options?)All-in-one: logging + timing + tokens + errors
merge(...hookSets)Combines multiple hook configurations

For logging, metrics, analytics:

.withHooks({
observers: {
onLLMCallStart: async (ctx) => {
console.log(`Iteration ${ctx.iteration} starting`);
},
onLLMCallComplete: async (ctx) => {
console.log(`Tokens: ${ctx.usage?.totalTokens}`);
},
onLLMCallError: async (ctx) => {
console.error(`Error: ${ctx.error.message}`);
},
onGadgetExecutionStart: async (ctx) => {
console.log(`Executing ${ctx.gadgetName}`);
},
onGadgetExecutionComplete: async (ctx) => {
console.log(`${ctx.gadgetName} took ${ctx.executionTimeMs}ms`);
},
},
})

Synchronous transformations:

.withHooks({
interceptors: {
// Transform text chunks before display
interceptTextChunk: (chunk, ctx) => {
return chunk.toUpperCase(); // or null to suppress
},
// Transform gadget parameters before execution
interceptGadgetParameters: (params, ctx) => {
return { ...params, modified: true };
},
// Transform gadget result before LLM sees it
interceptGadgetResult: (result, ctx) => {
return `Result: ${result}`;
},
},
})

Async control with short-circuit capability:

.withHooks({
controllers: {
// Before LLM call - can skip or modify
beforeLLMCall: async (ctx) => {
if (shouldCache(ctx)) {
return { action: 'skip', syntheticResponse: cachedResponse };
}
return { action: 'proceed', modifiedOptions: { temperature: 0.5 } };
},
// After LLM call - can modify or append
afterLLMCall: async (ctx) => {
return { action: 'continue' };
},
// Error recovery
afterLLMError: async (ctx) => {
if (isRetryable(ctx.error)) {
return { action: 'recover', fallbackResponse: 'Fallback text' };
}
return { action: 'rethrow' };
},
// Before gadget - can skip
beforeGadgetExecution: async (ctx) => {
if (shouldMock(ctx.gadgetName)) {
return { action: 'skip', syntheticResult: 'mocked' };
}
return { action: 'proceed' };
},
},
})
const isDev = process.env.NODE_ENV === 'development';
const hooks = isDev
? HookPresets.monitoring({ verbose: true })
: HookPresets.merge(
HookPresets.errorLogging(),
HookPresets.tokenTracking()
);
await LLMist.createAgent()
.withHooks(hooks)
.ask("Your prompt");
const BUDGET_TOKENS = 10_000;
let totalTokens = 0;
await LLMist.createAgent()
.withHooks(HookPresets.merge(
HookPresets.tokenTracking(),
{
observers: {
onLLMCallComplete: async (ctx) => {
totalTokens += ctx.usage?.totalTokens ?? 0;
console.log(`💰 Tokens used: ${totalTokens}/${BUDGET_TOKENS}`);
},
},
}
))
.ask("Your prompt");
describe('Agent tests', () => {
it('should calculate floppy disk requirements', async () => {
const result = await LLMist.createAgent()
.withHooks(HookPresets.silent())
.withGadgets(FloppyDisk)
.askAndCollect("How many floppies for a 10MB file?");
expect(result).toContain("7");
});
});

When using subagent gadgets, check ctx.subagentContext to distinguish events:

observers: {
onLLMCallStart: (ctx) => {
if (ctx.subagentContext) {
console.log(`↳ Subagent LLM call at depth ${ctx.subagentContext.depth}`);
} else {
console.log(`Main agent LLM call #${ctx.iteration}`);
}
},
}

Combine multiple hook configurations:

const myHooks = HookPresets.merge(
HookPresets.logging({ verbose: true }),
HookPresets.timing(),
{
observers: {
onLLMCallComplete: async (ctx) => {
await saveToDatabase(ctx.usage);
},
},
},
);
.withHooks(myHooks)

Merge behavior:

  • Observers: Composed (all handlers run)
  • Interceptors: Last one wins
  • Controllers: Last one wins
HookContext Properties
onLLMCallStartiteration, options, logger, subagentContext?
onLLMCallCompleteiteration, options, finishReason, usage, rawResponse, finalMessage, logger
onLLMCallErroriteration, options, error, recovered, logger
onGadgetExecutionStartiteration, gadgetName, invocationId, parameters, logger
onGadgetExecutionCompleteiteration, gadgetName, invocationId, parameters, originalResult, finalResult, error, executionTimeMs, breaksLoop, logger