Skip to main content
Agents

Operation Context (context)

The context field in OperationContext is a Map that passes custom data throughout a single agent operation. All components (hooks, tools, retrievers, sub-agents) can read and write to this shared data structure during execution.

Basic Concept​

Here's how context flows through an agent operation:

You → Agent → Hooks → Tools → Retrievers → Memory → Sub-Agents
↑ ↓
← ← ← ← ← ← context flows everywhere ← ← ← ← ← ← ← ← ←

Initialize context​

You can provide initial data in two ways:

Method 1: Set Default Context in Constructor​

Set default context when creating the agent. This context is used for all operations unless overridden at execution time:

import { Agent } from "@voltagent/core";
import { openai } from "@ai-sdk/openai";

// Set default context at agent creation
const defaultContext = new Map();
defaultContext.set("environment", "production");
defaultContext.set("projectId", "my-project");

const agent = new Agent({
name: "SimpleAgent",
model: openai("gpt-4o"),
instructions: "You are a helpful assistant.",
context: defaultContext, // Default context for all operations
});

// Uses default context automatically
const response1 = await agent.generateText("Hello!");
console.log("Environment:", response1.context?.get("environment")); // "production"

// Override with execution context (replaces default completely)
const response2 = await agent.generateText("Debug this", {
context: new Map([
["environment", "development"],
["projectId", "my-project"],
]),
});
console.log("Environment:", response2.context?.get("environment")); // "development"
console.log("Project ID:", response2.context?.get("projectId")); // "my-project"

Method 2: Pass Context During Execution​

Pass context when calling the agent to provide operation-specific data:

const agent = new Agent({
name: "SimpleAgent",
model: openai("gpt-4o"),
instructions: "You are a helpful assistant.",
});

// Pass context when calling the agent
const executionContext = new Map();
executionContext.set("language", "English");

const response = await agent.generateText("Hello!", {
context: executionContext,
});
// Now you can access the data from the response
console.log("Language:", response.context?.get("language"));

Sending User Information​

You can pass user information to the agent using the context. This information is automatically picked up by the tracing system and associated with the trace.

Supported fields:

  • user.name: User's full name
  • user.email: User's email address
  • user.avatar: User's avatar URL (optional, falls back to Gravatar using email if not provided)
  • user.metadata: Additional metadata object

When using the SDK, pass user information in the context Map:

import { Agent } from "@voltagent/core";

const agent = new Agent({ ... });

const context = new Map();
context.set("user.name", "John Doe");
context.set("user.email", "john@example.com");
context.set("user.avatar", "https://example.com/avatar.jpg");
context.set("user.metadata", { plan: "pro" });

await agent.generateText("Hello", { userId: "user_123", context });

Hooks Access context​

Hooks can read and write to context:

import { createHooks } from "@voltagent/core";

const agent = new Agent({
name: "HookAgent",
model: openai("gpt-4o"),
hooks: createHooks({
onStart: (context) => {
// Read data that was passed in
const language = context.context.get("language");
console.log(`Starting operation for language: ${language}`);

// Add new data
context.context.set("requestId", `req-${Date.now()}`);
context.context.set("startTime", new Date().toISOString());
},
onEnd: (context) => {
// Read data from context
const requestId = context.context.get("requestId");
const startTime = context.context.get("startTime");
console.log(`Request ${requestId} completed (started at ${startTime})`);
},
}),
instructions: "You are a helpful assistant.",
});

// Usage
const context = new Map();
context.set("language", "English");

await agent.generateText("Hello!", { context });

Tools Access Context​

Tools receive the full operation context through the second parameter (options), which includes all OperationContext fields directly accessible:

import { createTool } from "@voltagent/core";
import { z } from "zod";

const loggerTool = createTool({
name: "log_message",
description: "Logs a message with user context",
parameters: z.object({
message: z.string(),
}),
execute: async ({ message }, options) => {
// Access user-defined context directly
const language = options?.context?.get("language");
const requestId = options?.context?.get("requestId");

// Access operation metadata
const userId = options?.userId;
const operationId = options?.operationId;

// Use the operation-scoped logger
options?.logger?.info(`[${requestId}] User ${userId}: ${message}`);

console.log(`[${requestId}] Language ${language}: ${message}`);

// Write to context
const ctx = options?.context;
if (ctx) {
const logs = ctx.get("logs") || [];
logs.push({
message,
timestamp: new Date().toISOString(),
userId,
operationId,
});
ctx.set("logs", logs);
}

return `Message logged for language ${language}`;
},
});

