File size: 3,458 Bytes
b1df986
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import { createRetryContext, executeCodeRetry } from '../../../../workflow/code-retry/manager'
import type { ConceptInput, OutputMode, PromptOverrides, ProblemFramingPlan } from '../../../../types'

export interface RenderAttemptResult {
  success: boolean
  stderr: string
  stdout: string
  peakMemoryMB: number
  exitCode?: number
  codeSnippet?: string
}

interface RenderFailureLogArgs {
  attempt: number
  code: string
  codeSnippet?: string
  stderr: string
  stdout: string
  peakMemoryMB: number
  exitCode?: number
  promptRole: 'codeRetry' | 'single-render'
}

interface ExecuteRenderWithRetryArgs extends ConceptInput {
  problemPlan?: ProblemFramingPlan
  outputMode: OutputMode
  sceneDesign?: string
  promptOverrides?: PromptOverrides
  customApiConfig?: unknown
  initialCode: string
  usedAI: boolean
  timings?: Record<string, number>
  renderCode: (code: string) => Promise<RenderAttemptResult>
  logRenderFailure: (args: RenderFailureLogArgs) => Promise<void>
  ensureJobNotCancelled?: () => Promise<void>
  onRetryStart?: () => Promise<void> | void
}

export async function executeRenderWithRetry(args: ExecuteRenderWithRetryArgs): Promise<{
  finalCode: string
  renderPeakMemoryMB: number
}> {
  const {
    concept,
    outputMode,
    sceneDesign,
    promptOverrides,
    customApiConfig,
    initialCode,
    usedAI,
    timings,
    renderCode,
    logRenderFailure,
    ensureJobNotCancelled,
    onRetryStart
  } = args

  if (usedAI) {
    const retryContext = createRetryContext(
      concept,
      sceneDesign?.trim() || `概念: ${concept}`,
      promptOverrides,
      outputMode,
      args.problemPlan,
      args.referenceImages
    )

    const retryManagerResult = await executeCodeRetry(
      retryContext,
      renderCode,
      customApiConfig,
      initialCode,
      async (event) => {
        await logRenderFailure({ ...event, promptRole: 'codeRetry' })
      },
      ensureJobNotCancelled,
      timings,
      onRetryStart
    )

    if (typeof retryManagerResult.generationTimeMs === 'number' && timings) {
      timings.retry = retryManagerResult.generationTimeMs
    }

    if (!retryManagerResult.success) {
      // A cancelled render is not a code failure. Re-raise the job's cancellation with its
      // original JobCancelledError instead of the retry manager's wrapped diagnostic.
      if (retryManagerResult.failureCategory === 'cancelled') {
        await ensureJobNotCancelled?.()
      }
      const failurePrefix = retryManagerResult.retrySkipped
        ? `AI retry skipped for ${retryManagerResult.failureCategory || 'operational'} failure`
        : `Code retry failed after ${retryManagerResult.attempts} attempts`
      throw new Error(
        `${failurePrefix}: ${retryManagerResult.lastError}`
      )
    }

    return {
      finalCode: retryManagerResult.code,
      renderPeakMemoryMB: 0
    }
  }

  const singleAttempt = await renderCode(initialCode)
  if (!singleAttempt.success) {
    await logRenderFailure({
      attempt: 1,
      code: initialCode,
      codeSnippet: singleAttempt.codeSnippet,
      stderr: singleAttempt.stderr,
      stdout: singleAttempt.stdout,
      peakMemoryMB: singleAttempt.peakMemoryMB,
      exitCode: singleAttempt.exitCode,
      promptRole: 'single-render'
    })
    throw new Error(singleAttempt.stderr || 'Manim render failed')
  }

  return {
    finalCode: initialCode,
    renderPeakMemoryMB: singleAttempt.peakMemoryMB
  }
}