dsh-aha-tutor 1.2.0 → 1.3.0

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/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as intakeSummary, A as emptyProgress, B as passageRange, C as summaryPrompt, D as generateText, E as generateSteps, F as readMaterials, G as recommendNext, H as applyEvidence, I as readProfile, J as conceptIndex, K as boardToPlainText, L as readProgress, M as materialImages, N as materialsSummary, O as streamCounter, P as passagesFor, Q as insertLesson, R as documentParts, S as shortPrompt, T as generateJson, U as createConceptState, V as spreadPassages, W as masteryPercent, X as findLesson, Y as courseProgress, Z as flattenLessons, _ as projectsPrompt, a as blueprintPrompt, at as normalizePractice, b as reexplainPrompt, c as digestPartPrompt, ct as outlineForPrompt, d as gradePrompt, dt as assertId, et as lessonAfter, f as lessonPrompt, ft as newId, g as projectReviewPrompt, h as profilePrompt, i as assistPrompt, it as normalizeCheatsheet, j as masteryFor, k as EMPTY_PROFILE, l as digestPrompt, lt as safeRelativePath, m as practicePrompt, n as publishRuntime, nt as normalizeBlueprint, o as cheatsheetPrompt, ot as normalizeProject, p as materialBriefPrompt, q as normalizeBoard, r as askPrompt, rt as normalizeCards, s as detectLanguage, st as normalizeQuestionnaire, tt as nextLesson, u as flashcardsPrompt, ut as text, v as questionnairePrompt, w as TutorError, x as revisePrompt, y as quizPrompt, z as formatPassages } from "./runtime-ywaTyu56.js";
1
+ import { $ as courseProgress, A as streamCounter, B as documentParts, C as reworkPrompt, D as generateJson, E as TutorError, F as materialsSummary, G as createConceptState, H as passageRange, I as passagesFor, J as extractJson, K as masteryPercent, L as readMaterials, M as emptyProgress, N as masteryFor, O as generateSteps, P as materialImages, Q as conceptIndex, R as readProfile, S as revisePrompt, T as summaryPrompt, U as spreadPassages, V as formatPassages, W as applyEvidence, X as normalizeBoard, Y as boardToPlainText, Z as blueprintOutline, _ as projectReviewPrompt, _t as newId, a as blueprintPrompt, at as nextLesson, b as quizPrompt, c as detectLanguage, ct as normalizeCheatsheet, d as flashcardsPrompt, dt as normalizeProject, et as findLesson, f as gradePrompt, ft as normalizeQuestionnaire, g as profilePrompt, gt as assertId, h as practicePrompt, ht as text, i as assistPrompt, it as lessonAfter, j as EMPTY_PROFILE, k as generateText, l as digestPartPrompt, lt as normalizeLesson, m as materialBriefPrompt, mt as safeRelativePath, n as publishRuntime, nt as insertLesson, o as cheatsheetPrompt, ot as normalizeBlueprint, p as lessonPrompt, pt as outlineForPrompt, q as recommendNext, r as askPrompt, rt as intakeSummary, s as correctionPrompt, st as normalizeCards, tt as flattenLessons, u as digestPrompt, ut as normalizePractice, v as projectsPrompt, w as shortPrompt, x as reexplainPrompt, y as questionnairePrompt, z as readProgress } from "./runtime-BCGFUDlg.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, join, relative, sep } from "node:path";
4
4
  import { credentialKey } from "@deepseek-ai/dsh-credentials";
@@ -217,6 +217,25 @@ const TIMELINES = [
217
217
  "balanced",
218
218
  "relaxed"
219
219
  ];
