Agent Hooks
Hooks intercept specific points in the agent execution pipeline. Use them to observe or modify behavior during agent operations: before a request starts, when preparing messages for the LLM, before and after tool execution, or after completion.
Defining Hooks​
Define hooks using the createHooks helper function. Pass the resulting object to the Agent constructor, or to individual method calls like generateText and streamText.
import {
Agent,
createHooks,
messageHelpers,
type AgentTool,
type AgentOperationOutput,
type VoltAgentError,
type OnStartHookArgs,
type OnEndHookArgs,
type OnPrepareMessagesHookArgs,
type OnPrepareModelMessagesHookArgs,
type OnToolStartHookArgs,
type OnToolEndHookArgs,
type OnHandoffHookArgs,
} from "@voltagent/core";
import { openai } from "@ai-sdk/openai";
// Define a collection of hooks using the helper
const myAgentHooks = createHooks({
/**
* Called before the agent starts processing a request.
*/
onStart: async (args: OnStartHookArgs) => {
const { agent, context } = args;
console.log(`[Hook] Agent ${agent.name} starting interaction at ${new Date().toISOString()}`);
console.log(`[Hook] Operation ID: ${context.operationId}`);
},
/**
* Called after VoltAgent sanitizes UI messages but before the LLM receives them.
* `rawMessages` contains the unsanitized list for inspection or metadata recovery.
*/
onPrepareMessages: async (args: OnPrepareMessagesHookArgs) => {
const { messages, rawMessages, context } = args;
console.log(`Preparing ${messages.length} sanitized messages for LLM`);
// Add timestamp to each message
const timestamp = new Date().toLocaleTimeString();
const enhanced = messages.map((msg) => messageHelpers.addTimestampToMessage(msg, timestamp));
if (rawMessages) {
// Access raw message structure for audit logging
console.debug(`First raw message parts:`, rawMessages[0]?.parts);
}
return { messages: enhanced };
},
/**
* Called after UI messages are converted into provider-specific ModelMessage objects.
*/
onPrepareModelMessages: async (args: OnPrepareModelMessagesHookArgs) => {
const { modelMessages, uiMessages } = args;
console.log(`Model payload contains ${modelMessages.length} messages`);
// Inject a system message if none exists
if (!modelMessages.some((msg) => msg.role === "system")) {
return {
modelMessages: [
{
role: "system",
content: [{ type: "text", text: "Operate within safety budget" }],
},
...modelMessages,
],
};
}
return {};
},
/**
* Called after the agent completes a request (success or failure).
*/
onEnd: async (args: OnEndHookArgs) => {
const { agent, output, error, context } = args;
if (error) {
console.error(`[Hook] Agent ${agent.name} finished with error:`, error.message);
console.error(`[Hook] Error Details:`, JSON.stringify(error, null, 2));
} else if (output) {
console.log(`[Hook] Agent ${agent.name} finished successfully.`);
// Log usage or inspect output type
if ("usage" in output && output.usage) {
console.log(`[Hook] Token Usage: ${output.usage.totalTokens}`);
}
if ("text" in output && output.text) {
console.log(`[Hook] Final text length: ${output.text.length}`);
}
if ("object" in output && output.object) {
console.log(`[Hook] Final object keys: ${Object.keys(output.object).join(", ")}`);
}
}
},
/**
* Called before a tool executes.
*/
onToolStart: async (args: OnToolStartHookArgs) => {
const { agent, tool, context, args: toolArgs } = args;
console.log(`[Hook] Agent ${agent.name} starting tool: ${tool.name}`);
console.log(`[Hook] Tool arguments:`, toolArgs);
},
/**
* Called after a tool completes or throws an error.
*/
onToolEnd: async (args: OnToolEndHookArgs) => {
const { agent, tool, output, error, context } = args;
if (error) {
console.error(`[Hook] Tool ${tool.name} failed:`, error.message);
console.error(`[Hook] Tool Error Details:`, JSON.stringify(error, null, 2));
} else {
console.log(`[Hook] Tool ${tool.name} completed with result:`, output);
}
},
/**
* Called when a task is handed off from a source agent to this agent.
*/
onHandoff: async (args: OnHandoffHookArgs) => {
const { agent, sourceAgent } = args;
console.log(`[Hook] Task handed off from ${sourceAgent.name} to ${agent.name}`);
},
});
// Pass hooks to the Agent constructor
const agentWithHooks = new Agent({
name: "My Agent with Hooks",
instructions: "An assistant demonstrating hooks",
model: openai("gpt-4o"),
hooks: myAgentHooks,
});
// Or define hooks inline
const agentWithInlineHooks = new Agent({
name: "Inline Hooks Agent",
instructions: "Another assistant",
model: openai("gpt-4o"),
hooks: {
onStart: async ({ agent, context }) => {
/* ... */
},
onEnd: async ({ agent, output, error, context }) => {
/* ... */
},
},
});
Passing Hooks to Methods​
Pass hooks to generateText, streamText, generateObject, or streamObject to run hooks for that specific invocation only.
Method-level hooks do not override agent-level hooks. Both will execute. For most hooks, the method-level hook runs first, then the agent-level hook. For onPrepareMessages and onPrepareModelMessages, the method-level hook replaces the agent-level hook entirely.
const agent = new Agent({
name: "My Agent with Hooks",
instructions: "An assistant demonstrating hooks",
model: openai("gpt-4o"),
hooks: myAgentHooks,
});
await agent.generateText("Hello, how are you?", {
hooks: {
onEnd: async ({ context }) => {
console.log("End of generation for this invocation!");
},
},
});
For example, store conversation history only for specific endpoints:
const agent = new Agent({
name: "Translation Agent",
instructions: "A translation agent that translates text from English to French",
model: openai("gpt-4o"),
});
// for the translate endpoint, we don't want to store the conversation history
app.post("/api/translate", async (req, res) => {
const result = await agent.generateText(req.body.text);
return result;
});
// for the chat endpoint, we want to store the conversation history
app.post("/api/translate/chat", async (req, res) => {
const result = await agent.streamText(req.body.text, {
hooks: {
onEnd: async ({ context }) => {
await chatStore.save({
conversationId: context.conversationId,
messages: context.steps,
});
},
},
});
return result.textStream;
});
Available Hooks​
All hooks receive a single argument object containing relevant information.
Choosing the right message hook​
| Hook | Stage | When to use |
|---|---|---|
onPrepareMessages | Operates on sanitized UIMessage[] built from memory + user input | Transform messages using helper functions, redact content, or access rawMessages for logging. |
onPrepareModelMessages | Runs after convertToModelMessages on ModelMessage[] | Apply provider-specific adjustments, inject system directives, or modify the final payload structure. |
Use onPrepareMessages for general transformations and onPrepareModelMessages for provider-specific adjustments. Both hooks can be used together; VoltAgent applies them in that order.
onStart​
- Triggered: Before the agent begins processing a request (
generateText,streamText, etc.). - Argument Object (
OnStartHookArgs):{ agent: Agent, context: OperationContext } - Use Cases: Initialization logic, request logging, setting up request-scoped resources.
// Example: Log the start of an operation
onStart: async ({ agent, context }) => {
console.log(`Agent ${agent.name} starting operation ${context.operationId}`);
};
onPrepareMessages​
- Triggered: After VoltAgent assembles conversation history and sanitizes UI messages, before conversion to provider-specific payloads.
- Argument Object (
OnPrepareMessagesHookArgs):{ messages: UIMessage[], rawMessages?: UIMessage[], context: OperationContext, agent: Agent } - Use Cases: Transform sanitized messages (add timestamps, redact content), or access
rawMessagesfor logging or metadata recovery. - Return:
{ messages: UIMessage[] }to replace the sanitized list, or an empty object to keep the original. - Notes:
messagescontains sanitized UI messages safe for the LLM.rawMessagescontains the full structure before sanitization, useful for logging or metadata reconstruction.
onPrepareMessages: async ({ messages, rawMessages }) => {
const tagged = messages.map((msg) =>
messageHelpers.addTimestampToMessage(msg, new Date().toISOString())
);
if (rawMessages) {
auditTrail.write(rawMessages); // your own analytics sink
}
return { messages: tagged };
};
onPrepareModelMessages​
- Triggered: After UI messages are converted via
convertToModelMessages, immediately before the provider receives them. - Argument Object (
OnPrepareModelMessagesHookArgs):{ modelMessages: ModelMessage[], uiMessages: UIMessage[], context: OperationContext, agent: Agent } - Use Cases: Apply provider-specific transformations, inject final system messages, compress conversation length, or add structured metadata.
- Return:
{ modelMessages: ModelMessage[] }with a replacement list, or an empty object to keep the original. - Tip: Use
onPrepareMessagesfor general transformations, and this hook for provider-specific adjustments.
onPrepareModelMessages: async ({ modelMessages }) => {
// Force the last message to include a "speak clearly" reminder for voice models
const last = modelMessages.at(-1);
if (last && last.role === "user" && Array.isArray(last.content)) {
last.content.push({ type: "text", text: "Please answer succinctly." });
}
return { modelMessages };
};
onEnd​
- Triggered: After the agent completes processing a request (success or failure).
- Argument Object (
OnEndHookArgs):{ agent: Agent, output: AgentOperationOutput | undefined, error: VoltAgentError | undefined, conversationId: string, context: OperationContext } - Use Cases: Cleanup, logging, analyzing output or errors, recording usage statistics, storing conversation history.
- Note: The
outputstructure depends on the method called. Check fortextorobjectfields.errorcontains the structuredVoltAgentErroron failure.
// Example: Log the outcome of an operation and store conversation history
onEnd: async ({ agent, output, error, conversationId, context }) => {
if (error) {
console.error(`Agent ${agent.name} operation ${context.operationId} failed: ${error.message}`);
console.log(`User input: "${context.historyEntry.input}"`);
// Only user input available on error (no assistant response)
} else {
// Check output type if needed
if (output && "text" in output) {
console.log(
`Agent ${agent.name} operation ${context.operationId} succeeded with text output.`
);
} else if (output && "object" in output) {
console.log(
`Agent ${agent.name} operation ${context.operationId} succeeded with object output.`
);
} else {
console.log(`Agent ${agent.name} operation ${context.operationId} succeeded.`);
}
// Access input and output directly from context
console.log("Request input:", context.input);
console.log("Generated output:", context.output);
// Log the complete conversation flow
console.log(`Conversation flow:`, {
user: context.historyEntry.input,
assistant: context.steps, // the assistant steps
totalMessages: context.steps.length,
toolInteractions: context.steps.flatMap((s) => s.toolInvocations || []).length,
toolsUsed: context.steps.flatMap((s) => s.toolInvocations || []).map((t) => t.toolName),
});
// Log complete interaction using input/output
console.log("Full interaction:", {
input: context.input,
output: context.output,
userId: context.userId,
conversationId: context.conversationId,
operationId: context.operationId,
});
// Log usage if available
if (output?.usage) {
console.log(` Usage: ${output.usage.totalTokens} tokens`);
}
}
};
onToolStart​
- Triggered: Before an agent executes a tool.
- Argument Object (
OnToolStartHookArgs):{ agent: Agent, tool: AgentTool, args: any, context: OperationContext } - Use Cases: Logging tool usage, inspecting tool arguments, or validating inputs before execution.
// Example: Log tool invocation with arguments
onToolStart: async ({ agent, tool, args, context }) => {
console.log(`Agent ${agent.name} invoking tool '${tool.name}'`);
console.log(`Tool arguments:`, args);
};
onToolEnd​
- Triggered: After a tool execution completes or throws an error.
- Argument Object (
OnToolEndHookArgs):{ agent: Agent, tool: AgentTool, output: unknown | undefined, error: VoltAgentError | undefined, context: OperationContext } - Use Cases: Logging tool results or errors, post-processing output, triggering actions based on success or failure.
// Example: Log the result or error of a tool execution
onToolEnd: async ({ agent, tool, output, error, context }) => {
if (error) {
console.error(
`Tool '${tool.name}' failed in operation ${context.operationId}: ${error.message}`
);
} else {
console.log(
`Tool '${tool.name}' succeeded in operation ${context.operationId}. Result:`,
output
);
}
};
onHandoff​
- Triggered: When one agent delegates a task to another agent via the
delegate_tasktool. - Argument Object (
OnHandoffHookArgs):{ agent: Agent, sourceAgent: Agent } - Use Cases: Tracking workflow in multi-agent systems, logging agent collaboration.
// Example: Log agent handoffs
onHandoff: async ({ agent, sourceAgent }) => {
console.log(`Task handed off from agent '${sourceAgent.name}' to agent '${agent.name}'`);
};
Hook Execution and Error Handling​
- Async Execution: Hooks can be
asyncfunctions. VoltAgent awaits completion before proceeding. Long-running operations in hooks add latency to agent response time. - Error Handling: Errors thrown inside hooks may interrupt agent execution. Use
try...catchwithin hooks or design them to be reliable. - Hook Merging: When hooks are passed to both the Agent constructor and a method call:
- Most hooks (
onStart,onEnd,onError,onHandoff,onToolStart,onToolEnd,onStepFinish) execute both: method-level first, then agent-level. - Message hooks (
onPrepareMessages,onPrepareModelMessages) do not merge: method-level replaces agent-level entirely.
- Most hooks (
Additional Hooks​
Two additional hooks exist for advanced use cases:
onError: Called when an error occurs during agent execution. Receives{ agent: Agent, error: Error, context: OperationContext }.onStepFinish: Called after each step in multi-step agent execution. Receives{ agent: Agent, step: any, context: OperationContext }.
Common Use Cases​
- Logging & Observability: Track execution steps, timings, inputs, outputs, and errors.
- Analytics: Collect usage data (token counts, tool usage frequency, success/error rates).
- Message Transformation: Modify messages before they reach the LLM or provider.
- State Management: Initialize or clean up request-specific resources.
- Workflow Orchestration: Trigger external actions based on agent events.
- UI Integration: Convert
OperationContextto messages for the Vercel AI SDK using@voltagent/vercel-ui.
Examples​
Message Transformation with onPrepareMessages​
Transform messages before they reach the LLM using onPrepareMessages and message helper functions:
import { Agent, createHooks, messageHelpers } from "@voltagent/core";
import { openai } from "@ai-sdk/openai";
const enhancedHooks = createHooks({
onPrepareMessages: async ({ messages, context }) => {
const enhanced = messages.map((msg) => {
// Add timestamps to user messages
if (msg.role === "user") {
const timestamp = new Date().toLocaleTimeString();
msg = messageHelpers.addTimestampToMessage(msg, timestamp);
}
// Redact sensitive data
msg = messageHelpers.mapMessageContent(msg, (text) => {
text = text.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[SSN-REDACTED]");
text = text.replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, "[CC-REDACTED]");
return text;
});
return msg;
});
// Add context based on user
if (context.context?.get && context.context.get("userId")) {
const systemContext = {
role: "system" as const,
content: `User ID: ${context.context.get("userId")}. Provide personalized responses.`,
};
enhanced.unshift(systemContext);
}
return { messages: enhanced };
},
onEnd: async ({ output, context }) => {
console.log(`Messages processed for operation ${context.operationId}`);
if (output?.usage) {
console.log(`Tokens used: ${output.usage.totalTokens}`);
}
},
});
const agent = new Agent({
name: "Privacy-Aware Assistant",
instructions: "A helpful assistant that protects user privacy",
model: openai("gpt-4o-mini"),
hooks: enhancedHooks,
});
// User message: "My SSN is 123-45-6789"
// LLM receives: "[10:30:45] My SSN is [SSN-REDACTED]"
Inspecting Output in onEnd​
The output parameter structure depends on the agent method called. Check for text or object fields:
const hooks = createHooks({
onEnd: async ({ output }) => {
if (!output) return; // operation failed or was aborted
// Log usage if available
if (output.usage) {
console.log(`Total tokens: ${output.usage.totalTokens}`);
}
// Handle text results
if ("text" in output && output.text) {
console.log("Final text:", output.text);
return;
}
// Handle object results
if ("object" in output && output.object) {
console.log("Final object keys:", Object.keys(output.object));
}
},
});