use std::collections::BTreeMap;
use std::fmt;
use std::sync::Arc;
use converge_kernel::{StopReason, Suggestor};
use crate::compiler::CompiledFormationPlan;
use crate::formation::{Formation, FormationResult, Seed};
use crate::outcome::{FormationOutcomeRecord, FormationOutcomeStatus};
type SuggestorFactory = Arc<dyn Fn() -> Box<dyn Suggestor> + Send + Sync>;
#[derive(Clone, Default)]
pub struct ExecutableSuggestorCatalog {
factories: BTreeMap<String, SuggestorFactory>,
}
pub struct FormationExecutionRecord {
pub plan: CompiledFormationPlan,
pub result: FormationResult,
pub outcome: FormationOutcomeRecord,
}
impl ExecutableSuggestorCatalog {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register_factory<S, F>(
&mut self,
suggestor_id: impl Into<String>,
factory: F,
) -> Result<(), FormationInstantiationError>
where
S: Suggestor + 'static,
F: Fn() -> S + Send + Sync + 'static,
{
self.register_boxed_factory(suggestor_id, move || Box::new(factory()))
}
pub fn register_boxed_factory<F>(
&mut self,
suggestor_id: impl Into<String>,
factory: F,
) -> Result<(), FormationInstantiationError>
where
F: Fn() -> Box<dyn Suggestor> + Send + Sync + 'static,
{
let suggestor_id = suggestor_id.into();
if self.factories.contains_key(&suggestor_id) {
return Err(FormationInstantiationError::DuplicateSuggestorFactory { suggestor_id });
}
self.factories.insert(suggestor_id, Arc::new(factory));
Ok(())
}
#[must_use]
pub fn contains(&self, suggestor_id: &str) -> bool {
self.factories.contains_key(suggestor_id)
}
#[must_use]
pub fn suggestor_ids(&self) -> Vec<&str> {
self.factories.keys().map(String::as_str).collect()
}
pub fn instantiate(
&self,
plan: &CompiledFormationPlan,
seeds: impl IntoIterator<Item = Seed>,
) -> Result<Formation, FormationInstantiationError> {
self.instantiate_with_label(plan, seeds, plan.template_id.clone())
}
pub fn instantiate_with_label(
&self,
plan: &CompiledFormationPlan,
seeds: impl IntoIterator<Item = Seed>,
label: impl Into<String>,
) -> Result<Formation, FormationInstantiationError> {
let missing = plan
.roster
.iter()
.filter(|member| !self.factories.contains_key(member.suggestor_id.as_str()))
.map(|member| member.suggestor_id.to_string())
.collect::<Vec<_>>();
if !missing.is_empty() {
return Err(FormationInstantiationError::MissingSuggestorFactories {
suggestor_ids: missing,
});
}
let mut formation = Formation::new(label);
for seed in seeds {
formation = formation.seed(seed.key, seed.id, seed.content, seed.provenance);
}
for member in &plan.roster {
let factory = self
.factories
.get(member.suggestor_id.as_str())
.ok_or_else(|| FormationInstantiationError::MissingSuggestorFactories {
suggestor_ids: vec![member.suggestor_id.to_string()],
})?;
formation = formation.agent_boxed(factory());
}
Ok(formation)
}
}
impl FormationExecutionRecord {
#[must_use]
pub fn from_plan_and_result(plan: CompiledFormationPlan, result: FormationResult) -> Self {
let status = outcome_status(
&result.converge_result.stop_reason,
result.converge_result.converged,
);
let outcome = FormationOutcomeRecord::from_compiled_plan(&plan, status)
.with_stop_reason(format!("{:?}", result.converge_result.stop_reason));
Self {
plan,
result,
outcome,
}
}
}
impl fmt::Debug for ExecutableSuggestorCatalog {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExecutableSuggestorCatalog")
.field("suggestor_ids", &self.suggestor_ids())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum FormationInstantiationError {
#[error("executable suggestor factory already registered for '{suggestor_id}'")]
DuplicateSuggestorFactory { suggestor_id: String },
#[error(
"compiled formation references suggestors without executable factories: {suggestor_ids:?}"
)]
MissingSuggestorFactories { suggestor_ids: Vec<String> },
}
fn outcome_status(stop_reason: &StopReason, converged: bool) -> FormationOutcomeStatus {
match stop_reason {
StopReason::Converged if converged => FormationOutcomeStatus::Converged,
StopReason::CriteriaMet { .. } => FormationOutcomeStatus::Converged,
StopReason::HumanInterventionRequired { .. } | StopReason::HitlGatePending { .. } => {
FormationOutcomeStatus::NeedsReview
}
StopReason::CycleBudgetExhausted { .. }
| StopReason::FactBudgetExhausted { .. }
| StopReason::TokenBudgetExhausted { .. }
| StopReason::TimeBudgetExhausted { .. } => FormationOutcomeStatus::BudgetExhausted,
StopReason::InvariantViolated { .. } | StopReason::PromotionRejected { .. } => {
FormationOutcomeStatus::CriteriaBlocked
}
StopReason::UserCancelled | StopReason::AgentRefused { .. } | StopReason::Error { .. } => {
FormationOutcomeStatus::Failed
}
_ => FormationOutcomeStatus::Failed,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::provenance::ORGANISM_RUNTIME_PROVENANCE;
use converge_kernel::{AgentEffect, Context, ContextKey};
use converge_pack::{Provenance, ProvenanceSource, TextPayload};
const SEED_DEPENDENCIES: &[ContextKey] = &[ContextKey::Seeds];
struct TestSuggestor {
name: &'static str,
}
#[async_trait::async_trait]
impl Suggestor for TestSuggestor {
fn name(&self) -> &'static str {
self.name
}
fn dependencies(&self) -> &[ContextKey] {
SEED_DEPENDENCIES
}
fn provenance(&self) -> Provenance {
ORGANISM_RUNTIME_PROVENANCE.provenance()
}
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::builder()
.proposal(
crate::provenance::ORGANISM_RUNTIME_PROVENANCE.proposed_fact(
ContextKey::Hypotheses,
format!("{}-{}", self.name, seed.id()),
TextPayload::new("instantiated suggestor ran"),
),
)
.build()
}
}
#[test]
fn rejects_duplicate_factories() {
let mut catalog = ExecutableSuggestorCatalog::new();
catalog
.register_factory("dup", || TestSuggestor { name: "dup-a" })
.expect("first registration should succeed");
let error = catalog
.register_factory("dup", || TestSuggestor { name: "dup-b" })
.expect_err("duplicate registration should fail");
assert_eq!(
error,
FormationInstantiationError::DuplicateSuggestorFactory {
suggestor_id: "dup".to_string()
}
);
}
}