@xynehq/jaf
Version:
Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools
281 lines • 12 kB
JavaScript
/**
* A2A Task Cleanup Service for JAF
* Pure functional cleanup and expiration policies for A2A tasks
*/
import { createA2ATaskStorageError, createSuccess, createFailure } from './types.js';
/**
* Default cleanup configuration
*/
export const defaultCleanupConfig = {
enabled: true,
interval: 3600000, // 1 hour
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
maxCompletedTasks: 1000,
maxFailedTasks: 500,
retainStates: ['working', 'input-required', 'submitted'],
batchSize: 100,
dryRun: false
};
/**
* Pure function to perform task cleanup
*/
export const performTaskCleanup = async (taskProvider, config = defaultCleanupConfig) => {
try {
const errors = [];
let expiredCleaned = 0;
let excessCompletedCleaned = 0;
let excessFailedCleaned = 0;
// Step 1: Clean up expired tasks
if (config.enabled) {
try {
const expiredResult = await taskProvider.cleanupExpiredTasks();
if (expiredResult.success) {
expiredCleaned = expiredResult.data;
if (config.dryRun) {
console.log(`[DRY RUN] Would clean up ${expiredCleaned} expired tasks`);
expiredCleaned = 0; // Reset for dry run
}
}
else {
errors.push(`Failed to cleanup expired tasks: ${expiredResult.error.message}`);
}
}
catch (error) {
errors.push(`Error during expired task cleanup: ${error.message}`);
}
}
// Step 2: Clean up excess completed tasks
if (config.enabled && config.maxCompletedTasks > 0) {
try {
const completedTasksResult = await taskProvider.findTasks({
state: 'completed',
limit: config.maxCompletedTasks + config.batchSize
});
if (completedTasksResult.success) {
const completedTasks = completedTasksResult.data;
if (completedTasks.length > config.maxCompletedTasks) {
// Sort by completion time (oldest first) and remove excess
const sortedTasks = completedTasks
.sort((a, b) => {
const timeA = new Date(a.status.timestamp || '').getTime();
const timeB = new Date(b.status.timestamp || '').getTime();
return timeA - timeB;
});
const tasksToDelete = sortedTasks.slice(0, sortedTasks.length - config.maxCompletedTasks);
if (config.dryRun) {
console.log(`[DRY RUN] Would clean up ${tasksToDelete.length} excess completed tasks`);
}
else {
for (const task of tasksToDelete) {
const deleteResult = await taskProvider.deleteTask(task.id);
if (deleteResult.success && deleteResult.data) {
excessCompletedCleaned++;
}
else {
errors.push(`Failed to delete completed task ${task.id}`);
}
}
}
}
}
else {
errors.push(`Failed to find completed tasks: ${completedTasksResult.error.message}`);
}
}
catch (error) {
errors.push(`Error during completed task cleanup: ${error.message}`);
}
}
// Step 3: Clean up excess failed tasks
if (config.enabled && config.maxFailedTasks > 0) {
try {
const failedTasksResult = await taskProvider.findTasks({
state: 'failed',
limit: config.maxFailedTasks + config.batchSize
});
if (failedTasksResult.success) {
const failedTasks = failedTasksResult.data;
if (failedTasks.length > config.maxFailedTasks) {
// Sort by failure time (oldest first) and remove excess
const sortedTasks = failedTasks
.sort((a, b) => {
const timeA = new Date(a.status.timestamp || '').getTime();
const timeB = new Date(b.status.timestamp || '').getTime();
return timeA - timeB;
});
const tasksToDelete = sortedTasks.slice(0, sortedTasks.length - config.maxFailedTasks);
if (config.dryRun) {
console.log(`[DRY RUN] Would clean up ${tasksToDelete.length} excess failed tasks`);
}
else {
for (const task of tasksToDelete) {
const deleteResult = await taskProvider.deleteTask(task.id);
if (deleteResult.success && deleteResult.data) {
excessFailedCleaned++;
}
else {
errors.push(`Failed to delete failed task ${task.id}`);
}
}
}
}
}
else {
errors.push(`Failed to find failed tasks: ${failedTasksResult.error.message}`);
}
}
catch (error) {
errors.push(`Error during failed task cleanup: ${error.message}`);
}
}
// Step 4: Clean up old tasks beyond max age
if (config.enabled && config.maxAge > 0) {
try {
const cutoffDate = new Date(Date.now() - config.maxAge);
// Find old completed and failed tasks
for (const state of ['completed', 'failed', 'canceled']) {
if (config.retainStates.includes(state))
continue;
const oldTasksResult = await taskProvider.findTasks({
state: state,
until: cutoffDate,
limit: config.batchSize
});
if (oldTasksResult.success) {
const oldTasks = oldTasksResult.data;
if (config.dryRun) {
console.log(`[DRY RUN] Would clean up ${oldTasks.length} old ${state} tasks`);
}
else {
for (const task of oldTasks) {
const deleteResult = await taskProvider.deleteTask(task.id);
if (deleteResult.success && deleteResult.data) {
if (state === 'completed') {
excessCompletedCleaned++;
}
else if (state === 'failed') {
excessFailedCleaned++;
}
}
else {
errors.push(`Failed to delete old ${state} task ${task.id}`);
}
}
}
}
else {
errors.push(`Failed to find old ${state} tasks: ${oldTasksResult.error.message}`);
}
}
}
catch (error) {
errors.push(`Error during old task cleanup: ${error.message}`);
}
}
const totalCleaned = expiredCleaned + excessCompletedCleaned + excessFailedCleaned;
return createSuccess({
expiredCleaned,
excessCompletedCleaned,
excessFailedCleaned,
totalCleaned,
errors
});
}
catch (error) {
return createFailure(createA2ATaskStorageError('cleanup', 'unknown', undefined, error));
}
};
/**
* Pure function to create a cleanup scheduler
*/
export const createTaskCleanupScheduler = (taskProvider, config = defaultCleanupConfig) => {
let intervalId = null;
let isRunning = false;
const start = () => {
if (isRunning || !config.enabled)
return;
isRunning = true;
const runCleanup = async () => {
try {
const result = await performTaskCleanup(taskProvider, config);
if (result.success) {
const { totalCleaned, errors } = result.data;
if (totalCleaned > 0 || errors.length > 0) {
console.log(`A2A task cleanup completed: ${totalCleaned} tasks cleaned`);
if (errors.length > 0) {
console.warn(`A2A task cleanup errors: ${errors.join(', ')}`);
}
}
}
else {
console.error(`A2A task cleanup failed: ${result.error.message}`);
}
}
catch (error) {
console.error(`A2A task cleanup error: ${error.message}`);
}
};
// Run initial cleanup
runCleanup();
// Schedule periodic cleanup
intervalId = setInterval(runCleanup, config.interval);
};
const stop = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
isRunning = false;
};
const runOnce = () => {
return performTaskCleanup(taskProvider, config);
};
return {
start,
stop,
runOnce,
isRunning: () => isRunning,
config
};
};
/**
* Pure function to validate cleanup configuration
*/
export const validateCleanupConfig = (config) => {
const errors = [];
if (config.interval !== undefined && config.interval <= 0) {
errors.push('Cleanup interval must be greater than 0');
}
if (config.maxAge !== undefined && config.maxAge <= 0) {
errors.push('Max age must be greater than 0');
}
if (config.maxCompletedTasks !== undefined && config.maxCompletedTasks < 0) {
errors.push('Max completed tasks must be non-negative');
}
if (config.maxFailedTasks !== undefined && config.maxFailedTasks < 0) {
errors.push('Max failed tasks must be non-negative');
}
if (config.batchSize !== undefined && config.batchSize <= 0) {
errors.push('Batch size must be greater than 0');
}
return {
valid: errors.length === 0,
errors
};
};
/**
* Helper function to create cleanup config from environment variables
*/
export const createCleanupConfigFromEnv = () => {
return {
enabled: process.env.JAF_A2A_CLEANUP_ENABLED !== 'false',
interval: parseInt(process.env.JAF_A2A_CLEANUP_INTERVAL || '3600000'),
maxAge: parseInt(process.env.JAF_A2A_CLEANUP_MAX_AGE || '604800000'), // 7 days
maxCompletedTasks: parseInt(process.env.JAF_A2A_CLEANUP_MAX_COMPLETED || '1000'),
maxFailedTasks: parseInt(process.env.JAF_A2A_CLEANUP_MAX_FAILED || '500'),
retainStates: (process.env.JAF_A2A_CLEANUP_RETAIN_STATES || 'working,input-required,submitted').split(','),
batchSize: parseInt(process.env.JAF_A2A_CLEANUP_BATCH_SIZE || '100'),
dryRun: process.env.JAF_A2A_CLEANUP_DRY_RUN === 'true'
};
};
//# sourceMappingURL=cleanup.js.map