organism_runtime/
execution.rs1use std::collections::BTreeMap;
8use std::fmt;
9use std::sync::Arc;
10
11use converge_kernel::{StopReason, Suggestor};
12
13use crate::compiler::CompiledFormationPlan;
14use crate::formation::{Formation, FormationResult, Seed};
15use crate::outcome::{FormationOutcomeRecord, FormationOutcomeStatus};
16
17type SuggestorFactory = Arc<dyn Fn() -> Box<dyn Suggestor> + Send + Sync>;
18
19#[derive(Clone, Default)]
21pub struct ExecutableSuggestorCatalog {
22 factories: BTreeMap<String, SuggestorFactory>,
23}
24
25pub struct FormationExecutionRecord {
27 pub plan: CompiledFormationPlan,
28 pub result: FormationResult,
29 pub outcome: FormationOutcomeRecord,
30}
31
32impl ExecutableSuggestorCatalog {
33 #[must_use]
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn register_factory<S, F>(
40 &mut self,
41 suggestor_id: impl Into<String>,
42 factory: F,
43 ) -> Result<(), FormationInstantiationError>
44 where
45 S: Suggestor + 'static,
46 F: Fn() -> S + Send + Sync + 'static,
47 {
48 self.register_boxed_factory(suggestor_id, move || Box::new(factory()))
49 }
50
51 pub fn register_boxed_factory<F>(
53 &mut self,
54 suggestor_id: impl Into<String>,
55 factory: F,
56 ) -> Result<(), FormationInstantiationError>
57 where
58 F: Fn() -> Box<dyn Suggestor> + Send + Sync + 'static,
59 {
60 let suggestor_id = suggestor_id.into();
61 if self.factories.contains_key(&suggestor_id) {
62 return Err(FormationInstantiationError::DuplicateSuggestorFactory { suggestor_id });
63 }
64
65 self.factories.insert(suggestor_id, Arc::new(factory));
66 Ok(())
67 }
68
69 #[must_use]
70 pub fn contains(&self, suggestor_id: &str) -> bool {
71 self.factories.contains_key(suggestor_id)
72 }
73
74 #[must_use]
75 pub fn suggestor_ids(&self) -> Vec<&str> {
76 self.factories.keys().map(String::as_str).collect()
77 }
78
79 pub fn instantiate(
86 &self,
87 plan: &CompiledFormationPlan,
88 seeds: impl IntoIterator<Item = Seed>,
89 ) -> Result<Formation, FormationInstantiationError> {
90 self.instantiate_with_label(plan, seeds, plan.template_id.clone())
91 }
92
93 pub fn instantiate_with_label(
99 &self,
100 plan: &CompiledFormationPlan,
101 seeds: impl IntoIterator<Item = Seed>,
102 label: impl Into<String>,
103 ) -> Result<Formation, FormationInstantiationError> {
104 let missing = plan
105 .roster
106 .iter()
107 .filter(|member| !self.factories.contains_key(member.suggestor_id.as_str()))
108 .map(|member| member.suggestor_id.to_string())
109 .collect::<Vec<_>>();
110
111 if !missing.is_empty() {
112 return Err(FormationInstantiationError::MissingSuggestorFactories {
113 suggestor_ids: missing,
114 });
115 }
116
117 let mut formation = Formation::new(label);
118 for seed in seeds {
119 formation = formation.seed(seed.key, seed.id, seed.content, seed.provenance);
120 }
121
122 for member in &plan.roster {
123 let factory = self
124 .factories
125 .get(member.suggestor_id.as_str())
126 .ok_or_else(|| FormationInstantiationError::MissingSuggestorFactories {
127 suggestor_ids: vec![member.suggestor_id.to_string()],
128 })?;
129 formation = formation.agent_boxed(factory());
130 }
131
132 Ok(formation)
133 }
134}
135
136impl FormationExecutionRecord {
137 #[must_use]
138 pub fn from_plan_and_result(plan: CompiledFormationPlan, result: FormationResult) -> Self {
139 let status = outcome_status(
140 &result.converge_result.stop_reason,
141 result.converge_result.converged,
142 );
143 let outcome = FormationOutcomeRecord::from_compiled_plan(&plan, status)
144 .with_stop_reason(format!("{:?}", result.converge_result.stop_reason));
145
146 Self {
147 plan,
148 result,
149 outcome,
150 }
151 }
152}
153
154impl fmt::Debug for ExecutableSuggestorCatalog {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 f.debug_struct("ExecutableSuggestorCatalog")
157 .field("suggestor_ids", &self.suggestor_ids())
158 .finish()
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
163pub enum FormationInstantiationError {
164 #[error("executable suggestor factory already registered for '{suggestor_id}'")]
165 DuplicateSuggestorFactory { suggestor_id: String },
166 #[error(
167 "compiled formation references suggestors without executable factories: {suggestor_ids:?}"
168 )]
169 MissingSuggestorFactories { suggestor_ids: Vec<String> },
170}
171
172fn outcome_status(stop_reason: &StopReason, converged: bool) -> FormationOutcomeStatus {
173 match stop_reason {
174 StopReason::Converged if converged => FormationOutcomeStatus::Converged,
175 StopReason::CriteriaMet { .. } => FormationOutcomeStatus::Converged,
176 StopReason::HumanInterventionRequired { .. } | StopReason::HitlGatePending { .. } => {
177 FormationOutcomeStatus::NeedsReview
178 }
179 StopReason::CycleBudgetExhausted { .. }
180 | StopReason::FactBudgetExhausted { .. }
181 | StopReason::TokenBudgetExhausted { .. }
182 | StopReason::TimeBudgetExhausted { .. } => FormationOutcomeStatus::BudgetExhausted,
183 StopReason::InvariantViolated { .. } | StopReason::PromotionRejected { .. } => {
184 FormationOutcomeStatus::CriteriaBlocked
185 }
186 StopReason::UserCancelled | StopReason::AgentRefused { .. } | StopReason::Error { .. } => {
187 FormationOutcomeStatus::Failed
188 }
189 _ => FormationOutcomeStatus::Failed,
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::provenance::ORGANISM_RUNTIME_PROVENANCE;
197 use converge_kernel::{AgentEffect, Context, ContextKey};
198 use converge_pack::{Provenance, ProvenanceSource, TextPayload};
199
200 const SEED_DEPENDENCIES: &[ContextKey] = &[ContextKey::Seeds];
201
202 struct TestSuggestor {
203 name: &'static str,
204 }
205
206 #[async_trait::async_trait]
207 impl Suggestor for TestSuggestor {
208 fn name(&self) -> &'static str {
209 self.name
210 }
211
212 fn dependencies(&self) -> &[ContextKey] {
213 SEED_DEPENDENCIES
214 }
215
216 fn provenance(&self) -> Provenance {
217 ORGANISM_RUNTIME_PROVENANCE.provenance()
218 }
219
220 fn accepts(&self, ctx: &dyn Context) -> bool {
221 ctx.has(ContextKey::Seeds) && !ctx.has(ContextKey::Hypotheses)
222 }
223
224 async fn execute(&self, ctx: &dyn Context) -> AgentEffect {
225 let seed = &ctx.get(ContextKey::Seeds)[0];
226 AgentEffect::builder()
227 .proposal(
228 crate::provenance::ORGANISM_RUNTIME_PROVENANCE.proposed_fact(
229 ContextKey::Hypotheses,
230 format!("{}-{}", self.name, seed.id()),
231 TextPayload::new("instantiated suggestor ran"),
232 ),
233 )
234 .build()
235 }
236 }
237
238 #[test]
239 fn rejects_duplicate_factories() {
240 let mut catalog = ExecutableSuggestorCatalog::new();
241 catalog
242 .register_factory("dup", || TestSuggestor { name: "dup-a" })
243 .expect("first registration should succeed");
244
245 let error = catalog
246 .register_factory("dup", || TestSuggestor { name: "dup-b" })
247 .expect_err("duplicate registration should fail");
248
249 assert_eq!(
250 error,
251 FormationInstantiationError::DuplicateSuggestorFactory {
252 suggestor_id: "dup".to_string()
253 }
254 );
255 }
256}