aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
361 lines (317 loc) • 11 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const yaml = require('js-yaml');
const glob = require('glob');
/**
* ToolResolver - Resolves and loads AIOS tools from file system
*
* Features:
* - Map-based caching for performance (<5ms cached lookups)
* - Search path priority: squad → common → core
* - Glob-based file resolution
* - Schema validation
* - Health checking (tool_call, command, http methods)
* - Schema version auto-detection
*
* Target Performance: <50ms for uncached resolution
*/
class ToolResolver {
constructor() {
// Map-based cache for fast lookups
this.cache = new Map();
// Base search paths (in priority order)
this.basePaths = [
'aios-core/tools',
'common/tools',
// Squad paths added dynamically during resolution
];
}
/**
* Resolve a tool by name
*
* @param {string} toolName - Tool identifier (e.g., 'clickup', 'github-cli')
* @param {object} context - Resolution context (optional)
* @param {string} context.expansionPack - Specific squad to search
* @returns {object} Tool definition with schema_version detected
* @throws {Error} If tool not found or validation fails
*/
async resolveTool(toolName, context = {}) {
// 1. Check cache first (performance: <5ms cached)
const cacheKey = `${context.expansionPack || 'core'}:${toolName}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
// 2. Build search paths (squad → core priority)
const searchPaths = [];
if (context.expansionPack) {
searchPaths.push(`squads/${context.expansionPack}/tools`);
}
searchPaths.push(...this.basePaths);
// 3. Find tool file using glob (searches subdirectories)
let toolPath = null;
for (const basePath of searchPaths) {
const candidates = glob.sync(`${basePath}/**/${toolName}.yaml`);
if (candidates.length > 0) {
toolPath = candidates[0];
break;
}
}
if (!toolPath) {
throw new Error(`Tool '${toolName}' not found in search paths: ${searchPaths.join(', ')}`);
}
// 4. Load and parse YAML
const toolContent = await fs.readFile(toolPath, 'utf8');
let toolDef = yaml.load(toolContent);
// Extract tool object if wrapped (handles both formats)
if (toolDef.tool) {
toolDef = toolDef.tool;
}
// 5. Validate schema
await this.validateToolSchema(toolDef);
// 6. Detect schema version (auto-detection if not specified)
if (!toolDef.schema_version) {
toolDef.schema_version = this.detectSchemaVersion(toolDef);
}
// 7. Health check if configured
if (toolDef.health_check) {
const healthy = await this.checkHealth(toolDef);
if (!healthy && toolDef.health_check.required) {
throw new Error(`Required tool '${toolName}' health check failed`);
}
toolDef._healthStatus = healthy ? 'healthy' : 'unhealthy';
}
// 8. Cache and return (target: <50ms total)
this.cache.set(cacheKey, toolDef);
return toolDef;
}
/**
* Validate tool schema structure
*
* @param {object} tool - Tool definition
* @throws {Error} If required fields missing or invalid
*/
async validateToolSchema(tool) {
// Required fields for all tools
const requiredFields = ['id', 'type', 'name', 'version', 'description'];
for (const field of requiredFields) {
if (!tool[field]) {
throw new Error(`Tool missing required field: ${field}`);
}
}
// Validate type enum
const validTypes = ['mcp', 'cli', 'local', 'meta'];
if (!validTypes.includes(tool.type)) {
throw new Error(`Invalid tool type '${tool.type}'. Must be one of: ${validTypes.join(', ')}`);
}
// Validate version format (basic semver check)
const semverPattern = /^\d+\.\d+\.\d+$/;
if (!semverPattern.test(tool.version)) {
throw new Error(`Invalid version format '${tool.version}'. Expected semantic versioning (e.g., 1.0.0)`);
}
// Validate schema_version if present (support both string and numeric formats)
if (tool.schema_version) {
const version = typeof tool.schema_version === 'number'
? tool.schema_version
: parseFloat(tool.schema_version);
if (![1.0, 2.0].includes(version)) {
throw new Error(`Invalid schema_version '${tool.schema_version}'. Must be '1.0' or '2.0'`);
}
}
// v2.0 specific validation
if (tool.schema_version === '2.0' || tool.schema_version === 2.0) {
await this.validateV2Schema(tool);
}
}
/**
* Validate v2.0 schema-specific features
*
* @param {object} tool - Tool definition
* @throws {Error} If v2.0 features are invalid
*/
async validateV2Schema(tool) {
// Validate executable_knowledge if present
if (tool.executable_knowledge) {
const { helpers, processors, validators } = tool.executable_knowledge;
// Validate helpers
if (helpers) {
if (!Array.isArray(helpers)) {
throw new Error('executable_knowledge.helpers must be an array');
}
for (const helper of helpers) {
if (!helper.id || !helper.language || !helper.function) {
throw new Error('Helper must have id, language, and function fields');
}
if (helper.language !== 'javascript') {
throw new Error(`Unsupported helper language: ${helper.language}`);
}
}
}
// Validate validators
if (validators) {
if (!Array.isArray(validators)) {
throw new Error('executable_knowledge.validators must be an array');
}
for (const validator of validators) {
if (!validator.id || !validator.validates || !validator.function) {
throw new Error('Validator must have id, validates, and function fields');
}
}
}
}
// Validate anti_patterns if present
if (tool.anti_patterns) {
if (!Array.isArray(tool.anti_patterns)) {
throw new Error('anti_patterns must be an array');
}
for (const pattern of tool.anti_patterns) {
if (!pattern.pattern || !pattern.description || !pattern.wrong || !pattern.correct) {
throw new Error('Anti-pattern must have pattern, description, wrong, and correct fields');
}
}
}
}
/**
* Detect schema version from tool features
*
* @param {object} tool - Tool definition
* @returns {number} Detected schema version (1.0 or 2.0)
*/
detectSchemaVersion(tool) {
// Check for v2.0 features
const hasExecutableKnowledge = !!tool.executable_knowledge;
const hasApiComplexity = !!tool.api_complexity;
const hasAntiPatterns = !!tool.anti_patterns;
const hasEnhancedExamples = tool.examples &&
Object.values(tool.examples).some(ex =>
ex.some(e => e.scenario && ['success', 'failure_invalid_param'].includes(e.scenario)),
);
if (hasExecutableKnowledge || hasApiComplexity || hasAntiPatterns || hasEnhancedExamples) {
return 2.0;
}
// Default to v1.0 (simple tools)
return 1.0;
}
/**
* Perform health check on tool
*
* @param {object} tool - Tool definition with health_check config
* @returns {boolean} True if healthy, false otherwise
*/
async checkHealth(tool) {
const { health_check } = tool;
if (!health_check || !health_check.method) {
return true; // No health check configured = assume healthy
}
try {
switch (health_check.method) {
case 'tool_call':
// Execute a specific tool command and check response
// Note: Actual implementation would use ToolExecutor
// For now, check if command exists in tool definition
return !!tool.commands?.includes(health_check.command);
case 'command':
// Execute shell command and check exit code
// Note: Actual implementation would use child_process
// For now, return true (not implemented)
return true;
case 'http':
// Make HTTP request and check status
// Note: Actual implementation would use fetch/axios
// For now, return true (not implemented)
return true;
default:
console.warn(`Unknown health check method: ${health_check.method}`);
return true;
}
} catch (error) {
console.error(`Health check failed for tool '${tool.id}':`, error);
return false;
}
}
/**
* Clear cache (useful for testing or reloading tools)
*/
clearCache() {
this.cache.clear();
}
/**
* Get cache statistics
*
* @returns {object} Cache stats
*/
getCacheStats() {
return {
size: this.cache.size,
keys: Array.from(this.cache.keys()),
};
}
/**
* List all available tools across all search paths
*
* @returns {Array<string>} Array of tool file paths
*/
listAvailableTools() {
const allTools = [];
for (const basePath of this.basePaths) {
const tools = glob.sync(`${basePath}/**/*.yaml`);
allTools.push(...tools);
}
return allTools;
}
/**
* Check if a tool exists without loading it
*
* @param {string} toolName - Tool identifier
* @param {object} context - Resolution context
* @returns {boolean} True if tool exists
*/
async toolExists(toolName, context = {}) {
const searchPaths = [];
if (context.expansionPack) {
searchPaths.push(`squads/${context.expansionPack}/tools`);
}
searchPaths.push(...this.basePaths);
for (const basePath of searchPaths) {
const candidates = glob.sync(`${basePath}/**/${toolName}.yaml`);
if (candidates.length > 0) {
return true;
}
}
return false;
}
/**
* Set custom search paths (useful for testing)
*
* @param {string[]} paths - Array of search paths
*/
setSearchPaths(paths) {
this.basePaths = paths;
}
/**
* Reset search paths to default
*/
resetSearchPaths() {
this.basePaths = [
'aios-core/tools',
'common/tools',
];
}
}
// Export singleton instance
const toolResolverInstance = new ToolResolver();
// Save reference to instance method BEFORE it gets overwritten by the export
const yamlBasedResolveTool = toolResolverInstance.resolveTool.bind(toolResolverInstance);
/**
* Simple tool resolution function - delegates to YAML-based tool resolution
* All tools (including MCP tools like clickup and github) are loaded from YAML definitions
*
* @param {string} toolName - Name of the tool (e.g., 'clickup', 'github')
* @param {object} context - Resolution context (optional, includes expansionPack)
* @returns {object} Tool definition from YAML file
*/
async function resolveTool(toolName, context = {}) {
// All tools go through YAML-based resolution
return await yamlBasedResolveTool(toolName, context);
}
module.exports = toolResolverInstance;
module.exports.resolveTool = resolveTool;