Operation Context (userContext)
VoltAgent provides a powerful mechanism called userContext to pass custom data through the lifecycle of a single agent operation (like a generateText or streamObject call). This context is isolated to each individual operation, ensuring that data doesn't leak between concurrent or subsequent requests.
What is userContext?
userContext is a property within the OperationContext object. OperationContext itself encapsulates information about a specific agent task, including its unique ID (operationId), the associated history entry, and event tracking details.
userContext is specifically a Map<string | symbol, unknown>.
- Map: It allows you to store key-value pairs.
- Keys: Can be strings or symbols, providing flexibility in how you identify your context data.
- Values: Can be of
unknowntype, meaning you can store virtually any kind of data – strings, numbers, objects, custom class instances, etc.
Why Use userContext?
userContext solves the problem of needing to maintain and access request-specific state or data across different parts of an agent's execution flow, particularly between lifecycle hooks and tool executions.
Common use cases include:
- Tracing & Logging: Propagate unique request IDs or trace IDs generated at the start (
onStart) into tool executions for distributed tracing or detailed logging. - Request-Specific Configuration: Pass configuration details relevant only to the current operation (e.g., user preferences, tenant IDs) from
onStartto tools. - Metrics & Analytics: Store timing information or other metrics in
onStartand finalize/report them inonEnd. - Resource Management: Store references to resources allocated in
onStart(like database connections specific to the request) and release them inonEnd. - Passing Data Between Hooks: Set a value in
onStartand retrieve it inonEndfor the same operation.
Advanced Use Case: Managing Playwright Browser Instances
Another powerful use case for userContext is managing stateful resources that should be isolated per operation, such as a Playwright Browser or Page instance. This avoids the complexity of passing the instance explicitly between hooks and tools.
Scenario: You want an agent to perform browser automation tasks using Playwright. Each agent operation should have its own isolated browser session.
- Initialization (in Tools or Hooks): Instead of initializing the browser directly in
onStart, you can create a helper function (e.g.,ensureBrowser) that tools call. This function checksuserContextfirst. If aPageinstance for the currentoperationIddoesn't exist, it launches Playwright, creates aPage, and stores it inuserContextusing a unique key (like aSymbol). - Tool Access: Tools needing browser access (e.g.,
clickElement,navigateToUrl) call theensureBrowserhelper, passing theiroptions.operationContext. The helper retrieves the correctPageinstance fromuserContext. - Cleanup (
onEndHook): AnonEndhook retrieves theBrowserinstance fromuserContextusing the operation's context and callsbrowser.close()to ensure resources are released when the operation finishes.
import {
Agent,
createHooks,
createTool,
type OnEndHookArgs,
type OperationContext,
type ToolExecutionContext,
} from "@voltagent/core";
import { chromium, type Browser, type Page } from "playwright";
const PAGE_KEY = Symbol("playwrightPage");
const BROWSER_KEY = Symbol("playwrightBrowser");
// Helper to get/create page within the context
async function ensurePage(context: OperationContext): Promise<Page> {
let page = context.userContext.get(PAGE_KEY) as Page | undefined;
if (!page || page.isClosed()) {
console.log(`[${context.operationId}] Creating new browser/page for context...`);
const browser = await chromium.launch();
page = await browser.newPage();
context.userContext.set(BROWSER_KEY, browser); // Store browser for cleanup
context.userContext.set(PAGE_KEY, page);
}
return page;
}
// Hook for cleanup
const hooks = createHooks({
onEnd: async ({ context }: OnEndHookArgs) => {
const browser = context.userContext.get(BROWSER_KEY) as Browser | undefined;
if (browser) {
console.log(`[${context.operationId}] Closing browser for context...`);
await browser.close();
}
},
});
// Example Tool
const navigateTool = createTool({
name: "navigate",
parameters: z.object({ url: z.string().url() }),
execute: async ({ url }, options?: ToolExecutionContext) => {
if (!options?.operationContext) throw new Error("Context required");
const page = await ensurePage(options.operationContext); // Get page via context
await page.goto(url);
return `Navigated to ${url}`;
},
});
// Agent setup (LLM/Model details omitted)
const browserAgent = new Agent({
name: "Browser Agent",
// ... llm, model ...
hooks: hooks,
tools: [navigateTool],
});
// Usage:
// await browserAgent.generateText("Navigate to https://example.com");
// await browserAgent.generateText("Navigate to https://google.com"); // Uses a *different* browser instance
This pattern ensures each generateText call gets its own clean browser environment managed via the isolated userContext.
For a full implementation of this pattern, see the VoltAgent Playwright Example.
How it Works
- Initialization: When an agent operation (e.g.,
agent.generateText(...)) begins, VoltAgent creates a uniqueOperationContext. - Empty Map: Within this context,
userContextis initialized as an emptyMap. - Access via Hooks: The
OperationContext(includinguserContext) is passed as an argument to theonStartandonEndagent lifecycle hooks. - Access via Tools: The
OperationContextis also accessible within a tool'sexecutefunction via the optionaloptionsparameter (specificallyoptions.operationContext). - Isolation: Each call to an agent generation method (
generateText,streamText, etc.) gets its own independentOperationContextanduserContext. Data stored in one operation'suserContextis not visible to others.
Usage Example
This example demonstrates how to set context data in the onStart hook and access it in both the onEnd hook and within a tool's execute function.
import {
Agent,
createHooks,
createTool,
type OnStartHookArgs,
type OnEndHookArgs,
type OperationContext,
type ToolExecutionContext,
} from "@voltagent/core";
import { z } from "zod";
import { VercelAIProvider } from "@voltagent/vercel-ai";
import { openai } from "@ai-sdk/openai";
// Define hooks that set and retrieve data
const hooks = createHooks({
onStart: ({ agent, context }: OnStartHookArgs) => {
// Set a unique request ID for this operation
const requestId = `req-${Date.now()}`;
context.userContext.set("requestId", requestId);
console.log(`[${agent.name}] Operation started. RequestID: ${requestId}`);
},
onEnd: ({ agent, context }: OnEndHookArgs) => {
// Retrieve the request ID at the end of the operation
const requestId = context.userContext.get("requestId");
console.log(`[${agent.name}] Operation finished. RequestID: ${requestId}`);
// Use this ID for logging, metrics, cleanup, etc.
},
});
// Define a tool that uses the context data set in onStart
const customContextTool = createTool({
name: "custom_context_logger",
description: "Logs a message using the request ID from the user context.",
parameters: z.object({
message: z.string().describe("The message to log."),
}),
execute: async (params: { message: string }, options?: ToolExecutionContext) => {
// Access userContext via options.operationContext
const requestId = options?.operationContext?.userContext?.get("requestId") || "unknown-request";
const logMessage = `[RequestID: ${requestId}] Tool Log: ${params.message}`;
console.log(logMessage);
// In a real scenario, you might interact with external systems using this ID
return `Logged message with RequestID: ${requestId}`;
},
});
const agent = new Agent({
name: "MyCombinedAgent",
llm: new VercelAIProvider(),
model: openai("gpt-4o"),
tools: [customContextTool],
hooks: hooks,
});
// Trigger the agent.
await agent.generateText(
"Log the following information using the custom logger: 'User feedback received.'"
);
// Console output will show logs from onStart, the tool (if called), and onEnd,