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:
- Singleton Instance: Uses
globalThis
to ensure only oneOrchestrationManager
instance is created per server process. This is crucial for serverless/edge environments to reuse the manager instance across invocations where possible. - 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. - Standard Core Components: Uses the standard
createOrchestrationManager
function and other components imported directly fromagentdock-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:
- Orchestration logic is only engaged for relevant agents.
- No resources are wasted on non-orchestrated agents
- 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
andSessionManager
. - 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:
-
Configure Appropriate Cleanup Options
// Configure manager with cleanup disabled for edge environments orchestrationManager = createOrchestrationManager({ cleanup: { enabled: false } });
-
Rely on Client Caching
// Client-side: Use cache for state if (typeof window !== 'undefined') { return sessionCache.get(sessionId) || null; }
-
Minimize State Transfer
- Send only essential state in headers
- Parse and store on client
Multi-Region Deployments
For multi-region deployments:
-
Consider External State Store
- Redis or similar for shared state
- Keep state minimal for performance
-
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
-
Initialize Only When Needed
- Check
template.orchestration
before calling orchestration-specific functions likegetOrchestrationState
.
- Check
-
Configure for Environment
- Set appropriate cleanup options based on your deployment environment
- Disable cleanup timers in serverless environments
-
Client-Side Caching
- Store state in client-side cache
- Update from response headers
- Handle expired or invalid state gracefully
-
Clean Session IDs
- Use a consistent format with sufficient entropy
- Never expose sensitive information in session IDs
- Include timestamps for debugging
-
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
.