import { Injectable } from "@nestjs/common"; import { ContinuationPromptContext, FailureDetail, ContinuationPrompt } from "./interfaces"; import { BASE_CONTINUATION_SYSTEM, BASE_USER_PROMPT, TEST_FAILURE_TEMPLATE, BUILD_ERROR_TEMPLATE, COVERAGE_TEMPLATE, INCOMPLETE_WORK_TEMPLATE, } from "./templates"; /** * Service for generating continuation prompts when AI agent work is incomplete */ @Injectable() export class ContinuationPromptsService { /** * Generate a complete continuation prompt from context */ generatePrompt(context: ContinuationPromptContext): ContinuationPrompt { const systemPrompt = BASE_CONTINUATION_SYSTEM; const constraints = this.getConstraints(context.attemptNumber, context.maxAttempts); // Format failures based on their types const formattedFailures = this.formatFailuresByType(context.failures); // Build user prompt const userPrompt = BASE_USER_PROMPT.replace("{{taskDescription}}", context.originalTask) .replace("{{attemptNumber}}", String(context.attemptNumber)) .replace("{{maxAttempts}}", String(context.maxAttempts)) .replace("{{failures}}", formattedFailures) .replace("{{constraints}}", this.formatConstraints(constraints)); // Determine priority const priority = this.determinePriority(context); return { systemPrompt, userPrompt, constraints, priority, }; } /** * Generate test failure specific prompt */ generateTestFailurePrompt(failures: FailureDetail[]): string { const formattedFailures = this.formatFailuresForPrompt(failures); return TEST_FAILURE_TEMPLATE.replace("{{failures}}", formattedFailures); } /** * Generate build error specific prompt */ generateBuildErrorPrompt(failures: FailureDetail[]): string { const formattedErrors = this.formatFailuresForPrompt(failures); return BUILD_ERROR_TEMPLATE.replace("{{errors}}", formattedErrors); } /** * Generate coverage improvement prompt */ generateCoveragePrompt(current: number, required: number): string { const gap = required - current; return COVERAGE_TEMPLATE.replace("{{currentCoverage}}", String(current)) .replace("{{requiredCoverage}}", String(required)) .replace("{{gap}}", String(gap)) .replace("{{uncoveredFiles}}", "(See coverage report for details)"); } /** * Generate incomplete work prompt */ generateIncompleteWorkPrompt(issues: string[]): string { const formattedIssues = issues.map((issue) => `- ${issue}`).join("\n"); return INCOMPLETE_WORK_TEMPLATE.replace("{{issues}}", formattedIssues); } /** * Get constraints based on attempt number */ getConstraints(attemptNumber: number, maxAttempts: number): string[] { const constraints: string[] = [ "Address ALL failures listed above", "Run all quality checks before claiming completion", ]; if (attemptNumber >= 2) { constraints.push("Focus only on failures, no new features"); } if (attemptNumber >= 3) { constraints.push("Minimal changes only, fix exact issues"); } if (attemptNumber >= maxAttempts) { constraints.push("This is your LAST attempt. Failure means manual intervention required."); } return constraints; } /** * Format failures for inclusion in prompt */ formatFailuresForPrompt(failures: FailureDetail[]): string { if (failures.length === 0) { return ""; } return failures .map((failure, index) => { const parts: string[] = [`${String(index + 1)}. [${failure.type}] ${failure.message}`]; if (failure.location) { parts.push(` Location: ${failure.location}`); } if (failure.details) { parts.push(` Details: ${failure.details}`); } if (failure.suggestion) { parts.push(` Suggestion: ${failure.suggestion}`); } return parts.join("\n"); }) .join("\n\n"); } /** * Format failures by type using appropriate templates */ private formatFailuresByType(failures: FailureDetail[]): string { const sections: string[] = []; // Group failures by type const testFailures = failures.filter((f) => f.type === "test-failure"); const buildErrors = failures.filter((f) => f.type === "build-error"); const coverageIssues = failures.filter((f) => f.type === "coverage"); const incompleteWork = failures.filter((f) => f.type === "incomplete-work"); const lintErrors = failures.filter((f) => f.type === "lint-error"); if (testFailures.length > 0) { sections.push(this.generateTestFailurePrompt(testFailures)); } if (buildErrors.length > 0) { sections.push(this.generateBuildErrorPrompt(buildErrors)); } if (coverageIssues.length > 0) { // Extract coverage numbers from message if available const coverageFailure = coverageIssues[0]; if (coverageFailure) { const match = /(\d+)%.*?(\d+)%/.exec(coverageFailure.message); if (match?.[1] && match[2]) { sections.push(this.generateCoveragePrompt(parseInt(match[1]), parseInt(match[2]))); } else { sections.push(this.formatFailuresForPrompt(coverageIssues)); } } } if (incompleteWork.length > 0) { const issues = incompleteWork.map((f) => f.message); sections.push(this.generateIncompleteWorkPrompt(issues)); } if (lintErrors.length > 0) { sections.push("Lint Errors:\n" + this.formatFailuresForPrompt(lintErrors)); } return sections.join("\n\n---\n\n"); } /** * Format constraints as a bulleted list */ private formatConstraints(constraints: string[]): string { return "CONSTRAINTS:\n" + constraints.map((c) => `- ${c}`).join("\n"); } /** * Determine priority based on context */ private determinePriority(context: ContinuationPromptContext): "critical" | "high" | "normal" { // Final attempt is always critical if (context.attemptNumber >= context.maxAttempts) { return "critical"; } // Build errors and test failures are high priority const hasCriticalFailures = context.failures.some( (f) => f.type === "build-error" || f.type === "test-failure" ); if (hasCriticalFailures) { return "high"; } // Everything else is normal return "normal"; } }