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    impl Suggestor for TestSuggestor {
45        fn name(&self) -> &str {
46            "TestSuggestor"
47        }
48
49        fn dependencies(&self) -> &[ContextKey] {
50            &[ContextKey::Seeds]
51        }
52
53        fn accepts(&self, ctx: &dyn crate::ContextView) -> bool {
54            !ctx.get(ContextKey::Seeds)
55                .iter()
56                .any(|f| f.id == self.fact_id)
57        }
58
59        fn execute(&self, _ctx: &dyn crate::ContextView) -> AgentEffect {
60            AgentEffect::with_proposal(crate::ProposedFact::new(
61                ContextKey::Seeds,
62                self.fact_id.clone(),
63                "test content",
64                self.name(),
65            ))
66        }
67    }
68
69    #[test]
70    fn suggestor_accepts_when_fact_missing() {
71        let suggestor = TestSuggestor {
72            fact_id: "test-1".into(),
73        };
74        let ctx = crate::context::Context::new();
75        assert!(suggestor.accepts(&ctx));
76    }
77
78    #[test]
79    fn suggestor_rejects_when_fact_present() {
80        let suggestor = TestSuggestor {
81            fact_id: "test-1".into(),
82        };
83        let mut ctx = crate::context::Context::new();
84        let fact = converge_pack::fact::kernel_authority::new_fact(
85            ContextKey::Seeds,
86            "test-1",
87            "already here",
88        );
89        let _ = ctx.add_fact(fact);
90        assert!(!suggestor.accepts(&ctx));
91    }
92
93    #[test]
94    fn suggestor_produces_effect() {
95        let suggestor = TestSuggestor {
96            fact_id: "test-1".into(),
97        };
98        let ctx = crate::context::Context::new();
99        let effect = suggestor.execute(&ctx);
100        assert_eq!(effect.proposals.len(), 1);
101        assert_eq!(effect.proposals[0].id, "test-1");
102    }
103
104    #[test]
105    fn suggestor_id_ordering() {
106        let a = SuggestorId(1);
107        let b = SuggestorId(2);
108        let c = SuggestorId(1);
109        assert!(a < b);
110        assert_eq!(a, c);
111    }
112}