@thanh01.pmt/curriculum-kit 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -13525,6 +13525,78 @@ var DeterministicPipelineRunner = class {
13525
13525
 
13526
13526
  // src/services/prefillService.ts
13527
13527
  init_streamRunner();
13528
+ var DEFAULT_STREAM_IDLE_MS = 45e3;
13529
+ var DEFAULT_STREAM_TOTAL_MS = 3e5;
13530
+ var LAYER_TOTAL_BUDGET_MS = {
13531
+ 1: 18e4,
13532
+ 2: 36e4,
13533
+ 3: 36e4
13534
+ };
13535
+ function resolveStreamBudget(layer, opts) {
13536
+ const envIdle = Number(process.env.WIZARD_PREFILL_IDLE_TIMEOUT_MS);
13537
+ const envTotal = Number(process.env.WIZARD_PREFILL_TOTAL_TIMEOUT_MS);
13538
+ const layerTotal = layer !== void 0 ? LAYER_TOTAL_BUDGET_MS[layer] : void 0;
13539
+ return {
13540
+ idleMs: opts?.idleMs ?? (Number.isFinite(envIdle) && envIdle > 0 ? envIdle : DEFAULT_STREAM_IDLE_MS),
13541
+ totalMs: opts?.totalMs ?? (Number.isFinite(envTotal) && envTotal > 0 ? envTotal : layerTotal ?? DEFAULT_STREAM_TOTAL_MS)
13542
+ };
13543
+ }
13544
+ function extractStreamChunk(part) {
13545
+ if (!part || typeof part !== "object") return {};
13546
+ if (part.type === "reasoning-delta" || part.type === "reasoning") {
13547
+ const thought = part.text ?? part.delta ?? part.reasoning ?? "";
13548
+ return thought ? { thought } : {};
13549
+ }
13550
+ if (part.type === "text-delta") {
13551
+ const content = part.text ?? part.delta ?? "";
13552
+ return content ? { content } : {};
13553
+ }
13554
+ if (part.type === "raw") {
13555
+ const raw = part.rawValue;
13556
+ const delta = raw?.choices?.[0]?.delta;
13557
+ const reasoning = delta?.reasoning_content ?? delta?.reasoning ?? raw?.delta?.reasoning_content ?? raw?.delta?.reasoning;
13558
+ if (typeof reasoning === "string" && reasoning) return { thought: reasoning };
13559
+ const text = delta?.content ?? raw?.delta?.content;
13560
+ if (typeof text === "string" && text) return { content: text };
13561
+ return {};
13562
+ }
13563
+ return {};
13564
+ }
13565
+ function createStreamAbortSignal(budget) {
13566
+ const controller = new AbortController();
13567
+ let idleTimer = null;
13568
+ let totalTimer = null;
13569
+ const armIdle = () => {
13570
+ if (idleTimer) clearTimeout(idleTimer);
13571
+ if (budget.idleMs > 0) {
13572
+ idleTimer = setTimeout(
13573
+ () => controller.abort(new Error(`Idle timeout: no data for ${Math.round(budget.idleMs / 1e3)}s`)),
13574
+ budget.idleMs
13575
+ );
13576
+ idleTimer?.unref?.();
13577
+ }
13578
+ };
13579
+ armIdle();
13580
+ if (budget.totalMs > 0) {
13581
+ totalTimer = setTimeout(
13582
+ () => controller.abort(new Error(`Total stream timeout after ${Math.round(budget.totalMs / 1e3)}s`)),
13583
+ budget.totalMs
13584
+ );
13585
+ totalTimer?.unref?.();
13586
+ }
13587
+ return {
13588
+ signal: controller.signal,
13589
+ /** Reset the idle window — call on EVERY received stream part. */
13590
+ kick: armIdle,
13591
+ /** Clear both timers once the stream lifecycle is over. */
13592
+ dispose: () => {
13593
+ if (idleTimer) clearTimeout(idleTimer);
13594
+ if (totalTimer) clearTimeout(totalTimer);
13595
+ idleTimer = null;
13596
+ totalTimer = null;
13597
+ }
13598
+ };
13599
+ }
13528
13600
  function safeParseJson(rawText) {
13529
13601
  if (!rawText) return null;
13530
13602
  const cleaned = rawText.replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
@@ -13549,7 +13621,7 @@ function safeParseJson(rawText) {
13549
13621
  return null;
13550
13622
  }
13551
13623
  }
13552
- async function streamLLMWithFallback(systemPrompt, userPrompt, apiKeys = {}, onChunk, preferredModel, preferredProvider) {
13624
+ async function streamLLMWithFallback(systemPrompt, userPrompt, apiKeys = {}, onChunk, preferredModel, preferredProvider, budget) {
13553
13625
  const alibabaKey = apiKeys.alibaba || apiKeys.dashscope || process.env.ALIBABA_API_KEY || process.env.DASHSCOPE_API_KEY;
13554
13626
  const nvidiaKey = apiKeys.nvidia || process.env.NVIDIA_API_KEY;
13555
13627
  const deepseekKey = apiKeys.deepseek || process.env.DEEPSEEK_API_KEY;
@@ -13627,8 +13699,13 @@ async function streamLLMWithFallback(systemPrompt, userPrompt, apiKeys = {}, onC
13627
13699
  const systemInstructions = `${systemPrompt}
13628
13700
 
13629
13701
  THINKING DIRECTIVE: Keep reasoning concise, structured, and focused strictly on the core pedagogical trade-offs in 4-6 concise bullet points (under 120 words). Then output the JSON immediately.`;
13702
+ const resolvedBudget = {
13703
+ idleMs: budget?.idleMs ?? DEFAULT_STREAM_IDLE_MS,
13704
+ totalMs: budget?.totalMs ?? DEFAULT_STREAM_TOTAL_MS
13705
+ };
13630
13706
  for (const candidate of candidates) {
13631
13707
  const t0 = Date.now();
13708
+ const abort = createStreamAbortSignal(resolvedBudget);
13632
13709
  try {
13633
13710
  const modelInstance = getAIModel({
13634
13711
  provider: candidate.provider,
@@ -13641,26 +13718,16 @@ THINKING DIRECTIVE: Keep reasoning concise, structured, and focused strictly on
13641
13718
  prompt: userPrompt,
13642
13719
  temperature: 0.2,
13643
13720
  includeRawChunks: true,
13644
- abortSignal: AbortSignal.timeout(6e4)
13721
+ abortSignal: abort.signal
13645
13722
  });
13646
13723
  let fullContent = "";
13647
13724
  for await (const part of streamResult.fullStream) {
13648
- if (part.type === "reasoning-delta") {
13649
- const thoughtText = part.text ?? part.delta ?? "";
13650
- if (thoughtText) onChunk?.(thoughtText, "thought");
13651
- } else if (part.type === "raw") {
13652
- const raw = part.rawValue;
13653
- const delta = raw?.choices?.[0]?.delta;
13654
- const reasoning = delta?.reasoning_content || delta?.reasoning;
13655
- if (reasoning) {
13656
- onChunk?.(reasoning, "thought");
13657
- }
13658
- } else if (part.type === "text-delta") {
13659
- const textDelta = part.text ?? part.delta ?? "";
13660
- if (textDelta) {
13661
- fullContent += textDelta;
13662
- onChunk?.(textDelta, "content");
13663
- }
13725
+ abort.kick();
13726
+ const extracted = extractStreamChunk(part);
13727
+ if (extracted.thought) onChunk?.(extracted.thought, "thought");
13728
+ if (extracted.content) {
13729
+ fullContent += extracted.content;
13730
+ onChunk?.(extracted.content, "content");
13664
13731
  }
13665
13732
  }
13666
13733
  if (fullContent.trim()) {
@@ -13669,6 +13736,8 @@ THINKING DIRECTIVE: Keep reasoning concise, structured, and focused strictly on
13669
13736
  }
13670
13737
  } catch (e) {
13671
13738
  console.warn(`[curriculum-kit:VercelAI] Candidate ${candidate.provider} (${candidate.model}) failed in ${Date.now() - t0}ms:`, e?.message || e);
13739
+ } finally {
13740
+ abort.dispose();
13672
13741
  }
13673
13742
  }
13674
13743
  return null;
@@ -13709,7 +13778,11 @@ Return concise JSON matching:
13709
13778
  (chunk, type) => {
13710
13779
  if (type === "content") rawContent += chunk;
13711
13780
  onChunk?.(chunk, type);
13712
- }
13781
+ },
13782
+ options.model,
13783
+ options.provider,
13784
+ // Research may run live web grounding — generous budget, still idle-guarded.
13785
+ resolveStreamBudget(void 0, { idleMs: options.idleTimeoutMs, totalMs: options.timeoutMs })
13713
13786
  );
13714
13787
  let parsedResearch = {
13715
13788
  groundTruthSummary: `Verified ecosystem standards for ${projectName}`,
@@ -13723,6 +13796,68 @@ Return concise JSON matching:
13723
13796
  }
13724
13797
  return parsedResearch;
13725
13798
  }
13799
+ var CONTEXT_KEY_LABELS = {
13800
+ domain: "Domain",
13801
+ customDomain: "Custom Domain",
13802
+ context: "Educational Context (Tier)",
13803
+ contextLabel: "Educational Context",
13804
+ customContext: "Custom Educational Context",
13805
+ targetAgeTier: "Target Age Tier",
13806
+ targetAge: "Target Age & Audience",
13807
+ customAge: "Custom Age Group",
13808
+ courseDurationTier: "Course Duration Tier",
13809
+ courseDuration: "Planned Course Duration",
13810
+ customDuration: "Custom Course Duration",
13811
+ hardwareReadinessTier: "Hardware Readiness Tier",
13812
+ hardwareReadiness: "Hardware & Tool Readiness",
13813
+ customHardware: "Custom Hardware Setup",
13814
+ language: "Artifact Output Language",
13815
+ totalWeeks: "Total Weeks",
13816
+ sessionsPerWeek: "Sessions Per Week",
13817
+ sessionDurationMinutes: "Session Duration (minutes)",
13818
+ totalSessions: "Total Sessions",
13819
+ isConsecutiveSessions: "Consecutive (Block) Sessions",
13820
+ projectName: "Course Title",
13821
+ projectCode: "Project Identifier",
13822
+ courseDescription: "Idea Description"
13823
+ };
13824
+ var STRUCTURED_PROMPT_KEYS = /* @__PURE__ */ new Set([
13825
+ "projectName",
13826
+ "projectCode",
13827
+ "courseDescription",
13828
+ "language",
13829
+ "primaryGoal",
13830
+ "exitVision",
13831
+ "cognitiveDepth",
13832
+ "valueAndCertification",
13833
+ "entryBridge",
13834
+ "pedagogy",
13835
+ "classDynamic",
13836
+ "hardwareDeployment",
13837
+ "teacherRole",
13838
+ "lessonPacingModel",
13839
+ "voiceAndTone"
13840
+ ]);
13841
+ function buildRemainingContextBlock(accumulatedData) {
13842
+ const lines = [];
13843
+ for (const [key, rawVal] of Object.entries(accumulatedData)) {
13844
+ if (STRUCTURED_PROMPT_KEYS.has(key)) continue;
13845
+ if (rawVal === void 0 || rawVal === null || rawVal === "") continue;
13846
+ if (key.endsWith("_ids") || key.endsWith("_id")) continue;
13847
+ const label = CONTEXT_KEY_LABELS[key] || key.replace(/([A-Z])/g, " $1").replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase()).trim();
13848
+ let valueStr;
13849
+ if (Array.isArray(rawVal)) {
13850
+ valueStr = rawVal.map((v) => typeof v === "object" ? JSON.stringify(v) : String(v)).join("; ");
13851
+ } else if (typeof rawVal === "object") {
13852
+ valueStr = JSON.stringify(rawVal);
13853
+ } else {
13854
+ valueStr = String(rawVal);
13855
+ }
13856
+ if (!valueStr.trim()) continue;
13857
+ lines.push(`- ${label}: ${valueStr}`);
13858
+ }
13859
+ return lines.join("\n");
13860
+ }
13726
13861
  async function streamLayerPrefillWithFallback(options, onChunk) {
13727
13862
  const { action = "prefill", targetLayer = 1, accumulatedData = {}, apiKeys = {} } = options;
13728
13863
  if (action === "research") {
@@ -13733,26 +13868,21 @@ async function streamLayerPrefillWithFallback(options, onChunk) {
13733
13868
  projectName = "Curriculum Course",
13734
13869
  projectCode = "curriculum-course",
13735
13870
  courseDescription = "",
13736
- domain = "tech",
13737
- context = "center",
13738
- contextLabel = "STEM Academy",
13739
13871
  targetAgeTier = "secondary",
13740
13872
  targetAge = "Secondary (Ages 11-15 / Grades 6-9)",
13741
13873
  courseDurationTier = "standard",
13742
13874
  courseDuration = "Standard Course (16-24 sessions / 24-36 hours)",
13743
- hardwareReadinessTier = "none_budget",
13744
- hardwareReadiness = "No dedicated hardware (Plug-and-play simulator or economical kit)",
13745
13875
  primaryGoal = "",
13746
13876
  exitVision = "",
13747
13877
  valueAndCertification = "",
13748
- coreLearningOutcomes = "",
13878
+ entryBridge = "",
13749
13879
  pedagogy = "5E Instructional Model",
13750
- capstoneTheme = "",
13751
- teacherProfile = "Facilitator / STEM Teacher",
13752
- instructionalModality = "Teacher-led step-by-step",
13753
- cognitiveDepth = "Apply",
13754
- budgetOrExistingSpecs = "",
13755
- cognitiveScaffolding = ""
13880
+ lessonPacingModel = "",
13881
+ voiceAndTone = "",
13882
+ classDynamic = "",
13883
+ hardwareDeployment = "",
13884
+ teacherRole = "",
13885
+ cognitiveDepth = "Apply"
13756
13886
  } = accumulatedData;
13757
13887
  const language = typeof options.language === "string" && options.language ? options.language : typeof accumulatedData.language === "string" && accumulatedData.language ? accumulatedData.language : "vi";
13758
13888
  const textLangMandate = language === "vi" ? "natural, fluent Vietnamese (Ti\u1EBFng Vi\u1EC7t)" : language === "en" ? "natural, fluent English" : `natural, fluent ${language}`;
@@ -13788,7 +13918,7 @@ ${targetLayer === 1 ? `
13788
13918
  - Field 2 (id: 'brandingAndTone', type: 'single_choice'): Art Direction & Visual Mood (ART_DIRECTION: Future Maker Lab high-contrast vs Clean Minimalist Studio vs Gamified Playful).
13789
13919
  - Field 3 (id: 'differentiationStrategy', type: 'single_choice'): Differentiation & Scaffolding Strategy (CONTENT_STYLE_GUIDE: Tier 1/2/3 Bronze/Silver/Gold + EXT Challenges vs Open-ended Rubric Studio).
13790
13920
  - Field 4 (id: 'cognitiveScaffolding', type: 'textarea'): Concrete scaffolding methods (Starter Code, color-coded breadboards, 4-step debug poster, simulator preview).
13791
- - Field 5 (id: 'recommendedToolchain', type: 'single_choice'): 3 concrete toolchain/IDE options matching '${hardwareReadiness}'.
13921
+ - Field 5 (id: 'recommendedToolchain', type: 'single_choice'): 3 concrete toolchain/IDE options matching the confirmed Hardware & Tool Readiness from FULL ACCUMULATED CONTEXT.
13792
13922
  - Field 6 (id: 'artifactScope', type: 'multi_choice'): Deliverable artifacts (LESSON mandatory + ACT, QUIZ, SLIDE, WKS, HANDOUT, GUIDE, CODE).
13793
13923
  - Field 7 (id: 'assessmentStrategy', type: 'single_choice'): Grading weights (e.g. 40% Formative Process + 60% Capstone Rubric).
13794
13924
  `}
@@ -13818,10 +13948,8 @@ RETURN STRICT VALID JSON ONLY adhering exactly to this format:
13818
13948
  - Course Title: "${projectName}"
13819
13949
  - Project Identifier: "${projectCode}"
13820
13950
  - Idea Description: "${courseDescription}"
13821
- - Target Age & Audience: ${targetAge} (Tier: ${targetAgeTier})
13822
- - Planned Course Duration: ${courseDuration} (Tier: ${courseDurationTier})
13823
- - Hardware & Tool Readiness: ${hardwareReadiness} (Tier: ${hardwareReadinessTier})
13824
- - Educational Context: ${contextLabel || context} | Domain: ${domain}
13951
+ - Target Age & Audience: ${targetAge}
13952
+ - Planned Course Duration: ${courseDuration}
13825
13953
 
13826
13954
  ${targetLayer >= 2 ? `
13827
13955
  APPROVED PARAMETERS FROM LAYER 1 (STRATEGIC INTENT & OUTCOMES):
@@ -13829,17 +13957,23 @@ APPROVED PARAMETERS FROM LAYER 1 (STRATEGIC INTENT & OUTCOMES):
13829
13957
  - Competency Exit Vision: "${exitVision}"
13830
13958
  - Target Cognitive Depth: "${cognitiveDepth}"
13831
13959
  - Value & Recognition: "${valueAndCertification}"
13960
+ - Prerequisite Entry Bridge: "${entryBridge}"
13832
13961
  ` : ""}
13833
13962
 
13834
13963
  ${targetLayer >= 3 ? `
13835
13964
  APPROVED PARAMETERS FROM LAYER 2 (PEDAGOGY & LOGISTICS):
13836
- - Selected Pedagogy Model: ${pedagogy}
13837
- - Class Dynamics / Hardware Ratio: ${accumulatedData.classDynamic || "1:1 Individual"}
13838
- - Lab Deployment Mode: ${accumulatedData.hardwareDeployment || "Lab provided"}
13839
- - Instructional Role: ${accumulatedData.teacherRole || "Teacher-led"}
13965
+ - Lesson Pacing Model: "${lessonPacingModel}"
13966
+ - Selected Pedagogy Model: "${pedagogy}"
13967
+ - Voice & Tone: "${voiceAndTone}"
13968
+ - Class Dynamics / Hardware Ratio: "${classDynamic}"
13969
+ - Lab Deployment Mode: "${hardwareDeployment}"
13970
+ - Instructional Role: "${teacherRole}"
13840
13971
  ` : ""}
13841
13972
 
13842
- REQUIREMENT: Synthesize all parameters above and produce the high-fidelity pre-fill configuration for LAYER ${targetLayer}.`;
13973
+ FULL ACCUMULATED CONTEXT (ALL CONFIRMED PARAMETERS FROM EVERY PREVIOUS STEP \u2014 user decisions, organization/instructor/template profiles, schedules and constraints; every line below MUST be honored in your output):
13974
+ ${buildRemainingContextBlock(accumulatedData) || "(no additional context)"}
13975
+
13976
+ REQUIREMENT: Synthesize ALL parameters above \u2014 every single line in FULL ACCUMULATED CONTEXT is a confirmed user decision or system constraint and MUST influence the generated configuration. Produce the high-fidelity pre-fill configuration for LAYER ${targetLayer}.`;
13843
13977
  let rawContent = "";
13844
13978
  await streamLLMWithFallback(
13845
13979
  systemPrompt,
@@ -13850,7 +13984,10 @@ REQUIREMENT: Synthesize all parameters above and produce the high-fidelity pre-f
13850
13984
  onChunk?.(chunk, type);
13851
13985
  },
13852
13986
  options.model,
13853
- options.provider
13987
+ options.provider,
13988
+ // RC-W1: layer-aware budget — Layer 2 has the largest output and free-tier
13989
+ // models may think/stream for minutes. Idle window still catches hangs.
13990
+ resolveStreamBudget(targetLayer, { idleMs: options.idleTimeoutMs, totalMs: options.timeoutMs })
13854
13991
  );
13855
13992
  const parsed = safeParseJson(rawContent);
13856
13993
  return {
@@ -17129,6 +17266,8 @@ exports.CurriculumPlanSchema = CurriculumPlanSchema;
17129
17266
  exports.CurriculumQualityReportSchema = CurriculumQualityReportSchema;
17130
17267
  exports.DEFAULT_ENABLED_PROVIDERS = DEFAULT_ENABLED_PROVIDERS;
17131
17268
  exports.DEFAULT_GATE_SETTINGS = DEFAULT_GATE_SETTINGS;
17269
+ exports.DEFAULT_STREAM_IDLE_MS = DEFAULT_STREAM_IDLE_MS;
17270
+ exports.DEFAULT_STREAM_TOTAL_MS = DEFAULT_STREAM_TOTAL_MS;
17132
17271
  exports.DependencyEdgeSchema = DependencyEdgeSchema;
17133
17272
  exports.DepthAssignmentSchema = DepthAssignmentSchema;
17134
17273
  exports.DepthLevelSchema = DepthLevelSchema;
@@ -17173,6 +17312,7 @@ exports.HandoutSectionSchema = HandoutSectionSchema;
17173
17312
  exports.InstructionSectionSchema = InstructionSectionSchema;
17174
17313
  exports.InstructionStepSchema = InstructionStepSchema;
17175
17314
  exports.JudgeCriterionSchema = JudgeCriterionSchema;
17315
+ exports.LAYER_TOTAL_BUDGET_MS = LAYER_TOTAL_BUDGET_MS;
17176
17316
  exports.LESSON_PLAN_TEMPLATE = LESSON_PLAN_TEMPLATE;
17177
17317
  exports.LLMJudgeEngine = LLMJudgeEngine;
17178
17318
  exports.LabTierTaskSchema = LabTierTaskSchema;
@@ -17302,6 +17442,7 @@ exports.contentTools = contentTools;
17302
17442
  exports.convertRoadmapToFoundationSot = convertRoadmapToFoundationSot;
17303
17443
  exports.createAiInferenceError = createAiInferenceError;
17304
17444
  exports.createCurriculumStorage = createCurriculumStorage;
17445
+ exports.createStreamAbortSignal = createStreamAbortSignal;
17305
17446
  exports.curateMediaLedger = curateMediaLedger;
17306
17447
  exports.designerTools = designerTools;
17307
17448
  exports.detectProjectPedagogy = detectProjectPedagogy;
@@ -17314,6 +17455,7 @@ exports.exportAllProjectQuizzes = exportAllProjectQuizzes;
17314
17455
  exports.expositionCacheKey = expositionCacheKey;
17315
17456
  exports.extractScopeSequenceRows = extractScopeSequenceRows;
17316
17457
  exports.extractSessionSlice = extractSessionSlice;
17458
+ exports.extractStreamChunk = extractStreamChunk;
17317
17459
  exports.extractThoughtAndContent = extractThoughtAndContent;
17318
17460
  exports.formatQuizzesToCsv = formatQuizzesToCsv;
17319
17461
  exports.fulfillMediaLedger = fulfillMediaLedger;
@@ -17396,6 +17538,7 @@ exports.repairMasterLessonStep = repairMasterLessonStep;
17396
17538
  exports.researcherTools = researcherTools;
17397
17539
  exports.resolveGateSettings = resolveGateSettings;
17398
17540
  exports.resolveStandardsPacks = resolveStandardsPacks;
17541
+ exports.resolveStreamBudget = resolveStreamBudget;
17399
17542
  exports.resolveTranslationTargets = resolveTranslationTargets;
17400
17543
  exports.reviewerTools = reviewerTools;
17401
17544
  exports.runCurriculumAIInference = runCurriculumAIInference;