const agentWithTool = new Agent({
name: "ToolAgent",
model: openai("gpt-4o"),
tools: [loggerTool],
instructions: "Use the log_message tool to log what the user says.",
});

// Usage
const context = new Map();
context.set("language", "English");
context.set("requestId", "req-456");

const response = await agentWithTool.generateText("Log this: Hello world!", {
context,
userId: "user123",
});

// Check what was logged
const logs = response.context?.get("logs");
console.log("All logs:", logs);

What's Available in Tool Options?​

The options parameter in tool execute functions combines ToolExecuteOptions and Partial<OperationContext>:

From OperationContext:

  • operationId - Unique operation identifier
  • userId - User identifier (if provided)
  • conversationId - Conversation identifier (if provided)
  • context - User-defined context Map (read/write)
  • systemContext - Internal system context Map
  • logger - Operation-scoped logger with full context
  • abortController - For operation cancellation
  • traceContext - OpenTelemetry trace context
  • input - Original input to the agent
  • isActive - Whether operation is still active

Tool-specific context (from toolContext? - optional):

Note: toolContext is always populated when your tool is called from a VoltAgent agent. It may be undefined when called externally.

  • toolContext?.name - Name of the tool being executed
  • toolContext?.callId - Unique identifier for this tool call (from AI SDK)
  • toolContext?.messages - Message history at tool call time (from AI SDK)
  • toolContext?.abortSignal - Abort signal for cancellation (from AI SDK)
// Example: Authorization check using context
const sensitiveDataTool = createTool({
name: "get_sensitive_data",
description: "Retrieves sensitive user data",
parameters: z.object({
dataType: z.string(),
}),
execute: async ({ dataType }, options) => {
// Check user role from context
const userRole = options?.context?.get("userRole");

if (userRole !== "admin") {
throw new Error("Unauthorized: Admin role required");
}

// Log access with full context including tool name
const toolName = options?.toolContext?.name;
options?.logger?.warn(`${toolName}: Sensitive data accessed: ${dataType}`, {
userId: options?.userId,
operationId: options?.operationId,
});

return await fetchSensitiveData(dataType);
},
});

Access Input and Output in Context​

The OperationContext provides access to both the input and output of the current operation, making debugging and observability much easier.

Accessing Input​

The input field contains the original input provided to the agent operation:

import { createTool } from "@voltagent/core";
import { z } from "zod";

const logTool = createTool({
name: "log_input",
description: "Logs the original user input",
parameters: z.object({}),
execute: async (args, options) => {
const input = options?.operationContext?.input;
console.log("Original input:", input);
// input can be: string, UIMessage[], or BaseMessage[]

return "Input logged successfully";
},
});

Accessing Output​

The output field contains the generated response (only available after generation completes, mainly useful in hooks):

import { createHooks } from "@voltagent/core";

const hooks = createHooks({
onEnd: async ({ context }) => {
// Access both input and output
console.log("User asked:", context.input);
console.log("Agent responded:", context.output);

// Output type depends on the method used:
// - generateText/streamText: string
// - generateObject/streamObject: object

// Log complete interaction
const interaction = {
input: context.input,
output: context.output,
userId: context.userId,
conversationId: context.conversationId,
operationId: context.operationId,
};

console.log("Complete interaction:", interaction);
},
});

Use Cases​

The input and output fields are particularly useful for:

  • Audit Logging: Track what users asked and what the agent responded
  • Analytics: Analyze input/output patterns across conversations
  • Debugging: Trace issues by comparing input to output
  • Quality Assurance: Monitor agent responses for quality control
  • Custom Metrics: Calculate response time, token efficiency, etc.
import { Agent, createHooks } from "@voltagent/core";
import { openai } from "@ai-sdk/openai";

const auditHooks = createHooks({
onEnd: async ({ context, output }) => {
// Log complete audit trail
await auditLog.save({
timestamp: context.startTime,
operationId: context.operationId,
userId: context.userId,
conversationId: context.conversationId,
input: context.input,
output: context.output,
usage: output?.usage,
});
},
});

const auditedAgent = new Agent({
name: "Audited Assistant",
model: openai("gpt-4o"),
instructions: "You are a helpful assistant.",
hooks: auditHooks,
});

OperationContext Properties​

The OperationContext type (defined in types.ts) contains multiple properties beyond the user-managed context Map. Understanding these properties helps with debugging, observability, and advanced use cases.

User-Managed Fields​

These fields are set by you and used to identify and track operations:

