@thanh01.pmt/curriculum-kit 1.0.6 → 1.0.8

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
@@ -14,35 +14,16 @@ var supabaseJs = require('@supabase/supabase-js');
14
14
  var url = require('url');
15
15
  var jsonrepair = require('jsonrepair');
16
16
  var rest = require('@octokit/rest');
17
- var workflowPkg = require('workflow');
17
+ var workflow = require('workflow');
18
18
 
19
19
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
20
20
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
21
21
 
22
- function _interopNamespace(e) {
23
- if (e && e.__esModule) return e;
24
- var n = Object.create(null);
25
- if (e) {
26
- Object.keys(e).forEach(function (k) {
27
- if (k !== 'default') {
28
- var d = Object.getOwnPropertyDescriptor(e, k);
29
- Object.defineProperty(n, k, d.get ? d : {
30
- enumerable: true,
31
- get: function () { return e[k]; }
32
- });
33
- }
34
- });
35
- }
36
- n.default = e;
37
- return Object.freeze(n);
38
- }
39
-
40
22
  var fs2__default = /*#__PURE__*/_interopDefault(fs2);
41
23
  var path3__default = /*#__PURE__*/_interopDefault(path3);
42
24
  var pLimit__default = /*#__PURE__*/_interopDefault(pLimit);
43
25
  var fsPromises__default = /*#__PURE__*/_interopDefault(fsPromises);
44
26
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
45
- var workflowPkg__namespace = /*#__PURE__*/_interopNamespace(workflowPkg);
46
27
 
47
28
  var __defProp = Object.defineProperty;
48
29
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -9576,14 +9557,14 @@ init_errors();
9576
9557
 
9577
9558
  // src/workflow/gateSettings.ts
