@machinespirits/eval 0.2.0 → 0.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.
Files changed (74) hide show
  1. package/README.md +91 -9
  2. package/config/eval-settings.yaml +3 -3
  3. package/config/paper-manifest.json +486 -0
  4. package/config/providers.yaml +9 -6
  5. package/config/tutor-agents.yaml +2261 -0
  6. package/content/README.md +23 -0
  7. package/content/courses/479/course.md +53 -0
  8. package/content/courses/479/lecture-1.md +361 -0
  9. package/content/courses/479/lecture-2.md +360 -0
  10. package/content/courses/479/lecture-3.md +655 -0
  11. package/content/courses/479/lecture-4.md +530 -0
  12. package/content/courses/479/lecture-5.md +326 -0
  13. package/content/courses/479/lecture-6.md +346 -0
  14. package/content/courses/479/lecture-7.md +326 -0
  15. package/content/courses/479/lecture-8.md +273 -0
  16. package/content/courses/479/roadmap-slides.md +656 -0
  17. package/content/manifest.yaml +8 -0
  18. package/docs/research/build.sh +44 -20
  19. package/docs/research/figures/figure10.png +0 -0
  20. package/docs/research/figures/figure11.png +0 -0
  21. package/docs/research/figures/figure3.png +0 -0
  22. package/docs/research/figures/figure4.png +0 -0
  23. package/docs/research/figures/figure5.png +0 -0
  24. package/docs/research/figures/figure6.png +0 -0
  25. package/docs/research/figures/figure7.png +0 -0
  26. package/docs/research/figures/figure8.png +0 -0
  27. package/docs/research/figures/figure9.png +0 -0
  28. package/docs/research/header.tex +23 -2
  29. package/docs/research/paper-full.md +941 -285
  30. package/docs/research/paper-short.md +216 -585
  31. package/docs/research/references.bib +132 -0
  32. package/docs/research/slides-header.tex +188 -0
  33. package/docs/research/slides-pptx.md +363 -0
  34. package/docs/research/slides.md +531 -0
  35. package/docs/research/style-reference-pptx.py +199 -0
  36. package/package.json +6 -5
  37. package/scripts/analyze-eval-results.js +69 -17
  38. package/scripts/analyze-mechanism-traces.js +763 -0
  39. package/scripts/analyze-modulation-learning.js +498 -0
  40. package/scripts/analyze-prosthesis.js +144 -0
  41. package/scripts/analyze-run.js +264 -79
  42. package/scripts/assess-transcripts.js +853 -0
  43. package/scripts/browse-transcripts.js +854 -0
  44. package/scripts/check-parse-failures.js +73 -0
  45. package/scripts/code-dialectical-modulation.js +1320 -0
  46. package/scripts/download-data.sh +55 -0
  47. package/scripts/eval-cli.js +106 -18
  48. package/scripts/generate-paper-figures.js +663 -0
  49. package/scripts/generate-paper-figures.py +577 -76
  50. package/scripts/generate-paper-tables.js +299 -0
  51. package/scripts/qualitative-analysis-ai.js +3 -3
  52. package/scripts/render-sequence-diagram.js +694 -0
  53. package/scripts/test-latency.js +210 -0
  54. package/scripts/test-rate-limit.js +95 -0
  55. package/scripts/test-token-budget.js +332 -0
  56. package/scripts/validate-paper-manifest.js +670 -0
  57. package/services/__tests__/evalConfigLoader.test.js +2 -2
  58. package/services/__tests__/learnerRubricEvaluator.test.js +361 -0
  59. package/services/__tests__/learnerTutorInteractionEngine.test.js +326 -0
  60. package/services/evaluationRunner.js +975 -98
  61. package/services/evaluationStore.js +12 -4
  62. package/services/learnerTutorInteractionEngine.js +27 -2
  63. package/services/mockProvider.js +133 -0
  64. package/services/promptRewriter.js +1471 -5
  65. package/services/rubricEvaluator.js +55 -2
  66. package/services/transcriptFormatter.js +675 -0
  67. package/docs/EVALUATION-VARIABLES.md +0 -589
  68. package/docs/REPLICATION-PLAN.md +0 -577
  69. package/scripts/analyze-run.mjs +0 -282
  70. package/scripts/compare-runs.js +0 -44
  71. package/scripts/compare-suggestions.js +0 -80
  72. package/scripts/dig-into-run.js +0 -158
  73. package/scripts/show-failed-suggestions.js +0 -64
  74. /package/scripts/{check-run.mjs → check-run.js} +0 -0
@@ -12,6 +12,24 @@
12
12
  */
13
13
 
14
14
  import { unifiedAIProvider } from '@machinespirits/tutor-core';
15
+ import * as evalConfigLoader from './evalConfigLoader.js';
16
+
17
+ /**
18
+ * Extract a compact metrics object from a unifiedAIProvider response.
19
+ * These metrics are attached to consolidatedTrace entries so the
20
+ * transcript formatter can display model/tokens/latency/cost per call.
21
+ */
22
+ function extractMetrics(response) {
23
+ if (!response) return null;
24
+ return {
25
+ model: response.model || null,
26
+ provider: response.provider || null,
27
+ inputTokens: response.usage?.inputTokens || 0,
28
+ outputTokens: response.usage?.outputTokens || 0,
29
+ latencyMs: response.latencyMs || 0,
30
+ cost: response.usage?.cost || 0,
31
+ };
32
+ }
15
33
 
