UNPKG

@smythos/sdk

Version:
476 lines (443 loc) 16.3 kB
import { AccessCandidate, ConnectorService, DEFAULT_TEAM_ID, IScheduledJob, ISchedulerRequest, IJobMetadata, Job, Schedule, SchedulerConnector, TConnectorService, } from '@smythos/sre'; import { SDKObject } from '../Core/SDKObject.class'; import { TSchedulerProvider } from '../types/generated/Scheduler.types'; import { Agent } from '../Agent/Agent.class'; import EventEmitter from 'events'; /** * Simple hash function to generate a short unique ID from a string. * Not cryptographically secure - just for generating unique job IDs. */ function simpleHash(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash).toString(36); } /** * Builder class for creating scheduled jobs with chainable syntax. * Requires calling `.every()` or `.cron()` to actually schedule the job. * After scheduling, provides methods to pause, resume, or delete the job. * * @internal */ class SchedulerJobCommand { private _schedulerRequest: ISchedulerRequest; private _jobId: string; private _jobConfig: any; private _scheduled: boolean = false; private job: Job; private _scheduledJob: IScheduledJob; constructor(schedulerRequest: ISchedulerRequest, jobId: string, jobConfig: any) { this._schedulerRequest = schedulerRequest; this._jobId = jobId; this._jobConfig = jobConfig; } /** * Complete the job scheduling with an interval. * Returns the builder to allow job control operations. * * @param interval - Time interval (e.g., '5s', '10m', '1h', '1d') * @returns The builder instance with pause, resume, and delete methods * * @example * ```typescript * const job = await scheduler.call('skill-name', args).every('5s'); * // Later, you can pause, resume, or delete the job * await job.pause(); * await job.resume(); * await job.delete(); * ``` */ async every(interval: string): Promise<void> { this._scheduled = true; this.job = new Job(this._jobConfig); const schedule = Schedule.every(interval); await this._schedulerRequest.add(this._jobId, this.job, schedule); } /** * Complete the job scheduling with a cron expression. * * @param cronExpression - Cron expression (e.g., '0 0 * * *' for daily at midnight) * @returns The builder instance with pause, resume, and delete methods * * @example * ```typescript * const job = await scheduler.call('backup', {}).cron('0 0 * * *'); * await job.pause(); * ``` */ //TODO : implement cron scheduling // async cron(cronExpression: string): Promise<this> { // this._scheduled = true; // const job = new Job(this._jobConfig); // const schedule = Schedule.cron(cronExpression); // await this._schedulerRequest.add(this._jobId, job, schedule); // return this; // } /** * Pause the scheduled job. * * @returns Promise that resolves when the job is paused * * @example * ```typescript * const job = await scheduler.call('backup', {}).every('1h'); * await job.pause(); * ``` */ async pause(): Promise<void> { await this._schedulerRequest.pause(this._jobId); } /** * Resume a paused job. * * @returns Promise that resolves when the job is resumed * * @example * ```typescript * const job = await scheduler.call('backup', {}).every('1h'); * await job.pause(); * // Later... * await job.resume(); * ``` */ async resume(): Promise<void> { await this._schedulerRequest.resume(this._jobId); } /** * Delete the scheduled job permanently. * * @returns Promise that resolves when the job is deleted * * @example * ```typescript * const job = await scheduler.call('backup', {}).every('1h'); * // Later, when no longer needed... * await job.delete(); * ``` */ async delete(): Promise<void> { await this._schedulerRequest.delete(this._jobId); } /** * Override the then() method to detect when the builder is awaited without calling .every() or .cron(). * This makes the builder thenable, but will warn if scheduling is not completed properly. */ then<TResult1 = void, TResult2 = never>( onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null ): Promise<TResult1 | TResult2> { if (!this._scheduled) { const warning = `⚠️ Warning: Job '${this._jobId}' was not scheduled.\n` + ` Complete the chain with .every('interval') in order to schedule the job.`; console.warn(warning); } return Promise.resolve().then(onfulfilled, onrejected); } } /** * SDK wrapper for Scheduler operations. * Provides a simplified interface for scheduling and managing jobs. * * @example * ```typescript * const scheduler = new SchedulerInstance('LocalScheduler', {}, AccessCandidate.agent('my-agent')); * * await scheduler.add('cleanup', Schedule.every('1h'), new Job( * async () => console.log('Cleanup'), * { name: 'Cleanup Job' } * )); * ``` */ export class SchedulerInstance extends SDKObject { private _candidate: AccessCandidate; private _agent: Agent; private _schedulerRequest: ISchedulerRequest; private _teamId: string; constructor(providerId?: TSchedulerProvider, schedulerSettings: any = {}, candidate?: AccessCandidate | Agent) { super(); this._agent = candidate instanceof Agent ? (candidate as Agent) : (undefined as Agent); this._candidate = this._agent ? AccessCandidate.agent(this._agent.id) : (candidate as AccessCandidate) || AccessCandidate.team(DEFAULT_TEAM_ID); // Get or initialize the scheduler connector let connector = ConnectorService.getSchedulerConnector(providerId || ''); if (!connector?.valid) { connector = ConnectorService.init(TConnectorService.Scheduler, providerId, providerId, schedulerSettings); if (!connector?.valid) { console.error(`Scheduler connector ${providerId} is not available`); throw new Error(`Scheduler connector ${providerId} is not available`); } } const instance: SchedulerConnector = connector.instance(schedulerSettings || connector.settings); this._schedulerRequest = instance.requester(this._candidate); } public on(event: string, listener: (...args: any[]) => void): this { this._schedulerRequest.on(event, listener); return this; } public off(event: string, listener: (...args: any[]) => void): this { this._schedulerRequest.off(event, listener); return this; } /** * Add or update a scheduled job. * If a job with the same ID exists, it will be updated. * * @param jobId - Unique identifier for the job * @param schedule - Schedule definition (interval or cron) * @param job - Job instance with execution function and metadata * @returns Promise that resolves when the job is scheduled * * @example * ```typescript * await scheduler.add('backup', * Schedule.every('6h'), * new Job(async () => { * console.log('Running backup...'); * }, { * name: 'Backup Job', * retryOnFailure: true * }) * ); * ``` */ async add(jobId: string, job: Job, schedule: Schedule): Promise<boolean> { try { await this._schedulerRequest.add(jobId, job, schedule); return true; } catch (error) { console.error(`❌ Error adding job ${jobId}:`, error.message || error); } return false; } /** * List all scheduled jobs for this candidate. * * @returns Promise that resolves to an array of scheduled jobs * * @example * ```typescript * const jobs = await scheduler.list(); * jobs.forEach(job => { * console.log(`${job.metadata.name}: ${job.status}`); * }); * ``` */ async list(): Promise<IScheduledJob[]> { try { return await this._schedulerRequest.list(); } catch (error) { console.error('Error listing jobs:', error); throw error; } } /** * Get details of a specific scheduled job. * * @param jobId - Unique identifier of the job * @returns Promise that resolves to the job details, or undefined if not found * * @example * ```typescript * const job = await scheduler.get('backup'); * if (job) { * console.log(`Status: ${job.status}`); * console.log(`Next run: ${job.nextRun}`); * } * ``` */ async get(jobId: string): Promise<IScheduledJob | undefined> { try { return await this._schedulerRequest.get(jobId); } catch (error) { console.error(`Error getting job ${jobId}:`, error); throw error; } } /** * Pause a scheduled job. * The job will not execute until resumed. * * @param jobId - Unique identifier of the job to pause * @returns Promise that resolves when the job is paused * * @example * ```typescript * await scheduler.pause('backup'); * console.log('Backup job paused'); * ``` */ async pause(jobId: string): Promise<void> { try { await this._schedulerRequest.pause(jobId); } catch (error) { console.error(`Error pausing job ${jobId}:`, error); throw error; } } /** * Resume a paused job. * The job will start executing according to its schedule. * * @param jobId - Unique identifier of the job to resume * @returns Promise that resolves when the job is resumed * * @example * ```typescript * await scheduler.resume('backup'); * console.log('Backup job resumed'); * ``` */ async resume(jobId: string): Promise<void> { try { await this._schedulerRequest.resume(jobId); } catch (error) { console.error(`Error resuming job ${jobId}:`, error); throw error; } } /** * Delete a scheduled job. * This removes the job permanently and stops all scheduled executions. * * @param jobId - Unique identifier of the job to delete * @returns Promise that resolves when the job is deleted * * @example * ```typescript * await scheduler.delete('backup'); * console.log('Backup job deleted'); * ``` */ async delete(jobId: string): Promise<void> { try { await this._schedulerRequest.delete(jobId); } catch (error) { console.error(`Error deleting job ${jobId}:`, error); throw error; } } /** * Schedule an agent skill execution with chainable syntax. * Requires an agent to be associated with this scheduler instance. * * @param skillName - Name of the skill to execute * @param args - Arguments to pass to the skill * @param metadata - Optional job metadata (name, description, retryOnFailure, etc.) * @returns SchedulerJobBuilder that can be chained with .every() or .cron() * * @throws Error if no agent is associated with this scheduler * * @example * ```typescript * // Schedule a skill to run every 5 seconds * await scheduler.call('processData', { input: 'test' }, { name: 'Data Processor' }).every('5s'); * * // Schedule with cron expression * await scheduler.call('backup', {}, { retryOnFailure: true }).cron('0 0 * * *'); * ``` */ call(skillName: string, args?: Record<string, any> | any[], metadata?: IJobMetadata): SchedulerJobCommand { if (!this._agent) { throw new Error('Cannot use .call() without an agent. '); } const jobId = `${this._agent.id}-skill-${skillName}`; const jobConfig = { type: 'skill' as const, agentId: this._agent.id, skillName: skillName, args: args, metadata: metadata, }; return new SchedulerJobCommand(this._schedulerRequest, jobId, jobConfig); } /** * Schedule an agent prompt execution with chainable syntax. * Requires an agent to be associated with this scheduler instance. * * @param prompt - The prompt text to send to the agent * @param metadata - Optional job metadata (name, description, retryOnFailure, etc.) * @returns SchedulerJobBuilder that can be chained with .every() or .cron() * * @throws Error if no agent is associated with this scheduler * * @example * ```typescript * // Schedule a prompt to run every hour * await scheduler.prompt('Generate daily report', { name: 'Daily Report' }).every('1h'); * * // Schedule with cron expression * await scheduler.prompt('Summarize news', { name: 'News Summary' }).cron('0 9 * * *'); * ``` */ prompt(prompt: string, metadata?: IJobMetadata): SchedulerJobCommand { if (!this._agent) { throw new Error( 'Cannot use .prompt() without an agent. ' + 'Please provide an Agent instance when creating the SchedulerInstance: ' + 'new SchedulerInstance(provider, settings, agent)' ); } const promptHash = simpleHash(prompt); const jobId = `${this._agent.id}-prompt-${promptHash}`; const jobConfig = { type: 'prompt' as const, agentId: this._agent.id, prompt: prompt, metadata: metadata, }; return new SchedulerJobCommand(this._schedulerRequest, jobId, jobConfig); } /** * Schedule an agent trigger execution with chainable syntax. * Requires an agent to be associated with this scheduler instance. * * @param triggerName - Name of the trigger to execute * @param metadata - Optional job metadata (name, description, retryOnFailure, etc.) * @returns SchedulerJobBuilder that can be chained with .every() or .cron() * * @throws Error if no agent is associated with this scheduler * * @example * ```typescript * // Schedule a trigger to run every day * await scheduler.trigger('daily-sync', { name: 'Daily Sync' }).every('1d'); * * // Schedule with cron expression * await scheduler.trigger('weekly-report', { name: 'Weekly Report' }).cron('0 0 * * 0'); * ``` */ trigger(triggerName: string, metadata?: IJobMetadata): SchedulerJobCommand { if (!this._agent) { throw new Error( 'Cannot use .trigger() without an agent. ' + 'Please provide an Agent instance when creating the SchedulerInstance: ' + 'new SchedulerInstance(provider, settings, agent)' ); } const jobId = `${this._agent.id}-trigger-${triggerName}`; const jobConfig = { type: 'trigger' as const, agentId: this._agent.id, triggerName: triggerName, metadata: metadata, }; return new SchedulerJobCommand(this._schedulerRequest, jobId, jobConfig); } }