use converge_kernel::{
AgentEffect, Budget, Context, ContextKey, ContextState, ConvergeResult, Engine,
ExperienceEventObserver, Suggestor,
};
use converge_pack::ProposalId;
use std::sync::Arc;
struct BoxedAgent(Box<dyn Suggestor>);
#[async_trait::async_trait]
impl Suggestor for BoxedAgent {
fn name(&self) -> &str {
self.0.name()
}
fn dependencies(&self) -> &[ContextKey] {
self.0.dependencies()
}
fn accepts(&self, ctx: &dyn Context) -> bool {
self.0.accepts(ctx)
}
async fn execute(&self, ctx: &dyn Context) -> AgentEffect {
self.0.execute(ctx).await
}
}
pub struct Formation {
pub label: String,
agents: Vec<Box<dyn Suggestor>>,
seeds: Vec<Seed>,
pub budget: Budget,
}
pub struct Seed {
pub key: ContextKey,
pub id: ProposalId,
pub content: String,
pub provenance: String,
}
pub struct FormationResult {
pub label: String,
pub converge_result: ConvergeResult,
}
impl Formation {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
agents: Vec::new(),
seeds: Vec::new(),
budget: Budget::default(),
}
}
pub fn agent(mut self, suggestor: impl Suggestor + 'static) -> Self {
self.agents.push(Box::new(suggestor));
self
}
pub fn agent_boxed(mut self, suggestor: Box<dyn Suggestor>) -> Self {
self.agents.push(suggestor);
self
}
pub fn seed(
mut self,
key: ContextKey,
id: impl Into<ProposalId>,
content: impl Into<String>,
provenance: impl Into<String>,
) -> Self {
self.seeds.push(Seed {
key,
id: id.into(),
content: content.into(),
provenance: provenance.into(),
});
self
}
pub fn with_budget(mut self, budget: Budget) -> Self {
self.budget = budget;
self
}
pub async fn run(self) -> Result<FormationResult, FormationError> {
self.run_observed(None).await
}
pub async fn run_with_event_observer(
self,
observer: Arc<dyn ExperienceEventObserver>,
) -> Result<FormationResult, FormationError> {
self.run_observed(Some(observer)).await
}
async fn run_observed(
self,
observer: Option<Arc<dyn ExperienceEventObserver>>,
) -> Result<FormationResult, FormationError> {
let mut engine = Engine::with_budget(self.budget);
if let Some(observer) = observer {
engine.set_event_observer(observer);
}
for agent in self.agents {
engine.register_suggestor(BoxedAgent(agent));
}
let mut context = ContextState::new();
for seed in &self.seeds {
context
.add_input_with_provenance(
seed.key,
seed.id.clone(),
&seed.content,
&seed.provenance,
)
.map_err(|e| FormationError::ConvergenceFailed(e.to_string()))?;
}
let converge_result = engine
.run(context)
.await
.map_err(|e| FormationError::ConvergenceFailed(e.to_string()))?;
Ok(FormationResult {
label: self.label,
converge_result,
})
}
}
impl Formation {
pub fn with_simulation_swarm(self) -> Self {
use organism_simulation::{
CausalSimulationAgent, CostSimulationAgent, OperationalSimulationAgent,
OutcomeSimulationAgent, PolicySimulationAgent,
};
self.agent(OutcomeSimulationAgent::default_config())
.agent(CostSimulationAgent::default_config())
.agent(PolicySimulationAgent::default_config())
.agent(CausalSimulationAgent::default_config())
.agent(OperationalSimulationAgent::default_config())
}
pub fn with_adversarial_team(self) -> Self {
use organism_adversarial::{
AssumptionBreakerAgent, ConstraintCheckerAgent, EconomicSkepticAgent,
OperationalSkepticAgent,
};
self.agent(AssumptionBreakerAgent::new())
.agent(ConstraintCheckerAgent::default_config())
.agent(EconomicSkepticAgent::default_config())
.agent(OperationalSkepticAgent::default_config())
}
pub fn with_learning_priors(self) -> Self {
use organism_learning::PlanningPriorAgent;
self.agent(PlanningPriorAgent::new())
}
pub fn with_stress_test_pipeline(self) -> Self {
self.with_learning_priors()
.with_adversarial_team()
.with_simulation_swarm()
}
}
#[derive(Debug, thiserror::Error)]
pub enum FormationError {
#[error("convergence failed: {0}")]
ConvergenceFailed(String),
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
const SEED_DEPENDENCIES: &[ContextKey] = &[ContextKey::Seeds];
fn rt() -> tokio::runtime::Runtime {
tokio::runtime::Runtime::new().expect("runtime")
}
struct SeedObserver;
#[async_trait::async_trait]
impl Suggestor for SeedObserver {
fn name(&self) -> &'static str {
"seed-observer"
}
fn dependencies(&self) -> &[ContextKey] {
SEED_DEPENDENCIES
}
fn accepts(&self, ctx: &dyn Context) -> bool {
ctx.has(ContextKey::Seeds) && !ctx.has(ContextKey::Hypotheses)
}
async fn execute(&self, ctx: &dyn Context) -> AgentEffect {
let seed = &ctx.get(ContextKey::Seeds)[0];
AgentEffect::with_proposal(converge_kernel::ProposedFact::new(
ContextKey::Hypotheses,
format!("observed-{}", seed.id),
format!("observed {}", seed.content),
self.name(),
))
}
}
#[tokio::test]
async fn formation_promotes_valid_seed_before_agent_loop() {
let result = Formation::new("valid-seed")
.agent(SeedObserver)
.seed(
ContextKey::Seeds,
"seed-1",
"seed content",
"external-request",
)
.run()
.await
.expect("formation should converge");
assert!(result.converge_result.converged);
assert!(!result.converge_result.context.has_pending_proposals());
let seeds = result.converge_result.context.get(ContextKey::Seeds);
let hypotheses = result.converge_result.context.get(ContextKey::Hypotheses);
assert_eq!(seeds.len(), 1);
assert_eq!(seeds[0].id, "seed-1");
assert_eq!(seeds[0].content, "seed content");
assert_eq!(hypotheses.len(), 1);
assert_eq!(hypotheses[0].id, "observed-seed-1");
assert_eq!(hypotheses[0].content, "observed seed content");
}
#[tokio::test]
async fn formation_rejects_invalid_seed_before_agent_can_observe_it() {
let result = Formation::new("invalid-seed")
.agent(SeedObserver)
.seed(ContextKey::Seeds, "seed-1", " \t\n ", "external-request")
.run()
.await
.expect("formation should converge");
assert!(result.converge_result.converged);
assert!(!result.converge_result.context.has(ContextKey::Seeds));
assert!(!result.converge_result.context.has(ContextKey::Hypotheses));
assert!(!result.converge_result.context.has_pending_proposals());
}
#[test]
fn formation_rejects_conflicting_seed_ids_before_engine_run() {
let result = rt().block_on(
Formation::new("conflict")
.seed(ContextKey::Seeds, "seed-1", "version A", "user")
.seed(ContextKey::Seeds, "seed-1", "version B", "user")
.run(),
);
match result {
Err(FormationError::ConvergenceFailed(message)) => {
assert!(message.contains("conflict detected for fact 'seed-1'"));
}
Ok(_) => panic!("conflicting seeds must fail"),
}
}
proptest! {
#[test]
fn formation_roundtrips_valid_seed_inputs(
id in "[a-z0-9][a-z0-9-]{0,15}",
content in "[A-Za-z0-9][A-Za-z0-9 _-]{0,31}",
provenance in "[a-z][a-z0-9-]{2,15}",
) {
let result = rt()
.block_on(
Formation::new("prop-valid")
.agent(SeedObserver)
.seed(ContextKey::Seeds, id.clone(), content.clone(), provenance)
.run(),
)
.expect("formation should converge");
let seeds = result.converge_result.context.get(ContextKey::Seeds);
let hypotheses = result.converge_result.context.get(ContextKey::Hypotheses);
prop_assert_eq!(seeds.len(), 1);
prop_assert_eq!(&seeds[0].id, &id);
prop_assert_eq!(&seeds[0].content, &content);
prop_assert_eq!(hypotheses.len(), 1);
prop_assert_eq!(&hypotheses[0].content, &format!("observed {content}"));
prop_assert!(!result.converge_result.context.has_pending_proposals());
}
#[test]
fn formation_never_promotes_whitespace_only_seed_content(
id in "[a-z0-9][a-z0-9-]{0,15}",
content in "[ \\t\\n]{1,12}",
provenance in "[a-z][a-z0-9-]{2,15}",
) {
let result = rt()
.block_on(
Formation::new("prop-invalid")
.agent(SeedObserver)
.seed(ContextKey::Seeds, id, content, provenance)
.run(),
)
.expect("formation should converge");
prop_assert!(!result.converge_result.context.has(ContextKey::Seeds));
prop_assert!(!result.converge_result.context.has(ContextKey::Hypotheses));
prop_assert!(!result.converge_result.context.has_pending_proposals());
}
}
}