Implement prompt generation system that produces continuation prompts based on verification failures to force AI agents to complete work. Service: - generatePrompt: Complete prompt from failure context - generateTestFailurePrompt: Test-specific guidance - generateBuildErrorPrompt: Build error resolution - generateCoveragePrompt: Coverage improvement strategy - generateIncompleteWorkPrompt: Completion requirements Templates: - base.template: System/user prompt structure - test-failure.template: Test fix guidance - build-error.template: Compilation error guidance - coverage.template: Coverage improvement strategy - incomplete-work.template: Completion requirements Constraint escalation: - Attempt 1: Normal guidance - Attempt 2: Focus only on failures - Attempt 3: Minimal changes only - Final: Last attempt warning Priority levels: critical/high/normal based on failure severity Tests: 24 passing with 95.31% coverage Fixes #137 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
208 lines
6.2 KiB
TypeScript
208 lines
6.2 KiB
TypeScript
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";
|
|
}
|
|
}
|