@xynehq/jaf
Version:
Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools
451 lines • 16.7 kB
JavaScript
/**
* JAF ADK Layer - Session Management
*
* Functional session management with pluggable providers
*/
import { throwSessionError, createSessionError } from '../types';
// ========== Session Creation ==========
export const generateSessionId = () => {
// Use timestamp and random number to match expected pattern /^session_\d+_\d+$/
return `session_${Date.now()}_${Math.floor(Math.random() * 1000000000)}`;
};
export const createSession = (appName, userId, sessionId, metadata) => {
const id = sessionId || generateSessionId();
const sessionMetadata = {
created: new Date(),
tags: [],
properties: {},
...metadata
};
return {
id,
appName,
userId,
messages: [],
artifacts: {},
metadata: sessionMetadata
};
};
// ========== In-Memory Session Provider ==========
export const createInMemorySessionProvider = () => {
const sessions = new Map();
return {
createSession: async (context) => {
const session = createSession(context.appName, context.userId, context.sessionId);
sessions.set(session.id, session);
return session;
},
getSession: async (sessionId) => {
const session = sessions.get(sessionId);
if (session) {
// Update last accessed
session.metadata.lastAccessed = new Date();
sessions.set(sessionId, session);
}
return session || null;
},
updateSession: async (session) => {
session.metadata.lastAccessed = new Date();
sessions.set(session.id, session);
return session;
},
listSessions: async (userId) => {
return Array.from(sessions.values())
.filter(session => session.userId === userId)
.sort((a, b) => b.metadata.created.getTime() - a.metadata.created.getTime());
},
deleteSession: async (sessionId) => {
return sessions.delete(sessionId);
}
};
};
// ========== Redis Session Provider ==========
// Re-export from the real implementation
export { createRedisSessionProvider } from './redis-provider.js';
export const createMockRedisSessionProvider = (config) => {
let redis; // Will be typed properly when ioredis is added
let isRealRedis = false;
// Try to use real Redis if available
try {
// Dynamic import to avoid breaking if ioredis not installed
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Redis = require('ioredis');
redis = new Redis({
host: config.host,
port: config.port,
password: config.password,
db: config.database || 0,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
enableOfflineQueue: true,
maxRetriesPerRequest: 3,
lazyConnect: false
});
isRealRedis = true;
// Handle connection errors
redis.on('error', (err) => {
console.error('[ADK:Sessions] Redis connection error:', err);
});
redis.on('connect', () => {
console.log('[ADK:Sessions] Connected to Redis');
});
}
catch (error) {
console.warn('[ADK:Sessions] ioredis not found, falling back to mock implementation');
// Fallback to mock Map if ioredis not available
redis = new Map();
}
const keyPrefix = config.keyPrefix || 'jaf_adk_session:';
const getKey = (sessionId) => `${keyPrefix}${sessionId}`;
const getUserKey = (userId) => `${keyPrefix}user:${userId}`;
// Helper to deserialize session and restore Date objects
const deserializeSession = (sessionData) => {
const session = JSON.parse(sessionData);
// Restore Date objects from strings
if (session.metadata.created && typeof session.metadata.created === 'string') {
session.metadata.created = new Date(session.metadata.created);
}
if (session.metadata.lastAccessed && typeof session.metadata.lastAccessed === 'string') {
session.metadata.lastAccessed = new Date(session.metadata.lastAccessed);
}
return session;
};
return {
createSession: async (context) => {
const session = createSession(context.appName, context.userId, context.sessionId);
// Store session
redis.set(getKey(session.id), JSON.stringify(session));
// Add to user's session list
const userKey = getUserKey(context.userId);
const userSessions = redis.get(userKey);
const sessionIds = userSessions ? JSON.parse(userSessions) : [];
sessionIds.push(session.id);
redis.set(userKey, JSON.stringify(sessionIds));
return session;
},
getSession: async (sessionId) => {
const sessionData = redis.get(getKey(sessionId));
if (!sessionData) {
return null;
}
try {
const session = deserializeSession(sessionData);
// Update last accessed
session.metadata.lastAccessed = new Date();
redis.set(getKey(sessionId), JSON.stringify(session));
return session;
}
catch (error) {
throwSessionError(`Failed to parse session data: ${error}`, sessionId);
return null; // This will never be reached due to throwSessionError, but needed for TypeScript
}
},
updateSession: async (session) => {
session.metadata.lastAccessed = new Date();
redis.set(getKey(session.id), JSON.stringify(session));
return session;
},
listSessions: async (userId) => {
const userKey = getUserKey(userId);
const sessionIdsData = redis.get(userKey);
if (!sessionIdsData) {
return [];
}
try {
const sessionIds = JSON.parse(sessionIdsData);
const sessions = [];
for (const sessionId of sessionIds) {
const sessionData = redis.get(getKey(sessionId));
if (sessionData) {
sessions.push(deserializeSession(sessionData));
}
}
return sessions.sort((a, b) => b.metadata.created.getTime() - a.metadata.created.getTime());
}
catch (error) {
throwSessionError(`Failed to list sessions for user: ${error}`, undefined, { userId });
return []; // This will never be reached due to throwSessionError, but needed for TypeScript
}
},
deleteSession: async (sessionId) => {
const sessionData = redis.get(getKey(sessionId));
if (!sessionData) {
return false;
}
try {
const session = JSON.parse(sessionData);
// Remove from Redis
redis.delete(getKey(sessionId));
// Remove from user's session list
const userKey = getUserKey(session.userId);
const userSessionsData = redis.get(userKey);
if (userSessionsData) {
const sessionIds = JSON.parse(userSessionsData);
const updatedIds = sessionIds.filter(id => id !== sessionId);
redis.set(userKey, JSON.stringify(updatedIds));
}
return true;
}
catch (error) {
throwSessionError(`Failed to delete session: ${error}`, sessionId);
return false; // This will never be reached due to throwSessionError, but needed for TypeScript
}
}
};
};
// ========== Postgres Session Provider ==========
// Re-export from the real implementation
export { createPostgresSessionProvider } from './postgres-provider.js';
// ========== Session Operations ==========
export const addMessageToSession = (session, message) => {
return {
...session,
messages: [...session.messages, message],
metadata: {
...session.metadata,
lastAccessed: new Date()
}
};
};
export const addArtifactToSession = (session, key, value) => {
return {
...session,
artifacts: {
...session.artifacts,
[key]: value
},
metadata: {
...session.metadata,
lastAccessed: new Date()
}
};
};
export const removeArtifactFromSession = (session, key) => {
const { [key]: removed, ...remainingArtifacts } = session.artifacts;
return {
...session,
artifacts: remainingArtifacts,
metadata: {
...session.metadata,
lastAccessed: new Date()
}
};
};
export const updateSessionMetadata = (session, metadata) => {
return {
...session,
metadata: {
...session.metadata,
...metadata,
lastAccessed: new Date()
}
};
};
export const clearSessionMessages = (session) => {
return {
...session,
messages: [],
metadata: {
...session.metadata,
lastAccessed: new Date()
}
};
};
// ========== Session Validation ==========
export const validateSession = (session) => {
const errors = [];
if (!session.id || session.id.trim().length === 0) {
errors.push('Session ID is required');
}
if (!session.appName || session.appName.trim().length === 0) {
errors.push('App name is required');
}
if (!session.userId || session.userId.trim().length === 0) {
errors.push('User ID is required');
}
if (!Array.isArray(session.messages)) {
errors.push('Messages must be an array');
}
if (typeof session.artifacts !== 'object' || session.artifacts === null) {
errors.push('Artifacts must be an object');
}
if (!session.metadata || !session.metadata.created) {
errors.push('Session metadata with created date is required');
}
if (errors.length > 0) {
return { success: false, errors };
}
return { success: true, data: session };
};
export const validateSessionContext = (context) => {
const errors = [];
if (!context.appName || context.appName.trim().length === 0) {
errors.push('App name is required');
}
if (!context.userId || context.userId.trim().length === 0) {
errors.push('User ID is required');
}
if (errors.length > 0) {
return { success: false, errors };
}
return { success: true, data: context };
};
// ========== Session Utilities ==========
export const getOrCreateSession = async (provider, context) => {
// If sessionId is provided, try to get existing session
if (context.sessionId) {
const existingSession = await provider.getSession(context.sessionId);
if (existingSession) {
return existingSession;
}
}
// Create new session
return await provider.createSession(context);
};
export const getSessionStats = (session) => {
const messageCount = session.messages.length;
const userMessages = session.messages.filter(m => m.role === 'user').length;
const modelMessages = session.messages.filter(m => m.role === 'model').length;
const systemMessages = session.messages.filter(m => m.role === 'system').length;
const artifactCount = Object.keys(session.artifacts).length;
const totalTextLength = session.messages
.flatMap(m => m.parts)
.filter(p => p.type === 'text' && p.text)
.reduce((sum, p) => sum + (p.text?.length || 0), 0);
return {
id: session.id,
appName: session.appName,
userId: session.userId,
messageCount,
userMessages,
modelMessages,
systemMessages,
artifactCount,
totalTextLength,
created: session.metadata.created,
lastAccessed: session.metadata.lastAccessed,
tags: session.metadata.tags
};
};
export const cloneSession = (session, newId) => {
return {
...session,
id: newId || generateSessionId(),
messages: [...session.messages],
artifacts: { ...session.artifacts },
metadata: {
...session.metadata,
created: new Date()
}
};
};
export const mergeSessionArtifacts = (session, artifacts) => {
return {
...session,
artifacts: {
...session.artifacts,
...artifacts
},
metadata: {
...session.metadata,
lastAccessed: new Date()
}
};
};
// ========== Session Query Functions ==========
export const getLastUserMessage = (session) => {
const userMessages = session.messages.filter(m => m.role === 'user');
return userMessages.length > 0 ? userMessages[userMessages.length - 1] : null;
};
export const getLastModelMessage = (session) => {
const modelMessages = session.messages.filter(m => m.role === 'model');
return modelMessages.length > 0 ? modelMessages[modelMessages.length - 1] : null;
};
export const getMessagesByRole = (session, role) => {
return session.messages.filter(m => m.role === role);
};
export const hasArtifact = (session, key) => {
return key in session.artifacts;
};
export const getArtifact = (session, key) => {
return session.artifacts[key] || null;
};
export const getArtifactKeys = (session) => {
return Object.keys(session.artifacts);
};
// ========== Error Handling ==========
// Export createSessionError from types for external use
export { createSessionError };
export const withSessionErrorHandling = (fn, sessionId) => {
return async (...args) => {
try {
return await fn(...args);
}
catch (error) {
// Check if error is already a SessionError by checking its properties
// Handle both Error instances and plain SessionErrorObjects
if (error && typeof error === 'object' &&
(error.name === 'SessionError' ||
error.code === 'SESSION_ERROR')) {
throw error;
}
throwSessionError(`Session operation failed: ${error instanceof Error ? error.message : String(error)}`, sessionId, { originalError: error });
// This will never be reached due to throwSessionError, but needed for TypeScript
throw error;
}
};
};
// ========== Session Provider Bridge ==========
// Bridge JAF memory providers to ADK session providers
export const createMemoryProviderBridge = (memoryProvider) => {
return {
createSession: async (context) => {
const memory = await memoryProvider.createMemory(context.userId);
return sessionFromMemory(memory, context);
},
getSession: async (sessionId) => {
const memory = await memoryProvider.getMemory(sessionId);
return memory ? sessionFromMemory(memory) : null;
},
updateSession: async (session) => {
const memory = memoryFromSession(session);
await memoryProvider.updateMemory(session.id, memory);
return session;
},
listSessions: async (userId) => {
const memories = await memoryProvider.listMemories(userId);
return memories.map((memory) => sessionFromMemory(memory));
},
deleteSession: async (sessionId) => {
return await memoryProvider.deleteMemory(sessionId);
}
};
};
const sessionFromMemory = (memory, context) => {
return {
id: memory.id || generateSessionId(),
appName: context?.appName || 'unknown',
userId: memory.userId || context?.userId || 'unknown',
messages: memory.messages || [],
artifacts: memory.metadata || {},
metadata: {
created: memory.created || new Date(),
lastAccessed: memory.lastAccessed,
tags: [],
properties: {}
}
};
};
const memoryFromSession = (session) => {
return {
id: session.id,
userId: session.userId,
messages: session.messages,
metadata: session.artifacts,
created: session.metadata.created,
lastAccessed: session.metadata.lastAccessed
};
};
//# sourceMappingURL=index.js.map