1use std::collections::HashMap;
7
8pub type AgentId = u32;
9pub type Expertise = Vec<String>;
10
11#[derive(Debug, Clone, Copy)]
14pub struct BeliefScore {
15 pub confidence: f32,
16 pub trust: f32,
17 pub relevance: f32,
18}
19
20impl Default for BeliefScore {
21 fn default() -> Self { Self { confidence: 0.5, trust: 0.5, relevance: 0.5 } }
22}
23
24impl BeliefScore {
25 pub fn new(confidence: f32, trust: f32, relevance: f32) -> Self {
26 Self {
27 confidence: confidence.max(0.0).min(1.0),
28 trust: trust.max(0.0).min(1.0),
29 relevance: relevance.max(0.0).min(1.0),
30 }
31 }
32
33 pub fn composite(&self) -> f32 {
34 (self.confidence * self.trust * self.relevance).powf(1.0 / 3.0)
35 }
36
37 pub fn get(&self, dim: BeliefDimension) -> f32 {
38 match dim {
39 BeliefDimension::Confidence => self.confidence,
40 BeliefDimension::Trust => self.trust,
41 BeliefDimension::Relevance => self.relevance,
42 }
43 }
44
45 pub fn set(&mut self, dim: BeliefDimension, value: f32) {
46 let v = value.max(0.0).min(1.0);
47 match dim {
48 BeliefDimension::Confidence => self.confidence = v,
49 BeliefDimension::Trust => self.trust = v,
50 BeliefDimension::Relevance => self.relevance = v,
51 }
52 }
53
54 pub fn reinforce(&mut self, dim: BeliefDimension, strength: f32) {
55 let cur = self.get(dim);
56 self.set(dim, (cur * 4.0 + strength) / 5.0);
57 }
58
59 pub fn undermine(&mut self, dim: BeliefDimension, strength: f32) {
60 let cur = self.get(dim);
61 self.set(dim, (cur * 4.0 - strength).max(0.0) / 4.0);
62 }
63
64 pub fn decay(&mut self, rate: f32) {
65 let pull = |v: f32| (v + (0.5 - v) * rate).max(0.0).min(1.0);
66 self.confidence = pull(self.confidence);
67 self.trust = pull(self.trust);
68 self.relevance = pull(self.relevance);
69 }
70
71 pub fn actionable(&self, min_c: f32, min_t: f32, min_r: f32) -> bool {
72 self.confidence >= min_c && self.trust >= min_t && self.relevance >= min_r
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum BeliefDimension {
78 Confidence,
79 Trust,
80 Relevance,
81}
82
83pub struct BeliefStore {
86 beliefs: HashMap<String, BeliefScore>,
87 decay_per_tick: f32,
88}
89
90impl Default for BeliefStore {
91 fn default() -> Self { Self::new() }
92}
93
94impl BeliefStore {
95 pub fn new() -> Self {
96 Self { beliefs: HashMap::new(), decay_per_tick: 0.02 }
97 }
98
99 pub fn set(&mut self, key: &str, score: BeliefScore) {
100 self.beliefs.insert(key.to_string(), score);
101 }
102
103 pub fn get(&self, key: &str) -> Option<BeliefScore> {
104 self.beliefs.get(key).copied()
105 }
106
107 pub fn reinforce(&mut self, key: &str, dim: BeliefDimension, strength: f32) {
108 let score = self.beliefs.entry(key.to_string()).or_default();
109 score.reinforce(dim, strength);
110 }
111
112 pub fn undermine(&mut self, key: &str, dim: BeliefDimension, strength: f32) {
113 let score = self.beliefs.entry(key.to_string()).or_default();
114 score.undermine(dim, strength);
115 }
116
117 pub fn tick(&mut self) {
118 for score in self.beliefs.values_mut() {
119 score.decay(self.decay_per_tick);
120 }
121 }
122
123 pub fn len(&self) -> usize { self.beliefs.len() }
124 pub fn is_empty(&self) -> bool { self.beliefs.is_empty() }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum AuditOutcome { Pass, Fail(String) }
131
132pub struct ConstraintEngine {
133 forbidden: Vec<String>,
134}
135
136impl Default for ConstraintEngine {
137 fn default() -> Self { Self::new() }
138}
139
140impl ConstraintEngine {
141 pub fn new() -> Self {
142 Self { forbidden: vec!["RM -RF /".to_string(), "DELETE FROM USERS".to_string()] }
143 }
144
145 pub fn audit(&self, command: &str) -> AuditOutcome {
146 for pattern in &self.forbidden {
147 if command.to_uppercase().contains(pattern) {
148 return AuditOutcome::Fail(format!("Forbidden: contains {}", pattern));
149 }
150 }
151 AuditOutcome::Pass
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum LockSource { Inconsistency, Expert, Inferred, Observation }
159
160impl LockSource {
161 pub fn base_trust(&self) -> f32 {
162 match self {
163 LockSource::Expert => 1.0,
164 LockSource::Inconsistency => 0.8,
165 LockSource::Observation => 0.7,
166 LockSource::Inferred => 0.4,
167 }
168 }
169}
170
171#[derive(Debug, Clone)]
172pub struct Lock {
173 pub id: u64,
174 pub description: String,
175 pub trigger_pattern: String,
176 pub enforcement: String,
177 pub source: LockSource,
178 pub strength: f32,
179 pub verifications: u32,
180 pub violations: u32,
181}
182
183impl Lock {
184 pub fn new(description: &str, trigger: &str, enforcement: &str, source: LockSource) -> Self {
185 use std::time::{SystemTime, UNIX_EPOCH};
186 Self {
187 id: SystemTime::now().duration_since(UNIX_EPOCH)
188 .map(|d| d.as_nanos() as u64).unwrap_or(0),
189 description: description.to_string(),
190 trigger_pattern: trigger.to_string(),
191 enforcement: enforcement.to_string(),
192 source,
193 strength: source.base_trust(),
194 verifications: 0,
195 violations: 0,
196 }
197 }
198
199 pub fn verify(&mut self) -> f32 { self.verifications += 1; self.strength = (self.strength + 0.05).min(1.0); self.strength }
200 pub fn violate(&mut self) { self.violations += 1; self.strength = (self.strength - 0.15).max(0.0); }
201 pub fn is_active(&self, min: f32) -> bool { self.strength >= min }
202
203 pub fn confidence(&self) -> f32 {
204 if self.verifications == 0 && self.violations == 0 { return self.source.base_trust(); }
205 let total = (self.verifications + self.violations) as f32;
206 self.source.base_trust() * (self.verifications as f32 / total)
207 }
208
209 pub fn effective_strength(&self) -> f32 { self.strength * self.confidence() }
210}
211
212#[derive(Debug, Clone)]
213pub struct LockCheck {
214 pub lock_id: u64,
215 pub triggered: bool,
216 pub description: String,
217 pub enforcement: String,
218 pub effective_strength: f32,
219}
220
221pub struct LockAccumulator {
222 locks: HashMap<u64, Lock>,
223 min_strength: f32,
224}
225
226impl Default for LockAccumulator {
227 fn default() -> Self { Self::new() }
228}
229
230impl LockAccumulator {
231 pub fn new() -> Self { Self { locks: HashMap::new(), min_strength: 0.1 } }
232
233 pub fn add(&mut self, lock: Lock) -> u64 {
234 let id = lock.id;
235 self.locks.insert(id, lock);
236 id
237 }
238
239 pub fn check(&self, input: &str) -> Vec<LockCheck> {
240 let mut checks = Vec::new();
241 for lock in self.locks.values() {
242 if !lock.is_active(self.min_strength) { continue; }
243 if input.contains(&lock.trigger_pattern) {
244 checks.push(LockCheck {
245 lock_id: lock.id,
246 triggered: true,
247 description: lock.description.clone(),
248 enforcement: lock.enforcement.clone(),
249 effective_strength: lock.effective_strength(),
250 });
251 }
252 }
253 checks.sort_by(|a, b| b.effective_strength.partial_cmp(&a.effective_strength).unwrap());
254 checks
255 }
256
257 pub fn len(&self) -> usize { self.locks.len() }
258 pub fn is_empty(&self) -> bool { self.locks.is_empty() }
259}
260
261pub struct AgentState {
264 pub agent_id: AgentId,
265 pub beliefs: BeliefStore,
266 pub constraints: ConstraintEngine,
267 pub expertise: Expertise,
268}
269
270pub struct SharedState {
273 pub locks: LockAccumulator,
274 pub fused_beliefs: HashMap<AgentId, BeliefScore>,
275}
276
277pub struct MultiAgentDCS {
280 agents: HashMap<AgentId, AgentState>,
281 shared: SharedState,
282}
283
284impl Default for MultiAgentDCS {
285 fn default() -> Self { Self::new() }
286}
287
288impl MultiAgentDCS {
289 pub fn new() -> Self {
290 Self {
291 agents: HashMap::new(),
292 shared: SharedState {
293 locks: LockAccumulator::new(),
294 fused_beliefs: HashMap::new(),
295 },
296 }
297 }
298
299 pub fn agent_join(&mut self, agent_id: AgentId, expertise: Expertise) {
300 self.agents.insert(agent_id, AgentState {
301 agent_id,
302 beliefs: BeliefStore::new(),
303 constraints: ConstraintEngine::new(),
304 expertise,
305 });
306 }
307
308 pub fn agent_leave(&mut self, agent_id: AgentId) -> bool {
309 self.agents.remove(&agent_id).is_some()
310 }
311
312 pub fn agent_count(&self) -> usize { self.agents.len() }
313
314 pub fn dcs_query(&self, query: &str) -> Vec<(AgentId, BeliefScore)> {
316 let mut scored = Vec::new();
317 for (&aid, state) in &self.agents {
318 if let Some(belief) = state.beliefs.get(query) {
319 let fused = self.shared.fused_beliefs.get(&aid)
320 .copied()
321 .unwrap_or_default();
322 let weight = belief.composite() * fused.composite();
323 scored.push((aid, belief));
324 let _ = weight; }
326 }
327 scored.sort_by(|a, b| b.1.composite().partial_cmp(&a.1.composite()).unwrap());
328 scored
329 }
330
331 pub fn check_locks(&self, _agent_id: AgentId, command: &str) -> Vec<LockCheck> {
332 self.shared.locks.check(command)
333 }
334
335 pub fn update_belief(&mut self, agent_id: AgentId, key: &str, dim: BeliefDimension, strength: f32) {
336 if let Some(state) = self.agents.get_mut(&agent_id) {
337 if strength >= 0.0 {
338 state.beliefs.reinforce(key, dim, strength);
339 } else {
340 state.beliefs.undermine(key, dim, strength.abs());
341 }
342 if let Some(fused) = state.beliefs.get(key) {
343 self.shared.fused_beliefs.insert(agent_id, fused);
344 }
345 }
346 }
347
348 pub fn consensus_round(&mut self, agent_ids: &[AgentId]) -> ConsensusResult {
349 let active: Vec<_> = agent_ids.iter().filter(|id| self.agents.contains_key(id)).copied().collect();
350 let disagreement = agent_ids.len().saturating_sub(active.len());
351
352 ConsensusResult {
353 active_agents: active.len(),
354 disagreement_count: disagreement,
355 disagreement_rate: if agent_ids.is_empty() { 0.0 } else { disagreement as f64 / agent_ids.len() as f64 },
356 }
357 }
358
359 pub fn add_shared_lock(&mut self, lock: Lock) -> u64 {
360 self.shared.locks.add(lock)
361 }
362
363 pub fn constraint_audit(&self, agent_id: AgentId, command: &str) -> AuditOutcome {
364 if let Some(state) = self.agents.get(&agent_id) {
365 state.constraints.audit(command)
366 } else {
367 AuditOutcome::Fail("Agent not found".to_string())
368 }
369 }
370}
371
372#[derive(Debug, Clone)]
373pub struct ConsensusResult {
374 pub active_agents: usize,
375 pub disagreement_count: usize,
376 pub disagreement_rate: f64,
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_dcs_new() {
385 let dcs = MultiAgentDCS::new();
386 assert!(dcs.agents.is_empty());
387 assert!(dcs.shared.locks.is_empty());
388 }
389
390 #[test]
391 fn test_agent_join_leave() {
392 let mut dcs = MultiAgentDCS::new();
393 dcs.agent_join(1, vec!["math".to_string(), "geometry".to_string()]);
394 assert_eq!(dcs.agent_count(), 1);
395 assert!(dcs.agent_leave(1));
396 assert_eq!(dcs.agent_count(), 0);
397 assert!(!dcs.agent_leave(99)); }
399
400 #[test]
401 fn test_agent_expertise() {
402 let mut dcs = MultiAgentDCS::new();
403 dcs.agent_join(1, vec!["math".to_string(), "geometry".to_string()]);
404 let state = dcs.agents.get(&1).unwrap();
405 assert_eq!(state.expertise, vec!["math", "geometry"]);
406 }
407
408 #[test]
409 fn test_belief_store_set_get() {
410 let mut store = BeliefStore::new();
411 let b = BeliefScore::new(0.9, 0.8, 0.7);
412 store.set("pythagorean", b);
413 let got = store.get("pythagorean").unwrap();
414 assert!((got.confidence - 0.9).abs() < 0.001);
415 assert!(store.get("nonexistent").is_none());
416 }
417
418 #[test]
419 fn test_belief_reinforce_undermine() {
420 let mut store = BeliefStore::new();
421 store.set("test", BeliefScore::new(0.5, 0.5, 0.5));
422 store.reinforce("test", BeliefDimension::Confidence, 1.0);
423 let got = store.get("test").unwrap();
424 assert!(got.confidence > 0.5, "reinforce should increase");
425 store.undermine("test", BeliefDimension::Trust, 1.0);
426 let got2 = store.get("test").unwrap();
427 assert!(got2.trust < 0.5, "undermine should decrease");
428 }
429
430 #[test]
431 fn test_belief_decay() {
432 let mut store = BeliefStore::new();
433 store.set("test", BeliefScore::new(1.0, 1.0, 1.0));
434 store.tick();
435 let got = store.get("test").unwrap();
436 assert!(got.confidence < 1.0, "decay should pull toward 0.5");
437 }
438
439 #[test]
440 fn test_composite_geometric_mean() {
441 let b = BeliefScore::new(1.0, 1.0, 1.0);
442 assert!((b.composite() - 1.0).abs() < 0.001);
443 let z = BeliefScore::new(0.0, 0.0, 0.0);
444 assert!((z.composite() - 0.0).abs() < 0.001);
445 }
446
447 #[test]
448 fn test_actionable() {
449 let b = BeliefScore::new(0.9, 0.8, 0.7);
450 assert!(b.actionable(0.8, 0.7, 0.6));
451 assert!(!b.actionable(1.0, 0.0, 0.0));
452 }
453
454 #[test]
455 fn test_constraint_audit_pass() {
456 let engine = ConstraintEngine::new();
457 assert_eq!(engine.audit("select * from users"), AuditOutcome::Pass);
458 }
459
460 #[test]
461 fn test_constraint_audit_fail() {
462 let engine = ConstraintEngine::new();
463 match engine.audit("RM -RF / HOME") {
464 AuditOutcome::Fail(msg) => assert!(msg.contains("Forbidden")),
465 _ => panic!("should fail"),
466 }
467 }
468
469 #[test]
470 fn test_lock_new_verify_violate() {
471 let mut lock = Lock::new("test", "danger", "BLOCK", LockSource::Expert);
472 assert_eq!(lock.source.base_trust(), 1.0);
473 lock.verify();
474 assert_eq!(lock.verifications, 1);
475 assert!(lock.strength >= 1.0); lock.violate();
477 assert_eq!(lock.violations, 1);
478 }
479
480 #[test]
481 fn test_lock_confidence() {
482 let mut lock = Lock::new("test", "x", "BLOCK", LockSource::Observation);
483 for _ in 0..10 { lock.verify(); }
484 assert!((lock.confidence() - 0.7).abs() < 0.001); }
486
487 #[test]
488 fn test_lock_accumulator_check() {
489 let mut acc = LockAccumulator::new();
490 acc.add(Lock::new("rm guard", "rm -rf", "BLOCK", LockSource::Expert));
491 let checks = acc.check("rm -rf /tmp/stuff");
492 assert_eq!(checks.len(), 1);
493 assert!(checks[0].triggered);
494 let no_match = acc.check("echo hello");
495 assert!(no_match.is_empty());
496 }
497
498 #[test]
499 fn test_dcs_query_with_beliefs() {
500 let mut dcs = MultiAgentDCS::new();
501 dcs.agent_join(1, vec!["math".to_string()]);
502 dcs.agent_join(2, vec!["geometry".to_string()]);
503 dcs.update_belief(1, "pythagorean", BeliefDimension::Confidence, 0.9);
504 dcs.update_belief(2, "pythagorean", BeliefDimension::Confidence, 0.7);
505
506 let results = dcs.dcs_query("pythagorean");
507 assert_eq!(results.len(), 2);
508 assert_eq!(results[0].0, 1); }
510
511 #[test]
512 fn test_shared_locks_across_agents() {
513 let mut dcs = MultiAgentDCS::new();
514 dcs.agent_join(1, vec!["ops".to_string()]);
515 dcs.agent_join(2, vec!["ops".to_string()]);
516 dcs.add_shared_lock(Lock::new("no rm", "rm -rf", "BLOCK", LockSource::Expert));
517
518 let checks1 = dcs.check_locks(1, "rm -rf /");
519 let checks2 = dcs.check_locks(2, "rm -rf /");
520 assert_eq!(checks1.len(), checks2.len());
521 assert!(checks1[0].triggered);
522 }
523
524 #[test]
525 fn test_consensus_round() {
526 let mut dcs = MultiAgentDCS::new();
527 dcs.agent_join(1, vec!["a".to_string()]);
528 dcs.agent_join(2, vec!["b".to_string()]);
529 let result = dcs.consensus_round(&[1, 2, 99]); assert_eq!(result.active_agents, 2);
531 assert_eq!(result.disagreement_count, 1);
532 assert!((result.disagreement_rate - 0.333).abs() < 0.01);
533 }
534
535 #[test]
536 fn test_fused_belief_update() {
537 let mut dcs = MultiAgentDCS::new();
538 dcs.agent_join(1, vec!["test".to_string()]);
539 dcs.update_belief(1, "key", BeliefDimension::Confidence, 0.9);
540 let fused = dcs.shared.fused_beliefs.get(&1).unwrap();
541 assert!(fused.confidence > 0.5);
542 }
543
544 #[test]
545 fn test_constraint_audit_unknown_agent() {
546 let dcs = MultiAgentDCS::new();
547 match dcs.constraint_audit(99, "anything") {
548 AuditOutcome::Fail(msg) => assert!(msg.contains("not found")),
549 _ => panic!("should fail for unknown agent"),
550 }
551 }
552
553 #[test]
554 fn test_belief_dimension_set_get() {
555 let mut b = BeliefScore::default();
556 b.set(BeliefDimension::Trust, 0.99);
557 assert!((b.get(BeliefDimension::Trust) - 0.99).abs() < 0.001);
558 assert!((b.get(BeliefDimension::Confidence) - 0.5).abs() < 0.001); }
560
561 #[test]
562 fn test_lock_active_threshold() {
563 let lock = Lock::new("weak", "x", "WARN", LockSource::Inferred);
564 assert!(lock.is_active(0.3)); assert!(!lock.is_active(0.5)); }
567}