AgentDock Core Documentation

NextJS Session Integration

This document explains how AgentDock's session management integrates with Next.js applications, focusing on API routes, client-side handling, and runtime considerations.

Orchestration Adapter (src/lib/orchestration-adapter.ts)

The core integration logic connecting the Next.js application to agentdock-core's orchestration capabilities resides in src/lib/orchestration-adapter.ts. This adapter handles initializing the core OrchestrationManager with the correct environment configuration (storage provider, session TTL) and provides helper functions for interacting with it from API routes.

// Simplified structure from src/lib/orchestration-adapter.ts

import { 
    createOrchestrationManager,
    OrchestrationManager,
    // ... other agentdock-core imports
} from 'agentdock-core';

// Singleton pattern using globalThis for Node/Serverless environments
declare global {
  var __orchestrationManagerInstance: OrchestrationManager | null | undefined;
}

export function getOrchestrationManagerInstance(): OrchestrationManager {
  if (globalThis.__orchestrationManagerInstance) {
    return globalThis.__orchestrationManagerInstance;
  }
  
  // Determine storage provider (using getConfiguredStorageProvider helper)
  const storageProvider = getConfiguredStorageProvider(); 
  // Determine TTL from environment (SESSION_TTL_SECONDS)
  const sessionTtlMs = /* ... logic to parse env var ... */;

  // Create and store the single instance
  const newInstance = createOrchestrationManager({
      storageProvider: storageProvider, 
      cleanup: { enabled: false, ttlMs: sessionTtlMs } 
  });
  globalThis.__orchestrationManagerInstance = newInstance;
  return newInstance;
}

Key aspects of this adapter:

  1. Singleton Instance: Uses globalThis to ensure only one OrchestrationManager instance is created per server process. This is crucial for serverless/edge environments to reuse the manager instance across invocations where possible.
  2. Environment Configuration: Reads environment variables (KV_STORE_PROVIDER, SESSION_TTL_SECONDS, etc.) to dynamically configure the storage provider and session TTL when creating the singleton instance.
  3. Standard Core Components: Uses the standard createOrchestrationManager function and other components imported directly from agentdock-core.

Environment-Based TTL Configuration

The getOrchestrationManagerInstance function within src/lib/orchestration-adapter.ts handles reading the SESSION_TTL_SECONDS environment variable.

// Simplified logic from src/lib/orchestration-adapter.ts

import { 
  createOrchestrationManager, 
  getStorageFactory,
  OrchestrationManager,
  // ... other imports
} from 'agentdock-core';

declare global {
  var __orchestrationManagerInstance: OrchestrationManager | null | undefined;
}

export function getOrchestrationManagerInstance() {
  // Use global singleton
  if (globalThis.__orchestrationManagerInstance) {
    return globalThis.__orchestrationManagerInstance;
  }

  // Determine storage provider based on ENV (KV_STORE_PROVIDER, etc.)
  const storageProvider = getConfiguredStorageProvider(); // Internal helper function

  // Read and calculate TTL from ENV
  const sessionTtlSeconds = process.env.SESSION_TTL_SECONDS ? parseInt(process.env.SESSION_TTL_SECONDS, 10) : undefined;
  let sessionTtlMs: number | undefined = undefined;
  if (sessionTtlSeconds && sessionTtlSeconds > 0) {
      sessionTtlMs = sessionTtlSeconds * 1000; // Convert to ms
  }

  // Create the manager instance
  const newInstance = createOrchestrationManager({
      storageProvider: storageProvider, 
      // Pass configured TTL (undefined lets core use its 24h default)
      cleanup: { 
        enabled: false, // Cleanup timer managed within core if needed
        ttlMs: sessionTtlMs 
      }
  });

  // Store globally and return
  globalThis.__orchestrationManagerInstance = newInstance;
  return newInstance;
}

This ensures that the session TTL configured in the environment dictates the actual session lifespan managed by agentdock-core.

API Route Integration

Lazy Initialization

The OrchestrationManager instance itself is initialized lazily on the first call to getOrchestrationManagerInstance() within a server process. However, the decision to use orchestration features (like getting state) typically happens within the API route handler based on the specific agent's configuration:

