Skip to content

CEL Environment

A single cel_context() function builds the CEL evaluation context used by both validation rules and property formulas. Defined in the executor crate, it provides three parameters to every CEL expression.

crates/platform/executor/src/cel_context.rs [22:37]
pub fn cel_context<'a, M: Serialize>(
metadata: &'a M,
props: &'a HashMap<String, PropertyValue>,
value: Option<&'a PropertyValue>,
) -> Context<'a> { ... }
ParameterTypeDescription
metadata&M where M: SerializeThis component’s metadata. Always local, never parent’s.
props&HashMap<String, PropertyValue>All property values including unset ones as null. Every property is present.
valueOption<&PropertyValue>Current property’s value. Useful in per-property expressions.

The metadata parameter is the local component’s metadata — for a whiteboard component, this is WhiteboardMetadata (canvas dimensions, etc.), never the parent lesson plan’s metadata. Metadata types implement Serialize, and the cel crate’s TryIntoValue blanket impl handles conversion automatically.

All property values for the current component, including properties that have no value set (represented as null). This ensures CEL expressions can reference any property by key without worrying about missing keys.

The current property’s value when evaluating a per-property expression (validation rule or formula). Set to None when evaluating component-level or modality-level expressions that are not tied to a specific property.

The Validate trait takes a CEL evaluator closure rather than depending on the executor crate directly. The closure is built from cel_context() at the call site:

// At the call site (e.g., session or bridge code):
let eval_cel = |expr: &str| -> Result<bool, String> {
let ctx = cel_context(&metadata, &props, Some(&value));
evaluate_bool(expr, &ctx)
};
// Passed to Validate::validate():
let errors = property.validate(&eval_cel);

This indirection keeps modality_core free of executor dependencies.

Property formulas use the same cel_context() shape. A formula is a CEL expression that computes a derived property value from other properties and metadata. The context is identical to validation — same metadata, props, and value parameters.

Three rule types interact with the CEL environment differently:

A CEL expression that must evaluate to true. Hard gate at done time.

ValidationRule::Expression("size(value) <= 500".to_string())

Evaluated synchronously via cel_context(). If the expression returns false or errors, a ValidationError is produced. The agent must fix the value and call done again.

Natural language guidance included in the preamble. Never evaluated programmatically.

ValidationRule::Guideline("Use age-appropriate vocabulary for the target grade level".to_string())

Included in the agent’s system prompt via Describe. The agent follows these while generating. They never interact with the CEL environment.

A Rust-native predicate. Hard gate at done time.

ValidationRule::Function(|v| matches!(v, PropertyValue::Text(t) if !t.is_empty()))

Evaluated synchronously but outside CEL. Not describable to AI, so used only for structural invariants the agent cannot violate (e.g., type constraints that the schema already enforces).

The following were removed from the CEL context as part of the architecture simplification:

  • ModalityContext — theme/education level are now metadata fields per modality
  • skills — skill IDs live on metadata; full data is accessed via tool calls
  • sections — structural data accessed through modality state
  • ModalitySession — each modality owns state via Session<M, I>
  • Properties Overview — PropertySchema, PropertyType, PropertyValue
  • ValidationValidate trait that consumes the CEL evaluator
  • Subagents — validation loop that triggers CEL evaluation at done time
  • Bridge — dispatch path that connects tool calls to state mutations