Skip to content

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.

crates/session/src/children.rs [19:50]
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.

crates/core/src/template/slot.rs [21:26]
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>,
>>
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)
  1. On open — all children start Cold. The cached output comes from the parent’s synced state or is empty.

  2. 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.

  3. 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));
    }
  4. 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.

  5. On close — Hot sessions export their docs. The parent caches child output.

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 Cold slots 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.

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 + emit
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.