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:

  1. Retrieves the learner's runtime snapshot from the KV store
  2. Folds in any stored events (task completions, milestone progress)
  3. Merges live learner state back into projectV2
  4. 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 projectV2 to its design template using stripToDesignTemplate
  • 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 through pblLoopFallback when type: 'pbl' is detected in lib/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 PBLProjectV2 schema 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →