Context API — Agentuity Documentation

Context API

Storage, logging, and services available via the ctx.* object

AgentContext is what your agent handler and agent-scoped event listeners receive. It combines request-scoped state, conversation state, platform services, observability, and metadata about the current agent run.

Context Properties

The context object passed to your agent handler contains the following properties:

interface AgentContext<TConfig = unknown, TAppState = Record<string, never>> {
  // Lifecycle
  waitUntil(promise: Promise<void> | (() => void | Promise<void>)): void;
 
  // Observability
  logger: Logger;
  tracer: Tracer;
 
  // Identifiers
  sessionId: string;
  current: {
    name: string;
    agentId: string;
    id: string;
    filename: string;
    version: string;
    description?: string;
    inputSchemaCode?: string;
    outputSchemaCode?: string;
  };
 
  // State
  state: Map<string, unknown>;        // Request-scoped scratch state
  session: Session;                   // Request-scoped session object
  thread: Thread;                     // Conversation-scoped state
 
  // Agent and app config
  config: TConfig;                    // Returned from this agent's setup()
  app: TAppState;                     // Reserved app-level state. Defaults to {}
 
  // Services
  kv: KeyValueStorage;
  stream: StreamStorage;
  vector: VectorStorage;
  sandbox: SandboxService;
  queue: QueueService;
  email: EmailService;
  schedule: ScheduleService;
  task: TaskStorage;
 
  // Auth
  auth: AuthInterface | null;
}

Platform Services

Each service on the context object is documented on its own reference page:

ServicePropertyReference
Key-Value Storagectx.kvStorage APIs
Vector Searchctx.vectorStorage APIs
Durable Streamsctx.streamStorage APIs
Message Queuesctx.queueQueue Service
Tasksctx.taskTask Service
Emailctx.emailEmail Service
Schedulesctx.scheduleSchedule Service
Sandboxctx.sandboxSandbox Service

Service Access

All services are available in agents (ctx.*), routes (c.var.*), and standalone scripts. See Accessing Services for the complete access pattern reference.

Key Properties Explained

Identifiers:

  • sessionId: Unique identifier for the current request. Sub-agent calls within that same request share it. This is the same value as ctx.session.id.
  • current: Metadata about the currently executing agent:
    • name: The name passed to createAgent().
    • agentId: Stays the same across deployments. Use for state keys (e.g., ${ctx.current.agentId}_counter).
    • id: Changes with each deployment. Use when you need deployment-specific identifiers.
    • filename: Relative path to the agent file.
    • version: Changes when agent code changes. Use for cache keys or versioned storage.
    • description?: Human-readable description from createAgent() config.
    • inputSchemaCode?: Source code for the input schema (if defined).
    • outputSchemaCode?: Source code for the output schema (if defined).

Configuration:

  • config: Agent-specific configuration returned from the agent's setup() function. Fully typed based on what setup returns.
  • app: Reserved app-level shared state. Defaults to {}, so prefer module-scoped dependencies, service objects, or registerShutdownHook() in new code.

Agent Calling:

  • Import agents directly: import otherAgent from '@agent/other-agent/agent'
  • Call with: await otherAgent.run(input)

For orchestration patterns, see Calling Other Agents.

State Management:

  • session: Request-scoped session object for the current execution and its sub-agent calls.
  • thread: Conversation-scoped object for data that persists across related requests.
  • state: In-memory scratch Map for the current handler execution.

Example Usage:

import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
 
const agent = createAgent('QueryProcessor', {
  schema: {
    input: s.object({ query: s.string() }),
    output: s.object({
      result: s.string(),
      queryCount: s.number(),
    }),
  },
  setup: async () => ({ prefix: 'Processed' }),
  handler: async (ctx, input) => {
    ctx.logger.info(`Session ID: ${ctx.sessionId}`);
 
    await ctx.kv.set('cache', 'last-query', input.query);
    ctx.state.set('startTime', Date.now());
 
    const queryCount = (await ctx.thread.state.get<number>('queryCount')) ?? 0;
    await ctx.thread.state.set('queryCount', queryCount + 1);
 
    return {
      result: `${ctx.config.prefix}: ${input.query}`,
      queryCount: queryCount + 1,
    };
  },
});

Background Tasks (waitUntil)

waitUntil(callback: Promise<void> | (() => void | Promise<void>)): void

Execute background tasks that don't block the response to the caller. Tasks complete after the main response is sent.

Parameters

  • callback: A Promise, or a function that returns either void (synchronous) or a Promise (asynchronous), to be executed in the background

Example

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
 
const agent = createAgent('MessageReceiver', {
  schema: {
    input: z.object({ userId: z.string(), message: z.string() }),
    output: z.object({ status: z.string(), timestamp: z.number() }),
  },
  handler: async (ctx, input) => {
    const responseData = {
      status: 'received',
      timestamp: Date.now(),
    };
 
    // Schedule background tasks (async functions)
    ctx.waitUntil(async () => {
      // Log the message asynchronously
      await logMessageToDatabase(input.userId, input.message);
    });
 
    ctx.waitUntil(async () => {
      // Send push notification in the background
      await sendPushNotification(input.userId, input.message);
    });
 
    // Can also use synchronous functions
    ctx.waitUntil(() => {
      // Update analytics synchronously
      updateAnalyticsSync(input.userId, 'message_received');
    });
 
    // Return immediately without waiting for background tasks
    return responseData;
  },
});

Use Cases

  • Logging and analytics that don't affect the user experience
  • Sending push notifications
  • Database cleanup or maintenance tasks
  • Third-party API calls that don't impact the response
  • Background data processing or enrichment

Authentication (ctx.auth)

