The learning subsystem provides on-device incremental training. All code lives in src/learning/. The pipeline is fully integrated — Observer verdicts automatically fill training buffers, and a background scheduler triggers sleep cycles when thresholds are met.
| File | Purpose |
|---|---|
mod.rs |
TrainingSample, TrainingMethod, PipelineStatus structs |
teacher.rs |
Training orchestrator — SFT and preference training |
sleep.rs |
Background sleep consolidation cycle |
buffers.rs |
GoldenBuffer — high-quality sample storage |
buffers_rejection.rs |
RejectionBuffer + PreferencePair for DPO/KTO |
observer_buffer.rs |
Observer-scored sample buffer |
manifest.rs |
Training run metadata tracking |
distill.rs |
Knowledge distillation utilities |
curriculum.rs |
Course/lesson/scene data model, progress tracking, JSON persistence |
verification.rs |
Ground truth verification gate + QuarantineBuffer |
student.rs |
Student session loop — scene processing, local buffer accumulation |
research.rs |
PhD research engine — arXiv ingestion, hypothesis generation, adversarial defense |
mlx_bridge.rs |
MLX subprocess LoRA fine-tuning bridge — JSONL prep, EWC snapshots |
graduation.rs |
Education level promotion — gate criteria, adapter validation, model fusion |
review.rs |
Spaced repetition — Leitner box review cards, retention stats |
| File | Purpose |
|---|---|
mod.rs |
LoraConfig struct |
weights.rs |
LoraLayer — A/B matrix storage with forward pass |
forward.rs |
Forward pass through LoRA adapter |
training.rs |
train_step() and train_epoch() — finite-difference gradient computation |
loss.rs |
Cross-entropy loss |
loss_dpo.rs |
Direct Preference Optimization loss |
loss_kto.rs |
Kahneman-Tversky Optimization loss |
loss_simpo.rs |
Simple Preference Optimization loss |
optimizer.rs |
SGD optimizer |
adapters.rs |
Multi-adapter management |
ewc.rs |
Elastic Weight Consolidation (catastrophic forgetting prevention) |
training_alignment.rs |
Alignment training utilities |
| File | Purpose |
|---|---|
mod.rs |
Module declarations |
generation.rs |
Candidate response generation (online + offline) |
rewards.rs |
Multi-signal reward scoring (length, relevance, structure) + advantage computation |
training.rs |
GRPO loss with KL penalty |
pub struct TrainingSample {
pub id: String,
pub input: String,
pub output: String,
pub method: TrainingMethod,
pub quality_score: f32,
pub timestamp: DateTime<Utc>,
}pub enum TrainingMethod { Sft, Orpo, SimPO, Kto, Dpo, Grpo }pub struct LoraConfig {
pub rank: usize, // default: 4
pub alpha: f32, // default: 8.0
pub learning_rate: f64, // default: 1e-4
pub model_dim: usize, // default: 3584
}pub struct LoraLayer {
pub name: String,
pub a_matrix: Vec<Vec<f32>>, // [rank × input_dim]
pub b_matrix: Vec<Vec<f32>>, // [output_dim × rank]
pub rank: usize,
pub alpha: f32,
pub input_dim: usize,
pub output_dim: usize,
}Teacher.train_sft(samples) →
for each sample:
input_vec = text_to_vec(sample.input)
target_vec = text_to_vec(sample.output)
loss = training::train_step(&mut lora, &input, &target, lr)
→ TrainingResult { method: "SFT", samples, loss }
Teacher.train_preference(pairs, method) →
for each PreferencePair:
chosen_vec → LoRA forward
rejected_vec → LoRA forward
loss = chosen_loss - rejected_loss margin
→ TrainingResult { method: "DPO", samples, loss }
1. generate_candidates_offline(prompt, n) → n candidate responses
2. score_group(candidates, query) → reward scores (length + relevance + structure)
3. compute_advantages(scores) → zero-mean advantages
4. train_step(candidates, query, kl_coeff) → policy loss with KL penalty
Reward signals in rewards::score_group():
- Length score:
min(len / 200.0, 1.5)— rewards substantive responses - Relevance score: Keyword overlap between response and query
- Structure score: Presence of paragraphs, lists, code blocks
LoRA training uses finite-difference gradient estimation (not autograd). In training::train_step():
- Compute forward loss at current B-matrix weights
- For each weight, perturb by
epsilon(1e-4), compute loss again - Gradient =
(perturbed_loss - base_loss) / epsilon - Update:
weight -= learning_rate × gradient
This approach requires no autograd library and works on any hardware.
sleep::run_sleep_cycle() is a background task that:
- Drains the
GoldenBuffer(up to 32 samples) - Runs SFT training via
Teacher.train_sft() - Drains the
RejectionBuffer(up to 16 pairs) - Runs DPO training via
Teacher.train_preference() - Calls
synaptic.decay_all(0.95)to weaken inactive knowledge graph nodes - Calls
lessons.decay_unused(0.98, 0.3)to prune stale lessons
The sleep cycle is triggered by the sleep_cycle job in the cron engine (src/scheduler/). The scheduler ticks every 15 seconds, checking all enabled jobs against their schedule. The sleep_cycle job runs every 5 minutes and triggers when buffer thresholds are met:
- GoldenBuffer ≥ 10 samples, OR
- RejectionBuffer ≥ 5 pairs
pub struct GoldenBuffer {
samples: Vec<TrainingSample>,
max_size: usize,
}add(sample)— append (drops oldest if at capacity)drain_batch(count)— remove and return up tocountsamplescount()— current buffer size
pub struct PreferencePair {
pub id: String,
pub input: String,
pub chosen: String,
pub rejected: String,
pub rejection_reason: String,
pub timestamp: DateTime<Utc>,
}Created when the Observer rejects a response — the original (rejected) and retry (chosen) form a preference pair for DPO training.
src/web/training_capture.rs captures Observer verdicts as training signals:
- Approved responses →
capture_approved()→GoldenBuffer(SFT) - Rejected responses + retried replacement →
capture_rejection()→RejectionBuffer(DPO pairs)
Both operate as fire-and-forget tokio::spawn background tasks.
The learning tool (src/tools/learning_tool.rs) provides live access to the training pipeline through AppState:
status— real buffer counts and pipeline readinessbuffer_stats— detailed capacity infotrigger_training— queue a training runsleep— manually trigger the sleep consolidation cycle
LlamaCppConfig.lora_adapter specifies a trained adapter to load at inference via --lora flag. After the scheduler completes a sleep cycle and saves an adapter, a server restart loads it into the model.
The schooling pipeline enables Ern-OS to autonomously progress through a full education system. It uses 5 education levels with incremental weight updates at each stage.
| Level | EWC Lambda | Pass Threshold | Learning Mode |
|---|---|---|---|
| Primary | 0.1 | 0.5 | Read → Repeat → Quiz |
| Secondary | 0.3 | 0.6 | Read → Reason → Apply |
| Undergraduate | 0.5 | 0.65 | Read → Analyze → Synthesize |
| Masters | 0.7 | 0.75 | Read → Evaluate → Create |
| Doctoral | 0.9 | 0.8 | Survey → Hypothesize → Experiment → Defend |
Manages courses, lessons, scenes, and student progress. Persists to data/curriculum/ as JSON.
- Course — title, level, subject, lessons, prerequisites, completion criteria, source
- Lesson — ordered scenes with objectives and prerequisites
- Scene — teaching unit with scene type (Quiz, Lecture, Exercise, Essay, etc.), interaction type, expected output, and difficulty
- CourseProgress — per-course completion tracking with quiz scores
Curriculum sources: OpenMAIC ZIP, OSSU GitHub, arXiv papers, custom JSONL.
Every student answer passes through a verification stage before entering the Golden Buffer:
- Confirmed — answer matches ground truth or external verification → Golden Buffer
- Contradicted — answer proven wrong → Rejection Buffer
- Unverifiable — cannot confirm/deny → Quarantine Buffer
This prevents hallucination feedback loops where the model trains on its own wrong answers.
Processes scenes using StudentSession with local buffers:
- Scene prompt generated per education level and interaction type
- Student answer via
provider.chat_sync() - Verification against expected output
- Results accumulated in local buffers (no shared locks during inference)
- Batch flush to shared state at lesson end (microsecond lock durations)
Preemption: if cancel_flag is set (user activity), saves position and returns.
PhD-level learning — manages ResearchProject lifecycle:
- Literature Survey — arXiv search via
web_search, paper metadata extraction, embedding storage - Hypothesis Generation — novelty and testability scoring via LLM
- Experimentation — handled by curriculum scenes
- Paper Writing — handled by curriculum scenes
- Defense — adversarial self-play (Model A attacks, Model B defends, both scored)
Gate criteria: survey needs ≥10 papers, hypothesis needs ≥3 scored, defense needs ≥3 rounds with avg quality >0.7.
Subprocess-based LoRA fine-tuning using Apple's MLX framework:
prepare_training_data()— convertsTrainingSample→ JSONL chat formatsnapshot_ewc_params()— saves current adapter weights as EWC θ* referencerun_mlx_lora()— shells out topython3 -m mlx_lm.lorawith level-scaled hyperparametersfuse_adapter()—python3 -m mlx_lm.fuseto merge adapter into base modelcheck_mlx_available()— graceful feature gating (disabled if Python/MLX not installed)
Learning rate scales with education level: Primary 1e-5, Secondary 5e-6, Undergraduate 2e-6, Masters 1e-6, Doctoral 5e-7.
Auto-promotes between education levels when gate criteria are met:
| Level | Required Courses | Min Score | Capstone |
|---|---|---|---|
| Primary → Secondary | 3 | 0.60 | No |
| Secondary → Undergraduate | 4 | 0.65 | No |
| Undergraduate → Masters | 5 | 0.70 | No |
| Masters → Doctoral | 3 | 0.75 | Yes |
| Doctoral → Complete | 1 | 0.80 | Yes (defense) |
Graduation history persisted to data/graduation_history.json.
Leitner box system preventing curriculum-level catastrophic forgetting:
- Box 1: review daily (1 day)
- Box 2: review every 3 days
- Box 3: review weekly (7 days)
- Box 4: review biweekly (14 days)
- Box 5: review monthly (30 days)
Correct → promote one box. Wrong → demote to box 1 + rejection buffer for retraining.
Cards generated from completed course Quiz/Exercise/CriticalAnalysis scenes with expected outputs. Deck persisted to data/review_deck.json.
Three learning jobs run via the cron engine (src/scheduler/learning_tasks.rs):
| Job | Interval | Default |
|---|---|---|
attend_class |
4 hours | Disabled |
conduct_research |
24 hours | Disabled |
spaced_review |
12 hours | Disabled |
All disabled until the user adds courses to the curriculum. Enable via the scheduler API or WebUI.