aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
369 lines (315 loc) • 11.4 kB
JavaScript
/**
* Subagent Prompt Builder - Assembles prompts from REAL TASKS
*
* This module does NOT generate generic prompts. Instead, it loads:
* - Complete agent definition (.md file)
* - Complete task definition (.md file)
* - Referenced checklists
* - Referenced templates
*
* The subagent receives the FULL task instructions, not a summary.
*
* @module core/orchestration/subagent-prompt-builder
* @version 1.0.0
*/
const fs = require('fs-extra');
const path = require('path');
const yaml = require('js-yaml');
/**
* Builds structured prompts for subagents using real task definitions
*/
class SubagentPromptBuilder {
/**
* @param {string} projectRoot - Project root directory
*/
constructor(projectRoot) {
this.projectRoot = projectRoot;
this.aiosCoreRoot = path.join(projectRoot, '.aios-core');
// Paths to AIOS components
this.paths = {
agents: path.join(this.aiosCoreRoot, 'development', 'agents'),
tasks: path.join(this.aiosCoreRoot, 'development', 'tasks'),
checklists: path.join(this.aiosCoreRoot, 'product', 'checklists'),
templates: path.join(this.aiosCoreRoot, 'product', 'templates'),
};
}
/**
* Build a complete prompt for a subagent
* Loads REAL task files, NOT generic prompts
*
* @param {string} agentId - Agent identifier (e.g., 'architect', 'data-engineer')
* @param {string} taskFile - Task file name (e.g., 'document-project.md')
* @param {Object} context - Execution context
* @returns {Promise<string>} Complete prompt for subagent
*/
async buildPrompt(agentId, taskFile, context = {}) {
// 1. Load complete agent definition
const agentDef = await this.loadAgentDefinition(agentId);
// 2. Load complete task definition
const taskDef = await this.loadTaskDefinition(taskFile);
// 3. Extract and load referenced checklists from task
const checklists = await this.extractAndLoadChecklists(taskDef, context.checklist);
// 4. Extract and load referenced templates from task
const templates = await this.extractAndLoadTemplates(taskDef, context.template);
// 5. Build context section from previous phases
const contextSection = this.formatContextSection(context);
// 6. Assemble the complete prompt
return this.assemblePrompt({
agentId,
agentDef,
taskFile,
taskDef,
checklists,
templates,
context,
contextSection,
});
}
/**
* Load complete agent definition file
* @param {string} agentId - Agent identifier
* @returns {Promise<string>} Complete agent .md file content
*/
async loadAgentDefinition(agentId) {
// Try different file naming patterns
const patterns = [
`${agentId}.md`,
`${agentId.replace(/-/g, '_')}.md`,
];
for (const pattern of patterns) {
const filePath = path.join(this.paths.agents, pattern);
if (await fs.pathExists(filePath)) {
return await fs.readFile(filePath, 'utf8');
}
}
// Agent not found - return minimal definition
console.warn(`[SubagentPromptBuilder] Agent definition not found: ${agentId}`);
return `# Agent: ${agentId}\nNo definition file found.`;
}
/**
* Load complete task definition file
* @param {string} taskFile - Task file name
* @returns {Promise<string>} Complete task .md file content
*/
async loadTaskDefinition(taskFile) {
// Ensure .md extension
const fileName = taskFile.endsWith('.md') ? taskFile : `${taskFile}.md`;
const filePath = path.join(this.paths.tasks, fileName);
if (await fs.pathExists(filePath)) {
return await fs.readFile(filePath, 'utf8');
}
// Try to find by action name (e.g., 'document-project' -> 'document-project.md')
const altPath = path.join(this.paths.tasks, `${taskFile.replace('*', '')}.md`);
if (await fs.pathExists(altPath)) {
return await fs.readFile(altPath, 'utf8');
}
console.warn(`[SubagentPromptBuilder] Task definition not found: ${taskFile}`);
return `# Task: ${taskFile}\nNo definition file found.`;
}
/**
* Extract checklist references from task and load them
* @param {string} taskDef - Task definition content
* @param {string} overrideChecklist - Checklist from phase config (takes precedence)
* @returns {Promise<Object[]>} Array of loaded checklists
*/
async extractAndLoadChecklists(taskDef, overrideChecklist = null) {
const checklists = [];
// Priority 1: Override checklist from phase config
if (overrideChecklist) {
const content = await this.loadChecklist(overrideChecklist);
if (content) {
checklists.push({ name: overrideChecklist, content });
}
}
// Priority 2: Checklists referenced in task frontmatter
const frontmatterMatch = taskDef.match(/^---\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
try {
const frontmatter = yaml.load(frontmatterMatch[1]);
if (frontmatter?.checklists) {
for (const checklistName of frontmatter.checklists) {
if (checklistName !== overrideChecklist) {
const content = await this.loadChecklist(checklistName);
if (content) {
checklists.push({ name: checklistName, content });
}
}
}
}
} catch (_e) {
// Invalid YAML frontmatter - continue
}
}
return checklists;
}
/**
* Load a single checklist file
* @param {string} checklistName - Checklist file name
* @returns {Promise<string|null>} Checklist content or null
*/
async loadChecklist(checklistName) {
const fileName = checklistName.endsWith('.md') ? checklistName : `${checklistName}.md`;
const filePath = path.join(this.paths.checklists, fileName);
if (await fs.pathExists(filePath)) {
return await fs.readFile(filePath, 'utf8');
}
return null;
}
/**
* Extract template references from task and load them
* @param {string} taskDef - Task definition content
* @param {string} overrideTemplate - Template from phase config (takes precedence)
* @returns {Promise<Object[]>} Array of loaded templates
*/
async extractAndLoadTemplates(taskDef, overrideTemplate = null) {
const templates = [];
// Priority 1: Override template from phase config
if (overrideTemplate) {
const content = await this.loadTemplate(overrideTemplate);
if (content) {
templates.push({ name: overrideTemplate, content });
}
}
// Priority 2: Templates referenced in task frontmatter
const frontmatterMatch = taskDef.match(/^---\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
try {
const frontmatter = yaml.load(frontmatterMatch[1]);
if (frontmatter?.templates) {
for (const templateName of frontmatter.templates) {
if (templateName !== overrideTemplate) {
const content = await this.loadTemplate(templateName);
if (content) {
templates.push({ name: templateName, content });
}
}
}
}
} catch (_e) {
// Invalid YAML frontmatter - continue
}
}
return templates;
}
/**
* Load a single template file
* @param {string} templateName - Template file name
* @returns {Promise<string|null>} Template content or null
*/
async loadTemplate(templateName) {
// Try both .yaml and .md extensions
const extensions = ['.yaml', '.yml', '.md'];
const baseName = templateName.replace(/\.(yaml|yml|md)$/, '');
for (const ext of extensions) {
const filePath = path.join(this.paths.templates, `${baseName}${ext}`);
if (await fs.pathExists(filePath)) {
return await fs.readFile(filePath, 'utf8');
}
}
return null;
}
/**
* Format context from previous phases
* @param {Object} context - Execution context
* @returns {string} Formatted context section
*/
formatContextSection(context) {
if (!context.previousPhases || Object.keys(context.previousPhases).length === 0) {
return '(No previous phase outputs available)';
}
let section = '';
for (const [phaseNum, phaseData] of Object.entries(context.previousPhases)) {
section += `### Phase ${phaseNum}: ${phaseData.agent}\n`;
section += `- Action: ${phaseData.action}\n`;
if (phaseData.result?.output_path) {
section += `- Output: ${phaseData.result.output_path}\n`;
}
if (phaseData.result?.summary) {
section += `- Summary: ${phaseData.result.summary}\n`;
}
section += '\n';
}
return section;
}
/**
* Assemble the final prompt from all components
* @param {Object} components - All loaded components
* @returns {string} Complete assembled prompt
*/
assemblePrompt(components) {
const {
agentId,
agentDef,
taskFile,
taskDef,
checklists,
templates,
context,
contextSection,
} = components;
let prompt = `# AGENT TRANSFORMATION
You are being activated as the **@${agentId}** agent.
## COMPLETE AGENT DEFINITION
The following is your COMPLETE agent definition. Adopt this persona fully.
---
${agentDef}
---
## TASK TO EXECUTE
**Task File:** ${taskFile}
**Expected Output:** ${context.creates || 'See task definition'}
**Execution Mode:** ${context.yoloMode ? 'YOLO (autonomous)' : 'Interactive'}
**Execution Profile:** ${context.executionProfile || 'balanced'}
**Elicitation Required:** ${context.elicit ? 'Yes' : 'No'}
**Risk Policy:** ${JSON.stringify(context.executionPolicy || {}, null, 0)}
### Complete Task Definition:
---
${taskDef}
---
`;
// Add checklists if available
if (checklists.length > 0) {
prompt += '\n## QUALITY CHECKLISTS\n\n';
prompt += 'Execute these checklists to validate your work:\n\n';
for (const checklist of checklists) {
prompt += `### ${checklist.name}\n\n`;
prompt += `---\n${checklist.content}\n---\n\n`;
}
}
// Add templates if available
if (templates.length > 0) {
prompt += '\n## OUTPUT TEMPLATES\n\n';
prompt += 'Use these templates as the basis for your output:\n\n';
for (const template of templates) {
prompt += `### ${template.name}\n\n`;
prompt += `\`\`\`yaml\n${template.content}\n\`\`\`\n\n`;
}
}
// Add context from previous phases
prompt += '\n## CONTEXT FROM PREVIOUS PHASES\n\n';
prompt += contextSection;
// Add execution instructions
prompt += `
## EXECUTION INSTRUCTIONS
1. **Adopt the persona completely** - Use the communication style and vocabulary defined
2. **Follow the task definition exactly** - Do not improvise or skip steps
3. **Use the templates provided** - They ensure consistency and quality
4. **Run the checklists** - Validate your work before marking complete
5. **Create the expected output** - Save to: ${context.creates || 'as specified in task'}
### Output Format
Return a structured result:
\`\`\`json
{
"status": "success|failed",
"output_path": "${context.creates || 'path/to/output'}",
"summary": "Brief summary of what was accomplished",
"findings": ["Key findings or items discovered"],
"next_phase_context": {
// Data relevant for the next phase
}
}
\`\`\`
`;
return prompt;
}
}
module.exports = SubagentPromptBuilder;