1#![allow(clippy::unnecessary_literal_bound)]
11use crate::agent::Agent;
33use crate::context::{Context, ContextKey, Fact};
34use crate::effect::AgentEffect;
35
36pub struct SeedAgent {
43 fact_id: String,
44 content: String,
45}
46
47impl SeedAgent {
48 #[must_use]
50 pub fn new(fact_id: impl Into<String>, content: impl Into<String>) -> Self {
51 Self {
52 fact_id: fact_id.into(),
53 content: content.into(),
54 }
55 }
56}
57
58impl Agent for SeedAgent {
59 fn name(&self) -> &str {
60 "SeedAgent"
61 }
62
63 fn dependencies(&self) -> &[ContextKey] {
64 &[] }
66
67 fn accepts(&self, ctx: &Context) -> bool {
68 !ctx.get(ContextKey::Seeds)
70 .iter()
71 .any(|f| f.id == self.fact_id)
72 }
73
74 fn execute(&self, _ctx: &Context) -> AgentEffect {
75 AgentEffect::with_fact(Fact {
76 key: ContextKey::Seeds,
77 id: self.fact_id.clone(),
78 content: self.content.clone(),
79 })
80 }
81}
82
83pub struct ReactOnceAgent {
90 fact_id: String,
91 content: String,
92}
93
94impl ReactOnceAgent {
95 #[must_use]
97 pub fn new(fact_id: impl Into<String>, content: impl Into<String>) -> Self {
98 Self {
99 fact_id: fact_id.into(),
100 content: content.into(),
101 }
102 }
103}
104
105impl Agent for ReactOnceAgent {
106 fn name(&self) -> &str {
107 "ReactOnceAgent"
108 }
109
110 fn dependencies(&self) -> &[ContextKey] {
111 &[ContextKey::Seeds] }
113
114 fn accepts(&self, ctx: &Context) -> bool {
115 ctx.has(ContextKey::Seeds)
117 && !ctx
118 .get(ContextKey::Hypotheses)
119 .iter()
120 .any(|f| f.id == self.fact_id)
121 }
122
123 fn execute(&self, _ctx: &Context) -> AgentEffect {
124 AgentEffect::with_fact(Fact {
125 key: ContextKey::Hypotheses,
126 id: self.fact_id.clone(),
127 content: self.content.clone(),
128 })
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::engine::Engine;
136
137 #[test]
138 fn seed_agent_emits_once() {
139 let mut engine = Engine::new();
140 engine.register(SeedAgent::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(SeedAgent::new("s1", "seed"));
152 engine.register(ReactOnceAgent::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(SeedAgent::new("s1", "first"));
165 engine.register(SeedAgent::new("s2", "second"));
166 engine.register(SeedAgent::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 Agent 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: &Context) -> bool {
189 ctx.has(ContextKey::Hypotheses) && !ctx.has(ContextKey::Strategies)
190 }
191
192 fn execute(&self, _ctx: &Context) -> AgentEffect {
193 AgentEffect::with_fact(Fact {
194 key: ContextKey::Strategies,
195 id: "strat-1".into(),
196 content: "derived strategy".into(),
197 })
198 }
199 }
200
201 let mut engine = Engine::new();
202 engine.register(SeedAgent::new("s1", "seed"));
203 engine.register(ReactOnceAgent::new("h1", "hypothesis"));
204 engine.register(StrategyAgent);
205
206 let result = engine.run(Context::new()).expect("converges");
207
208 assert!(result.converged);
209 assert!(result.context.has(ContextKey::Seeds));
210 assert!(result.context.has(ContextKey::Hypotheses));
211 assert!(result.context.has(ContextKey::Strategies));
212 }
213}