Files
qwen-code/packages/core/src/subagents/validation.ts
2025-09-15 14:11:31 +08:00

355 lines
9.8 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { SubagentError, SubagentErrorCode } from './types.js';
import type {
ModelConfig,
RunConfig,
SubagentConfig,
ValidationResult,
} from './types.js';
/**
* Validates subagent configurations to ensure they are well-formed
* and compatible with the runtime system.
*/
export class SubagentValidator {
/**
* Validates a complete subagent configuration.
*
* @param config - The subagent configuration to validate
* @returns ValidationResult with errors and warnings
*/
validateConfig(config: SubagentConfig): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
// Validate name
const nameValidation = this.validateName(config.name);
if (!nameValidation.isValid) {
errors.push(...nameValidation.errors);
}
// Validate description
if (!config.description || config.description.trim().length === 0) {
errors.push('Description is required and cannot be empty');
} else if (config.description.length > 500) {
warnings.push(
'Description is quite long (>500 chars), consider shortening for better readability',
);
}
// Validate system prompt
const promptValidation = this.validateSystemPrompt(config.systemPrompt);
if (!promptValidation.isValid) {
errors.push(...promptValidation.errors);
}
warnings.push(...promptValidation.warnings);
// Validate tools if specified
if (config.tools) {
const toolsValidation = this.validateTools(config.tools);
if (!toolsValidation.isValid) {
errors.push(...toolsValidation.errors);
}
warnings.push(...toolsValidation.warnings);
}
// Validate model config if specified
if (config.modelConfig) {
const modelValidation = this.validateModelConfig(config.modelConfig);
if (!modelValidation.isValid) {
errors.push(...modelValidation.errors);
}
warnings.push(...modelValidation.warnings);
}
// Validate run config if specified
if (config.runConfig) {
const runValidation = this.validateRunConfig(config.runConfig);
if (!runValidation.isValid) {
errors.push(...runValidation.errors);
}
warnings.push(...runValidation.warnings);
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validates a subagent name.
* Names must be valid identifiers that can be used in file paths and tool calls.
*
* @param name - The name to validate
* @returns ValidationResult
*/
validateName(name: string): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!name || name.trim().length === 0) {
errors.push('Name is required and cannot be empty');
return { isValid: false, errors, warnings };
}
const trimmedName = name.trim();
// Check length constraints
if (trimmedName.length < 2) {
errors.push('Name must be at least 2 characters long');
}
if (trimmedName.length > 50) {
errors.push('Name must be 50 characters or less');
}
// Check valid characters (alphanumeric, hyphens, underscores)
const validNameRegex = /^[a-zA-Z0-9_-]+$/;
if (!validNameRegex.test(trimmedName)) {
errors.push(
'Name can only contain letters, numbers, hyphens, and underscores',
);
}
// Check that it doesn't start or end with special characters
if (trimmedName.startsWith('-') || trimmedName.startsWith('_')) {
errors.push('Name cannot start with a hyphen or underscore');
}
if (trimmedName.endsWith('-') || trimmedName.endsWith('_')) {
errors.push('Name cannot end with a hyphen or underscore');
}
// Check for reserved names
const reservedNames = [
'self',
'system',
'user',
'model',
'tool',
'config',
'default',
];
if (reservedNames.includes(trimmedName.toLowerCase())) {
errors.push(`"${trimmedName}" is a reserved name and cannot be used`);
}
// Warnings for naming conventions
if (trimmedName !== trimmedName.toLowerCase()) {
warnings.push('Consider using lowercase names for consistency');
}
if (trimmedName.includes('_') && trimmedName.includes('-')) {
warnings.push(
'Consider using either hyphens or underscores consistently, not both',
);
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validates a system prompt.
*
* @param prompt - The system prompt to validate
* @returns ValidationResult
*/
validateSystemPrompt(prompt: string): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!prompt || prompt.trim().length === 0) {
errors.push('System prompt is required and cannot be empty');
return { isValid: false, errors, warnings };
}
const trimmedPrompt = prompt.trim();
// Check minimum length for meaningful prompts
if (trimmedPrompt.length < 10) {
errors.push('System prompt must be at least 10 characters long');
}
// Check maximum length to prevent token issues
if (trimmedPrompt.length > 10000) {
errors.push('System prompt is too long (>10,000 characters)');
} else if (trimmedPrompt.length > 5000) {
warnings.push(
'System prompt is quite long (>5,000 characters), consider shortening',
);
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validates a list of tool names.
*
* @param tools - Array of tool names to validate
* @returns ValidationResult
*/
validateTools(tools: string[]): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!Array.isArray(tools)) {
errors.push('Tools must be an array of strings');
return { isValid: false, errors, warnings };
}
if (tools.length === 0) {
warnings.push(
'Empty tools array - subagent will inherit all available tools',
);
return { isValid: true, errors, warnings };
}
// Check for duplicates
const uniqueTools = new Set(tools);
if (uniqueTools.size !== tools.length) {
warnings.push('Duplicate tool names found in tools array');
}
// Validate each tool name
for (const tool of tools) {
if (typeof tool !== 'string') {
errors.push(`Tool name must be a string, got: ${typeof tool}`);
continue;
}
if (tool.trim().length === 0) {
errors.push('Tool name cannot be empty');
continue;
}
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validates model configuration.
*
* @param modelConfig - Partial model configuration to validate
* @returns ValidationResult
*/
validateModelConfig(modelConfig: ModelConfig): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (modelConfig.model !== undefined) {
if (
typeof modelConfig.model !== 'string' ||
modelConfig.model.trim().length === 0
) {
errors.push('Model name must be a non-empty string');
}
}
if (modelConfig.temp !== undefined) {
if (typeof modelConfig.temp !== 'number') {
errors.push('Temperature must be a number');
} else if (modelConfig.temp < 0 || modelConfig.temp > 2) {
errors.push('Temperature must be between 0 and 2');
} else if (modelConfig.temp > 1) {
warnings.push(
'High temperature (>1) may produce very creative but unpredictable results',
);
}
}
if (modelConfig.top_p !== undefined) {
if (typeof modelConfig.top_p !== 'number') {
errors.push('top_p must be a number');
} else if (modelConfig.top_p < 0 || modelConfig.top_p > 1) {
errors.push('top_p must be between 0 and 1');
}
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validates runtime configuration.
*
* @param runConfig - Partial run configuration to validate
* @returns ValidationResult
*/
validateRunConfig(runConfig: RunConfig): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (runConfig.max_time_minutes !== undefined) {
if (typeof runConfig.max_time_minutes !== 'number') {
errors.push('max_time_minutes must be a number');
} else if (runConfig.max_time_minutes <= 0) {
errors.push('max_time_minutes must be greater than 0');
} else if (runConfig.max_time_minutes > 60) {
warnings.push(
'Very long execution time (>60 minutes) may cause resource issues',
);
}
}
if (runConfig.max_turns !== undefined) {
if (typeof runConfig.max_turns !== 'number') {
errors.push('max_turns must be a number');
} else if (runConfig.max_turns <= 0) {
errors.push('max_turns must be greater than 0');
} else if (!Number.isInteger(runConfig.max_turns)) {
errors.push('max_turns must be an integer');
} else if (runConfig.max_turns > 100) {
warnings.push(
'Very high turn limit (>100) may cause long execution times',
);
}
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* Throws a SubagentError if validation fails.
*
* @param config - Configuration to validate
* @param subagentName - Name for error context
* @throws SubagentError if validation fails
*/
validateOrThrow(config: SubagentConfig, subagentName?: string): void {
const result = this.validateConfig(config);
if (!result.isValid) {
throw new SubagentError(
`Validation failed: ${result.errors.join(', ')}`,
SubagentErrorCode.VALIDATION_ERROR,
subagentName || config.name,
);
}
}
}