@@ -127,6 +127,26 @@ export interface AgentResult {
127127 correlationId ?: string ;
128128}
129129
130+ // ─── Middleware types ─────────────────────────────────────────────────────────────
131+
132+ /**
133+ * Task middleware function type.
134+ *
135+ * Middleware functions are executed in registration order before the task
136+ * is dispatched to the appropriate tool. Each middleware receives the task
137+ * and a `next` function to continue execution. Calling `next()` passes control
138+ * to the next middleware or the actual task execution. If a middleware returns
139+ * a result without calling `next()`, it short-circuits execution.
140+ *
141+ * @param task - The task to be executed
142+ * @param next - Function to call the next middleware or the actual task
143+ * @returns The result of task execution or middleware short-circuit
144+ */
145+ export type TaskMiddleware = (
146+ task : AgentTask ,
147+ next : ( ) => Promise < AgentResult >
148+ ) => Promise < AgentResult > ;
149+
130150// ─── Payload sanitisation ─────────────────────────────────────────────────────
131151
132152const SECRET_KEY_RE = / ^ (?< prefix > .* ?[ " ' : \s ] ? ) (?< secret > S [ A - Z 2 - 7 ] { 55 } ) (?< suffix > [ " ' \s ] ? .* ) $ / i;
@@ -178,6 +198,9 @@ export class PayFiAgent extends EventEmitter {
178198 // reference — EventEmitter requires identity equality for removal.
179199 private readonly _boundHandlers = new Map < string , ( ...args : unknown [ ] ) => void > ( ) ;
180200
201+ // Middleware array for pre/post task execution hooks
202+ private middlewares : TaskMiddleware [ ] = [ ] ;
203+
181204 constructor ( ) {
182205 super ( ) ;
183206
@@ -337,6 +360,22 @@ export class PayFiAgent extends EventEmitter {
337360 logger . info ( "Agent draining — rejecting new tasks" ) ;
338361 }
339362
363+ /**
364+ * Register a middleware function for pre/post task execution hooks.
365+ *
366+ * Middleware functions are executed in registration order before the task
367+ * is dispatched to the appropriate tool. Each middleware can:
368+ * - Inspect and modify the task
369+ * - Short-circuit execution by returning a result without calling next()
370+ * - Call next() to continue to the next middleware or actual task execution
371+ *
372+ * @param middleware - Middleware function to register
373+ */
374+ use ( middleware : TaskMiddleware ) : void {
375+ this . middlewares . push ( middleware ) ;
376+ logger . info ( "Middleware registered" , { totalMiddlewares : this . middlewares . length } ) ;
377+ }
378+
340379 async waitForPendingTasks ( ) : Promise < void > {
341380 if ( this . activeTasks === 0 && this . taskQueue . length === 0 ) return ;
342381 logger . info ( "Waiting for pending tasks to finish" , {
@@ -450,74 +489,116 @@ export class PayFiAgent extends EventEmitter {
450489 ) : Promise < AgentResult > {
451490 this . activeTasks ++ ;
452491 taskLog . info ( { taskType : task . type } , "Running task" ) ;
453- try {
454- let data : unknown ;
455-
456- switch ( task . type ) {
457- case "stellar_payment" : {
458- const p = task . payload as Record < string , unknown > ;
459- assertWithinSpendingLimit ( p ?. amount ) ;
460- const paymentResult = await this . paymentTool . execute ( task . payload ) ;
461- data = {
462- ...paymentResult ,
463- network : config . STELLAR_NETWORK ,
464- } ;
465- break ;
466- }
467492
468- case "soroban_invoke" : {
469- data = await this . sorobanTool . execute ( task . payload ) ;
470- break ;
471- }
493+ // ── Compose middleware chain ────────────────────────────────────────────────
494+ const executeTask = async ( ) : Promise < AgentResult > => {
495+ try {
496+ let data : unknown ;
497+
498+ switch ( task . type ) {
499+ case "stellar_payment" : {
500+ const p = task . payload as Record < string , unknown > ;
501+ assertWithinSpendingLimit ( p ?. amount ) ;
502+ const paymentResult = await this . paymentTool . execute ( task . payload ) ;
503+ data = {
504+ ...paymentResult ,
505+ network : config . STELLAR_NETWORK ,
506+ } ;
507+ break ;
508+ }
472509
473- case "soroban_query" :
474- data = await this . sorobanQueryTool . query ( task . payload ) ;
475- break ;
510+ case "soroban_invoke" : {
511+ data = await this . sorobanTool . execute ( task . payload ) ;
512+ break ;
513+ }
476514
477- case "x402_respond" : {
478- const p = task . payload as Record < string , unknown > ;
479- assertWithinSpendingLimit ( p ?. amount ) ;
480- data = await this . x402Tool . respond ( task . payload ) ;
481- break ;
482- }
515+ case "soroban_query" :
516+ data = await this . sorobanQueryTool . query ( task . payload ) ;
517+ break ;
483518
484- case "account_info" :
485- data = await this . accountInfoTool . fetch ( ) ;
486- break ;
519+ case "x402_respond" : {
520+ const p = task . payload as Record < string , unknown > ;
521+ assertWithinSpendingLimit ( p ?. amount ) ;
522+ data = await this . x402Tool . respond ( task . payload ) ;
523+ break ;
524+ }
487525
488- case "change_trust" :
489- data = await this . trustlineTool . execute ( task . payload ) ;
490- break ;
526+ case "account_info" :
527+ data = await this . accountInfoTool . fetch ( ) ;
528+ break ;
529+
530+ case "change_trust" :
531+ data = await this . trustlineTool . execute ( task . payload ) ;
532+ break ;
491533
492- case "multisig_payment" :
493- data = await this . multiSigTool . execute ( task . payload ) ;
534+ case "multisig_payment" :
535+ data = await this . multiSigTool . execute ( task . payload ) ;
494536 break ;
495537
496538
497- case "batch_payment" :
498- data = await this . batchPaymentTool . execute ( task . payload ) ;
499- break ;
539+ case "batch_payment" :
540+ data = await this . batchPaymentTool . execute ( task . payload ) ;
541+ break ;
500542
501- case "balance_check" :
502- data = await this . balanceCheckTool . getBalance ( task . payload ) ;
503- break ;
543+ case "balance_check" :
544+ data = await this . balanceCheckTool . getBalance ( task . payload ) ;
545+ break ;
504546
505- case "path_payment" :
506- data = await this . pathPaymentTool . execute ( task . payload ) ;
507- break ;
547+ case "path_payment" :
548+ data = await this . pathPaymentTool . execute ( task . payload ) ;
549+ break ;
508550
509- case "fee_bump" :
510- data = await this . feeBumpTool . execute ( task . payload ) ;
511- break ;
551+ case "fee_bump" :
552+ data = await this . feeBumpTool . execute ( task . payload ) ;
553+ break ;
512554
513- case "dex_offer" :
514- data = await this . dexOfferTool . execute ( task . payload ) ;
515- break ;
555+ case "dex_offer" :
556+ data = await this . dexOfferTool . execute ( task . payload ) ;
557+ break ;
516558
517- default :
518- throw new Error ( `Unknown task type: ${ ( task as AgentTask ) . type } ` ) ;
559+ default :
560+ throw new Error ( `Unknown task type: ${ ( task as AgentTask ) . type } ` ) ;
561+ }
562+
563+ taskLog . info ( { taskType : task . type } , "Task completed" ) ;
564+ const result : AgentResult = { success : true , taskType : task . type , data, correlationId } ;
565+ this . emit ( "task:complete" , result ) ;
566+
567+ saveResult ( { ...result , timestamp : new Date ( ) . toISOString ( ) } ) ;
568+ void dispatchWebhook ( result ) ;
569+
570+ return result ;
571+ } catch ( err ) {
572+ const message = err instanceof Error ? err . message : String ( err ) ;
573+ const safe = redactSecretString ( message ) ;
574+ const sanitized = sanitizePayload ( task . payload ) ;
575+ taskLog . error (
576+ { taskType : task . type , error : safe , sanitizedPayload : sanitized } ,
577+ "Task failed"
578+ ) ;
579+ const result : AgentResult = {
580+ success : false ,
581+ taskType : task . type ,
582+ error : safe ,
583+ errorType : getErrorType ( err ) ,
584+ correlationId,
585+ } ;
586+ this . emit ( "task:failed" , result ) ;
587+ void dispatchWebhook ( result ) ;
588+ return result ;
519589 }
590+ } ;
520591
592+ // Build middleware chain: middleware[n] -> middleware[n-1] -> ... -> executeTask
593+ let chain : ( ) => Promise < AgentResult > = executeTask ;
594+ for ( let i = this . middlewares . length - 1 ; i >= 0 ; i -- ) {
595+ const middleware = this . middlewares [ i ] ;
596+ const next : ( ) => Promise < AgentResult > = chain ;
597+ chain = ( ) => middleware ( task , next ) ;
598+ }
599+
600+ try {
601+ return await chain ( ) ;
521602 taskLog . info ( { taskType : task . type } , "Task completed" ) ;
522603 const result : AgentResult = { success : true , taskType : task . type , data, correlationId } ;
523604 this . emit ( "task:complete" , result ) ;
0 commit comments