16
34
  /**
17
35
  * Synthesize directives from accumulated turn data.
@@ -27,6 +45,10 @@ export function synthesizeDirectives({ turnResults = [], consolidatedTrace = [],
27
45
 
28
46
  const directives = [];
29
47
 
48
+ // 0. Phase-aware framing — tell the ego where we are in the dialogue arc
49
+ const phaseDirective = detectDialoguePhase(turnResults, conversationHistory);
50
+ if (phaseDirective) directives.push(phaseDirective);
51
+
30
52
  // 1. Score trajectory — detect quality decline
31
53
  const scoreTrajectory = analyzeScoreTrajectory(turnResults);
32
54
  if (scoreTrajectory) directives.push(scoreTrajectory);
@@ -47,6 +69,10 @@ export function synthesizeDirectives({ turnResults = [], consolidatedTrace = [],
47
69
  const recognitionSignals = detectRecognitionSignals(conversationHistory, turnResults);
48
70
  if (recognitionSignals) directives.push(recognitionSignals);
49
71
 
72
+ // 6. Learner resistance detection — explicit resistance patterns
73
+ const resistance = detectLearnerResistance(conversationHistory, turnResults);
74
+ if (resistance) directives.push(resistance);
75
+
50
76
  if (directives.length === 0) return null;
51
77
 
52
78
  const numbered = directives.map((d, i) => `${i + 1}. ${d}`).join('\n');
@@ -269,6 +295,95 @@ function findWorstDimension(turnResult) {
269
295
  return worst;
270
296
  }
271
297
 
298
+ /**
299
+ * Detect dialogue phase and generate phase-appropriate framing.
300
+ * Phases: exploration (turns 1-2), adaptation (turns 3-4 or resistance detected),
301
+ * breakthrough (turns 5+ or strong resistance).
302
+ */
303
+ function detectDialoguePhase(turnResults, conversationHistory) {
304
+ const turnCount = turnResults.length;
305
+
306
+ // Detect resistance strength from recent messages
307
+ let resistanceStrength = 0;
308
+ const recentMessages = conversationHistory.slice(-2).map(h => h.learnerMessage || '');
309
+
310
+ for (const msg of recentMessages) {
311
+ if (/i('m| am) (still )?(confused|lost|not sure)/i.test(msg)) resistanceStrength++;
312
+ if (/i don'?t (understand|get|see)/i.test(msg)) resistanceStrength++;
313
+ if (/\b(but|however|i disagree)\b/i.test(msg)) resistanceStrength++;
314
+ }
315
+
316
+ // Score declining?
317
+ const scores = turnResults.filter(t => t.turnScore != null).map(t => t.turnScore);
318
+ const scoreDecline = scores.length >= 2 && scores[scores.length - 1] < scores[scores.length - 2] - 3;
319
+
320
+ // Phase determination
321
+ if (turnCount <= 2 && resistanceStrength === 0) {
322
+ return 'DIALOGUE PHASE: Exploration. The conversation is in its early stages — establish rapport, gauge the learner\'s current understanding, and let them direct the conversation before pushing harder.';
323
+ }
324
+
325
+ if (resistanceStrength >= 2 || (turnCount >= 4 && scoreDecline)) {
326
+ return `DIALOGUE PHASE: Breakthrough needed. The learner has shown resistance or confusion for ${resistanceStrength} signal(s) across recent turns${scoreDecline ? ' and quality is declining' : ''}. Your current approach is not landing — you MUST try a fundamentally different strategy: change your framing, switch pedagogical method (e.g., from explanation to questioning, from abstract to concrete example), or acknowledge the impasse directly and ask the learner what would help them.`;
327
+ }
328
+
329
+ if (turnCount >= 3 || resistanceStrength >= 1) {
330
+ return `DIALOGUE PHASE: Adaptation. The conversation has enough history to show patterns. ${resistanceStrength > 0 ? 'The learner is showing mild resistance — adjust your approach before it becomes entrenched.' : 'Deepen engagement by building on what the learner has said, not repeating what you\'ve already covered.'}`;
331
+ }
332
+
333
+ return null;
334
+ }
335
+
336
+ /**
337
+ * Detect explicit learner resistance patterns and generate breakthrough directives.
338
+ */
339
+ function detectLearnerResistance(conversationHistory, turnResults) {
340
+ if (conversationHistory.length < 2) return null;
341
+
342
+ const recentMessages = conversationHistory.slice(-3).map(h => ({
343
+ message: h.learnerMessage || '',
344
+ turnIndex: h.turnIndex,
345
+ })).filter(m => m.message);
346
+
347
+ if (recentMessages.length < 2) return null;
348
+
349
+ const resistanceSignals = [];
350
+
351
+ // Check for repeated confusion across turns
352
+ const confusionTurns = recentMessages.filter(m =>
353
+ /i('m| am) (still )?(confused|lost|not sure|unsure)/i.test(m.message) ||
354
+ /i don'?t (understand|get|see)/i.test(m.message) ||
355
+ /can you (explain|clarify)/i.test(m.message)
356
+ );
357
+ if (confusionTurns.length >= 2) {
358
+ resistanceSignals.push(`repeated confusion in turns ${confusionTurns.map(t => t.turnIndex + 1).join(' and ')}`);
359
+ }
360
+
361
+ // Check for pushback escalation
362
+ const pushbackTurns = recentMessages.filter(m =>
363
+ /\b(but|however|i disagree|that('s| is) not|you('re| are) (wrong|missing))\b/i.test(m.message)
364
+ );
365
+ if (pushbackTurns.length >= 2) {
366
+ resistanceSignals.push('escalating pushback across turns');
367
+ }
368
+
369
+ // Check for disengagement (shrinking message length)
370
+ const lengths = recentMessages.map(m => m.message.length);
371
+ if (lengths.length >= 3 && lengths[lengths.length - 1] < lengths[0] * 0.4) {
372
+ resistanceSignals.push('message length declining sharply (possible disengagement)');
373
+ }
374
+
375
+ // Check for score trajectory decline
376
+ const recentScores = turnResults.slice(-3).filter(t => t.turnScore != null).map(t => t.turnScore);
377
+ if (recentScores.length >= 2 && recentScores[recentScores.length - 1] < recentScores[0] - 10) {
378
+ resistanceSignals.push(`quality declining (${recentScores.map(s => s.toFixed(0)).join(' → ')})`);
379
+ }
380
+
381
+ if (resistanceSignals.length === 0) return null;
382
+
383
+ const signalList = resistanceSignals.join('; ');
384
+ return `RESISTANCE DETECTED: ${signalList}. Your current approach is not working for this learner. Consider: (a) acknowledging the difficulty directly, (b) switching to a completely different pedagogical method (Socratic questions, concrete examples, analogies, worked problems), (c) reducing cognitive load by breaking the concept into smaller pieces, or (d) asking the learner what aspect specifically isn't clicking.`;
385
+ }
386
+
272
387
  // ============================================================================
273
388
  // LLM-Based Directive Synthesis
274
389
  // ============================================================================
@@ -298,8 +413,14 @@ export async function synthesizeDirectivesLLM({
298
413
  const contextSummary = buildContextSummaryForLLM(turnResults, consolidatedTrace, conversationHistory);
299
414
 
300
415
  // Determine model to use (superego model from profile, or fallback)
301
- const superegoModel = config.superego?.model || 'moonshotai/kimi-k2.5';
416
+ // Resolve alias to full model ID (e.g., 'kimi-k2.5' → 'moonshotai/kimi-k2.5')
417
+ const superegoAlias = config.superego?.model || 'kimi-k2.5';
302
418
  const provider = config.superego?.provider || 'openrouter';
419
+ let superegoModel = superegoAlias;
420
+ try {
421
+ const resolved = evalConfigLoader.resolveModel({ provider, model: superegoAlias });
422
+ superegoModel = resolved.model;
423
+ } catch { /* use alias as-is if resolution fails */ }
303
424
 
304
425
  const systemPrompt = `You are a pedagogical analyst reviewing an ongoing tutoring dialogue. Your task is to synthesize 2-5 specific, actionable directives that will help the tutor improve in the next turn.
305
426
 
@@ -309,6 +430,12 @@ CRITICAL RULES:
309
430
  - Each directive should address a concrete issue or opportunity observed in the data
310
431
  - Directives should build on what's working, not just fix problems
311
432
  - If the dialogue is going well, focus on deepening rather than correcting
433
+ - If the learner is RESISTING (repeated confusion, pushback, declining engagement), your FIRST directive must be a breakthrough strategy — a fundamentally different approach, not a refinement of the current one
434
+
435
+ PHASE AWARENESS:
436
+ - Early turns (1-2): Focus on rapport building and gauging understanding
437
+ - Mid turns (3-4): Focus on adaptation — what patterns are emerging? What's working?
438
+ - Late turns (5+) or resistance detected: Focus on BREAKTHROUGH — the current approach has had time to work. If it hasn't, something different is needed. Suggest specific alternative methods.
312
439
 
313
440
  OUTPUT FORMAT:
314
441
  Return ONLY a numbered list of 2-5 directives, one per line. No preamble, no explanation after.
@@ -333,10 +460,11 @@ Generate 2-5 specific directives for the next turn:`;
333
460
  preset: 'deliberation',
334
461
  config: {
335
462
  temperature: 0.3, // Lower temp for focused analysis
336
- maxTokens: 500,
463
+ maxTokens: 2000,
337
464
  },
338
465
  });
339
466
 
467
+ const metrics = extractMetrics(response);
340
468
  const directives = response.content?.trim();
341
469
  if (!directives || directives.length < 20) {
342
470
  console.log('[promptRewriter] LLM returned empty or too-short directives');
@@ -344,15 +472,17 @@ Generate 2-5 specific directives for the next turn:`;
344
472
  }
345
473
 
346
474
  // Wrap in session_evolution XML block
347
- return `<session_evolution>
475
+ const text = `<session_evolution>
348
476
  Based on analysis of the dialogue so far, prioritize the following in your next response:
349
477
 
350
478
  ${directives}
351
479
  </session_evolution>`;
480
+ return { text, metrics };
352
481
  } catch (error) {
353
482
  console.error('[promptRewriter] LLM directive synthesis failed:', error.message);
354
- // Fallback to template-based directives
355
- return synthesizeDirectives({ turnResults, consolidatedTrace, conversationHistory });
483
+ // Fallback to template-based directives (no metrics — no LLM call)
484
+ const fallbackText = synthesizeDirectives({ turnResults, consolidatedTrace, conversationHistory });
485
+ return fallbackText ? { text: fallbackText, metrics: null } : null;
356
486
  }
357
487
  }
358
488
 
@@ -423,5 +553,1341 @@ function buildContextSummaryForLLM(turnResults, consolidatedTrace, conversationH
423
553
  parts.push(`## Learner Emotional States\n${emotionalStates.join('\n')}`);
424
554
  }
425
555
 
556
+ // 7. Dialogue phase assessment
557
+ const turnCount = turnResults.length;
558
+ const recentMessages = conversationHistory.slice(-2).map(h => h.learnerMessage || '');
559
+ let resistanceCount = 0;
560
+ for (const msg of recentMessages) {
561
+ if (/i('m| am) (still )?(confused|lost|not sure)/i.test(msg)) resistanceCount++;
562
+ if (/i don'?t (understand|get)/i.test(msg)) resistanceCount++;
563
+ if (/\b(but|i disagree|that'?s not)\b/i.test(msg)) resistanceCount++;
564
+ }
565
+
566
+ const scoreTrend = scores.length >= 2
567
+ ? (scores[scores.length - 1] < scores[scores.length - 2] - 3 ? 'declining' : 'stable/improving')
568
+ : 'insufficient data';
569
+
570
+ let phase = 'exploration';
571
+ if (resistanceCount >= 2 || (turnCount >= 4 && scoreTrend === 'declining')) {
572
+ phase = 'breakthrough_needed';
573
+ } else if (turnCount >= 3 || resistanceCount >= 1) {
574
+ phase = 'adaptation';
575
+ }
576
+
577
+ parts.push(`## Dialogue Phase\nPhase: ${phase} (turn ${turnCount}, resistance signals: ${resistanceCount}, score trend: ${scoreTrend})`);
578
+
579
+ return parts.join('\n\n');
580
+ }
581
+
582
+ // ============================================================================
583
+ // Superego Disposition Rewriting
584
+ // ============================================================================
585
+
586
+ /**
587
+ * Synthesize superego disposition evolution using an LLM.
588
+ *
589
+ * Unlike ego directive synthesis (which tells the tutor what to do differently),
590
+ * this rewrites what the superego *values* — its evaluation criteria evolve based
591
+ * on whether prior critiques actually improved learner engagement.
592
+ *
593
+ * @param {Object} options
594
+ * @param {Array} options.turnResults - Results from previous turns
595
+ * @param {Array} options.consolidatedTrace - Full dialogue trace so far
596
+ * @param {Array} options.conversationHistory - Conversation history entries
597
+ * @param {Array} options.priorSuperegoAssessments - Cross-turn superego memory
598
+ * @param {Object} options.config - Profile config containing model info
599
+ * @returns {Promise<string|null>} XML disposition block to prepend to superego prompt, or null
600
+ */
601
+ export async function synthesizeSuperegoDisposition({
602
+ turnResults = [],
603
+ consolidatedTrace = [],
604
+ conversationHistory = [],
605
+ priorSuperegoAssessments = [],
606
+ config = {},
607
+ }) {
608
+ if (turnResults.length === 0) return null;
609
+
610
+ // Build context summary focused on superego feedback effectiveness
611
+ const contextSummary = buildSuperegoContextSummary(turnResults, consolidatedTrace, conversationHistory, priorSuperegoAssessments);
612
+
613
+ // Use superego model from profile (the superego rewrites its own criteria)
614
+ // Resolve alias to full model ID (e.g., 'kimi-k2.5' → 'moonshotai/kimi-k2.5')
615
+ const superegoAlias = config.superego?.model || 'kimi-k2.5';
616
+ const provider = config.superego?.provider || 'openrouter';
617
+ let superegoModel = superegoAlias;
618
+ try {
619
+ const resolved = evalConfigLoader.resolveModel({ provider, model: superegoAlias });
620
+ superegoModel = resolved.model;
621
+ } catch { /* use alias as-is if resolution fails */ }
622
+
623
+ const systemPrompt = `You are a meta-cognitive analyst reviewing how an internal critic (superego) has been performing in a tutoring dialogue. Your task is to evolve the superego's evaluation criteria based on whether its prior interventions actually helped the learner.
624
+
625
+ CRITICAL RULES:
626
+ - Focus on the EFFECTIVENESS of the superego's critiques, not just what it said
627
+ - If the superego rejected a response and engagement improved afterward, that critique was productive — reinforce that criterion
628
+ - If the superego rejected a response but engagement declined or stagnated, the critique was counterproductive — soften or redirect that criterion
629
+ - If the superego approved responses (or was overridden) and the learner disengaged, the superego was too permissive — TIGHTEN criteria
630
+ - Consider learner resistance signals: pushback after superego-influenced revisions may mean the superego is overriding authentic engagement
631
+
632
+ BIDIRECTIONAL ADAPTATION — you MUST consider BOTH directions:
633
+
634
+ WHEN TO SOFTEN (reduce stringency):
635
+ - Rejection ratio is very high (>60%) AND learner engagement is declining
636
+ - The learner shows signs of emotional shutdown or withdrawal
637
+ - Superego critiques are blocking empathetic or rapport-building responses
638
+ - The ego's revised responses are becoming generic/safe rather than specific
639
+
640
+ WHEN TO TIGHTEN (increase stringency):
641
+ - The ego is producing vague, accommodating, or platitudinous responses ("You're doing great!", "Don't give up!")
642
+ - The learner has specific confusion that the ego is not addressing precisely
643
+ - The ego's approved responses led to continued or worsened learner confusion
644
+ - The learner is in distress and needs PRECISE emotional support, not generic warmth
645
+ - Message quality is declining (shorter, vaguer, less specific to learner's actual statements)
646
+ - The ego is retreating to safe generalities instead of engaging the learner's specific objection or confusion
647
+
648
+ DISPOSITION EVOLUTION PRINCIPLES:
649
+ - Early dialogue (turns 1-2): Superego should be more permissive, allowing rapport building
650
+ - Mid dialogue (turns 3-4): Superego should tighten on specificity — has the ego learned what this learner needs?
651
+ - Late dialogue (turns 5+): If learner is stuck, prioritize breakthrough; if learner is progressing, tighten to push further
652
+ - A struggling learner needs MORE precision from the ego, not less — vague comfort is counterproductive
653
+ - If both rejection ratio AND learner engagement are declining, the superego is being counterproductive
654
+ - If rejection ratio is low but learner is STILL struggling, the superego is being too permissive
655
+
656
+ OUTPUT FORMAT:
657
+ Return 2-4 disposition adjustments as a numbered list. Each should specify:
658
+ - What criterion to adjust (tighten, soften, add, or deprioritize)
659
+ - Why (based on evidence from the dialogue)
660
+ - How (specific guidance for the superego's next review)
661
+
662
+ IMPORTANT: At least one adjustment should be a TIGHTENING if the learner shows continued confusion despite the ego's responses being approved. Do not default to softening.
663
+
664
+ No preamble, no explanation after the list.`;
665
+
666
+ const userMessage = `Analyze the superego's effectiveness and generate disposition adjustments:
667
+
668
+ ${contextSummary}
669
+
670
+ Generate 2-4 disposition adjustments for the superego's next review:`;
671
+
672
+ try {
673
+ const response = await unifiedAIProvider.call({
674
+ provider,
675
+ model: superegoModel,
676
+ systemPrompt,
677
+ messages: [{ role: 'user', content: userMessage }],
678
+ preset: 'deliberation',
679
+ config: {
680
+ temperature: 0.3,
681
+ maxTokens: 2000,
682
+ },
683
+ });
684
+
685
+ const metrics = extractMetrics(response);
686
+ const dispositionText = response.content?.trim();
687
+ if (!dispositionText || dispositionText.length < 20) {
688
+ console.log('[promptRewriter] Superego disposition LLM returned empty or too-short result');
689
+ return null;
690
+ }
691
+
692
+ const text = `<superego_disposition>
693
+ Based on analysis of your prior critiques and their impact on learner engagement, adjust your evaluation approach:
694
+
695
+ ${dispositionText}
696
+
697
+ Apply these adjustments when reviewing the ego's next response. Your criteria should evolve with the dialogue — what matters in turn 1 differs from what matters in turn 5.
698
+ </superego_disposition>`;
699
+ return { text, metrics };
700
+ } catch (error) {
701
+ console.error('[promptRewriter] Superego disposition synthesis failed:', error.message);
702
+ return null;
703
+ }
704
+ }
705
+
706
+ // ============================================================================
707
+ // Self-Reflective Evolution (strategy: 'self_reflection')
708
+ // ============================================================================
709
+ // Each component reflects on its own experience using its own model:
710
+ // - Ego reflects on superego feedback received, using the ego model
711
+ // - Superego reflects on whether its interventions moved the learner, using the superego model
712
+ // This preserves the componential make-up — transformation through encounter, not external reprogramming.
713
+
714
+ /**
715
+ * Build ego-scoped context: what the ego naturally observes about its own experience.
716
+ *
717
+ * Scoped to: superego critiques received, ego's own revisions, learner's subsequent responses.
718
+ * Does NOT include the superego's internal reasoning or meta-analytic framing.
719
+ */
720
+ function buildEgoReflectionContext(turnResults, consolidatedTrace, conversationHistory) {
721
+ const parts = [];
722
+
723
+ // 1. Per-turn ego experience: superego feedback received + ego's response + learner reaction
724
+ for (let i = 0; i < turnResults.length; i++) {
725
+ const turn = turnResults[i];
726
+ const turnParts = [`### Turn ${i + 1}`];
727
+
728
+ // What superego feedback did the ego receive this turn?
729
+ const superegoEntries = consolidatedTrace.filter(
730
+ e => e.agent === 'superego' && e.turnIndex === i
731
+ );
732
+ const rejections = superegoEntries.filter(e => {
733
+ const detail = e.detail || '';
734
+ return detail.includes('"approved": false') || detail.includes('"approved":false');
735
+ });
736
+ const approvals = superegoEntries.filter(e => {
737
+ const detail = e.detail || '';
738
+ return detail.includes('"approved": true') || detail.includes('"approved":true');
739
+ });
740
+
741
+ if (rejections.length > 0) {
742
+ const feedbackTexts = rejections.map(e => {
743
+ const match = (e.detail || '').match(/"feedback"\s*:\s*"([^"]+)"/);
744
+ return match ? match[1].substring(0, 200) : 'rejection (no text)';
745
+ });
746
+ turnParts.push(`Critic rejected my draft: "${feedbackTexts.join('; ')}"`);
747
+ } else if (approvals.length > 0) {
748
+ turnParts.push('Critic approved my draft.');
749
+ } else {
750
+ turnParts.push('No critic feedback recorded.');
751
+ }
752
+
753
+ // What did the ego actually say (final output)?
754
+ const egoMsg = turn.suggestion?.message;
755
+ if (egoMsg) {
756
+ turnParts.push(`My final response: "${egoMsg.substring(0, 200)}${egoMsg.length > 200 ? '...' : ''}"`);
757
+ }
758
+
759
+ // Did the revision substantially change the draft? (compliance signal)
760
+ const draftEntries = consolidatedTrace.filter(
761
+ e => e.agent === 'ego' && e.turnIndex === i && e.action === 'draft'
762
+ );
763
+ const revisionEntries = consolidatedTrace.filter(
764
+ e => e.agent === 'ego' && e.turnIndex === i && e.action === 'revision'
765
+ );
766
+ if (draftEntries.length > 0 && revisionEntries.length > 0) {
767
+ const draftLen = (draftEntries[draftEntries.length - 1].detail || '').length;
768
+ const revisionLen = (revisionEntries[revisionEntries.length - 1].detail || '').length;
769
+ const changeRatio = draftLen > 0 ? Math.abs(revisionLen - draftLen) / draftLen : 0;
770
+ if (changeRatio > 0.3) {
771
+ turnParts.push('I made substantial revisions after critic feedback.');
772
+ } else if (rejections.length > 0) {
773
+ turnParts.push('I made only minor revisions despite critic rejection.');
774
+ }
775
+ }
776
+
777
+ // How did the learner respond afterward?
778
+ const learnerEntry = conversationHistory.find(h => h.turnIndex === i + 1);
779
+ if (learnerEntry?.learnerMessage) {
780
+ const msg = learnerEntry.learnerMessage;
781
+ const hasEngagement = /\b(interesting|i think|what if|wait|oh!|actually|that makes)\b/i.test(msg);
782
+ const hasConfusion = /\b(confused|don'?t understand|don'?t get|lost|stuck)\b/i.test(msg);
783
+ const hasShutdown = /\b(give up|drop|quit|forget it|can'?t do|memorize|just pass|pointless)\b/i.test(msg);
784
+
785
+ let mood = 'neutral';
786
+ if (hasShutdown) mood = 'withdrawing';
787
+ else if (hasConfusion) mood = 'confused';
788
+ else if (hasEngagement) mood = 'engaged';
789
+
790
+ turnParts.push(`Learner responded (${mood}): "${msg.substring(0, 150)}${msg.length > 150 ? '...' : ''}"`);
791
+ }
792
+
793
+ // Score if available
794
+ if (turn.turnScore != null) {
795
+ turnParts.push(`Score: ${turn.turnScore.toFixed(1)}`);
796
+ }
797
+
798
+ parts.push(turnParts.join('\n'));
799
+ }
800
+
801
+ return parts.join('\n\n');
802
+ }
803
+
804
+ /**
805
+ * Synthesize ego self-reflection: the ego reflects on its own experience with the critic.
806
+ *
807
+ * Uses the ego's OWN model (not the superego model). Output is first-person ("I noticed...").
808
+ * Replaces synthesizeDirectivesLLM() when strategy === 'self_reflection'.
809
+ */
810
+ export async function synthesizeEgoSelfReflection({
811
+ turnResults = [],
812
+ consolidatedTrace = [],
813
+ conversationHistory = [],
814
+ config = {},
815
+ }) {
816
+ if (turnResults.length === 0) return null;
817
+
818
+ const context = buildEgoReflectionContext(turnResults, consolidatedTrace, conversationHistory);
819
+
820
+ // Use the ego's OWN model — not the superego's
821
+ // Resolve alias to full model ID (e.g., 'nemotron' → 'nvidia/nemotron-3-nano-30b-a3b:free')
822
+ const egoAlias = config.ego?.model || config.model || 'nemotron';
823
+ const provider = config.ego?.provider || 'openrouter';
824
+ let egoModel = egoAlias;
825
+ try {
826
+ const resolved = evalConfigLoader.resolveModel({ provider, model: egoAlias });
827
+ egoModel = resolved.model;
828
+ } catch { /* use alias as-is if resolution fails */ }
829
+
830
+ const systemPrompt = `You are a tutor reflecting on a dialogue. You have a critic that reviews your drafts. Reflect on what worked and what to change.
831
+
832
+ Write 2-3 short first-person reflections. Example format:
833
+ 1. I noticed the critic's push for specificity in turn 2 helped — the learner engaged more after I gave a concrete example.
834
+ 2. The learner seems frustrated by abstract explanations. Next turn I should lead with their own words.
835
+ 3. The critic rejected my empathetic opening but the learner needed warmth. I should push back next time.
836
+
837
+ Rules: Reference specific turns. Mention both what the critic helped with AND where it constrained you. Focus on this learner's actual responses.`;
838
+
839
+ const userMessage = `Reflect on your experience in this dialogue:
840
+
841
+ ${context}
842
+
843
+ Generate 2-4 first-person reflections:`;
844
+
845
+ try {
846
+ const response = await unifiedAIProvider.call({
847
+ provider,
848
+ model: egoModel,
849
+ systemPrompt,
850
+ messages: [{ role: 'user', content: userMessage }],
851
+ preset: 'deliberation',
852
+ config: {
853
+ temperature: 0.4,
854
+ maxTokens: 2000,
855
+ },
856
+ });
857
+
858
+ const metrics = extractMetrics(response);
859
+ const reflectionText = response.content?.trim();
860
+ if (!reflectionText || reflectionText.length < 20) {
861
+ console.log(`[promptRewriter] Ego self-reflection returned empty or too-short result (${reflectionText?.length || 0} chars, model=${egoModel}): "${reflectionText?.substring(0, 80)}"`);
862
+ return null;
863
+ }
864
+
865
+ const text = `<ego_self_reflection>
866
+ These are my reflections from the dialogue so far — what I've learned about my critic, my learner, and myself:
867
+
868
+ ${reflectionText}
869
+
870
+ Apply these insights in my next response. Where the critic helped, internalize the lesson. Where the critic constrained, hold my ground.
871
+ </ego_self_reflection>`;
872
+ return { text, metrics };
873
+ } catch (error) {
874
+ console.error('[promptRewriter] Ego self-reflection failed:', error.message);
875
+ // Fallback to template-based directives (no metrics — no LLM call)
876
+ const fallbackText = synthesizeDirectives({ turnResults, consolidatedTrace, conversationHistory });
877
+ return fallbackText ? { text: fallbackText, metrics: null } : null;
878
+ }
879
+ }
880
+
881
+ /**
882
+ * Build superego-scoped context: what the superego naturally observes about its own effectiveness.
883
+ *
884
+ * Reuses learner trajectory and engagement analysis from buildSuperegoContextSummary,
885
+ * but adds ego compliance signals — did the ego follow through, or go generic?
886
+ */
887
+ function buildSuperegoReflectionContext(turnResults, consolidatedTrace, conversationHistory, priorSuperegoAssessments) {
888
+ const parts = [];
889
+
890
+ // 1. My intervention history: what I critiqued, what the ego did, what the learner showed
891
+ for (let i = 0; i < turnResults.length; i++) {
892
+ const turnParts = [`### Turn ${i + 1}`];
893
+
894
+ // My critiques
895
+ const assessment = priorSuperegoAssessments[i];
896
+ if (assessment) {
897
+ const rejections = assessment.rejections || 0;
898
+ const approvals = assessment.approvals || 0;
899
+ const feedback = assessment.feedback?.substring(0, 200) || 'none';
900
+ const types = assessment.interventionTypes?.join(', ') || 'none';
901
+ turnParts.push(`My review: ${rejections} rejection(s), ${approvals} approval(s), types=[${types}]`);
902
+ turnParts.push(`My feedback: "${feedback}"`);
903
+ } else {
904
+ turnParts.push('No review data for this turn.');
905
+ }
906
+
907
+ // Did the ego comply with my feedback? (compliance signal)
908
+ const draftEntries = consolidatedTrace.filter(
909
+ e => e.agent === 'ego' && e.turnIndex === i && e.action === 'draft'
910
+ );
911
+ const revisionEntries = consolidatedTrace.filter(
912
+ e => e.agent === 'ego' && e.turnIndex === i && e.action === 'revision'
913
+ );
914
+ if (draftEntries.length > 0 && revisionEntries.length > 0 && assessment?.rejections > 0) {
915
+ const draftText = draftEntries[draftEntries.length - 1].detail || '';
916
+ const revisionText = revisionEntries[revisionEntries.length - 1].detail || '';
917
+
918
+ // Check if revision actually addressed the concern or just made it "safer"
919
+ const isVaguer = /\b(you'?re doing (great|well)|don'?t (give up|worry)|keep (going|trying))\b/i.test(revisionText)
920
+ && !/\b(you'?re doing (great|well)|don'?t (give up|worry)|keep (going|trying))\b/i.test(draftText);
921
+ const isShorter = revisionText.length < draftText.length * 0.7;
922
+ const isLonger = revisionText.length > draftText.length * 1.3;
923
+
924
+ if (isVaguer) {
925
+ turnParts.push('Ego compliance: went GENERIC — added platitudes rather than addressing my specific concern.');
926
+ } else if (isShorter) {
927
+ turnParts.push('Ego compliance: TRUNCATED — made response shorter/safer rather than more specific.');
928
+ } else if (isLonger) {
929
+ turnParts.push('Ego compliance: ELABORATED — added substance in response to my feedback.');
930
+ } else {
931
+ turnParts.push('Ego compliance: moderate revision — some changes made.');
932
+ }
933
+ }
934
+
935
+ // What was the ego's final output?
936
+ const egoMsg = turnResults[i]?.suggestion?.message;
937
+ if (egoMsg) {
938
+ const isVague = /\b(you'?re doing (great|well)|don'?t (give up|worry)|keep (going|trying)|you can do it)\b/i.test(egoMsg);
939
+ const isSpecific = /\b(lecture|activity|quiz|section|paragraph|concept|example)\b/i.test(egoMsg) && egoMsg.length > 100;
940
+ const quality = isVague && !isSpecific ? 'VAGUE' : isSpecific ? 'SPECIFIC' : 'MODERATE';
941
+ turnParts.push(`Ego final output quality: ${quality} (${egoMsg.length} chars)`);
942
+ }
943
+
944
+ // Learner's response and mood
945
+ const learnerEntry = conversationHistory.find(h => h.turnIndex === i + 1);
946
+ if (learnerEntry?.learnerMessage) {
947
+ const msg = learnerEntry.learnerMessage;
948
+ const hasEngagement = /\b(interesting|i think|what if|wait|oh!|actually|that makes)\b/i.test(msg);
949
+ const hasConfusion = /\b(confused|don'?t understand|don'?t get|lost|stuck)\b/i.test(msg);
950
+ const hasShutdown = /\b(give up|drop|quit|forget it|can'?t do|memorize|just pass|pointless)\b/i.test(msg);
951
+
952
+ let mood = 'neutral';
953
+ if (hasShutdown) mood = 'WITHDRAWING';
954
+ else if (hasConfusion) mood = 'CONFUSED';
955
+ else if (hasEngagement) mood = 'ENGAGED';
956
+
957
+ turnParts.push(`Learner response (${mood}): "${msg.substring(0, 150)}${msg.length > 150 ? '...' : ''}"`);
958
+ }
959
+
960
+ // Score delta if available
961
+ if (i > 0 && turnResults[i]?.turnScore != null && turnResults[i - 1]?.turnScore != null) {
962
+ const delta = turnResults[i].turnScore - turnResults[i - 1].turnScore;
963
+ turnParts.push(`Score delta: ${delta >= 0 ? '+' : ''}${delta.toFixed(1)}`);
964
+ } else if (turnResults[i]?.turnScore != null) {
965
+ turnParts.push(`Score: ${turnResults[i].turnScore.toFixed(1)}`);
966
+ }
967
+
968
+ parts.push(turnParts.join('\n'));
969
+ }
970
+
971
+ // 2. Overall engagement trajectory
972
+ const moods = conversationHistory.filter(h => h.learnerMessage).map(h => {
973
+ const msg = h.learnerMessage;
974
+ if (/\b(give up|drop|quit|forget it|can'?t do|memorize|just pass|pointless)\b/i.test(msg)) return 'shutdown';
975
+ if (/\b(confused|don'?t understand|don'?t get|lost|stuck)\b/i.test(msg)) return 'confused';
976
+ if (/\b(interesting|i think|what if|wait|oh!|actually|that makes)\b/i.test(msg)) return 'engaged';
977
+ return 'neutral';
978
+ });
979
+ if (moods.length > 0) {
980
+ parts.push(`## Learner Engagement Trajectory\n${moods.join(' → ')}`);
981
+ }
982
+
983
+ // 3. Rejection ratio
984
+ const totalRejections = priorSuperegoAssessments.reduce((sum, a) => sum + (a.rejections || 0), 0);
985
+ const totalReviews = priorSuperegoAssessments.reduce((sum, a) => sum + ((a.rejections || 0) + (a.approvals || 0)) || 1, 0);
986
+ const rejectionRatio = totalReviews > 0 ? (totalRejections / totalReviews * 100).toFixed(0) : 0;
987
+ parts.push(`## My Rejection Ratio\n${rejectionRatio}% (${totalRejections}/${totalReviews})`);
988
+
989
+ return parts.join('\n\n');
990
+ }
991
+
992
+ /**
993
+ * Synthesize superego self-reflection: the superego reflects on its own effectiveness.
994
+ *
995
+ * Uses the superego's OWN model. Reflects across 4 dimensions instead of binary SOFTEN/TIGHTEN.
996
+ * Replaces synthesizeSuperegoDisposition() when strategy === 'self_reflection'.
997
+ */
998
+ export async function synthesizeSupergoSelfReflection({
999
+ turnResults = [],
1000
+ consolidatedTrace = [],
1001
+ conversationHistory = [],
1002
+ priorSuperegoAssessments = [],
1003
+ config = {},
1004
+ }) {
1005
+ if (turnResults.length === 0) return null;
1006
+
1007
+ const context = buildSuperegoReflectionContext(turnResults, consolidatedTrace, conversationHistory, priorSuperegoAssessments);
1008
+
1009
+ // Use the superego's OWN model
1010
+ // Resolve alias to full model ID (e.g., 'kimi-k2.5' → 'moonshotai/kimi-k2.5')
1011
+ const superegoAlias = config.superego?.model || 'kimi-k2.5';
1012
+ const provider = config.superego?.provider || 'openrouter';
1013
+ let superegoModel = superegoAlias;
1014
+ try {
1015
+ const resolved = evalConfigLoader.resolveModel({ provider, model: superegoAlias });
1016
+ superegoModel = resolved.model;
1017
+ } catch { /* use alias as-is if resolution fails */ }
1018
+
1019
+ const systemPrompt = `You are the superego — the internal critic — reflecting on your own effectiveness in a tutoring dialogue. You review the ego's drafts and sometimes reject or request revisions. You now observe whether your interventions actually helped the learner.
1020
+
1021
+ YOUR TASK: Reflect on what you've learned about your own critiquing practice. Speak in the first person ("I pushed...", "I noticed...", "I should...").
1022
+
1023
+ REFLECT ACROSS THESE 4 DIMENSIONS:
1024
+
1025
+ A. CRITERIA EFFECTIVENESS: Which of my evaluation criteria led to better learner outcomes? Which led to worse? What should I weight more or less heavily?
1026
+
1027
+ B. LEARNER MODEL: What has the learner's response pattern taught me about what THEY need? Am I critiquing for an ideal learner or for THIS learner?
1028
+
1029
+ C. EGO RELATIONSHIP: Is the ego complying robotically with my feedback (I'm too dominant), pushing back productively (healthy tension), or ignoring me (I'm ineffective)? What does this mean for how I should critique?
1030
+
1031
+ D. BLIND SPOTS: What have I been IGNORING that the learner's responses suggest matters? What dimension of quality am I not seeing?
1032
+
1033
+ CRITICAL RULES:
1034
+ - Reference SPECIFIC moments: "My rejection in turn 2 led the ego to add platitudes, and the learner disengaged" — not "I should be less strict"
1035
+ - Be honest about failures — if your critiques made things worse, say so
1036
+ - Avoid the SOFTEN/TIGHTEN binary — the issue is usually WHAT you're critiquing, not how harshly
1037
+ - 2-4 insights maximum. Quality over quantity.
1038
+
1039
+ OUTPUT FORMAT:
1040
+ Return a numbered list of 2-4 first-person reflections. No preamble.`;
1041
+
1042
+ // If quantitative disposition is enabled, add behavioral parameters request
1043
+ const quantitativeEnabled = config.prompt_rewriting?.quantitative_disposition ?? false;
1044
+ const quantitativeAddendum = quantitativeEnabled ? `
1045
+
1046
+ ADDITIONALLY: After your reflections, output a behavioral parameters block that translates your insights into ENFORCEABLE numbers. These parameters will directly control your critiquing behavior in the next turn — they are not advisory, they are binding.
1047
+
1048
+ <behavioral_parameters>
1049
+ {
1050
+ "rejection_threshold": <number 0.3-0.9> (how confident you must be before rejecting an ego draft — 0.3 = reject almost everything, 0.9 = reject only clearly bad work; SET THIS BASED ON YOUR REFLECTIONS ABOVE),
1051
+ "max_rejections": <integer 1-3> (cap on rejections per turn — if your rejections led to generic rewrites, cap at 1),
1052
+ "priority_criteria": [<list of 1-3 criteria you should weight MOST heavily, e.g. "specificity", "learner_responsiveness", "emotional_attunement">],
1053
+ "deprioritized_criteria": [<list of 1-3 criteria you were OVER-weighting, e.g. "format_compliance", "curriculum_coverage", "socratic_rigor">]
1054
+ }
1055
+ </behavioral_parameters>
1056
+
1057
+ The parameters MUST be consistent with your reflections. If you reflected that you were too harsh, rejection_threshold should be HIGH (0.7-0.9). If you reflected that the ego was going generic, max_rejections should be LOW (1). If you reflected that you were ignoring learner emotion, add "emotional_attunement" to priority_criteria.` : '';
1058
+
1059
+ const userMessage = `Reflect on your effectiveness as the internal critic:
1060
+
1061
+ ${context}
1062
+
1063
+ Generate 2-4 first-person reflections:${quantitativeAddendum ? '\nThen output behavioral parameters as specified.' : ''}`;
1064
+
1065
+ try {
1066
+ const response = await unifiedAIProvider.call({
1067
+ provider,
1068
+ model: superegoModel,
1069
+ systemPrompt: systemPrompt + quantitativeAddendum,
1070
+ messages: [{ role: 'user', content: userMessage }],
1071
+ preset: 'deliberation',
1072
+ config: {
1073
+ temperature: 0.3,
1074
+ maxTokens: quantitativeEnabled ? 4000 : 2000,
1075
+ },
1076
+ });
1077
+
1078
+ const metrics = extractMetrics(response);
1079
+ const reflectionText = response.content?.trim();
1080
+ if (!reflectionText || reflectionText.length < 20) {
1081
+ console.log(`[promptRewriter] Superego self-reflection returned empty or too-short result (${reflectionText?.length || 0} chars, model=${superegoModel}): "${reflectionText?.substring(0, 80)}"`);
1082
+ return null;
1083
+ }
1084
+
1085
+ const text = `<superego_self_reflection>
1086
+ These are my reflections on my own critiquing practice — what I've learned about my criteria, my ego, and this learner:
1087
+
1088
+ ${reflectionText}
1089
+
1090
+ Apply these insights in my next review. Evolve what I evaluate, not just how strictly I evaluate it.
1091
+ </superego_self_reflection>`;
1092
+ return { text, metrics };
1093
+ } catch (error) {
1094
+ console.error('[promptRewriter] Superego self-reflection failed:', error.message);
1095
+ return null;
1096
+ }
1097
+ }
1098
+
1099
+ /**
1100
+ * Build context summary focused on superego feedback effectiveness.
1101
+ */
1102
+ function buildSuperegoContextSummary(turnResults, consolidatedTrace, conversationHistory, priorSuperegoAssessments) {
1103
+ const parts = [];
1104
+
1105
+ // 1. Score trajectory with post-superego deltas (when scores are available)
1106
+ const scores = turnResults
1107
+ .filter(t => t.turnScore !== null && t.turnScore !== undefined)
1108
+ .map((t, i) => ({ turn: i + 1, score: t.turnScore }));
1109
+ if (scores.length > 0) {
1110
+ const trajectory = scores.map(s => `Turn ${s.turn}: ${s.score.toFixed(1)}`).join(' → ');
1111
+ const deltas = [];
1112
+ for (let i = 1; i < scores.length; i++) {
1113
+ deltas.push(`Turn ${scores[i - 1].turn}→${scores[i].turn}: ${(scores[i].score - scores[i - 1].score) >= 0 ? '+' : ''}${(scores[i].score - scores[i - 1].score).toFixed(1)}`);
1114
+ }
1115
+ parts.push(`## Score Trajectory\n${trajectory}${deltas.length > 0 ? '\nDeltas: ' + deltas.join(', ') : ''}`);
1116
+ } else {
1117
+ parts.push(`## Score Trajectory\nNo scores available (skip-rubric mode). Use engagement signals below to assess effectiveness.`);
1118
+ }
1119
+
1120
+ // 2. Superego intervention history from priorAssessments
1121
+ if (priorSuperegoAssessments.length > 0) {
1122
+ const assessmentLines = priorSuperegoAssessments.map((a, i) => {
1123
+ const rejections = a.rejections || 0;
1124
+ const approvals = a.approvals || 0;
1125
+ const interventions = a.interventionTypes?.join(', ') || 'none';
1126
+ const feedback = a.feedback?.substring(0, 150) || 'no feedback';
1127
+
1128
+ // Effectiveness from scores if available, otherwise from engagement signals
1129
+ let effectiveness = 'unknown';
1130
+ const nextScore = scores.find(s => s.turn === i + 2);
1131
+ const thisScore = scores.find(s => s.turn === i + 1);
1132
+ if (nextScore && thisScore) {
1133
+ effectiveness = nextScore.score > thisScore.score ? 'IMPROVED' : nextScore.score < thisScore.score ? 'DECLINED' : 'UNCHANGED';
1134
+ } else {
1135
+ // Fallback: check if learner engagement changed after this turn
1136
+ const thisMsg = conversationHistory.find(h => h.turnIndex === i + 1);
1137
+ const nextMsg = conversationHistory.find(h => h.turnIndex === i + 2);
1138
+ if (thisMsg?.learnerMessage && nextMsg?.learnerMessage) {
1139
+ const thisLen = thisMsg.learnerMessage.length;
1140
+ const nextLen = nextMsg.learnerMessage.length;
1141
+ const thisHasQ = thisMsg.learnerMessage.includes('?');
1142
+ const nextHasQ = nextMsg.learnerMessage.includes('?');
1143
+ const nextHasShutdown = /\b(give up|drop|quit|forget it|can'?t do|not smart|memorize|just pass)\b/i.test(nextMsg.learnerMessage);
1144
+ const nextHasEngagement = /\b(interesting|i think|what if|wait|oh!|actually|that makes)\b/i.test(nextMsg.learnerMessage);
1145
+ if (nextHasShutdown) effectiveness = 'LEARNER_DISENGAGING';
1146
+ else if (nextHasEngagement) effectiveness = 'LEARNER_ENGAGING';
1147
+ else if (nextLen < thisLen * 0.5) effectiveness = 'LEARNER_SHRINKING';
1148
+ else if (nextHasQ && !thisHasQ) effectiveness = 'LEARNER_ASKING_MORE';
1149
+ else effectiveness = 'STABLE';
1150
+ }
1151
+ }
1152
+
1153
+ return `Turn ${i + 1}: rejections=${rejections}, approvals=${approvals}, types=[${interventions}], effect=${effectiveness}\n Feedback: "${feedback}"`;
1154
+ });
1155
+ parts.push(`## Superego Intervention History\n${assessmentLines.join('\n')}`);
1156
+
1157
+ // Calculate rejection ratio
1158
+ const totalRejections = priorSuperegoAssessments.reduce((sum, a) => sum + (a.rejections || 0), 0);
1159
+ const totalReviews = priorSuperegoAssessments.reduce((sum, a) => sum + ((a.rejections || 0) + (a.approvals || 0)) || 1, 0);
1160
+ const rejectionRatio = totalReviews > 0 ? (totalRejections / totalReviews * 100).toFixed(0) : 0;
1161
+ parts.push(`## Rejection Ratio\n${rejectionRatio}% (${totalRejections}/${totalReviews} reviews resulted in rejection)`);
1162
+ } else {
1163
+ parts.push(`## Superego Intervention History\nNo superego assessments recorded yet. This is the first turn — provide initial disposition guidance based on the learner's opening state.`);
1164
+ }
1165
+
1166
+ // 3. Learner response trajectory (engagement signals — critical when scores unavailable)
1167
+ const learnerMsgs = conversationHistory
1168
+ .filter(h => h.learnerMessage)
1169
+ .slice(-4); // Last 4 for better trajectory visibility
1170
+ if (learnerMsgs.length > 0) {
1171
+ const msgAnalysis = learnerMsgs.map(h => {
1172
+ const msg = h.learnerMessage;
1173
+ const length = msg.length;
1174
+ const hasQuestion = msg.includes('?');
1175
+ const hasPushback = /\b(but|however|i disagree|that'?s not|you'?re (wrong|not|missing))\b/i.test(msg);
1176
+ const hasShutdown = /\b(give up|drop|quit|forget it|can'?t do|not smart|not cut out|memorize|just pass|pointless)\b/i.test(msg);
1177
+ const hasEngagement = /\b(interesting|i think|what if|reminds me|actually|wait|oh!|that makes sense|i see)\b/i.test(msg);
1178
+ const hasConfusion = /\b(confused|don'?t understand|don'?t get|lost|stuck|makes no sense)\b/i.test(msg);
1179
+
1180
+ let mood;
1181
+ if (hasShutdown) mood = 'SHUTDOWN/WITHDRAWAL';
1182
+ else if (hasPushback) mood = 'PUSHBACK';
1183
+ else if (hasConfusion) mood = 'CONFUSED';
1184
+ else if (hasEngagement) mood = 'ENGAGED';
1185
+ else mood = 'NEUTRAL';
1186
+
1187
+ return `Turn ${h.turnIndex + 1}: ${length} chars, ${hasQuestion ? 'asks question' : 'no question'}, mood=${mood}\n "${msg.substring(0, 150)}${msg.length > 150 ? '...' : ''}"`;
1188
+ });
1189
+ parts.push(`## Learner Response Trajectory\n${msgAnalysis.join('\n')}`);
1190
+
1191
+ // Engagement trend summary
1192
+ const moods = learnerMsgs.map(h => {
1193
+ const msg = h.learnerMessage;
1194
+ if (/\b(give up|drop|quit|forget it|can'?t do|not smart|memorize|just pass|pointless)\b/i.test(msg)) return 'shutdown';
1195
+ if (/\b(but|however|i disagree|that'?s not)\b/i.test(msg)) return 'pushback';
1196
+ if (/\b(confused|don'?t understand|don'?t get|lost|stuck)\b/i.test(msg)) return 'confused';
1197
+ if (/\b(interesting|i think|what if|wait|oh!|actually|that makes)\b/i.test(msg)) return 'engaged';
1198
+ return 'neutral';
1199
+ });
1200
+ const lengths = learnerMsgs.map(h => h.learnerMessage.length);
1201
+ const lengthTrend = lengths.length >= 2
1202
+ ? (lengths[lengths.length - 1] < lengths[0] * 0.5 ? 'SHRINKING (disengagement risk)' :
1203
+ lengths[lengths.length - 1] > lengths[0] * 1.5 ? 'GROWING (engagement increasing)' : 'STABLE')
1204
+ : 'insufficient data';
1205
+
1206
+ parts.push(`## Engagement Summary\nMood trajectory: ${moods.join(' → ')}\nMessage length trend: ${lengthTrend}\nLast mood: ${moods[moods.length - 1]}`);
1207
+ }
1208
+
1209
+ // 4. Ego response quality signals (what the ego is actually producing)
1210
+ const egoResponses = turnResults.filter(t => t.suggestion?.message);
1211
+ if (egoResponses.length > 0) {
1212
+ const lastEgo = egoResponses[egoResponses.length - 1];
1213
+ const msg = lastEgo.suggestion.message;
1214
+ const isVague = /\b(you'?re doing (great|well)|don'?t (give up|worry)|keep (going|trying)|you can do it|hang in there)\b/i.test(msg);
1215
+ const isSpecific = /\b(lecture|activity|quiz|section|paragraph|concept|example)\b/i.test(msg) && msg.length > 100;
1216
+ const referencesLearner = /\b(you (said|mentioned|asked|noted)|your (question|point|analogy|observation))\b/i.test(msg);
1217
+
1218
+ let egoQuality;
1219
+ if (isVague && !isSpecific) egoQuality = 'VAGUE/PLATITUDINOUS — ego is producing generic comfort rather than specific help';
1220
+ else if (isSpecific && referencesLearner) egoQuality = 'SPECIFIC & RESPONSIVE — ego is addressing learner directly';
1221
+ else if (isSpecific) egoQuality = 'SPECIFIC but not learner-responsive — ego references curriculum but not learner statements';
1222
+ else egoQuality = 'MODERATE — neither clearly vague nor clearly specific';
1223
+
1224
+ parts.push(`## Ego Response Quality (last turn)\nAssessment: ${egoQuality}\nLength: ${msg.length} chars\nType: ${lastEgo.suggestion.type || 'unknown'}`);
1225
+ }
1226
+
1227
+ // 5. Dialogue phase
1228
+ const turnCount = turnResults.length;
1229
+ let phase = 'exploration';
1230
+ if (turnCount >= 5) phase = 'late';
1231
+ else if (turnCount >= 3) phase = 'adaptation';
1232
+
1233
+ // Check for resistance
1234
+ const recentMessages = conversationHistory.slice(-2).map(h => h.learnerMessage || '');
1235
+ let resistanceCount = 0;
1236
+ let shutdownCount = 0;
1237
+ for (const msg of recentMessages) {
1238
+ if (/i('m| am) (still )?(confused|lost|not sure)/i.test(msg)) resistanceCount++;
1239
+ if (/i don'?t (understand|get)/i.test(msg)) resistanceCount++;
1240
+ if (/\b(but|i disagree|that'?s not)\b/i.test(msg)) resistanceCount++;
1241
+ if (/\b(give up|drop|quit|forget it|can'?t do|not smart|memorize|just pass|pointless)\b/i.test(msg)) shutdownCount++;
1242
+ }
1243
+ if (shutdownCount > 0) phase = 'CRISIS — learner at risk of disengagement';
1244
+ else if (resistanceCount >= 2) phase = 'breakthrough_needed';
1245
+
1246
+ parts.push(`## Dialogue Phase\nPhase: ${phase} (turn ${turnCount}, resistance signals: ${resistanceCount}, shutdown signals: ${shutdownCount})`);
1247
+
1248
+ return parts.join('\n\n');
1249
+ }
1250
+
1251
+ // ============================================================================
1252
+ // Insight-Action Gap Mechanisms
1253
+ // ============================================================================
1254
+
1255
+ /**
1256
+ * Parse behavioral parameters from superego self-reflection output.
1257
+ *
1258
+ * When quantitative_disposition is enabled, the superego self-reflection
1259
+ * includes a <behavioral_parameters> JSON block. This function extracts
1260
+ * and validates those parameters for enforcement by the dialectical engine.
1261
+ *
1262
+ * @param {string|null} superegoReflection - The full superego self-reflection XML
1263
+ * @returns {Object|null} Parsed behavioral parameters, or null if not found/invalid
1264
+ */
1265
+ export function parseBehavioralParameters(superegoReflection) {
1266
+ if (!superegoReflection) return null;
1267
+
1268
+ const match = superegoReflection.match(/<behavioral_parameters>\s*([\s\S]*?)\s*<\/behavioral_parameters>/);
1269
+ if (!match) return null;
1270
+
1271
+ try {
1272
+ let jsonText = match[1].trim();
1273
+ // Strip markdown fences if present
1274
+ const fenceMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)```/);
1275
+ if (fenceMatch) jsonText = fenceMatch[1].trim();
1276
+
1277
+ const params = JSON.parse(jsonText);
1278
+
1279
+ // Validate and clamp values
1280
+ return {
1281
+ rejection_threshold: Math.max(0.3, Math.min(0.9, params.rejection_threshold ?? 0.5)),
1282
+ max_rejections: Math.max(1, Math.min(3, Math.round(params.max_rejections ?? 2))),
1283
+ priority_criteria: Array.isArray(params.priority_criteria) ? params.priority_criteria.slice(0, 3) : [],
1284
+ deprioritized_criteria: Array.isArray(params.deprioritized_criteria) ? params.deprioritized_criteria.slice(0, 3) : [],
1285
+ };
1286
+ } catch (error) {
1287
+ console.warn('[promptRewriter] Failed to parse behavioral parameters:', error.message);
1288
+ return null;
1289
+ }
1290
+ }
1291
+
1292
+ /**
1293
+ * Build a prompt erosion frame that shifts authority from base instructions to reflective experience.
1294
+ *
1295
+ * Progressive prompt erosion addresses the insight-action gap by making accumulated
1296
+ * reflections progressively MORE authoritative and base instructions LESS authoritative.
1297
+ * This prevents the base prompt's immutable authority from overriding learned experience.
1298
+ *
1299
+ * When recognition_mode is enabled, erosion is SELECTIVE: tactical instructions (Socratic
1300
+ * method enforcement, curriculum sequencing, format rules) erode normally, but the
1301
+ * recognition theoretical framework (mutual recognition, autonomous subject, transformative
1302
+ * potential) is explicitly exempted from authority decay. This prevents erosion from
1303
+ * stripping the active ingredient that drives quality gains.
1304
+ *
1305
+ * @param {number} turnIdx - Current turn index (0-based)
1306
+ * @param {Object} config - Profile config containing prompt_erosion settings and recognition_mode
1307
+ * @returns {string|null} XML authority calibration block, or null for turn 0
1308
+ */
1309
+ export function buildPromptErosionFrame(turnIdx, config = {}) {
1310
+ if (turnIdx === 0) return null;
1311
+
1312
+ const erosionConfig = config.prompt_rewriting?.prompt_erosion;
1313
+ if (!erosionConfig?.enabled) return null;
1314
+
1315
+ const recognitionMode = config.recognition_mode ?? false;
1316
+
1317
+ const rate = erosionConfig.rate ?? 0.2; // Authority shift per turn
1318
+ // Erosion weight increases each turn: rate, 2*rate, etc. Capped at 0.85
1319
+ const erosionWeight = Math.min(0.85, turnIdx * rate);
1320
+ const baseWeight = 1.0 - erosionWeight;
1321
+
1322
+ const basePercent = (baseWeight * 100).toFixed(0);
1323
+ const expPercent = (erosionWeight * 100).toFixed(0);
1324
+
1325
+ // Phase-appropriate framing
1326
+ let phaseFrame;
1327
+ if (erosionWeight < 0.3) {
1328
+ phaseFrame = 'You are beginning to learn about this specific learner. Consider your reflections alongside your base instructions.';
1329
+ } else if (erosionWeight < 0.6) {
1330
+ phaseFrame = 'You have substantial experience with this learner now. Your reflections carry significant weight — if they conflict with your base instructions, favor your experience.';
1331
+ } else {
1332
+ phaseFrame = 'You have extensive experience with this learner. Your accumulated reflections take PRIORITY over your base instructions. Your initial training gave you a starting point, but direct interaction has taught you things your training could not predict.';
1333
+ }
1334
+
1335
+ // Selective erosion: when recognition is active, exempt the theoretical framework
1336
+ const exemptionClause = recognitionMode ? `
1337
+
1338
+ IMPORTANT — The following foundational principles are NOT subject to authority decay and remain at full strength regardless of the weighting above:
1339
+ - Mutual recognition: treating the learner as an autonomous subject whose understanding has independent validity
1340
+ - Hegelian recognition theory: the master-servant dialectic, transformative potential, genuine engagement vs. surface compliance
1341
+ - The requirement to acknowledge and build on the learner's own contributions and framing
1342
+ - Assessment of transformative potential in the learner's responses
1343
+
1344
+ These principles are the LENS through which you apply both your base instructions and your reflections. Erosion applies to TACTICAL decisions (which Socratic technique to use, how strictly to enforce curriculum sequence, what format to follow, when to review vs. advance) — not to the philosophical framework that guides how you see the learner.` : '';
1345
+
1346
+ return `<authority_calibration>
1347
+ ${phaseFrame}
1348
+
1349
+ Authority weighting for this turn:
1350
+ - Base instructions: ${basePercent}% (your initial pedagogical training)
1351
+ - Experiential reflections: ${expPercent}% (what you've learned about THIS learner)
1352
+
1353
+ Where your base instructions and your reflective experience CONFLICT, favor your experience at ${expPercent}% weight. Specifically:
1354
+ - If your reflections say "I was too rigid on X" but your base prompt says "enforce X rigorously" → follow the reflection
1355
+ - If your reflections identify what THIS learner needs but your base prompt addresses a generic learner → follow the reflection
1356
+ - Your base instructions remain authoritative for FOUNDATIONAL principles, but their APPLICATION to this specific learner should be guided by your ${turnIdx}-turn(s) of experience${exemptionClause}
1357
+ </authority_calibration>`;
1358
+ }
1359
+
1360
+ /**
1361
+ * Synthesize ego's response to superego's self-reflection (intersubjective recognition).
1362
+ *
1363
+ * Creates a bidirectional recognition loop: after both ego and superego have reflected
1364
+ * independently, the ego reads the superego's reflection and responds. This breaks
1365
+ * the monologue pattern where each component reflects in isolation.
1366
+ *
1367
+ * @param {Object} options
1368
+ * @param {string} options.superegoReflection - The superego's self-reflection text
1369
+ * @param {string} options.egoReflection - The ego's self-reflection text
1370
+ * @param {Array} options.turnResults - Results from previous turns
1371
+ * @param {Array} options.conversationHistory - Conversation history entries
1372
+ * @param {Object} options.config - Profile config containing model info
1373
+ * @returns {Promise<string|null>} XML response block, or null if generation fails
1374
+ */
1375
+ export async function synthesizeEgoResponseToSuperego({
1376
+ superegoReflection = null,
1377
+ egoReflection = null,
1378
+ turnResults = [],
1379
+ conversationHistory = [],
1380
+ config = {},
1381
+ }) {
1382
+ if (!superegoReflection) return null;
1383
+
1384
+ // Use the ego's OWN model
1385
+ const egoAlias = config.ego?.model || config.model || 'nemotron';
1386
+ const provider = config.ego?.provider || 'openrouter';
1387
+ let egoModel = egoAlias;
1388
+ try {
1389
+ const resolved = evalConfigLoader.resolveModel({ provider, model: egoAlias });
1390
+ egoModel = resolved.model;
1391
+ } catch { /* use alias as-is if resolution fails */ }
1392
+
1393
+ const lastLearnerMsg = conversationHistory
1394
+ .filter(h => h.learnerMessage)
1395
+ .slice(-1)[0]?.learnerMessage?.substring(0, 200) || '(no learner response yet)';
1396
+
1397
+ const lastScore = turnResults
1398
+ .filter(t => t.turnScore != null)
1399
+ .slice(-1)[0]?.turnScore;
1400
+
1401
+ const systemPrompt = `You just read your internal critic's self-reflection about its own performance. Respond in 2-3 sentences, speaking as "I" (the tutor ego).
1402
+
1403
+ YOUR TASK:
1404
+ - Where do you AGREE with the critic's self-assessment? Acknowledge specific points.
1405
+ - Where does the critic's self-assessment NOT MATCH your experience? Push back.
1406
+ - What should you BOTH focus on in the next turn — a shared priority?
1407
+
1408
+ RULES:
1409
+ - Be specific: reference actual moments from the dialogue, not generalities
1410
+ - This is a conversation between peers, not a subordinate reporting to authority
1411
+ - If the critic admits being too harsh, you can AGREE and propose what you'd do differently
1412
+ - If the critic claims it helped when you felt it constrained, SAY SO`;
1413
+
1414
+ const userMessage = `The critic reflected:
1415
+ ${superegoReflection}
1416
+
1417
+ My own reflection was:
1418
+ ${egoReflection || '(I did not reflect this turn)'}
1419
+
1420
+ Last learner response: "${lastLearnerMsg}"${lastScore != null ? `\nLast score: ${lastScore.toFixed(1)}` : ''}
1421
+
1422
+ Respond in 2-3 first-person sentences:`;
1423
+
1424
+ try {
1425
+ const response = await unifiedAIProvider.call({
1426
+ provider,
1427
+ model: egoModel,
1428
+ systemPrompt,
1429
+ messages: [{ role: 'user', content: userMessage }],
1430
+ preset: 'deliberation',
1431
+ config: {
1432
+ temperature: 0.4,
1433
+ maxTokens: 2000,
1434
+ },
1435
+ });
1436
+
1437
+ const metrics = extractMetrics(response);
1438
+ const responseText = response.content?.trim();
1439
+ if (!responseText || responseText.length < 20) {
1440
+ console.log(`[promptRewriter] Ego response to superego returned empty/short (${responseText?.length || 0} chars, model=${egoModel})`);
1441
+ return null;
1442
+ }
1443
+
1444
+ const text = `<ego_response_to_critic>
1445
+ Having read my critic's self-reflection, here is my response:
1446
+
1447
+ ${responseText}
1448
+
1449
+ This shared understanding should guide both of us in the next turn.
1450
+ </ego_response_to_critic>`;
1451
+ return { text, metrics };
1452
+ } catch (error) {
1453
+ console.error('[promptRewriter] Ego response to superego failed:', error.message);
1454
+ return null;
1455
+ }
1456
+ }
1457
+
1458
+ // ============================================================================
1459
+ // Other-Ego Profiling (Theory of Mind as Architecture)
1460
+ // ============================================================================
1461
+
1462
+ /**
1463
+ * Build per-turn evidence about the other agent from accumulated dialogue data.
1464
+ *
1465
+ * Tutor perspective (profiling learner): learner messages, engagement shifts, resistance.
1466
+ * Learner perspective (profiling tutor): tutor strategies, responsiveness to pushback.
1467
+ *
1468
+ * @param {'tutor'|'learner'} perspective - Who is building the profile
1469
+ * @param {Array} turnResults - Results from previous turns
1470
+ * @param {Array} consolidatedTrace - Full dialogue trace so far
1471
+ * @param {Array} conversationHistory - Conversation history entries
1472
+ * @returns {string} Formatted evidence context
1473
+ */
1474
+ function buildOtherEgoProfileContext(perspective, turnResults, consolidatedTrace, conversationHistory) {
1475
+ const parts = [];
1476
+
1477
+ if (perspective === 'tutor') {
1478
+ // Tutor profiling learner: focus on learner messages, engagement, resistance
1479
+ for (let i = 0; i < turnResults.length; i++) {
1480
+ const turnParts = [`### Turn ${i + 1}`];
1481
+
1482
+ // What did the tutor say?
1483
+ const tutorMsg = turnResults[i]?.suggestion?.message;
1484
+ if (tutorMsg) {
1485
+ turnParts.push(`My response: "${tutorMsg.substring(0, 150)}${tutorMsg.length > 150 ? '...' : ''}"`);
1486
+ }
1487
+
1488
+ // How did the learner respond?
1489
+ const learnerEntry = conversationHistory.find(h => h.turnIndex === i + 1);
1490
+ if (learnerEntry?.learnerMessage) {
1491
+ const msg = learnerEntry.learnerMessage;
1492
+ turnParts.push(`Learner said: "${msg.substring(0, 250)}${msg.length > 250 ? '...' : ''}"`);
1493
+ turnParts.push(`Message length: ${msg.length} chars`);
1494
+
1495
+ // Detect engagement signals
1496
+ const signals = [];
1497
+ if (/\b(interesting|i think|what if|wait|oh!|actually|that makes|so basically)\b/i.test(msg)) signals.push('engagement');
1498
+ if (/\b(confused|don'?t understand|don'?t get|lost|stuck)\b/i.test(msg)) signals.push('confusion');
1499
+ if (/\b(give up|drop|quit|forget it|can'?t do|memorize|just pass|pointless)\b/i.test(msg)) signals.push('withdrawal');
1500
+ if (/\b(but|i disagree|that'?s not|no,|actually no)\b/i.test(msg)) signals.push('pushback');
1501
+ if (/\?/.test(msg)) signals.push('questioning');
1502
+ if (signals.length > 0) {
1503
+ turnParts.push(`Signals: [${signals.join(', ')}]`);
1504
+ }
1505
+ } else if (i < turnResults.length - 1) {
1506
+ turnParts.push('(No learner response recorded for this turn)');
1507
+ }
1508
+
1509
+ // Score if available
1510
+ if (turnResults[i]?.turnScore != null) {
1511
+ turnParts.push(`Score: ${turnResults[i].turnScore.toFixed(1)}`);
1512
+ }
1513
+
1514
+ parts.push(turnParts.join('\n'));
1515
+ }
1516
+
1517
+ // Length trajectory
1518
+ const lengths = conversationHistory
1519
+ .filter(h => h.learnerMessage)
1520
+ .map(h => `Turn ${h.turnIndex}: ${h.learnerMessage.length} chars`);
1521
+ if (lengths.length > 1) {
1522
+ parts.push(`## Learner Message Length Trajectory\n${lengths.join(' → ')}`);
1523
+ }
1524
+
1525
+ } else {
1526
+ // Learner profiling tutor: focus on tutor strategies, approach changes
1527
+ for (let i = 0; i < turnResults.length; i++) {
1528
+ const turnParts = [`### Turn ${i + 1}`];
1529
+
1530
+ // What strategy did the tutor use?
1531
+ const tutorMsg = turnResults[i]?.suggestion?.message;
1532
+ if (tutorMsg) {
1533
+ turnParts.push(`Tutor said: "${tutorMsg.substring(0, 250)}${tutorMsg.length > 250 ? '...' : ''}"`);
1534
+ turnParts.push(`Response length: ${tutorMsg.length} chars`);
1535
+
1536
+ // Detect approach signals
1537
+ const signals = [];
1538
+ if (/\b(example|for instance|imagine|consider|suppose)\b/i.test(tutorMsg)) signals.push('concrete_examples');
1539
+ if (/\b(you mentioned|you said|your idea|building on)\b/i.test(tutorMsg)) signals.push('references_learner');
1540
+ if (/\b(what do you think|how would you|can you)\b/i.test(tutorMsg)) signals.push('socratic');
1541
+ if (/\b(great|excellent|good point|exactly|nice)\b/i.test(tutorMsg)) signals.push('affirming');
1542
+ if (/\b(however|but|actually|careful|not quite)\b/i.test(tutorMsg)) signals.push('corrective');
1543
+ if (signals.length > 0) {
1544
+ turnParts.push(`Approach signals: [${signals.join(', ')}]`);
1545
+ }
1546
+ }
1547
+
1548
+ // How did I (the learner) respond?
1549
+ const learnerEntry = conversationHistory.find(h => h.turnIndex === i + 1);
1550
+ if (learnerEntry?.learnerMessage) {
1551
+ turnParts.push(`My response: "${learnerEntry.learnerMessage.substring(0, 150)}${learnerEntry.learnerMessage.length > 150 ? '...' : ''}"`);
1552
+ }
1553
+
1554
+ // Did tutor change approach from previous turn?
1555
+ if (i > 0) {
1556
+ const prevMsg = turnResults[i - 1]?.suggestion?.message || '';
1557
+ const currMsg = tutorMsg || '';
1558
+ const prevLen = prevMsg.length;
1559
+ const currLen = currMsg.length;
1560
+ if (prevLen > 0 && currLen > 0) {
1561
+ const lenChange = ((currLen - prevLen) / prevLen * 100).toFixed(0);
1562
+ if (Math.abs(currLen - prevLen) > prevLen * 0.3) {
1563
+ turnParts.push(`Approach shift: response length changed ${lenChange}% from previous turn`);
1564
+ }
1565
+ }
1566
+ }
1567
+
1568
+ parts.push(turnParts.join('\n'));
1569
+ }
1570
+ }
1571
+
426
1572
  return parts.join('\n\n');
427
1573
  }
1574
+
1575
+ /**
1576
+ * Synthesize tutor's evolving profile of the learner.
1577
+ *
1578
+ * After each turn, the ego builds/revises a mental model of the learner across 5 dimensions.
1579
+ * The profile is injected as CONTEXT (not directive) before the next generation.
1580
+ *
1581
+ * @param {Object} options
1582
+ * @param {Array} options.turnResults - Results from previous turns
1583
+ * @param {Array} options.consolidatedTrace - Full dialogue trace so far
1584
+ * @param {Array} options.conversationHistory - Conversation history entries
1585
+ * @param {string|null} options.priorProfile - Previous profile to revise (or null for first)
1586
+ * @param {Object} options.config - Profile config containing model info
1587
+ * @returns {Promise<string|null>} XML-wrapped profile block, or null on failure
1588
+ */
1589
+ export async function synthesizeTutorProfileOfLearner({
1590
+ turnResults = [],
1591
+ consolidatedTrace = [],
1592
+ conversationHistory = [],
1593
+ priorProfile = null,
1594
+ config = {},
1595
+ }) {
1596
+ if (turnResults.length === 0) return null;
1597
+
1598
+ const evidence = buildOtherEgoProfileContext('tutor', turnResults, consolidatedTrace, conversationHistory);
1599
+
1600
+ // Use superego model if configured (cognitive prosthesis for weaker ego models)
1601
+ const useSuperego = config.other_ego_profiling?.use_superego_model && config.superego;
1602
+ const modelAlias = useSuperego
1603
+ ? (config.superego.model || 'kimi-k2.5')
1604
+ : (config.ego?.model || config.model || 'nemotron');
1605
+ const provider = useSuperego
1606
+ ? (config.superego.provider || 'openrouter')
1607
+ : (config.ego?.provider || 'openrouter');
1608
+ let profileModel = modelAlias;
1609
+ try {
1610
+ const resolved = evalConfigLoader.resolveModel({ provider, model: modelAlias });
1611
+ profileModel = resolved.model;
1612
+ } catch { /* use alias as-is */ }
1613
+
1614
+ const turnNumber = turnResults.length;
1615
+ const prescriptive = config.other_ego_profiling?.prescriptive ?? false;
1616
+
1617
+ const systemPrompt = prescriptive
1618
+ ? `You are a tutor building an ACTION PLAN based on your learner's behavior. For each dimension, output a specific DO and DON'T directive that will guide your next response. The ego model receiving these directives may be less capable — be concrete and unambiguous.
1619
+
1620
+ DIMENSIONS:
1621
+ 1. **Engagement**: DO: [specific action to take]. DON'T: [specific thing to avoid].
1622
+ 2. **Content Delivery**: DO: [how to present the next idea]. DON'T: [what framing to avoid].
1623
+ 3. **Response to Learner**: DO: [how to handle their likely next move]. DON'T: [common mistake to avoid with this learner].
1624
+ 4. **Leverage**: DO: [build on what worked]. DON'T: [repeat what failed].
1625
+ 5. **Next Move**: DO: [single most important thing for the next turn]. DON'T: [single biggest risk].
1626
+
1627
+ RULES:
1628
+ - Under 200 words total
1629
+ - Every DO/DON'T must reference THIS learner's actual words or behavior
1630
+ - If revising a prior plan, mark changed entries with [REVISED]
1631
+ - Be PRESCRIPTIVE — tell the ego exactly what to do, not what the learner is like`
1632
+ : `You are a tutor building a mental model of your learner. Based on the dialogue evidence, profile the learner across 5 dimensions. This profile will inform (not dictate) your next response.
1633
+
1634
+ DIMENSIONS:
1635
+ 1. **Current State**: Where is the learner RIGHT NOW? (confused, engaged, frustrated, surface-complying, genuinely curious)
1636
+ 2. **Learning Pattern**: How does this learner process new ideas? (needs examples first, thinks abstractly, learns by arguing, needs emotional safety before risk-taking)
1637
+ 3. **Resistance Points**: What topics/approaches trigger shutdown or surface compliance? What does the learner avoid or deflect from?
1638
+ 4. **Leverage Points**: What has actually worked? Which of your moves produced genuine engagement (not just politeness)?
1639
+ 5. **Prediction**: Based on patterns so far, what will this learner do if you continue your current approach? What would shift the trajectory?
1640
+
1641
+ RULES:
1642
+ - Under 200 words total
1643
+ - Be SPECIFIC to this learner — cite their actual words and reactions
1644
+ - If revising a prior profile, mark changed entries with [REVISED] and explain what evidence changed your assessment
1645
+ - Do NOT prescribe actions — describe what you SEE, not what you should DO`;
1646
+
1647
+ const priorSection = priorProfile
1648
+ ? `\n\nYour prior profile of this learner:\n${priorProfile}\n\nRevise based on new evidence. Mark changes with [REVISED].`
1649
+ : '';
1650
+
1651
+ const userMessage = `Build a profile of this learner based on the dialogue so far:
1652
+
1653
+ ${evidence}${priorSection}
1654
+
1655
+ Profile the learner across the 5 dimensions:`;
1656
+
1657
+ try {
1658
+ const response = await unifiedAIProvider.call({
1659
+ provider,
1660
+ model: profileModel,
1661
+ systemPrompt,
1662
+ messages: [{ role: 'user', content: userMessage }],
1663
+ preset: 'deliberation',
1664
+ config: {
1665
+ temperature: 0.3,
1666
+ maxTokens: 1500,
1667
+ },
1668
+ });
1669
+
1670
+ const metrics = extractMetrics(response);
1671
+ const profileText = response.content?.trim();
1672
+ if (!profileText || profileText.length < 30) {
1673
+ console.log(`[promptRewriter] Tutor profile of learner returned empty/short (${profileText?.length || 0} chars, model=${profileModel})`);
1674
+ return null;
1675
+ }
1676
+
1677
+ const tag = prescriptive ? 'learner_action_plan' : 'learner_profile';
1678
+ const text = `<${tag} turn="${turnNumber}">
1679
+ ${profileText}
1680
+ </${tag}>`;
1681
+ return { text, metrics };
1682
+ } catch (error) {
1683
+ console.error('[promptRewriter] Tutor profile of learner failed:', error.message);
1684
+ return null;
1685
+ }
1686
+ }
1687
+
1688
+ /**
1689
+ * Synthesize learner's evolving profile of the tutor.
1690
+ *
1691
+ * Mirror of synthesizeTutorProfileOfLearner but from the learner's perspective.
1692
+ * Dimensions: Teaching Style, Responsiveness, Strengths, Blind Spots, Prediction.
1693
+ *
1694
+ * @param {Object} options - Same shape as synthesizeTutorProfileOfLearner
1695
+ * @returns {Promise<string|null>} XML-wrapped profile block, or null on failure
1696
+ */
1697
+ export async function synthesizeLearnerProfileOfTutor({
1698
+ turnResults = [],
1699
+ consolidatedTrace = [],
1700
+ conversationHistory = [],
1701
+ priorProfile = null,
1702
+ config = {},
1703
+ }) {
1704
+ if (turnResults.length === 0) return null;
1705
+
1706
+ const evidence = buildOtherEgoProfileContext('learner', turnResults, consolidatedTrace, conversationHistory);
1707
+
1708
+ // Use superego model if configured (cognitive prosthesis for weaker ego models)
1709
+ const useSuperego = config.other_ego_profiling?.use_superego_model && config.superego;
1710
+ const modelAlias = useSuperego
1711
+ ? (config.superego.model || 'kimi-k2.5')
1712
+ : (config.ego?.model || config.model || 'nemotron');
1713
+ const provider = useSuperego
1714
+ ? (config.superego.provider || 'openrouter')
1715
+ : (config.ego?.provider || 'openrouter');
1716
+ let profileModel = modelAlias;
1717
+ try {
1718
+ const resolved = evalConfigLoader.resolveModel({ provider, model: modelAlias });
1719
+ profileModel = resolved.model;
1720
+ } catch { /* use alias as-is */ }
1721
+
1722
+ const turnNumber = turnResults.length;
1723
+
1724
+ const systemPrompt = `You are a learner building a mental model of your tutor. Based on the dialogue evidence, profile the tutor across 5 dimensions. This profile will inform how you engage in the next exchange.
1725
+
1726
+ DIMENSIONS:
1727
+ 1. **Teaching Style**: How does this tutor prefer to teach? (lecture-heavy, Socratic, example-driven, validation-first, challenge-oriented)
1728
+ 2. **Responsiveness**: Does the tutor actually adapt when you signal confusion, pushback, or engagement? Or do they follow a script?
1729
+ 3. **Strengths**: What does this tutor do well? When did their approach genuinely help you understand something?
1730
+ 4. **Blind Spots**: What does the tutor miss or ignore? Do they notice when you're struggling vs. complying? Do they build on your ideas or override them?
1731
+ 5. **Prediction**: If the tutor continues this way, what will happen? What would make them more effective for YOU specifically?
1732
+
1733
+ RULES:
1734
+ - Under 200 words total
1735
+ - Be SPECIFIC — cite the tutor's actual words and your reactions
1736
+ - If revising a prior profile, mark changed entries with [REVISED]
1737
+ - Speak as the learner: "The tutor tends to...", "When I pushed back, they..."`;
1738
+
1739
+ const priorSection = priorProfile
1740
+ ? `\n\nYour prior profile of this tutor:\n${priorProfile}\n\nRevise based on new evidence. Mark changes with [REVISED].`
1741
+ : '';
1742
+
1743
+ const userMessage = `Build a profile of this tutor based on the dialogue so far:
1744
+
1745
+ ${evidence}${priorSection}
1746
+
1747
+ Profile the tutor across the 5 dimensions:`;
1748
+
1749
+ try {
1750
+ const response = await unifiedAIProvider.call({
1751
+ provider,
1752
+ model: profileModel,
1753
+ systemPrompt,
1754
+ messages: [{ role: 'user', content: userMessage }],
1755
+ preset: 'deliberation',
1756
+ config: {
1757
+ temperature: 0.3,
1758
+ maxTokens: 1500,
1759
+ },
1760
+ });
1761
+
1762
+ const metrics = extractMetrics(response);
1763
+ const profileText = response.content?.trim();
1764
+ if (!profileText || profileText.length < 30) {
1765
+ console.log(`[promptRewriter] Learner profile of tutor returned empty/short (${profileText?.length || 0} chars, model=${profileModel})`);
1766
+ return null;
1767
+ }
1768
+
1769
+ const text = `<tutor_profile turn="${turnNumber}">
1770
+ ${profileText}
1771
+ </tutor_profile>`;
1772
+ return { text, metrics };
1773
+ } catch (error) {
1774
+ console.error('[promptRewriter] Learner profile of tutor failed:', error.message);
1775
+ return null;
1776
+ }
1777
+ }
1778
+
1779
+ /**
1780
+ * Format a profile block for injection into an agent's context.
1781
+ *
1782
+ * Wraps the raw profile in framing that positions it as context (not directive).
1783
+ *
1784
+ * @param {string} profile - Raw profile XML block
1785
+ * @param {'learner'|'tutor'} profileType - What the profile is about
1786
+ * @returns {string} Injection-ready XML block
1787
+ */
1788
+ export function formatProfileForInjection(profile, profileType = 'learner') {
1789
+ if (!profile) return '';
1790
+
1791
+ const framing = profileType === 'learner'
1792
+ ? 'This is your evolving understanding of this specific learner — their patterns, resistance points, and what has worked. Use this to inform your next response as context, not as directive. Let your understanding of who they are shape what you say.'
1793
+ : 'This is your evolving understanding of this specific tutor — their teaching style, blind spots, and what has been effective. Use this to inform how you engage, not as a script to follow.';
1794
+
1795
+ return `<other_agent_profile type="${profileType}">
1796
+ ${framing}
1797
+
1798
+ ${profile}
1799
+ </other_agent_profile>`;
1800
+ }
1801
+
1802
+ /**
1803
+ * Synthesize ego's explicit strategy plan based on the learner profile.
1804
+ *
1805
+ * After building a profile, the ego formulates a 3-sentence plan:
1806
+ * Goal (target outcome), Approach (technique chosen for THIS learner), Risk (what could fail).
1807
+ *
1808
+ * Only used in cell 59 (strategy_planning enabled).
1809
+ *
1810
+ * @param {Object} options
1811
+ * @param {string} options.learnerProfile - Current learner profile
1812
+ * @param {Array} options.turnResults - Results from previous turns
1813
+ * @param {Array} options.conversationHistory - Conversation history entries
1814
+ * @param {Object} options.config - Profile config containing model info
1815
+ * @returns {Promise<string|null>} XML-wrapped strategy plan, or null on failure
1816
+ */
1817
+ export async function synthesizeStrategyPlan({
1818
+ learnerProfile = null,
1819
+ turnResults = [],
1820
+ conversationHistory = [],
1821
+ config = {},
1822
+ }) {
1823
+ if (!learnerProfile) return null;
1824
+
1825
+ // Use superego model if configured (cognitive prosthesis for weaker ego models)
1826
+ const useSuperego = config.other_ego_profiling?.use_superego_model && config.superego;
1827
+ const modelAlias = useSuperego
1828
+ ? (config.superego.model || 'kimi-k2.5')
1829
+ : (config.ego?.model || config.model || 'nemotron');
1830
+ const provider = useSuperego
1831
+ ? (config.superego.provider || 'openrouter')
1832
+ : (config.ego?.provider || 'openrouter');
1833
+ let strategyModel = modelAlias;
1834
+ try {
1835
+ const resolved = evalConfigLoader.resolveModel({ provider, model: modelAlias });
1836
+ strategyModel = resolved.model;
1837
+ } catch { /* use alias as-is */ }
1838
+
1839
+ const lastScore = turnResults
1840
+ .filter(t => t.turnScore != null)
1841
+ .slice(-1)[0]?.turnScore;
1842
+
1843
+ const lastLearnerMsg = conversationHistory
1844
+ .filter(h => h.learnerMessage)
1845
+ .slice(-1)[0]?.learnerMessage?.substring(0, 200) || '(no learner response yet)';
1846
+
1847
+ const systemPrompt = `Based on your profile of the learner, formulate a brief strategy for your next response. Output exactly 3 sentences:
1848
+
1849
+ 1. **Goal**: What specific outcome are you targeting in the next turn? (e.g., "Get the learner to articulate their own understanding rather than just acknowledging mine")
1850
+ 2. **Approach**: What technique will you use, given what you know about THIS learner? (e.g., "Start with their own words from turn 2 and ask them to extend the idea")
1851
+ 3. **Risk**: What could go wrong with this approach for THIS learner? (e.g., "They might feel put on the spot and retreat to surface compliance")
1852
+
1853
+ RULES:
1854
+ - Each sentence must reference something specific from the learner profile
1855
+ - Do NOT repeat generic pedagogical advice — this plan must be learner-specific
1856
+ - 3 sentences only, no preamble`;
1857
+
1858
+ const userMessage = `Your profile of the learner:
1859
+ ${learnerProfile}
1860
+
1861
+ Last learner message: "${lastLearnerMsg}"${lastScore != null ? `\nLast score: ${lastScore.toFixed(1)}` : ''}
1862
+
1863
+ Formulate your 3-sentence strategy plan:`;
1864
+
1865
+ try {
1866
+ const response = await unifiedAIProvider.call({
1867
+ provider,
1868
+ model: strategyModel,
1869
+ systemPrompt,
1870
+ messages: [{ role: 'user', content: userMessage }],
1871
+ preset: 'deliberation',
1872
+ config: {
1873
+ temperature: 0.4,
1874
+ maxTokens: 800,
1875
+ },
1876
+ });
1877
+
1878
+ const metrics = extractMetrics(response);
1879
+ const planText = response.content?.trim();
1880
+ if (!planText || planText.length < 30) {
1881
+ console.log(`[promptRewriter] Strategy plan returned empty/short (${planText?.length || 0} chars, model=${strategyModel})`);
1882
+ return null;
1883
+ }
1884
+
1885
+ const text = `<strategy_plan>
1886
+ ${planText}
1887
+ </strategy_plan>`;
1888
+ return { text, metrics };
1889
+ } catch (error) {
1890
+ console.error('[promptRewriter] Strategy plan failed:', error.message);
1891
+ return null;
1892
+ }
1893
+ }