Skip to main content

AI Agent v2.0 - Detailed Module Specifications

Architecture Phase Deliverable Swarm ID: swarm_1764655531178_udtox74dx Created: 2025-12-01


Module 1: Task Planner

Responsibility

Transform natural language user requests into structured, dependency-resolved action plans.

Public API

class TaskPlanner {
  constructor(options = {}) {
    this.intentPatterns = options.patterns || DEFAULT_PATTERNS;
    this.llmClient = options.llmClient; // Optional: for fallback extraction
    this.siteContext = null; // Injected later
  }

  /**
   * Parse user message into structured intent
   * @param {string} message - User's natural language input
   * @returns {Promise<Intent>}
   */
  async parseIntent(message) {
    // 1. Normalize input
    const normalized = message.trim().toLowerCase();

    // 2. Match against patterns
    const intentType = this._matchIntentType(normalized);

    // 3. Extract entities
    const entities = await this._extractEntities(message, intentType);

    // 4. Calculate confidence
    const confidence = this._calculateConfidence(intentType, entities);

    return {
      type: intentType,
      confidence,
      entities,
      rawQuery: message,
      timestamp: Date.now()
    };
  }

  /**
   * Generate action plan from intent
   * @param {Intent} intent - Parsed intent
   * @param {Context} context - Site context for enrichment
   * @returns {Promise<ActionPlan>}
   */
  async planActions(intent, context) {
    this.siteContext = context;

    // 1. Generate actions based on intent type
    const actions = await this._generateActionsForIntent(intent);

    // 2. Enrich with context (schemas, defaults)
    const enriched = this._enrichActions(actions, context);

    // 3. Resolve dependencies
    const ordered = this.resolveDependencies(enriched);

    // 4. Estimate impact
    const impact = this.estimateImpact(ordered);

    return {
      id: `plan_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`,
      intent,
      actions: ordered,
      metadata: {
        createdAt: Date.now(),
        estimatedDuration: this._estimateDuration(ordered),
        totalImpact: impact
      }
    };
  }

  /**
   * Resolve action dependencies using topological sort
   * @param {Action[]} actions - Array of actions
   * @returns {Action[]} - Topologically sorted actions
   */
  resolveDependencies(actions) {
    // Build adjacency list
    const graph = new Map();
    const inDegree = new Map();

    for (const action of actions) {
      graph.set(action.id, action.dependencies || []);
      inDegree.set(action.id, 0);
    }

    // Count in-degrees
    for (const [, deps] of graph) {
      for (const dep of deps) {
        inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
      }
    }

    // Topological sort (Kahn's algorithm)
    const sorted = [];
    const queue = actions.filter(a => (inDegree.get(a.id) || 0) === 0);

    while (queue.length > 0) {
      const current = queue.shift();
      sorted.push(current);

      const deps = graph.get(current.id) || [];
      for (const depId of deps) {
        inDegree.set(depId, inDegree.get(depId) - 1);
        if (inDegree.get(depId) === 0) {
          const depAction = actions.find(a => a.id === depId);
          queue.push(depAction);
        }
      }
    }

    // Check for cycles
    if (sorted.length !== actions.length) {
      throw new Error('Circular dependency detected in action plan');
    }

    return sorted;
  }

  /**
   * Estimate impact of action plan
   * @param {Action[]} actions - Ordered actions
   * @returns {ImpactReport}
   */
  estimateImpact(actions) {
    let filesCreated = 0;
    let filesModified = 0;
    let filesDeleted = 0;
    let highRiskActions = 0;

    for (const action of actions) {
      switch (action.type) {
        case 'CREATE_FILE':
          filesCreated++;
          break;
        case 'EDIT_FILE':
        case 'EDIT_YAML':
          filesModified++;
          break;
        case 'DELETE_FILE':
          filesDeleted++;
          highRiskActions++;
          break;
      }

      // Check if high-risk file
      if (this._isHighRiskFile(action.target)) {
        highRiskActions++;
      }
    }

    const totalChanges = filesCreated + filesModified + filesDeleted;
    const risk = highRiskActions > 0 ? 'high' :
                 totalChanges > 5 ? 'medium' : 'low';

    return {
      filesCreated,
      filesModified,
      filesDeleted,
      totalChanges,
      risk,
      highRiskActions
    };
  }