// Example check within an API Route Handler (e.g., /api/chat/[agentId]/route.ts)
if (template && 'orchestration' in template && template.orchestration) {
  logger.debug(
    LogCategory.API,
    'ChatRoute',
    'Initializing orchestration for agent with orchestration', 
    { agentId }
  );
  
  // Get the manager instance (initializes on first call)
  const manager = getOrchestrationManagerInstance(); 
  
  // Ensure orchestration state exists
  if (finalSessionId) {
    await getOrchestrationState(finalSessionId, template);
  }
}

This ensures that orchestration state operations (getOrchestrationState) are only performed for agents configured to use orchestration, even though the manager instance might have already been created by a previous request in the same process.

Benefits:

  1. Orchestration logic is only engaged for relevant agents.
  2. No resources are wasted on non-orchestrated agents
  3. Cold starts are faster for simple agents

Session ID Management

Session ID creation and management happens at the route handler level:

// Get session ID from various sources with priority
const headerSessionId = request.headers.get('x-session-id');
const requestSessionId = requestJson.sessionId;

// Use existing session ID or create a new one
const finalSessionId = headerSessionId || requestSessionId || 
  `session-${agentId}-${Date.now()}-${crypto.randomUUID()}`;

The session ID is then passed to the agent adapter:

// Process the message using the adapter
const result = await processAgentMessage({
  agentId,
  messages,
  sessionId: finalSessionId, // Always provide a valid session ID
  apiKey,
  fallbackApiKey,
  provider: llmInfo.provider,
  system,
  config
});

Response Headers

Session state is included in response headers for client tracking:

// Create and return the response with proper headers
return createAgentResponse(result, finalSessionId);

// Implementation of createAgentResponse
function createAgentResponse(result: any, sessionId: string): Response {
  // Convert the result to a stream
  const stream = streamText(() => result);
  const response = toDataStreamResponse(stream);
  
  // Add orchestration state to response - required for session continuity
  const orchestrationState = result._orchestrationState;
  if (orchestrationState) {
    response.headers.set('x-orchestration-state', JSON.stringify(orchestrationState));
  }
  
  // Always ensure the session ID is present in the response headers
  response.headers.set('x-session-id', sessionId);
  
  return response;
}

This approach:

  • Ensures clients can maintain session continuity
  • Provides access to orchestration state when needed
  • Follows HTTP standards for custom headers

Client-Side State Management

On the client side, we use a simple cache to maintain state between requests:

// Cache to store user session information
const sessionCache = new Map<string, OrchestrationState>();

/**
 * Update orchestration state in the cache
 */
export function updateOrchestrationCache(
  sessionId: string,
  state: OrchestrationState | Partial<OrchestrationState>
): void {
  if (!sessionId) return;
  
  // Update the cache with the new state
  const existing = sessionCache.get(sessionId);
  if (existing) {
    sessionCache.set(sessionId, { ...existing, ...state });
  } else {
    sessionCache.set(sessionId, state as OrchestrationState);
  }
}

The client component then handles this state:

// In the chat component
useEffect(() => {
  // Extract orchestration state from headers
  const orchestrationStateHeader = response.headers.get('x-orchestration-state');
  if (orchestrationStateHeader) {
    try {
      const stateData = JSON.parse(orchestrationStateHeader);
      updateOrchestrationCache(stateData.sessionId, stateData);
    } catch (e) {
      console.error('Failed to parse orchestration state:', e);
    }
  }
}, [response]);

Agent Adapter Integration

The agent adapter in Next.js uses the orchestration wrapper:

// Import helper functions directly from the adapter
import { getOrchestrationState } from '@/lib/orchestration-adapter';

// ...

export async function processAgentMessage(options: {
  agentId: string;
  messages: CoreMessage[];
  sessionId: string;
  // ...
}) {
  // ...
  
  // Get orchestration state if needed
  if (config.orchestration) {
    const orchestrationState = await getOrchestrationState(
      sessionId,
      config
    );
    
    if (orchestrationState) {
      logger.debug(
        LogCategory.ADAPTER,
        'AgentAdapter',
        'Using orchestration',
        {
          sessionId,
          activeStep: orchestrationState.activeStep
        }
      );
    }
  }
  
  // ...
}

Edge Runtime Considerations

The suitability and performance in Edge Runtime environments depend on:

  • The inherent efficiency of the core OrchestrationManager and SessionManager.
  • The chosen Storage Provider: Using providers compatible with the Edge runtime (like Vercel KV via @vercel/kv, or potentially Redis via @upstash/redis) is crucial. In-memory storage will not persist between Edge function invocations.

