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.
ValidationRule
Section titled “ValidationRule”pub enum ValidationRule { Expression(String), // CEL expression → hard gate Guideline(String), // Natural language → preamble only Function(fn(&PropertyValue) -> bool), // Rust predicate → hard gate}Expression
Section titled “Expression”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.
Guideline
Section titled “Guideline”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.
Function
Section titled “Function”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.
Auto-generated rules
Section titled “Auto-generated rules”PropertyType automatically generates Expression rules based on its constraints. Components don’t need to manually write CEL for basic type checks:
| PropertyType | Generated 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.
Validate trait
Section titled “Validate trait”pub trait Validate { fn rules(&self) -> Vec<ValidationRule>;
fn validate( &self, eval_cel: &dyn Fn(&str) -> Result<bool, String>, ) -> Vec<ValidationError>;}rules()
Section titled “rules()”Returns all validation rules for this type. For Property, this combines auto-generated type rules with any component-level rules.
validate()
Section titled “validate()”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()}ValidationError
Section titled “ValidationError”pub struct ValidationError { pub rule: String, // The rule that failed (expression text or description) pub message: String, // Human-readable error message}Pending trait
Section titled “Pending trait”Pending checks whether a type still needs AI generation:
pub trait Pending { fn is_pending(&self) -> bool;}Property implementation
Section titled “Property implementation”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 }}Where Pending is used
Section titled “Where Pending is used”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.
Validation in the agent loop
Section titled “Validation in the agent loop”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- The agent uses tools to set property values via intents.
- The agent calls
doneto signal completion. Validate::validate()runs allExpressionandFunctionrules.- If all pass, the agent stops successfully.
- If any fail, the errors are returned as the
donetool’s result. The agent reads the errors and fixes its work. - The agent can also call
validateproactively to check its work before signalingdone.
Implementors
Section titled “Implementors”| Type | Validate | Pending |
|---|---|---|
Property | Auto-generated type rules + component rules | required && value.is_null() |
| Component | Component-level rules | Any required property pending |
| Modality | Modality-level rules | Metadata or any component pending |
| Metadata | Metadata-level rules | Required metadata fields missing |
Related
Section titled “Related”- Properties Overview — PropertySchema, PropertyType, PropertyValue
- CEL Environment — expression evaluation context for rules
- AI Traits — Describe, Agent, Validate, Pending trait signatures
- Subagents — the agent loop that triggers validation