Pedagogical personalization architecture
Date: 2026-08-11 Status: target architecture and incremental plan; does not describe behavior already implemented. Applies to: the dynamic courses v2 runtime and a future external component library.
1. Decision
Personalization is split into five decisions with different authority. A single prompt is not asked to simultaneously choose what to teach, how to practice it, and which component to draw.
Validated CourseNode
-> learning objective (what the learner must be able to do; not personalized)
-> cognitive mission (what they will do to learn it)
-> representation (text, image, audio, table, diagram...)
-> component (concrete catalog capability)
-> support (hints, example, density, feedback)
-> validated, frozen UI spec
The valuable variation happens in representation, component, and support. The objective and the critical facts stay stable. So “personalizing” does not mean generating an arbitrarily different lesson, but producing comparable variants of the same pedagogical intent.
This architecture is concrete and does not replace the decisions in
adaptive-learning.md,
arquitectura-componentes-funcional.md, and
v2-dynamic-courses.md.
The publishing architecture that builds variants during course generation, exposes the neutral
LearningExperience boundary, and normalizes evidence from Didact, video, games, or simulations
toward mastery is defined in
learning-experience-architecture.md. This document keeps
authority over profile projection, support, caching, and capability-based resolution.
2. The five layers
| Layer | Question | Main input | Output | Authority |
|---|---|---|---|---|
| Objective | What evidence will demonstrate learning? | CourseNode.outcome, source, and criticality |
observable criterion | creator; schema gate |
| Cognitive mission | What core action will the learner take? | objective, knowledge type, and prior error | recognize, reconstruct, interpret, decide, explain, or produce |
pedagogical policy |
| Representation | In what modality is it expressed? | source capabilities, declared preference, and accessibility | one or more compatible modalities | user + hard constraints |
| Component | What catalog capability implements mission and representation? | catalog descriptor, domain, cost, and requirements | component_id@version or rejection |
deterministic resolver |
| Support | How much help does this person need right now? | mastery band, experience, errors, reading adjustments | density, hints, example, and feedback | adaptive policy |
ContentFunction still describes the usable form of the source (CONTRAST, PROCEDURALIZE,
QUANTIFY…). The mission describes the learner’s action. They are not the same axis: a
procedural source can serve to reconstruct the order, or to decide which step to apply when
facing an exception.
3. Intermediate contract
Before the blueprint there must be a typed, small, auditable plan. It is a domain decision, not a UI spec, and it contains no generated prose.
@dataclass(frozen=True)
class LearningExperiencePlan:
objective_id: str
objective_version: int
mission: CognitiveMission
source_functions: tuple[ContentFunction, ...]
representations: tuple[Presentation, ...]
required_facts: tuple[str, ...]
required_safety: tuple[str, ...]
support: SupportPolicy
component_candidates: tuple[ComponentRef, ...]
rationale_codes: tuple[str, ...]
policy_version: str
The plan contains references to facts or source spans, not copies rewritten by the model. Content agents receive the plan and fill in components; they cannot change the mission, candidates, criticality, or constraints. The assembler accepts only IDs declared by the blueprint: an orphan block is a reparable error, not a component that gets automatically wired to the root.
Plan invariants
- There is one central mission per node. It can materialize as a rich component with many states and actions, or as several coherent components; it does not equate to limiting the amount of UI. What is avoided is a second, competing mission or redundant blocks that add no new capability.
- Verification observes the objective; it is not chosen for visual variety.
- The warnings, limits, and prohibitions of a critical source are
required_safetyand survive any variant. - A requested modality appears when a compatible capability exists. If it doesn’t, the plan
records a visible
fallback_reason; it never pretends to have satisfied it. - Accessibility is a mandatory filter applied before ranking. A preference cannot select a component the person cannot operate.
- Any producer can return
Declined(reason). With insufficient data it falls back to the next candidate; it does not invent figures, spatial relationships, branches, or media. - The renderer only receives a validated UI spec; never free-form HTML or generated code.
4. Personalization projection
The planner does not receive memory_md, raw events, or free-text profile. It receives a
deterministic, closed-vocabulary, identity-free projection:
{
"declared_presentations": ["image"],
"inferred_presentation_bucket": "exercise-high",
"support_band": "novice",
"density": 2,
"accessibility_capabilities": ["keyboard", "reduced_motion"],
"error_signal": "procedural",
"calibrating": false,
"projection_version": "personalization/1"
}
The declared preference is a positive constraint: “include image”, not “remove text, practice, and
feedback”. The current format_vector remains as a secondary inferred signal. Support changes the
amount of help, not the truth of the explanation nor the objective.
The initial compilation lives in src/personalization/projection.py. It accepts the profile shape
already loaded by the runtime, but discards role_title and sector: they can still contextualize
examples in the current pipeline, but are not preferences and are not copied into the plan. It also
does not accept user_id, memory_md, or raw events. During the three calibration nodes the
inferred vector is marked as unknown. The legacy codigo dimension also remains unknown, because
assigning it to text or image would invent a semantics that the current events do not measure.
5. Resolution against the component library
The library publishes capabilities; SkillNet keeps the policy. Its versioned descriptor must be able to answer, without knowing React, these questions:
- what missions, source functions, and representations it supports;
- what affordances it offers (manipulate, build, rehearse, inspect result…);
- what requirements it needs (
numeric_series,image_asset,branching_script…); - what accessible operations it offers and what the alternative to dragging is;
- what events and evidence it produces and what feedback contract it guarantees;
- what versioned state model it needs;
- what producer builds it (
content,assessment,media,simulation, ordeterministic); - what cost and latency it adds;
- what props schema and renderer version it needs.
Resolution is intersection plus ranking:
candidates = catalog
∩ mission
∩ source function
∩ requested representation
∩ accessibility capabilities
∩ actually available requirements
∩ deployment budget
chosen = rank(candidates, policy, controlled exploration)
The result is not just a name. The plan freezes a ResolvedComponent with component_id@version,
producer_kind, affordances, requirements, state_model_ref, and evidence events. The pedagogical
function does not decide which agent generates it: a simulation can teach and assess at the same
time, but it needs a producer and validator different from a QuizItem.
Image generated within OpenUI
A future course image does not break the on the fly principle: the plan resolves a descriptor
with presentations={image}, producer_kind=media, and requirements such as source_spans,
image_brief, or an already available image_asset. The media producer generates or retrieves the
asset, preserves provenance and alt text, and returns a typed reference. The assembler incorporates
it into the OpenUI UI spec alongside practice and safety; it does not generate HTML or embed a
free-form prompt on the client. If a producer, budget, or sufficient evidence is missing, the
candidate returns Declined(reason) and another compatible representation is chosen. The image is
thus a dynamic catalog capability, not a fixed template or an exception in the orchestrator.
The catalog can grow to hundreds of components without widening the prompt: the LLM, when
necessary, sees only the few already-filtered candidates. component_id@version is persisted;
React class names are not. The current kit and the new library coexist through two adapters that
consume the same plan and produce the same canonical IR.
6. Cache, privacy, and stability
The shared cache is only safe if every signal reaching the prompt is represented in a non-identifying key. The target key material is:
node_id | schema_version | objective_version | policy_version |
projection_version | preference_bucket | support_band | density |
accessibility_capability_bucket | component_catalog_version |
backend | model | prompt_version
Rules:
- never include
user_id, free-text memory, diagnostics, or individual events; - never introduce into the prompt a datum that isn’t projected in the key;
- buckets must be discrete and have enough population to avoid pseudonymization;
- the render is frozen when the node is opened, as v2 already decides; it does not change during an active screen;
- changes to objective, policy, catalog, or prompt explicitly invalidate the key;
- the plan and its
rationale_codesare stored for auditing, separate from the render prose.
7. Layered validation
Current syntactic validation is necessary, but not sufficient. Each render passes distinct gates:
| Gate | Checks | Type |
|---|---|---|
| Plan | single mission, constraints, available candidate | deterministic |
| Source | figures, relationships, media, and backed facts | deterministic + focused eval |
| Component | props schema, version, accessibility, and cost | deterministic |
| Assembly | exact IDs, reachability from root, order, and closure |
deterministic |
| Pedagogical | observable objective, informative feedback, coherent support | rubric-based eval |
| Safety | preservation of required_safety, no impossible or incoherent states |
deterministic where possible |
Metrics distinguish generated components from components reachable from root. An orphan block
does not count as variety or as compliance. For rich interactive components, specific evals are
added: unique solution, possible states, visual clarity, consistency between state and feedback,
and plausibility of the simulation.
8. Experiments that produce knowledge
It’s not “prompt A versus prompt B” compared by aggregating all courses. Objective and source are fixed, and a single layer is changed:
- Perspectives: the same objective as
interpret,detect, anddecide; identical critical facts. - Representation: same mission with table, diagram, or image; the requested modality is kept across all variants assigned to that person.
- Support: same component with a worked example, graded hints, or a direct case.
- Resolver: same plan against the current kit and the new library; equivalent golden specs and events.
- Ablation: with/without inferred preference, never with/without mandatory accessibility.
Per generation, the following is measured: first-pass validation, fallback, facts omitted/invented, safety preserved, reachability, mission fulfilled, variety across equivalent objectives, latency, and cost. With users, the following are separated: preference, engagement, immediate mastery, and transfer. A less flat screen that doesn’t improve the learner’s action does not win.
9. Incremental migration
Phase 0 — observability with no behavior change
- Log the current decision as
PlanTrace: format,ContentFunction, blueprint, reachable blocks, corrections, and fallback. - Fix the bench to count only the subgraph reachable from
root. - Create a small corpus labeled by objective, expected mission, and mandatory facts.
Gate: zero difference in served UI specs.
Phase 1 — plan in shadow mode
- Build
LearningExperiencePlanas a pure function from the state already available. - Do not use it yet to generate; compare its selection against the real pipeline.
- Version the projection and the policy from day one.
Gate: deterministic, explainable plans; no free-form signal enters the prompt or the cache.
Phase 2 — typed mission and support
- Make the blueprint consume
missionandSupportPolicy. - Keep the current component mapping to isolate the architectural effect.
- Reject extras and orphans instead of wiring them to the root.
Gate: safety and verification do not regress; blueprint compliance increases.
Phase 3 — capability resolver
- Move function, requirements, accessibility, and cost into the registry.
- Resolve candidates before calling the writer agents.
- Keep fallback to the current kit and
Declined(reason).
Gate: adding a component does not modify shape.py, the global prompt, or the assembler.
Phase 4 — declared preference
- Add editable presentation preferences in onboarding and settings.
- Compile
user.mdintoPersonalizationProjection; do not inject it directly. - Incorporate the buckets into the cache and show modality fallback reasons.
Gate: E2E tests demonstrate that a supported preference appears and persists, and that changing it regenerates only compatible renders, without leaking identity into the cache.
Phase 5 — external library and exploration
- Dual adapter with golden specs before replacing components.
- Enable new components via capability flags and by family.
- Explore variants only within valid plans and with rollback capability.
Gate: functional equivalence between the old and new adapters; the new components pass their specific evals before production.
10. Proposed location in code
src/personalization/
projection.py # profile/events -> PersonalizationProjection, pure
policy.py # objective + projection -> mission and support, pure
plan.py # types and invariants of LearningExperiencePlan
resolver.py # plan + catalog -> candidates or Declined, pure
cache.py # versioned serialization of buckets
src/components/
catalog.py # catalog Protocol, no React
legacy_adapter.py # descriptors of the current kit
library_adapter.py # future external library
LangGraph orchestrates loading, planning, writing, validation, and persistence. The domain rules above remain pure functions outside the nodes and the prompts. Subagents receive a frozen contract and narrow responsibilities: planning, writing content, designing interaction, and assembling; none can reinterpret another layer’s decisions.
11. Architectural success criteria
The architecture will have worked when:
- adding a component means registering capabilities and tests, not editing central decisions;
- two renders can vary without changing the objective or the mandatory facts;
- every difference can be explained through policy codes;
- an explicit modality request is either fulfilled or produces an honest reason;
- no invalid variant reaches the human just because it compiles;
- the system can revert to the previous kit without losing state or measured learning.
The concrete strategy for consuming Didact’s large catalog, limiting each decision to a shortlist,
distinguishing recipes from new components, and moving toward level-3 GenUI is in
didact-integration-strategy.md.