9578
9559
  var DEFAULT_GATE_SETTINGS = Object.freeze({
9579
- LEARNER_PROFILE: "HITL",
9580
- PROJECT_BRIEF: "HITL",
9581
- REFERENCE_PACK: "HITL",
9582
- CURRICULUM_FRAMEWORK: "HITL",
9583
- KNOWLEDGE_EXPOSITION: "LLM_JUDGE",
9584
- STYLE_GUIDE: "HITL",
9585
- ART_DIRECTION: "HITL",
9586
- LESSON: "LLM_JUDGE",
9560
+ LEARNER_PROFILE: "OFF",
9561
+ PROJECT_BRIEF: "OFF",
9562
+ REFERENCE_PACK: "OFF",
9563
+ CURRICULUM_FRAMEWORK: "OFF",
9564
+ KNOWLEDGE_EXPOSITION: "OFF",
9565
+ STYLE_GUIDE: "OFF",
9566
+ ART_DIRECTION: "OFF",
9567
+ LESSON: "OFF",
9587
9568
  ACT: "OFF",
9588
9569
  QUIZ: "OFF",
9589
9570
  SLIDE: "OFF",
@@ -9591,7 +9572,7 @@ var DEFAULT_GATE_SETTINGS = Object.freeze({
9591
9572
  WKS: "OFF",
9592
9573
  HANDOUT: "OFF",
9593
9574
  EXT: "OFF",
9594
- CODE: "LLM_JUDGE",
9575
+ CODE: "OFF",
9595
9576
  MEDIA_SCRIPT: "OFF",
9596
9577
  EXIT_TICKET: "OFF"
9597
9578
  });
@@ -11480,9 +11461,10 @@ Fix ALL issues above and output the complete corrected document.` }],
11480
11461
  const taskState = pipelineState.tasks[lessonCode] || {};
11481
11462
  const currentLessonInfo = taskState.LESSON;
11482
11463
  const lessonGate = gateModeFor(gates, "LESSON");
11483
- let isLessonApproved = lessonGate !== "HITL" || // OFF/LLM_JUDGE: không cần duyệt người
11484
- currentLessonInfo?.state === "approved" || currentLessonInfo?.review?.decision === "APPROVED" || approvedArtifacts.includes("LESSON") || approvedArtifacts.includes(lessonCode);
11485
- if (lessonGate === "LLM_JUDGE" && !isLessonApproved && lessonContent) {
11464
+ const humanApproved = currentLessonInfo?.state === "approved" || currentLessonInfo?.review?.decision === "APPROVED" || approvedArtifacts.includes("LESSON") || approvedArtifacts.includes(lessonCode);
11465
+ const needsJudge = lessonGate === "LLM_JUDGE" && !humanApproved;
11466
+ let isLessonApproved = lessonGate === "OFF" || humanApproved;
11467
+ if (needsJudge && lessonContent) {
11486
11468
  onProgress?.("@reviewer", `\u{1F916} Executing LLM-as-Judge academic evaluation on LESSON_${lessonCode}.md...`);
11487
11469
  try {
11488
11470
  let injectYamlReviewMetadata2 = function(content, meta) {
@@ -12521,6 +12503,8 @@ async function generateSingleArtifact(req) {
12521
12503
  extraContext,
12522
12504
  config,
12523
12505
  contextSot = {},
12506
+ mediaPolicy,
12507
+ researchContext,
12524
12508
  options = {},
12525
12509
  onChunk
12526
12510
  } = req;
@@ -12558,7 +12542,10 @@ ${langRule}
12558
12542
  3. PEDAGOGICAL GROUNDING: Strictly align with the ${pedagogy.toUpperCase()} model, Bloom's Revised Taxonomy, and IEEE/ACM CS2023 guidelines.
12559
12543
  4. SCAFFOLDING LEVEL: "${scaffoldingLevel.toUpperCase()}".${scaffoldingRule}
12560
12544
  5. TARGET AUDIENCE: ${gradeLevel ? `Grade ${gradeLevel}, ` : ""}${targetAge} on ${hwStr}.${deviceRule}${classDynamicsRule}${extraContextRule}
12561
- 6. OUTPUT INTEGRITY: Output clean Markdown starting directly with YAML frontmatter. Do not wrap entire response in triple backticks.${artifactSpecializedPrompt}`;
12545
+ 6. OUTPUT INTEGRITY: Output clean Markdown starting directly with YAML frontmatter. Do not wrap entire response in triple backticks.${artifactSpecializedPrompt}${mediaPolicy ? `
12546
+
12547
+ IMAGE PLANNING (media ledger) \u2014 QUOTA GEN: t\u1ED1i \u0111a ${mediaPolicy.maxGenImagesPerArtifact} \u1EA3nh AI cho artifact n\xE0y (\u1EA3nh search kho kh\xF4ng gi\u1EDBi h\u1EA1n).
12548
+ Khi n\u1ED9i dung c\u1EA7n minh h\u1ECDa (diagram quy tr\xECnh, s\u01A1 \u0111\u1ED3 kh\xE1i ni\u1EC7m, step-by-step visual), ch\xE8n placeholder \u0111\xFAng format [IMAGE: slug] (slug ch\u1EEF th\u01B0\u1EDDng-g\u1EA1ch ngang, unique trong b\xE0i, vd img-for-loop-diagram) T\u1EA0I \u0110\xDANG CH\u1ED6 c\u1EA7n \u1EA3nh, tr\xEAn d\xF2ng ri\xEAng. KH\xD4NG t\u1EF1 sinh \u1EA3nh, KH\xD4NG d\xF9ng markdown image \u2014 placeholder s\u1EBD \u0111\u01B0\u1EE3c thay b\u1EB1ng \u1EA3nh th\u1EADt sau khi Media Curator duy\u1EC7t. Ch\u1EC9 \u0111\u1EB7t cho \u1EA3nh TH\u1EF0C S\u1EF0 c\u1EA7n thi\u1EBFt.` : ""}`;
12562
12549
  const userPrompt = `### CONTEXT GROUND TRUTH:
12563
12550
  - Topic / Concept Domain: "${topic}"
12564
12551
  - Lesson ID: "${lessonId || "ON_DEMAND"}"
@@ -12573,6 +12560,9 @@ ${selectedDevices ? `- Device Mode: "${selectedDevices}"
12573
12560
  ` : ""}${extraContext ? `- User Specific Guidance & Requirements: "${extraContext}"
12574
12561
  ` : ""}${config ? `- Full Task Config: ${JSON.stringify(config)}
12575
12562
  ` : ""}
12563
+ ${researchContext ? `### RESEARCH GROUND TRUTH (fact-checked context \u2014 \u01B0u ti\xEAn d\xF9ng, KH\xD4NG b\u1ECBa s\u1ED1 li\u1EC7u):
12564
+ ${researchContext.slice(0, 4e3)}
12565
+ ` : ""}
12576
12566
  ${contextSot.framework ? `### SOT CURRICULUM FRAMEWORK:
12577
12567
  ${contextSot.framework.slice(0, 1500)}
12578
12568
  ` : ""}
@@ -12613,13 +12603,171 @@ Generate the complete, thorough, publication-grade "${artifactType.toUpperCase()
12613
12603
  }
12614
12604
  );
12615
12605
  const { thought, content } = extractThoughtAndContent(rawResult);
