#![cfg_attr(coverage_nightly, coverage(off))]
use super::features::{AgentFeature, QualityLevel};
use super::hybrid::{CoreSpec, WrapperSpec};
use super::templates::AgentTemplate;
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentContext {
pub name: String,
pub template_type: AgentTemplate,
pub features: HashSet<AgentFeature>,
pub quality_level: QualityLevel,
pub deterministic_core: Option<CoreSpec>,
pub probabilistic_wrapper: Option<WrapperSpec>,
}
impl Default for AgentContext {
fn default() -> Self {
Self {
name: String::new(),
template_type: AgentTemplate::MCPToolServer,
features: HashSet::new(),
quality_level: QualityLevel::Standard,
deterministic_core: None,
probabilistic_wrapper: None,
}
}
}
pub struct AgentContextBuilder {
context: AgentContext,
}
impl AgentContextBuilder {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new(name: impl Into<String>, template: impl Into<String>) -> Self {
let template_str = template.into();
let template_type = match template_str.as_str() {
"calculator" => AgentTemplate::DeterministicCalculator,
"state-machine" => AgentTemplate::StateMachineWorkflow,
"hybrid" => AgentTemplate::HybridAnalyzer,
"mcp-server" => AgentTemplate::MCPToolServer,
path if path.starts_with("custom:") => AgentTemplate::CustomAgent(
path.strip_prefix("custom:").expect("internal error").into(),
),
_ => AgentTemplate::MCPToolServer,
};
Self {
context: AgentContext {
name: name.into(),
template_type,
..Default::default()
},
}
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_feature(mut self, feature: AgentFeature) -> Self {
self.context.features.insert(feature);
self
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_feature_str(mut self, feature_str: &str) -> Self {
if let Ok(feature) = feature_str.parse::<AgentFeature>() {
self.context.features.insert(feature);
}
self
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_quality_level(mut self, level: QualityLevel) -> Self {
self.context.quality_level = level;
self
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_deterministic_core(mut self, core: CoreSpec) -> Self {
self.context.deterministic_core = Some(core);
self
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_probabilistic_wrapper(mut self, wrapper: WrapperSpec) -> Self {
self.context.probabilistic_wrapper = Some(wrapper);
self
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn build(self) -> Result<AgentContext> {
let ctx = self.context;
if ctx.name.is_empty() {
bail!("Agent name cannot be empty");
}
if !ctx.name.chars().all(|c| c.is_alphanumeric() || c == '_') {
bail!("Agent name must be alphanumeric with underscores only");
}
if ctx.name.chars().next().is_some_and(char::is_numeric) {
bail!("Agent name cannot start with a number");
}
if matches!(ctx.template_type, AgentTemplate::HybridAnalyzer)
&& (ctx.deterministic_core.is_none() || ctx.probabilistic_wrapper.is_none())
{
bail!("Hybrid agents require both deterministic core and probabilistic wrapper specifications");
}
Ok(ctx)
}
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn create_agent_context(name: &str, template: &str) -> AgentContextBuilder {
AgentContextBuilder::new(name, template)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_context_creation() {
let ctx = AgentContext {
name: "test_agent".to_string(),
template_type: AgentTemplate::MCPToolServer,
features: HashSet::new(),
quality_level: QualityLevel::Extreme,
deterministic_core: None,
probabilistic_wrapper: None,
};
assert_eq!(ctx.name, "test_agent");
assert_eq!(ctx.quality_level, QualityLevel::Extreme);
}
#[test]
fn test_agent_context_builder() {
let result = create_agent_context("my_agent", "mcp-server")
.with_quality_level(QualityLevel::Strict)
.build();
assert!(result.is_ok());
let ctx = result.expect("internal error");
assert_eq!(ctx.name, "my_agent");
assert_eq!(ctx.quality_level, QualityLevel::Strict);
}
#[test]
fn test_invalid_agent_name() {
let result = create_agent_context("", "mcp-server").build();
assert!(result.is_err());
let result = create_agent_context("123agent", "mcp-server").build();
assert!(result.is_err());
let result = create_agent_context("agent-name", "mcp-server").build();
assert!(result.is_err());
}
#[test]
fn test_hybrid_validation() {
let result = create_agent_context("hybrid_agent", "hybrid").build();
assert!(result.is_err());
}
#[test]
fn test_default_agent_context_fields() {
let ctx = AgentContext::default();
assert_eq!(ctx.name, "");
assert!(matches!(ctx.template_type, AgentTemplate::MCPToolServer));
assert!(ctx.features.is_empty());
assert_eq!(ctx.quality_level, QualityLevel::Standard);
assert!(ctx.deterministic_core.is_none());
assert!(ctx.probabilistic_wrapper.is_none());
}
#[test]
fn test_builder_maps_calculator_template_string() {
let ctx = create_agent_context("c", "calculator").build().unwrap();
assert!(matches!(
ctx.template_type,
AgentTemplate::DeterministicCalculator
));
}
#[test]
fn test_builder_maps_state_machine_template_string() {
let ctx = create_agent_context("c", "state-machine").build().unwrap();
assert!(matches!(
ctx.template_type,
AgentTemplate::StateMachineWorkflow
));
}
#[test]
fn test_builder_maps_mcp_server_template_string() {
let ctx = create_agent_context("c", "mcp-server").build().unwrap();
assert!(matches!(ctx.template_type, AgentTemplate::MCPToolServer));
}
#[test]
fn test_builder_maps_hybrid_template_string() {
use crate::scaffold::agent::context::AgentContextBuilder;
let builder = AgentContextBuilder::new("c", "hybrid");
assert!(matches!(
builder.context.template_type,
AgentTemplate::HybridAnalyzer
));
}
#[test]
fn test_builder_maps_custom_prefix_to_custom_agent() {
use crate::scaffold::agent::context::AgentContextBuilder;
let builder = AgentContextBuilder::new("c", "custom:/path/to/tpl.toml");
match &builder.context.template_type {
AgentTemplate::CustomAgent(p) => {
assert_eq!(p.to_string_lossy(), "/path/to/tpl.toml")
}
other => panic!("expected CustomAgent, got {other:?}"),
}
}
#[test]
fn test_builder_falls_back_to_mcp_server_on_unknown_template_string() {
use crate::scaffold::agent::context::AgentContextBuilder;
let builder = AgentContextBuilder::new("c", "bogus-template-string");
assert!(matches!(
builder.context.template_type,
AgentTemplate::MCPToolServer
));
}
#[test]
fn test_with_feature_inserts_feature() {
let ctx = create_agent_context("c", "mcp-server")
.with_feature(AgentFeature::ToolComposition)
.with_feature(AgentFeature::AsyncHandlers)
.build()
.unwrap();
assert_eq!(ctx.features.len(), 2);
assert!(ctx.features.contains(&AgentFeature::ToolComposition));
assert!(ctx.features.contains(&AgentFeature::AsyncHandlers));
}
#[test]
fn test_with_feature_str_parses_valid_feature() {
let ctx = create_agent_context("c", "mcp-server")
.with_feature_str("tool-composition")
.build()
.unwrap();
assert!(ctx.features.contains(&AgentFeature::ToolComposition));
}
#[test]
fn test_with_feature_str_silently_drops_invalid_feature() {
let ctx = create_agent_context("c", "mcp-server")
.with_feature_str("nonexistent-feature")
.build()
.unwrap();
assert!(ctx.features.is_empty());
}
#[test]
fn test_with_quality_level_sets_value() {
let ctx = create_agent_context("c", "mcp-server")
.with_quality_level(QualityLevel::Extreme)
.build()
.unwrap();
assert_eq!(ctx.quality_level, QualityLevel::Extreme);
}
#[test]
fn test_with_deterministic_core_sets_value() {
use crate::scaffold::agent::hybrid::CoreSpec;
let ctx = create_agent_context("c", "mcp-server")
.with_deterministic_core(CoreSpec::default())
.build()
.unwrap();
assert!(ctx.deterministic_core.is_some());
}
#[test]
fn test_with_probabilistic_wrapper_sets_value() {
use crate::scaffold::agent::hybrid::WrapperSpec;
let ctx = create_agent_context("c", "mcp-server")
.with_probabilistic_wrapper(WrapperSpec::default())
.build()
.unwrap();
assert!(ctx.probabilistic_wrapper.is_some());
}
#[test]
fn test_hybrid_build_succeeds_with_both_specs() {
use crate::scaffold::agent::hybrid::{CoreSpec, WrapperSpec};
let ctx = create_agent_context("hybrid_agent", "hybrid")
.with_deterministic_core(CoreSpec::default())
.with_probabilistic_wrapper(WrapperSpec::default())
.build()
.unwrap();
assert!(matches!(ctx.template_type, AgentTemplate::HybridAnalyzer));
assert!(ctx.deterministic_core.is_some());
assert!(ctx.probabilistic_wrapper.is_some());
}
#[test]
fn test_hybrid_build_fails_when_only_core_set() {
use crate::scaffold::agent::hybrid::CoreSpec;
let err = create_agent_context("hybrid_agent", "hybrid")
.with_deterministic_core(CoreSpec::default())
.build()
.unwrap_err()
.to_string();
assert!(err.contains("Hybrid"), "err was: {err}");
}
#[test]
fn test_hybrid_build_fails_when_only_wrapper_set() {
use crate::scaffold::agent::hybrid::WrapperSpec;
let err = create_agent_context("hybrid_agent", "hybrid")
.with_probabilistic_wrapper(WrapperSpec::default())
.build()
.unwrap_err()
.to_string();
assert!(err.contains("Hybrid"), "err was: {err}");
}
#[test]
fn test_build_rejects_non_alphanumeric_name() {
let err = create_agent_context("agent.name", "mcp-server")
.build()
.unwrap_err()
.to_string();
assert!(err.contains("alphanumeric"), "err was: {err}");
}
#[test]
fn test_build_accepts_underscore_in_name() {
let ctx = create_agent_context("valid_name", "mcp-server")
.build()
.unwrap();
assert_eq!(ctx.name, "valid_name");
}
#[test]
fn test_create_agent_context_builder_reuses_same_state() {
use crate::scaffold::agent::context::AgentContextBuilder;
let a = create_agent_context("nm", "calculator").build().unwrap();
let b = AgentContextBuilder::new("nm", "calculator")
.build()
.unwrap();
assert_eq!(a.name, b.name);
assert!(matches!(
a.template_type,
AgentTemplate::DeterministicCalculator
));
assert!(matches!(
b.template_type,
AgentTemplate::DeterministicCalculator
));
}
#[test]
fn test_custom_template_build_succeeds_with_valid_name() {
let ctx = create_agent_context("cust", "custom:/tpl.toml")
.build()
.unwrap();
match &ctx.template_type {
AgentTemplate::CustomAgent(p) => assert_eq!(p.to_string_lossy(), "/tpl.toml"),
other => panic!("expected CustomAgent, got {other:?}"),
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}