arbiter_engine/
world.rs

1//! The world module contains the core world abstraction for the Arbiter Engine.
2
3use std::collections::VecDeque;
4
5use arbiter_core::{database::ArbiterDB, environment::Environment, middleware::ArbiterMiddleware};
6use futures_util::future::join_all;
7use serde::de::DeserializeOwned;
8use tokio::spawn;
9
10use super::*;
11use crate::{
12    agent::{Agent, AgentBuilder},
13    machine::{CreateStateMachine, MachineInstruction},
14};
15
16/// A world is a collection of agents that use the same type of provider, e.g.,
17/// operate on the same blockchain or same `Environment`. The world is
18/// responsible for managing the agents and their state transitions.
19///
20/// # How it works
21/// The [`World`] holds on to a collection of [`Agent`]s and can run them all
22/// concurrently when the [`run`] method is called. The [`World`] takes in
23/// [`AgentBuilder`]s and when it does so, it creates [`Agent`]s that are now
24/// connected to the world via a client ([`Arc<RevmMiddleware>`]) and a messager
25/// ([`Messager`]).
26#[derive(Debug)]
27pub struct World {
28    /// The identifier of the world.
29    pub id: String,
30
31    /// The agents in the world.
32    pub agents: Option<HashMap<String, Agent>>,
33
34    /// The environment for the world.
35    pub environment: Option<Environment>,
36
37    /// The messaging layer for the world.
38    pub messager: Messager,
39}
40
41use std::{fs::File, io::Read};
42impl World {
43    /// Creates a new [`World`] with the given identifier and provider.
44    pub fn new(id: &str) -> Self {
45        Self {
46            id: id.to_owned(),
47            agents: Some(HashMap::new()),
48            environment: Some(Environment::builder().build()),
49            messager: Messager::new(),
50        }
51    }
52
53    /// Builds and adds agents to the world from a configuration file.
54    ///
55    /// This method reads a configuration file specified by `config_path`, which
56    /// should be a TOML file containing the definitions of agents and their
57    /// behaviors. Each agent is identified by a unique string key, and
58    /// associated with a list of behaviors. These behaviors are
59    /// deserialized into instances that implement the `CreateStateMachine`
60    /// trait, allowing them to be converted into state machines that define
61    /// the agent's behavior within the world.
62    ///
63    /// # Type Parameters
64    ///
65    /// - `C`: The type of the behavior component that each agent will be
66    ///   associated with.
67    /// This type must implement the `CreateStateMachine`, `Serialize`,
68    /// `DeserializeOwned`, and `Debug` traits.
69    ///
70    /// # Arguments
71    ///
72    /// - `config_path`: A string slice that holds the path to the configuration
73    ///   file
74    /// relative to the current working directory.
75    ///
76    /// # Panics
77    ///
78    /// This method will panic if:
79    /// - The current working directory cannot be determined.
80    /// - The configuration file specified by `config_path` cannot be opened.
81    /// - The configuration file cannot be read into a string.
82    /// - The contents of the configuration file cannot be deserialized into the
83    ///   expected
84    /// `HashMap<String, Vec<C>>` format.
85    ///
86    /// # Examples
87    ///
88    /// Assuming a TOML file named `agents_config.toml` exists in the current
89    /// working directory with the following content:
90    ///
91    /// ```toml
92    /// [[agent1]]
93    /// BehaviorTypeA = { ... } ,
94    /// [[agent1]]
95    /// BehaviorTypeB = { ... }
96    ///
97    /// [agent2]
98    /// BehaviorTypeC = { ... }
99    /// ```
100    pub fn from_config<C: CreateStateMachine + Serialize + DeserializeOwned + Debug>(
101        config_path: &str,
102    ) -> Result<Self, ArbiterEngineError> {
103        let cwd = std::env::current_dir()?;
104        let path = cwd.join(config_path);
105        info!("Reading from path: {:?}", path);
106        let mut file = File::open(path)?;
107
108        let mut contents = String::new();
109        file.read_to_string(&mut contents)?;
110
111        #[derive(Deserialize)]
112        struct Config<C> {
113            id: Option<String>,
114            #[serde(flatten)]
115            agents_map: HashMap<String, Vec<C>>,
116        }
117
118        let config: Config<C> = toml::from_str(&contents)?;
119
120        let mut world = World::new(&config.id.unwrap_or_else(|| "world".to_owned()));
121
122        for (agent, behaviors) in config.agents_map {
123            let mut next_agent = Agent::builder(&agent);
124            for behavior in behaviors {
125                let engine = behavior.create_state_machine();
126                next_agent = next_agent.with_engine(engine);
127            }
128            world.add_agent(next_agent);
129        }
130        Ok(world)
131    }
132
133    /// Adds an agent, constructed from the provided `AgentBuilder`, to the
134    /// world.
135    ///
136    /// This method takes an `AgentBuilder` instance, extracts its identifier,
137    /// and uses it to create both a `RevmMiddleware` client and a
138    /// `Messager` specific to the agent. It then builds the `Agent` from
139    /// the `AgentBuilder` using these components. Finally, the newly
140    /// created `Agent` is inserted into the world's internal collection of
141    /// agents.
142    ///
143    /// # Panics
144    ///
145    /// This method will panic if:
146    /// - It fails to create a `RevmMiddleware` client for the agent.
147    /// - The `AgentBuilder` fails to build the `Agent`.
148    /// - The world's internal collection of agents is not initialized.
149    ///
150    /// # Examples
151    ///
152    /// Assuming you have an `AgentBuilder` instance named `agent_builder`:
153    ///
154    /// ```ignore
155    /// world.add_agent(agent_builder);
156    /// ```
157    ///
158    /// This will add the agent defined by `agent_builder` to the world.
159    pub fn add_agent(&mut self, agent_builder: AgentBuilder) {
160        let id = agent_builder.id.clone();
161        let client = ArbiterMiddleware::new(self.environment.as_ref().unwrap(), Some(&id))
162            .expect("Failed to create RevmMiddleware client for agent");
163        let messager = self.messager.for_agent(&id);
164        let agent = agent_builder
165            .build(client, messager)
166            .expect("Failed to build agent from AgentBuilder");
167        let agents = self
168            .agents
169            .as_mut()
170            .expect("Agents collection not initialized");
171        agents.insert(id.to_owned(), agent);
172    }
173
174    /// Executes all agents and their behaviors concurrently within the world.
175    ///
176    /// This method takes all the agents registered in the world and runs their
177    /// associated behaviors in parallel. Each agent's behaviors are
178    /// executed with their respective messaging and client context. This
179    /// method ensures that all agents and their behaviors are started
180    /// simultaneously, leveraging asynchronous execution to manage concurrent
181    /// operations.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if no agents are found in the world, possibly
186    /// indicating that the world has already been run or that no agents
187    /// were added prior to execution.
188    pub async fn run(&mut self) -> Result<ArbiterDB, ArbiterEngineError> {
189        let agents = match self.agents.take() {
190            Some(agents) => agents,
191            None => {
192                return Err(ArbiterEngineError::WorldError(
193                    "No agents found. Has the world already been ran?".to_owned(),
194                ))
195            }
196        };
197        let mut tasks = vec![];
198        // Prepare a queue for messagers corresponding to each behavior engine.
199        let mut messagers = VecDeque::new();
200        // Populate the messagers queue.
201        for (_, agent) in agents.iter() {
202            for _ in &agent.behavior_engines {
203                messagers.push_back(agent.messager.clone());
204            }
205        }
206        // For each agent, spawn a task for each of its behavior engines.
207        // Unwrap here is safe as we just built the dang thing.
208        for (_, mut agent) in agents {
209            for mut engine in agent.behavior_engines.drain(..) {
210                let client = agent.client.clone();
211                let messager = messagers.pop_front().unwrap();
212                tasks.push(spawn(async move {
213                    engine
214                        .execute(MachineInstruction::Start(client, messager))
215                        .await
216                }));
217            }
218        }
219        // Await the completion of all tasks.
220        join_all(tasks).await;
221
222        let db = self.environment.take().unwrap().stop()?;
223        Ok(db)
224    }
225}