  // Private methods
  _matchIntentType(normalized) {
    for (const [intentType, patterns] of Object.entries(this.intentPatterns)) {
      for (const pattern of patterns) {
        if (pattern.test(normalized)) {
          return intentType;
        }
      }
    }
    return 'UNKNOWN';
  }

  async _extractEntities(message, intentType) {
    const entities = {};

    switch (intentType) {
      case 'ADD_PROJECT':
        entities.projectName = this._extractProjectName(message);
        entities.technologies = this._extractTechnologies(message);
        entities.description = this._extractDescription(message);
        entities.category = this._inferCategory(message);
        break;

      case 'SEARCH_PROJECTS':
        entities.query = this._extractSearchQuery(message);
        entities.filters = this._extractFilters(message);
        break;

      // ... other intent types
    }

    return entities;
  }

  async _generateActionsForIntent(intent) {
    const generator = this[`_generate_${intent.type}`];
    if (!generator) {
      throw new Error(`Unknown intent type: ${intent.type}`);
    }
    return generator.call(this, intent.entities);
  }

  _generate_ADD_PROJECT(entities) {
    return [
      {
        id: `act_${Date.now()}_1`,
        type: 'EDIT_YAML',
        target: '_data/projects.yml',
        operation: {
          append: {
            name: entities.projectName,
            description: entities.description,
            technologies: entities.technologies,
            category: entities.category,
            status: 'active'
          }
        },
        dependencies: [],
        estimatedImpact: {
          filesModified: 1,
          filesCreated: 0,
          filesDeleted: 0,
          risk: 'low'
        }
      },
      {
        id: `act_${Date.now()}_2`,
        type: 'CREATE_FILE',
        target: `_projects/${this._slugify(entities.projectName)}.md`,
        operation: {
          content: this._generateProjectMarkdown(entities)
        },
        dependencies: [`act_${Date.now()}_1`],
        estimatedImpact: {
          filesModified: 0,
          filesCreated: 1,
          filesDeleted: 0,
          risk: 'low'
        }
      }
    ];
  }

  _isHighRiskFile(filepath) {
    const highRiskFiles = [
      '_config.yml',
      'Gemfile',
      'package.json',
      '.gitignore'
    ];
    return highRiskFiles.some(f => filepath.endsWith(f));
  }

  _slugify(text) {
    return text
      .toLowerCase()
      .replace(/[^\w\s-]/g, '')
      .replace(/\s+/g, '-');
  }

  _estimateDuration(actions) {
    // Estimate in milliseconds
    const baseTime = 500; // Base overhead
    const perActionTime = 200;
    return baseTime + (actions.length * perActionTime);
  }
}

Intent Patterns Configuration

const DEFAULT_PATTERNS = {
  ADD_PROJECT: [
    /add (?:a |an |new )?project/i,
    /create (?:a |an |new )?project/i,
    /new project (?:about|for|using)/i,
    /i want to add (?:a |an )?project/i
  ],

  UPDATE_PROJECT: [
    /update (?:the |my )?project/i,
    /modify (?:the |my )?project/i,
    /change (?:the |my )?project/i,
    /edit (?:the |my )?project/i
  ],

  REMOVE_PROJECT: [
    /remove (?:the |my )?project/i,
    /delete (?:the |my )?project/i,
    /take down (?:the |my )?project/i
  ],

  ADD_CONTENT: [
    /add content/i,
    /create (?:a |an )?(?:post|page)/i,
    /write (?:a |an )?(?:post|article)/i
  ],

  UPDATE_CONTENT: [
    /update content/i,
    /modify (?:the |my )?(?:post|page)/i,
    /change (?:the |my )?(?:post|page)/i
  ],

  SEARCH_PROJECTS: [
    /find projects? (?:about|using|with)/i,
    /search for projects?/i,
    /show (?:me )?projects? (?:that|using|with)/i,
    /what projects? (?:use|have)/i
  ],

  QUERY_GRAPH: [
    /what projects? use/i,
    /related to/i,
    /similar projects?/i,
    /projects? (?:using|with) (?:the )?same/i
  ],

  GENERATE_CONTENT: [
    /generate (?:a )?description/i,
    /write (?:a )?description/i,
    /create content for/i
  ]
};

Module 2: Change Preview

Responsibility

