Children
Parent modalities (currently LessonPlan) contain child sessions that have their own state, persistence, and reduce cycles. The HasChildren trait, the Slot<C, O> container, and ParentSessionIntent work together to provide lazy warming, independent child dispatch, and parent-level recompilation.
HasChildren Trait
Section titled “HasChildren Trait”pub trait HasChildren: Modality { type ChildModality: Modality; type ChildSessionIntent: ReduceIntent<Self::ChildModality>;
fn children(&self) -> &HashMap<String, Slot< Session<Self::ChildModality, Self::ChildSessionIntent>, <Self::ChildModality as Component>::Output, >>;
fn children_mut(&mut self) -> &mut HashMap<String, Slot< Session<Self::ChildModality, Self::ChildSessionIntent>, <Self::ChildModality as Component>::Output, >>;
fn selected_child_id(&self) -> Option<&str>; fn reconcile_children(&mut self);}HasChildren is implemented by the modality struct (e.g., LessonPlan), not by Session. Session accesses children through self.modality.children_mut() inside ReduceIntent impls.
Slot<C, O> — Hot/Cold Container
Section titled “Slot<C, O> — Hot/Cold Container”pub enum Slot<C, O> { Cold(O), // cached compiled output, no recompilation Hot(Arc<Mutex<C>>), // live child behind Arc<Mutex>}Slot implements Component:
- Cold(O) — returns the cached output directly. Zero cost.
- Hot(C) — delegates to the inner component’s
compile().
Slot does not implement Reducer. Dispatch to a Hot child goes through child_session.dispatch() directly. Dispatching to a Cold slot is an error — it must be warmed first.
For LessonPlan, the concrete type is:
HashMap<String, Slot< Session<Whiteboard, SessionIntent<WhiteboardIntent>>, Vec<WhiteboardElement>,>>Hot/Cold Lifecycle
Section titled “Hot/Cold Lifecycle”stateDiagram-v2 [*] --> Cold: Session opens Cold --> Warming: WarmChild { id } Warming --> Hot: ChildWarmed { id, bytes } Hot --> Hot: Child { id, intent } Hot --> Cold: Session closes (export + cache)Step by Step
Section titled “Step by Step”-
On open — all children start
Cold. The cached output comes from the parent’s synced state or is empty. -
Navigate to child — Dart dispatches
ParentSessionIntent::WarmChild { id }:WarmChild { id } => {fx.spawn(move |svc, sender| {let bytes = svc.backend.load(&id).unwrap();sender.send(Command::Feedback(ParentSessionFeedback::ChildWarmed { id, bytes }));});}This spawns an async backend load. The feedback arrives on the next dispatch cycle.
-
ChildWarmed feedback — creates the Hot session from loaded bytes:
ParentSessionFeedback::ChildWarmed { id, bytes } => {let child_session = /* create Session from bytes */;state.modality.children_mut().insert(id, Slot::Hot(child_session));} -
Child dispatch — intents route through
ParentSessionIntent::Child:Child { id, intent } => {let child_id = id.unwrap_or_else(||state.modality.selected_child_id().unwrap().to_string());match state.modality.children_mut().get_mut(&child_id) {Some(Slot::Hot(child_session)) => {child_session.dispatch(Command::Intent(intent));// parent invalidates cache for that placement}Some(Slot::Cold(_)) => { /* error: must warm first */ }None => { /* error: unknown child */ }}}The child does its own full reduce cycle (drain, reduce, lifecycle, spawn). The parent then runs its own lifecycle, recompiling with the child’s updated output.
-
On close — Hot sessions export their docs. The parent caches child output.
reconcile_children
Section titled “reconcile_children”Called inside the ReduceIntent impl after the modality reduce, before the lifecycle runs:
// Inside ParentSessionIntent::reduce, after Base(Modality(i)) reduce:state.modality.reconcile_children();reconcile_children() ensures the children map stays in sync with placements:
- Creates
Coldslots for new placements (new slides added). - Removes slots for stale placements (deleted slides).
This runs before recompilation, so the cache and layout see a consistent set of children.
Child Dispatch Flow
Section titled “Child Dispatch Flow”All child lifecycle logic lives inside ReduceIntent impls. The FRB API layer just calls session.dispatch(cmd).
sequenceDiagram participant Dart participant ParentSession as Parent Session participant ReduceIntent as ParentSessionIntent::reduce participant ChildSession as Child Session participant ParentLifecycle as Parent Lifecycle
Dart->>ParentSession: dispatch(Child { id, intent }) ParentSession->>ReduceIntent: intent.reduce(state, fx) ReduceIntent->>ChildSession: dispatch(Command::Intent(intent)) Note over ChildSession: Child runs full lifecycle:<br/>reduce -> recompile -> commit -> emit ChildSession-->>ReduceIntent: returns ReduceIntent-->>ParentSession: returns Note over ParentSession: Parent lifecycle runs:<br/>recompile (uses child output) -> commit -> emit ParentSession->>ParentLifecycle: recompile + commit + emitLessonPlan — Concrete Example
Section titled “LessonPlan — Concrete Example”pub struct LessonPlan { rt: Runtime<Self>, children: HashMap<String, Slot<Session<Whiteboard>, Vec<WhiteboardElement>>>,}
impl HasChildren for LessonPlan { type ChildModality = Whiteboard; type ChildSessionIntent = SessionIntent<WhiteboardIntent>;
fn children(&self) -> &HashMap<...> { &self.children } fn children_mut(&mut self) -> &mut HashMap<...> { &mut self.children } fn selected_child_id(&self) -> Option<&str> { self.rt.state.ephemeral.selected_slide_id.as_deref() } fn reconcile_children(&mut self) { /* sync children map with placements */ }}Session<Whiteboard> uses the default type parameter I = SessionIntent<WhiteboardIntent>. The full session type for the parent:
Session<LessonPlan, ParentSessionIntent<LessonPlanIntent, SessionIntent<WhiteboardIntent>>>Each child Whiteboard session has its own Doc, CompileCache, SessionEphemeral (own AI state), sinks, and version counter. During AI generation, children can run their own subagents independently.
Related Pages
Section titled “Related Pages”- Session Overview — the Session struct and generic parameters
- ReduceIntent —
ParentSessionIntentandParentSessionFeedback - Dispatch Lifecycle — how child and parent lifecycles interact
- Snapshots — parent snapshots include child state