Key optimizations:

  • Minimized dependency loading
  • Efficient state structures
  • No cleanup timers in Edge mode
  • Simplified operations

Deployment Considerations

Different deployment environments have different requirements:

Vercel and Edge Functions

For Vercel and other serverless/Edge environments:

  1. Configure Appropriate Cleanup Options

    // Configure manager with cleanup disabled for edge environments
    orchestrationManager = createOrchestrationManager({
      cleanup: { enabled: false }
    });
    
  2. Rely on Client Caching

    // Client-side: Use cache for state
    if (typeof window !== 'undefined') {
      return sessionCache.get(sessionId) || null;
    }
    
  3. Minimize State Transfer

    • Send only essential state in headers
    • Parse and store on client

Multi-Region Deployments

For multi-region deployments:

  1. Consider External State Store

    • Redis or similar for shared state
    • Keep state minimal for performance
  2. Proper Session Routing

    • Use sticky sessions if possible
    • Include region info in session IDs

Debugging Support

Debugging Tools

For debugging session and orchestration state:

function ChatDebug({ sessionId, orchestrationState }: {
  sessionId: string;
  orchestrationState: OrchestrationState | null;
}) {
  if (!orchestrationState) return null;
  
  return (
    <div className="debug-panel">
      <h3>Session Debug</h3>
      <div>Session ID: {sessionId}</div>
      <div>Active Step: {orchestrationState.activeStep || 'None'}</div>
      <div>Tools Used: {orchestrationState.recentlyUsedTools.join(', ')}</div>
      <div>Sequence Position: {orchestrationState.sequenceIndex}</div>
    </div>
  );
}

This helps with:

  • Verifying state persistence
  • Checking sequence progression
  • Identifying orchestration issues

Best Practices

  1. Initialize Only When Needed

    • Check template.orchestration before calling orchestration-specific functions like getOrchestrationState.
  2. Configure for Environment

    • Set appropriate cleanup options based on your deployment environment
    • Disable cleanup timers in serverless environments
  3. Client-Side Caching

    • Store state in client-side cache
    • Update from response headers
    • Handle expired or invalid state gracefully
  4. Clean Session IDs

    • Use a consistent format with sufficient entropy
    • Never expose sensitive information in session IDs
    • Include timestamps for debugging
  5. Error Handling

    • Have fallbacks for orchestration failures
    • Parse state carefully with try/catch
    • Log orchestration errors but continue if possible

Environment-Based TTL Configuration

The src/lib/orchestration-adapter.ts file handles reading environment variables to configure the agentdock-core OrchestrationManager instance. It ensures a single instance (singleton) is used per server process.

// Simplified logic from src/lib/orchestration-adapter.ts

import { 
  createOrchestrationManager, 
  getStorageFactory,
  OrchestrationManager,
  // ... other imports
} from 'agentdock-core';

declare global {
  var __orchestrationManagerInstance: OrchestrationManager | null | undefined;
}

export function getOrchestrationManagerInstance() {
  // Use global singleton
  if (globalThis.__orchestrationManagerInstance) {
    return globalThis.__orchestrationManagerInstance;
  }

  // Determine storage provider based on ENV (KV_STORE_PROVIDER, etc.)
  const storageProvider = getConfiguredStorageProvider(); // Internal helper function

  // Read and calculate TTL from ENV
  const sessionTtlSeconds = process.env.SESSION_TTL_SECONDS ? parseInt(process.env.SESSION_TTL_SECONDS, 10) : undefined;
  let sessionTtlMs: number | undefined = undefined;
  if (sessionTtlSeconds && sessionTtlSeconds > 0) {
      sessionTtlMs = sessionTtlSeconds * 1000; // Convert to ms
  }

  // Create the manager instance
  const newInstance = createOrchestrationManager({
      storageProvider: storageProvider, 
      // Pass configured TTL (undefined lets core use its 24h default)
      cleanup: { 
        enabled: false, // Cleanup timer managed within core if needed
        ttlMs: sessionTtlMs 
      }
  });

  // Store globally and return
  globalThis.__orchestrationManagerInstance = newInstance;
  return newInstance;
}

This ensures that the session TTL configured in the environment dictates the actual session lifespan managed by agentdock-core.