Generate human-readable previews of file changes with unified diffs and YAML-aware formatting.

Public API

class ChangePreview {
  constructor(options = {}) {
    this.contextLines = options.contextLines || 3;
    this.maxPreviewLines = options.maxPreviewLines || 500;
    this.colorize = options.colorize !== false;
  }

  /**
   * Preview a single file change
   * @param {string} filepath - Path to file
   * @param {string} newContent - New file content
   * @returns {Promise<Diff>}
   */
  async previewFileChange(filepath, newContent) {
    let oldContent = '';
    try {
      oldContent = await fs.readFile(filepath, 'utf-8');
    } catch (error) {
      // File doesn't exist (new file)
      oldContent = '';
    }

    const changeType = oldContent === '' ? 'create' :
                       newContent === '' ? 'delete' : 'modify';

    const lineDiff = this._generateLineDiff(oldContent, newContent);

    return {
      filepath,
      changeType,
      oldContent: changeType !== 'create' ? oldContent : undefined,
      newContent: changeType !== 'delete' ? newContent : undefined,
      lineDiff,
      summary: this._summarizeDiff(lineDiff)
    };
  }

  /**
   * Preview YAML structure changes
   * @param {string} filepath - Path to YAML file
   * @param {object} changes - Structured changes to YAML
   * @returns {Promise<YamlDiff>}
   */
  async previewYamlChange(filepath, changes) {
    // Load existing YAML
    const oldContent = await fs.readFile(filepath, 'utf-8');
    const oldData = yaml.load(oldContent);

    // Apply changes (without writing)
    const newData = this._applyYamlChanges(oldData, changes);

    // Generate structured diff
    const yamlDiffs = this._diffYamlStructure(oldData, newData);

    return {
      filepath,
      changeType: 'modify',
      yamlDiffs,
      summary: {
        fieldsAdded: yamlDiffs.filter(d => d.operation === 'add').length,
        fieldsRemoved: yamlDiffs.filter(d => d.operation === 'remove').length,
        fieldsModified: yamlDiffs.filter(d => d.operation === 'update').length
      }
    };
  }

  /**
   * Preview entire action plan
   * @param {ActionPlan} plan - Action plan to preview
   * @returns {Promise<Preview>}
   */
  async previewPlan(plan) {
    const changes = [];
    const yamlChanges = [];

    for (const action of plan.actions) {
      switch (action.type) {
        case 'CREATE_FILE':
        case 'EDIT_FILE':
          const diff = await this.previewFileChange(
            action.target,
            action.operation.content
          );
          changes.push(diff);
          break;

        case 'EDIT_YAML':
          const yamlDiff = await this.previewYamlChange(
            action.target,
            action.operation
          );
          yamlChanges.push(yamlDiff);
          break;

        case 'DELETE_FILE':
          const deleteDiff = await this.previewFileChange(
            action.target,
            ''
          );
          changes.push(deleteDiff);
          break;
      }
    }

    const summary = this._summarizeAllChanges(changes, yamlChanges);
    const formatted = this.formatPreview({ plan, changes, yamlChanges, summary });

    return {
      plan,
      changes,
      yamlChanges,
      summary,
      formattedOutput: formatted
    };
  }

  /**
   * Format preview for display
   * @param {Preview} preview - Preview object
   * @returns {string} - Formatted output
   */
  formatPreview(preview) {
    const lines = [];
    const { plan, changes, yamlChanges, summary } = preview;

    // Header
    lines.push(this._formatHeader(plan.intent));
    lines.push('');

    // Summary
    lines.push(this._formatSummary(summary));
    lines.push('');

    // Individual changes
    for (const change of changes) {
      lines.push(this._formatFileChange(change));
      lines.push('');
    }

    for (const yamlChange of yamlChanges) {
      lines.push(this._formatYamlChange(yamlChange));
      lines.push('');
    }

    return lines.join('\n');
  }

  // Private methods
  _generateLineDiff(oldContent, newContent) {
    const diff = require('diff');
    const patches = diff.createPatch('file', oldContent, newContent);
    const lines = patches.split('\n').slice(4); // Skip header

    const lineDiff = [];
    let lineNumber = 1;

    for (const line of lines) {
      if (line.startsWith('+')) {
        lineDiff.push({ lineNumber, type: '+', content: line.slice(1) });
      } else if (line.startsWith('-')) {
        lineDiff.push({ lineNumber, type: '-', content: line.slice(1) });
      } else if (line.startsWith(' ')) {
        lineDiff.push({ lineNumber, type: ' ', content: line.slice(1) });
        lineNumber++;
      }
    }

    return lineDiff;
  }

