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