Skip to content

Validation

Validation ensures property values satisfy constraints before AI generation completes. Three rule types cover different enforcement strategies, and the Pending trait checks whether required fields still need values.

crates/core/src/template/validate.rs [5:12]
pub enum ValidationRule {
Expression(String), // CEL expression → hard gate
Guideline(String), // Natural language → preamble only
Function(fn(&PropertyValue) -> bool), // Rust predicate → hard gate
}

A CEL expression that must evaluate to true. Checked at done time and when the agent calls validate.

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

If the expression returns false or errors, a ValidationError is produced. The agent reads the error and fixes its work.

Natural language guidance included in the agent’s system prompt. Never evaluated programmatically.

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

Guidelines influence the AI through the preamble but are not enforced as gates. They’re included via Describe when building the agent context.

A Rust-native predicate. Hard gate at done time, but not describable to the AI agent.

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

Use for structural invariants the agent cannot violate — type constraints, format checks that the schema already enforces. Since Function rules can’t be communicated to the agent, they should only catch things the agent wouldn’t produce given correct Expression and Guideline rules.

PropertyType automatically generates Expression rules based on its constraints. Components don’t need to manually write CEL for basic type checks:

PropertyTypeGenerated rule
Text { max_length: Some(100) }"size(value) <= 100"
Number { min: Some(0.0), max: None }"value >= 0.0"
Number { min: None, max: Some(100.0) }"value <= 100.0"
Number { min: Some(0.0), max: Some(100.0) }"value >= 0.0", "value <= 100.0"
Select { options, multiple: false }"value in ['opt1', 'opt2', ...]"

These are combined with any manually-specified rules from the Validate implementation.

crates/core/src/template/validate.rs [34:66]
pub trait Validate {
fn rules(&self) -> Vec<ValidationRule>;
fn validate(
&self,
eval_cel: &dyn Fn(&str) -> Result<bool, String>,
) -> Vec<ValidationError>;
}

Returns all validation rules for this type. For Property, this combines auto-generated type rules with any component-level rules.

Evaluates all Expression and Function rules. Guideline rules are skipped. The eval_cel closure is built from cel_context() at the call site — this indirection keeps modality_core free of executor dependencies.

// Default validate() implementation:
fn validate(&self, eval_cel: &dyn Fn(&str) -> Result<bool, String>) -> Vec<ValidationError> {
self.rules().into_iter().filter_map(|rule| {
match rule {
ValidationRule::Expression(expr) => {
match eval_cel(&expr) {
Ok(true) => None,
Ok(false) => Some(ValidationError {
rule: expr.clone(),
message: format!("Validation failed: {}", expr),
}),
Err(e) => Some(ValidationError {
rule: expr.clone(),
message: format!("CEL error: {}", e),
}),
}
}
ValidationRule::Function(_f) => {
// Skipped in default impl — concrete impls (e.g. Property)
// override to pass the value.
None
}
ValidationRule::Guideline(_) => None, // Never checked
}
}).collect()
}
pub struct ValidationError {
pub rule: String, // The rule that failed (expression text or description)
pub message: String, // Human-readable error message
}

Pending checks whether a type still needs AI generation:

crates/core/src/template/pending.rs [6:8]
pub trait Pending {
fn is_pending(&self) -> bool;
}

A property is pending when it’s required and has no value:

impl Pending for Property {
fn is_pending(&self) -> bool {
self.schema.required && self.value == PropertyValue::Null
}
}

The orchestrator checks is_pending() to decide whether to spawn a subagent:

// Orchestrator decision logic:
if metadata.is_pending() {
run_metadata_subagent(&context).await;
}
if node.is_pending() {
run_node_subagent(&context).await;
}

Components, modalities, and metadata all implement Pending. The typical pattern is checking whether any required property is still Null.

flowchart TD
START[Subagent starts] --> TOOLS[Use tools to fill values]
TOOLS --> CHECK{Agent calls done}
CHECK --> GATE["Validate::validate()"]
GATE -->|all pass| SUCCESS[Agent stops]
GATE -->|errors| ERR[Errors returned to agent]
ERR --> TOOLS
  1. The agent uses tools to set property values via intents.
  2. The agent calls done to signal completion.
  3. Validate::validate() runs all Expression and Function rules.
  4. If all pass, the agent stops successfully.
  5. If any fail, the errors are returned as the done tool’s result. The agent reads the errors and fixes its work.
  6. The agent can also call validate proactively to check its work before signaling done.
TypeValidatePending
PropertyAuto-generated type rules + component rulesrequired && value.is_null()
ComponentComponent-level rulesAny required property pending
ModalityModality-level rulesMetadata or any component pending
MetadataMetadata-level rulesRequired metadata fields missing