Skip to content

AI Traits

Three core traits plus two Modality methods govern how types participate in AI generation. The traits live in modality_core with zero platform dependencies.

Universal self-description as text. Used for prompt composition, CEL documentation, and UI display.

crates/core/src/template/describe.rs [5:7]
pub trait Describe {
fn describe(&self) -> String;
}

Self-contained — every impl describes only itself. No type ever knows about its parents. The orchestrator concatenates ancestor descriptions when building context for child subagents. Tree awareness lives entirely in the orchestrator, never in the types themselves.

TypeExample describe() output
PropertyType"text (max 500 chars)"
PropertySchema"difficulty: one of easy, medium, hard -- How challenging"
PropertyValue"Photosynthesis"
Property"difficulty: hard (one of: easy, medium, hard)"
ValidationRule"value must be at most 500 characters"
ComponentPlacement<P>"text_box at (100, 200, 400, 300)"
WhiteboardPosition"x:100 y:200 w:400 h:300"
GridPosition"row 2, col 3"
SlidePosition"slide in section X, enabled, 5min"
Metadata (per-modality)"canvas: 1920x1080"
Component"TextBox: renders text with font, size, color"
Modality"Whiteboard: canvas with positioned components"

There is no standalone Agent trait. Instead, Modality provides the AI surface via two default methods:

crates/core/src/template/modality.rs [110:121]
// On trait Modality:
fn system_prompt(&self) -> String { String::new() }
fn agent_config(&self) -> AgentConfig { AgentConfig::default() }
crates/core/src/state/traits.rs [117:131]
pub struct AgentConfig {
pub preamble: Option<String>, // static role instructions
pub temperature: Option<f64>, // 0.0–2.0
pub max_tokens: Option<u64>,
pub model: Option<String>, // override global model
pub max_turns: u32, // multi-turn tool-call limit (default 50)
pub validate: bool, // inject DoneTool/ValidateTool
}

Modalities override system_prompt() to provide context-rich prompts describing canvas, components, and current state. agent_config() sets defaults like temperature. The runner merges the static preamble with the dynamic system_prompt().

Tools come from CommandTool on the modality’s intent enum — not from a trait method. Each non-hidden intent variant becomes a rig tool via #[derive(CommandTool)].

Validation rules that constrain property values and modality state.

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> {
// Default: evaluate Expression + Function rules.
// Guideline rules are NOT checked.
}
}

Takes a CEL evaluator closure, which avoids modality_core depending on the executor crate. The closure is built from cel_context() at the call site, using the same context shape used for formulas.

Three ValidationRule variants with different enforcement strategies:

crates/core/src/template/validate.rs [5:12]
pub enum ValidationRule {
Expression(String), // CEL expression -> bool. Hard gate.
Guideline(String), // Natural language. Preamble only. Never checked.
Function(fn(&PropertyValue) -> bool), // Rust native predicate. Hard gate.
}
VariantEvaluated whenEnforcementAI-generatable
Expressiondone tool callHard gate — blocks completionYes
GuidelineNeverIncluded in preamble via DescribeYes
Functiondone tool callHard gate — blocks completionNo (hand-written)
  • Expression rules are CEL strings evaluated synchronously when the agent calls the done tool. If any fail, errors are returned to the agent to fix.
  • Guideline rules are natural language injected into the system prompt. The agent follows them while generating. They are never evaluated programmatically.
  • Function rules are Rust-native predicates evaluated at done time. Not describable to AI, so only used for invariants the agent cannot violate.

ValidationRule implements Describe — Expression and Guideline return their string content, Function returns "(native validation)".

Determines whether something needs AI resolution.

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

The meaning of “pending” varies by type:

Typeis_pending() returns true when
PropertyRequired and has no value
ComponentAny required property is pending
ModalityProps or placements are pending
Metadata (per-modality)Any required metadata field is unset

Optional or defaulted fields do not trigger generation, but may be filled during the same agent call if the agent chooses. The orchestrator uses is_pending() to decide whether to run a subagent at each level.

TypeDescribeValidatePendingAI methods
PropertyTypex
Propertyxxx
PropertySchemax
PropertyValuex
ValidationRulex
ComponentPlacement<P>x
Position typesx
Metadata (per-modality)xxx
Componentxxx
Modalityxxxsystem_prompt(), agent_config()
  • Orchestrator — deterministic code that drives Describe/Pending checks
  • Subagents — rig agents that use system_prompt() and CommandTool tools
  • CEL Environment — the evaluator closure passed to Validate::validate()
  • Bridge — how CommandTool intent variants become rig tools
  • Modalitysystem_prompt() and agent_config() methods