220
+ const SIZES = [
221
+ "auto",
222
+ "short",
223
+ "standard",
224
+ "full"
225
+ ];
226
+ function outlineStream(api) {
227
+ let soFar = "";
228
+ const counter = streamCounter("\"estMinutes\"", (count) => {
229
+ api.progress(count, 0, "blueprint");
230
+ try {
231
+ api.preview(blueprintOutline(extractJson(soFar)));
232
+ } catch {}
233
+ });
234
+ return (delta) => {
235
+ soFar += String(delta ?? "");
236
+ counter(delta);
237
+ };
238
+ }
220
239
  function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date() }) {
221
240
  const stamp = () => now().toISOString();
222
241
  async function readCourse(courseId) {
@@ -265,7 +284,11 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
265
284
  }
266
285
  async function get({ courseId }) {
267
286
  const course = await readCourse(courseId);
268
- const [progress, intake] = await Promise.all([readProgress(store, courseId), store.readJson(store.paths.intake(courseId), null)]);
287
+ const [progress, intake, proposal] = await Promise.all([
288
+ readProgress(store, courseId),
289
+ store.readJson(store.paths.intake(courseId), null),
290
+ store.readJson(store.paths.proposal(courseId), null)
291
+ ]);
269
292
  const boards = new Set(await store.listIds(store.paths.boardsDir(courseId), "les"));
270
293
  const practice = new Set(await store.listIds(store.paths.practiceDir(courseId), "les"));
271
294
  const sheets = new Set(await store.listIds(store.paths.sheetsDir(courseId), "unt"));
@@ -273,6 +296,7 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
273
296
  course,
274
297
  progress,
275
298
  intake,
299
+ proposal,
276
300
  jobs: {
277
301
  questions: jobs.running(`questions:${courseId}`),
278
302
  blueprint: jobs.running(`blueprint:${courseId}`),
@@ -339,7 +363,7 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
339
363
  meta: { courseId }
340
364
  });
341
365
  }
342
- function submitIntake({ courseId, answers = {}, timeline = "balanced", weeklyMinutes, deadline }) {
366
+ function submitIntake({ courseId, answers = {}, timeline = "balanced", size = "auto", weeklyMinutes, deadline }) {
343
367
  assertId(courseId, "crs");
344
368
  return jobs.start("craft/blueprint", async (api) => {
345
369
  const course = await readCourse(courseId);
@@ -353,10 +377,12 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
353
377
  answers: cleanAnswers,
354
378
  at: stamp()
355
379
  });
380
+ if (course.materialIds?.length) api.progress(0, 0, "materials");
356
381
  const materials = await readMaterials(store, course.materialIds ?? []);
357
382
  const profile = await readProfile(store);
358
383
  const settings = {
359
384
  timeline: TIMELINES.includes(timeline) ? timeline : "balanced",
385
+ size: SIZES.includes(size) ? size : "auto",
360
386
  weeklyMinutes: clampMinutes(weeklyMinutes),
361
387
  deadline: /^\d{4}-\d{2}-\d{2}$/u.test(String(deadline ?? "")) ? deadline : null
362
388
  };
@@ -370,12 +396,13 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
370
396
  materialsSummary: materialsSummary(materials),
371
397
  profile,
372
398
  timeline: settings.timeline,
399
+ size: settings.size,
373
400
  weeklyMinutes: settings.weeklyMinutes
374
401
  }),
375
402
  images: llm.supportsImages?.("teach") ? await materialImages(store, materials) : void 0,
376
403
  maxTokens: 16e3,
377
404
  signal: api.signal,
378
- onText: streamCounter("\"estMinutes\"", (count) => api.progress(count, 0, "blueprint")),
405
+ onText: outlineStream(api),
379
406
  normalize: (raw) => attachProjectUnits(raw, normalizeBlueprint(raw, { language: course.language }))
380
407
  });
381
408
  const next = {
@@ -420,19 +447,78 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
420
447
  onText: streamCounter("\"estMinutes\"", (count) => api.progress(count, 0, "blueprint")),
421
448
  normalize: (raw) => attachProjectUnits(raw, normalizeBlueprint(raw, { language: course.language }))
422
449
  });
423
- const next = {
424
- ...course,
425
- ...revised,
426
- language: course.language,
427
- status: course.status === "ready" ? "ready" : "blueprint"
450
+ const proposal = {
451
+ instruction: request,
452
+ at: stamp(),
453
+ blueprint: revised
454
+ };
455
+ await store.writeJson(store.paths.proposal(courseId), proposal);
456
+ return {
457
+ course,
458
+ proposal
428
459
  };
429
- await saveCourse(next);
430
- return { course: next };
431
460
  }, {
432
461
  key: `blueprint:${courseId}`,
433
462
  meta: { courseId }
434
463
  });
