pub struct InteractiveScaffolder {
term: Term,
theme: ColorfulTheme,
}
impl InteractiveScaffolder {
#[must_use]
pub fn new() -> Self {
Self {
term: Term::stdout(),
theme: ColorfulTheme::default(),
}
}
pub fn run(&mut self) -> Result<AgentContext> {
self.term.clear_screen()?;
self.show_header()?;
let name = self.prompt_name()?;
let template = self.prompt_template()?;
let features = self.prompt_features(&template)?;
let quality = self.prompt_quality_level()?;
let mut builder = AgentContextBuilder::new(&name, self.template_to_string(&template));
for feature in &features {
builder = builder.with_feature(feature.clone());
}
builder = builder.with_quality_level(quality);
if matches!(template, AgentTemplate::HybridAnalyzer) {
let core = self.prompt_deterministic_core()?;
let wrapper = self.prompt_probabilistic_wrapper()?;
builder = builder
.with_deterministic_core(core)
.with_probabilistic_wrapper(wrapper);
}
let ctx = builder.build()?;
self.confirm_and_display(&ctx)?;
Ok(ctx)
}
fn show_header(&self) -> Result<()> {
println!("╔══════════════════════════════════════════╗");
println!("║ PMAT Agent Scaffolder v0.1.0 ║");
println!("║ Interactive Agent Creation Wizard ║");
println!("╚══════════════════════════════════════════╝");
println!();
Ok(())
}
fn prompt_name(&self) -> Result<String> {
loop {
let name: String = Input::with_theme(&self.theme)
.with_prompt("Agent name")
.validate_with(|input: &String| -> Result<(), &str> {
if input.is_empty() {
Err("Name cannot be empty")
} else if !input.chars().all(|c| c.is_alphanumeric() || c == '_') {
Err("Name must be alphanumeric with underscores only")
} else if input.chars().next().is_some_and(char::is_numeric) {
Err("Name cannot start with a number")
} else {
Ok(())
}
})
.interact_text()?;
if name.len() > 64 {
eprintln!("Warning: Name is quite long. Consider a shorter name.");
if !Confirm::with_theme(&self.theme)
.with_prompt("Continue with this name?")
.interact()?
{
continue;
}
}
return Ok(name);
}
}
fn prompt_template(&self) -> Result<AgentTemplate> {
let items = [
("MCP Tool Server", "Standard MCP server with async handlers"),
(
"State Machine Workflow",
"Agent with state transitions and invariants",
),
(
"Deterministic Calculator",
"Pure deterministic computation agent",
),
("Hybrid Analyzer", "Deterministic core with AI wrapper"),
("Custom Template", "Use a custom template from file"),
];
let selection = Select::with_theme(&self.theme)
.with_prompt("Select template type")
.items(
items
.iter()
.map(|(name, desc)| format!("{name}\n {desc}"))
.collect::<Vec<_>>(),
)
.default(0)
.interact()?;
Ok(match selection {
0 => AgentTemplate::MCPToolServer,
1 => AgentTemplate::StateMachineWorkflow,
2 => AgentTemplate::DeterministicCalculator,
3 => AgentTemplate::HybridAnalyzer,
4 => {
let path: String = Input::with_theme(&self.theme)
.with_prompt("Custom template path")
.interact_text()?;
AgentTemplate::CustomAgent(PathBuf::from(path))
}
_ => unreachable!(),
})
}
fn prompt_features(&self, template: &AgentTemplate) -> Result<HashSet<AgentFeature>> {
let available_features = self.get_available_features(template);
if available_features.is_empty() {
return Ok(HashSet::new());
}
let feature_names: Vec<String> = available_features
.iter()
.map(|f| self.feature_to_string(f))
.collect();
let selections = MultiSelect::with_theme(&self.theme)
.with_prompt("Select features to include (Space to select, Enter to confirm)")
.items(&feature_names)
.interact()?;
let mut features = HashSet::new();
for idx in selections {
features.insert(available_features[idx].clone());
}
Ok(features)
}
fn get_available_features(&self, template: &AgentTemplate) -> Vec<AgentFeature> {
match template {
AgentTemplate::MCPToolServer => vec![
AgentFeature::ToolComposition,
AgentFeature::AsyncHandlers,
AgentFeature::ResourceSubscriptions,
AgentFeature::Monitoring {
backend: super::features::MonitoringBackend::Prometheus,
},
AgentFeature::Tracing {
exporter: super::features::TraceExporter::OTLP,
},
AgentFeature::HealthChecks,
],
AgentTemplate::StateMachineWorkflow => vec![
AgentFeature::StateMachine {
states: vec![
"Initial".to_string(),
"Processing".to_string(),
"Complete".to_string(),
],
},
AgentFeature::QualityGates {
level: QualityLevel::Extreme,
},
],
AgentTemplate::HybridAnalyzer => vec![
AgentFeature::ComplexityAnalysis,
AgentFeature::SATDDetection,
AgentFeature::DeadCodeElimination,
],
_ => vec![],
}
}
fn feature_to_string(&self, feature: &AgentFeature) -> String {
match feature {
AgentFeature::StateMachine { .. } => "State Machine with transitions".to_string(),
AgentFeature::QualityGates { .. } => "Quality Gates enforcement".to_string(),
AgentFeature::ToolComposition => "Tool Composition support".to_string(),
AgentFeature::AsyncHandlers => "Async request handlers".to_string(),
AgentFeature::ResourceSubscriptions => "Resource subscriptions".to_string(),
AgentFeature::ComplexityAnalysis => "Complexity analysis".to_string(),
AgentFeature::SATDDetection => "SATD detection".to_string(),
AgentFeature::DeadCodeElimination => "Dead code elimination".to_string(),
AgentFeature::Monitoring { .. } => "Monitoring integration".to_string(),
AgentFeature::Tracing { .. } => "Distributed tracing".to_string(),
AgentFeature::HealthChecks => "Health check endpoints".to_string(),
}
}
fn prompt_quality_level(&self) -> Result<QualityLevel> {
let items = [
("Standard", "Basic quality checks, suitable for prototypes"),
("Strict", "Zero warnings, high test coverage"),
(
"Extreme (Toyota Way)",
"Zero SATD, max complexity 10, full verification",
),
];
let selection = Select::with_theme(&self.theme)
.with_prompt("Quality level")
.items(
items
.iter()
.map(|(name, desc)| format!("{name}\n {desc}"))
.collect::<Vec<_>>(),
)
.default(1)
.interact()?;
Ok(match selection {
0 => QualityLevel::Standard,
1 => QualityLevel::Strict,
2 => QualityLevel::Extreme,
_ => unreachable!(),
})
}
fn prompt_deterministic_core(&self) -> Result<CoreSpec> {
println!("\n=== Deterministic Core Configuration ===");
let verification_items = ["Property-based tests", "Formal proof", "Model checking"];
let verification_idx = Select::with_theme(&self.theme)
.with_prompt("Verification method")
.items(verification_items)
.default(0)
.interact()?;
let verification_method = match verification_idx {
0 => VerificationMethod::PropertyTests,
1 => VerificationMethod::FormalProof,
2 => VerificationMethod::ModelChecking,
_ => unreachable!(),
};
let max_complexity = Input::with_theme(&self.theme)
.with_prompt("Maximum cyclomatic complexity")
.default(10)
.validate_with(|input: &u32| -> Result<(), &str> {
if *input == 0 {
Err("Complexity must be at least 1")
} else if *input > 50 {
Err("Complexity should not exceed 50")
} else {
Ok(())
}
})
.interact()?;
Ok(CoreSpec {
verification_method,
max_complexity,
invariants: Vec::new(),
})
}
fn prompt_probabilistic_wrapper(&self) -> Result<WrapperSpec> {
println!("\n=== Probabilistic Wrapper Configuration ===");
let model_items = ["GPT-4", "Claude", "Local model"];
let model_idx = Select::with_theme(&self.theme)
.with_prompt("AI model type")
.items(model_items)
.default(0)
.interact()?;
let model_type = match model_idx {
0 => ModelType::GPT4,
1 => ModelType::Claude,
2 => {
let path = Input::with_theme(&self.theme)
.with_prompt("Local model path")
.interact_text()?;
ModelType::Local(path)
}
_ => unreachable!(),
};
let fallback_items = ["Deterministic fallback", "Default value", "Return error"];
let fallback_idx = Select::with_theme(&self.theme)
.with_prompt("Fallback strategy")
.items(fallback_items)
.default(0)
.interact()?;
let fallback_strategy = match fallback_idx {
0 => FallbackStrategy::Deterministic,
1 => FallbackStrategy::DefaultValue,
2 => FallbackStrategy::Error,
_ => unreachable!(),
};
let confidence_threshold = Input::with_theme(&self.theme)
.with_prompt("Confidence threshold (0.0-1.0)")
.default(0.95)
.validate_with(|input: &f64| -> Result<(), &str> {
if *input < 0.0 || *input > 1.0 {
Err("Threshold must be between 0.0 and 1.0")
} else {
Ok(())
}
})
.interact()?;
Ok(WrapperSpec {
model_type,
fallback_strategy,
confidence_threshold,
})
}
fn template_to_string(&self, template: &AgentTemplate) -> String {
match template {
AgentTemplate::MCPToolServer => "mcp-server".to_string(),
AgentTemplate::StateMachineWorkflow => "state-machine".to_string(),
AgentTemplate::DeterministicCalculator => "calculator".to_string(),
AgentTemplate::HybridAnalyzer => "hybrid".to_string(),
AgentTemplate::CustomAgent(path) => format!("custom:{}", path.display()),
}
}
fn confirm_and_display(&self, ctx: &AgentContext) -> Result<()> {
println!("\n╔══════════════════════════════════════════╗");
println!("║ Agent Configuration Summary ║");
println!("╚══════════════════════════════════════════╝");
println!();
println!(" Name: {}", ctx.name);
println!(" Template: {:?}", ctx.template_type);
println!(" Quality: {:?}", ctx.quality_level);
println!(" Features: {} enabled", ctx.features.len());
if let Some(core) = &ctx.deterministic_core {
println!("\n Deterministic Core:");
println!(" Verification: {:?}", core.verification_method);
println!(" Max Complexity: {}", core.max_complexity);
}
if let Some(wrapper) = &ctx.probabilistic_wrapper {
println!("\n Probabilistic Wrapper:");
println!(" Model: {:?}", wrapper.model_type);
println!(" Fallback: {:?}", wrapper.fallback_strategy);
println!(" Confidence: {:.2}", wrapper.confidence_threshold);
}
println!();
let confirm = Confirm::with_theme(&self.theme)
.with_prompt("Generate agent with these settings?")
.default(true)
.interact()?;
if !confirm {
return Err(ScaffoldError::UserCancelled.into());
}
Ok(())
}
}
impl Default for InteractiveScaffolder {
fn default() -> Self {
Self::new()
}
}