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
unknown
type, 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
onStart
to tools. - Metrics & Analytics: Store timing information or other metrics in
onStart
and 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
onStart
and retrieve it inonEnd
for 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 checksuserContext
first. If aPage
instance for the currentoperationId
doesn't exist, it launches Playwright, creates aPage
, and stores it inuserContext
using a unique key (like aSymbol
). - Tool Access: Tools needing browser access (e.g.,
clickElement
,navigateToUrl
) call theensureBrowser
helper, passing theiroptions.operationContext
. The helper retrieves the correctPage
instance fromuserContext
. - Cleanup (
onEnd
Hook): AnonEnd
hook retrieves theBrowser
instance fromuserContext
using 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,
userContext
is initialized as an emptyMap
. - Access via Hooks: The
OperationContext
(includinguserContext
) is passed as an argument to theonStart
andonEnd
agent lifecycle hooks. - Access via Tools: The
OperationContext
is also accessible within a tool'sexecute
function via the optionaloptions
parameter (specificallyoptions.operationContext
). - Isolation: Each call to an agent generation method (
generateText
,streamText
, etc.) gets its own independentOperationContext
anduserContext
. Data stored in one operation'suserContext
is 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,