How PBL Scenes Are Handled in the OpenMAIC Generation Pipeline
OpenMAIC (THU-MAIC/OpenMAIC) processes Problem-Based Learning (PBL) scenes through a two-phase pipeline: design-time generation via the PBL v2 planner agent for initial project creation, followed by runtime-aware hydration and design-template persistence to keep learner state separate from the immutable project blueprint.
Problem-Based Learning (PBL) scenes require special handling in educational AI systems because they combine structured project design with dynamic learner collaboration. The OpenMAIC platform, developed by THU-MAIC, implements a sophisticated generation pipeline that separates author-defined project templates from learner-specific runtime state. This article examines how PBL scenes move through the OpenMAIC generation pipeline, from initial LLM-based planning to document persistence.
Design-Time Generation: The PBL v2 Planner
When converting classroom outlines into concrete scenes, the generateClassroom routine in lib/server/classroom-generation.ts detects PBL outlines by their type: 'pbl' field. Standard scenes receive a direct LLM call, but PBL scenes trigger specialized planning logic.
Injecting the PBL Planner as a Fallback
The content generation call receives a pblLoopFallback option that delegates project construction to the PBL v2 planner agent:
// Inside generateClassroom (lib/server/classroom-generation.ts)
const content = await withGenerationRetry(
() =>
generateSceneContent(
safeOutline,
contentCall.aiCall,
{
agents,
languageDirective,
allowProceduralSkill: vocationalActive,
// PBL scenes use the planner as a fallback LLM loop
...(safeOutline.type === 'pbl'
? {
pblLoopFallback: (input) =>
generatePBLV2Project(
input,
contentCall.model,
callLLM,
{ logger: log },
contentCall.thinking,
),
}
: {}),
},
),
{ /* retry options */ },
);
The generatePBLV2Project function in lib/pbl/v2/agents/planner.ts executes a tool-calling loop that populates a complete PBLProjectV2 schema:
- Project metadata and description
- Exactly one Instructor role
- Milestones with associated micro-tasks
- Optional role-play scenario fields
The loop terminates when the mark_design_complete tool succeeds. The resulting PBLProjectV2 object attaches to the scene as scene.content.projectV2, containing no learner-specific state—only the author-intended project structure.
Runtime Hydration: Merging Live Learner State
Once a PBL scene exists, OpenMAIC must reconcile the static design template with each learner's progress. This happens through runtime hydration in lib/pbl/v2/runtime/hydration.ts.
The hydratePBLScenesFromRuntime Function
// lib/pbl/v2/runtime/hydration.ts
export async function hydratePBLScenesFromRuntime(
stageId: string,
scenes: readonly Scene[],
options: Pick<HydratePBLProjectArgs, 'store' | 'kv' | 'learnerKey'> = {},
): Promise<Scene[]> {
return Promise.all(
scenes.map(async (scene) => {
if (scene.content.type !== 'pbl') return scene;
const resolved = resolvePBLContent(scene.content);
if (resolved.kind !== 'v2') return scene;
const result = await hydratePBLProjectFromRuntime({
stageId,
sceneId: scene.id,
project: resolved.projectV2,
...options,
});
return { ...scene, content: { ...scene.content, projectV2: result.project } };
}),
);
}
For each v2 PBL scene, hydratePBLScenesFromRuntime:
- Retrieves the learner's runtime snapshot from the KV store
- Folds in any stored events (task completions, milestone progress)
- Merges live learner state back into
projectV2 - Falls back to the original document state if hydration fails
This ensures instructors and learners always see current progress without polluting the underlying design template.
Document Persistence: Separating Design from Runtime Data
Before storing a classroom document, preparePBLScenesForDocumentPersistence in lib/pbl/v2/runtime/document-persistence.ts performs a two-step synchronization:
// lib/pbl/v2/runtime/document-persistence.ts
export async function preparePBLScenesForDocumentPersistence(
stageId: string,
scenes: readonly Scene[],
): Promise<Scene[]> {
// 1️⃣ Write any pending runtime changes
await Promise.all(
scenes.map(async (scene) => {
const content = scene.content;
if (content.type !== 'pbl') return;
const resolved = resolvePBLContent(content);
if (resolved.kind !== 'v2') return;
await synchronizePBLProjectRuntime({
stageId,
sceneId: scene.id,
project: resolved.projectV2,
});
}),
);
// 2️⃣ Return scenes with only the design template stored
return scenes.map((scene) => {
const content = scene.content;
if (content.type !== 'pbl') return scene;
const resolved = resolvePBLContent(content);
if (resolved.kind !== 'v2') return scene;
const designTemplate = stripToDesignTemplate(resolved.projectV2);
return {
...scene,
content: { ...content, projectV2: designTemplate },
};
});
}
This function:
- Synchronizes pending learner state to the runtime store via
synchronizePBLProjectRuntime - Strips the full
projectV2to its design template usingstripToDesignTemplate - Returns scenes containing only the immutable blueprint, ensuring collaborative editing always starts from a consistent baseline
Key Code Files and Responsibilities
| File | Purpose |
|---|---|
lib/server/classroom-generation.ts |
Orchestrates generation; injects pblLoopFallback for PBL outlines |
lib/pbl/v2/agents/planner.ts |
generatePBLV2Project — agentic planner building PBLProjectV2 |
lib/pbl/v2/runtime/hydration.ts |
hydratePBLScenesFromRuntime — merges runtime state into scenes |
lib/pbl/v2/runtime/document-persistence.ts |
preparePBLScenesForDocumentPersistence — persists runtime changes and strips to template |
lib/pbl/legacy/read.ts |
resolvePBLContent — detects v1 vs. v2 PBL project formats |
Summary
- PBL scene generation in OpenMAIC uses a specialized planner agent (
generatePBLV2Project) invoked throughpblLoopFallbackwhentype: 'pbl'is detected inlib/server/classroom-generation.ts. - Runtime hydration (
hydratePBLScenesFromRuntime) merges live learner progress from the KV store without modifying the stored document. - Document persistence (
preparePBLScenesForDocumentPersistence) writes runtime changes to storage, then strips scenes to their design template for clean separation of concerns. - The
PBLProjectV2schema maintains strict boundaries: design-time data is immutable, runtime data is ephemeral and learner-specific.
Frequently Asked Questions
What triggers the PBL v2 planner during generation?
The planner triggers when a classroom outline specifies type: 'pbl'. The generateClassroom function passes this type to generateSceneContent, which activates the pblLoopFallback option. This fallback delegates to generatePBLV2Project rather than making a standard LLM call, enabling the multi-tool planning loop required for complex PBL project structures.
How does OpenMAIC keep learner progress separate from the project design?
OpenMAIC maintains two distinct storage layers: the classroom document stores only the design template (the output of stripToDesignTemplate), while learner-specific state lives in a separate KV/runtime store. The hydratePBLScenesFromRuntime function merges these on demand when a learner loads the scene, and synchronizePBLProjectRuntime writes changes back before persistence. This architecture enables multiple learners to share the same project blueprint with individual progress tracking.
What happens if runtime hydration fails for a PBL scene?
According to the implementation in lib/pbl/v2/runtime/hydration.ts, the hydration function keeps the original document state as a fallback when retrieval or merging fails. Learners see the baseline project template rather than an error, though they may need to re-establish their runtime session. This graceful degradation ensures classroom usability even with transient storage issues.
How can I distinguish between v1 and v2 PBL projects in the codebase?
Use resolvePBLContent from lib/pbl/legacy/read.ts. This utility examines scene.content and returns a discriminated union indicating whether the project follows the legacy v1 format or the current v2 schema. All modern PBL generation in OpenMAIC targets v2, but the resolver maintains backward compatibility for existing classrooms.
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 →