1use crate::substrate_impl::SubstrateImpl;
15use phago_agents::fitness::FitnessTracker;
16use phago_core::agent::Agent;
17use phago_core::semantic::{compute_semantic_weight, SemanticWiringConfig};
18use phago_core::substrate::Substrate;
19use phago_core::topology::TopologyGraph;
20use phago_core::types::*;
21use serde::Serialize;
22use serde_json;
23
24#[derive(Debug, Clone, Serialize)]
26pub enum ColonyEvent {
27 Spawned { id: AgentId, agent_type: String },
29 Moved { id: AgentId, to: Position },
31 Engulfed { id: AgentId, document: DocumentId },
33 Presented { id: AgentId, fragment_count: usize, node_ids: Vec<NodeId> },
35 Deposited { id: AgentId, location: SubstrateLocation },
37 Wired { id: AgentId, connection_count: usize },
39 Died { signal: DeathSignal },
41 TickComplete { tick: Tick, alive: usize, dead_this_tick: usize },
43 CapabilityExported { agent_id: AgentId, terms_count: usize },
45 CapabilityIntegrated { agent_id: AgentId, from_agent: AgentId, terms_count: usize },
47 Symbiosis { host: AgentId, absorbed: AgentId, host_type: String, absorbed_type: String },
49 Dissolved { agent_id: AgentId, permeability: f64, terms_externalized: usize },
51}
52
53#[derive(Debug, Clone, Serialize)]
55pub struct ColonyStats {
56 pub tick: Tick,
57 pub agents_alive: usize,
58 pub agents_died: usize,
59 pub total_spawned: usize,
60 pub graph_nodes: usize,
61 pub graph_edges: usize,
62 pub total_signals: usize,
63 pub documents_total: usize,
64 pub documents_digested: usize,
65}
66
67#[derive(Debug, Clone, Serialize)]
69pub struct AgentSnapshot {
70 pub id: AgentId,
71 pub agent_type: String,
72 pub position: Position,
73 pub age: Tick,
74 pub permeability: f64,
75 pub vocabulary_size: usize,
76}
77
78#[derive(Debug, Clone, Serialize)]
80pub struct NodeSnapshot {
81 pub id: NodeId,
82 pub label: String,
83 pub node_type: NodeType,
84 pub position: Position,
85 pub access_count: u64,
86}
87
88#[derive(Debug, Clone, Serialize)]
90pub struct EdgeSnapshot {
91 pub from_label: String,
92 pub to_label: String,
93 pub weight: f64,
94 pub co_activations: u64,
95}
96
97#[derive(Debug, Clone, Serialize)]
99pub struct ColonySnapshot {
100 pub tick: Tick,
101 pub agents: Vec<AgentSnapshot>,
102 pub nodes: Vec<NodeSnapshot>,
103 pub edges: Vec<EdgeSnapshot>,
104 pub stats: ColonyStats,
105}
106
107pub struct Colony {
109 substrate: SubstrateImpl,
110 agents: Vec<Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>>,
111 death_signals: Vec<DeathSignal>,
112 event_history: Vec<(Tick, ColonyEvent)>,
113 total_spawned: usize,
114 total_died: usize,
115 fitness_tracker: FitnessTracker,
116
117 signal_decay_rate: f64,
119 signal_removal_threshold: f64,
120 trace_decay_rate: f64,
121 trace_removal_threshold: f64,
122 edge_decay_rate: f64,
123 edge_prune_threshold: f64,
124 staleness_factor: f64,
125 maturation_ticks: u64,
126 max_edge_degree: usize,
127 semantic_wiring: SemanticWiringConfig,
128}
129
130impl Colony {
131 pub fn new() -> Self {
132 Self {
133 substrate: SubstrateImpl::new(),
134 agents: Vec::new(),
135 death_signals: Vec::new(),
136 event_history: Vec::new(),
137 total_spawned: 0,
138 total_died: 0,
139 fitness_tracker: FitnessTracker::new(),
140 signal_decay_rate: 0.05,
141 signal_removal_threshold: 0.01,
142 trace_decay_rate: 0.02,
143 trace_removal_threshold: 0.01,
144 edge_decay_rate: 0.005,
145 edge_prune_threshold: 0.05,
146 staleness_factor: 1.5,
147 maturation_ticks: 50,
148 max_edge_degree: 30,
149 semantic_wiring: SemanticWiringConfig::default(),
150 }
151 }
152
153 pub fn with_semantic_wiring(mut self, config: SemanticWiringConfig) -> Self {
155 self.semantic_wiring = config;
156 self
157 }
158
159 pub fn semantic_wiring_config(&self) -> &SemanticWiringConfig {
161 &self.semantic_wiring
162 }
163
164 pub fn set_semantic_wiring(&mut self, config: SemanticWiringConfig) {
166 self.semantic_wiring = config;
167 }
168
169 pub fn spawn(
171 &mut self,
172 agent: Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>,
173 ) -> AgentId {
174 let id = agent.id();
175 self.total_spawned += 1;
176 self.fitness_tracker.register(id, 0);
177 self.agents.push(agent);
178 id
179 }
180
181 pub fn ingest_document(&mut self, title: &str, content: &str, position: Position) -> DocumentId {
186 let doc = Document {
187 id: DocumentId::new(),
188 title: title.to_string(),
189 content: content.to_string(),
190 position,
191 digested: false,
192 };
193 let doc_id = doc.id;
194 let doc_pos = doc.position;
195
196 self.substrate.add_document(doc);
197
198 self.substrate.emit_signal(Signal::new(
200 SignalType::Input,
201 1.0,
202 doc_pos,
203 AgentId::new(), self.substrate.current_tick(),
205 ));
206
207 doc_id
208 }
209
210 pub fn tick(&mut self) -> Vec<ColonyEvent> {
212 let mut events = Vec::new();
213 let mut actions: Vec<(usize, AgentAction)> = Vec::new();
214
215 for (idx, agent) in self.agents.iter_mut().enumerate() {
217 let action = agent.tick(&self.substrate);
218 actions.push((idx, action));
219 }
220
221 let mut to_die = Vec::new();
223 let mut symbiotic_deaths: Vec<(usize, AgentId)> = Vec::new(); for (idx, action) in actions {
226 match action {
227 AgentAction::Move(pos) => {
228 self.agents[idx].set_position(pos);
229 events.push(ColonyEvent::Moved {
230 id: self.agents[idx].id(),
231 to: pos,
232 });
233 }
234
235 AgentAction::EngulfDocument(doc_id) => {
236 if let Some(content) = self.substrate.consume_document(&doc_id) {
238 self.agents[idx].engulf(content);
239 events.push(ColonyEvent::Engulfed {
242 id: self.agents[idx].id(),
243 document: doc_id,
244 });
245 }
246 }
247
248 AgentAction::PresentFragments(fragments) => {
249 let agent_id = self.agents[idx].id();
250 let tick = self.substrate.current_tick();
251 let mut node_ids = Vec::new();
252
253 for frag in &fragments {
254 let existing = self.substrate.graph().find_nodes_by_label(&frag.label);
256 let node_id = if let Some(&existing_id) = existing.first() {
257 if let Some(node) = self.substrate.graph_mut().get_node_mut(&existing_id) {
259 node.access_count += 1;
260 }
261 existing_id
262 } else {
263 let node = NodeData {
265 id: NodeId::new(),
266 label: frag.label.clone(),
267 node_type: frag.node_type.clone(),
268 position: frag.position,
269 access_count: 1,
270 created_tick: tick,
271 embedding: None,
272 };
273 self.substrate.add_node(node)
274 };
275 node_ids.push(node_id);
276 }
277
278 let concept_node_ids: Vec<NodeId> = node_ids.iter().filter(|id| {
293 self.substrate.graph().get_node(id)
294 .map_or(false, |n| n.node_type == NodeType::Concept)
295 }).copied().collect();
296 let mut wire_events = Vec::new();
297 for i in 0..concept_node_ids.len() {
298 for j in (i + 1)..concept_node_ids.len() {
299 let from = concept_node_ids[i];
300 let to = concept_node_ids[j];
301
302 let embedding_from = self.substrate.graph().get_node(&from)
304 .and_then(|n| n.embedding.clone());
305 let embedding_to = self.substrate.graph().get_node(&to)
306 .and_then(|n| n.embedding.clone());
307
308 let base_weight = 0.1;
310 let semantic_weight = compute_semantic_weight(
311 base_weight,
312 embedding_from.as_deref(),
313 embedding_to.as_deref(),
314 &self.semantic_wiring,
315 );
316
317 if let Some(edge) = self.substrate.graph_mut().get_edge_mut(&from, &to) {
318 let reinforcement = semantic_weight.unwrap_or(base_weight);
321 edge.weight = (edge.weight + reinforcement).min(1.0);
322 edge.co_activations += 1;
323 edge.last_activated_tick = tick;
324 wire_events.push((from, to));
325 } else {
326 let weight = semantic_weight;
329
330 if let Some(w) = weight {
332 self.substrate.set_edge(from, to, EdgeData {
333 weight: w,
334 co_activations: 1,
335 created_tick: tick,
336 last_activated_tick: tick,
337 });
338 wire_events.push((from, to));
339 }
340 }
341 }
342 }
343
344 events.push(ColonyEvent::Presented {
345 id: agent_id,
346 fragment_count: fragments.len(),
347 node_ids,
348 });
349
350 if !wire_events.is_empty() {
351 events.push(ColonyEvent::Wired {
352 id: agent_id,
353 connection_count: wire_events.len(),
354 });
355 }
356 }
357
358 AgentAction::Deposit(location, trace) => {
359 let agent_id = self.agents[idx].id();
360 self.substrate.deposit_trace(&location, trace);
361 events.push(ColonyEvent::Deposited {
362 id: agent_id,
363 location,
364 });
365 }
366
367 AgentAction::Emit(signal) => {
368 self.substrate.emit_signal(signal);
369 }
370
371 AgentAction::WireNodes(connections) => {
372 let agent_id = self.agents[idx].id();
373 let tick = self.substrate.current_tick();
374 let mut wired_count = 0;
375 for (from, to, base_weight) in &connections {
376 let embedding_from = self.substrate.graph().get_node(from)
378 .and_then(|n| n.embedding.clone());
379 let embedding_to = self.substrate.graph().get_node(to)
380 .and_then(|n| n.embedding.clone());
381
382 let weight = compute_semantic_weight(
384 *base_weight,
385 embedding_from.as_deref(),
386 embedding_to.as_deref(),
387 &self.semantic_wiring,
388 );
389
390 if let Some(w) = weight {
391 if let Some(edge) = self.substrate.graph_mut().get_edge_mut(from, to) {
392 edge.weight = (edge.weight + w).min(1.0);
393 edge.co_activations += 1;
394 edge.last_activated_tick = tick;
395 } else {
396 self.substrate.set_edge(*from, *to, EdgeData {
397 weight: w,
398 co_activations: 1,
399 created_tick: tick,
400 last_activated_tick: tick,
401 });
402 }
403 wired_count += 1;
404 }
405 }
406 if wired_count > 0 {
407 events.push(ColonyEvent::Wired {
408 id: agent_id,
409 connection_count: wired_count,
410 });
411 }
412 }
413
414 AgentAction::ExportCapability(_cap_id) => {
415 let agent_id = self.agents[idx].id();
416 let agent_pos = self.agents[idx].position();
417 if let Some(vocab_bytes) = self.agents[idx].export_vocabulary() {
418 let terms_count = serde_json::from_slice::<VocabularyCapability>(&vocab_bytes)
420 .map(|v| v.terms.len())
421 .unwrap_or(0);
422
423 let trace = Trace {
425 agent_id,
426 trace_type: TraceType::CapabilityDeposit,
427 intensity: 1.0,
428 tick: self.substrate.current_tick(),
429 payload: vocab_bytes,
430 };
431 self.substrate.deposit_trace(
432 &SubstrateLocation::Spatial(agent_pos),
433 trace,
434 );
435
436 self.substrate.emit_signal(Signal::new(
438 SignalType::Capability,
439 0.8,
440 agent_pos,
441 agent_id,
442 self.substrate.current_tick(),
443 ));
444
445 events.push(ColonyEvent::CapabilityExported {
446 agent_id,
447 terms_count,
448 });
449 }
450 }
451
452 AgentAction::SymbioseWith(target_id) => {
453 let host_idx = idx;
454 let host_id = self.agents[host_idx].id();
455
456 if let Some(target_idx) = self.agents.iter().position(|a| a.id() == target_id) {
458 let target_profile = self.agents[target_idx].profile();
460 let target_vocab = self.agents[target_idx].export_vocabulary()
461 .unwrap_or_default();
462
463 if let Some(SymbiosisEval::Integrate) =
465 self.agents[host_idx].evaluate_symbiosis(&target_profile)
466 {
467 let host_type = self.agents[host_idx].agent_type().to_string();
468 let absorbed_type = self.agents[target_idx].agent_type().to_string();
469
470 self.agents[host_idx].absorb_symbiont(target_profile, target_vocab);
472
473 symbiotic_deaths.push((target_idx, host_id));
475
476 events.push(ColonyEvent::Symbiosis {
477 host: host_id,
478 absorbed: target_id,
479 host_type,
480 absorbed_type,
481 });
482 }
483 }
484 }
485
486 AgentAction::Apoptose => {
487 to_die.push(idx);
488 }
489
490 AgentAction::Idle => {}
491
492 _ => {}
493 }
494 }
495
496 {
500 let _tick = self.substrate.current_tick();
501 let agent_count = self.agents.len();
502
503 for i in 0..agent_count {
504 let agent_id = self.agents[i].id();
505 let agent_pos = self.agents[i].position();
506 let agent_age = self.agents[i].age();
507
508 let vocab_terms = self.agents[i].externalize_vocabulary();
510 let mut reinforcement_count = 0u64;
511 let graph = self.substrate.graph();
512 for term in &vocab_terms {
513 let matching = graph.find_nodes_by_exact_label(term);
514 for nid in matching {
515 if let Some(node) = graph.get_node(nid) {
516 reinforcement_count += node.access_count;
517 }
518 }
519 }
520
521 let useful_outputs_estimate = reinforcement_count.min(100);
522 let trust = if agent_age > 0 {
523 (useful_outputs_estimate as f64 / agent_age as f64).min(1.0)
524 } else {
525 0.0
526 };
527
528 let context = BoundaryContext {
529 reinforcement_count,
530 age: agent_age,
531 trust,
532 };
533
534 self.agents[i].modulate_boundary(&context);
535 let permeability = self.agents[i].permeability();
536
537 if permeability > 0.5 {
539 let mut terms_externalized = 0usize;
541 for term in &vocab_terms {
542 let matching: Vec<NodeId> = self.substrate.graph().find_nodes_by_exact_label(term).to_vec();
543 for nid in &matching {
544 if let Some(node) = self.substrate.graph_mut().get_node_mut(nid) {
545 node.access_count += 1;
546 terms_externalized += 1;
547 }
548 }
549 }
550 if terms_externalized > 0 {
551 events.push(ColonyEvent::Dissolved {
552 agent_id,
553 permeability,
554 terms_externalized,
555 });
556 }
557 }
558
559 if permeability > 0.0 {
561 let all_nodes = self.substrate.graph().all_nodes();
562 let nearby_labels: Vec<String> = all_nodes.iter()
563 .filter_map(|nid| {
564 let node = self.substrate.graph().get_node(nid)?;
565 if node.position.distance_to(&agent_pos) <= 15.0
566 && node.node_type == NodeType::Concept
567 {
568 Some(node.label.clone())
569 } else {
570 None
571 }
572 })
573 .collect();
574 if !nearby_labels.is_empty() {
575 self.agents[i].internalize_vocabulary(&nearby_labels);
576 }
577 }
578
579 let traces = self.substrate.traces_near(
581 &agent_pos,
582 10.0,
583 &TraceType::CapabilityDeposit,
584 );
585 for trace in &traces {
586 if trace.agent_id != agent_id
587 && !trace.payload.is_empty()
588 {
589 let payload = trace.payload.clone();
590 let from_agent = trace.agent_id;
591 let terms_count = serde_json::from_slice::<VocabularyCapability>(&payload)
592 .map(|v| v.terms.len())
593 .unwrap_or(0);
594 if self.agents[i].integrate_vocabulary(&payload) {
595 events.push(ColonyEvent::CapabilityIntegrated {
596 agent_id,
597 from_agent,
598 terms_count,
599 });
600 }
601 }
602 }
603 }
604 }
605
606 for (idx, _absorber_id) in &symbiotic_deaths {
608 if !to_die.contains(idx) {
609 to_die.push(*idx);
610 }
611 }
612
613 to_die.sort();
615 to_die.dedup();
616 let dead_count = to_die.len();
617 for idx in to_die.into_iter().rev() {
618 let agent = self.agents.remove(idx);
619 let mut death_signal = agent.prepare_death_signal();
620
621 if let Some((_, absorber_id)) = symbiotic_deaths.iter().find(|(i, _)| *i == idx) {
623 death_signal.cause = DeathCause::SymbioticAbsorption(*absorber_id);
624 }
625
626 events.push(ColonyEvent::Died {
627 signal: death_signal.clone(),
628 });
629 self.death_signals.push(death_signal);
630 self.total_died += 1;
631 }
632
633 self.substrate
635 .decay_signals(self.signal_decay_rate, self.signal_removal_threshold);
636 self.substrate
637 .decay_traces(self.trace_decay_rate, self.trace_removal_threshold);
638 let current_tick = self.substrate.current_tick();
640 self.substrate.graph_mut().decay_edges_activity(
641 self.edge_decay_rate,
642 self.edge_prune_threshold,
643 current_tick,
644 self.staleness_factor,
645 self.maturation_ticks,
646 );
647 self.substrate
649 .graph_mut()
650 .prune_to_max_degree(self.max_edge_degree);
651
652 for event in &events {
654 match event {
655 ColonyEvent::Presented { id, fragment_count, .. } => {
656 self.fitness_tracker.record_concepts(id, *fragment_count as u64);
657 }
658 ColonyEvent::Wired { id, connection_count } => {
659 self.fitness_tracker.record_edges(id, *connection_count as u64);
660 }
661 _ => {}
662 }
663 }
664 let alive_ids: Vec<AgentId> = self.agents.iter().map(|a| a.id()).collect();
665 self.fitness_tracker.tick_all(&alive_ids);
666
667 self.substrate.advance_tick();
669
670 events.push(ColonyEvent::TickComplete {
671 tick: self.substrate.current_tick(),
672 alive: self.agents.len(),
673 dead_this_tick: dead_count,
674 });
675
676 let current_tick = self.substrate.current_tick();
678 for event in &events {
679 self.event_history.push((current_tick, event.clone()));
680 }
681
682 events
683 }
684
685 pub fn run(&mut self, ticks: u64) -> Vec<Vec<ColonyEvent>> {
687 let mut all_events = Vec::new();
688 for _ in 0..ticks {
689 all_events.push(self.tick());
690 }
691 all_events
692 }
693
694 pub fn stats(&self) -> ColonyStats {
696 let docs = self.substrate.all_documents();
697 let digested = docs.iter().filter(|d| d.digested).count();
698 ColonyStats {
699 tick: self.substrate.current_tick(),
700 agents_alive: self.agents.len(),
701 agents_died: self.total_died,
702 total_spawned: self.total_spawned,
703 graph_nodes: self.substrate.node_count(),
704 graph_edges: self.substrate.edge_count(),
705 total_signals: self.substrate.all_signals().len(),
706 documents_total: docs.len(),
707 documents_digested: digested,
708 }
709 }
710
711 pub fn substrate(&self) -> &SubstrateImpl {
713 &self.substrate
714 }
715
716 pub fn substrate_mut(&mut self) -> &mut SubstrateImpl {
718 &mut self.substrate
719 }
720
721 pub fn alive_count(&self) -> usize {
723 self.agents.len()
724 }
725
726 pub fn death_signals(&self) -> &[DeathSignal] {
728 &self.death_signals
729 }
730
731 pub fn feed_agent(&mut self, agent_idx: usize, input: String) -> Option<DigestionResult> {
733 self.agents
734 .get_mut(agent_idx)
735 .map(|agent| agent.engulf(input))
736 }
737
738 pub fn snapshot(&self) -> ColonySnapshot {
740 let graph = self.substrate.graph();
741
742 let agents: Vec<AgentSnapshot> = self.agents.iter().map(|a| {
743 AgentSnapshot {
744 id: a.id(),
745 agent_type: a.agent_type().to_string(),
746 position: a.position(),
747 age: a.age(),
748 permeability: a.permeability(),
749 vocabulary_size: a.vocabulary_size(),
750 }
751 }).collect();
752
753 let nodes: Vec<NodeSnapshot> = graph.all_nodes().iter().filter_map(|nid| {
754 let n = graph.get_node(nid)?;
755 Some(NodeSnapshot {
756 id: n.id,
757 label: n.label.clone(),
758 node_type: n.node_type.clone(),
759 position: n.position,
760 access_count: n.access_count,
761 })
762 }).collect();
763
764 let edges: Vec<EdgeSnapshot> = graph.all_edges().iter().map(|(from, to, data)| {
765 let from_label = graph.get_node(from).map(|n| n.label.clone()).unwrap_or_default();
766 let to_label = graph.get_node(to).map(|n| n.label.clone()).unwrap_or_default();
767 EdgeSnapshot {
768 from_label,
769 to_label,
770 weight: data.weight,
771 co_activations: data.co_activations,
772 }
773 }).collect();
774
775 ColonySnapshot {
776 tick: self.substrate.current_tick(),
777 agents,
778 nodes,
779 edges,
780 stats: self.stats(),
781 }
782 }
783
784 pub fn event_history(&self) -> &[(Tick, ColonyEvent)] {
786 &self.event_history
787 }
788
789 pub fn agents(&self) -> &[Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>] {
791 &self.agents
792 }
793
794 pub fn fitness_tracker(&self) -> &FitnessTracker {
796 &self.fitness_tracker
797 }
798
799 pub fn fitness_tracker_mut(&mut self) -> &mut FitnessTracker {
801 &mut self.fitness_tracker
802 }
803
804 pub fn emit_input_signal(&mut self, position: Position, intensity: f64) {
806 let signal = Signal::new(
807 SignalType::Input,
808 intensity,
809 position,
810 AgentId::new(),
811 self.substrate.current_tick(),
812 );
813 self.substrate.emit_signal(signal);
814 }
815}
816
817impl Default for Colony {
818 fn default() -> Self {
819 Self::new()
820 }
821}
822
823#[cfg(test)]
824mod tests {
825 use super::*;
826 use phago_agents::digester::Digester;
827
828 #[test]
829 fn spawn_and_count_agents() {
830 let mut colony = Colony::new();
831 colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0))));
832 colony.spawn(Box::new(Digester::new(Position::new(5.0, 5.0))));
833 assert_eq!(colony.alive_count(), 2);
834 assert_eq!(colony.stats().total_spawned, 2);
835 }
836
837 #[test]
838 fn tick_advances_simulation() {
839 let mut colony = Colony::new();
840 colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0))));
841 colony.tick();
842 assert_eq!(colony.stats().tick, 1);
843 }
844
845 #[test]
846 fn agent_apoptosis_in_colony() {
847 let mut colony = Colony::new();
848 colony.spawn(Box::new(
849 Digester::new(Position::new(0.0, 0.0)).with_max_idle(3),
850 ));
851
852 assert_eq!(colony.alive_count(), 1);
853
854 for _ in 0..5 {
855 colony.tick();
856 }
857
858 assert_eq!(colony.alive_count(), 0);
859 assert_eq!(colony.stats().agents_died, 1);
860 assert_eq!(colony.death_signals().len(), 1);
861 }
862
863 #[test]
864 fn ingest_document_creates_signal() {
865 let mut colony = Colony::new();
866 let pos = Position::new(5.0, 5.0);
867 let doc_id = colony.ingest_document("Test Doc", "cell membrane protein", pos);
868
869 let stats = colony.stats();
870 assert_eq!(stats.documents_total, 1);
871 assert_eq!(stats.documents_digested, 0);
872 assert_eq!(stats.total_signals, 1); let doc = colony.substrate().get_document(&doc_id);
876 assert!(doc.is_some());
877 assert!(!doc.unwrap().digested);
878 }
879
880 #[test]
881 fn agent_finds_and_digests_document() {
882 let mut colony = Colony::new();
883
884 colony.ingest_document(
886 "Biology 101",
887 "The cell membrane controls transport of molecules into and out of the cell. \
888 Proteins embedded in the membrane serve as channels and receptors.",
889 Position::new(0.0, 0.0),
890 );
891
892 colony.spawn(Box::new(
894 Digester::new(Position::new(0.0, 0.0)).with_max_idle(50),
895 ));
896
897 colony.run(10);
903
904 let stats = colony.stats();
905 assert_eq!(stats.documents_digested, 1, "Document should be digested");
906 assert!(stats.graph_nodes > 0, "Should have concept nodes: got {}", stats.graph_nodes);
907 assert!(stats.graph_edges > 0, "Should have edges: got {}", stats.graph_edges);
908 }
909
910 #[test]
911 fn multiple_documents_build_graph() {
912 let mut colony = Colony::new();
913
914 colony.ingest_document(
916 "Cell Biology",
917 "The cell membrane is a lipid bilayer that controls transport. \
918 Proteins in the membrane act as channels and receptors for signaling.",
919 Position::new(0.0, 0.0),
920 );
921 colony.ingest_document(
922 "Molecular Transport",
923 "Active transport across the cell membrane requires ATP energy. \
924 Channel proteins facilitate passive transport of ions and molecules.",
925 Position::new(2.0, 0.0),
926 );
927
928 colony.spawn(Box::new(
930 Digester::new(Position::new(0.0, 0.0)).with_max_idle(50),
931 ));
932 colony.spawn(Box::new(
933 Digester::new(Position::new(2.0, 0.0)).with_max_idle(50),
934 ));
935
936 colony.run(20);
937
938 let stats = colony.stats();
939 assert_eq!(stats.documents_digested, 2, "Both documents should be digested");
940 assert!(stats.graph_nodes >= 5, "Expected at least 5 concept nodes, got {}", stats.graph_nodes);
943 }
944
945 #[test]
946 fn colony_stats_are_accurate() {
947 let mut colony = Colony::new();
948 colony.spawn(Box::new(
949 Digester::new(Position::new(0.0, 0.0)).with_max_idle(2),
950 ));
951 colony.spawn(Box::new(
952 Digester::new(Position::new(5.0, 5.0)).with_max_idle(100),
953 ));
954
955 colony.run(5);
956
957 let stats = colony.stats();
958 assert_eq!(stats.total_spawned, 2);
959 assert_eq!(stats.agents_died, 1);
960 assert_eq!(stats.agents_alive, 1);
961 }
962
963 #[test]
964 fn semantic_wiring_config_is_accessible() {
965 let colony = Colony::new();
966 let config = colony.semantic_wiring_config();
967 assert!(!config.require_embeddings);
969 assert!(config.min_similarity >= 0.0);
970 assert!(config.similarity_influence >= 0.0);
971 }
972
973 #[test]
974 fn with_semantic_wiring_configures_colony() {
975 use phago_core::semantic::SemanticWiringConfig;
976
977 let colony = Colony::new()
978 .with_semantic_wiring(SemanticWiringConfig::strict());
979
980 let config = colony.semantic_wiring_config();
981 assert!(config.require_embeddings);
982 assert!(config.min_similarity > 0.0);
983 }
984
985 #[test]
986 fn semantic_wiring_boosts_similar_concept_edges() {
987 use phago_core::semantic::SemanticWiringConfig;
988
989 let mut colony = Colony::new()
990 .with_semantic_wiring(SemanticWiringConfig::default());
991
992 let emb_a = vec![1.0, 0.0, 0.0]; let emb_b = vec![0.95, 0.31, 0.0]; let node_a = colony.substrate_mut().add_node(NodeData {
997 id: NodeId::new(),
998 label: "concept_a".to_string(),
999 node_type: NodeType::Concept,
1000 position: Position::new(0.0, 0.0),
1001 access_count: 1,
1002 created_tick: 0,
1003 embedding: Some(emb_a),
1004 });
1005
1006 let node_b = colony.substrate_mut().add_node(NodeData {
1007 id: NodeId::new(),
1008 label: "concept_b".to_string(),
1009 node_type: NodeType::Concept,
1010 position: Position::new(1.0, 0.0),
1011 access_count: 1,
1012 created_tick: 0,
1013 embedding: Some(emb_b),
1014 });
1015
1016 colony.substrate_mut().set_edge(node_a, node_b, EdgeData {
1018 weight: 0.1,
1019 co_activations: 1,
1020 created_tick: 0,
1021 last_activated_tick: 0,
1022 });
1023
1024 let edge = colony.substrate().graph().get_edge(&node_a, &node_b).unwrap();
1025 assert!(edge.weight >= 0.1, "Edge should have at least base weight");
1026 }
1027
1028 #[test]
1029 fn semantic_wiring_with_no_embeddings_uses_base_weight() {
1030 use phago_core::semantic::SemanticWiringConfig;
1031
1032 let mut colony = Colony::new()
1033 .with_semantic_wiring(SemanticWiringConfig::default());
1034
1035 let node_a = colony.substrate_mut().add_node(NodeData {
1037 id: NodeId::new(),
1038 label: "plain_a".to_string(),
1039 node_type: NodeType::Concept,
1040 position: Position::new(0.0, 0.0),
1041 access_count: 1,
1042 created_tick: 0,
1043 embedding: None,
1044 });
1045
1046 let node_b = colony.substrate_mut().add_node(NodeData {
1047 id: NodeId::new(),
1048 label: "plain_b".to_string(),
1049 node_type: NodeType::Concept,
1050 position: Position::new(1.0, 0.0),
1051 access_count: 1,
1052 created_tick: 0,
1053 embedding: None,
1054 });
1055
1056 colony.substrate_mut().set_edge(node_a, node_b, EdgeData {
1058 weight: 0.1,
1059 co_activations: 1,
1060 created_tick: 0,
1061 last_activated_tick: 0,
1062 });
1063
1064 let edge = colony.substrate().graph().get_edge(&node_a, &node_b).unwrap();
1065 assert!((edge.weight - 0.1).abs() < 0.01, "Edge should use base weight: got {}", edge.weight);
1067 }
1068}