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:
- Review these specifications
- Use them as implementation guides for coder agents
- Reference during code reviews
- Update as design evolves
Status: Architecture Complete, Ready for Implementation Phase