  _formatFileChange(change) {
    const { filepath, changeType, lineDiff } = change;

    const header = `File: ${filepath} ${this._changeTypeLabel(changeType)}`;
    const separator = ''.repeat(header.length);

    const diffLines = lineDiff.slice(0, this.maxPreviewLines).map(line => {
      const prefix = line.type === '+' ? chalk.green('+') :
                     line.type === '-' ? chalk.red('-') :
                     chalk.gray(' ');
      const content = this.colorize ?
        this._colorizeContent(line.type, line.content) :
        line.content;
      return `${prefix} ${content}`;
    });

    if (lineDiff.length > this.maxPreviewLines) {
      diffLines.push(chalk.gray(`... (${lineDiff.length - this.maxPreviewLines} more lines)`));
    }

    return [
      chalk.bold(header),
      separator,
      ...diffLines
    ].join('\n');
  }

  _formatYamlChange(yamlChange) {
    const { filepath, yamlDiffs } = yamlChange;

    const lines = [
      chalk.bold(`File: ${filepath} (YAML)`),
      ''.repeat(40)
    ];

    for (const diff of yamlDiffs) {
      const pathStr = diff.path.join('.');
      const op = diff.operation === 'add' ? chalk.green('[ADD]') :
                 diff.operation === 'remove' ? chalk.red('[REMOVE]') :
                 chalk.yellow('[UPDATE]');

      lines.push(`${op} ${pathStr}`);

      if (diff.operation === 'update') {
        lines.push(`  Old: ${JSON.stringify(diff.oldValue)}`);
        lines.push(`  New: ${JSON.stringify(diff.newValue)}`);
      } else if (diff.operation === 'add') {
        lines.push(`  Value: ${JSON.stringify(diff.newValue)}`);
      }
    }

    return lines.join('\n');
  }

  _changeTypeLabel(type) {
    switch (type) {
      case 'create': return chalk.green('(NEW)');
      case 'delete': return chalk.red('(DELETED)');
      case 'modify': return chalk.yellow('(MODIFIED)');
      default: return '';
    }
  }

  _colorizeContent(type, content) {
    switch (type) {
      case '+': return chalk.green(content);
      case '-': return chalk.red(content);
      default: return chalk.gray(content);
    }
  }
}

Module 3: RuVector Bridge

Responsibility

Integrate RuVector search, graph, and recommendation engines with the agent system.

Public API

class RuVectorBridge {
  constructor(options = {}) {
    this.searchEngine = null;
    this.graphEngine = null;
    this.recommender = null;
    this.initialized = false;

    this.config = {
      indexUrl: options.indexUrl || '/assets/indices/projects-index.json',
      graphUrl: options.graphUrl || '/assets/indices/knowledge-graph.json',
      fallbackMode: options.fallbackMode !== false
    };
  }

  /**
   * Initialize RuVector engines
   * @returns {Promise<void>}
   */
  async initialize() {
    if (this.initialized) return;

    try {
      // Import RuVector modules
      const { SearchEngine } = await import('../../../assets/js/ruvector/search-engine.js');
      const { GraphEngine } = await import('../../../assets/js/ruvector/graph-engine.js');
      const { RecommendationLearner } = await import('../../../assets/js/ruvector/recommendation-learner.js');

      // Initialize search engine
      this.searchEngine = new SearchEngine({
        indexUrl: this.config.indexUrl,
        minRelevanceScore: 0.3,
        maxResults: 10
      });
      await this.searchEngine.initialize();

      // Initialize graph engine
      this.graphEngine = new GraphEngine({
        graphUrl: this.config.graphUrl
      });
      await this.graphEngine.initialize();

      // Initialize recommender
      this.recommender = new RecommendationLearner({
        knowledgeGraph: this.graphEngine.graph
      });

      this.initialized = true;
      console.log('[RuVectorBridge] Initialized successfully');

    } catch (error) {
      console.warn('[RuVectorBridge] Initialization failed, using fallback:', error.message);

      if (this.config.fallbackMode) {
        this._initializeFallback();
      } else {
        throw error;
      }
    }
  }