interface OperationContext {
// User data storage - read and write custom values
readonly context: Map<string | symbol, unknown>;

// Optional user identifier
userId?: string;

// Optional conversation identifier
conversationId?: string;
}

Usage example:

const response = await agent.generateText("Hello", {
userId: "user-123",
conversationId: "conv-456",
context: new Map([["language", "en"]]),
});

// Access in hooks or tools
console.log(operationContext.userId); // "user-123"
console.log(operationContext.conversationId); // "conv-456"
console.log(operationContext.context.get("language")); // "en"

Operation Metadata (Read-Only)​

These fields are automatically set by the framework and provide metadata about the current operation:

interface OperationContext {
// Unique identifier for this operation
readonly operationId: string;

// When the operation started
startTime: Date;

// Whether the operation is still active
isActive: boolean;

// Parent agent ID if this is a sub-agent execution
parentAgentId?: string;
}

Usage example:

const agent = new Agent({
name: "TrackedAgent",
model: openai("gpt-4o"),
hooks: createHooks({
onEnd: async ({ context }) => {
const duration = Date.now() - context.startTime.getTime();
console.log(`Operation ${context.operationId} took ${duration}ms`);

if (context.parentAgentId) {
console.log(`Executed as sub-agent of ${context.parentAgentId}`);
}
},
}),
instructions: "You are a helpful assistant.",
});

System Fields (Advanced)​

These fields are used internally by the framework. Most users don't need to interact with them directly:

interface OperationContext {
// Internal state management (used by framework)
readonly systemContext: Map<string | symbol, unknown>;

// Scoped logger with operation context
logger: Logger;

// OpenTelemetry span management
traceContext: AgentTraceContext;

// Full conversation history including tool calls
conversationSteps?: StepWithContent[];

// Cancellation control
abortController: AbortController;

// Error to throw when operation is aborted
cancellationError?: CancellationError;

// Bridge for requesting user input (human-in-the-loop)
elicitation?: (request: unknown) => Promise<unknown>;
}

System Context vs User Context:

  • context: Your data - use this for sharing information between components
  • systemContext: Framework internals - stores stream writers, internal state, etc.

The framework uses systemContext to pass internal state between components. For example, when a sub-agent streams data, the parent agent's stream writer is stored in systemContext (see subagent/index.ts:145-148).

Input/Output (Automatically Set)​

These fields capture the operation's input and output:

interface OperationContext {
// Original input provided to the agent
input?: string | UIMessage[] | BaseMessage[];

// Generated response (text or object)
output?: string | object;
}

These fields are documented in detail in the "Access Input and Output in Context" section below.

Operation Lifecycle Example​

Here's how to use metadata fields for observability:

import { Agent, createHooks } from "@voltagent/core";
import { openai } from "@ai-sdk/openai";

const observableAgent = new Agent({
name: "ObservableAgent",
model: openai("gpt-4o"),
hooks: createHooks({
onStart: ({ context }) => {
// Use the scoped logger
context.logger.info("Operation started", {
operationId: context.operationId,
userId: context.userId,
conversationId: context.conversationId,
});

// Store start metrics
context.context.set("metrics", {
startTime: context.startTime,
operationId: context.operationId,
});
},
onEnd: ({ context, output }) => {
const metrics = context.context.get("metrics") as any;
const duration = Date.now() - context.startTime.getTime();

context.logger.info("Operation completed", {
operationId: context.operationId,
duration,
inputType: typeof context.input,
outputType: typeof context.output,
isActive: context.isActive,
});
},
}),
instructions: "You are a helpful assistant.",
});

// The logger automatically includes userId, conversationId, and operationId
const response = await observableAgent.generateText("Hello", {
userId: "user-123",
conversationId: "conv-456",
});

Cancellation with AbortController​

The abortController field allows operation cancellation:

import { Agent } from "@voltagent/core";
import { openai } from "@ai-sdk/openai";

const agent = new Agent({
name: "CancellableAgent",
model: openai("gpt-4o"),
instructions: "You are a helpful assistant.",
});

// Create an abort controller
const abortController = new AbortController();

// Pass the signal to the agent
const responsePromise = agent.generateText("Write a long story", {
abortSignal: abortController.signal,
});

// Cancel after 1 second
setTimeout(() => {
abortController.abort("User cancelled");
}, 1000);

try {
const response = await responsePromise;
} catch (error) {
if (error.name === "AbortError") {
console.log("Operation was cancelled");
}
}

