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::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/// The colony — manages agent lifecycle and simulation.
108pub struct Colony {
109    substrate: SubstrateImpl,
110    agents: Vec<Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>>,
111    death_signals: Vec<DeathSignal>,
112    event_history: Vec<(Tick, ColonyEvent)>,
113    total_spawned: usize,
114    total_died: usize,
115    fitness_tracker: FitnessTracker,
116
117    // Configuration
118    signal_decay_rate: f64,
119    signal_removal_threshold: f64,
120    trace_decay_rate: f64,
121    trace_removal_threshold: f64,
122    edge_decay_rate: f64,
123    edge_prune_threshold: f64,
124    staleness_factor: f64,
125    maturation_ticks: u64,
126    max_edge_degree: usize,
127    semantic_wiring: SemanticWiringConfig,
128}
129
130impl Colony {
131    pub fn new() -> Self {
132        Self {
133            substrate: SubstrateImpl::new(),
134            agents: Vec::new(),
135            death_signals: Vec::new(),
136            event_history: Vec::new(),
137            total_spawned: 0,
138            total_died: 0,
139            fitness_tracker: FitnessTracker::new(),
140            signal_decay_rate: 0.05,
141            signal_removal_threshold: 0.01,
142            trace_decay_rate: 0.02,
143            trace_removal_threshold: 0.01,
144            edge_decay_rate: 0.005,
145            edge_prune_threshold: 0.05,
146            staleness_factor: 1.5,
147            maturation_ticks: 50,
148            max_edge_degree: 30,
149            semantic_wiring: SemanticWiringConfig::default(),
150        }
151    }
152
153    /// Configure semantic wiring for embedding-based edge weights.
154    pub fn with_semantic_wiring(mut self, config: SemanticWiringConfig) -> Self {
155        self.semantic_wiring = config;
156        self
157    }
158
159    /// Get the current semantic wiring configuration.
160    pub fn semantic_wiring_config(&self) -> &SemanticWiringConfig {
161        &self.semantic_wiring
162    }
163
164    /// Set the semantic wiring configuration.
165    pub fn set_semantic_wiring(&mut self, config: SemanticWiringConfig) {
166        self.semantic_wiring = config;
167    }
168
169    /// Spawn an agent into the colony.
170    pub fn spawn(
171        &mut self,
172        agent: Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>,
173    ) -> AgentId {
174        let id = agent.id();
175        self.total_spawned += 1;
176        self.fitness_tracker.register(id, 0);
177        self.agents.push(agent);
178        id
179    }
180
181    /// Ingest a document into the substrate.
182    ///
183    /// Places the document at the given position and emits an Input signal
184    /// to attract nearby agents via chemotaxis.
185    pub fn ingest_document(&mut self, title: &str, content: &str, position: Position) -> DocumentId {
186        let doc = Document {
187            id: DocumentId::new(),
188            title: title.to_string(),
189            content: content.to_string(),
190            position,
191            digested: false,
192        };
193        let doc_id = doc.id;
194        let doc_pos = doc.position;
195
196        self.substrate.add_document(doc);
197
198        // Emit input signal to attract agents
199        self.substrate.emit_signal(Signal::new(
200            SignalType::Input,
201            1.0,
202            doc_pos,
203            AgentId::new(), // System-emitted
204            self.substrate.current_tick(),
205        ));
206
207        doc_id
208    }
209
210    /// Run a single simulation tick.
211    pub fn tick(&mut self) -> Vec<ColonyEvent> {
212        let mut events = Vec::new();
213        let mut actions: Vec<(usize, AgentAction)> = Vec::new();
214
215        // Phase 1: All agents sense and decide
216        for (idx, agent) in self.agents.iter_mut().enumerate() {
217            let action = agent.tick(&self.substrate);
218            actions.push((idx, action));
219        }
220
221        // Phase 2: Process actions
222        let mut to_die = Vec::new();
223        let mut symbiotic_deaths: Vec<(usize, AgentId)> = Vec::new(); // (idx, absorber_id)
224
225        for (idx, action) in actions {
226            match action {
227                AgentAction::Move(pos) => {
228                    self.agents[idx].set_position(pos);
229                    events.push(ColonyEvent::Moved {
230                        id: self.agents[idx].id(),
231                        to: pos,
232                    });
233                }
234
235                AgentAction::EngulfDocument(doc_id) => {
236                    // Try to consume the document from substrate
237                    if let Some(content) = self.substrate.consume_document(&doc_id) {
238                        self.agents[idx].engulf(content);
239                        // Also set the document context via downcast
240                        // (The agent's state machine will handle digestion next tick)
241                        events.push(ColonyEvent::Engulfed {
242                            id: self.agents[idx].id(),
243                            document: doc_id,
244                        });
245                    }
246                }
247
248                AgentAction::PresentFragments(fragments) => {
249                    let agent_id = self.agents[idx].id();
250                    let tick = self.substrate.current_tick();
251                    let mut node_ids = Vec::new();
252
253                    for frag in &fragments {
254                        // Check if this concept already exists in the graph
255                        let existing = self.substrate.graph().find_nodes_by_label(&frag.label);
256                        let node_id = if let Some(&existing_id) = existing.first() {
257                            // Reinforce existing node
258                            if let Some(node) = self.substrate.graph_mut().get_node_mut(&existing_id) {
259                                node.access_count += 1;
260                            }
261                            existing_id
262                        } else {
263                            // Create new node with the type specified by the agent
264                            let node = NodeData {
265                                id: NodeId::new(),
266                                label: frag.label.clone(),
267                                node_type: frag.node_type.clone(),
268                                position: frag.position,
269                                access_count: 1,
270                                created_tick: tick,
271                                embedding: None,
272                            };
273                            self.substrate.add_node(node)
274                        };
275                        node_ids.push(node_id);
276                    }
277
278                    // Wire co-occurring concepts (from same document)
279                    // Only wire Concept nodes — Insight/Anomaly nodes don't co-occur
280                    //
281                    // Co-activation gating (Hebbian LTP model):
282                    // - First co-occurrence: create a TENTATIVE edge with low weight (0.1)
283                    // - Subsequent co-occurrences: reinforce to full weight (+0.1 per hit)
284                    // - Only edges reinforced by multiple documents survive synaptic pruning
285                    // This reduces the dense graph problem: single-doc edges are weak
286                    // and decay quickly unless reinforced by cross-document co-occurrence.
287                    //
288                    // Semantic wiring (Phase 9.3):
289                    // - If nodes have embeddings, modulate edge weight by similarity
290                    // - weight = base_weight * (1 + similarity_influence * similarity)
291                    // - Below min_similarity threshold: skip or use base weight
292                    let concept_node_ids: Vec<NodeId> = node_ids.iter().filter(|id| {
293                        self.substrate.graph().get_node(id)
294                            .map_or(false, |n| n.node_type == NodeType::Concept)
295                    }).copied().collect();
296                    let mut wire_events = Vec::new();
297                    for i in 0..concept_node_ids.len() {
298                        for j in (i + 1)..concept_node_ids.len() {
299                            let from = concept_node_ids[i];
300                            let to = concept_node_ids[j];
301
302                            // Get embeddings for semantic wiring (clone to avoid borrow issues)
303                            let embedding_from = self.substrate.graph().get_node(&from)
304                                .and_then(|n| n.embedding.clone());
305                            let embedding_to = self.substrate.graph().get_node(&to)
306                                .and_then(|n| n.embedding.clone());
307
308                            // Compute semantic weight before mutating graph
309                            let base_weight = 0.1;
310                            let semantic_weight = compute_semantic_weight(
311                                base_weight,
312                                embedding_from.as_deref(),
313                                embedding_to.as_deref(),
314                                &self.semantic_wiring,
315                            );
316
317                            if let Some(edge) = self.substrate.graph_mut().get_edge_mut(&from, &to) {
318                                // Edge already exists: strengthen it (Hebbian reinforcement)
319                                // Use semantic similarity to modulate reinforcement
320                                let reinforcement = semantic_weight.unwrap_or(base_weight);
321                                edge.weight = (edge.weight + reinforcement).min(1.0);
322                                edge.co_activations += 1;
323                                edge.last_activated_tick = tick;
324                                wire_events.push((from, to));
325                            } else {
326                                // First co-occurrence: create tentative edge with low weight.
327                                // Use semantic similarity to compute initial weight.
328                                let weight = semantic_weight;
329
330                                // Only create edge if semantic check passes
331                                if let Some(w) = weight {
332                                    self.substrate.set_edge(from, to, EdgeData {
333                                        weight: w,
334                                        co_activations: 1,
335                                        created_tick: tick,
336                                        last_activated_tick: tick,
337                                    });
338                                    wire_events.push((from, to));
339                                }
340                            }
341                        }
342                    }
343
344                    events.push(ColonyEvent::Presented {
345                        id: agent_id,
346                        fragment_count: fragments.len(),
347                        node_ids,
348                    });
349
350                    if !wire_events.is_empty() {
351                        events.push(ColonyEvent::Wired {
352                            id: agent_id,
353                            connection_count: wire_events.len(),
354                        });
355                    }
356                }
357
358                AgentAction::Deposit(location, trace) => {
359                    let agent_id = self.agents[idx].id();
360                    self.substrate.deposit_trace(&location, trace);
361                    events.push(ColonyEvent::Deposited {
362                        id: agent_id,
363                        location,
364                    });
365                }
366
367                AgentAction::Emit(signal) => {
368                    self.substrate.emit_signal(signal);
369                }
370
371                AgentAction::WireNodes(connections) => {
372                    let agent_id = self.agents[idx].id();
373                    let tick = self.substrate.current_tick();
374                    let mut wired_count = 0;
375                    for (from, to, base_weight) in &connections {
376                        // Get embeddings for semantic wiring (clone to avoid borrow issues)
377                        let embedding_from = self.substrate.graph().get_node(from)
378                            .and_then(|n| n.embedding.clone());
379                        let embedding_to = self.substrate.graph().get_node(to)
380                            .and_then(|n| n.embedding.clone());
381
382                        // Compute semantic weight before mutating graph
383                        let weight = compute_semantic_weight(
384                            *base_weight,
385                            embedding_from.as_deref(),
386                            embedding_to.as_deref(),
387                            &self.semantic_wiring,
388                        );
389
390                        if let Some(w) = weight {
391                            if let Some(edge) = self.substrate.graph_mut().get_edge_mut(from, to) {
392                                edge.weight = (edge.weight + w).min(1.0);
393                                edge.co_activations += 1;
394                                edge.last_activated_tick = tick;
395                            } else {
396                                self.substrate.set_edge(*from, *to, EdgeData {
397                                    weight: w,
398                                    co_activations: 1,
399                                    created_tick: tick,
400                                    last_activated_tick: tick,
401                                });
402                            }
403                            wired_count += 1;
404                        }
405                    }
406                    if wired_count > 0 {
407                        events.push(ColonyEvent::Wired {
408                            id: agent_id,
409                            connection_count: wired_count,
410                        });
411                    }
412                }
413
414                AgentAction::ExportCapability(_cap_id) => {
415                    let agent_id = self.agents[idx].id();
416                    let agent_pos = self.agents[idx].position();
417                    if let Some(vocab_bytes) = self.agents[idx].export_vocabulary() {
418                        // Count terms for event
419                        let terms_count = serde_json::from_slice::<VocabularyCapability>(&vocab_bytes)
420                            .map(|v| v.terms.len())
421                            .unwrap_or(0);
422
423                        // Deposit as CapabilityDeposit trace at agent position
424                        let trace = Trace {
425                            agent_id,
426                            trace_type: TraceType::CapabilityDeposit,
427                            intensity: 1.0,
428                            tick: self.substrate.current_tick(),
429                            payload: vocab_bytes,
430                        };
431                        self.substrate.deposit_trace(
432                            &SubstrateLocation::Spatial(agent_pos),
433                            trace,
434                        );
435
436                        // Emit Capability signal to attract other agents
437                        self.substrate.emit_signal(Signal::new(
438                            SignalType::Capability,
439                            0.8,
440                            agent_pos,
441                            agent_id,
442                            self.substrate.current_tick(),
443                        ));
444
445                        events.push(ColonyEvent::CapabilityExported {
446                            agent_id,
447                            terms_count,
448                        });
449                    }
450                }
451
452                AgentAction::SymbioseWith(target_id) => {
453                    let host_idx = idx;
454                    let host_id = self.agents[host_idx].id();
455
456                    // Find target agent
457                    if let Some(target_idx) = self.agents.iter().position(|a| a.id() == target_id) {
458                        // Build target's profile and extract vocabulary
459                        let target_profile = self.agents[target_idx].profile();
460                        let target_vocab = self.agents[target_idx].export_vocabulary()
461                            .unwrap_or_default();
462
463                        // Evaluate symbiosis
464                        if let Some(SymbiosisEval::Integrate) =
465                            self.agents[host_idx].evaluate_symbiosis(&target_profile)
466                        {
467                            let host_type = self.agents[host_idx].agent_type().to_string();
468                            let absorbed_type = self.agents[target_idx].agent_type().to_string();
469
470                            // Host absorbs the symbiont
471                            self.agents[host_idx].absorb_symbiont(target_profile, target_vocab);
472
473                            // Mark target for removal via symbiotic absorption
474                            symbiotic_deaths.push((target_idx, host_id));
475
476                            events.push(ColonyEvent::Symbiosis {
477                                host: host_id,
478                                absorbed: target_id,
479                                host_type,
480                                absorbed_type,
481                            });
482                        }
483                    }
484                }
485
486                AgentAction::Apoptose => {
487                    to_die.push(idx);
488                }
489
490                AgentAction::Idle => {}
491
492                _ => {}
493            }
494        }
495
496        // Phase 2.5: Dissolution + Capability Integration
497        // For each agent: compute BoundaryContext, modulate boundary,
498        // externalize/internalize vocabulary, integrate nearby capabilities
499        {
500            let _tick = self.substrate.current_tick();
501            let agent_count = self.agents.len();
502
503            for i in 0..agent_count {
504                let agent_id = self.agents[i].id();
505                let agent_pos = self.agents[i].position();
506                let agent_age = self.agents[i].age();
507
508                // Compute BoundaryContext — cache externalized vocab for reuse
509                let vocab_terms = self.agents[i].externalize_vocabulary();
510                let mut reinforcement_count = 0u64;
511                let graph = self.substrate.graph();
512                for term in &vocab_terms {
513                    let matching = graph.find_nodes_by_exact_label(term);
514                    for nid in matching {
515                        if let Some(node) = graph.get_node(nid) {
516                            reinforcement_count += node.access_count;
517                        }
518                    }
519                }
520
521                let useful_outputs_estimate = reinforcement_count.min(100);
522                let trust = if agent_age > 0 {
523                    (useful_outputs_estimate as f64 / agent_age as f64).min(1.0)
524                } else {
525                    0.0
526                };
527
528                let context = BoundaryContext {
529                    reinforcement_count,
530                    age: agent_age,
531                    trust,
532                };
533
534                self.agents[i].modulate_boundary(&context);
535                let permeability = self.agents[i].permeability();
536
537                // High permeability: boost matching graph nodes' access_count
538                if permeability > 0.5 {
539                    // Reuse cached vocab_terms instead of calling externalize_vocabulary again
540                    let mut terms_externalized = 0usize;
541                    for term in &vocab_terms {
542                        let matching: Vec<NodeId> = self.substrate.graph().find_nodes_by_exact_label(term).to_vec();
543                        for nid in &matching {
544                            if let Some(node) = self.substrate.graph_mut().get_node_mut(nid) {
545                                node.access_count += 1;
546                                terms_externalized += 1;
547                            }
548                        }
549                    }
550                    if terms_externalized > 0 {
551                        events.push(ColonyEvent::Dissolved {
552                            agent_id,
553                            permeability,
554                            terms_externalized,
555                        });
556                    }
557                }
558
559                // Any permeability > 0: internalize nearby concept labels
560                if permeability > 0.0 {
561                    let all_nodes = self.substrate.graph().all_nodes();
562                    let nearby_labels: Vec<String> = all_nodes.iter()
563                        .filter_map(|nid| {
564                            let node = self.substrate.graph().get_node(nid)?;
565                            if node.position.distance_to(&agent_pos) <= 15.0
566                                && node.node_type == NodeType::Concept
567                            {
568                                Some(node.label.clone())
569                            } else {
570                                None
571                            }
572                        })
573                        .collect();
574                    if !nearby_labels.is_empty() {
575                        self.agents[i].internalize_vocabulary(&nearby_labels);
576                    }
577                }
578
579                // Capability integration: check for CapabilityDeposit traces near agent
580                let traces = self.substrate.traces_near(
581                    &agent_pos,
582                    10.0,
583                    &TraceType::CapabilityDeposit,
584                );
585                for trace in &traces {
586                    if trace.agent_id != agent_id
587                        && !trace.payload.is_empty()
588                    {
589                        let payload = trace.payload.clone();
590                        let from_agent = trace.agent_id;
591                        let terms_count = serde_json::from_slice::<VocabularyCapability>(&payload)
592                            .map(|v| v.terms.len())
593                            .unwrap_or(0);
594                        if self.agents[i].integrate_vocabulary(&payload) {
595                            events.push(ColonyEvent::CapabilityIntegrated {
596                                agent_id,
597                                from_agent,
598                                terms_count,
599                            });
600                        }
601                    }
602                }
603            }
604        }
605
606        // Add symbiotic deaths to the death list
607        for (idx, _absorber_id) in &symbiotic_deaths {
608            if !to_die.contains(idx) {
609                to_die.push(*idx);
610            }
611        }
612
613        // Phase 3: Remove dead agents
614        to_die.sort();
615        to_die.dedup();
616        let dead_count = to_die.len();
617        for idx in to_die.into_iter().rev() {
618            let agent = self.agents.remove(idx);
619            let mut death_signal = agent.prepare_death_signal();
620
621            // Override cause if this was a symbiotic absorption
622            if let Some((_, absorber_id)) = symbiotic_deaths.iter().find(|(i, _)| *i == idx) {
623                death_signal.cause = DeathCause::SymbioticAbsorption(*absorber_id);
624            }
625
626            events.push(ColonyEvent::Died {
627                signal: death_signal.clone(),
628            });
629            self.death_signals.push(death_signal);
630            self.total_died += 1;
631        }
632
633        // Phase 4: Substrate decay
634        self.substrate
635            .decay_signals(self.signal_decay_rate, self.signal_removal_threshold);
636        self.substrate
637            .decay_traces(self.trace_decay_rate, self.trace_removal_threshold);
638        // Synaptic pruning: activity-based decay with maturation protection
639        let current_tick = self.substrate.current_tick();
640        self.substrate.graph_mut().decay_edges_activity(
641            self.edge_decay_rate,
642            self.edge_prune_threshold,
643            current_tick,
644            self.staleness_factor,
645            self.maturation_ticks,
646        );
647        // Competitive pruning: cap per-node degree
648        self.substrate
649            .graph_mut()
650            .prune_to_max_degree(self.max_edge_degree);
651
652        // Phase 4b: Fitness tracking — wire colony events to the tracker
653        for event in &events {
654            match event {
655                ColonyEvent::Presented { id, fragment_count, .. } => {
656                    self.fitness_tracker.record_concepts(id, *fragment_count as u64);
657                }
658                ColonyEvent::Wired { id, connection_count } => {
659                    self.fitness_tracker.record_edges(id, *connection_count as u64);
660                }
661                _ => {}
662            }
663        }
664        let alive_ids: Vec<AgentId> = self.agents.iter().map(|a| a.id()).collect();
665        self.fitness_tracker.tick_all(&alive_ids);
666
667        // Phase 5: Advance tick
668        self.substrate.advance_tick();
669
670        events.push(ColonyEvent::TickComplete {
671            tick: self.substrate.current_tick(),
672            alive: self.agents.len(),
673            dead_this_tick: dead_count,
674        });
675
676        // Record events in history
677        let current_tick = self.substrate.current_tick();
678        for event in &events {
679            self.event_history.push((current_tick, event.clone()));
680        }
681
682        events
683    }
684
685    /// Run the simulation for N ticks.
686    pub fn run(&mut self, ticks: u64) -> Vec<Vec<ColonyEvent>> {
687        let mut all_events = Vec::new();
688        for _ in 0..ticks {
689            all_events.push(self.tick());
690        }
691        all_events
692    }
693
694    /// Get colony statistics.
695    pub fn stats(&self) -> ColonyStats {
696        let docs = self.substrate.all_documents();
697        let digested = docs.iter().filter(|d| d.digested).count();
698        ColonyStats {
699            tick: self.substrate.current_tick(),
700            agents_alive: self.agents.len(),
701            agents_died: self.total_died,
702            total_spawned: self.total_spawned,
703            graph_nodes: self.substrate.node_count(),
704            graph_edges: self.substrate.edge_count(),
705            total_signals: self.substrate.all_signals().len(),
706            documents_total: docs.len(),
707            documents_digested: digested,
708        }
709    }
710
711    /// Get a reference to the substrate.
712    pub fn substrate(&self) -> &SubstrateImpl {
713        &self.substrate
714    }
715
716    /// Get a mutable reference to the substrate.
717    pub fn substrate_mut(&mut self) -> &mut SubstrateImpl {
718        &mut self.substrate
719    }
720
721    /// Number of agents currently alive.
722    pub fn alive_count(&self) -> usize {
723        self.agents.len()
724    }
725
726    /// All death signals collected during the simulation.
727    pub fn death_signals(&self) -> &[DeathSignal] {
728        &self.death_signals
729    }
730
731    /// Feed text input to a specific agent by index.
732    pub fn feed_agent(&mut self, agent_idx: usize, input: String) -> Option<DigestionResult> {
733        self.agents
734            .get_mut(agent_idx)
735            .map(|agent| agent.engulf(input))
736    }
737
738    /// Take a serializable snapshot of the colony's current state.
739    pub fn snapshot(&self) -> ColonySnapshot {
740        let graph = self.substrate.graph();
741
742        let agents: Vec<AgentSnapshot> = self.agents.iter().map(|a| {
743            AgentSnapshot {
744                id: a.id(),
745                agent_type: a.agent_type().to_string(),
746                position: a.position(),
747                age: a.age(),
748                permeability: a.permeability(),
749                vocabulary_size: a.vocabulary_size(),
750            }
751        }).collect();
752
753        let nodes: Vec<NodeSnapshot> = graph.all_nodes().iter().filter_map(|nid| {
754            let n = graph.get_node(nid)?;
755            Some(NodeSnapshot {
756                id: n.id,
757                label: n.label.clone(),
758                node_type: n.node_type.clone(),
759                position: n.position,
760                access_count: n.access_count,
761            })
762        }).collect();
763
764        let edges: Vec<EdgeSnapshot> = graph.all_edges().iter().map(|(from, to, data)| {
765            let from_label = graph.get_node(from).map(|n| n.label.clone()).unwrap_or_default();
766            let to_label = graph.get_node(to).map(|n| n.label.clone()).unwrap_or_default();
767            EdgeSnapshot {
768                from_label,
769                to_label,
770                weight: data.weight,
771                co_activations: data.co_activations,
772            }
773        }).collect();
774
775        ColonySnapshot {
776            tick: self.substrate.current_tick(),
777            agents,
778            nodes,
779            edges,
780            stats: self.stats(),
781        }
782    }
783
784    /// Get the full event history with tick numbers.
785    pub fn event_history(&self) -> &[(Tick, ColonyEvent)] {
786        &self.event_history
787    }
788
789    /// Get a reference to the agents.
790    pub fn agents(&self) -> &[Box<dyn Agent<Input = String, Fragment = String, Presentation = Vec<String>>>] {
791        &self.agents
792    }
793
794    /// Get a reference to the fitness tracker.
795    pub fn fitness_tracker(&self) -> &FitnessTracker {
796        &self.fitness_tracker
797    }
798
799    /// Get a mutable reference to the fitness tracker.
800    pub fn fitness_tracker_mut(&mut self) -> &mut FitnessTracker {
801        &mut self.fitness_tracker
802    }
803
804    /// Emit an input signal at a position (to attract agents).
805    pub fn emit_input_signal(&mut self, position: Position, intensity: f64) {
806        let signal = Signal::new(
807            SignalType::Input,
808            intensity,
809            position,
810            AgentId::new(),
811            self.substrate.current_tick(),
812        );
813        self.substrate.emit_signal(signal);
814    }
815}
816
817impl Default for Colony {
818    fn default() -> Self {
819        Self::new()
820    }
821}
822
823#[cfg(test)]
824mod tests {
825    use super::*;
826    use phago_agents::digester::Digester;
827
828    #[test]
829    fn spawn_and_count_agents() {
830        let mut colony = Colony::new();
831        colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0))));
832        colony.spawn(Box::new(Digester::new(Position::new(5.0, 5.0))));
833        assert_eq!(colony.alive_count(), 2);
834        assert_eq!(colony.stats().total_spawned, 2);
835    }
836
837    #[test]
838    fn tick_advances_simulation() {
839        let mut colony = Colony::new();
840        colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0))));
841        colony.tick();
842        assert_eq!(colony.stats().tick, 1);
843    }
844
845    #[test]
846    fn agent_apoptosis_in_colony() {
847        let mut colony = Colony::new();
848        colony.spawn(Box::new(
849            Digester::new(Position::new(0.0, 0.0)).with_max_idle(3),
850        ));
851
852        assert_eq!(colony.alive_count(), 1);
853
854        for _ in 0..5 {
855            colony.tick();
856        }
857
858        assert_eq!(colony.alive_count(), 0);
859        assert_eq!(colony.stats().agents_died, 1);
860        assert_eq!(colony.death_signals().len(), 1);
861    }
862
863    #[test]
864    fn ingest_document_creates_signal() {
865        let mut colony = Colony::new();
866        let pos = Position::new(5.0, 5.0);
867        let doc_id = colony.ingest_document("Test Doc", "cell membrane protein", pos);
868
869        let stats = colony.stats();
870        assert_eq!(stats.documents_total, 1);
871        assert_eq!(stats.documents_digested, 0);
872        assert_eq!(stats.total_signals, 1); // Input signal emitted
873
874        // Document is in the substrate
875        let doc = colony.substrate().get_document(&doc_id);
876        assert!(doc.is_some());
877        assert!(!doc.unwrap().digested);
878    }
879
880    #[test]
881    fn agent_finds_and_digests_document() {
882        let mut colony = Colony::new();
883
884        // Place document at origin
885        colony.ingest_document(
886            "Biology 101",
887            "The cell membrane controls transport of molecules into and out of the cell. \
888             Proteins embedded in the membrane serve as channels and receptors.",
889            Position::new(0.0, 0.0),
890        );
891
892        // Spawn agent at origin (right on top of the document)
893        colony.spawn(Box::new(
894            Digester::new(Position::new(0.0, 0.0)).with_max_idle(50),
895        ));
896
897        // Run enough ticks for the full cycle:
898        // tick 1: Seeking → finds doc → EngulfDocument
899        // tick 2: FoundTarget → engulfed → Digesting
900        // tick 3: Digesting → lyse → PresentFragments
901        // tick 4: Presenting → Deposit trace
902        colony.run(10);
903
904        let stats = colony.stats();
905        assert_eq!(stats.documents_digested, 1, "Document should be digested");
906        assert!(stats.graph_nodes > 0, "Should have concept nodes: got {}", stats.graph_nodes);
907        assert!(stats.graph_edges > 0, "Should have edges: got {}", stats.graph_edges);
908    }
909
910    #[test]
911    fn multiple_documents_build_graph() {
912        let mut colony = Colony::new();
913
914        // Two documents about related topics
915        colony.ingest_document(
916            "Cell Biology",
917            "The cell membrane is a lipid bilayer that controls transport. \
918             Proteins in the membrane act as channels and receptors for signaling.",
919            Position::new(0.0, 0.0),
920        );
921        colony.ingest_document(
922            "Molecular Transport",
923            "Active transport across the cell membrane requires ATP energy. \
924             Channel proteins facilitate passive transport of ions and molecules.",
925            Position::new(2.0, 0.0),
926        );
927
928        // Spawn agents near each document
929        colony.spawn(Box::new(
930            Digester::new(Position::new(0.0, 0.0)).with_max_idle(50),
931        ));
932        colony.spawn(Box::new(
933            Digester::new(Position::new(2.0, 0.0)).with_max_idle(50),
934        ));
935
936        colony.run(20);
937
938        let stats = colony.stats();
939        assert_eq!(stats.documents_digested, 2, "Both documents should be digested");
940        // Shared concepts (cell, membrane, transport, proteins) should create
941        // overlapping graph nodes and strengthen edges
942        assert!(stats.graph_nodes >= 5, "Expected at least 5 concept nodes, got {}", stats.graph_nodes);
943    }
944
945    #[test]
946    fn colony_stats_are_accurate() {
947        let mut colony = Colony::new();
948        colony.spawn(Box::new(
949            Digester::new(Position::new(0.0, 0.0)).with_max_idle(2),
950        ));
951        colony.spawn(Box::new(
952            Digester::new(Position::new(5.0, 5.0)).with_max_idle(100),
953        ));
954
955        colony.run(5);
956
957        let stats = colony.stats();
958        assert_eq!(stats.total_spawned, 2);
959        assert_eq!(stats.agents_died, 1);
960        assert_eq!(stats.agents_alive, 1);
961    }
962
963    #[test]
964    fn semantic_wiring_config_is_accessible() {
965        let colony = Colony::new();
966        let config = colony.semantic_wiring_config();
967        // Default config should not require embeddings
968        assert!(!config.require_embeddings);
969        assert!(config.min_similarity >= 0.0);
970        assert!(config.similarity_influence >= 0.0);
971    }
972
973    #[test]
974    fn with_semantic_wiring_configures_colony() {
975        use phago_core::semantic::SemanticWiringConfig;
976
977        let colony = Colony::new()
978            .with_semantic_wiring(SemanticWiringConfig::strict());
979
980        let config = colony.semantic_wiring_config();
981        assert!(config.require_embeddings);
982        assert!(config.min_similarity > 0.0);
983    }
984
985    #[test]
986    fn semantic_wiring_boosts_similar_concept_edges() {
987        use phago_core::semantic::SemanticWiringConfig;
988
989        let mut colony = Colony::new()
990            .with_semantic_wiring(SemanticWiringConfig::default());
991
992        // Manually add two nodes with similar embeddings
993        let emb_a = vec![1.0, 0.0, 0.0];  // Unit vector along x
994        let emb_b = vec![0.95, 0.31, 0.0]; // ~18° from emb_a (high similarity)
995
996        let node_a = colony.substrate_mut().add_node(NodeData {
997            id: NodeId::new(),
998            label: "concept_a".to_string(),
999            node_type: NodeType::Concept,
1000            position: Position::new(0.0, 0.0),
1001            access_count: 1,
1002            created_tick: 0,
1003            embedding: Some(emb_a),
1004        });
1005
1006        let node_b = colony.substrate_mut().add_node(NodeData {
1007            id: NodeId::new(),
1008            label: "concept_b".to_string(),
1009            node_type: NodeType::Concept,
1010            position: Position::new(1.0, 0.0),
1011            access_count: 1,
1012            created_tick: 0,
1013            embedding: Some(emb_b),
1014        });
1015
1016        // Wire them manually using WireNodes action
1017        colony.substrate_mut().set_edge(node_a, node_b, EdgeData {
1018            weight: 0.1,
1019            co_activations: 1,
1020            created_tick: 0,
1021            last_activated_tick: 0,
1022        });
1023
1024        let edge = colony.substrate().graph().get_edge(&node_a, &node_b).unwrap();
1025        assert!(edge.weight >= 0.1, "Edge should have at least base weight");
1026    }
1027
1028    #[test]
1029    fn semantic_wiring_with_no_embeddings_uses_base_weight() {
1030        use phago_core::semantic::SemanticWiringConfig;
1031
1032        let mut colony = Colony::new()
1033            .with_semantic_wiring(SemanticWiringConfig::default());
1034
1035        // Add two nodes WITHOUT embeddings
1036        let node_a = colony.substrate_mut().add_node(NodeData {
1037            id: NodeId::new(),
1038            label: "plain_a".to_string(),
1039            node_type: NodeType::Concept,
1040            position: Position::new(0.0, 0.0),
1041            access_count: 1,
1042            created_tick: 0,
1043            embedding: None,
1044        });
1045
1046        let node_b = colony.substrate_mut().add_node(NodeData {
1047            id: NodeId::new(),
1048            label: "plain_b".to_string(),
1049            node_type: NodeType::Concept,
1050            position: Position::new(1.0, 0.0),
1051            access_count: 1,
1052            created_tick: 0,
1053            embedding: None,
1054        });
1055
1056        // Wire them
1057        colony.substrate_mut().set_edge(node_a, node_b, EdgeData {
1058            weight: 0.1,
1059            co_activations: 1,
1060            created_tick: 0,
1061            last_activated_tick: 0,
1062        });
1063
1064        let edge = colony.substrate().graph().get_edge(&node_a, &node_b).unwrap();
1065        // With default config, no embeddings means base weight is used
1066        assert!((edge.weight - 0.1).abs() < 0.01, "Edge should use base weight: got {}", edge.weight);
1067    }
1068}