  /**
   * Search projects semantically
   * @param {string} query - Search query
   * @param {object} options - Search options
   * @returns {Promise<SearchResults>}
   */
  async searchProjects(query, options = {}) {
    this._ensureInitialized();

    if (this.searchEngine) {
      const results = await this.searchEngine.search(query, {
        category: options.category,
        technologies: options.technologies,
        status: options.status,
        limit: options.limit || 5
      });

      return {
        results: results.map(r => ({
          id: r.id,
          name: r.name,
          description: r.description,
          technologies: r.technologies,
          category: r.category,
          score: r.score
        })),
        source: 'ruvector',
        confidence: 0.9
      };
    } else {
      return this._fallbackSearch(query, options);
    }
  }

  /**
   * Query knowledge graph
   * @param {string} query - Graph query (simplified Cypher-like)
   * @returns {Promise<GraphResults>}
   */
  async queryGraph(query) {
    this._ensureInitialized();

    if (!this.graphEngine) {
      return { results: [], source: 'fallback' };
    }

    // Parse simple query patterns
    // Example: "projects using React"
    if (query.match(/projects? using (\w+)/i)) {
      const tech = query.match(/projects? using (\w+)/i)[1];
      const projects = await this.graphEngine.getProjectsByTechnology(tech);
      return { results: projects, source: 'graph' };
    }

    // Example: "related to {project-id}"
    if (query.match(/related to ([\w-]+)/i)) {
      const projectId = query.match(/related to ([\w-]+)/i)[1];
      const related = await this.graphEngine.getRelatedProjects(projectId, 5);
      return { results: related, source: 'graph' };
    }

    return { results: [], source: 'unknown_query' };
  }

  /**
   * Find related items
   * @param {string} itemId - Item identifier
   * @param {string} itemType - Type: 'project' | 'technology' | 'category'
   * @returns {Promise<RelatedItems>}
   */
  async findRelated(itemId, itemType) {
    this._ensureInitialized();

    if (!this.graphEngine) {
      return { items: [], source: 'fallback' };
    }

    switch (itemType) {
      case 'project':
        const related = await this.graphEngine.getRelatedProjects(itemId, 5);
        return { items: related, source: 'graph' };

      case 'technology':
        const projects = await this.graphEngine.getProjectsByTechnology(itemId);
        return { items: projects, source: 'graph' };

      default:
        return { items: [], source: 'unknown_type' };
    }
  }

  /**
   * Get recommendations based on context
   * @param {object} context - Context for recommendations
   * @returns {Promise<Recommendations>}
   */
  async getRecommendations(context) {
    this._ensureInitialized();

    if (!this.recommender) {
      return { recommendations: [], source: 'fallback' };
    }

    // Build recommendation query from context
    const query = {
      technologies: context.technologies || [],
      category: context.category,
      recentProjects: context.recentProjects || []
    };

    const recommendations = await this.recommender.getRecommendations(query);

    return {
      recommendations: recommendations.map(r => ({
        type: r.type,
        value: r.value,
        reason: r.reason,
        confidence: r.confidence
      })),
      source: 'recommender'
    };
  }

  // Private methods
  _ensureInitialized() {
    if (!this.initialized) {
      throw new Error('RuVectorBridge not initialized. Call initialize() first.');
    }
  }

  _initializeFallback() {
    console.log('[RuVectorBridge] Using fallback search mode');
    this.initialized = true;
    // Fallback is just basic keyword matching
  }

  _fallbackSearch(query, options) {
    console.warn('[RuVectorBridge] Using fallback search (no RuVector)');

    // Simple keyword matching (very basic)
    const keywords = query.toLowerCase().split(/\s+/);

    return {
      results: [],
      source: 'fallback',
      confidence: 0.3,
      message: 'RuVector unavailable, showing limited results'
    };
  }
}

Storage in Swarm Directory

These specifications have been stored in:

  • /swarm/ai-agent-v2/architect/module-specifications.md

Next Steps:

  1. Review these specifications
  2. Use them as implementation guides for coder agents
  3. Reference during code reviews
  4. Update as design evolves

Status: Architecture Complete, Ready for Implementation Phase