Skip to main content

converge_pack/
context.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! Context keys and the shared context contract.
5//!
6//! Context is the API. Suggestors don't call each other — they read from and
7//! write to shared context through typed keys.
8
9use serde::{Deserialize, Serialize};
10
11use crate::fact::{Fact, ProposedFact};
12
13/// Typed keys for the shared context namespace.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
15#[cfg_attr(feature = "strum", derive(strum::EnumIter))]
16pub enum ContextKey {
17    /// Initial inputs from the root intent. Set once at initialization.
18    Seeds,
19    /// Proposed ideas and hypotheses from analysis suggestors.
20    Hypotheses,
21    /// Action plans and strategic recommendations.
22    Strategies,
23    /// Limitations, rules, and boundary conditions.
24    Constraints,
25    /// Observations, market data, and signals from the environment.
26    Signals,
27    /// Competitive intelligence and comparisons.
28    Competitors,
29    /// Assessments, ratings, and evaluations of other facts.
30    Evaluations,
31    /// LLM-generated suggestions awaiting validation.
32    Proposals,
33    /// Error and debugging information. Never blocks convergence.
34    Diagnostic,
35}
36
37/// Read-only view of the shared context.
38///
39/// Suggestors receive `&dyn Context` during `accepts()` and `execute()`.
40/// They cannot mutate it directly — mutations happen through `AgentEffect`
41/// after the engine collects all effects and merges them deterministically.
42pub trait Context: Send + Sync {
43    /// Check whether any facts exist under this key.
44    fn has(&self, key: ContextKey) -> bool;
45
46    /// Get all facts under this key.
47    fn get(&self, key: ContextKey) -> &[Fact];
48
49    /// Get all proposed facts (unvalidated).
50    fn get_proposals(&self, key: ContextKey) -> &[ProposedFact] {
51        let _ = key;
52        &[]
53    }
54
55    /// Count of facts under a key.
56    fn count(&self, key: ContextKey) -> usize {
57        self.get(key).len()
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    struct MockContext {
66        facts: std::collections::HashMap<ContextKey, Vec<Fact>>,
67    }
68
69    impl MockContext {
70        fn empty() -> Self {
71            Self {
72                facts: std::collections::HashMap::new(),
73            }
74        }
75    }
76
77    impl Context for MockContext {
78        fn has(&self, key: ContextKey) -> bool {
79            self.facts.get(&key).is_some_and(|v| !v.is_empty())
80        }
81
82        fn get(&self, key: ContextKey) -> &[Fact] {
83            self.facts.get(&key).map_or(&[], Vec::as_slice)
84        }
85    }
86
87    #[test]
88    fn get_proposals_default_returns_empty() {
89        let ctx = MockContext::empty();
90        assert!(ctx.get_proposals(ContextKey::Seeds).is_empty());
91        assert!(ctx.get_proposals(ContextKey::Hypotheses).is_empty());
92    }
93
94    #[test]
95    fn count_default_delegates_to_get() {
96        let ctx = MockContext::empty();
97        assert_eq!(ctx.count(ContextKey::Seeds), 0);
98    }
99
100    #[test]
101    fn has_returns_false_for_empty() {
102        let ctx = MockContext::empty();
103        assert!(!ctx.has(ContextKey::Seeds));
104    }
105
106    #[cfg(feature = "kernel-authority")]
107    #[test]
108    fn count_reflects_facts() {
109        use crate::fact::kernel_authority;
110
111        let mut ctx = MockContext::empty();
112        ctx.facts.insert(
113            ContextKey::Seeds,
114            vec![kernel_authority::new_fact(ContextKey::Seeds, "f1", "a")],
115        );
116        assert_eq!(ctx.count(ContextKey::Seeds), 1);
117        assert!(ctx.has(ContextKey::Seeds));
118        assert!(!ctx.has(ContextKey::Hypotheses));
119    }
120}