Skip to main content

phago_runtime/
colony.rs

1//! Colony — agent lifecycle management.
2//!
3//! The colony is the organism. It manages the birth, life, and death
4//! of agents, runs the tick-based simulation loop, and coordinates
5//! agent access to the shared substrate.
6//!
7//! Each tick:
8//! 1. All agents sense the substrate and decide an action
9//! 2. The colony processes all actions (moves, digestions, signals)
10//! 3. Dead agents are removed, death signals collected
11//! 4. The substrate decays signals and traces
12//! 5. The tick counter advances
13
14use crate::substrate_impl::SubstrateImpl;
15use phago_agents::fitness::FitnessTracker;
16use phago_core::agent::Agent;
17use phago_core::substrate::Substrate;
18use phago_core::topology::TopologyGraph;
19use phago_core::types::*;
20use serde::Serialize;
21use serde_json;
22
23/// Event emitted by the colony during simulation.
24#[derive(Debug, Clone, Serialize)]
25pub enum ColonyEvent {
26    /// An agent was spawned.
27    Spawned { id: AgentId, agent_type: String },
28    /// An agent moved to a new position.
29    Moved { id: AgentId, to: Position },
30    /// An agent engulfed a document.
31    Engulfed { id: AgentId, document: DocumentId },
32    /// An agent presented fragments to the knowledge graph.
33    Presented { id: AgentId, fragment_count: usize, node_ids: Vec<NodeId> },
34    /// An agent deposited a trace.
35    Deposited { id: AgentId, location: SubstrateLocation },
36    /// An agent wired connections in the graph.
37    Wired { id: AgentId, connection_count: usize },
38    /// An agent triggered apoptosis.
39    Died { signal: DeathSignal },
40    /// A tick completed.
41    TickComplete { tick: Tick, alive: usize, dead_this_tick: usize },
42    /// An agent exported its vocabulary as a capability deposit.
43    CapabilityExported { agent_id: AgentId, terms_count: usize },
44    /// An agent integrated vocabulary from a capability deposit.
45    CapabilityIntegrated { agent_id: AgentId, from_agent: AgentId, terms_count: usize },
46    /// An agent absorbed another through symbiosis.
47    Symbiosis { host: AgentId, absorbed: AgentId, host_type: String, absorbed_type: String },
48    /// An agent's boundary dissolved, externalizing vocabulary.
49    Dissolved { agent_id: AgentId, permeability: f64, terms_externalized: usize },
50}
51
52/// Statistics about the colony.
53#[derive(Debug, Clone, Serialize)]
54pub struct ColonyStats {
55    pub tick: Tick,
56    pub agents_alive: usize,
57    pub agents_died: usize,
58    pub total_spawned: usize,
59    pub graph_nodes: usize,
60    pub graph_edges: usize,
61    pub total_signals: usize,
62    pub documents_total: usize,
63    pub documents_digested: usize,
64}
65
66/// A serializable snapshot of an agent's state.
67#[derive(Debug, Clone, Serialize)]
68pub struct AgentSnapshot {
69    pub id: AgentId,
70    pub agent_type: String,
71    pub position: Position,
72    pub age: Tick,
73    pub permeability: f64,
74    pub vocabulary_size: usize,
75}
76
77/// A serializable snapshot of a graph node.
78#[derive(Debug, Clone, Serialize)]
79pub struct NodeSnapshot {
80    pub id: NodeId,
81    pub label: String,
82    pub node_type: NodeType,
83    pub position: Position,
84    pub access_count: u64,
85}
86
87/// A serializable snapshot of a graph edge.
88#[derive(Debug, Clone, Serialize)]
89pub struct EdgeSnapshot {
90    pub from_label: String,
91    pub to_label: String,
92    pub weight: f64,
93    pub co_activations: u64,
94}
95
96/// A complete serializable snapshot of the colony at a point in time.
97#[derive(Debug, Clone, Serialize)]
98pub struct ColonySnapshot {
99    pub tick: Tick,
100    pub agents: Vec<AgentSnapshot>,
101    pub nodes: Vec<NodeSnapshot>,
102    pub edges: Vec<EdgeSnapshot>,
103    pub stats: ColonyStats,
104}
105
106/// The colony — manages agent lifecycle and simulation.
107pub struct Colony {
108    substrate: SubstrateImpl,
109    agents: Vec<Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>>,
110    death_signals: Vec<DeathSignal>,
111    event_history: Vec<(Tick, ColonyEvent)>,
112    total_spawned: usize,
113    total_died: usize,
114    fitness_tracker: FitnessTracker,
115
116    // Configuration
117    signal_decay_rate: f64,
118    signal_removal_threshold: f64,
119    trace_decay_rate: f64,
120    trace_removal_threshold: f64,
121    edge_decay_rate: f64,
122    edge_prune_threshold: f64,
123    staleness_factor: f64,
124    maturation_ticks: u64,
125    max_edge_degree: usize,
126}
127
128impl Colony {
129    pub fn new() -> Self {
130        Self {
131            substrate: SubstrateImpl::new(),
132            agents: Vec::new(),
133            death_signals: Vec::new(),
134            event_history: Vec::new(),
135            total_spawned: 0,
136            total_died: 0,
137            fitness_tracker: FitnessTracker::new(),
138            signal_decay_rate: 0.05,
139            signal_removal_threshold: 0.01,
140            trace_decay_rate: 0.02,
141            trace_removal_threshold: 0.01,
142            edge_decay_rate: 0.005,
143            edge_prune_threshold: 0.05,
144            staleness_factor: 1.5,
145            maturation_ticks: 50,
146            max_edge_degree: 30,
147        }
148    }
149
150    /// Spawn an agent into the colony.
151    pub fn spawn(
152        &mut self,
153        agent: Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>,
154    ) -> AgentId {
155        let id = agent.id();
156        self.total_spawned += 1;
157        self.fitness_tracker.register(id, 0);
158        self.agents.push(agent);
159        id
160    }
161
162    /// Ingest a document into the substrate.
163    ///
164    /// Places the document at the given position and emits an Input signal
165    /// to attract nearby agents via chemotaxis.
166    pub fn ingest_document(&mut self, title: &str, content: &str, position: Position) -> DocumentId {
167        let doc = Document {
168            id: DocumentId::new(),
169            title: title.to_string(),
170            content: content.to_string(),
171            position,
172            digested: false,
173        };
174        let doc_id = doc.id;
175        let doc_pos = doc.position;
176
177        self.substrate.add_document(doc);
178
179        // Emit input signal to attract agents
180        self.substrate.emit_signal(Signal::new(
181            SignalType::Input,
182            1.0,
183            doc_pos,
184            AgentId::new(), // System-emitted
185            self.substrate.current_tick(),
186        ));
187
188        doc_id
189    }
190
191    /// Run a single simulation tick.
192    pub fn tick(&mut self) -> Vec<ColonyEvent> {
193        let mut events = Vec::new();
194        let mut actions: Vec<(usize, AgentAction)> = Vec::new();
195
196        // Phase 1: All agents sense and decide
197        for (idx, agent) in self.agents.iter_mut().enumerate() {
198            let action = agent.tick(&self.substrate);
199            actions.push((idx, action));
200        }
201
202        // Phase 2: Process actions
203        let mut to_die = Vec::new();
204        let mut symbiotic_deaths: Vec<(usize, AgentId)> = Vec::new(); // (idx, absorber_id)
205
206        for (idx, action) in actions {
207            match action {
208                AgentAction::Move(pos) => {
209                    self.agents[idx].set_position(pos);
210                    events.push(ColonyEvent::Moved {
211                        id: self.agents[idx].id(),
212                        to: pos,
213                    });
214                }
215
216                AgentAction::EngulfDocument(doc_id) => {
217                    // Try to consume the document from substrate
218                    if let Some(content) = self.substrate.consume_document(&doc_id) {
219                        self.agents[idx].engulf(content);
220                        // Also set the document context via downcast
221                        // (The agent's state machine will handle digestion next tick)
222                        events.push(ColonyEvent::Engulfed {
223                            id: self.agents[idx].id(),
224                            document: doc_id,
225                        });
226                    }
227                }
228
229                AgentAction::PresentFragments(fragments) => {
230                    let agent_id = self.agents[idx].id();
231                    let tick = self.substrate.current_tick();
232                    let mut node_ids = Vec::new();
233
234                    for frag in &fragments {
235                        // Check if this concept already exists in the graph
236                        let existing = self.substrate.graph().find_nodes_by_label(&frag.label);
237                        let node_id = if let Some(&existing_id) = existing.first() {
238                            // Reinforce existing node
239                            if let Some(node) = self.substrate.graph_mut().get_node_mut(&existing_id) {
240                                node.access_count += 1;
241                            }
242                            existing_id
243                        } else {
244                            // Create new node with the type specified by the agent
245                            let node = NodeData {
246                                id: NodeId::new(),
247                                label: frag.label.clone(),
248                                node_type: frag.node_type.clone(),
249                                position: frag.position,
250                                access_count: 1,
251                                created_tick: tick,
252                            };
253                            self.substrate.add_node(node)
254                        };
255                        node_ids.push(node_id);
256                    }
257
258                    // Wire co-occurring concepts (from same document)
259                    // Only wire Concept nodes — Insight/Anomaly nodes don't co-occur
260                    //
261                    // Co-activation gating (Hebbian LTP model):
262                    // - First co-occurrence: create a TENTATIVE edge with low weight (0.1)
263                    // - Subsequent co-occurrences: reinforce to full weight (+0.1 per hit)
264                    // - Only edges reinforced by multiple documents survive synaptic pruning
265                    // This reduces the dense graph problem: single-doc edges are weak
266                    // and decay quickly unless reinforced by cross-document co-occurrence.
267                    let concept_node_ids: Vec<NodeId> = node_ids.iter().filter(|id| {
268                        self.substrate.graph().get_node(id)
269                            .map_or(false, |n| n.node_type == NodeType::Concept)
270                    }).copied().collect();
271                    let mut wire_events = Vec::new();
272                    for i in 0..concept_node_ids.len() {
273                        for j in (i + 1)..concept_node_ids.len() {
274                            let from = concept_node_ids[i];
275                            let to = concept_node_ids[j];
276                            if let Some(edge) = self.substrate.graph_mut().get_edge_mut(&from, &to) {
277                                // Edge already exists: strengthen it (Hebbian reinforcement)
278                                edge.weight = (edge.weight + 0.1).min(1.0);
279                                edge.co_activations += 1;
280                                edge.last_activated_tick = tick;
281                            } else {
282                                // First co-occurrence: create tentative edge with low weight.
283                                // Low initial weight means single-doc edges decay faster
284                                // under synaptic pruning unless reinforced.
285                                self.substrate.set_edge(from, to, EdgeData {
286                                    weight: 0.1,  // Tentative: half the reinforced weight
287                                    co_activations: 1,
288                                    created_tick: tick,
289                                    last_activated_tick: tick,
290                                });
291                            }
292                            wire_events.push((from, to));
293                        }
294                    }
295
296                    events.push(ColonyEvent::Presented {
297                        id: agent_id,
298                        fragment_count: fragments.len(),
299                        node_ids,
300                    });
301
302                    if !wire_events.is_empty() {
303                        events.push(ColonyEvent::Wired {
304                            id: agent_id,
305                            connection_count: wire_events.len(),
306                        });
307                    }
308                }
309
310                AgentAction::Deposit(location, trace) => {
311                    let agent_id = self.agents[idx].id();
312                    self.substrate.deposit_trace(&location, trace);
313                    events.push(ColonyEvent::Deposited {
314                        id: agent_id,
315                        location,
316                    });
317                }
318
319                AgentAction::Emit(signal) => {
320                    self.substrate.emit_signal(signal);
321                }
322
323                AgentAction::WireNodes(connections) => {
324                    let agent_id = self.agents[idx].id();
325                    let tick = self.substrate.current_tick();
326                    for (from, to, weight) in &connections {
327                        if let Some(edge) = self.substrate.graph_mut().get_edge_mut(from, to) {
328                            edge.weight = (edge.weight + weight).min(1.0);
329                            edge.co_activations += 1;
330                            edge.last_activated_tick = tick;
331                        } else {
332                            self.substrate.set_edge(*from, *to, EdgeData {
333                                weight: *weight,
334                                co_activations: 1,
335                                created_tick: tick,
336                                last_activated_tick: tick,
337                            });
338                        }
339                    }
340                    events.push(ColonyEvent::Wired {
341                        id: agent_id,
342                        connection_count: connections.len(),
343                    });
344                }
345
346                AgentAction::ExportCapability(_cap_id) => {
347                    let agent_id = self.agents[idx].id();
348                    let agent_pos = self.agents[idx].position();
349                    if let Some(vocab_bytes) = self.agents[idx].export_vocabulary() {
350                        // Count terms for event
351                        let terms_count = serde_json::from_slice::<VocabularyCapability>(&vocab_bytes)
352                            .map(|v| v.terms.len())
353                            .unwrap_or(0);
354
355                        // Deposit as CapabilityDeposit trace at agent position
356                        let trace = Trace {
357                            agent_id,
358                            trace_type: TraceType::CapabilityDeposit,
359                            intensity: 1.0,
360                            tick: self.substrate.current_tick(),
361                            payload: vocab_bytes,
362                        };
363                        self.substrate.deposit_trace(
364                            &SubstrateLocation::Spatial(agent_pos),
365                            trace,
366                        );
367
368                        // Emit Capability signal to attract other agents
369                        self.substrate.emit_signal(Signal::new(
370                            SignalType::Capability,
371                            0.8,
372                            agent_pos,
373                            agent_id,
374                            self.substrate.current_tick(),
375                        ));
376
377                        events.push(ColonyEvent::CapabilityExported {
378                            agent_id,
379                            terms_count,
380                        });
381                    }
382                }
383
384                AgentAction::SymbioseWith(target_id) => {
385                    let host_idx = idx;
386                    let host_id = self.agents[host_idx].id();
387
388                    // Find target agent
389                    if let Some(target_idx) = self.agents.iter().position(|a| a.id() == target_id) {
390                        // Build target's profile and extract vocabulary
391                        let target_profile = self.agents[target_idx].profile();
392                        let target_vocab = self.agents[target_idx].export_vocabulary()
393                            .unwrap_or_default();
394
395                        // Evaluate symbiosis
396                        if let Some(SymbiosisEval::Integrate) =
397                            self.agents[host_idx].evaluate_symbiosis(&target_profile)
398                        {
399                            let host_type = self.agents[host_idx].agent_type().to_string();
400                            let absorbed_type = self.agents[target_idx].agent_type().to_string();
401
402                            // Host absorbs the symbiont
403                            self.agents[host_idx].absorb_symbiont(target_profile, target_vocab);
404
405                            // Mark target for removal via symbiotic absorption
406                            symbiotic_deaths.push((target_idx, host_id));
407
408                            events.push(ColonyEvent::Symbiosis {
409                                host: host_id,
410                                absorbed: target_id,
411                                host_type,
412                                absorbed_type,
413                            });
414                        }
415                    }
416                }
417
418                AgentAction::Apoptose => {
419                    to_die.push(idx);
420                }
421
422                AgentAction::Idle => {}
423
424                _ => {}
425            }
426        }
427
428        // Phase 2.5: Dissolution + Capability Integration
429        // For each agent: compute BoundaryContext, modulate boundary,
430        // externalize/internalize vocabulary, integrate nearby capabilities
431        {
432            let _tick = self.substrate.current_tick();
433            let agent_count = self.agents.len();
434
435            for i in 0..agent_count {
436                let agent_id = self.agents[i].id();
437                let agent_pos = self.agents[i].position();
438                let agent_age = self.agents[i].age();
439
440                // Compute BoundaryContext — cache externalized vocab for reuse
441                let vocab_terms = self.agents[i].externalize_vocabulary();
442                let mut reinforcement_count = 0u64;
443                let graph = self.substrate.graph();
444                for term in &vocab_terms {
445                    let matching = graph.find_nodes_by_exact_label(term);
446                    for nid in matching {
447                        if let Some(node) = graph.get_node(nid) {
448                            reinforcement_count += node.access_count;
449                        }
450                    }
451                }
452
453                let useful_outputs_estimate = reinforcement_count.min(100);
454                let trust = if agent_age > 0 {
455                    (useful_outputs_estimate as f64 / agent_age as f64).min(1.0)
456                } else {
457                    0.0
458                };
459
460                let context = BoundaryContext {
461                    reinforcement_count,
462                    age: agent_age,
463                    trust,
464                };
465
466                self.agents[i].modulate_boundary(&context);
467                let permeability = self.agents[i].permeability();
468
469                // High permeability: boost matching graph nodes' access_count
470                if permeability > 0.5 {
471                    // Reuse cached vocab_terms instead of calling externalize_vocabulary again
472                    let mut terms_externalized = 0usize;
473                    for term in &vocab_terms {
474                        let matching: Vec<NodeId> = self.substrate.graph().find_nodes_by_exact_label(term).to_vec();
475                        for nid in &matching {
476                            if let Some(node) = self.substrate.graph_mut().get_node_mut(nid) {
477                                node.access_count += 1;
478                                terms_externalized += 1;
479                            }
480                        }
481                    }
482                    if terms_externalized > 0 {
483                        events.push(ColonyEvent::Dissolved {
484                            agent_id,
485                            permeability,
486                            terms_externalized,
487                        });
488                    }
489                }
490
491                // Any permeability > 0: internalize nearby concept labels
492                if permeability > 0.0 {
493                    let all_nodes = self.substrate.graph().all_nodes();
494                    let nearby_labels: Vec<String> = all_nodes.iter()
495                        .filter_map(|nid| {
496                            let node = self.substrate.graph().get_node(nid)?;
497                            if node.position.distance_to(&agent_pos) <= 15.0
498                                && node.node_type == NodeType::Concept
499                            {
500                                Some(node.label.clone())
501                            } else {
502                                None
503                            }
504                        })
505                        .collect();
506                    if !nearby_labels.is_empty() {
507                        self.agents[i].internalize_vocabulary(&nearby_labels);
508                    }
509                }
510
511                // Capability integration: check for CapabilityDeposit traces near agent
512                let traces = self.substrate.traces_near(
513                    &agent_pos,
514                    10.0,
515                    &TraceType::CapabilityDeposit,
516                );
517                for trace in &traces {
518                    if trace.agent_id != agent_id
519                        && !trace.payload.is_empty()
520                    {
521                        let payload = trace.payload.clone();
522                        let from_agent = trace.agent_id;
523                        let terms_count = serde_json::from_slice::<VocabularyCapability>(&payload)
524                            .map(|v| v.terms.len())
525                            .unwrap_or(0);
526                        if self.agents[i].integrate_vocabulary(&payload) {
527                            events.push(ColonyEvent::CapabilityIntegrated {
528                                agent_id,
529                                from_agent,
530                                terms_count,
531                            });
532                        }
533                    }
534                }
535            }
536        }
537
538        // Add symbiotic deaths to the death list
539        for (idx, _absorber_id) in &symbiotic_deaths {
540            if !to_die.contains(idx) {
541                to_die.push(*idx);
542            }
543        }
544
545        // Phase 3: Remove dead agents
546        to_die.sort();
547        to_die.dedup();
548        let dead_count = to_die.len();
549        for idx in to_die.into_iter().rev() {
550            let agent = self.agents.remove(idx);
551            let mut death_signal = agent.prepare_death_signal();
552
553            // Override cause if this was a symbiotic absorption
554            if let Some((_, absorber_id)) = symbiotic_deaths.iter().find(|(i, _)| *i == idx) {
555                death_signal.cause = DeathCause::SymbioticAbsorption(*absorber_id);
556            }
557
558            events.push(ColonyEvent::Died {
559                signal: death_signal.clone(),
560            });
561            self.death_signals.push(death_signal);
562            self.total_died += 1;
563        }
564
565        // Phase 4: Substrate decay
566        self.substrate
567            .decay_signals(self.signal_decay_rate, self.signal_removal_threshold);
568        self.substrate
569            .decay_traces(self.trace_decay_rate, self.trace_removal_threshold);
570        // Synaptic pruning: activity-based decay with maturation protection
571        let current_tick = self.substrate.current_tick();
572        self.substrate.graph_mut().decay_edges_activity(
573            self.edge_decay_rate,
574            self.edge_prune_threshold,
575            current_tick,
576            self.staleness_factor,
577            self.maturation_ticks,
578        );
579        // Competitive pruning: cap per-node degree
580        self.substrate
581            .graph_mut()
582            .prune_to_max_degree(self.max_edge_degree);
583
584        // Phase 4b: Fitness tracking — wire colony events to the tracker
585        for event in &events {
586            match event {
587                ColonyEvent::Presented { id, fragment_count, .. } => {
588                    self.fitness_tracker.record_concepts(id, *fragment_count as u64);
589                }
590                ColonyEvent::Wired { id, connection_count } => {
591                    self.fitness_tracker.record_edges(id, *connection_count as u64);
592                }
593                _ => {}
594            }
595        }
596        let alive_ids: Vec<AgentId> = self.agents.iter().map(|a| a.id()).collect();
597        self.fitness_tracker.tick_all(&alive_ids);
598
599        // Phase 5: Advance tick
600        self.substrate.advance_tick();
601
602        events.push(ColonyEvent::TickComplete {
603            tick: self.substrate.current_tick(),
604            alive: self.agents.len(),
605            dead_this_tick: dead_count,
606        });
607
608        // Record events in history
609        let current_tick = self.substrate.current_tick();
610        for event in &events {
611            self.event_history.push((current_tick, event.clone()));
612        }
613
614        events
615    }
616
617    /// Run the simulation for N ticks.
618    pub fn run(&mut self, ticks: u64) -> Vec<Vec<ColonyEvent>> {
619        let mut all_events = Vec::new();
620        for _ in 0..ticks {
621            all_events.push(self.tick());
622        }
623        all_events
624    }
625
626    /// Get colony statistics.
627    pub fn stats(&self) -> ColonyStats {
628        let docs = self.substrate.all_documents();
629        let digested = docs.iter().filter(|d| d.digested).count();
630        ColonyStats {
631            tick: self.substrate.current_tick(),
632            agents_alive: self.agents.len(),
633            agents_died: self.total_died,
634            total_spawned: self.total_spawned,
635            graph_nodes: self.substrate.node_count(),
636            graph_edges: self.substrate.edge_count(),
637            total_signals: self.substrate.all_signals().len(),
638            documents_total: docs.len(),
639            documents_digested: digested,
640        }
641    }
642
643    /// Get a reference to the substrate.
644    pub fn substrate(&self) -> &SubstrateImpl {
645        &self.substrate
646    }
647
648    /// Get a mutable reference to the substrate.
649    pub fn substrate_mut(&mut self) -> &mut SubstrateImpl {
650        &mut self.substrate
651    }
652
653    /// Number of agents currently alive.
654    pub fn alive_count(&self) -> usize {
655        self.agents.len()
656    }
657
658    /// All death signals collected during the simulation.
659    pub fn death_signals(&self) -> &[DeathSignal] {
660        &self.death_signals
661    }
662
663    /// Feed text input to a specific agent by index.
664    pub fn feed_agent(&mut self, agent_idx: usize, input: String) -> Option<DigestionResult> {
665        self.agents
666            .get_mut(agent_idx)
667            .map(|agent| agent.engulf(input))
668    }
669
670    /// Take a serializable snapshot of the colony's current state.
671    pub fn snapshot(&self) -> ColonySnapshot {
672        let graph = self.substrate.graph();
673
674        let agents: Vec<AgentSnapshot> = self.agents.iter().map(|a| {
675            AgentSnapshot {
676                id: a.id(),
677                agent_type: a.agent_type().to_string(),
678                position: a.position(),
679                age: a.age(),
680                permeability: a.permeability(),
681                vocabulary_size: a.vocabulary_size(),
682            }
683        }).collect();
684
685        let nodes: Vec<NodeSnapshot> = graph.all_nodes().iter().filter_map(|nid| {
686            let n = graph.get_node(nid)?;
687            Some(NodeSnapshot {
688                id: n.id,
689                label: n.label.clone(),
690                node_type: n.node_type.clone(),
691                position: n.position,
692                access_count: n.access_count,
693            })
694        }).collect();
695
696        let edges: Vec<EdgeSnapshot> = graph.all_edges().iter().map(|(from, to, data)| {
697            let from_label = graph.get_node(from).map(|n| n.label.clone()).unwrap_or_default();
698            let to_label = graph.get_node(to).map(|n| n.label.clone()).unwrap_or_default();
699            EdgeSnapshot {
700                from_label,
701                to_label,
702                weight: data.weight,
703                co_activations: data.co_activations,
704            }
705        }).collect();
706
707        ColonySnapshot {
708            tick: self.substrate.current_tick(),
709            agents,
710            nodes,
711            edges,
712            stats: self.stats(),
713        }
714    }
715
716    /// Get the full event history with tick numbers.
717    pub fn event_history(&self) -> &[(Tick, ColonyEvent)] {
718        &self.event_history
719    }
720
721    /// Get a reference to the agents.
722    pub fn agents(&self) -> &[Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>] {
723        &self.agents
724    }
725
726    /// Get a reference to the fitness tracker.
727    pub fn fitness_tracker(&self) -> &FitnessTracker {
728        &self.fitness_tracker
729    }
730
731    /// Get a mutable reference to the fitness tracker.
732    pub fn fitness_tracker_mut(&mut self) -> &mut FitnessTracker {
733        &mut self.fitness_tracker
734    }
735
736    /// Emit an input signal at a position (to attract agents).
737    pub fn emit_input_signal(&mut self, position: Position, intensity: f64) {
738        let signal = Signal::new(
739            SignalType::Input,
740            intensity,
741            position,
742            AgentId::new(),
743            self.substrate.current_tick(),
744        );
745        self.substrate.emit_signal(signal);
746    }
747}
748
749impl Default for Colony {
750    fn default() -> Self {
751        Self::new()
752    }
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758    use phago_agents::digester::Digester;
759
760    #[test]
761    fn spawn_and_count_agents() {
762        let mut colony = Colony::new();
763        colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0))));
764        colony.spawn(Box::new(Digester::new(Position::new(5.0, 5.0))));
765        assert_eq!(colony.alive_count(), 2);
766        assert_eq!(colony.stats().total_spawned, 2);
767    }
768
769    #[test]
770    fn tick_advances_simulation() {
771        let mut colony = Colony::new();
772        colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0))));
773        colony.tick();
774        assert_eq!(colony.stats().tick, 1);
775    }
776
777    #[test]
778    fn agent_apoptosis_in_colony() {
779        let mut colony = Colony::new();
780        colony.spawn(Box::new(
781            Digester::new(Position::new(0.0, 0.0)).with_max_idle(3),
782        ));
783
784        assert_eq!(colony.alive_count(), 1);
785
786        for _ in 0..5 {
787            colony.tick();
788        }
789
790        assert_eq!(colony.alive_count(), 0);
791        assert_eq!(colony.stats().agents_died, 1);
792        assert_eq!(colony.death_signals().len(), 1);
793    }
794
795    #[test]
796    fn ingest_document_creates_signal() {
797        let mut colony = Colony::new();
798        let pos = Position::new(5.0, 5.0);
799        let doc_id = colony.ingest_document("Test Doc", "cell membrane protein", pos);
800
801        let stats = colony.stats();
802        assert_eq!(stats.documents_total, 1);
803        assert_eq!(stats.documents_digested, 0);
804        assert_eq!(stats.total_signals, 1); // Input signal emitted
805
806        // Document is in the substrate
807        let doc = colony.substrate().get_document(&doc_id);
808        assert!(doc.is_some());
809        assert!(!doc.unwrap().digested);
810    }
811
812    #[test]
813    fn agent_finds_and_digests_document() {
814        let mut colony = Colony::new();
815
816        // Place document at origin
817        colony.ingest_document(
818            "Biology 101",
819            "The cell membrane controls transport of molecules into and out of the cell. \
820             Proteins embedded in the membrane serve as channels and receptors.",
821            Position::new(0.0, 0.0),
822        );
823
824        // Spawn agent at origin (right on top of the document)
825        colony.spawn(Box::new(
826            Digester::new(Position::new(0.0, 0.0)).with_max_idle(50),
827        ));
828
829        // Run enough ticks for the full cycle:
830        // tick 1: Seeking → finds doc → EngulfDocument
831        // tick 2: FoundTarget → engulfed → Digesting
832        // tick 3: Digesting → lyse → PresentFragments
833        // tick 4: Presenting → Deposit trace
834        colony.run(10);
835
836        let stats = colony.stats();
837        assert_eq!(stats.documents_digested, 1, "Document should be digested");
838        assert!(stats.graph_nodes > 0, "Should have concept nodes: got {}", stats.graph_nodes);
839        assert!(stats.graph_edges > 0, "Should have edges: got {}", stats.graph_edges);
840    }
841
842    #[test]
843    fn multiple_documents_build_graph() {
844        let mut colony = Colony::new();
845
846        // Two documents about related topics
847        colony.ingest_document(
848            "Cell Biology",
849            "The cell membrane is a lipid bilayer that controls transport. \
850             Proteins in the membrane act as channels and receptors for signaling.",
851            Position::new(0.0, 0.0),
852        );
853        colony.ingest_document(
854            "Molecular Transport",
855            "Active transport across the cell membrane requires ATP energy. \
856             Channel proteins facilitate passive transport of ions and molecules.",
857            Position::new(2.0, 0.0),
858        );
859
860        // Spawn agents near each document
861        colony.spawn(Box::new(
862            Digester::new(Position::new(0.0, 0.0)).with_max_idle(50),
863        ));
864        colony.spawn(Box::new(
865            Digester::new(Position::new(2.0, 0.0)).with_max_idle(50),
866        ));
867
868        colony.run(20);
869
870        let stats = colony.stats();
871        assert_eq!(stats.documents_digested, 2, "Both documents should be digested");
872        // Shared concepts (cell, membrane, transport, proteins) should create
873        // overlapping graph nodes and strengthen edges
874        assert!(stats.graph_nodes >= 5, "Expected at least 5 concept nodes, got {}", stats.graph_nodes);
875    }
876
877    #[test]
878    fn colony_stats_are_accurate() {
879        let mut colony = Colony::new();
880        colony.spawn(Box::new(
881            Digester::new(Position::new(0.0, 0.0)).with_max_idle(2),
882        ));
883        colony.spawn(Box::new(
884            Digester::new(Position::new(5.0, 5.0)).with_max_idle(100),
885        ));
886
887        colony.run(5);
888
889        let stats = colony.stats();
890        assert_eq!(stats.total_spawned, 2);
891        assert_eq!(stats.agents_died, 1);
892        assert_eq!(stats.agents_alive, 1);
893    }
894}