435
464
  }
465
+ async function applyProposal({ courseId }) {
466
+ const course = await readCourse(courseId);
467
+ const proposal = await store.readJson(store.paths.proposal(courseId), null);
468
+ if (!proposal?.blueprint?.units) throw new TutorError("not-found", "There is no proposed change");
469
+ const next = {
470
+ ...course,
471
+ ...proposal.blueprint,
472
+ language: course.language,
473
+ status: course.status === "ready" ? "ready" : "blueprint"
474
+ };
475
+ await saveCourse(next);
476
+ await store.removeDir(store.paths.proposal(courseId));
477
+ return { course: next };
478
+ }
479
+ async function discardProposal({ courseId }) {
480
+ assertId(courseId, "crs");
481
+ await store.removeDir(store.paths.proposal(courseId));
482
+ return { ok: true };
483
+ }
484
+ function reworkLesson({ courseId, lesson: raw, unitTitle, lectureTitle, mode }) {
485
+ const split = mode === "split";
486
+ const lesson = normalizeLesson(raw);
487
+ if (!lesson) throw new TutorError("invalid", "Unknown lesson");
488
+ return jobs.start("craft/rework", async (api) => {
489
+ const course = await readCourse(courseId);
490
+ return { lessons: await generateJson(llm, {
491
+ purpose: "teach",
492
+ prompt: reworkPrompt({
493
+ outline: outlineForPrompt(course, { maxChars: 4e3 }),
494
+ unitTitle: text(unitTitle, 120),
495
+ lectureTitle: text(lectureTitle, 120),
496
+ lesson: {
497
+ title: lesson.title,
498
+ summary: lesson.summary,
499
+ objectives: lesson.objectives,
500
+ concepts: lesson.concepts.map((concept) => concept.name),
501
+ estMinutes: lesson.estMinutes
502
+ },
503
+ mode: split ? "split" : "deepen",
504
+ language: course.language
505
+ }),
506
+ maxTokens: 3e3,
507
+ signal: api.signal,
508
+ normalize: (value) => {
509
+ const list = (Array.isArray(value?.lessons) ? value.lessons : [value]).map((entry) => normalizeLesson({
510
+ ...entry,
511
+ id: void 0
512
+ })).filter(Boolean).slice(0, split ? 3 : 1);
513
+ if (split ? list.length < 2 : !list.length) throw new Error("no usable lessons");
514
+ return list;
515
+ }
516
+ }) };
517
+ }, {
518
+ key: `rework:${courseId}:${lesson.id}`,
519
+ meta: { courseId }
520
+ });
521
+ }
436
522
  async function saveBlueprint({ courseId, blueprint }) {
437
523
  const course = await readCourse(courseId);
438
524
  const normalized = normalizeBlueprint({
@@ -463,7 +549,7 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
463
549
  language: course.language,
464
550
  passages: passagesFor(sources, query, 5e3)
465
551
  }),
466
- maxTokens: Math.min(16e3, 3e3 + lessons.length * 2200),
552
+ maxTokens: Math.min(16e3, 3e3 + lessons.length * 2600),
467
553
  signal,
468
554
  normalize: (raw) => raw?.lessons ?? raw
469
555
  });
@@ -642,6 +728,9 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
642
728
  startCraft,
643
729
  submitIntake,
644
730
  revise,
731
+ applyProposal,
732
+ discardProposal,
733
+ reworkLesson,
645
734
  saveBlueprint,
646
735
  writeLecturePractice,
647
736
  confirm,
@@ -1522,6 +1611,7 @@ function createJobs({ logger, now = Date.now } = {}) {
1522
1611
  label: ""
1523
1612
  },
1524
1613
  partial: [],
1614
+ preview: null,
1525
1615
  result: void 0,
1526
1616
  error: void 0,
1527
1617
  startedAt: now(),
@@ -1541,6 +1631,9 @@ function createJobs({ logger, now = Date.now } = {}) {
1541
1631
  },
