ManimCat / src /workflow /code-retry /manager.ts
Bin29's picture
Sync from main: 5548f84 fix(workflow): preserve reference images across generation and recovery [skip ci]
b1df986
Raw History Blame Contribute Delete
6.38 kB
import { performance } from 'node:perf_hooks'
import { createLogger } from '../../shared/logging/logger'
import type {
CodeRetryContext,
RenderResult,
RetryManagerResult,
RetryCheckpoint
} from './types'
import type { OutputMode, PromptOverrides, ProblemFramingPlan, ReferenceImage } from '../../types'
import { extractErrorContext, extractErrorMessage, getErrorType } from './utils'
import { retryCodeGeneration } from './code-generation'
import { JobCancelledError } from '../../shared/errors/errors'
import { classifyRenderFailure } from './render-failure-policy'
const logger = createLogger('CodeRetryManager')
const MAX_RETRIES = parseInt(process.env.CODE_RETRY_MAX_RETRIES || '6', 10)
export function createRetryContext(
concept: string,
sceneDesign: string,
promptOverrides?: PromptOverrides,
outputMode: OutputMode = 'video',
problemPlan?: ProblemFramingPlan,
referenceImages?: ReferenceImage[]
): CodeRetryContext {
return {
referenceImages,
problemPlan,
concept,
sceneDesign,
outputMode,
promptOverrides
}
}
export { buildRetryFixPrompt } from './prompt-builder'
export async function executeCodeRetry(
context: CodeRetryContext,
renderer: (code: string) => Promise<RenderResult>,
customApiConfig?: any,
initialCode?: string,
onRenderFailure?: (event: {
attempt: number
code: string
codeSnippet?: string
stderr: string
stdout: string
peakMemoryMB: number
exitCode?: number
}) => Promise<void> | void,
onCheckpoint?: RetryCheckpoint,
timings?: Record<string, number>,
onRetryStart?: () => Promise<void> | void
): Promise<RetryManagerResult> {
logger.info('Starting code retry manager', {
concept: context.concept,
maxRetries: MAX_RETRIES
})
let generationTimeMs = 0
let currentCode = initialCode?.trim() || ''
if (!currentCode) {
throw new Error('Code retry requires existing code; full regeneration mode is disabled')
}
if (onCheckpoint) {
await onCheckpoint()
}
let renderResult = await renderer(currentCode)
let currentCodeSnippet = renderResult.codeSnippet
if (renderResult.success) {
logger.info('Initial render succeeded')
return { code: currentCode, success: true, attempts: 1, generationTimeMs }
}
if (onRenderFailure) {
try {
await onRenderFailure({
attempt: 1,
code: currentCode,
codeSnippet: renderResult.codeSnippet,
stderr: renderResult.stderr,
stdout: renderResult.stdout,
peakMemoryMB: renderResult.peakMemoryMB,
exitCode: renderResult.exitCode
})
} catch (error) {
logger.warn('onRenderFailure callback failed', { attempt: 1, error: String(error) })
}
}
let errorMessage = extractErrorContext(renderResult.stderr)
let errorType = getErrorType(renderResult.stderr)
const failureDecision = classifyRenderFailure(renderResult.stderr)
logger.warn('Initial render failed', { errorType, error: errorMessage })
if (!failureDecision.allowAiRepair) {
logger.warn('Skipping AI code retry for non-repairable render failure', {
category: failureDecision.category,
reason: failureDecision.reason
})
return {
code: currentCode,
success: false,
attempts: 1,
generationTimeMs,
lastError: `${failureDecision.reason}: ${extractErrorMessage(renderResult.stderr)}`,
failureCategory: failureDecision.category,
retrySkipped: true
}
}
if (onRetryStart) {
await onRetryStart()
if (timings) timings.retry = generationTimeMs
}
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
logger.info('Starting retry patch attempt', {
totalAttempts: attempt + 1,
errorType,
error: errorMessage
})
const generationStart = performance.now()
let generationRecorded = false
try {
if (onCheckpoint) {
await onCheckpoint()
}
currentCode = await retryCodeGeneration(
context,
errorMessage,
attempt,
currentCode,
currentCodeSnippet,
customApiConfig,
onCheckpoint
)
generationTimeMs += performance.now() - generationStart
generationRecorded = true
if (timings) timings.retry = generationTimeMs
if (onCheckpoint) {
await onCheckpoint()
}
renderResult = await renderer(currentCode)
currentCodeSnippet = renderResult.codeSnippet
if (renderResult.success) {
logger.info('Retry render succeeded', { attempt: attempt + 1 })
return { code: currentCode, success: true, attempts: attempt + 1, generationTimeMs }
}
if (onRenderFailure) {
try {
await onRenderFailure({
attempt: attempt + 1,
code: currentCode,
codeSnippet: renderResult.codeSnippet || currentCode,
stderr: renderResult.stderr,
stdout: renderResult.stdout,
peakMemoryMB: renderResult.peakMemoryMB,
exitCode: renderResult.exitCode
})
} catch (error) {
logger.warn('onRenderFailure callback failed', { attempt: attempt + 1, error: String(error) })
}
}
errorMessage = extractErrorContext(renderResult.stderr)
errorType = getErrorType(renderResult.stderr)
logger.warn('Retry render failed', { attempt: attempt + 1, errorType, error: errorMessage })
} catch (error) {
if (!generationRecorded) {
generationTimeMs += performance.now() - generationStart
if (timings) timings.retry = generationTimeMs
}
if (error instanceof JobCancelledError) {
logger.warn('Code retry aborted because job was cancelled', {
attempt: attempt + 1,
reason: error.details
})
throw error
}
logger.error('Retry patch process failed', { attempt: attempt + 1, error: String(error) })
}
}
logger.error('All retry patch attempts failed', {
totalAttempts: MAX_RETRIES + 1,
finalError: extractErrorMessage(renderResult.stderr)
})
return {
code: currentCode,
success: false,
attempts: MAX_RETRIES + 1,
generationTimeMs,
lastError: extractErrorMessage(renderResult.stderr)
}
}
export type {
CodeRetryOptions,
CodeRetryResult,
RenderResult,
RetryManagerResult,
ChatMessage,
CodeRetryContext
} from './types'