The agent creates an internal AbortController and cascades the external signal to it (see agent.ts:3385-3395). Sub-agents automatically inherit the parent's abort controller.

Retrievers Store References​

Retrievers can store source information in context:

import { BaseRetriever } from "@voltagent/core";

class SimpleRetriever extends BaseRetriever {
async retrieve(input, options) {
// Simulate finding documents
const foundDocs = [
{ title: "VoltAgent Guide", url: "https://docs.example.com" },
{ title: "Agent Tutorial", url: "https://tutorial.example.com" },
];

// Store references in context
if (options?.context) {
options.context.set("references", foundDocs);
options.context.set("searchQuery", input);
}

// Return content for LLM
return foundDocs.map((doc) => `${doc.title}: Some helpful content...`).join("\n");
}
}

const agentWithRetriever = new Agent({
name: "RetrievalAgent",
model: openai("gpt-4o"),
retriever: new SimpleRetriever(),
instructions: "Answer using retrieved information.",
});

// Usage
const response = await agentWithRetriever.generateText("How do I use VoltAgent?");

console.log("Answer:", response.text);
console.log("Search query:", response.context?.get("searchQuery"));
console.log("References:", response.context?.get("references"));

Sub-Agents Automatically Inherit Context​

When a supervisor delegates to sub-agents, the parent's OperationContext is passed down. The sub-agent receives the same context Map reference (not a copy), so modifications are visible to both parent and sub-agent.

// Worker agent - automatically receives supervisor's context
const workerAgent = new Agent({
name: "WorkerAgent",
model: openai("gpt-4o"),
hooks: createHooks({
onStart: (context) => {
// Automatically gets context from supervisor
const projectId = context.context.get("projectId");
const language = context.context.get("language");
console.log(`Worker starting for project ${projectId}, language ${language}`);

// Can add its own data too
context.context.set("workerStartTime", new Date().toISOString());
},
}),
instructions: "You are a worker that processes tasks.",
});

// Supervisor agent
const supervisorAgent = new Agent({
name: "SupervisorAgent",
model: openai("gpt-4o"),
subAgents: [workerAgent],
hooks: createHooks({
onStart: (context) => {
// Set up project context
context.context.set("projectId", `project-${Date.now()}`);
context.context.set("supervisorId", "supervisor-001");
},
}),
instructions: "You supervise tasks. Delegate work to WorkerAgent when needed.",
});

// Usage
const initialContext = new Map();
initialContext.set("language", "English");
initialContext.set("priority", "high");

const response = await supervisorAgent.generateText("Please delegate this task to the worker", {
context: initialContext,
});

// Final context includes data from both supervisor and worker
console.log("Project ID:", response.context?.get("projectId"));
console.log("Worker start time:", response.context?.get("workerStartTime"));

Context Inheritance Priority​

When creating an OperationContext, the framework follows this priority (see agent.ts:3343-3371):

  1. Parent context (if sub-agent): Use parent's context Map reference
  2. Runtime context (if provided at execution): Use the provided Map
  3. Agent-level context (if set in constructor): Use the agent's default Map
  4. New Map: Create an empty Map if none exists

The framework then fills in missing keys from lower-priority sources without overriding existing values. This means:

  • Sub-agents see all parent context data
  • Parent's values take precedence over runtime and agent-level defaults
  • Runtime values take precedence over agent-level defaults

Important: The context Map reference is preserved, not cloned. This enables bidirectional updates - changes made by sub-agents are visible to the parent.

For more details on sub-agent architecture, see the Sub-Agents guide.

Complete Flow Example​

Here's how all pieces work together:

import { Agent, createHooks, createTool, BaseRetriever } from "@voltagent/core";
import { z } from "zod";

// Simple retriever
class BasicRetriever extends BaseRetriever {
async retrieve(input, options) {
if (options.context) {
options.context.set("references", [{ title: "Document 1", source: "knowledge-base" }]);
}
return "Retrieved content about the topic";
}
}

// Simple tool
const counterTool = createTool({
name: "increment_counter",
description: "Increments a counter",
parameters: z.object({}),
execute: async (_, options) => {
const ctx = options?.context;
if (ctx) {
const count = (ctx.get("counter") || 0) + 1;
ctx.set("counter", count);
return `Counter is now: ${count}`;
}
return "Counter incremented";
},
});

