claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
590 lines (511 loc) • 19.6 kB
text/typescript
/**
* MDAP Implementer Module
*
* Core implementation logic for MDAP tasks with support for:
* - Diff mode: Apply targeted fixes instead of full file generation
* - Validation loop: Retry failed fixes with NACK prompts
* - Syntax validation: Ensure bracket/brace balance
* - GLM 4.6 integration: Fast implementation without thinking
*
* Extracted from Trigger.dev cfn-mdap-implementer task
*
* @module implementer
* @version 1.0.0
*/
import {
callGLMFast,
type GLMResponse,
} from './glm-client.js';
import {
extractJSONFromResponse,
} from './validation.js';
import {
type CompilerError,
type FixInstruction,
type MDAPResult,
} from './types.js';
// =============================================
// Types
// =============================================
export interface ImplementerPayload {
/** Unique task identifier */
taskId: string;
/** Task description */
taskDescription: string;
/** Target file to create/modify */
targetFile: string;
/** Working directory context */
workDir?: string;
/** Context hints from decomposition */
contextHints?: string[];
/** Pre-read file contents for context */
fileContents?: Array<{ path: string; content: string }>;
/** Programming language hint */
language?: string;
/**
* Raw output mode - skips JSON wrapping for transformation tasks
* When true, the AI response is returned directly without parsing
* Use for: YAML transformation, text processing, format conversion
*/
rawOutput?: boolean;
/**
* Diff mode - LLM returns fix instructions instead of full file
* Reduces token usage by ~80-90% for large files
* Requires: errors array and fullFileContent
*/
diffMode?: boolean;
/** Compiler errors to fix (required for diffMode) */
errors?: CompilerError[];
/** Full file content for diff mode (required for diffMode) */
fullFileContent?: string;
}
export interface ImplementerResult extends MDAPResult {
/** Target file path */
targetFile: string;
/** Model name used */
modelName?: string;
/** Estimated cost in USD */
estimatedCost?: number;
/** Token usage */
tokens?: {
input: number;
output: number;
};
/** Generated code content */
generatedCode?: string;
/** Diff mode specific: fixes that were applied */
fixesApplied?: number;
/** Diff mode specific: fixes that failed */
fixesFailed?: Array<{ fix: FixInstruction; reason: string }>;
/** Diff mode specific: whether syntax validation passed */
syntaxValid?: boolean;
/** Diff mode specific: number of retry attempts for validation */
retryCount?: number;
}
// =============================================
// Constants
// =============================================
/** Maximum retries for diff mode validation failures */
const MAX_DIFF_RETRIES = 2;
/** Default language for syntax validation */
const DEFAULT_LANGUAGE = 'typescript';
// =============================================
// Diff Mode Functions
// =============================================
/**
* Validate bracket/brace/paren balance in code
* Skips strings and comments for accurate validation
*/
function validateSyntax(content: string, language: string = DEFAULT_LANGUAGE): boolean {
const stack: string[] = [];
const pairs: Record<string, string> = { "(": ")", "[": "]", "{": "}" };
const opens = new Set(Object.keys(pairs));
const closes = new Set(Object.values(pairs));
let inString = false;
let stringChar = "";
let inLineComment = false;
let inBlockComment = false;
// Handle language-specific comment delimiters
const isTypeScriptLike = ['typescript', 'javascript', 'tsx', 'jsx'].includes(language.toLowerCase());
for (let i = 0; i < content.length; i++) {
const char = content[i];
const next = content[i + 1];
const prev = content[i - 1];
// Handle newlines
if (char === "\n") {
inLineComment = false;
continue;
}
// Skip line comments (TypeScript-like languages)
if (isTypeScriptLike && !inString && !inBlockComment && char === "/" && next === "/") {
inLineComment = true;
continue;
}
if (inLineComment) continue;
// Skip block comments (TypeScript-like languages)
if (isTypeScriptLike && !inString && char === "/" && next === "*") {
inBlockComment = true;
i++;
continue;
}
if (inBlockComment && char === "*" && next === "/") {
inBlockComment = false;
i++;
continue;
}
if (inBlockComment) continue;
// Handle strings
if ((char === '"' || char === "'" || char === "`") && prev !== "\\") {
if (!inString) {
inString = true;
stringChar = char;
} else if (char === stringChar) {
inString = false;
}
continue;
}
if (inString) continue;
// Track brackets
if (opens.has(char)) {
stack.push(pairs[char]);
} else if (closes.has(char)) {
if (stack.length === 0 || stack.pop() !== char) {
return false;
}
}
}
return stack.length === 0;
}
/**
* Extract error context windows from file content
* Returns only the lines around each error for minimal token usage
*/
function extractErrorContext(
content: string,
errors: CompilerError[],
windowSize: number = 10
): string {
const lines = content.split('\n');
const chunks: string[] = [];
const includedRanges: Array<{ start: number; end: number }> = [];
const sortedErrors = [...errors].sort((a, b) => a.line - b.line);
for (const error of sortedErrors) {
const start = Math.max(0, error.line - 1 - windowSize);
const end = Math.min(lines.length, error.line + windowSize);
const lastRange = includedRanges[includedRanges.length - 1];
if (lastRange && start <= lastRange.end + 2) {
lastRange.end = Math.max(lastRange.end, end);
} else {
includedRanges.push({ start, end });
}
}
for (const range of includedRanges) {
const relevantErrors = sortedErrors.filter(
e => e.line > range.start && e.line <= range.end
);
chunks.push(`// Lines ${range.start + 1}-${range.end} (errors: ${relevantErrors.map(e => `L${e.line}:${e.code}`).join(', ')})`);
for (let i = range.start; i < range.end; i++) {
const lineNum = i + 1;
const isErrorLine = relevantErrors.some(e => e.line === lineNum);
const prefix = isErrorLine ? '>>> ' : ' ';
chunks.push(`${prefix}${lineNum}: ${lines[i]}`);
}
chunks.push('');
}
return chunks.join('\n');
}
/**
* Parse fix instructions from LLM response
*/
function parseFixInstructions(content: string): { fixes: FixInstruction[]; explanation?: string } {
try {
// Extract JSON from response
const jsonStr = extractJSONFromResponse(content, 'fix-instructions');
if (!jsonStr) {
throw new Error("No JSON found in response");
}
// Parse JSON
const parsed = JSON.parse(jsonStr);
if (!parsed.fixes || !Array.isArray(parsed.fixes)) {
throw new Error("Response missing 'fixes' array");
}
// Validate each fix has required fields
for (const fix of parsed.fixes) {
if (typeof fix.line !== 'number' || fix.line < 1) {
throw new Error(`Invalid line number: ${fix.line}`);
}
if (!['replace', 'insert_before', 'insert_after', 'delete'].includes(fix.action)) {
throw new Error(`Invalid action: ${fix.action}`);
}
if (fix.action !== 'delete' && fix.content === undefined) {
throw new Error(`Missing content for ${fix.action} action at line ${fix.line}`);
}
}
return parsed;
} catch (error) {
throw new Error(`Failed to parse fix instructions: ${(error as Error).message}`);
}
}
// =============================================
// Main Implementer Function
// =============================================
/**
* Build implementation prompt for code generation
*/
function buildImplementationPrompt(payload: ImplementerPayload): string {
// RAW OUTPUT MODE: For transformation tasks
if (payload.rawOutput) {
return payload.taskDescription;
}
// DIFF MODE: Build specialized prompt for error fixing
if (payload.diffMode && payload.errors && payload.errors.length > 0) {
const sections: string[] = [];
const lang = payload.language || DEFAULT_LANGUAGE;
sections.push(`You are an expert ${lang} developer fixing compiler errors.`);
sections.push(`Analyze the errors and provide ONLY the specific line fixes needed.`);
sections.push('');
sections.push(`## File: \`${payload.targetFile}\``);
sections.push('');
sections.push(`## Compiler Errors to Fix`);
for (const error of payload.errors) {
sections.push(`- **Line ${error.line}** [${error.code}]: ${error.message}`);
if (error.suggestion) {
sections.push(` Suggestion: ${error.suggestion}`);
}
}
sections.push('');
sections.push(`## Code Context (error regions only)`);
sections.push('```' + lang.toLowerCase());
sections.push(extractErrorContext(payload.fullFileContent || '', payload.errors));
sections.push('```');
sections.push('');
if (payload.fileContents && payload.fileContents.length > 0) {
sections.push(`## Related Files (for type references)`);
for (const { path, content } of payload.fileContents) {
sections.push(`### ${path}`);
sections.push('```');
sections.push(content.slice(0, 1500));
sections.push('```');
}
sections.push('');
}
sections.push(`## Output Format`);
sections.push(`Return ONLY valid JSON with fix instructions:`);
sections.push('```json');
sections.push(JSON.stringify({
fixes: [
{ line: 45, action: "replace", content: "fixed line content here" },
{ line: 102, action: "insert_after", content: "new line to insert" },
{ line: 156, action: "delete" }
],
explanation: "Brief explanation of fixes"
}, null, 2));
sections.push('```');
sections.push('');
sections.push(`## Available Actions`);
sections.push(`- \`replace\`: Replace line(s) with new content. Use \`endLine\` for multi-line.`);
sections.push(`- \`insert_before\`: Insert new line(s) before the specified line.`);
sections.push(`- \`insert_after\`: Insert new line(s) after the specified line.`);
sections.push(`- \`delete\`: Remove line(s). Use \`endLine\` for multi-line deletion.`);
sections.push('');
if (lang === 'rust') {
sections.push(`## Rust-Specific Guidance`);
sections.push(`- E0599 (method not found): Add impl block or use correct trait`);
sections.push(`- E0560 (struct field missing): Add the missing field to struct`);
sections.push(`- E0308 (type mismatch): Fix the type or add conversion`);
sections.push(`- E0277 (trait not implemented): Add impl or derive macro`);
sections.push(`- E0382 (moved value): Use clone, reference, or restructure`);
sections.push('');
} else if (['typescript', 'javascript'].includes(lang.toLowerCase())) {
sections.push(`## TypeScript-Specific Guidance`);
sections.push(`- TS2304 (cannot find name): Add import or declare`);
sections.push(`- TS2339 (property doesn't exist): Add to interface or type assertion`);
sections.push(`- TS2345 (argument type): Fix type or add conversion`);
sections.push(`- TS2322 (type not assignable): Fix assignment or add type guard`);
sections.push('');
}
sections.push(`IMPORTANT: Return ONLY the JSON object with fixes array. Do NOT return full file content.`);
return sections.join('\n');
}
// STANDARD MODE: Code generation with JSON wrapping
const sections: string[] = [];
// Role and task
sections.push(`You are an expert ${payload.language || DEFAULT_LANGUAGE} developer.`);
sections.push(`Generate code for this atomic micro-task.`);
sections.push('');
// Task description
sections.push(`## Task`);
sections.push(payload.taskDescription);
sections.push('');
// Target file
sections.push(`## Target File`);
sections.push(`\`${payload.targetFile}\``);
sections.push('');
// Context hints
if (payload.contextHints && payload.contextHints.length > 0) {
sections.push(`## Context Hints`);
payload.contextHints.forEach(hint => sections.push(`- ${hint}`));
sections.push('');
}
// Pre-read file contents
if (payload.fileContents && payload.fileContents.length > 0) {
sections.push(`## Existing Code Context`);
for (const { path, content } of payload.fileContents) {
sections.push(`### ${path}`);
sections.push('```');
sections.push(content.slice(0, 2000)); // Limit context size
sections.push('```');
sections.push('');
}
}
// Output format
sections.push(`## Output Format`);
sections.push(`Return ONLY valid JSON with this structure:`);
sections.push('```json');
sections.push(JSON.stringify({
code: "// Your generated code here",
explanation: "Brief explanation of what the code does"
}, null, 2));
sections.push('```');
sections.push('');
sections.push(`IMPORTANT: Return ONLY the JSON object, no additional text.`);
return sections.join('\n');
}
/**
* Build NACK prompt for retry after validation failure
*/
function buildNackPrompt(
payload: ImplementerPayload,
failedFixes: Array<{ fix: FixInstruction; reason: string }>,
syntaxValid: boolean,
previousFixes: FixInstruction[]
): string {
const sections: string[] = [];
const lang = payload.language || DEFAULT_LANGUAGE;
sections.push(`Your previous fix attempt FAILED. Please provide corrected fixes.`);
sections.push('');
sections.push(`## Validation Failures`);
if (!syntaxValid) {
sections.push(`- **SYNTAX ERROR**: The resulting code has unbalanced brackets/braces/parentheses`);
}
if (failedFixes.length > 0) {
sections.push(`- **Failed Fixes**:`);
for (const { fix, reason } of failedFixes) {
sections.push(` - Line ${fix.line} (${fix.action}): ${reason}`);
}
}
sections.push('');
sections.push(`## Your Previous Fixes (that failed)`);
sections.push('```json');
sections.push(JSON.stringify({ fixes: previousFixes }, null, 2));
sections.push('```');
sections.push('');
sections.push(`## Original Errors to Fix`);
for (const error of payload.errors!) {
sections.push(`- **Line ${error.line}** [${error.code}]: ${error.message}`);
}
sections.push('');
sections.push(`## Code Context`);
sections.push('```' + lang.toLowerCase());
sections.push(extractErrorContext(payload.fullFileContent!, payload.errors!));
sections.push('```');
sections.push('');
sections.push(`## Instructions`);
sections.push(`1. Analyze why your previous fixes failed`);
sections.push(`2. Ensure all line numbers are correct (1-indexed)`);
sections.push(`3. Ensure bracket/brace balance is maintained`);
sections.push(`4. Return corrected JSON with fixes array`);
sections.push('');
sections.push(`IMPORTANT: Return ONLY valid JSON with the corrected fixes array.`);
return sections.join('\n');
}
/**
* Main implementer function
*
* Implements MDAP tasks with support for diff mode and validation loops.
* Uses GLM 4.6 with thinking disabled for fast implementation.
*
* @param payload - Task implementation details
* @returns Promise<ImplementerResult> - Implementation result
*/
export async function implement(payload: ImplementerPayload): Promise<ImplementerResult> {
const startTime = Date.now();
let retryCount = 0;
let fixes: FixInstruction[] = [];
let failedFixes: Array<{ fix: FixInstruction; reason: string }> = [];
let syntaxValid = false;
let generatedCode = '';
try {
// DIFF MODE: Apply fixes with validation loop
if (payload.diffMode && payload.errors && payload.errors.length > 0) {
while (retryCount <= MAX_DIFF_RETRIES) {
const prompt = retryCount === 0
? buildImplementationPrompt(payload)
: buildNackPrompt(payload, failedFixes, syntaxValid, fixes);
// Call GLM with thinking disabled for fast implementation
const response: GLMResponse = await callGLMFast(prompt, {
maxTokens: 2048,
temperature: 0.3,
});
// Parse fix instructions
const parsed = parseFixInstructions(response.content);
fixes = parsed.fixes;
// Apply fixes using diff applicator
const { applyFixes } = await import('./diff-applicator.js');
const result = applyFixes(payload.fullFileContent || '', fixes, payload.language || DEFAULT_LANGUAGE);
generatedCode = result.content;
failedFixes = result.failedFixes;
syntaxValid = result.syntaxValid;
// If successful, break the retry loop
if (result.success && syntaxValid) {
break;
}
retryCount++;
}
return {
success: syntaxValid && failedFixes.length === 0,
filePath: payload.targetFile,
targetFile: payload.targetFile,
linesWritten: generatedCode.split('\n').length,
confidence: syntaxValid && failedFixes.length === 0 ? 0.9 : 0.5,
durationMs: Date.now() - startTime,
modelName: 'zai-glm-4.6',
generatedCode,
fixesApplied: fixes.length - failedFixes.length,
fixesFailed: failedFixes,
syntaxValid,
retryCount,
error: syntaxValid && failedFixes.length === 0 ? undefined : 'Some fixes failed validation',
};
}
// STANDARD MODE: Generate full code
const prompt = buildImplementationPrompt(payload);
// Call GLM with thinking disabled for fast implementation
const response: GLMResponse = await callGLMFast(prompt, {
maxTokens: 4096,
temperature: 0.5,
});
// Parse the response
if (payload.rawOutput) {
// Return raw response without JSON parsing
generatedCode = response.content;
} else {
// Extract JSON from response
const jsonStr = extractJSONFromResponse(response.content, 'code-generation');
if (!jsonStr) {
throw new Error("No JSON found in response");
}
// Parse JSON
const parsed = JSON.parse(jsonStr);
if (!parsed.code) {
throw new Error("Response missing 'code' field");
}
generatedCode = parsed.code;
}
return {
success: true,
filePath: payload.targetFile,
targetFile: payload.targetFile,
linesWritten: generatedCode.split('\n').length,
confidence: 0.9,
durationMs: Date.now() - startTime,
modelName: 'zai-glm-4.6',
tokens: {
input: response.inputTokens,
output: response.outputTokens,
},
generatedCode,
};
} catch (error) {
return {
success: false,
filePath: payload.targetFile,
targetFile: payload.targetFile,
linesWritten: 0,
confidence: 0.0,
durationMs: Date.now() - startTime,
error: error instanceof Error ? error.message : String(error),
generatedCode,
};
}
}