1#![allow(clippy::unnecessary_literal_bound)]
8use crate::agent::Suggestor;
30use crate::context::{ContextKey, ProposedFact};
31use crate::effect::AgentEffect;
32
33pub struct SeedSuggestor {
40 fact_id: String,
41 content: String,
42}
43
44impl SeedSuggestor {
45 #[must_use]
47 pub fn new(fact_id: impl Into<String>, content: impl Into<String>) -> Self {
48 Self {
49 fact_id: fact_id.into(),
50 content: content.into(),
51 }
52 }
53}
54
55#[async_trait::async_trait]
56impl Suggestor for SeedSuggestor {
57 fn name(&self) -> &str {
58 "SeedSuggestor"
59 }
60
61 fn dependencies(&self) -> &[ContextKey] {
62 &[] }
64
65 fn accepts(&self, ctx: &dyn crate::Context) -> bool {
66 !ctx.get(ContextKey::Seeds)
68 .iter()
69 .any(|f| f.id().as_str() == self.fact_id)
70 }
71
72 async fn execute(&self, _ctx: &dyn crate::Context) -> AgentEffect {
73 AgentEffect::with_proposal(ProposedFact::new(
74 ContextKey::Seeds,
75 self.fact_id.clone(),
76 self.content.clone(),
77 self.name(),
78 ))
79 }
80}
81
82pub struct ReactOnceSuggestor {
89 fact_id: String,
90 content: String,
91}
92
93impl ReactOnceSuggestor {
94 #[must_use]
96 pub fn new(fact_id: impl Into<String>, content: impl Into<String>) -> Self {
97 Self {
98 fact_id: fact_id.into(),
99 content: content.into(),
100 }
101 }
102}
103
104#[async_trait::async_trait]
105impl Suggestor for ReactOnceSuggestor {
106 fn name(&self) -> &str {
107 "ReactOnceSuggestor"
108 }
109
110 fn dependencies(&self) -> &[ContextKey] {
111 &[ContextKey::Seeds] }
113
114 fn accepts(&self, ctx: &dyn crate::Context) -> bool {
115 ctx.has(ContextKey::Seeds)
117 && !ctx
118 .get(ContextKey::Hypotheses)
119 .iter()
120 .any(|f| f.id().as_str() == self.fact_id)
121 }
122
123 async fn execute(&self, _ctx: &dyn crate::Context) -> AgentEffect {
124 AgentEffect::with_proposal(ProposedFact::new(
125 ContextKey::Hypotheses,
126 self.fact_id.clone(),
127 self.content.clone(),
128 self.name(),
129 ))
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::context::ContextState;
137 use crate::engine::Engine;
138
139 #[tokio::test]
140 async fn seed_agent_emits_once() {
141 let mut engine = Engine::new();
142 engine.register_suggestor(SeedSuggestor::new("s1", "value"));
143
144 let result = engine.run(ContextState::new()).await.expect("converges");
145
146 assert!(result.converged);
147 assert_eq!(result.context.get(ContextKey::Seeds).len(), 1);
148 }
149
150 #[tokio::test]
151 async fn react_once_agent_chains_from_seed() {
152 let mut engine = Engine::new();
153 engine.register_suggestor(SeedSuggestor::new("s1", "seed"));
154 engine.register_suggestor(ReactOnceSuggestor::new("h1", "hypothesis"));
155
156 let result = engine.run(ContextState::new()).await.expect("converges");
157
158 assert!(result.converged);
159 assert!(result.context.has(ContextKey::Seeds));
160 assert!(result.context.has(ContextKey::Hypotheses));
161 }
162
163 #[tokio::test]
164 async fn multiple_seeds_all_converge() {
165 let mut engine = Engine::new();
166 engine.register_suggestor(SeedSuggestor::new("s1", "first"));
167 engine.register_suggestor(SeedSuggestor::new("s2", "second"));
168 engine.register_suggestor(SeedSuggestor::new("s3", "third"));
169
170 let result = engine.run(ContextState::new()).await.expect("converges");
171
172 assert!(result.converged);
173 assert_eq!(result.context.get(ContextKey::Seeds).len(), 3);
174 }
175
176 #[tokio::test]
177 async fn chain_of_three_converges() {
178 struct StrategyAgent;
180
181 #[async_trait::async_trait]
182 impl Suggestor for StrategyAgent {
183 fn name(&self) -> &str {
184 "StrategyAgent"
185 }
186
187 fn dependencies(&self) -> &[ContextKey] {
188 &[ContextKey::Hypotheses]
189 }
190
191 fn accepts(&self, ctx: &dyn crate::Context) -> bool {
192 ctx.has(ContextKey::Hypotheses) && !ctx.has(ContextKey::Strategies)
193 }
194
195 async fn execute(&self, _ctx: &dyn crate::Context) -> AgentEffect {
196 AgentEffect::with_proposal(ProposedFact::new(
197 ContextKey::Strategies,
198 "strat-1",
199 "derived strategy",
200 self.name(),
201 ))
202 }
203 }
204
205 let mut engine = Engine::new();
206 engine.register_suggestor(SeedSuggestor::new("s1", "seed"));
207 engine.register_suggestor(ReactOnceSuggestor::new("h1", "hypothesis"));
208 engine.register_suggestor(StrategyAgent);
209
210 let result = engine.run(ContextState::new()).await.expect("converges");
211
212 assert!(result.converged);
213 assert!(result.context.has(ContextKey::Seeds));
214 assert!(result.context.has(ContextKey::Hypotheses));
215 assert!(result.context.has(ContextKey::Strategies));
216 }
217}