// Agent with everything
const fullAgent = new Agent({
name: "FullAgent",
model: openai("gpt-4o"),
retriever: new BasicRetriever(),
tools: [counterTool],
hooks: createHooks({
onStart: (context) => {
console.log("🚀 Operation started");
context.context.set("operationId", `op-${Date.now()}`);
},
onEnd: (context) => {
const opId = context.context.get("operationId");
const counter = context.context.get("counter");
const references = context.context.get("references");

console.log("✅ Operation completed");
console.log(`Operation ID: ${opId}`);
console.log(`Counter final value: ${counter}`);
console.log(`References found: ${references?.length || 0}`);
},
}),
instructions: "Use tools and retrieval to help users. Always increment the counter.",
});

// Usage showing the complete flow
async function demonstrateFlow() {
const initialContext = new Map();
initialContext.set("language", "English");

const response = await fullAgent.generateText(
"Use the increment tool and search for information",
{ context: initialContext }
);

console.log("Text:", response.text);
const finalContext = response.context;
for (const [key, value] of finalContext.entries()) {
console.log(`${String(key)}: ${JSON.stringify(value)}`);
}
}

Key Points​

Context Access by Component​

  1. Hooks: Access via context.context in onStart/onEnd callbacks
  2. Tools: Access via options.operationContext.context in execute function
  3. Retrievers: Access via options.context in retrieve method
  4. Sub-Agents: Automatically receive parent's context Map reference
  5. Response: Access final state via response.context
  6. Dynamic Values: Constructor context is available in dynamic instructions, model, and tools functions

OperationContext Fields​

The OperationContext contains:

  • User-managed: context (Map), userId, conversationId
  • Metadata: operationId, startTime, isActive, parentAgentId
  • System: systemContext, logger, traceContext, conversationSteps, abortController, cancellationError, elicitation
  • Input/Output: input, output (automatically set)

Context Initialization Priority​

  1. Parent context (for sub-agents) - highest priority
  2. Runtime context (execution parameter)
  3. Agent-level context (constructor)
  4. New empty Map - lowest priority

Lower-priority sources only fill in missing keys; they don't override existing values. The Map reference is preserved (not cloned) to enable bidirectional updates between parent and sub-agents.

Tips​

Extend rather than replace default context:

const agent = new Agent({
context: new Map([["env", "production"]]),
// ...
});

// Extend the default context
const extendedContext = new Map(agent.context);
extendedContext.set("requestId", "req-123");

await agent.generateText("Hello", { context: extendedContext });

Use metadata fields for observability:

hooks: createHooks({
onEnd: ({ context }) => {
console.log(
`Operation ${context.operationId} completed in ${Date.now() - context.startTime.getTime()}ms`
);
},
});

Memory Adapters​

Custom memory adapters can access OperationContext to implement cross-cutting concerns like multi-tenancy, logging, and access control without modifying core adapter logic.

Example: Multi-Tenant Data Isolation​

Custom memory adapters can read user-provided context to implement tenant-scoped data access:

import { InMemoryStorageAdapter } from "@voltagent/core";
import type { OperationContext, GetMessagesOptions, UIMessage } from "@voltagent/core/agent";

class TenantMemoryAdapter extends InMemoryStorageAdapter {
async getMessages(
userId: string,
conversationId: string,
options?: GetMessagesOptions,
context?: OperationContext
): Promise<UIMessage[]> {
// Read tenant ID from user-provided context
const tenantId = context?.context.get("tenantId") as string;

if (!tenantId) {
throw new Error("Tenant ID is required");
}

// Create tenant-scoped user ID
const scopedUserId = `${tenantId}:${userId}`;

// Log access for audit trail
context?.logger.info("Tenant memory access", {
tenantId,
userId,
scopedUserId,
traceId: context.traceContext.getTraceId(),
});

// Use scoped user ID to isolate tenant data
return super.getMessages(scopedUserId, conversationId, options, context);
}
}

// Use the adapter
const agent = new Agent({
memory: new Memory({ storage: new TenantMemoryAdapter() }),
});

// Tenant A: user-123's data
await agent.generateText("Show my history", {
userId: "user-123",
context: new Map([["tenantId", "company-abc"]]), // Stores as "company-abc:user-123"
});

// Tenant B: same user ID, but different tenant = different data
await agent.generateText("Show my history", {
userId: "user-123",
context: new Map([["tenantId", "company-xyz"]]), // Stores as "company-xyz:user-123"
});

What this enables:

  • Data isolation between tenants using composite keys
  • Per-request tenant context without adapter reconfiguration
  • Audit logging with full operation context
  • Flexible access control based on runtime values

All memory adapters (InMemory, Postgres, LibSQL, Supabase) support OperationContext as an optional parameter.

Table of Contents