@xynehq/jaf
Version:
Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools
369 lines • 18.8 kB
JavaScript
/**
* A2A Task Provider Factory for JAF
* Pure functional factory for creating A2A task providers
*/
import { createA2ATaskStorageError, A2ATaskProviderConfigSchema } from './types.js';
import { createA2AInMemoryTaskProvider } from './providers/in-memory.js';
import { createA2ARedisTaskProvider } from './providers/redis.js';
import { createA2APostgresTaskProvider } from './providers/postgres.js';
/**
* Create an A2A task provider from configuration
*/
export const createA2ATaskProvider = async (config, externalClients) => {
// Validate configuration first
const validationResult = validateA2ATaskProviderConfig(config);
if (!validationResult.valid) {
throw createA2ATaskStorageError('create-provider', config?.type || 'unknown', undefined, new Error(`Invalid configuration: ${validationResult.errors.join(', ')}`));
}
switch (config.type) {
case 'memory':
return createA2AInMemoryTaskProvider(config);
case 'redis':
if (!externalClients?.redis) {
throw createA2ATaskStorageError('create-provider', 'redis', undefined, new Error('Redis client instance required. Please provide a Redis client in externalClients.redis'));
}
return await createA2ARedisTaskProvider(config, externalClients.redis);
case 'postgres':
if (!externalClients?.postgres) {
throw createA2ATaskStorageError('create-provider', 'postgres', undefined, new Error('PostgreSQL client instance required. Please provide a PostgreSQL client in externalClients.postgres'));
}
return await createA2APostgresTaskProvider(config, externalClients.postgres);
default:
throw new Error(`Unknown A2A task provider type: ${config.type}`);
}
};
/**
* Create A2A task provider from environment variables
*/
export const createA2ATaskProviderFromEnv = async (externalClients) => {
const taskMemoryType = process.env.JAF_A2A_TASK_PROVIDER_TYPE || 'memory';
switch (taskMemoryType) {
case 'memory':
return createA2AInMemoryTaskProvider({
type: 'memory',
keyPrefix: process.env.JAF_A2A_TASK_PROVIDER_KEY_PREFIX || 'jaf:a2a:tasks:',
defaultTtl: process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL ? parseInt(process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL) : undefined,
cleanupInterval: parseInt(process.env.JAF_A2A_TASK_PROVIDER_CLEANUP_INTERVAL || '3600'),
maxTasks: parseInt(process.env.JAF_A2A_TASK_PROVIDER_MAX_TASKS || '10000'),
maxTasksPerContext: parseInt(process.env.JAF_A2A_TASK_PROVIDER_MAX_TASKS_PER_CONTEXT || '1000'),
enableHistory: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_HISTORY !== 'false',
enableArtifacts: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_ARTIFACTS !== 'false'
});
case 'redis':
if (!externalClients?.redis) {
// Fall back to in-memory provider when Redis client is not available
console.warn('Redis client not provided, falling back to in-memory A2A task provider');
return createA2AInMemoryTaskProvider({
type: 'memory',
keyPrefix: process.env.JAF_A2A_TASK_PROVIDER_KEY_PREFIX || 'jaf:a2a:tasks:',
defaultTtl: process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL ? parseInt(process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL) : undefined,
cleanupInterval: parseInt(process.env.JAF_A2A_TASK_PROVIDER_CLEANUP_INTERVAL || '3600'),
maxTasks: parseInt(process.env.JAF_A2A_TASK_PROVIDER_MAX_TASKS || '10000'),
maxTasksPerContext: parseInt(process.env.JAF_A2A_TASK_PROVIDER_MAX_TASKS_PER_CONTEXT || '1000'),
enableHistory: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_HISTORY !== 'false',
enableArtifacts: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_ARTIFACTS !== 'false'
});
}
return await createA2ARedisTaskProvider({
type: 'redis',
keyPrefix: process.env.JAF_A2A_TASK_PROVIDER_KEY_PREFIX || 'jaf:a2a:tasks:',
defaultTtl: process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL ? parseInt(process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL) : undefined,
cleanupInterval: parseInt(process.env.JAF_A2A_TASK_PROVIDER_CLEANUP_INTERVAL || '3600'),
maxTasks: parseInt(process.env.JAF_A2A_TASK_PROVIDER_MAX_TASKS || '10000'),
enableHistory: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_HISTORY !== 'false',
enableArtifacts: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_ARTIFACTS !== 'false',
host: process.env.JAF_A2A_TASK_PROVIDER_REDIS_HOST || 'localhost',
port: parseInt(process.env.JAF_A2A_TASK_PROVIDER_REDIS_PORT || '6379'),
password: process.env.JAF_A2A_TASK_PROVIDER_REDIS_PASSWORD,
db: parseInt(process.env.JAF_A2A_TASK_PROVIDER_REDIS_DB || '0')
}, externalClients.redis);
case 'postgres':
if (!externalClients?.postgres) {
// Fall back to in-memory provider when PostgreSQL client is not available
console.warn('PostgreSQL client not provided, falling back to in-memory A2A task provider');
return createA2AInMemoryTaskProvider({
type: 'memory',
keyPrefix: process.env.JAF_A2A_TASK_PROVIDER_KEY_PREFIX || 'jaf:a2a:tasks:',
defaultTtl: process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL ? parseInt(process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL) : undefined,
cleanupInterval: parseInt(process.env.JAF_A2A_TASK_PROVIDER_CLEANUP_INTERVAL || '3600'),
maxTasks: parseInt(process.env.JAF_A2A_TASK_PROVIDER_MAX_TASKS || '10000'),
maxTasksPerContext: parseInt(process.env.JAF_A2A_TASK_PROVIDER_MAX_TASKS_PER_CONTEXT || '1000'),
enableHistory: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_HISTORY !== 'false',
enableArtifacts: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_ARTIFACTS !== 'false'
});
}
return await createA2APostgresTaskProvider({
type: 'postgres',
keyPrefix: process.env.JAF_A2A_TASK_PROVIDER_KEY_PREFIX || 'jaf:a2a:tasks:',
defaultTtl: process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL ? parseInt(process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL) : undefined,
cleanupInterval: parseInt(process.env.JAF_A2A_TASK_PROVIDER_CLEANUP_INTERVAL || '3600'),
maxTasks: parseInt(process.env.JAF_A2A_TASK_PROVIDER_MAX_TASKS || '10000'),
enableHistory: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_HISTORY !== 'false',
enableArtifacts: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_ARTIFACTS !== 'false',
host: process.env.JAF_A2A_TASK_PROVIDER_POSTGRES_HOST || 'localhost',
port: parseInt(process.env.JAF_A2A_TASK_PROVIDER_POSTGRES_PORT || '5432'),
database: process.env.JAF_A2A_TASK_PROVIDER_POSTGRES_DATABASE || 'jaf_a2a',
username: process.env.JAF_A2A_TASK_PROVIDER_POSTGRES_USERNAME || 'postgres',
password: process.env.JAF_A2A_TASK_PROVIDER_POSTGRES_PASSWORD,
ssl: process.env.JAF_A2A_TASK_PROVIDER_POSTGRES_SSL === 'true',
tableName: process.env.JAF_A2A_TASK_PROVIDER_POSTGRES_TABLE || 'a2a_tasks',
maxConnections: parseInt(process.env.JAF_A2A_TASK_PROVIDER_POSTGRES_MAX_CONNECTIONS || '10')
}, externalClients.postgres);
default:
// Fall back to in-memory provider for unknown types
console.warn(`Unknown A2A task provider type "${taskMemoryType}", falling back to in-memory provider`);
return createA2AInMemoryTaskProvider({
type: 'memory',
keyPrefix: process.env.JAF_A2A_TASK_PROVIDER_KEY_PREFIX || 'jaf:a2a:tasks:',
defaultTtl: process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL ? parseInt(process.env.JAF_A2A_TASK_PROVIDER_DEFAULT_TTL) : undefined,
cleanupInterval: parseInt(process.env.JAF_A2A_TASK_PROVIDER_CLEANUP_INTERVAL || '3600'),
maxTasks: parseInt(process.env.JAF_A2A_TASK_PROVIDER_MAX_TASKS || '10000'),
maxTasksPerContext: parseInt(process.env.JAF_A2A_TASK_PROVIDER_MAX_TASKS_PER_CONTEXT || '1000'),
enableHistory: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_HISTORY !== 'false',
enableArtifacts: process.env.JAF_A2A_TASK_PROVIDER_ENABLE_ARTIFACTS !== 'false'
});
}
};
export async function createSimpleA2ATaskProvider(type, client, config) {
switch (type) {
case 'memory':
return createA2AInMemoryTaskProvider({
type: 'memory',
keyPrefix: 'jaf:a2a:tasks:',
defaultTtl: undefined,
cleanupInterval: 3600,
maxTasks: 10000,
maxTasksPerContext: 1000,
enableHistory: true,
enableArtifacts: true,
...config
});
case 'redis':
if (!client) {
throw new Error('Redis client required for Redis A2A task provider');
}
return await createA2ARedisTaskProvider({
type: 'redis',
keyPrefix: 'jaf:a2a:tasks:',
defaultTtl: undefined,
cleanupInterval: 3600,
maxTasks: 10000,
enableHistory: true,
enableArtifacts: true,
host: 'localhost',
port: 6379,
password: undefined,
db: 0,
...config
}, client);
case 'postgres':
if (!client) {
throw new Error('PostgreSQL client required for PostgreSQL A2A task provider');
}
return await createA2APostgresTaskProvider({
type: 'postgres',
keyPrefix: 'jaf:a2a:tasks:',
defaultTtl: undefined,
cleanupInterval: 3600,
maxTasks: 10000,
enableHistory: true,
enableArtifacts: true,
host: 'localhost',
port: 5432,
database: 'jaf_a2a',
username: 'postgres',
password: undefined,
ssl: false,
tableName: 'a2a_tasks',
maxConnections: 10,
...config
}, client);
default:
throw new Error(`Unknown A2A task provider type: ${type}`);
}
}
/**
* Create a composite A2A task provider that can use multiple backends
* Useful for implementing failover or read/write splitting
*/
export const createCompositeA2ATaskProvider = (primary, fallback) => {
return {
storeTask: async (task, metadata) => {
const result = await primary.storeTask(task, metadata);
if (!result.success && fallback) {
return await fallback.storeTask(task, metadata);
}
return result;
},
getTask: async (taskId) => {
const result = await primary.getTask(taskId);
if (!result.success && fallback) {
return await fallback.getTask(taskId);
}
return result;
},
updateTask: async (task, metadata) => {
const result = await primary.updateTask(task, metadata);
if (!result.success && fallback) {
return await fallback.updateTask(task, metadata);
}
return result;
},
updateTaskStatus: async (taskId, state, statusMessage, timestamp) => {
const result = await primary.updateTaskStatus(taskId, state, statusMessage, timestamp);
if (!result.success && fallback) {
return await fallback.updateTaskStatus(taskId, state, statusMessage, timestamp);
}
return result;
},
findTasks: async (query) => {
const result = await primary.findTasks(query);
if (!result.success && fallback) {
return await fallback.findTasks(query);
}
return result;
},
getTasksByContext: async (contextId, limit) => {
const result = await primary.getTasksByContext(contextId, limit);
if (!result.success && fallback) {
return await fallback.getTasksByContext(contextId, limit);
}
return result;
},
deleteTask: async (taskId) => {
const result = await primary.deleteTask(taskId);
// For delete operations, try both providers regardless of success
if (fallback) {
await fallback.deleteTask(taskId);
}
return result;
},
deleteTasksByContext: async (contextId) => {
const result = await primary.deleteTasksByContext(contextId);
// For delete operations, try both providers regardless of success
if (fallback) {
await fallback.deleteTasksByContext(contextId);
}
return result;
},
cleanupExpiredTasks: async () => {
const primaryResult = await primary.cleanupExpiredTasks();
let totalCleaned = primaryResult.success ? primaryResult.data : 0;
if (fallback) {
const fallbackResult = await fallback.cleanupExpiredTasks();
if (fallbackResult.success) {
totalCleaned += fallbackResult.data;
}
}
return primaryResult.success
? { success: true, data: totalCleaned }
: primaryResult;
},
getTaskStats: async (contextId) => {
const result = await primary.getTaskStats(contextId);
if (!result.success && fallback) {
return await fallback.getTaskStats(contextId);
}
return result;
},
healthCheck: async () => {
const primaryHealth = await primary.healthCheck();
const fallbackHealth = fallback ? await fallback.healthCheck() : { success: true, data: { healthy: true } };
const isPrimaryHealthy = primaryHealth.success && primaryHealth.data.healthy;
const isFallbackHealthy = fallbackHealth.success && fallbackHealth.data.healthy;
return {
success: true,
data: {
healthy: isPrimaryHealthy || isFallbackHealthy,
latencyMs: primaryHealth.success ? primaryHealth.data.latencyMs : undefined,
error: isPrimaryHealthy ? undefined : (primaryHealth.success ? primaryHealth.data.error : 'Primary provider failed')
}
};
},
close: async () => {
const results = await Promise.allSettled([
primary.close(),
fallback ? fallback.close() : Promise.resolve({ success: true, data: undefined })
]);
const primaryResult = results[0];
if (primaryResult.status === 'fulfilled' && primaryResult.value.success) {
return primaryResult.value;
}
throw new Error('Failed to close composite A2A task provider');
}
};
};
/**
* Pure function to validate A2A task provider configuration
*/
export const validateA2ATaskProviderConfig = (config) => {
const errors = [];
try {
// Basic checks first
if (!config || typeof config !== 'object') {
errors.push('Configuration must be a valid object');
return { valid: false, errors };
}
// Check for completely empty config (should fail)
if (Object.keys(config).length === 0) {
errors.push('Configuration cannot be empty');
return { valid: false, errors };
}
// Check for missing type field (this is the only truly required field)
if (!config.type) {
errors.push('Provider type is required');
return { valid: false, errors };
}
// Try to parse with the schema - this will validate type and apply defaults
const parsed = A2ATaskProviderConfigSchema.safeParse(config);
if (!parsed.success) {
// Extract error messages from Zod validation
parsed.error.errors.forEach((err) => {
const path = err.path.length > 0 ? err.path.join('.') : 'root';
errors.push(`${path}: ${err.message}`);
});
}
// Additional custom validation for values that would be problematic
if (config.maxTasks !== undefined && config.maxTasks <= 0) {
errors.push('maxTasks must be greater than 0');
}
if (config.cleanupInterval !== undefined && config.cleanupInterval <= 0) {
errors.push('cleanupInterval must be greater than 0');
}
if (config.defaultTtl !== undefined && config.defaultTtl <= 0) {
errors.push('defaultTtl must be greater than 0');
}
// Type-specific validation
switch (config.type) {
case 'memory':
if (config.maxTasksPerContext !== undefined && config.maxTasksPerContext <= 0) {
errors.push('maxTasksPerContext must be greater than 0');
}
break;
case 'redis':
if (config.port !== undefined && (config.port < 1 || config.port > 65535)) {
errors.push('Redis port must be between 1 and 65535');
}
if (config.db !== undefined && config.db < 0) {
errors.push('Redis database index must be non-negative');
}
break;
case 'postgres':
if (config.port !== undefined && (config.port < 1 || config.port > 65535)) {
errors.push('PostgreSQL port must be between 1 and 65535');
}
if (config.maxConnections !== undefined && config.maxConnections <= 0) {
errors.push('maxConnections must be greater than 0');
}
break;
}
}
catch (error) {
errors.push(`Validation error: ${error.message || 'Unknown error'}`);
}
return {
valid: errors.length === 0,
errors
};
};
//# sourceMappingURL=factory.js.map