Skip to main content

converge_core/
agent.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! Suggestor trait and types for Converge.
5//!
6//! The `Suggestor` trait is defined in `converge-pack` and re-exported here.
7//! `SuggestorId` is a core-internal type for deterministic ordering.
8
9// Re-export the canonical Suggestor trait
10pub use converge_pack::Suggestor;
11
12/// Unique identifier for a registered suggestor.
13///
14/// Assigned monotonically at registration time.
15/// Used for deterministic effect merge ordering.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
17pub struct SuggestorId(pub(crate) u32);
18
19impl SuggestorId {
20    /// Returns the raw numeric ID.
21    #[must_use]
22    pub fn as_u32(self) -> u32 {
23        self.0
24    }
25}
26
27impl std::fmt::Display for SuggestorId {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "Suggestor({})", self.0)
30    }
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36    use crate::context::ContextKey;
37    use crate::effect::AgentEffect;
38
39    /// A minimal test suggestor that emits one proposal then stops.
40    struct TestSuggestor {
41        fact_id: String,
42    }
43
44    #[async_trait::async_trait]
45    impl Suggestor for TestSuggestor {
46        fn name(&self) -> &str {
47            "TestSuggestor"
48        }
49
50        fn dependencies(&self) -> &[ContextKey] {
51            &[ContextKey::Seeds]
52        }
53
54        fn accepts(&self, ctx: &dyn crate::Context) -> bool {
55            !ctx.get(ContextKey::Seeds)
56                .iter()
57                .any(|f| f.id().as_str() == self.fact_id)
58        }
59
60        async fn execute(&self, _ctx: &dyn crate::Context) -> AgentEffect {
61            AgentEffect::with_proposal(crate::ProposedFact::new(
62                ContextKey::Seeds,
63                self.fact_id.clone(),
64                crate::TextPayload::new("test content"),
65                self.provenance(),
66            ))
67        }
68    }
69
70    #[test]
71    fn suggestor_accepts_when_fact_missing() {
72        let suggestor = TestSuggestor {
73            fact_id: "test-1".into(),
74        };
75        let ctx = crate::context::ContextState::new();
76        assert!(suggestor.accepts(&ctx));
77    }
78
79    #[test]
80    fn suggestor_rejects_when_fact_present() {
81        let suggestor = TestSuggestor {
82            fact_id: "test-1".into(),
83        };
84        let mut ctx = crate::context::ContextState::new();
85        let fact = crate::context::new_fact(ContextKey::Seeds, "test-1", "already here");
86        let _ = ctx.add_fact(fact);
87        assert!(!suggestor.accepts(&ctx));
88    }
89
90    #[tokio::test]
91    async fn suggestor_produces_effect() {
92        let suggestor = TestSuggestor {
93            fact_id: "test-1".into(),
94        };
95        let ctx = crate::context::ContextState::new();
96        let effect = suggestor.execute(&ctx).await;
97        assert_eq!(effect.proposals().len(), 1);
98        assert_eq!(effect.proposals()[0].id, "test-1");
99    }
100
101    #[test]
102    fn suggestor_id_ordering() {
103        let a = SuggestorId(1);
104        let b = SuggestorId(2);
105        let c = SuggestorId(1);
106        assert!(a < b);
107        assert_eq!(a, c);
108    }
109}