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::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/// Event emitted by the colony during simulation.
25#[derive(Debug, Clone, Serialize)]
26pub enum ColonyEvent {
27    /// An agent was spawned.
28    Spawned { id: AgentId, agent_type: String },
29    /// An agent moved to a new position.
30    Moved { id: AgentId, to: Position },
31    /// An agent engulfed a document.
32    Engulfed { id: AgentId, document: DocumentId },
33    /// An agent presented fragments to the knowledge graph.
34    Presented { id: AgentId, fragment_count: usize, node_ids: Vec<NodeId> },
35    /// An agent deposited a trace.
36    Deposited { id: AgentId, location: SubstrateLocation },
37    /// An agent wired connections in the graph.
38    Wired { id: AgentId, connection_count: usize },
39    /// An agent triggered apoptosis.
40    Died { signal: DeathSignal },
41    /// A tick completed.
42    TickComplete { tick: Tick, alive: usize, dead_this_tick: usize },
43    /// An agent exported its vocabulary as a capability deposit.
44    CapabilityExported { agent_id: AgentId, terms_count: usize },
45    /// An agent integrated vocabulary from a capability deposit.
46    CapabilityIntegrated { agent_id: AgentId, from_agent: AgentId, terms_count: usize },
47    /// An agent absorbed another through symbiosis.
48    Symbiosis { host: AgentId, absorbed: AgentId, host_type: String, absorbed_type: String },
49    /// An agent's boundary dissolved, externalizing vocabulary.
50    Dissolved { agent_id: AgentId, permeability: f64, terms_externalized: usize },
51}
52
53/// Statistics about the colony.
54#[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/// A serializable snapshot of an agent's state.
68#[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/// A serializable snapshot of a graph node.
79#[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/// A serializable snapshot of a graph edge.
89#[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/// A complete serializable snapshot of the colony at a point in time.
98#[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/// Configuration for colony simulation parameters.
108///
109/// This struct contains all the tunable parameters that were previously
110/// hardcoded in Colony::new(). Use with Colony::from_config() to create
111/// a colony with custom settings.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ColonyConfig {
114    /// Rate at which signals decay per tick (default: 0.05).
115    pub signal_decay_rate: f64,
116    /// Threshold below which signals are removed (default: 0.01).
117    pub signal_removal_threshold: f64,
118    /// Rate at which traces decay per tick (default: 0.02).
119    pub trace_decay_rate: f64,
120    /// Threshold below which traces are removed (default: 0.01).
121    pub trace_removal_threshold: f64,
122    /// Rate at which edges decay per tick (default: 0.005).
123    pub edge_decay_rate: f64,
124    /// Threshold below which edges are pruned (default: 0.05).
125    pub edge_prune_threshold: f64,
126    /// Factor for staleness-based decay (default: 1.5).
127    pub staleness_factor: f64,
128    /// Number of ticks before edges mature and become decay-resistant (default: 50).
129    pub maturation_ticks: u64,
130    /// Maximum number of edges per node before pruning (default: 30).
131    pub max_edge_degree: usize,
132    /// Semantic wiring configuration.
133    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
153/// The colony — manages agent lifecycle and simulation.
154pub 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    // Configuration
164    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    /// Create a new colony with default configuration.
178    pub fn new() -> Self {
179        Self::from_config(ColonyConfig::default())
180    }
181
182    /// Create a new colony with the specified configuration.
183    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    /// Get the current configuration.
206    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    /// Configure semantic wiring for embedding-based edge weights.
222    pub fn with_semantic_wiring(mut self, config: SemanticWiringConfig) -> Self {
223        self.semantic_wiring = config;
224        self
225    }
226
227    /// Get the current semantic wiring configuration.
228    pub fn semantic_wiring_config(&self) -> &SemanticWiringConfig {
229        &self.semantic_wiring
230    }
231
232    /// Set the semantic wiring configuration.
233    pub fn set_semantic_wiring(&mut self, config: SemanticWiringConfig) {
234        self.semantic_wiring = config;
235    }
236
237    /// Spawn an agent into the colony.
238    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    /// Ingest a document into the substrate.
250    ///
251    /// Places the document at the given position and emits an Input signal
252    /// to attract nearby agents via chemotaxis.
253    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        // Emit input signal to attract agents
267        self.substrate.emit_signal(Signal::new(
268            SignalType::Input,
269            1.0,
270            doc_pos,
271            AgentId::new(), // System-emitted
272            self.substrate.current_tick(),
273        ));
274
275        doc_id
276    }
277
278    /// Run a single simulation tick.
279    pub fn tick(&mut self) -> Vec<ColonyEvent> {
280        let mut events = Vec::new();
281        let mut actions: Vec<(usize, AgentAction)> = Vec::new();
282
283        // Phase 1: All agents sense and decide
284        for (idx, agent) in self.agents.iter_mut().enumerate() {
285            let action = agent.tick(&self.substrate);
286            actions.push((idx, action));
287        }
288
289        // Phase 2: Process actions
290        let mut to_die = Vec::new();
291        let mut symbiotic_deaths: Vec<(usize, AgentId)> = Vec::new(); // (idx, absorber_id)
292
293        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                    // Try to consume the document from substrate
305                    if let Some(content) = self.substrate.consume_document(&doc_id) {
306                        self.agents[idx].engulf(content);
307                        // Also set the document context via downcast
308                        // (The agent's state machine will handle digestion next tick)
309                        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                        // Check if this concept already exists in the graph
323                        let existing = self.substrate.graph().find_nodes_by_label(&frag.label);
324                        let node_id = if let Some(&existing_id) = existing.first() {
325                            // Reinforce existing node
326                            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                            // Create new node with the type specified by the agent
332                            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                    // Wire co-occurring concepts (from same document)
347                    // Only wire Concept nodes — Insight/Anomaly nodes don't co-occur
348                    //
349                    // Co-activation gating (Hebbian LTP model):
350                    // - First co-occurrence: create a TENTATIVE edge with low weight (0.1)
351                    // - Subsequent co-occurrences: reinforce to full weight (+0.1 per hit)
352                    // - Only edges reinforced by multiple documents survive synaptic pruning
353                    // This reduces the dense graph problem: single-doc edges are weak
354                    // and decay quickly unless reinforced by cross-document co-occurrence.
355                    //
356                    // Semantic wiring (Phase 9.3):
357                    // - If nodes have embeddings, modulate edge weight by similarity
358                    // - weight = base_weight * (1 + similarity_influence * similarity)
359                    // - Below min_similarity threshold: skip or use base weight
360                    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                            // Get embeddings for semantic wiring (clone to avoid borrow issues)
371                            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                            // Compute semantic weight before mutating graph
377                            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                                // Edge already exists: strengthen it (Hebbian reinforcement)
387                                // Use semantic similarity to modulate reinforcement
388                                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                                // First co-occurrence: create tentative edge with low weight.
395                                // Use semantic similarity to compute initial weight.
396                                let weight = semantic_weight;
397
398                                // Only create edge if semantic check passes
399                                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                        // Get embeddings for semantic wiring (clone to avoid borrow issues)
445                        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                        // Compute semantic weight before mutating graph
451                        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                        // Count terms for event
487                        let terms_count = serde_json::from_slice::<VocabularyCapability>(&vocab_bytes)
488                            .map(|v| v.terms.len())
489                            .unwrap_or(0);
490
491                        // Deposit as CapabilityDeposit trace at agent position
492                        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                        // Emit Capability signal to attract other agents
505                        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                    // Find target agent
525                    if let Some(target_idx) = self.agents.iter().position(|a| a.id() == target_id) {
526                        // Build target's profile and extract vocabulary
527                        let target_profile = self.agents[target_idx].profile();
528                        let target_vocab = self.agents[target_idx].export_vocabulary()
529                            .unwrap_or_default();
530
531                        // Evaluate symbiosis
532                        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                            // Host absorbs the symbiont
539                            self.agents[host_idx].absorb_symbiont(target_profile, target_vocab);
540
541                            // Mark target for removal via symbiotic absorption
542                            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        // Phase 2.5: Dissolution + Capability Integration
565        // For each agent: compute BoundaryContext, modulate boundary,
566        // externalize/internalize vocabulary, integrate nearby capabilities
567        {
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                // Compute BoundaryContext — cache externalized vocab for reuse
577                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                // High permeability: boost matching graph nodes' access_count
606                if permeability > 0.5 {
607                    // Reuse cached vocab_terms instead of calling externalize_vocabulary again
608                    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                // Any permeability > 0: internalize nearby concept labels
628                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                // Capability integration: check for CapabilityDeposit traces near agent
648                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        // Add symbiotic deaths to the death list
675        for (idx, _absorber_id) in &symbiotic_deaths {
676            if !to_die.contains(idx) {
677                to_die.push(*idx);
678            }
679        }
680
681        // Phase 3: Remove dead agents
682        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            // Override cause if this was a symbiotic absorption
690            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        // Phase 4: Substrate decay
702        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        // Synaptic pruning: activity-based decay with maturation protection
707        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        // Competitive pruning: cap per-node degree
716        self.substrate
717            .graph_mut()
718            .prune_to_max_degree(self.max_edge_degree);
719
720        // Phase 4b: Fitness tracking — wire colony events to the tracker
721        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        // Phase 5: Advance tick
736        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        // Record events in history
745        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    /// Run the simulation for N ticks.
754    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    /// Get colony statistics.
763    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    /// Get a reference to the substrate.
780    pub fn substrate(&self) -> &SubstrateImpl {
781        &self.substrate
782    }
783
784    /// Get a mutable reference to the substrate.
785    pub fn substrate_mut(&mut self) -> &mut SubstrateImpl {
786        &mut self.substrate
787    }
788
789    /// Number of agents currently alive.
790    pub fn alive_count(&self) -> usize {
791        self.agents.len()
792    }
793
794    /// All death signals collected during the simulation.
795    pub fn death_signals(&self) -> &[DeathSignal] {
796        &self.death_signals
797    }
798
799    /// Feed text input to a specific agent by index.
800    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    /// Take a serializable snapshot of the colony's current state.
807    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    /// Get the full event history with tick numbers.
853    pub fn event_history(&self) -> &[(Tick, ColonyEvent)] {
854        &self.event_history
855    }
856
857    /// Get a reference to the agents.
858    pub fn agents(&self) -> &[Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>] {
859        &self.agents
860    }
861
862    /// Get a reference to the fitness tracker.
863    pub fn fitness_tracker(&self) -> &FitnessTracker {
864        &self.fitness_tracker
865    }
866
867    /// Get a mutable reference to the fitness tracker.
868    pub fn fitness_tracker_mut(&mut self) -> &mut FitnessTracker {
869        &mut self.fitness_tracker
870    }
871
872    /// Emit an input signal at a position (to attract agents).
873    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); // Input signal emitted
941
942        // Document is in the substrate
943        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        // Place document at origin
953        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        // Spawn agent at origin (right on top of the document)
961        colony.spawn(Box::new(
962            Digester::new(Position::new(0.0, 0.0)).with_max_idle(50),
963        ));
964
965        // Run enough ticks for the full cycle:
966        // tick 1: Seeking → finds doc → EngulfDocument
967        // tick 2: FoundTarget → engulfed → Digesting
968        // tick 3: Digesting → lyse → PresentFragments
969        // tick 4: Presenting → Deposit trace
970        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        // Two documents about related topics
983        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        // Spawn agents near each document
997        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        // Shared concepts (cell, membrane, transport, proteins) should create
1009        // overlapping graph nodes and strengthen edges
1010        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        // Default config should not require embeddings
1036        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        // Manually add two nodes with similar embeddings
1061        let emb_a = vec![1.0, 0.0, 0.0];  // Unit vector along x
1062        let emb_b = vec![0.95, 0.31, 0.0]; // ~18° from emb_a (high similarity)
1063
1064        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        // Wire them manually using WireNodes action
1085        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        // Add two nodes WITHOUT embeddings
1104        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        // Wire them
1125        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        // With default config, no embeddings means base weight is used
1134        assert!((edge.weight - 0.1).abs() < 0.01, "Edge should use base weight: got {}", edge.weight);
1135    }
1136}