Skip to main content

holographic_memory/core/agency/
goals.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Goal storage and utility scoring.
5//!
6//! Goals are hypervectors composed from desired state descriptions.
7//! Each goal has a utility score: `u(goal) = relevance * urgency * (1 - cost)`.
8//! GoalStore is separate from AtomMemory to keep goal vectors distinct from
9//! knowledge atoms.
10
11use fxhash::FxHashMap;
12use parking_lot::RwLock;
13
14use crate::core::entangled::EntangledHVec;
15
16/// A goal with its metadata and utility components.
17#[derive(Clone, Debug)]
18pub struct Goal {
19    pub name: String,
20    pub description: String,
21    pub vector: EntangledHVec,
22    pub relevance: f64,
23    pub urgency: f64,
24    pub cost: f64,
25    pub active: bool,
26}
27
28impl Goal {
29    pub fn utility(&self) -> f64 {
30        self.relevance * self.urgency * (1.0 - self.cost.clamp(0.0, 0.99))
31    }
32}
33
34struct GoalStoreInner {
35    goals: Vec<Goal>,
36    by_name: FxHashMap<String, usize>,
37}
38
39/// Stores goals separately from the knowledge base.
40pub struct GoalStore {
41    inner: RwLock<GoalStoreInner>,
42}
43
44impl Default for GoalStore {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl GoalStore {
51    pub fn new() -> Self {
52        Self {
53            inner: RwLock::new(GoalStoreInner {
54                goals: Vec::new(),
55                by_name: FxHashMap::default(),
56            }),
57        }
58    }
59
60    pub fn add(&self, goal: Goal) -> usize {
61        let mut inner = self.inner.write();
62        let idx = inner.goals.len();
63        inner.by_name.insert(goal.name.clone(), idx);
64        inner.goals.push(goal);
65        idx
66    }
67
68    pub fn get(&self, name: &str) -> Option<Goal> {
69        let inner = self.inner.read();
70        let idx = *inner.by_name.get(name)?;
71        inner.goals.get(idx).cloned()
72    }
73
74    pub fn deactivate(&self, name: &str) -> bool {
75        let mut inner = self.inner.write();
76        if let Some(&idx) = inner.by_name.get(name) {
77            inner.goals[idx].active = false;
78            true
79        } else {
80            false
81        }
82    }
83
84    pub fn active_goals(&self) -> Vec<Goal> {
85        self.inner
86            .read()
87            .goals
88            .iter()
89            .filter(|g| g.active)
90            .cloned()
91            .collect()
92    }
93
94    /// Return active goals sorted by utility (highest first).
95    pub fn prioritized(&self) -> Vec<Goal> {
96        let mut active = self.active_goals();
97        active.sort_by(|a, b| {
98            b.utility()
99                .partial_cmp(&a.utility())
100                .unwrap_or(std::cmp::Ordering::Equal)
101        });
102        active
103    }
104
105    /// Compute overlap between a vector and the highest-priority active goal.
106    /// Returns 0.0 if no active goals.
107    pub fn goal_relevance(&self, vec: &EntangledHVec) -> f64 {
108        let goals = self.prioritized();
109        if goals.is_empty() {
110            return 0.0;
111        }
112        goals[0].vector.similarity(vec)
113    }
114
115    pub fn count(&self) -> usize {
116        self.inner.read().goals.len()
117    }
118
119    pub fn active_count(&self) -> usize {
120        self.inner.read().goals.iter().filter(|g| g.active).count()
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    fn make_goal(name: &str, seed: u64, relevance: f64, urgency: f64, cost: f64) -> Goal {
129        Goal {
130            name: name.to_string(),
131            description: format!("Goal: {}", name),
132            vector: EntangledHVec::new_deterministic(16384, seed),
133            relevance,
134            urgency,
135            cost,
136            active: true,
137        }
138    }
139
140    #[test]
141    fn test_utility_scoring() {
142        let g = make_goal("test", 1, 0.8, 0.9, 0.2);
143        let u = g.utility();
144        let expected = 0.8 * 0.9 * (1.0 - 0.2);
145        assert!((u - expected).abs() < 0.001);
146    }
147
148    #[test]
149    fn test_utility_clamps_cost() {
150        let g = make_goal("test", 1, 1.0, 1.0, 1.5);
151        assert!(g.utility() > 0.0, "cost should be clamped to 0.99");
152    }
153
154    #[test]
155    fn test_add_get() {
156        let store = GoalStore::new();
157        store.add(make_goal("learn_rust", 1, 0.9, 0.8, 0.3));
158        let g = store.get("learn_rust").unwrap();
159        assert_eq!(g.name, "learn_rust");
160        assert!(g.active);
161    }
162
163    #[test]
164    fn test_deactivate() {
165        let store = GoalStore::new();
166        store.add(make_goal("g1", 1, 1.0, 1.0, 0.0));
167        assert_eq!(store.active_count(), 1);
168        store.deactivate("g1");
169        assert_eq!(store.active_count(), 0);
170    }
171
172    #[test]
173    fn test_prioritized_ordering() {
174        let store = GoalStore::new();
175        store.add(make_goal("low", 1, 0.1, 0.1, 0.0));
176        store.add(make_goal("high", 2, 1.0, 1.0, 0.0));
177        store.add(make_goal("mid", 3, 0.5, 0.5, 0.0));
178        let pri = store.prioritized();
179        assert_eq!(pri[0].name, "high");
180        assert_eq!(pri[1].name, "mid");
181        assert_eq!(pri[2].name, "low");
182    }
183
184    #[test]
185    fn test_goal_relevance() {
186        let store = GoalStore::new();
187        let g = make_goal("g1", 42, 1.0, 1.0, 0.0);
188        let vec = g.vector.clone();
189        store.add(g);
190        let rel = store.goal_relevance(&vec);
191        assert!(
192            (rel - 1.0).abs() < 0.001,
193            "Same vector should have relevance ~1.0"
194        );
195    }
196
197    #[test]
198    fn test_empty_store() {
199        let store = GoalStore::new();
200        assert_eq!(store.count(), 0);
201        assert_eq!(store.active_count(), 0);
202        assert!(store.get("x").is_none());
203        let v = EntangledHVec::new_deterministic(16384, 1);
204        assert_eq!(store.goal_relevance(&v), 0.0);
205    }
206}