base-handler.ts•7.59 kB
import { Logger } from '../../utils/logger.js';
import { SFCCConfig } from '../../types/types.js';
export interface HandlerContext {
  logger: Logger;
  config: SFCCConfig;
  capabilities: {
    canAccessLogs: boolean;
    canAccessOCAPI: boolean;
  };
}
export interface ToolExecutionResult {
  content: Array<{ type: 'text'; text: string }>;
  isError?: boolean;
}
export interface ToolArguments {
  [key: string]: any;
}
/**
 * Generic tool specification interface
 * Defines the contract for declarative tool configuration
 */
export interface GenericToolSpec<TArgs = ToolArguments, TResult = any> {
  /** Optional validation function for tool arguments */
  validate?: (args: TArgs, toolName: string) => void;
  /** Optional function to apply default values to arguments */
  defaults?: (args: TArgs) => Partial<TArgs>;
  /** Main execution function for the tool */
  exec: (args: TArgs, context: ToolExecutionContext) => Promise<TResult>;
  /** Function to generate log message for the tool execution */
  logMessage: (args: TArgs) => string;
}
/**
 * Context provided to tool execution functions
 * Allows tools to access clients and other resources
 */
export interface ToolExecutionContext {
  /** Handler context with configuration and capabilities */
  handlerContext: HandlerContext;
  /** Logger instance for the handler */
  logger: any;
  /** Additional context data that can be provided by concrete handlers */
  [key: string]: any;
}
export class HandlerError extends Error {
  constructor(
    message: string,
    public readonly toolName: string,
    public readonly code: string = 'HANDLER_ERROR',
    public readonly details?: any,
  ) {
    super(message);
    this.name = 'HandlerError';
  }
}
export abstract class BaseToolHandler<TToolName extends string = string> {
  protected context: HandlerContext;
  protected logger: Logger;
  private _isInitialized = false;
  constructor(context: HandlerContext, subLoggerName: string) {
    this.context = context;
    this.logger = Logger.getChildLogger(`Handler:${subLoggerName}`);
  }
  /**
   * Abstract method to get tool configuration
   * Each concrete handler implements this with their specific config
   */
  protected abstract getToolConfig(): Record<TToolName, GenericToolSpec>;
  /**
   * Abstract method to get tool name set for O(1) lookup
   * Each concrete handler implements this with their specific tool set
   */
  protected abstract getToolNameSet(): Set<string>;
  /**
   * Abstract method to create execution context
   * Each concrete handler can provide specialized context
   */
  protected abstract createExecutionContext(): Promise<ToolExecutionContext>;
  /**
   * Check if this handler can handle the given tool
   */
  canHandle(toolName: string): boolean {
    return this.getToolNameSet().has(toolName);
  }
  /**
   * Config-driven tool execution
   * Handles validation, defaults, execution, and logging uniformly
   */
  async handle(toolName: string, args: ToolArguments, startTime: number): Promise<ToolExecutionResult> {
    if (!this.canHandle(toolName)) {
      throw new Error(`Unsupported tool: ${toolName}`);
    }
    const toolConfig = this.getToolConfig();
    const spec = toolConfig[toolName as TToolName];
    if (!spec) {
      throw new Error(`No configuration found for tool: ${toolName}`);
    }
    return this.executeWithLogging(
      toolName,
      startTime,
      () => this.dispatchTool(spec, args),
      spec.logMessage(this.applyDefaults(spec, args)),
    );
  }
  /**
   * Generic tool dispatch using configuration
   * Handles validation, defaults, and execution
   */
  private async dispatchTool(spec: GenericToolSpec, args: ToolArguments): Promise<any> {
    const context = await this.createExecutionContext();
    const processedArgs = this.createValidatedArgs(spec, args, 'tool');
    return spec.exec(processedArgs, context);
  }
  /**
   * Apply default values to arguments
   */
  private applyDefaults(spec: GenericToolSpec, args: ToolArguments): ToolArguments {
    if (!spec.defaults) {
      return args;
    }
    const defaults = spec.defaults(args);
    return { ...args, ...defaults };
  }
  /**
   * Create validated arguments with defaults applied
   */
  private createValidatedArgs(spec: GenericToolSpec, args: ToolArguments, toolName: string): ToolArguments {
    // Apply defaults first
    const processedArgs = this.applyDefaults(spec, args);
    // Validate if validator exists
    if (spec.validate) {
      spec.validate(processedArgs, toolName);
    }
    return processedArgs;
  }
  /**
   * Initialize the handler (lazy initialization)
   */
  protected async initialize(): Promise<void> {
    if (this._isInitialized) {
      return;
    }
    await this.onInitialize();
    this._isInitialized = true;
  }
  /**
   * Override this method for custom initialization logic
   */
  protected async onInitialize(): Promise<void> {
    // Default: no-op
  }
  /**
   * Clean up resources when handler is destroyed
   */
  async dispose(): Promise<void> {
    await this.onDispose();
    this._isInitialized = false;
  }
  /**
   * Override this method for custom cleanup logic
   */
  protected async onDispose(): Promise<void> {
    // Default: no-op
  }
  /**
   * Validate required arguments
   */
  protected validateArgs(args: ToolArguments, required: string[], toolName: string): void {
    for (const field of required) {
      if (!args?.[field]) {
        throw new HandlerError(
          `${field} is required`,
          toolName,
          'MISSING_ARGUMENT',
          { required, provided: Object.keys(args || {}) },
        );
      }
    }
  }
  /**
   * Create a standardized response
   */
  protected createResponse(data: any, stringify: boolean = true): ToolExecutionResult {
    return {
      content: [
        { type: 'text', text: stringify ? JSON.stringify(data, null, 2) : data },
      ],
      isError: false,
    };
  }
  /**
   * Create an error response
   */
  protected createErrorResponse(error: Error, toolName: string): ToolExecutionResult {
    this.logger.error(`Error in ${toolName}:`, error);
    return {
      content: [
        {
          type: 'text',
          text: error instanceof HandlerError
            ? `Error: ${error.message}`
            : `Error: ${error.message}`,
        },
      ],
      isError: true,
    };
  }
  /**
   * Execute a tool operation with standardized logging and error handling
   */
  protected async executeWithLogging(
    toolName: string,
    startTime: number,
    operation: () => Promise<any>,
    logMessage?: string,
  ): Promise<ToolExecutionResult> {
    try {
      await this.initialize();
      if (logMessage) {
        this.logger.debug(logMessage);
      }
      const result = await operation();
      this.logger.timing(toolName, startTime);
      // Log result metadata for debugging
      this.logger.debug(`${toolName} completed successfully`, {
        resultType: typeof result,
        resultLength: Array.isArray(result) ? result.length : undefined,
        hasData: result != null,
      });
      return this.createResponse(result);
    } catch (error) {
      this.logger.timing(`${toolName}_error`, startTime);
      return this.createErrorResponse(error as Error, toolName);
    }
  }
  /**
   * @deprecated Use executeWithLogging instead
   */
  protected async wrap(
    toolName: string,
    startTime: number,
    fn: () => Promise<any>,
    logMessage?: string,
  ): Promise<ToolExecutionResult> {
    return this.executeWithLogging(toolName, startTime, fn, logMessage);
  }
}