Skip to main content

rustant_core/multi/
mod.rs

1//! Multi-agent system — isolation, routing, spawning, and inter-agent messaging.
2//!
3//! Provides the building blocks for running multiple agents within a single
4//! Rustant instance, each with its own isolated memory and safety context.
5
6pub mod isolation;
7pub mod messaging;
8pub mod orchestrator;
9pub mod routing;
10pub mod spawner;
11
12pub use isolation::{AgentContext, AgentStatus, ResourceLimits};
13pub use messaging::{AgentEnvelope, AgentPayload, MessageBus, MessagePriority};
14pub use orchestrator::{AgentOrchestrator, TaskHandler};
15pub use routing::{AgentRoute, AgentRouter};
16pub use spawner::AgentSpawner;
17
18#[cfg(test)]
19mod tests {
20    use super::*;
21    use crate::channels::ChannelType;
22    use std::collections::HashMap;
23    use uuid::Uuid;
24
25    #[test]
26    fn test_multi_module_exports() {
27        // Verify that key types are accessible via the module re-exports.
28        let bus = MessageBus::new(100);
29        assert_eq!(bus.pending_count_all(), 0);
30    }
31
32    #[test]
33    fn test_channel_to_agent_to_node_flow() {
34        // ChannelMessage → AgentRouter → MessageBus → AgentPayload::TaskRequest
35        let agent_id = Uuid::new_v4();
36        let mut router = AgentRouter::new();
37        router.add_route(AgentRoute {
38            priority: 1,
39            target_agent_id: agent_id,
40            conditions: vec![routing::RouteCondition::ChannelType(ChannelType::Telegram)],
41        });
42
43        // Route a channel message
44        let req = routing::RouteRequest::new()
45            .with_channel(ChannelType::Telegram)
46            .with_message("run shell ls");
47        let target = router.route(&req).unwrap();
48        assert_eq!(target, agent_id);
49
50        // Deliver as a TaskRequest via MessageBus
51        let mut bus = MessageBus::new(100);
52        bus.register(agent_id);
53
54        let from = Uuid::new_v4();
55        let mut args = HashMap::new();
56        args.insert("channel_type".into(), "Telegram".into());
57        let envelope = AgentEnvelope::new(
58            from,
59            agent_id,
60            AgentPayload::TaskRequest {
61                description: "run shell ls".into(),
62                args,
63            },
64        );
65        bus.send(envelope).unwrap();
66
67        // Agent receives the message
68        let received = bus.receive(&agent_id).unwrap();
69        match &received.payload {
70            AgentPayload::TaskRequest { description, args } => {
71                assert_eq!(description, "run shell ls");
72                assert_eq!(args.get("channel_type").unwrap(), "Telegram");
73            }
74            _ => panic!("Expected TaskRequest"),
75        }
76    }
77
78    #[test]
79    fn test_multi_agent_delegation() {
80        // Parent spawns child, sends TaskRequest, child responds with TaskResult
81        let mut spawner = AgentSpawner::default();
82        let parent = spawner.spawn("parent").unwrap();
83        let child = spawner.spawn_child("child", parent).unwrap();
84
85        let mut bus = MessageBus::new(100);
86        bus.register(parent);
87        bus.register(child);
88
89        // Parent delegates a task to child
90        let task = AgentEnvelope::new(
91            parent,
92            child,
93            AgentPayload::TaskRequest {
94                description: "analyze code".into(),
95                args: HashMap::new(),
96            },
97        );
98        let correlation = task.id;
99        bus.send(task).unwrap();
100
101        // Child receives
102        let received = bus.receive(&child).unwrap();
103        assert_eq!(received.from, parent);
104
105        // Child responds with a result using the correlation ID
106        let response = AgentEnvelope::new(
107            child,
108            parent,
109            AgentPayload::TaskResult {
110                success: true,
111                output: "Analysis complete".into(),
112            },
113        )
114        .with_correlation(correlation);
115        bus.send(response).unwrap();
116
117        // Parent receives correlated response
118        let result = bus.receive(&parent).unwrap();
119        assert_eq!(result.correlation_id, Some(correlation));
120        match &result.payload {
121            AgentPayload::TaskResult { success, output } => {
122                assert!(success);
123                assert_eq!(output, "Analysis complete");
124            }
125            _ => panic!("Expected TaskResult"),
126        }
127    }
128
129    #[test]
130    fn test_agent_fact_sharing() {
131        // Agent A sends FactShare to Agent B, B receives and processes
132        let mut spawner = AgentSpawner::default();
133        let agent_a = spawner.spawn("agent-a").unwrap();
134        let agent_b = spawner.spawn("agent-b").unwrap();
135
136        let mut bus = MessageBus::new(100);
137        bus.register(agent_a);
138        bus.register(agent_b);
139
140        // Agent A shares a fact with Agent B
141        let fact = AgentEnvelope::new(
142            agent_a,
143            agent_b,
144            AgentPayload::FactShare {
145                key: "project.language".into(),
146                value: "Rust".into(),
147            },
148        );
149        bus.send(fact).unwrap();
150
151        // Agent B receives the fact
152        let received = bus.receive(&agent_b).unwrap();
153        assert_eq!(received.from, agent_a);
154        match &received.payload {
155            AgentPayload::FactShare { key, value } => {
156                assert_eq!(key, "project.language");
157                assert_eq!(value, "Rust");
158            }
159            _ => panic!("Expected FactShare"),
160        }
161    }
162
163    #[test]
164    fn test_full_lifecycle() {
165        // Spawn agents, register routes, send messages, terminate cascading
166        let mut spawner = AgentSpawner::default();
167        let supervisor = spawner.spawn("supervisor").unwrap();
168        let worker1 = spawner.spawn_child("worker-1", supervisor).unwrap();
169        let worker2 = spawner.spawn_child("worker-2", supervisor).unwrap();
170        assert_eq!(spawner.agent_count(), 3);
171
172        // Set up routing
173        let mut router = AgentRouter::new().with_default(supervisor);
174        router.add_route(AgentRoute {
175            priority: 1,
176            target_agent_id: worker1,
177            conditions: vec![routing::RouteCondition::TaskPrefix("code:".into())],
178        });
179        router.add_route(AgentRoute {
180            priority: 1,
181            target_agent_id: worker2,
182            conditions: vec![routing::RouteCondition::TaskPrefix("test:".into())],
183        });
184
185        // Route tasks
186        let req1 = routing::RouteRequest::new().with_task("code:refactor");
187        let req2 = routing::RouteRequest::new().with_task("test:unit");
188        let req3 = routing::RouteRequest::new().with_task("deploy:prod");
189
190        assert_eq!(router.route(&req1), Some(worker1));
191        assert_eq!(router.route(&req2), Some(worker2));
192        assert_eq!(router.route(&req3), Some(supervisor)); // default
193
194        // Set up bus and send messages
195        let mut bus = MessageBus::new(100);
196        bus.register(supervisor);
197        bus.register(worker1);
198        bus.register(worker2);
199
200        let task = AgentEnvelope::new(
201            supervisor,
202            worker1,
203            AgentPayload::TaskRequest {
204                description: "code:refactor main.rs".into(),
205                args: HashMap::new(),
206            },
207        );
208        bus.send(task).unwrap();
209        assert_eq!(bus.pending_count(&worker1), 1);
210
211        // Terminate supervisor cascades to workers
212        let removed = spawner.terminate(supervisor);
213        assert_eq!(removed, 3);
214        assert_eq!(spawner.agent_count(), 0);
215
216        // Bus still has the message (bus and spawner are independent)
217        assert_eq!(bus.pending_count(&worker1), 1);
218    }
219}