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}