CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
validation.ts344 linesDownload Raw Back to subagents
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import { SubagentError, SubagentErrorCode } from './types.js';8import type { SubagentConfig, ValidationResult } from './types.js';9import type { RunConfig } from '../agents/runtime/agent-types.js';10import { resolveModelId } from '../utils/modelId.js';11 12/**13 * Validates subagent configurations to ensure they are well-formed14 * and compatible with the runtime system.15 */16export class SubagentValidator {17  /**18   * Validates a complete subagent configuration.19   *20   * @param config - The subagent configuration to validate21   * @returns ValidationResult with errors and warnings22   */23  validateConfig(config: SubagentConfig): ValidationResult {24    const errors: string[] = [];25    const warnings: string[] = [];26 27    const nameValidation = this.validateName(config.name);28    if (!nameValidation.isValid) {29      errors.push(...nameValidation.errors);30    }31 32    if (!config.description || config.description.trim().length === 0) {33      errors.push('Description is required and cannot be empty');34    } else if (config.description.length > 1000) {35      warnings.push(36        'Description is quite long (>1,000 chars), consider shortening for better readability',37      );38    }39 40    const promptValidation = this.validateSystemPrompt(config.systemPrompt);41    if (!promptValidation.isValid) {42      errors.push(...promptValidation.errors);43    }44    warnings.push(...promptValidation.warnings);45 46    if (config.tools) {47      const toolsValidation = this.validateTools(config.tools);48      if (!toolsValidation.isValid) {49        errors.push(...toolsValidation.errors);50      }51      warnings.push(...toolsValidation.warnings);52    }53 54    if (config.disallowedTools && config.disallowedTools.length > 0) {55      const disallowedValidation = this.validateTools(config.disallowedTools);56      if (!disallowedValidation.isValid) {57        errors.push(...disallowedValidation.errors);58      }59      warnings.push(...disallowedValidation.warnings);60    }61 62    if (config.model) {63      const modelValidation = this.validateModel(config.model);64      if (!modelValidation.isValid) {65        errors.push(...modelValidation.errors);66      }67      warnings.push(...modelValidation.warnings);68    }69 70    if (config.runConfig) {71      const runValidation = this.validateRunConfig(config.runConfig);72      if (!runValidation.isValid) {73        errors.push(...runValidation.errors);74      }75      warnings.push(...runValidation.warnings);76    }77 78    return {79      isValid: errors.length === 0,80      errors,81      warnings,82    };83  }84 85  /**86   * Validates a subagent name.87   * Names must be valid identifiers that can be used in file paths and tool calls.88   *89   * @param name - The name to validate90   * @returns ValidationResult91   */92  validateName(name: string): ValidationResult {93    const errors: string[] = [];94    const warnings: string[] = [];95 96    if (!name || name.trim().length === 0) {97      errors.push('Name is required and cannot be empty');98      return { isValid: false, errors, warnings };99    }100 101    const trimmedName = name.trim();102 103    if (trimmedName.length < 2) {104      errors.push('Name must be at least 2 characters long');105    }106 107    if (trimmedName.length > 50) {108      errors.push('Name must be 50 characters or less');109    }110 111    const validNameRegex = /^[\p{L}\p{N}_-]+$/u;112    if (!validNameRegex.test(trimmedName)) {113      errors.push(114        'Name can only contain letters, numbers, hyphens, and underscores',115      );116    }117 118    if (trimmedName.startsWith('-') || trimmedName.startsWith('_')) {119      errors.push('Name cannot start with a hyphen or underscore');120    }121 122    if (trimmedName.endsWith('-') || trimmedName.endsWith('_')) {123      errors.push('Name cannot end with a hyphen or underscore');124    }125 126    // Check for reserved names. `main` is the sentinel used by the /stats127    // attribution pipeline to label the main (non-subagent) conversation;128    // a subagent named `main` would collide with that sentinel and be129    // silently merged into the main bucket.130    const reservedNames = [131      'self',132      'system',133      'user',134      'model',135      'tool',136      'config',137      'default',138      'main',139    ];140    if (reservedNames.includes(trimmedName.toLowerCase())) {141      errors.push(`"${trimmedName}" is a reserved name and cannot be used`);142    }143 144    // Only warn about naming conventions for names that contain case distinctions145    if (146      trimmedName !== trimmedName.toLowerCase() &&147      /[a-zA-Z]/.test(trimmedName)148    ) {149      warnings.push('Consider using lowercase names for consistency');150    }151 152    if (trimmedName.includes('_') && trimmedName.includes('-')) {153      warnings.push(154        'Consider using either hyphens or underscores consistently, not both',155      );156    }157 158    return {159      isValid: errors.length === 0,160      errors,161      warnings,162    };163  }164 165  /**166   * Validates a system prompt.167   *168   * @param prompt - The system prompt to validate169   * @returns ValidationResult170   */171  validateSystemPrompt(prompt: string): ValidationResult {172    const errors: string[] = [];173    const warnings: string[] = [];174 175    if (!prompt || prompt.trim().length === 0) {176      errors.push('System prompt is required and cannot be empty');177      return { isValid: false, errors, warnings };178    }179 180    const trimmedPrompt = prompt.trim();181 182    if (trimmedPrompt.length < 10) {183      errors.push('System prompt must be at least 10 characters long');184    }185 186    if (trimmedPrompt.length > 10000) {187      warnings.push(188        'System prompt is quite long (>10,000 characters), consider shortening',189      );190    }191 192    return {193      isValid: errors.length === 0,194      errors,195      warnings,196    };197  }198 199  /**200   * Validates a list of tool names.201   *202   * @param tools - Array of tool names to validate203   * @returns ValidationResult204   */205  validateTools(tools: string[]): ValidationResult {206    const errors: string[] = [];207    const warnings: string[] = [];208 209    if (!Array.isArray(tools)) {210      errors.push('Tools must be an array of strings');211      return { isValid: false, errors, warnings };212    }213 214    if (tools.length === 0) {215      warnings.push(216        'Empty tools array - subagent will inherit all available tools',217      );218      return { isValid: true, errors, warnings };219    }220 221    const uniqueTools = new Set(tools);222    if (uniqueTools.size !== tools.length) {223      warnings.push('Duplicate tool names found in tools array');224    }225 226    for (const tool of tools) {227      if (typeof tool !== 'string') {228        errors.push(`Tool name must be a string, got: ${typeof tool}`);229        continue;230      }231 232      if (tool.trim().length === 0) {233        errors.push('Tool name cannot be empty');234        continue;235      }236    }237 238    return {239      isValid: errors.length === 0,240      errors,241      warnings,242    };243  }244 245  /**246   * Validates a subagent model selector.247   *248   * @param model - Model selector to validate249   * @returns ValidationResult250   */251  validateModel(model: string): ValidationResult {252    const errors: string[] = [];253    const warnings: string[] = [];254 255    if (typeof model !== 'string' || model.trim().length === 0) {256      errors.push('Model must be a non-empty string');257      return {258        isValid: false,259        errors,260        warnings,261      };262    }263 264    try {265      resolveModelId(model);266    } catch (error) {267      errors.push(error instanceof Error ? error.message : 'Invalid model');268    }269 270    if (model.trim() === 'inherit') {271      warnings.push(272        'Explicit "inherit" is optional because omitting the model uses the main conversation model',273      );274    }275 276    return {277      isValid: errors.length === 0,278      errors,279      warnings,280    };281  }282 283  /**284   * Validates runtime configuration.285   *286   * @param runConfig - Partial run configuration to validate287   * @returns ValidationResult288   */289  validateRunConfig(runConfig: RunConfig): ValidationResult {290    const errors: string[] = [];291    const warnings: string[] = [];292 293    if (runConfig.max_time_minutes !== undefined) {294      if (typeof runConfig.max_time_minutes !== 'number') {295        errors.push('max_time_minutes must be a number');296      } else if (runConfig.max_time_minutes <= 0) {297        errors.push('max_time_minutes must be greater than 0');298      } else if (runConfig.max_time_minutes > 60) {299        warnings.push(300          'Very long execution time (>60 minutes) may cause resource issues',301        );302      }303    }304 305    if (runConfig.max_turns !== undefined) {306      if (typeof runConfig.max_turns !== 'number') {307        errors.push('max_turns must be a number');308      } else if (runConfig.max_turns <= 0) {309        errors.push('max_turns must be greater than 0');310      } else if (!Number.isInteger(runConfig.max_turns)) {311        errors.push('max_turns must be an integer');312      } else if (runConfig.max_turns > 100) {313        warnings.push(314          'Very high turn limit (>100) may cause long execution times',315        );316      }317    }318 319    return {320      isValid: errors.length === 0,321      errors,322      warnings,323    };324  }325 326  /**327   * Throws a SubagentError if validation fails.328   *329   * @param config - Configuration to validate330   * @param subagentName - Name for error context331   * @throws SubagentError if validation fails332   */333  validateOrThrow(config: SubagentConfig, subagentName?: string): void {334    const result = this.validateConfig(config);335    if (!result.isValid) {336      throw new SubagentError(337        `Validation failed: ${result.errors.join(', ')}`,338        SubagentErrorCode.VALIDATION_ERROR,339        subagentName || config.name,340      );341    }342  }343}344 
basant307/AI_Governance_Project · CoolFace