Modality
Modality is the top-level trait that ties everything together. It is a supertrait of both Reducer and Component, adding layout orchestration, child management, and snapshot emission. Each modality struct (Whiteboard, Worksheet, LessonPlan) implements all three traits.
Trait Definition
Section titled “Trait Definition”pub trait Modality: Reducer<State = State<Self::Synced, Self::Ephemeral>> + Component { type Synced: Synced + HasResourceMeta + HasValues; type Ephemeral: Default + Send + Sync + 'static + HasOutput<Output = <Self as Component>::Output>; // NO Clone type Position: Clone + PartialEq + Hash + Send + Sync + 'static; type ChildComponent: Component + ?Sized; type Snapshot: Clone + Send + Sync + 'static;
fn new(synced: Self::Synced, services: Arc<Self::Services>, doc: &LoroDoc) -> Self;
fn placements(&self) -> &[ComponentPlacement<Self::Position>]; fn metadata(&self) -> <Self as Component>::Metadata; fn child_metadata( &self, placement: &ComponentPlacement<Self::Position>, ) -> <Self::ChildComponent as Component>::Metadata;
fn compile_child_hash( &self, placement: &ComponentPlacement<Self::Position>, meta: &<Self::ChildComponent as Component>::Metadata, values: &HashMap<String, PropertyValue>, ) -> u64 where <Self::ChildComponent as Component>::Metadata: Hash, { default_compile_child_hash(&placement.id, meta, values) }
fn compile_child( &self, placement: &ComponentPlacement<Self::Position>, meta: &<Self::ChildComponent as Component>::Metadata, values: &HashMap<String, PropertyValue>, ) -> Result<<Self::ChildComponent as Component>::Output, CompileError>;
fn assemble( &self, pairs: Vec<( ComponentPlacement<Self::Position>, <Self::ChildComponent as Component>::Output, )>, ) -> <Self as Component>::Output;
/// Called after synced state is restored from undo. Fix up local ephemeral state. fn on_undo(&mut self) {}
/// Called after synced state is restored from redo. Fix up local ephemeral state. fn on_redo(&mut self) {}
fn snapshot( &self, ai: &AiState, export: &ExportState, version: u64, can_undo: bool, can_redo: bool, ) -> Self::Snapshot;
fn export_bytes(output: &<Self as Component>::Output, format: OutputFormat) -> Result<Vec<u8>, String> { Err("Export not supported".into()) }
fn system_prompt(&self) -> String { String::new() }
fn agent_config(&self) -> AgentConfig { AgentConfig::default() }
fn layout<F>( &self, compile_child: F, values: &HashMap<String, PropertyValue>, ) -> Result< Vec<( ComponentPlacement<Self::Position>, <Self::ChildComponent as Component>::Output, )>, CompileError, > where F: FnMut( &ComponentPlacement<Self::Position>, &<Self::ChildComponent as Component>::Metadata, &HashMap<String, PropertyValue>, ) -> Result<<Self::ChildComponent as Component>::Output, CompileError>;}Associated Types
Section titled “Associated Types”| Type | Bound | Purpose |
|---|---|---|
Synced | Synced + HasResourceMeta + HasValues | Persisted state, stored in LoroDoc. HasValues provides property value access for binding resolution. |
Ephemeral | Default + Send + Sync + 'static + HasOutput | Runtime-only state. HasOutput stores latest compiled output. No Clone — allows non-clonable children like Session<Whiteboard>. |
Position | Clone + PartialEq + Hash + Send + Sync + 'static | Modality-specific spatial info. Hash enables cache keys. |
ChildComponent | Component + ?Sized | The child component type. ?Sized allows trait objects (dyn WhiteboardComponent). |
Snapshot | Clone + Send + Sync + 'static | DTO pushed to sinks. Each modality defines its own concrete type. |
State is pinned
Section titled “State is pinned”Modality pins the Reducer state type to State<Self::Synced, Self::Ephemeral>. The Synced/Ephemeral split is a Modality concern, not a Reducer concern. state() and state_mut() accessors live on Reducer and are inherited through the supertrait.
Ephemeral has no Clone
Section titled “Ephemeral has no Clone”Clone is not required on Ephemeral. This is deliberate — LessonPlanEphemeral holds HashMap<String, Slot<Session<Whiteboard, ...>, ...>>, and Session is not clonable. Snapshots are built via snapshot() which takes references, not clones.
Compilation Flow
Section titled “Compilation Flow”compile() (from Component) drives a top-down pipeline through layout() and assemble():
flowchart TD A["compile(metadata, values)"] --> B["layout(compile_child, values)"] B --> C["for each placement"] C --> D["child_metadata(placement)"] D --> E["resolve_bindings(bindings, values)"] E --> F["compile_child(placement, meta, resolved)"] F --> G["collect pairs"] G --> H["assemble(pairs)"] H --> I["Output"]
style A fill:#f0f0f0,stroke:#333 style I fill:#f0f0f0,stroke:#333Step by step
Section titled “Step by step”compile(metadata, values)— entry point from Component trait. Callslayout()with a closure, thenassemble().layout(compile_child, values)— iteratesplacements(), resolves bindings, delegates to thecompile_childclosure. Has a default implementation that can be overridden.child_metadata(placement)— computes the metadata for a child based on its placement.compile_child(placement, meta, values)— compiles a single child component.assemble(pairs)— combines compiled child outputs into the modality’s final output.
Default layout implementation
Section titled “Default layout implementation”fn layout<F>(&self, mut compile_child: F, values: &HashMap<String, PropertyValue>) -> Result<Vec<(ComponentPlacement<Self::Position>, ChildOutput)>, CompileError>where F: FnMut(&ComponentPlacement<Self::Position>, &ChildMeta, &HashMap<String, PropertyValue>) -> Result<ChildOutput, CompileError>,{ self.placements().iter() .map(|p| { let meta = self.child_metadata(p); let resolved = resolve_bindings(&p.bindings, values); compile_child(p, &meta, &resolved).map(|out| (p.clone(), out)) }) .collect()}Modalities override layout() for custom behavior. For example, LessonPlan filters out disabled slides before compilation.
Recursive compilation
Section titled “Recursive compilation”Compilation flows recursively through the modality tree:
flowchart LR A["compile()"] --> B["layout()"] --> C["compile_child closure"] C --> D["child compile()"] --> E["child layout()"] --> F["..."]Session wraps the compile_child closure with hash-based caching for incremental recompilation. The hash is computed by compile_child_hash(), which modalities can override when compile output depends on state beyond metadata and property values (e.g. Whiteboard includes LoroText content for text placements). The default implementation hashes (placement_id, metadata, values) via the free function default_compile_child_hash().
Methods
Section titled “Methods”| Method | Purpose |
|---|---|
new(synced, services, doc) | Construct from synced state, shared services, and LoroDoc reference. |
placements() | Return all component placements in the synced state. |
metadata() | Return the modality’s own Component metadata. |
child_metadata(placement) | Compute the metadata for a specific child placement. |
compile_child(placement, meta, values) | Compile a single child. |
assemble(pairs) | Combine compiled children into final output. |
on_undo() | Hook called after synced state is restored from undo. Fix up ephemeral state. Default no-op. |
on_redo() | Hook called after synced state is restored from redo. Fix up ephemeral state. Default no-op. |
snapshot(ai, export, version, can_undo, can_redo) | Build a snapshot DTO from current state. Undo availability passed from Session. |
export_bytes(output, format) | Export compiled output to bytes. Default returns error. |
system_prompt() | AI system prompt built from current state. Default returns empty. |
agent_config() | Agent configuration defaults (temperature, model, preamble). |
layout(compile_child, values) | Orchestrate child compilation with a provided FnMut closure. |
Per-Modality Summary
Section titled “Per-Modality Summary”| Modality | Metadata | Output | ChildComponent | Child Metadata | Session Type |
|---|---|---|---|---|---|
| Whiteboard | WhiteboardMetadata | Vec<WhiteboardElement> | dyn WhiteboardComponent | WhiteboardChildMeta | Session<Whiteboard, SessionIntent<WhiteboardIntent>> |
| Worksheet | WorksheetMetadata | Vec<WorksheetElement> | dyn WorksheetComponent | () | Session<Worksheet, SessionIntent<WorksheetIntent>> |
| LessonPlan | LessonPlanMetadata | Vec<CompiledSlide> | Slot<Session<Whiteboard, ...>, Vec<WhiteboardElement>> | WhiteboardMetadata | Session<LessonPlan, ParentSessionIntent<...>> |
Concrete structs
Section titled “Concrete structs”pub struct Whiteboard { rt: Runtime<Self>,}
pub struct Worksheet { rt: Runtime<Self>,}
pub struct LessonPlan { rt: Runtime<Self>, children: HashMap<String, Slot<Session<Whiteboard>, Vec<WhiteboardElement>>>,}All modality structs embed Runtime<Self>, which holds State<M::Synced, M::Ephemeral> inside rt.state. Session<Whiteboard> uses the default type parameter I = SessionIntent<WhiteboardIntent>.
Note that LessonPlan holds children directly. Its ChildComponent type is Slot, which implements Component by delegating to Hot sessions or returning cached Cold output.
Key Design Insights
Section titled “Key Design Insights”- Reducer is stateless (static reduce). Modality owns state. Session owns the modality + everything else.
layout()never loads state — storage is abstracted via thecompile_childclosure. Session wraps this closure with caching.- Elements are compiled output, never directly mutated. Modify placements, values, metadata — recompilation flows downward.
snapshot()builds from references, not clones. This removes the Clone bound on Ephemeral.