When @agentuity/auth middleware is configured, ctx.auth provides access to the authenticated user, organization, and API key context. It is null for unauthenticated requests, cron jobs, and agent-to-agent calls without auth propagation.

import { createAgent } from '@agentuity/runtime';
 
export default createAgent('protected-agent', {
  handler: async (ctx) => {
    if (!ctx.auth) {
      return { error: 'Please sign in' };
    }
 
    const user = await ctx.auth.getUser();
    ctx.logger.info('Request from %s', user.email);
 
    // Check organization role
    if (await ctx.auth.hasOrgRole('admin')) {
      // Admin-only logic
    }
 
    // Check API key permissions (for API key auth)
    if (ctx.auth.authMethod === 'api-key') {
      if (!ctx.auth.hasPermission('data', 'read')) {
        return { error: 'Insufficient permissions' };
      }
    }
 
    return { userId: user.id };
  },
});

Available properties and methods:

  • getUser() returns the authenticated user (id, email, name, image, timestamps).
  • getToken() returns the raw JWT token, or null.
  • org is the active organization context (id, slug, name, role, memberId), or null if no org is active.
  • getOrg() returns the active organization context, or null.
  • getOrgRole() returns the user's role in the active organization, or null.
  • hasOrgRole(...roles) returns true if the user's org role matches one of the provided roles.
  • authMethod indicates how the request was authenticated: 'session', 'api-key', or 'bearer'.
  • apiKey is the API key context when authenticated via API key, or null.
  • hasPermission(resource, ...actions) checks whether the API key has all specified actions for a resource. Supports '*' wildcard.

For setting up authentication middleware and React client providers, see Adding Authentication.

Thread and Session State

The context provides three levels of state storage, each scoped differently.

ctx.thread persists across multiple requests in a conversation. Thread state uses async lazy-loading, so data is only fetched from storage on first read.

// Store conversation history across requests
const count = await ctx.thread.state.get<number>('messageCount') ?? 0;
await ctx.thread.state.set('messageCount', count + 1);
 
// Append to arrays efficiently without loading the full array
await ctx.thread.state.push('messages', { role: 'user', content: input.text });
 
// Keep a sliding window of the last 100 messages
await ctx.thread.state.push('messages', newMessage, 100);
 
// Merge first because setMetadata replaces the full metadata object
const metadata = await ctx.thread.getMetadata();
await ctx.thread.setMetadata({ ...metadata, userId: user.id, topic: 'support' });

ctx.session is scoped to a single request-response cycle. Each HTTP request creates a new session within the same thread. Session state is a synchronous Map<string, unknown> and does not persist into later requests.

// Track timing for this request only
ctx.session.state.set('startTime', Date.now());
 
// Access the parent thread from the session
ctx.logger.info('Thread: %s, Session: %s', ctx.session.thread.id, ctx.session.id);
 
// Session metadata (stored unencrypted for querying)
ctx.session.metadata.requestType = 'chat';

ctx.state is request-scoped, in-memory only, and cleared between requests. It is a synchronous Map<string, unknown> for passing data within a single handler execution.

ctx.state.set('processingStep', 'validation');
const step = ctx.state.get('processingStep');
 
if (typeof step === 'string') {
  ctx.logger.info('Processing step', { step });
}

For a complete chat example, see Chat with Conversation History.

When to use each:

ScopePersistedAsyncUse case
ctx.thread.stateYes, across requestsYes (lazy-loaded)Conversation history, user preferences
ctx.session.stateNo, request onlyNo (synchronous Map)Request timing, data needed in session.completed listeners
ctx.stateNoNo (synchronous Map)Passing data within the handler

Session Interface

The Session object is available via ctx.session. It represents the current request's session within a thread. Session state is request-scoped only, even when the thread continues across later requests.

interface Session {
  id: string;                      // Unique session identifier (same as ctx.sessionId)
  thread: Thread;                  // Reference to the current thread
  state: Map<string, unknown>;     // Request-scoped state (synchronous)
  metadata: Record<string, unknown>; // Unencrypted metadata for querying
 
  // Lifecycle event: fires when the session completes
  addEventListener(
    eventName: 'completed',
    callback: (eventName: 'completed', session: Session) => Promise<void> | void
  ): void;
  removeEventListener(
    eventName: 'completed',
    callback: (eventName: 'completed', session: Session) => Promise<void> | void
  ): void;
}

Thread Interface

The Thread object is available via ctx.thread. It persists across multiple sessions (requests) in a conversation.

interface Thread {
  id: string;                      // Unique thread identifier
  state: ThreadState;              // Async lazy-loaded state
  getMetadata(): Promise<Record<string, unknown>>;
  setMetadata(metadata: Record<string, unknown>): Promise<void>; // Full replace
 
  // Lifecycle event: fires when the thread is destroyed
  addEventListener(
    eventName: 'destroyed',
    callback: (eventName: 'destroyed', thread: Thread) => Promise<void> | void
  ): void;
  removeEventListener(
    eventName: 'destroyed',
    callback: (eventName: 'destroyed', thread: Thread) => Promise<void> | void
  ): void;
 
  // Remove the thread and all its state
  destroy(): Promise<void>;
  empty(): Promise<boolean>;
}

Call destroy() to clean up a thread when a conversation ends. This removes the thread's persisted state and fires the destroyed event.

Config Type Inference

When you define a setup() function, its return type automatically flows through to ctx.config:

import { createAgent } from '@agentuity/runtime';
 
export default createAgent('my-agent', {
  setup: async () => ({
    cache: new Map<string, string>(),
    maxRetries: 3,
  }),
  handler: async (ctx) => {
    // ctx.config is typed as { cache: Map<string, string>, maxRetries: number }
    ctx.config.cache.set('key', 'value'); 
    ctx.logger.info('Max retries: %d', ctx.config.maxRetries);
  },
});