Skip to main content

converge_pack/
pack_suggestor.rs

1//! Suggestor adapter -- bridges Pack trait to Converge Suggestor.
2//!
3//! Every domain pack becomes a first-class Suggestor, participatable
4//! in the convergence loop via `PackSuggestor`.
5
6use async_trait::async_trait;
7
8use crate::Suggestor;
9use crate::context::{Context, ContextKey};
10use crate::effect::AgentEffect;
11use crate::fact::ProposedFact;
12use crate::gate::{ObjectiveSpec, ProblemSpec};
13use crate::pack::Pack;
14
15/// Wraps any Pack as a Converge Suggestor.
16///
17/// The adapter reads problem specifications from context (`input_key`),
18/// runs the solver, and proposes the solution as a fact to `output_key`.
19pub struct PackSuggestor<P: Pack> {
20    pack: P,
21    input_key: ContextKey,
22    output_key: ContextKey,
23}
24
25impl<P: Pack> PackSuggestor<P> {
26    /// Create a new `PackSuggestor` wrapping the given pack.
27    pub fn new(pack: P, input_key: ContextKey, output_key: ContextKey) -> Self {
28        Self {
29            pack,
30            input_key,
31            output_key,
32        }
33    }
34}
35
36#[async_trait]
37impl<P: Pack> Suggestor for PackSuggestor<P> {
38    fn name(&self) -> &str {
39        self.pack.name()
40    }
41
42    fn dependencies(&self) -> &[ContextKey] {
43        std::slice::from_ref(&self.input_key)
44    }
45
46    fn accepts(&self, ctx: &dyn Context) -> bool {
47        ctx.has(self.input_key) && !ctx.has(self.output_key)
48    }
49
50    async fn execute(&self, ctx: &dyn Context) -> AgentEffect {
51        let facts = ctx.get(self.input_key);
52        let Some(seed_fact) = facts.first() else {
53            return AgentEffect::empty();
54        };
55
56        let inputs: serde_json::Value = match serde_json::from_str(&seed_fact.content) {
57            Ok(v) => v,
58            Err(_) => return AgentEffect::empty(),
59        };
60
61        let spec = match ProblemSpec::builder(format!("{}-converge", self.pack.name()), "converge")
62            .objective(ObjectiveSpec::maximize("default"))
63            .inputs_raw(inputs)
64            .build()
65        {
66            Ok(s) => s,
67            Err(_) => return AgentEffect::empty(),
68        };
69
70        match self.pack.solve(&spec) {
71            Ok(result) => {
72                let content = serde_json::to_string(&result.plan).unwrap_or_default();
73                let confidence = result.plan.confidence();
74                let proposal = ProposedFact::new(
75                    self.output_key,
76                    format!("{}-solution", self.pack.name()),
77                    content,
78                    format!("solver:{}", self.pack.name()),
79                )
80                .with_confidence(confidence);
81                AgentEffect::with_proposal(proposal)
82            }
83            Err(_) => AgentEffect::empty(),
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn pack_suggestor_constructed() {
94        // Basic construction test -- full integration tests live in downstream crates
95        struct DummyPack;
96        impl Pack for DummyPack {
97            fn name(&self) -> &'static str {
98                "dummy"
99            }
100            fn version(&self) -> &'static str {
101                "0.1.0"
102            }
103            fn validate_inputs(&self, _: &serde_json::Value) -> crate::gate::GateResult<()> {
104                Ok(())
105            }
106            fn invariants(&self) -> &[crate::pack::InvariantDef] {
107                &[]
108            }
109            fn solve(
110                &self,
111                _: &ProblemSpec,
112            ) -> crate::gate::GateResult<crate::pack::PackSolveResult> {
113                Err(crate::gate::GateError::invalid_input("not implemented"))
114            }
115            fn check_invariants(
116                &self,
117                _: &crate::gate::ProposedPlan,
118            ) -> crate::gate::GateResult<Vec<crate::pack::InvariantResult>> {
119                Ok(vec![])
120            }
121            fn evaluate_gate(
122                &self,
123                _: &crate::gate::ProposedPlan,
124                _: &[crate::pack::InvariantResult],
125            ) -> crate::gate::PromotionGate {
126                crate::gate::PromotionGate::auto_promote("ok")
127            }
128        }
129
130        let s = PackSuggestor::new(DummyPack, ContextKey::Seeds, ContextKey::Strategies);
131        assert_eq!(s.name(), "dummy");
132        assert_eq!(s.dependencies(), &[ContextKey::Seeds]);
133    }
134}