How OpenMAIC Implements Generation Retry Logic: A Code-Deep Analysis
OpenMAIC implements generation retry logic through a centralized withGenerationRetry helper in packages/@openmaic/generation/src/generation-retry.ts, which wraps async operations with configurable exponential back-off, intelligent error classification, and result-based retry predicates.
The THU-MAIC/OpenMAIC repository provides a sophisticated, declarative approach to handling transient failures in LLM-powered generation workflows. Rather than scattering retry logic across dozens of call sites, the codebase centralizes resilience patterns in a single, well-typed utility that every generation flow—from slides to quizzes to interactive widgets—can leverage consistently.
Core Architecture of the Retry System
The generation retry logic in OpenMAIC rests on six integrated components, all implemented in the same source file.
GenerationRetryOptions: Configuration Interface
The GenerationRetryOptions<T> interface exposes fine-grained control over retry behavior. Parameters include:
label– Human-readable identifier for loggingmaxRetries– Maximum retry attempts (default varies by caller)baseDelayMs/maxDelayMs– Back-off timing boundssignal– AbortController integrationsleep– Injectable sleep function for testabilityrandom– Jitter source (default:Math.random)shouldRetryResult– Predicate to trigger retry based on returned valueonRetry– Callback for progress surfacing (UI notifications, logs)
isRetryableGenerationError: Error Classification Engine
This function performs deep inspection of errors to determine retry eligibility. As implemented in generation-retry.ts, it evaluates:
| Check | Implementation |
|---|---|
| Explicit flag | error.isRetryable === true |
| HTTP status codes | Retryable: 408, 409, 425, 429; Non-retryable: 400-404, 422 |
| Network indicators | Substring matches: rate limit, ECONNRESET, ETIMEDOUT, etc. |
| Timeout detection | error.name === 'TimeoutError' or similar |
| Nested error traversal | Recursively inspects cause, lastError, errors fields |
This recursive design handles wrapped errors from HTTP clients, LLM SDKs, and intermediate processors without flattening useful context.
retryDelayMs: Exponential Back-off with Jitter
The delay calculation implements industry-standard exponential back-off with capped growth and randomization:
delay = min(maxDelayMs, baseDelayMs * 2^(attempt - 1))
jitter = delay * random() * 0.2 // ≤ 20% of calculated delay
finalDelay = delay + jitter
Jitter prevents thundering herd scenarios when multiple generation requests fail simultaneously.
withGenerationRetry: The Main Execution Loop
The withGenerationRetry(operation, options) function orchestrates the entire retry lifecycle. Its control flow handles four distinct paths:
- Success with acceptable result – Return immediately if
shouldRetryResultis absent or returnsfalse - Success requiring retry – Emit
onRetrywith reason "empty result" and back-off - Abort error – Re-throw immediately, bypassing retry logic
- Retryable error – Emit
onRetrywith derived reason and back-off; re-throw if max attempts exhausted
// Simplified structure from generation-retry.ts
const result = await withGenerationRetry(
async (attempt) => await aiCall(systemPrompt, userPrompt, visionImages),
{
label: `slide-${outline.title}`,
maxRetries: 5,
baseDelayMs: 1000,
onRetry: (e) => log.info(`Retry ${e.attempt}/${e.maxAttempts}: ${e.reason}`),
},
);
The attempt parameter passed to the operation callback enables sophisticated logging or alternative routing on subsequent tries.
isAbortError: Cross-Environment Abort Detection
OpenMAIC normalizes abort detection across:
- Browsers –
DOMExceptionwithname === 'AbortError' - Node.js – Native
AbortErrorinstances - Test environments – Plain objects with
nameproperty
This ensures AbortController signals propagate correctly regardless of execution context.
onRetry Callback: Observable Progress
The GenerationRetryEvent payload includes:
label– Operation identifierattempt/maxAttempts– Progress contextnextDelayMs– When the next attempt will occurreason– Human-readable classification ("rate limit", "empty result", etc.)
This enables UI components to display "Retrying in 3s..." messages and analytics pipelines to track retry patterns.
Integration Points in the Codebase
Scene Generation Pipeline
In packages/@openmaic/generation/src/scene-generator.ts, three core functions employ the retry wrapper:
generateSlideContent()– Wraps slide LLM callsgenerateQuizContent()– Wraps quiz generationgenerateWidgetContent()– Wraps interactive component creation
Each passes a contextual label containing the outline title, enabling precise failure attribution in distributed traces.
Classroom Generation Server
The server-side entry point at lib/server/classroom-generation.ts applies the same pattern to higher-level planner orchestration. This demonstrates the utility's flexibility: it handles not just raw LLM calls but composite operations spanning multiple model invocations.
Practical Code Examples
Basic LLM Call Wrapping
import { withGenerationRetry } from '@openmaic/generation';
import { aiCall } from './ai-service';
async function generateSlide(outline) {
const prompts = buildPrompt(outline);
const result = await withGenerationRetry(
async (attempt) => await aiCall(prompts.system, prompts.user),
{
label: `slide-${outline.title}`,
maxRetries: 4,
baseDelayMs: 1500,
onRetry: (e) => console.log(`Retry ${e.attempt}: ${e.reason} → ${e.nextDelayMs}ms`),
},
);
return parseJsonResponse(result);
}
Result-Based Retry: Empty Array Handling
await withGenerationRetry(
async () => await fetchActions(),
{
label: 'actions-fetch',
maxRetries: 3,
shouldRetryResult: (arr) => Array.isArray(arr) && arr.length === 0,
onRetry: (e) => console.warn(`Empty result, retrying: ${e.reason}`),
},
);
Test-Injectable Sleep for Deterministic Timing
await withGenerationRetry(
async () => await aiCall(prompts),
{
label: 'test-retry',
sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
// In Jest: replace with jest.advanceTimersByTime(ms) for instant resolution
},
);
AbortController Integration
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
try {
await withGenerationRetry(
async () => await aiCall(prompts),
{ label: 'abort-demo', signal: controller.signal },
);
} catch (e) {
if (e.name === 'AbortError') {
console.log('Generation aborted by user');
}
}
Key Source Files
| File | Role | Link |
|---|---|---|
packages/@openmaic/generation/src/generation-retry.ts |
Core implementation: helper, error classification, back-off logic | View source |
packages/@openmaic/generation/src/scene-generator.ts |
Scene generation integration (slides, quizzes, widgets) | View source |
lib/server/classroom-generation.ts |
Classroom planner pipeline integration | View source |
tests/server/generation-retry.test.ts |
Unit tests: retry triggers, back-off math, error classification | View source |
Summary
- Centralization – OpenMAIC implements generation retry logic in a single file (
generation-retry.ts) consumed across all generation workflows - Intelligent classification –
isRetryableGenerationErrorrecursively inspects errors for HTTP codes, network messages, and timeout indicators - Configurable back-off – Exponential delays with 20% jitter prevent thundering herds while bounding maximum wait times
- Dual trigger paths – Retries fire on both exceptions (
isRetryableGenerationError) and successful but unsatisfactory results (shouldRetryResult) - Full observability – The
onRetrycallback exposes structured events for logging, analytics, and UI progress indicators - Testability – Injectable
sleepandrandomfunctions enable deterministic unit tests without real delays
Frequently Asked Questions
How does OpenMAIC determine whether an error should trigger a retry?
The isRetryableGenerationError function in generation-retry.ts applies hierarchical checks: explicit isRetryable flags, specific HTTP status codes (408, 409, 425, 429), network-related message substrings (rate limit, ECONNRESET), timeout indicators, and recursive traversal of nested error fields (cause, lastError, errors). Errors classified as client errors (400-404, 422) bypass retry logic entirely.
Can OpenMAIC retry generation based on the result value, not just exceptions?
Yes. The shouldRetryResult predicate in GenerationRetryOptions enables result-driven retries. Callers provide a function that inspects the successful return value and returns true to trigger another attempt. This pattern handles cases where LLMs return valid but semantically empty responses, such as empty arrays or placeholder strings.
How does the retry back-off calculation work in OpenMAIC?
The retryDelayMs function implements capped exponential back-off with jitter. For attempt n, the base delay equals baseDelayMs * 2^(n-1), capped at maxDelayMs. A random jitter up to 20% of this value is added. The default baseDelayMs of 1000ms yields retry delays of approximately 1s, 2s, 4s, 8s, etc., with variation preventing synchronized retry storms.
Where is the generation retry logic tested in OpenMAIC?
Comprehensive unit tests reside in tests/server/generation-retry.test.ts. These verify: retry activation on null results, suppression for non-retryable HTTP codes, mathematical correctness of exponential back-off, proper handling of nested error structures, and immediate propagation of abort signals. The tests leverage the injectable sleep parameter to execute synchronously with Jest fake timers.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →