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::{Deserialize, 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
107#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ColonyConfig {
114 pub signal_decay_rate: f64,
116 pub signal_removal_threshold: f64,
118 pub trace_decay_rate: f64,
120 pub trace_removal_threshold: f64,
122 pub edge_decay_rate: f64,
124 pub edge_prune_threshold: f64,
126 pub staleness_factor: f64,
128 pub maturation_ticks: u64,
130 pub max_edge_degree: usize,
132 pub semantic_wiring: SemanticWiringConfig,
134}
135
136impl Default for ColonyConfig {
137 fn default() -> Self {
138 Self {
139 signal_decay_rate: 0.05,
140 signal_removal_threshold: 0.01,
141 trace_decay_rate: 0.02,
142 trace_removal_threshold: 0.01,
143 edge_decay_rate: 0.005,
144 edge_prune_threshold: 0.05,
145 staleness_factor: 1.5,
146 maturation_ticks: 50,
147 max_edge_degree: 30,
148 semantic_wiring: SemanticWiringConfig::default(),
149 }
150 }
151}
152
153pub struct Colony {
155 substrate: SubstrateImpl,
156 agents: Vec<Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>>,
157 death_signals: Vec<DeathSignal>,
158 event_history: Vec<(Tick, ColonyEvent)>,
159 total_spawned: usize,
160 total_died: usize,
161 fitness_tracker: FitnessTracker,
162
163 signal_decay_rate: f64,
165 signal_removal_threshold: f64,
166 trace_decay_rate: f64,
167 trace_removal_threshold: f64,
168 edge_decay_rate: f64,
169 edge_prune_threshold: f64,
170 staleness_factor: f64,
171 maturation_ticks: u64,
172 max_edge_degree: usize,
173 semantic_wiring: SemanticWiringConfig,
174}
175
176impl Colony {
177 pub fn new() -> Self {
179 Self::from_config(ColonyConfig::default())
180 }
181
182 pub fn from_config(config: ColonyConfig) -> Self {
184 Self {
185 substrate: SubstrateImpl::new(),
186 agents: Vec::new(),
187 death_signals: Vec::new(),
188 event_history: Vec::new(),
189 total_spawned: 0,
190 total_died: 0,
191 fitness_tracker: FitnessTracker::new(),
192 signal_decay_rate: config.signal_decay_rate,
193 signal_removal_threshold: config.signal_removal_threshold,
194 trace_decay_rate: config.trace_decay_rate,
195 trace_removal_threshold: config.trace_removal_threshold,
196 edge_decay_rate: config.edge_decay_rate,
197 edge_prune_threshold: config.edge_prune_threshold,
198 staleness_factor: config.staleness_factor,
199 maturation_ticks: config.maturation_ticks,
200 max_edge_degree: config.max_edge_degree,
201 semantic_wiring: config.semantic_wiring,
202 }
203 }
204
205 pub fn config(&self) -> ColonyConfig {
207 ColonyConfig {
208 signal_decay_rate: self.signal_decay_rate,
209 signal_removal_threshold: self.signal_removal_threshold,
210 trace_decay_rate: self.trace_decay_rate,
211 trace_removal_threshold: self.trace_removal_threshold,
212 edge_decay_rate: self.edge_decay_rate,
213 edge_prune_threshold: self.edge_prune_threshold,
214 staleness_factor: self.staleness_factor,
215 maturation_ticks: self.maturation_ticks,
216 max_edge_degree: self.max_edge_degree,
217 semantic_wiring: self.semantic_wiring.clone(),
218 }
219 }
220
221 pub fn with_semantic_wiring(mut self, config: SemanticWiringConfig) -> Self {
223 self.semantic_wiring = config;
224 self
225 }
226
227 pub fn semantic_wiring_config(&self) -> &SemanticWiringConfig {
229 &self.semantic_wiring
230 }
231
232 pub fn set_semantic_wiring(&mut self, config: SemanticWiringConfig) {
234 self.semantic_wiring = config;
235 }
236
237 pub fn spawn(
239 &mut self,
240 agent: Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>,
241 ) -> AgentId {
242 let id = agent.id();
243 self.total_spawned += 1;
244 self.fitness_tracker.register(id, 0);
245 self.agents.push(agent);
246 id
247 }
248
249 pub fn ingest_document(&mut self, title: &str, content: &str, position: Position) -> DocumentId {
254 let doc = Document {
255 id: DocumentId::new(),
256 title: title.to_string(),
257 content: content.to_string(),
258 position,
259 digested: false,
260 };
261 let doc_id = doc.id;
262 let doc_pos = doc.position;
263
264 self.substrate.add_document(doc);
265
266 self.substrate.emit_signal(Signal::new(
268 SignalType::Input,
269 1.0,
270 doc_pos,
271 AgentId::new(), self.substrate.current_tick(),
273 ));
274
275 doc_id
276 }
277
278 pub fn tick(&mut self) -> Vec<ColonyEvent> {
280 let mut events = Vec::new();
281 let mut actions: Vec<(usize, AgentAction)> = Vec::new();
282
283 for (idx, agent) in self.agents.iter_mut().enumerate() {
285 let action = agent.tick(&self.substrate);
286 actions.push((idx, action));
287 }
288
289 let mut to_die = Vec::new();
291 let mut symbiotic_deaths: Vec<(usize, AgentId)> = Vec::new(); for (idx, action) in actions {
294 match action {
295 AgentAction::Move(pos) => {
296 self.agents[idx].set_position(pos);
297 events.push(ColonyEvent::Moved {
298 id: self.agents[idx].id(),
299 to: pos,
300 });
301 }
302
303 AgentAction::EngulfDocument(doc_id) => {
304 if let Some(content) = self.substrate.consume_document(&doc_id) {
306 self.agents[idx].engulf(content);
307 events.push(ColonyEvent::Engulfed {
310 id: self.agents[idx].id(),
311 document: doc_id,
312 });
313 }
314 }
315
316 AgentAction::PresentFragments(fragments) => {
317 let agent_id = self.agents[idx].id();
318 let tick = self.substrate.current_tick();
319 let mut node_ids = Vec::new();
320
321 for frag in &fragments {
322 let existing = self.substrate.graph().find_nodes_by_label(&frag.label);
324 let node_id = if let Some(&existing_id) = existing.first() {
325 if let Some(node) = self.substrate.graph_mut().get_node_mut(&existing_id) {
327 node.access_count += 1;
328 }
329 existing_id
330 } else {
331 let node = NodeData {
333 id: NodeId::new(),
334 label: frag.label.clone(),
335 node_type: frag.node_type.clone(),
336 position: frag.position,
337 access_count: 1,
338 created_tick: tick,
339 embedding: None,
340 };
341 self.substrate.add_node(node)
342 };
343 node_ids.push(node_id);
344 }
345
346 let concept_node_ids: Vec<NodeId> = node_ids.iter().filter(|id| {
361 self.substrate.graph().get_node(id)
362 .map_or(false, |n| n.node_type == NodeType::Concept)
363 }).copied().collect();
364 let mut wire_events = Vec::new();
365 for i in 0..concept_node_ids.len() {
366 for j in (i + 1)..concept_node_ids.len() {
367 let from = concept_node_ids[i];
368 let to = concept_node_ids[j];
369
370 let embedding_from = self.substrate.graph().get_node(&from)
372 .and_then(|n| n.embedding.clone());
373 let embedding_to = self.substrate.graph().get_node(&to)
374 .and_then(|n| n.embedding.clone());
375
376 let base_weight = 0.1;
378 let semantic_weight = compute_semantic_weight(
379 base_weight,
380 embedding_from.as_deref(),
381 embedding_to.as_deref(),
382 &self.semantic_wiring,
383 );
384
385 if let Some(edge) = self.substrate.graph_mut().get_edge_mut(&from, &to) {
386 let reinforcement = semantic_weight.unwrap_or(base_weight);
389 edge.weight = (edge.weight + reinforcement).min(1.0);
390 edge.co_activations += 1;
391 edge.last_activated_tick = tick;
392 wire_events.push((from, to));
393 } else {
394 let weight = semantic_weight;
397
398 if let Some(w) = weight {
400 self.substrate.set_edge(from, to, EdgeData {
401 weight: w,
402 co_activations: 1,
403 created_tick: tick,
404 last_activated_tick: tick,
405 });
406 wire_events.push((from, to));
407 }
408 }
409 }
410 }
411
412 events.push(ColonyEvent::Presented {
413 id: agent_id,
414 fragment_count: fragments.len(),
415 node_ids,
416 });
417
418 if !wire_events.is_empty() {
419 events.push(ColonyEvent::Wired {
420 id: agent_id,
421 connection_count: wire_events.len(),
422 });
423 }
424 }
425
426 AgentAction::Deposit(location, trace) => {
427 let agent_id = self.agents[idx].id();
428 self.substrate.deposit_trace(&location, trace);
429 events.push(ColonyEvent::Deposited {
430 id: agent_id,
431 location,
432 });
433 }
434
435 AgentAction::Emit(signal) => {
436 self.substrate.emit_signal(signal);
437 }
438
439 AgentAction::WireNodes(connections) => {
440 let agent_id = self.agents[idx].id();
441 let tick = self.substrate.current_tick();
442 let mut wired_count = 0;
443 for (from, to, base_weight) in &connections {
444 let embedding_from = self.substrate.graph().get_node(from)
446 .and_then(|n| n.embedding.clone());
447 let embedding_to = self.substrate.graph().get_node(to)
448 .and_then(|n| n.embedding.clone());
449
450 let weight = compute_semantic_weight(
452 *base_weight,
453 embedding_from.as_deref(),
454 embedding_to.as_deref(),
455 &self.semantic_wiring,
456 );
457
458 if let Some(w) = weight {
459 if let Some(edge) = self.substrate.graph_mut().get_edge_mut(from, to) {
460 edge.weight = (edge.weight + w).min(1.0);
461 edge.co_activations += 1;
462 edge.last_activated_tick = tick;
463 } else {
464 self.substrate.set_edge(*from, *to, EdgeData {
465 weight: w,
466 co_activations: 1,
467 created_tick: tick,
468 last_activated_tick: tick,
469 });
470 }
471 wired_count += 1;
472 }
473 }
474 if wired_count > 0 {
475 events.push(ColonyEvent::Wired {
476 id: agent_id,
477 connection_count: wired_count,
478 });
479 }
480 }
481
482 AgentAction::ExportCapability(_cap_id) => {
483 let agent_id = self.agents[idx].id();
484 let agent_pos = self.agents[idx].position();
485 if let Some(vocab_bytes) = self.agents[idx].export_vocabulary() {
486 let terms_count = serde_json::from_slice::<VocabularyCapability>(&vocab_bytes)
488 .map(|v| v.terms.len())
489 .unwrap_or(0);
490
491 let trace = Trace {
493 agent_id,
494 trace_type: TraceType::CapabilityDeposit,
495 intensity: 1.0,
496 tick: self.substrate.current_tick(),
497 payload: vocab_bytes,
498 };
499 self.substrate.deposit_trace(
500 &SubstrateLocation::Spatial(agent_pos),
501 trace,
502 );
503
504 self.substrate.emit_signal(Signal::new(
506 SignalType::Capability,
507 0.8,
508 agent_pos,
509 agent_id,
510 self.substrate.current_tick(),
511 ));
512
513 events.push(ColonyEvent::CapabilityExported {
514 agent_id,
515 terms_count,
516 });
517 }
518 }
519
520 AgentAction::SymbioseWith(target_id) => {
521 let host_idx = idx;
522 let host_id = self.agents[host_idx].id();
523
524 if let Some(target_idx) = self.agents.iter().position(|a| a.id() == target_id) {
526 let target_profile = self.agents[target_idx].profile();
528 let target_vocab = self.agents[target_idx].export_vocabulary()
529 .unwrap_or_default();
530
531 if let Some(SymbiosisEval::Integrate) =
533 self.agents[host_idx].evaluate_symbiosis(&target_profile)
534 {
535 let host_type = self.agents[host_idx].agent_type().to_string();
536 let absorbed_type = self.agents[target_idx].agent_type().to_string();
537
538 self.agents[host_idx].absorb_symbiont(target_profile, target_vocab);
540
541 symbiotic_deaths.push((target_idx, host_id));
543
544 events.push(ColonyEvent::Symbiosis {
545 host: host_id,
546 absorbed: target_id,
547 host_type,
548 absorbed_type,
549 });
550 }
551 }
552 }
553
554 AgentAction::Apoptose => {
555 to_die.push(idx);
556 }
557
558 AgentAction::Idle => {}
559
560 _ => {}
561 }
562 }
563
564 {
568 let _tick = self.substrate.current_tick();
569 let agent_count = self.agents.len();
570
571 for i in 0..agent_count {
572 let agent_id = self.agents[i].id();
573 let agent_pos = self.agents[i].position();
574 let agent_age = self.agents[i].age();
575
576 let vocab_terms = self.agents[i].externalize_vocabulary();
578 let mut reinforcement_count = 0u64;
579 let graph = self.substrate.graph();
580 for term in &vocab_terms {
581 let matching = graph.find_nodes_by_exact_label(term);
582 for nid in matching {
583 if let Some(node) = graph.get_node(nid) {
584 reinforcement_count += node.access_count;
585 }
586 }
587 }
588
589 let useful_outputs_estimate = reinforcement_count.min(100);
590 let trust = if agent_age > 0 {
591 (useful_outputs_estimate as f64 / agent_age as f64).min(1.0)
592 } else {
593 0.0
594 };
595
596 let context = BoundaryContext {
597 reinforcement_count,
598 age: agent_age,
599 trust,
600 };
601
602 self.agents[i].modulate_boundary(&context);
603 let permeability = self.agents[i].permeability();
604
605 if permeability > 0.5 {
607 let mut terms_externalized = 0usize;
609 for term in &vocab_terms {
610 let matching: Vec<NodeId> = self.substrate.graph().find_nodes_by_exact_label(term).to_vec();
611 for nid in &matching {
612 if let Some(node) = self.substrate.graph_mut().get_node_mut(nid) {
613 node.access_count += 1;
614 terms_externalized += 1;
615 }
616 }
617 }
618 if terms_externalized > 0 {
619 events.push(ColonyEvent::Dissolved {
620 agent_id,
621 permeability,
622 terms_externalized,
623 });
624 }
625 }
626
627 if permeability > 0.0 {
629 let all_nodes = self.substrate.graph().all_nodes();
630 let nearby_labels: Vec<String> = all_nodes.iter()
631 .filter_map(|nid| {
632 let node = self.substrate.graph().get_node(nid)?;
633 if node.position.distance_to(&agent_pos) <= 15.0
634 && node.node_type == NodeType::Concept
635 {
636 Some(node.label.clone())
637 } else {
638 None
639 }
640 })
641 .collect();
642 if !nearby_labels.is_empty() {
643 self.agents[i].internalize_vocabulary(&nearby_labels);
644 }
645 }
646
647 let traces = self.substrate.traces_near(
649 &agent_pos,
650 10.0,
651 &TraceType::CapabilityDeposit,
652 );
653 for trace in &traces {
654 if trace.agent_id != agent_id
655 && !trace.payload.is_empty()
656 {
657 let payload = trace.payload.clone();
658 let from_agent = trace.agent_id;
659 let terms_count = serde_json::from_slice::<VocabularyCapability>(&payload)
660 .map(|v| v.terms.len())
661 .unwrap_or(0);
662 if self.agents[i].integrate_vocabulary(&payload) {
663 events.push(ColonyEvent::CapabilityIntegrated {
664 agent_id,
665 from_agent,
666 terms_count,
667 });
668 }
669 }
670 }
671 }
672 }
673
674 for (idx, _absorber_id) in &symbiotic_deaths {
676 if !to_die.contains(idx) {
677 to_die.push(*idx);
678 }
679 }
680
681 to_die.sort();
683 to_die.dedup();
684 let dead_count = to_die.len();
685 for idx in to_die.into_iter().rev() {
686 let agent = self.agents.remove(idx);
687 let mut death_signal = agent.prepare_death_signal();
688
689 if let Some((_, absorber_id)) = symbiotic_deaths.iter().find(|(i, _)| *i == idx) {
691 death_signal.cause = DeathCause::SymbioticAbsorption(*absorber_id);
692 }
693
694 events.push(ColonyEvent::Died {
695 signal: death_signal.clone(),
696 });
697 self.death_signals.push(death_signal);
698 self.total_died += 1;
699 }
700
701 self.substrate
703 .decay_signals(self.signal_decay_rate, self.signal_removal_threshold);
704 self.substrate
705 .decay_traces(self.trace_decay_rate, self.trace_removal_threshold);
706 let current_tick = self.substrate.current_tick();
708 self.substrate.graph_mut().decay_edges_activity(
709 self.edge_decay_rate,
710 self.edge_prune_threshold,
711 current_tick,
712 self.staleness_factor,
713 self.maturation_ticks,
714 );
715 self.substrate
717 .graph_mut()
718 .prune_to_max_degree(self.max_edge_degree);
719
720 for event in &events {
722 match event {
723 ColonyEvent::Presented { id, fragment_count, .. } => {
724 self.fitness_tracker.record_concepts(id, *fragment_count as u64);
725 }
726 ColonyEvent::Wired { id, connection_count } => {
727 self.fitness_tracker.record_edges(id, *connection_count as u64);
728 }
729 _ => {}
730 }
731 }
732 let alive_ids: Vec<AgentId> = self.agents.iter().map(|a| a.id()).collect();
733 self.fitness_tracker.tick_all(&alive_ids);
734
735 self.substrate.advance_tick();
737
738 events.push(ColonyEvent::TickComplete {
739 tick: self.substrate.current_tick(),
740 alive: self.agents.len(),
741 dead_this_tick: dead_count,
742 });
743
744 let current_tick = self.substrate.current_tick();
746 for event in &events {
747 self.event_history.push((current_tick, event.clone()));
748 }
749
750 events
751 }
752
753 pub fn run(&mut self, ticks: u64) -> Vec<Vec<ColonyEvent>> {
755 let mut all_events = Vec::new();
756 for _ in 0..ticks {
757 all_events.push(self.tick());
758 }
759 all_events
760 }
761
762 pub fn stats(&self) -> ColonyStats {
764 let docs = self.substrate.all_documents();
765 let digested = docs.iter().filter(|d| d.digested).count();
766 ColonyStats {
767 tick: self.substrate.current_tick(),
768 agents_alive: self.agents.len(),
769 agents_died: self.total_died,
770 total_spawned: self.total_spawned,
771 graph_nodes: self.substrate.node_count(),
772 graph_edges: self.substrate.edge_count(),
773 total_signals: self.substrate.all_signals().len(),
774 documents_total: docs.len(),
775 documents_digested: digested,
776 }
777 }
778
779 pub fn substrate(&self) -> &SubstrateImpl {
781 &self.substrate
782 }
783
784 pub fn substrate_mut(&mut self) -> &mut SubstrateImpl {
786 &mut self.substrate
787 }
788
789 pub fn alive_count(&self) -> usize {
791 self.agents.len()
792 }
793
794 pub fn death_signals(&self) -> &[DeathSignal] {
796 &self.death_signals
797 }
798
799 pub fn feed_agent(&mut self, agent_idx: usize, input: String) -> Option<DigestionResult> {
801 self.agents
802 .get_mut(agent_idx)
803 .map(|agent| agent.engulf(input))
804 }
805
806 pub fn snapshot(&self) -> ColonySnapshot {
808 let graph = self.substrate.graph();
809
810 let agents: Vec<AgentSnapshot> = self.agents.iter().map(|a| {
811 AgentSnapshot {
812 id: a.id(),
813 agent_type: a.agent_type().to_string(),
814 position: a.position(),
815 age: a.age(),
816 permeability: a.permeability(),
817 vocabulary_size: a.vocabulary_size(),
818 }
819 }).collect();
820
821 let nodes: Vec<NodeSnapshot> = graph.all_nodes().iter().filter_map(|nid| {
822 let n = graph.get_node(nid)?;
823 Some(NodeSnapshot {
824 id: n.id,
825 label: n.label.clone(),
826 node_type: n.node_type.clone(),
827 position: n.position,
828 access_count: n.access_count,
829 })
830 }).collect();
831
832 let edges: Vec<EdgeSnapshot> = graph.all_edges().iter().map(|(from, to, data)| {
833 let from_label = graph.get_node(from).map(|n| n.label.clone()).unwrap_or_default();
834 let to_label = graph.get_node(to).map(|n| n.label.clone()).unwrap_or_default();
835 EdgeSnapshot {
836 from_label,
837 to_label,
838 weight: data.weight,
839 co_activations: data.co_activations,
840 }
841 }).collect();
842
843 ColonySnapshot {
844 tick: self.substrate.current_tick(),
845 agents,
846 nodes,
847 edges,
848 stats: self.stats(),
849 }
850 }
851
852 pub fn event_history(&self) -> &[(Tick, ColonyEvent)] {
854 &self.event_history
855 }
856
857 pub fn agents(&self) -> &[Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>] {
859 &self.agents
860 }
861
862 pub fn fitness_tracker(&self) -> &FitnessTracker {
864 &self.fitness_tracker
865 }
866
867 pub fn fitness_tracker_mut(&mut self) -> &mut FitnessTracker {
869 &mut self.fitness_tracker
870 }
871
872 pub fn emit_input_signal(&mut self, position: Position, intensity: f64) {
874 let signal = Signal::new(
875 SignalType::Input,
876 intensity,
877 position,
878 AgentId::new(),
879 self.substrate.current_tick(),
880 );
881 self.substrate.emit_signal(signal);
882 }
883}
884
885impl Default for Colony {
886 fn default() -> Self {
887 Self::new()
888 }
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894 use phago_agents::digester::Digester;
895
896 #[test]
897 fn spawn_and_count_agents() {
898 let mut colony = Colony::new();
899 colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0))));
900 colony.spawn(Box::new(Digester::new(Position::new(5.0, 5.0))));
901 assert_eq!(colony.alive_count(), 2);
902 assert_eq!(colony.stats().total_spawned, 2);
903 }
904
905 #[test]
906 fn tick_advances_simulation() {
907 let mut colony = Colony::new();
908 colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0))));
909 colony.tick();
910 assert_eq!(colony.stats().tick, 1);
911 }
912
913 #[test]
914 fn agent_apoptosis_in_colony() {
915 let mut colony = Colony::new();
916 colony.spawn(Box::new(
917 Digester::new(Position::new(0.0, 0.0)).with_max_idle(3),
918 ));
919
920 assert_eq!(colony.alive_count(), 1);
921
922 for _ in 0..5 {
923 colony.tick();
924 }
925
926 assert_eq!(colony.alive_count(), 0);
927 assert_eq!(colony.stats().agents_died, 1);
928 assert_eq!(colony.death_signals().len(), 1);
929 }
930
931 #[test]
932 fn ingest_document_creates_signal() {
933 let mut colony = Colony::new();
934 let pos = Position::new(5.0, 5.0);
935 let doc_id = colony.ingest_document("Test Doc", "cell membrane protein", pos);
936
937 let stats = colony.stats();
938 assert_eq!(stats.documents_total, 1);
939 assert_eq!(stats.documents_digested, 0);
940 assert_eq!(stats.total_signals, 1); let doc = colony.substrate().get_document(&doc_id);
944 assert!(doc.is_some());
945 assert!(!doc.unwrap().digested);
946 }
947
948 #[test]
949 fn agent_finds_and_digests_document() {
950 let mut colony = Colony::new();
951
952 colony.ingest_document(
954 "Biology 101",
955 "The cell membrane controls transport of molecules into and out of the cell. \
956 Proteins embedded in the membrane serve as channels and receptors.",
957 Position::new(0.0, 0.0),
958 );
959
960 colony.spawn(Box::new(
962 Digester::new(Position::new(0.0, 0.0)).with_max_idle(50),
963 ));
964
965 colony.run(10);
971
972 let stats = colony.stats();
973 assert_eq!(stats.documents_digested, 1, "Document should be digested");
974 assert!(stats.graph_nodes > 0, "Should have concept nodes: got {}", stats.graph_nodes);
975 assert!(stats.graph_edges > 0, "Should have edges: got {}", stats.graph_edges);
976 }
977
978 #[test]
979 fn multiple_documents_build_graph() {
980 let mut colony = Colony::new();
981
982 colony.ingest_document(
984 "Cell Biology",
985 "The cell membrane is a lipid bilayer that controls transport. \
986 Proteins in the membrane act as channels and receptors for signaling.",
987 Position::new(0.0, 0.0),
988 );
989 colony.ingest_document(
990 "Molecular Transport",
991 "Active transport across the cell membrane requires ATP energy. \
992 Channel proteins facilitate passive transport of ions and molecules.",
993 Position::new(2.0, 0.0),
994 );
995
996 colony.spawn(Box::new(
998 Digester::new(Position::new(0.0, 0.0)).with_max_idle(50),
999 ));
1000 colony.spawn(Box::new(
1001 Digester::new(Position::new(2.0, 0.0)).with_max_idle(50),
1002 ));
1003
1004 colony.run(20);
1005
1006 let stats = colony.stats();
1007 assert_eq!(stats.documents_digested, 2, "Both documents should be digested");
1008 assert!(stats.graph_nodes >= 5, "Expected at least 5 concept nodes, got {}", stats.graph_nodes);
1011 }
1012
1013 #[test]
1014 fn colony_stats_are_accurate() {
1015 let mut colony = Colony::new();
1016 colony.spawn(Box::new(
1017 Digester::new(Position::new(0.0, 0.0)).with_max_idle(2),
1018 ));
1019 colony.spawn(Box::new(
1020 Digester::new(Position::new(5.0, 5.0)).with_max_idle(100),
1021 ));
1022
1023 colony.run(5);
1024
1025 let stats = colony.stats();
1026 assert_eq!(stats.total_spawned, 2);
1027 assert_eq!(stats.agents_died, 1);
1028 assert_eq!(stats.agents_alive, 1);
1029 }
1030
1031 #[test]
1032 fn semantic_wiring_config_is_accessible() {
1033 let colony = Colony::new();
1034 let config = colony.semantic_wiring_config();
1035 assert!(!config.require_embeddings);
1037 assert!(config.min_similarity >= 0.0);
1038 assert!(config.similarity_influence >= 0.0);
1039 }
1040
1041 #[test]
1042 fn with_semantic_wiring_configures_colony() {
1043 use phago_core::semantic::SemanticWiringConfig;
1044
1045 let colony = Colony::new()
1046 .with_semantic_wiring(SemanticWiringConfig::strict());
1047
1048 let config = colony.semantic_wiring_config();
1049 assert!(config.require_embeddings);
1050 assert!(config.min_similarity > 0.0);
1051 }
1052
1053 #[test]
1054 fn semantic_wiring_boosts_similar_concept_edges() {
1055 use phago_core::semantic::SemanticWiringConfig;
1056
1057 let mut colony = Colony::new()
1058 .with_semantic_wiring(SemanticWiringConfig::default());
1059
1060 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 {
1065 id: NodeId::new(),
1066 label: "concept_a".to_string(),
1067 node_type: NodeType::Concept,
1068 position: Position::new(0.0, 0.0),
1069 access_count: 1,
1070 created_tick: 0,
1071 embedding: Some(emb_a),
1072 });
1073
1074 let node_b = colony.substrate_mut().add_node(NodeData {
1075 id: NodeId::new(),
1076 label: "concept_b".to_string(),
1077 node_type: NodeType::Concept,
1078 position: Position::new(1.0, 0.0),
1079 access_count: 1,
1080 created_tick: 0,
1081 embedding: Some(emb_b),
1082 });
1083
1084 colony.substrate_mut().set_edge(node_a, node_b, EdgeData {
1086 weight: 0.1,
1087 co_activations: 1,
1088 created_tick: 0,
1089 last_activated_tick: 0,
1090 });
1091
1092 let edge = colony.substrate().graph().get_edge(&node_a, &node_b).unwrap();
1093 assert!(edge.weight >= 0.1, "Edge should have at least base weight");
1094 }
1095
1096 #[test]
1097 fn semantic_wiring_with_no_embeddings_uses_base_weight() {
1098 use phago_core::semantic::SemanticWiringConfig;
1099
1100 let mut colony = Colony::new()
1101 .with_semantic_wiring(SemanticWiringConfig::default());
1102
1103 let node_a = colony.substrate_mut().add_node(NodeData {
1105 id: NodeId::new(),
1106 label: "plain_a".to_string(),
1107 node_type: NodeType::Concept,
1108 position: Position::new(0.0, 0.0),
1109 access_count: 1,
1110 created_tick: 0,
1111 embedding: None,
1112 });
1113
1114 let node_b = colony.substrate_mut().add_node(NodeData {
1115 id: NodeId::new(),
1116 label: "plain_b".to_string(),
1117 node_type: NodeType::Concept,
1118 position: Position::new(1.0, 0.0),
1119 access_count: 1,
1120 created_tick: 0,
1121 embedding: None,
1122 });
1123
1124 colony.substrate_mut().set_edge(node_a, node_b, EdgeData {
1126 weight: 0.1,
1127 co_activations: 1,
1128 created_tick: 0,
1129 last_activated_tick: 0,
1130 });
1131
1132 let edge = colony.substrate().graph().get_edge(&node_a, &node_b).unwrap();
1133 assert!((edge.weight - 0.1).abs() < 0.01, "Edge should use base weight: got {}", edge.weight);
1135 }
1136}