#![allow(clippy::unnecessary_literal_bound)]
use crate::agent::Suggestor;
use crate::context::{ContextKey, ProposedFact};
use crate::effect::AgentEffect;
pub struct SeedSuggestor {
fact_id: String,
content: String,
}
impl SeedSuggestor {
#[must_use]
pub fn new(fact_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
fact_id: fact_id.into(),
content: content.into(),
}
}
}
#[async_trait::async_trait]
impl Suggestor for SeedSuggestor {
fn name(&self) -> &str {
"SeedSuggestor"
}
fn dependencies(&self) -> &[ContextKey] {
&[] }
fn accepts(&self, ctx: &dyn crate::ContextView) -> bool {
!ctx.get(ContextKey::Seeds)
.iter()
.any(|f| f.id == self.fact_id)
}
async fn execute(&self, _ctx: &dyn crate::ContextView) -> AgentEffect {
AgentEffect::with_proposal(ProposedFact::new(
ContextKey::Seeds,
self.fact_id.clone(),
self.content.clone(),
self.name(),
))
}
}
pub struct ReactOnceSuggestor {
fact_id: String,
content: String,
}
impl ReactOnceSuggestor {
#[must_use]
pub fn new(fact_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
fact_id: fact_id.into(),
content: content.into(),
}
}
}
#[async_trait::async_trait]
impl Suggestor for ReactOnceSuggestor {
fn name(&self) -> &str {
"ReactOnceSuggestor"
}
fn dependencies(&self) -> &[ContextKey] {
&[ContextKey::Seeds] }
fn accepts(&self, ctx: &dyn crate::ContextView) -> bool {
ctx.has(ContextKey::Seeds)
&& !ctx
.get(ContextKey::Hypotheses)
.iter()
.any(|f| f.id == self.fact_id)
}
async fn execute(&self, _ctx: &dyn crate::ContextView) -> AgentEffect {
AgentEffect::with_proposal(ProposedFact::new(
ContextKey::Hypotheses,
self.fact_id.clone(),
self.content.clone(),
self.name(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::Context;
use crate::engine::Engine;
#[tokio::test]
async fn seed_agent_emits_once() {
let mut engine = Engine::new();
engine.register_suggestor(SeedSuggestor::new("s1", "value"));
let result = engine.run(Context::new()).await.expect("converges");
assert!(result.converged);
assert_eq!(result.context.get(ContextKey::Seeds).len(), 1);
}
#[tokio::test]
async fn react_once_agent_chains_from_seed() {
let mut engine = Engine::new();
engine.register_suggestor(SeedSuggestor::new("s1", "seed"));
engine.register_suggestor(ReactOnceSuggestor::new("h1", "hypothesis"));
let result = engine.run(Context::new()).await.expect("converges");
assert!(result.converged);
assert!(result.context.has(ContextKey::Seeds));
assert!(result.context.has(ContextKey::Hypotheses));
}
#[tokio::test]
async fn multiple_seeds_all_converge() {
let mut engine = Engine::new();
engine.register_suggestor(SeedSuggestor::new("s1", "first"));
engine.register_suggestor(SeedSuggestor::new("s2", "second"));
engine.register_suggestor(SeedSuggestor::new("s3", "third"));
let result = engine.run(Context::new()).await.expect("converges");
assert!(result.converged);
assert_eq!(result.context.get(ContextKey::Seeds).len(), 3);
}
#[tokio::test]
async fn chain_of_three_converges() {
struct StrategyAgent;
#[async_trait::async_trait]
impl Suggestor for StrategyAgent {
fn name(&self) -> &str {
"StrategyAgent"
}
fn dependencies(&self) -> &[ContextKey] {
&[ContextKey::Hypotheses]
}
fn accepts(&self, ctx: &dyn crate::ContextView) -> bool {
ctx.has(ContextKey::Hypotheses) && !ctx.has(ContextKey::Strategies)
}
async fn execute(&self, _ctx: &dyn crate::ContextView) -> AgentEffect {
AgentEffect::with_proposal(ProposedFact::new(
ContextKey::Strategies,
"strat-1",
"derived strategy",
self.name(),
))
}
}
let mut engine = Engine::new();
engine.register_suggestor(SeedSuggestor::new("s1", "seed"));
engine.register_suggestor(ReactOnceSuggestor::new("h1", "hypothesis"));
engine.register_suggestor(StrategyAgent);
let result = engine.run(Context::new()).await.expect("converges");
assert!(result.converged);
assert!(result.context.has(ContextKey::Seeds));
assert!(result.context.has(ContextKey::Hypotheses));
assert!(result.context.has(ContextKey::Strategies));
}
}