Files
stack/apps/api/src/continuation-prompts/continuation-prompts.service.ts
Jason Woltje 0387cce116 feat(#137): create Forced Continuation Prompt System
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>
2026-01-31 13:51:46 -06:00

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";
}
}