12616
- return {
12617
- artifactType,
12618
- filename: meta.filename,
12619
- content: content || accumulatedContent,
12620
- persona: meta.persona,
12621
- thought: thought || accumulatedThought
12606
+ const finalContent = content || accumulatedContent;
12607
+ const finalThought = thought || accumulatedThought;
12608
+ const gates = resolveGateSettings(req.gateSettings);
12609
+ const kitGateType = {
12610
+ lesson: "LESSON",
12611
+ slide: "SLIDE",
12612
+ act: "ACT",
12613
+ quiz: "QUIZ",
12614
+ guide: "GUIDE",
12615
+ handout: "HANDOUT",
12616
+ worksheet: "WKS",
12617
+ ext: "EXT",
12618
+ codelab: "CODE",
12619
+ exit_ticket: "EXIT_TICKET"
12620
+ }[artifactType];
12621
+ const gateMode = kitGateType ? gateModeFor(gates, kitGateType) : "OFF";
12622
+ if (gateMode === "OFF") {
12623
+ return { artifactType, filename: meta.filename, content: finalContent, persona: meta.persona, thought: finalThought };
12624
+ }
12625
+ const injectReviewMeta = (doc, metaMap) => {
12626
+ const fm = doc.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
12627
+ if (!fm) {
12628
+ return `---
12629
+ ${Object.entries(metaMap).map(([k, v]) => `${k}: "${v}"`).join("\n")}
12630
+ ---
12631
+
12632
+ ${doc}`;
12633
+ }
12634
+ let yaml = fm[1] ?? "";
12635
+ for (const [k, v] of Object.entries(metaMap)) {
12636
+ const re = new RegExp(`^${k}:.*$`, "m");
12637
+ if (re.test(yaml)) yaml = yaml.replace(re, `${k}: "${v}"`);
12638
+ else yaml += `
12639
+ ${k}: "${v}"`;
12640
+ }
12641
+ return doc.replace(fm[0], `---
12642
+ ${yaml.trim()}
12643
+ ---
12644
+ `);
12622
12645
  };
12646
+ if (gateMode === "HITL") {
12647
+ return {
12648
+ artifactType,
12649
+ filename: meta.filename,
12650
+ content: injectReviewMeta(finalContent, { review_status: "PENDING_REVIEW", review_decision: "PENDING" }),
12651
+ persona: meta.persona,
12652
+ thought: finalThought,
12653
+ pausedForReview: true,
12654
+ gateStatus: "awaiting_approval"
12655
+ };
12656
+ }
12657
+ const judgeObjectives = [{ code: "A1", description: `Artifact fulfills its pedagogical contract for topic "${topic}" with complete required sections`, bloomLevel: "Analyze" }];
12658
+ try {
12659
+ const verdict = await LLMJudgeEngine.evaluateArtifactWithLLM({
12660
+ targetArtifactType: artifactType.toUpperCase(),
12661
+ lessonId: lessonId || "ON_DEMAND",
12662
+ expectedLanguage: language.toLowerCase().includes("viet") ? "vietnamese" : "english",
12663
+ targetObjectives: judgeObjectives,
12664
+ generatedContentJson: JSON.stringify({ content: finalContent, artifactType, lessonId: lessonId || "ON_DEMAND", title: displayTitle })
12665
+ });
12666
+ const passed = Boolean(verdict.passed) && verdict.score >= 85;
12667
+ if (passed) {
12668
+ return {
12669
+ artifactType,
12670
+ filename: meta.filename,
12671
+ content: injectReviewMeta(finalContent, {
12672
+ review_status: "APPROVED",
12673
+ reviewed_by: "@reviewer (LLM-as-Judge)",
12674
+ review_decision: "APPROVED"
12675
+ }),
12676
+ persona: meta.persona,
12677
+ thought: finalThought,
12678
+ judgeScore: verdict.score,
12679
+ judgeCritique: verdict.critique
12680
+ };
12681
+ }
12682
+ const repairFeedback = [verdict.autoRepairInstructions, ...verdict.weaknesses || []].filter(Boolean).join("\n");
12683
+ const repairedRaw = await streamCurriculumAIInference(
12684
+ [
12685
+ { role: "user", content: `You previously authored \`${meta.filename}\` and the academic Judge REJECTED it with the critique below.
12686
+ Rewrite the COMPLETE corrected document: fix every listed issue, keep the mandatory structure, frontmatter and all approved sections. Output 100% clean Markdown only.
12687
+
12688
+ JUDGE CRITIQUE:
12689
+ ${repairFeedback}
12690
+
12691
+ CURRENT DOCUMENT:
12692
+ ${finalContent}` }
12693
+ ],
12694
+ userPrompt,
12695
+ { ...options, systemPersona: systemPrompt },
12696
+ (chunk, type) => {
12697
+ if (onChunk) onChunk(chunk, type === "thought" ? "thought" : "content");
12698
+ }
12699
+ );
12700
+ const repairedExtract = extractThoughtAndContent(repairedRaw);
12701
+ const repaired = repairedExtract.content || repairedRaw;
12702
+ if (repaired && repaired.trim().length > 100) {
12703
+ const reVerdict = await LLMJudgeEngine.evaluateArtifactWithLLM({
12704
+ targetArtifactType: artifactType.toUpperCase(),
12705
+ lessonId: lessonId || "ON_DEMAND",
12706
+ expectedLanguage: language.toLowerCase().includes("viet") ? "vietnamese" : "english",
12707
+ targetObjectives: judgeObjectives,
12708
+ generatedContentJson: JSON.stringify({ content: repaired, artifactType, lessonId: lessonId || "ON_DEMAND", title: displayTitle })
12709
+ });
12710
+ if (reVerdict.passed && reVerdict.score >= 85) {
12711
+ return {
12712
+ artifactType,
12713
+ filename: meta.filename,
12714
+ content: injectReviewMeta(repaired, {
12715
+ review_status: "APPROVED",
12716
+ reviewed_by: "@reviewer (LLM-as-Judge)",
12717
+ review_decision: "APPROVED"
12718
+ }),
12719
+ persona: meta.persona,
12720
+ thought: repairedExtract.thought || "",
12721
+ judgeScore: reVerdict.score,
12722
+ judgeCritique: reVerdict.critique
12723
+ };
12724
+ }
12725
+ return {
12726
+ artifactType,
12727
+ filename: meta.filename,
12728
+ content: injectReviewMeta(repaired, {
12729
+ review_status: "NEEDS_REVISION",
12730
+ reviewed_by: "@reviewer (LLM-as-Judge)",
12731
+ review_decision: "NEEDS_REVISION"
12732
+ }),
12733
+ persona: meta.persona,
12734
+ thought: repairedExtract.thought || "",
12735
+ pausedForReview: true,
12736
+ gateStatus: "judge_needs_revision",
12737
+ judgeScore: reVerdict.score,
12738
+ judgeCritique: reVerdict.critique
12739
+ };
12740
+ }
12741
+ return {
12742
+ artifactType,
12743
+ filename: meta.filename,
12744
+ content: injectReviewMeta(finalContent, {
12745
+ review_status: "NEEDS_REVISION",
12746
+ reviewed_by: "@reviewer (LLM-as-Judge)",
12747
+ review_decision: "NEEDS_REVISION"
12748
+ }),
12749
+ persona: meta.persona,
12750
+ thought: finalThought,
12751
+ pausedForReview: true,
12752
+ gateStatus: "judge_needs_revision",
12753
+ judgeScore: verdict.score,
12754
+ judgeCritique: verdict.critique
12755
+ };
12756
+ } catch (judgeErr) {
12757
+ return {
12758
+ artifactType,
12759
+ filename: meta.filename,
12760
+ content: injectReviewMeta(finalContent, {
12761
+ review_status: "NEEDS_REVISION",
12762
+ review_decision: "NEEDS_REVISION",
12763
+ critique: `Judge unavailable (fail-closed): ${String(judgeErr?.message || judgeErr).slice(0, 300)}`
12764
+ }),
12765
+ persona: meta.persona,
12766
+ thought: finalThought,
12767
+ pausedForReview: true,
12768
+ gateStatus: "judge_error"
12769
+ };
12770
+ }
12623
12771
  } catch (error) {
12624
12772
  throw createAiInferenceError({
12625
12773
  errorCode: "ERR_AI_ALL_PROVIDERS_FAILED",
@@ -13375,43 +13523,15 @@ function safeParseJson(rawText) {
13375
13523
  return null;
13376
13524
  }
13377
13525
  }
13378
- async function readSseStream(reader, decoder, onData) {
13379
- let buffer = "";
13380
- while (true) {
13381
- const { done, value } = await reader.read();
13382
- if (done) break;
13383
- buffer += decoder.decode(value, { stream: true });
13384
- const lines = buffer.split("\n");
13385
- buffer = lines.pop() || "";
13386
- for (const line of lines) {
13387
- const trimmed = line.trim();
13388
- if (!trimmed || trimmed.startsWith(":")) continue;
13389
- if (trimmed === "data: [DONE]") return;
13390
- if (trimmed.startsWith("data: ")) {
13391
- const dataStr = trimmed.slice(6).trim();
13392
- if (dataStr) onData(dataStr);
13393
- }
13394
- }
13395
- }
13396
- }
13397
13526
  async function streamLLMWithFallback(systemPrompt, userPrompt, apiKeys = {}, onChunk) {
13398
- apiKeys.alibaba || apiKeys.dashscope || process.env.ALIBABA_API_KEY || process.env.DASHSCOPE_API_KEY;
13527
+ const alibabaKey = apiKeys.alibaba || apiKeys.dashscope || process.env.ALIBABA_API_KEY || process.env.DASHSCOPE_API_KEY;
13399
13528
  const nvidiaKey = apiKeys.nvidia || process.env.NVIDIA_API_KEY;
13400
- apiKeys.deepseek || process.env.DEEPSEEK_API_KEY;
13529
+ const deepseekKey = apiKeys.deepseek || process.env.DEEPSEEK_API_KEY;
13401
13530
  const openrouterKey = apiKeys.openrouter || process.env.OPENROUTER_API_KEY;
13402
- apiKeys.gemini || process.env.GEMINI_API_KEY;
13531
+ const geminiKey = apiKeys.gemini || process.env.GEMINI_API_KEY;
13403
13532
  const openrouterPreset = process.env.OPENROUTER_FREE_PRESET || "@preset/coding-free";
13404
- const messages = [
13405
- {
13406
- role: "system",
13407
- content: `${systemPrompt}
13408
-
13409
- 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.`
13410
- },
13411
- { role: "user", content: userPrompt }
13412
- ];
13533
+ const candidates = [];
13413
13534
  if (openrouterKey && isProviderEnabled("openrouter")) {
13414
- const t0 = Date.now();
13415
13535
  const orModels = [
13416
13536
  openrouterPreset,
13417
13537
  "openrouter/free",
@@ -13421,174 +13541,82 @@ THINKING DIRECTIVE: Keep reasoning concise, structured, and focused strictly on
13421
13541
  "minimax/minimax-m3:free",
13422
13542
  "cohere/north-mini-code:free"
13423
13543
  ].filter((m) => isModelAllowed(m));
13424
- const uniqueOrModels = Array.from(new Set(orModels));
13425
- for (const model of uniqueOrModels) {
13426
- try {
13427
- const controller = new AbortController();
13428
- const timeout = setTimeout(() => controller.abort(), 25e3);
13429
- const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
13430
- method: "POST",
13431
- headers: {
13432
- "Content-Type": "application/json",
13433
- "Authorization": `Bearer ${openrouterKey}`
13434
- },
13435
- body: JSON.stringify({
13436
- model,
13437
- messages,
13438
- stream: true,
13439
- temperature: 0.3
13440
- }),
13441
- signal: controller.signal
13442
- });
13443
- clearTimeout(timeout);
13444
- if (res.ok && res.body) {
13445
- const reader = res.body.getReader();
13446
- const decoder = new TextDecoder();
13447
- let fullContent = "";
13448
- await readSseStream(reader, decoder, (dataStr) => {
13449
- try {
13450
- const parsed = JSON.parse(dataStr);
13451
- const delta = parsed.choices?.[0]?.delta;
13452
- if (delta?.content) {
13453
- fullContent += delta.content;
13454
- onChunk?.(delta.content, "content");
13455
- }
13456
- } catch {
13457
- }
13458
- });
13459
- if (fullContent.trim()) {
13460
- console.log(`[curriculum-kit:LayerPrefill] \u2705 Tier 1 OpenRouter (${model}) streamed in ${Date.now() - t0}ms`);
13461
- return fullContent.trim();
13462
- }
13463
- }
13464
- } catch (e) {
13465
- console.warn(`[curriculum-kit:LayerPrefill] Tier 1 OpenRouter stream failed for ${model} in ${Date.now() - t0}ms:`, e?.message || e);
13466
- }
13544
+ for (const m of Array.from(new Set(orModels))) {
13545
+ candidates.push({ provider: "openrouter", model: m, apiKey: openrouterKey });
13467
13546
  }
13468
13547
  }
13469
13548
  if (nvidiaKey && isProviderEnabled("nvidia")) {
13470
- const t0 = Date.now();
13471
13549
  const nemotronModels = [
13472
13550
  "nvidia/nemotron-3-ultra-550b-a55b",
13473
13551
  "nvidia/nemotron-3.5-lightning-30b-a3b",
13474
13552
  "nvidia/nemotron-3-super-120b-a12b",
13475
13553
  "nvidia/llama-3.1-nemotron-70b-instruct"
13476
13554
  ].filter((m) => isModelAllowed(m));
13477
- for (const model of nemotronModels) {
13478
- try {
13479
- const controller = new AbortController();
13480
- const timeout = setTimeout(() => controller.abort(), 25e3);
13481
- const res = await fetch("https://integrate.api.nvidia.com/v1/chat/completions", {
13482
- method: "POST",
13483
- headers: {
13484
- "Content-Type": "application/json",
13485
- "Authorization": `Bearer ${nvidiaKey}`
13486
- },
13487
- body: JSON.stringify({
13488
- model,
13489
- messages,
13490
- stream: true,
13491
- temperature: 0.2
13492
- }),
13493
- signal: controller.signal
13494
- });
13495
- clearTimeout(timeout);
13496
- if (res.ok && res.body) {
13497
- const reader = res.body.getReader();
13498
- const decoder = new TextDecoder();
13499
- let fullContent = "";
13500
- await readSseStream(reader, decoder, (dataStr) => {
13501
- try {
13502
- const parsed = JSON.parse(dataStr);
13503
- const delta = parsed.choices?.[0]?.delta;
13504
- if (delta?.content) {
13505
- fullContent += delta.content;
13506
- onChunk?.(delta.content, "content");
13507
- }
13508
- } catch {
13509
- }
13510
- });
13511
- if (fullContent.trim()) {
13512
- console.log(`[curriculum-kit:LayerPrefill] \u2705 Tier 2 NVIDIA NIM (${model}) streamed in ${Date.now() - t0}ms`);
13513
- return fullContent.trim();
13514
- }
13515
- }
13516
- } catch (e) {
13517
- console.warn(`[curriculum-kit:LayerPrefill] Tier 2 NVIDIA NIM stream failed for ${model} in ${Date.now() - t0}ms:`, e?.message || e);
13518
- }
13555
+ for (const m of nemotronModels) {
13556
+ candidates.push({ provider: "nvidia", model: m, apiKey: nvidiaKey });
13557
+ }
13558
+ }
13559
+ if (alibabaKey && isProviderEnabled("alibaba")) {
13560
+ const aliModels = ["qwen-plus", "qwen-turbo", "qwen3.7-plus"].filter((m) => isModelAllowed(m));
13561
+ for (const m of aliModels) {
13562
+ candidates.push({ provider: "alibaba", model: m, apiKey: alibabaKey });
13563
+ }
13564
+ }
13565
+ if (deepseekKey && isProviderEnabled("deepseek")) {
13566
+ const dsModels = ["deepseek-chat", "deepseek-coder"].filter((m) => isModelAllowed(m));
13567
+ for (const m of dsModels) {
13568
+ candidates.push({ provider: "deepseek", model: m, apiKey: deepseekKey });
13569
+ }
13570
+ }
13571
+ if (geminiKey && (isProviderEnabled("gemini") || isProviderEnabled("google"))) {
13572
+ const gModels = ["gemini-1.5-flash", "gemini-2.0-flash", "gemini-1.5-pro"].filter((m) => isModelAllowed(m));
13573
+ for (const m of gModels) {
13574
+ candidates.push({ provider: "google", model: m, apiKey: geminiKey });
13519
13575
  }
13520
13576
  }
13521
13577
  const fallbackChain = getDesignatedFallbackChain();
13522
13578
  for (const target of fallbackChain) {
13523
- const { provider, model } = target;
13524
- if (provider === "nvidia" && nvidiaKey && isProviderEnabled("nvidia")) {
13525
- try {
13526
- const res = await fetch("https://integrate.api.nvidia.com/v1/chat/completions", {
13527
- method: "POST",
13528
- headers: {
13529
- "Content-Type": "application/json",
13530
- "Authorization": `Bearer ${nvidiaKey}`
13531
- },
13532
- body: JSON.stringify({
13533
- model,
13534
- messages,
13535
- stream: true,
13536
- temperature: 0.2
13537
- })
13538
- });
13539
- if (res.ok && res.body) {
13540
- const reader = res.body.getReader();
13541
- const decoder = new TextDecoder();
13542
- let fullContent = "";
13543
- await readSseStream(reader, decoder, (dataStr) => {
13544
- try {
13545
- const parsed = JSON.parse(dataStr);
13546
- const delta = parsed.choices?.[0]?.delta;
13547
- if (delta?.content) {
13548
- fullContent += delta.content;
13549
- onChunk?.(delta.content, "content");
13550
- }
13551
- } catch {
13552
- }
13553
- });
13554
- if (fullContent.trim()) return fullContent.trim();
13579
+ if (!candidates.some((c) => c.provider === target.provider && c.model === target.model)) {
13580
+ candidates.push({
13581
+ provider: target.provider,
13582
+ model: target.model,
13583
+ apiKey: apiKeys[target.provider]
13584
+ });
13585
+ }
13586
+ }
13587
+ const systemInstructions = `${systemPrompt}
13588
+
13589
+ 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.`;
13590
+ for (const candidate of candidates) {
13591
+ const t0 = Date.now();
13592
+ try {
13593
+ const modelInstance = getAIModel({
13594
+ provider: candidate.provider,
13595
+ modelName: candidate.model,
13596
+ apiKey: candidate.apiKey
13597
+ });
13598
+ const streamResult = ai.streamText({
13599
+ model: modelInstance,
13600
+ system: systemInstructions,
13601
+ prompt: userPrompt,
13602
+ temperature: 0.2,
13603
+ abortSignal: AbortSignal.timeout(25e3)
13604
+ });
13605
+ let fullContent = "";
13606
+ for await (const part of streamResult.fullStream) {
13607
+ if (part.type === "reasoning-delta") {
13608
+ onChunk?.(part.text, "thought");
13609
+ } else if (part.type === "text-delta") {
13610
+ fullContent += part.text;
13611
+ onChunk?.(part.text, "content");
13555
13612
  }
13556
- } catch {
13557
13613
  }
13558
- } else if (provider === "openrouter" && openrouterKey && isProviderEnabled("openrouter")) {
13559
- try {
13560
- const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
13561
- method: "POST",
13562
- headers: {
13563
- "Content-Type": "application/json",
13564
- "Authorization": `Bearer ${openrouterKey}`
13565
- },
13566
- body: JSON.stringify({
13567
- model,
13568
- messages,
13569
- stream: true,
13570
- temperature: 0.2
13571
- })
13572
- });
13573
- if (res.ok && res.body) {
13574
- const reader = res.body.getReader();
13575
- const decoder = new TextDecoder();
13576
- let fullContent = "";
13577
- await readSseStream(reader, decoder, (dataStr) => {
13578
- try {
13579
- const parsed = JSON.parse(dataStr);
13580
- const delta = parsed.choices?.[0]?.delta;
13581
- if (delta?.content) {
13582
- fullContent += delta.content;
13583
- onChunk?.(delta.content, "content");
13584
- }
13585
- } catch {
13586
- }
13587
- });
13588
- if (fullContent.trim()) return fullContent.trim();
13589
- }
13590
- } catch {
13614
+ if (fullContent.trim()) {
13615
+ console.log(`[curriculum-kit:VercelAI] \u2705 ${candidate.provider} (${candidate.model}) streamed in ${Date.now() - t0}ms`);
13616
+ return fullContent.trim();
13591
13617
  }
13618
+ } catch (e) {
13619
+ console.warn(`[curriculum-kit:VercelAI] Candidate ${candidate.provider} (${candidate.model}) failed in ${Date.now() - t0}ms:`, e?.message || e);
13592
13620
  }
13593
13621
  }
13594
13622
  return null;
@@ -14455,7 +14483,7 @@ template_contract: "artifact-template-v1"
14455
14483
  async function withRateLimitBackoff(options) {
14456
14484
  let attempt = 1;
14457
14485
  try {
14458
- const meta = workflowPkg.getStepMetadata();
14486
+ const meta = workflow.getStepMetadata();
14459
14487
  if (meta && typeof meta.attempt === "number") {
14460
14488
  attempt = meta.attempt;
14461
14489
  }
@@ -14471,7 +14499,7 @@ async function withRateLimitBackoff(options) {
14471
14499
  if (isRateLimited || isTransientError) {
14472
14500
  const retryAfterSeconds = Math.min(120, Math.pow(2, attempt) * 2);
14473
14501
  const reason = isRateLimited ? "Rate limit exceeded (429)" : `Transient server error (${status || "network"})`;
14474
- throw new workflowPkg.RetryableError(`[${options.stepName}] ${reason}. Retrying attempt ${attempt + 1}...`, {
14502
+ throw new workflow.RetryableError(`[${options.stepName}] ${reason}. Retrying attempt ${attempt + 1}...`, {
14475
14503
  retryAfter: `${retryAfterSeconds}s`
14476
14504
  });
14477
14505
  }
@@ -14756,10 +14784,10 @@ var approvalPayloadSchema = zod.z.object({
14756
14784
  feedback: zod.z.string().optional().describe("Actionable feedback or required changes if rejected"),
14757
14785
  timestamp: zod.z.string().optional().describe("ISO timestamp of the approval action")
14758
14786
  });
14759
- var hookFactory = workflowPkg__namespace.defineHook || workflowPkg__namespace.createHook || ((opts) => opts);
14760
- var lessonApprovalHook = hookFactory({
14761
- schema: approvalPayloadSchema
14762
- });
14787
+ var lessonApprovalHook = workflow.defineHook();
14788
+ function approvalHookToken(projectId, lessonId, artifactType) {
14789
+ return `approval:${projectId}:${lessonId}:${artifactType}`;
14790
+ }
14763
14791
 
14764
14792
  // src/standards/standardsCoverageGate.ts
14765
14793
  function resolveStatementRef(ref, packs) {
@@ -15109,9 +15137,9 @@ async function generateMilestoneWorkflow(input) {
15109
15137
  } else if (lessonGate === "HITL") {
15110
15138
  const hookToken = "gate:lesson:" + milestoneId;
15111
15139
  console.log(" \u23F8 [Gate] LESSON awaiting HUMAN review (HITL) \u2014 hook " + hookToken);
15112
- const workflowPkg2 = await import('workflow');
15113
- const createHook2 = workflowPkg2.createHook;
15114
- const hook = createHook2({ token: hookToken });
15140
+ const workflowPkg = await import('workflow');
15141
+ const createHook = workflowPkg.createHook;
15142
+ const hook = createHook({ token: hookToken });
15115
15143
  const review = await hook;
15116
15144
  console.log(" \u2705 [Gate] Human verdict: " + (review.approved ? "APPROVED" : "REJECTED") + (review.feedback ? " \u2014 " + review.feedback : ""));
15117
15145
  if (!review.approved) {
@@ -15131,7 +15159,7 @@ async function generateMilestoneWorkflow(input) {
15131
15159
  standardsContext: standardsContext || void 0,
15132
15160
  modelOptions: input.modelOptions
15133
15161
  });
15134
- const hook2 = createHook2({ token: hookToken + ":v2" });
15162
+ const hook2 = createHook({ token: hookToken + ":v2" });
15135
15163
  const review2 = await hook2;
15136
15164
  console.log(" \u2705 [Gate] Human verdict (v2): " + (review2.approved ? "APPROVED" : "REJECTED"));
15137
15165
  if (!review2.approved) {
@@ -15319,7 +15347,7 @@ async function generateRoadmapWorkflow(input) {
15319
15347
  }
15320
15348
  }
15321
15349
  if (i + batchSize < milestones.length) {
15322
- await workflowPkg.sleep("2s");
15350
+ await workflow.sleep("2s");
15323
15351
  }
15324
15352
  }
15325
15353
  const successfulMilestones = milestoneOutcomes.filter((m) => m.status === "FULFILLED").length;
@@ -16741,6 +16769,36 @@ var MediaLedger = class {
16741
16769
  doneCount() {
16742
16770
  return this.listByStatus("done").length;
16743
16771
  }
16772
+ /**
16773
+ * Scan artifact content và seed pending entries cho mọi placeholder [IMAGE: slug].
16774
+ * Được gọi sau khi agent sinh artifact (trước khi Curator duyệt) — đây là cầu nối
16775
+ * duy nhất giữa "agent chèn placeholder" và "sổ cái có entry để Curator/Fulfiller xử".
16776
+ * Idempotent: slug đã có entry active (pending/approved/done) → giữ nguyên;
16777
+ * chỉ re-describe khi entry trước bị rejected/failed.
16778
+ */
16779
+ seedFromArtifact(artifactId, content) {
16780
+ const seeded = [];
16781
+ const re = /\[IMAGE\s*:\s*([a-z0-9][a-z0-9-]*)\s*\]/gi;
16782
+ const seen = /* @__PURE__ */ new Set();
16783
+ let m;
16784
+ while ((m = re.exec(content)) !== null) {
16785
+ const slug = m[1].toLowerCase();
16786
+ if (seen.has(slug)) continue;
16787
+ seen.add(slug);
16788
+ const existing = this.entries.get(slug);
16789
+ const isActive = existing && (existing.status === "pending" || existing.status === "approved" || existing.status === "done");
16790
+ if (isActive) continue;
16791
+ seeded.push(this.upsert({
16792
+ slug,
16793
+ artifactId,
16794
+ name: slug.replace(/^(img-|image-)/, "").replace(/-/g, " "),
16795
+ description: `Placeholder ch\xE8n b\u1EDFi agent trong artifact ${artifactId} \u2014 ch\u1EDD Curator enrich (m\xF4 t\u1EA3 g\u1ED1c ch\u01B0a c\xF3).`,
16796
+ mode: "generate",
16797
+ status: "pending"
16798
+ }));
16799
+ }
16800
+ return seeded;
16801
+ }
16744
16802
  };
16745
16803
 
16746
16804
  // src/media/curator.ts
@@ -17155,6 +17213,7 @@ exports.WorksheetSchema = WorksheetSchema;
17155
17213
  exports.activityTools = activityTools;
17156
17214
  exports.analystTools = analystTools;
17157
17215
  exports.analyzeProjectCreationIntent = analyzeProjectCreationIntent;
17216
+ exports.approvalHookToken = approvalHookToken;
17158
17217
  exports.approvalPayloadSchema = approvalPayloadSchema;
17159
17218
  exports.assertAcyclic = assertAcyclic;
17160
17219
  exports.assessorTools = assessorTools;