UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.

590 lines (511 loc) 19.6 kB
/** * 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, }; } }