1542
1632
  push(item) {
1543
1633
  job.partial.push(item);
1634
+ },
1635
+ preview(value) {
1636
+ job.preview = value ?? null;
1544
1637
  }
1545
1638
  };
1546
1639
  const aborted = new Promise((_, reject) => {
@@ -1573,6 +1666,7 @@ function createJobs({ logger, now = Date.now } = {}) {
1573
1666
  progress: job.progress,
1574
1667
  partial: job.partial.slice(since),
1575
1668
  partialCount: job.partial.length,
1669
+ preview: job.preview ?? void 0,
1576
1670
  result: job.status === "done" ? job.result : void 0,
1577
1671
  error: job.error
1578
1672
  };
@@ -1668,6 +1762,7 @@ function buildPlan({ today, settings, courses = [], reviews = [], exams = [] })
1668
1762
  const queues = courses.map((course) => ({
1669
1763
  id: course.id,
1670
1764
  title: course.title,
1765
+ emoji: course.emoji || "",
1671
1766
  deadline: course.deadline || null,
1672
1767
  priority: Number(course.priority) || 0,
1673
1768
  remaining: (course.lessons || []).filter((lesson) => !lesson.done).map((lesson) => ({
@@ -1728,6 +1823,7 @@ function buildPlan({ today, settings, courses = [], reviews = [], exams = [] })
1728
1823
  courseId: queue.id,
1729
1824
  title: lesson.title,
1730
1825
  courseTitle: queue.title,
1826
+ courseEmoji: queue.emoji,
1731
1827
  minutes: lesson.minutes
1732
1828
  });
1733
1829
  if (queue.remaining.length === 0) queue.finishDay = day;
@@ -1936,6 +2032,7 @@ function createLearner({ store, llm, now = () => /* @__PURE__ */ new Date(), log
1936
2032
  courses: active.map(({ course, progress }) => ({
1937
2033
  id: course.id,
1938
2034
  title: course.title,
2035
+ emoji: course.emoji ?? "",
1939
2036
  deadline: course.settings?.deadline ?? null,
1940
2037
  lessons: flattenLessons(course).map(({ lesson }) => ({
1941
2038
  id: lesson.id,
@@ -1990,10 +2087,11 @@ function createLearner({ store, llm, now = () => /* @__PURE__ */ new Date(), log
1990
2087
  }));
1991
2088
  return plan();
1992
2089
  }
1993
- async function reviewSession({ limit = 8 } = {}) {
2090
+ async function reviewSession({ limit = 8, courseId } = {}) {
1994
2091
  const at = now().getTime();
1995
2092
  const items = [];
1996
2093
  for (const { course, progress } of await activeCourses()) {
2094
+ if (courseId && course.id !== courseId) continue;
1997
2095
  const index = conceptIndex(course);
1998
2096
  const due = Object.entries(progress.reviews ?? {}).filter(([id, card]) => index.has(id) && Date.parse(card.dueAt) <= at).sort((a, b) => Date.parse(a[1].dueAt) - Date.parse(b[1].dueAt));
1999
2097
  for (const [conceptId] of due) {
@@ -2002,7 +2100,7 @@ function createLearner({ store, llm, now = () => /* @__PURE__ */ new Date(), log
2002
2100
  const lesson = findLesson(course, concept.lessonId)?.lesson;
2003
2101
  const set = await store.readJson(store.paths.practice(course.id, concept.lessonId), null);
2004
2102
  const board = await store.readJson(store.paths.board(course.id, concept.lessonId), null);
2005
- const candidates = [...set?.items ?? [], ...(board?.steps ?? []).map((step) => step.check).filter(Boolean)].filter((item) => item.conceptIds?.includes(conceptId) && item.kind !== "short");
2103
+ const candidates = [...set?.items ?? [], ...(board?.steps ?? []).map((step) => step.check).filter(Boolean)].filter((item) => item.conceptIds?.includes(conceptId) && item.kind !== "short" && item.kind !== "work");
2006
2104
  const item = candidates[Math.floor(Math.random() * candidates.length)];
2007
2105
  if (item) items.push({
2008
2106
  courseId: course.id,
@@ -2177,6 +2275,7 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
2177
2275
  const kindDirective = row.lesson.kind === "remedial" ? "remediate" : row.lesson.kind === "review" ? "review" : row.lesson.kind === "challenge" ? "challenge" : void 0;
2178
2276
  return generateSteps(llm, {
2179
2277
  purpose: "teach",
2278
+ effort: "low",
2180
2279
  prompt: lessonPrompt({
2181
2280
  course,
2182
2281
  unit: row.unit,
@@ -2359,6 +2458,7 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
2359
2458
  const index = Math.min(board.steps.length - 1, Math.max(0, Number(stepIndex) || 0));
2360
2459
  const answer = await generateSteps(llm, {
2361
2460
  purpose: "teach",
2461
+ effort: "low",
2362
2462
  prompt: reexplainPrompt({
2363
2463
  lesson,
2364
2464
  language,
@@ -2439,18 +2539,25 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
2439
2539
  }
2440
2540
  async function grade({ item, response, language = "zh-CN" }) {
2441
2541
  if (!item?.prompt) throw new TutorError("invalid", "Missing question");
2542
+ const work = item.kind === "work";
2442
2543
  const answer = text(response, 4e3);
2443
2544
  if (!answer) return {
2444
2545
  score: 0,
2445
2546
  verdict: "incorrect",
2446
2547
  feedback: "",
2447
2548
  errorTag: "",
2448
- misconception: false
2549
+ misconception: false,
2550
+ ...work ? {
2551
+ wrongStep: "",
2552
+ why: "",
2553
+ fix: ""
2554
+ } : {}
2449
2555
  };
2450
2556
  return await generateJson(llm, {
2451
2557
  purpose: "quick",
2452
2558
  prompt: gradePrompt({
2453
2559
  item: {
2560
+ kind: work ? "work" : "short",
2454
2561
  prompt: text(item.prompt, 2e3),
2455
2562
  answer: text(item.answer, 3e3),
2456
2563
  rubric: Array.isArray(item.rubric) ? item.rubric.map((entry) => text(entry, 300)) : []
@@ -2458,7 +2565,7 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
2458
2565
  response: answer,
2459
2566
  language
2460
2567
  }),
2461
- maxTokens: 1200,
2568
+ maxTokens: work ? 1800 : 1200,
2462
2569
  normalize: (raw) => ({
2463
2570
  score: Math.min(1, Math.max(0, Number(raw?.score) || 0)),
2464
2571
  verdict: [
@@ -2468,10 +2575,60 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
2468
2575
  ].includes(raw?.verdict) ? raw.verdict : Number(raw?.score) >= .8 ? "correct" : Number(raw?.score) >= .4 ? "partial" : "incorrect",
2469
2576
  feedback: text(raw?.feedback, 1200),
2470
2577
  errorTag: text(raw?.errorTag, 80),
2471
- misconception: raw?.misconception === true
2578
+ misconception: raw?.misconception === true,
2579
+ ...work ? {
2580
+ wrongStep: text(raw?.wrongStep, 400),
2581
+ why: text(raw?.why, 400),
2582
+ fix: text(raw?.fix, 400)
2583
+ } : {}
2472
2584
  })
2473
2585
  });
2474
2586
  }
2587
+ function correct({ scope, stepIndex = 0, anchorStepId, item, response, grade: graded }) {
2588
+ const working = text(response, 4e3);
2589
+ if (!item?.prompt || !working) throw new TutorError("invalid", "Nothing to correct");
2590
+ const notes = graded && typeof graded === "object" ? graded : {};
2591
+ return jobs.start("lesson/correct", async (api) => {
2592
+ const { course, lesson, board, language } = await scopeBoard(scope);
2593
+ const answer = await generateSteps(llm, {
2594
+ purpose: "quick",
2595
+ prompt: correctionPrompt({
2596
+ course,
2597
+ lesson,
2598
+ language,
2599
+ boardSoFar: board ? boardToPlainText(board, {
2600
+ uptoStep: Number(stepIndex) || 0,
2601
+ maxChars: 5e3
2602
+ }) : "",
2603
+ item: {
2604
+ prompt: text(item.prompt, 2e3),
2605
+ answer: text(item.answer, 3e3)
2606
+ },
2607
+ response: working,
2608
+ grade: {
2609
+ feedback: text(notes.feedback, 1200),
2610
+ wrongStep: text(notes.wrongStep, 400),
2611
+ why: text(notes.why, 400),
2612
+ fix: text(notes.fix, 400)
2613
+ }
2614
+ }),
2615
+ maxTokens: 4e3,
2616
+ signal: api.signal,
2617
+ conceptIds: lesson?.concepts?.map((concept) => concept.id) ?? [],
2618
+ onStep: (step) => api.push(step)
2619
+ });
2620
+ const aside = await keepAside(scope, {
2621
+ kind: "correction",
2622
+ errorTag: text(notes.errorTag, 80),
2623
+ anchorStepId: anchorFor(board, stepIndex, anchorStepId),
2624
+ steps: answer.steps
2625
+ });
2626
+ return {
2627
+ steps: answer.steps,
2628
+ aside
2629
+ };
2630
+ });
2631
+ }
2475
2632
  async function savePosition({ courseId, lessonId, stepIndex = 0 }) {
2476
2633
  assertId(lessonId, "les");
2477
2634
  await store.updateJson(store.paths.progress(assertId(courseId, "crs")), null, (progress) => {
@@ -2744,6 +2901,7 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
2744
2901
  reexplain,
2745
2902
  record,
2746
2903
  grade,
2904
+ correct,
2747
2905
  savePosition,
2748
2906
  complete,
2749
2907
  practice,
@@ -2781,6 +2939,17 @@ const RETRYABLE$1 = /* @__PURE__ */ new Set([
2781
2939
  "TIMEOUT",
2782
2940
  "TRANSPORT"
2783
2941
  ]);
2942
+ const REASONING_ROOM = Object.freeze({
2943
+ off: 0,
2944
+ none: 0,
2945
+ minimal: 4e3,
2946
+ low: 8e3,
2947
+ medium: 16e3,
2948
+ high: 24e3,
2949
+ xhigh: 48e3,
2950
+ max: 48e3
2951
+ });
2952
+ const STOPPED_AT_LIMIT = /* @__PURE__ */ new Set(["max-tokens", "length"]);
2784
2953
  const CALL_TIMEOUT_MS = 6 * 6e4;
2785
2954
  const MAX_RETRIES = 2;
2786
2955
  const USER_ERRORS = Object.freeze({
@@ -2794,6 +2963,7 @@ const USER_ERRORS = Object.freeze({
2794
2963
  CONTEXT_WINDOW_EXCEEDED: ["llm-context", "The material is too long for this model. Pick fewer materials or a model with a larger context."],
2795
2964
  NO_ADAPTER: ["llm-unavailable", "The selected model is not available. Pick another model in Aha settings."],
2796
2965
  TIMEOUT: ["llm-timeout", "The model took too long to respond. Try again."],
2966
+ EMPTY_RESPONSE: ["llm-empty", "The model returned nothing this time. Try again."],
2797
2967
  ABORTED: ["cancelled", "Cancelled"]
2798
2968
  });
2799
2969
  function toTutorError(code, fallback) {
@@ -2801,6 +2971,17 @@ function toTutorError(code, fallback) {
2801
2971
  if (known) return new TutorError(known[0], known[1]);
2802
2972
  return new TutorError("llm-failed", `The model call failed${code ? ` (${code})` : ""}${fallback ? `: ${fallback}` : ""}`);
2803
2973
  }
2974
+ function pickEffort(efforts, wanted) {
2975
+ if (!efforts?.length || !wanted) return void 0;
2976
+ if (wanted === "lowest") return efforts[0].id;
2977
+ return (efforts.find((effort) => effort.id === wanted) ?? efforts.find((effort) => effort.id !== "off" && effort.id !== "none") ?? efforts[0]).id;
2978
+ }
2979
+ function outputBudget(maxTokens, effort, model) {
2980
+ const cap = Number(model?.defaultMaxTokens) || 0;
2981
+ if (!cap || !model?.reasoning) return maxTokens;
2982
+ const room = REASONING_ROOM[effort ?? model.reasoning.defaultEffort] ?? REASONING_ROOM.high;
2983
+ return Math.max(Math.min(maxTokens, cap), Math.min(maxTokens + room, cap));
2984
+ }
2804
2985
  const sleep$1 = (ms, signal) => new Promise((resolve, reject) => {
2805
2986
  const timer = setTimeout(resolve, ms);
2806
2987
  signal?.addEventListener("abort", () => {
@@ -2860,7 +3041,7 @@ function createLlmPort({ getLlm, getDefault, getAttachments, prefs, logger }) {
2860
3041
  }
2861
3042
  return blocks;
2862
3043
  }
2863
- async function attempt({ purpose, system, user, images, maxTokens, signal, onText }) {
3044
+ async function attempt({ purpose, effort, system, user, images, maxTokens, signal, onText }) {
2864
3045
  const llm = getLlm();
2865
3046
  if (!llm) throw new TutorError("llm-unavailable", "DSH has no model service in this profile.");
2866
3047
  const target = route(purpose);
@@ -2871,13 +3052,14 @@ function createLlmPort({ getLlm, getDefault, getAttachments, prefs, logger }) {
2871
3052
  prepared = await llm.prepareCall({
2872
3053
  ...target,
2873
3054
  maxTokens
2874
- }, fused, purpose === "quick" ? (controls, model) => {
2875
- const lowest = model?.reasoning?.efforts?.[0];
2876
- return lowest ? {
3055
+ }, fused, (controls, model) => {
3056
+ const chosen = pickEffort(model?.reasoning?.efforts, purpose === "quick" ? "lowest" : effort);
3057
+ return {
2877
3058
  ...controls,
2878
- reasoningEffort: lowest.id
2879
- } : controls;
2880
- } : void 0);
3059
+ ...chosen ? { reasoningEffort: chosen } : {},
3060
+ maxTokens: outputBudget(maxTokens, chosen, model)
3061
+ };
3062
+ });
2881
3063
  } catch (error) {
2882
3064
  if (signal?.aborted) throw signal.reason ?? error;
2883
3065
  throw Object.assign(toTutorError(error?.code, error?.message), { retryable: false });
@@ -2916,18 +3098,24 @@ function createLlmPort({ getLlm, getDefault, getAttachments, prefs, logger }) {
2916
3098
  }
2917
3099
  if (!textValue.trim()) throw Object.assign(toTutorError("EMPTY_RESPONSE"), {
2918
3100
  retryable: true,
2919
- streamed: false
3101
+ streamed: false,
3102
+ grow: STOPPED_AT_LIMIT.has(finish?.kind)
2920
3103
  });
2921
3104
  return textValue;
2922
3105
  }
2923
3106
  async function call(options) {
2924
3107
  let lastError;
3108
+ let current = options;
2925
3109
  for (let tries = 0; tries <= MAX_RETRIES; tries += 1) try {
2926
- return await attempt(options);
3110
+ return await attempt(current);
2927
3111
  } catch (error) {
2928
3112
  lastError = error;
2929
3113
  if (options.signal?.aborted || !error?.retryable || error?.streamed || tries === MAX_RETRIES) throw error;
2930
- await sleep$1(Math.min(2e4, Number(error.retryAfterMs) || 1500 * (tries + 1)), options.signal);
3114
+ if (error.grow) current = {
3115
+ ...current,
3116
+ maxTokens: current.maxTokens * 2
3117
+ };
3118
+ else await sleep$1(Math.min(2e4, Number(error.retryAfterMs) || 1500 * (tries + 1)), options.signal);
2931
3119
  }
2932
3120
  throw lastError;
2933
3121
  }
@@ -3029,6 +3217,7 @@ function createStore({ root }) {
3029
3217
  course: (id) => join(paths.courseDir(id), "course.json"),
3030
3218
  progress: (id) => join(paths.courseDir(id), "progress.json"),
3031
3219
  intake: (id) => join(paths.courseDir(id), "intake.json"),
3220
+ proposal: (id) => join(paths.courseDir(id), "proposal.json"),
3032
3221
  cards: (id) => join(paths.courseDir(id), "cards.json"),
3033
3222
  boardsDir: (id) => join(paths.courseDir(id), "boards"),
3034
3223
  practiceDir: (id) => join(paths.courseDir(id), "practice"),
@@ -3732,10 +3921,20 @@ function createRoutes({ courses, lessons, learner, studio, projects, jobs, setti
3732
3921
  quick: payload.quick === true,
3733
3922
  language: typeof payload.language === "string" ? payload.language.slice(0, 12) : void 0
3734
3923
  }) }),
3924
+ "course/proposal-apply": (payload) => courses.applyProposal({ courseId: need(payload, "courseId", "crs") }),
3925
+ "course/proposal-discard": (payload) => courses.discardProposal({ courseId: need(payload, "courseId", "crs") }),
3926
+ "course/rework-lesson": (payload) => ({ job: courses.reworkLesson({
3927
+ courseId: need(payload, "courseId", "crs"),
3928
+ lesson: payload.lesson,
3929
+ unitTitle: payload.unitTitle,
3930
+ lectureTitle: payload.lectureTitle,
3931
+ mode: payload.mode === "split" ? "split" : "deepen"
3932
+ }) }),
3735
3933
  "course/intake": (payload) => ({ job: courses.submitIntake({
3736
3934
  courseId: need(payload, "courseId", "crs"),
3737
3935
  answers: payload.answers,
3738
3936
  timeline: payload.timeline,
3937
+ size: payload.size,
3739
3938
  weeklyMinutes: payload.weeklyMinutes,
3740
3939
  deadline: payload.deadline
3741
3940
  }) }),
@@ -3818,6 +4017,14 @@ function createRoutes({ courses, lessons, learner, studio, projects, jobs, setti
3818
4017
  response: payload.response,
3819
4018
  language: typeof payload.language === "string" ? payload.language.slice(0, 12) : void 0
3820
4019
  }),
4020
+ "lesson/correct": (payload) => ({ job: lessons.correct({
4021
+ scope: scopeOf(payload),
4022
+ stepIndex: payload.stepIndex,
4023
+ anchorStepId: payload.anchorStepId,
4024
+ item: payload.item,
4025
+ response: payload.response,
4026
+ grade: payload.grade
4027
+ }) }),
3821
4028
  "lesson/position": (payload) => lessons.savePosition({
3822
4029
  courseId: need(payload, "courseId", "crs"),
3823
4030
  lessonId: need(payload, "lessonId", "les"),
@@ -3869,7 +4076,7 @@ function createRoutes({ courses, lessons, learner, studio, projects, jobs, setti
3869
4076
  courseId: payload.courseId
3870
4077
  }),
3871
4078
  "plan/remove-exam": (payload) => learner.removeExam({ id: need(payload, "id", "exm") }),
3872
- "review/session": () => learner.reviewSession(),
4079
+ "review/session": (payload) => learner.reviewSession({ courseId: optional(payload, "courseId", "crs") }),
3873
4080
  "material/upload": (payload) => ({ job: studio.upload({
3874
4081
  name: payload.name,
3875
4082
  base64: payload.base64
@@ -5033,6 +5240,7 @@ function createStudio({ store, llm, jobs, now = () => /* @__PURE__ */ new Date()
5033
5240
  const images = await assistImages(ids);
5034
5241
  const board = await generateSteps(llm, {
5035
5242
  purpose: "teach",
5243
+ effort: "low",
5036
5244
  prompt: assistPrompt({
5037
5245
  mode: chosen,
5038
5246
  request: ask,
@@ -5094,6 +5302,7 @@ function createStudio({ store, llm, jobs, now = () => /* @__PURE__ */ new Date()
5094
5302
  const language = detectLanguage(subject || passages, uiLanguage());
5095
5303
  const board = await generateSteps(llm, {
5096
5304
  purpose: "teach",
5305
+ effort: "low",
5097
5306
  prompt: shortPrompt({
5098
5307
  topic: subject || sourceTitle,
5099
5308
  passages,