diff --git a/Cargo.lock b/Cargo.lock index a0cf7f3c..0d78808a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,7 +138,7 @@ checksum = "1a047897373be4bbb0224c1afdabca92648dc57a9c9ef6e7b0be3aff7a859c83" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -231,13 +231,12 @@ dependencies = [ "ethers", "foundry-config", "proc-macro2", - "quote", "rayon", "revm", "revm-primitives 1.3.0 (git+https://github.com/bluealloy/revm.git?rev=30bbcdfe81446c9d1e9b37acc95f208943ddf858)", "serde", "serde_json", - "syn 2.0.43", + "syn 2.0.48", "tempfile", "thiserror", "tokio", @@ -295,6 +294,7 @@ dependencies = [ "anyhow", "arbiter-bindings", "arbiter-core", + "arbiter-macros", "async-stream", "async-trait", "crossbeam-channel", @@ -306,11 +306,20 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "toml 0.8.9", "tracing", "tracing-subscriber", "tracing-test", ] +[[package]] +name = "arbiter-macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn 2.0.48", +] + [[package]] name = "argminmax" version = "0.6.1" @@ -515,7 +524,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -526,7 +535,7 @@ checksum = "531b97fb4cd3dfdce92c35dedbfdc1f0b9d8091c8ca943d6dae340ef5012d514" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -582,7 +591,7 @@ checksum = "823b8bb275161044e2ac7a25879cb3e2480cb403e3943022c7c769c599b756aa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -655,7 +664,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.43", + "syn 2.0.48", "which", ] @@ -806,7 +815,7 @@ checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -996,7 +1005,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1536,7 +1545,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1547,7 +1556,7 @@ checksum = "c2ad8cef1d801a4686bfd8919f0b30eac4c8e48968c437a6405ded4fb5272d2b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1720,7 +1729,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "syn 2.0.43", + "syn 2.0.48", "toml 0.8.9", "walkdir", ] @@ -1738,7 +1747,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1764,7 +1773,7 @@ dependencies = [ "serde", "serde_json", "strum", - "syn 2.0.43", + "syn 2.0.48", "tempfile", "thiserror", "tiny-keccak", @@ -2148,7 +2157,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -3146,7 +3155,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -3347,7 +3356,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -3402,7 +3411,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -3466,7 +3475,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -3504,7 +3513,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -3950,7 +3959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -4012,7 +4021,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", "version_check", "yansi 1.0.0-rc.1", ] @@ -4056,9 +4065,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -4181,7 +4190,7 @@ checksum = "2566c4bf6845f2c2e83b27043c3f5dfcd5ba8f2937d6c00dc009bfb51a079dc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -4750,7 +4759,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -5107,7 +5116,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -5175,9 +5184,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.43" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -5285,7 +5294,7 @@ checksum = "7ba277e77219e9eea169e8508942db1bf5d8a41ff2db9b20aab5a5aadc9fa25d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -5305,7 +5314,7 @@ checksum = "268026685b2be38d7103e9e507c938a1fcb3d7e6eb15e87870b617bf37b6d581" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -5409,7 +5418,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -5558,7 +5567,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -5870,7 +5879,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -5904,7 +5913,7 @@ checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6226,7 +6235,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -6246,7 +6255,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 075c14da..77b89dcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,12 @@ [workspace] # List of crates included in this workspace -members = [ "arbiter-bindings", "arbiter-core", "arbiter-engine", "documentation"] +members = [ + "arbiter-bindings", + "arbiter-core", + "arbiter-engine", + "arbiter-macros", + "documentation", +] # List of crates excluded from this workspace exclude = ["benches"] @@ -10,7 +16,10 @@ exclude = ["benches"] name = "arbiter" version = "0.4.13" edition = "2021" -authors = ["Waylon Jepsen ", "Colin Roberts "] +authors = [ + "Waylon Jepsen ", + "Colin Roberts ", +] description = "Allowing smart contract developers to do simulation driven development via an EVM emulator" license = "Apache-2.0" keywords = ["ethereum", "evm", "emulator", "testing", "smart-contracts"] @@ -23,21 +32,27 @@ path = "bin/main.rs" [workspace.dependencies] arbiter-bindings = { version = "*", path = "./arbiter-bindings" } arbiter-core = { version = "*", path = "./arbiter-core" } +arbiter-macros = { path = "./arbiter-macros" } +arbiter-engine = { path = "./arbiter-engine" } ethers = { version = "2.0.13" } serde = { version = "1.0.193", features = ["derive"] } serde_json = { version = "=1.0.108" } -revm = { git = "https://github.com/bluealloy/revm.git", features = [ "ethersdb", "std", "serde"], rev = "30bbcdfe81446c9d1e9b37acc95f208943ddf858" } +revm = { git = "https://github.com/bluealloy/revm.git", features = [ + "ethersdb", + "std", + "serde", +], rev = "30bbcdfe81446c9d1e9b37acc95f208943ddf858" } revm-primitives = { git = "https://github.com/bluealloy/revm.git", rev = "30bbcdfe81446c9d1e9b37acc95f208943ddf858" } thiserror = { version = "1.0.55" } -syn = { version = "2.0.43" } -quote = { version = "=1.0.33" } +syn = { version = "2.0.48", features = ["full"] } proc-macro2 = { version = "1.0.78" } tokio = { version = "1.36.0", features = ["macros", "full"] } -crossbeam-channel = { version = "0.5.11" } -futures-util = { version = "=0.3.30" } -async-trait = { version = "0.1.76" } +crossbeam-channel = { version = "0.5.11" } +futures-util = { version = "=0.3.30" } +async-trait = { version = "0.1.76" } tracing = "0.1.40" async-stream = "0.3.5" +toml = { version = "=0.8.9" } # Dependencies for the release build [dependencies] @@ -50,15 +65,14 @@ serde_json.workspace = true config = { version = "=0.14.0" } ethers.workspace = true revm.workspace = true -toml = { version = "=0.8.9" } +toml.workspace = true proc-macro2.workspace = true syn.workspace = true Inflector = { version = "=0.11.4" } # Building files -quote.workspace = true foundry-config = { version = "=0.2.0" } -tempfile = { version = "3.9.0"} +tempfile = { version = "3.9.0" } # Errors thiserror.workspace = true @@ -76,5 +90,3 @@ lto = true # The Rust compiler splits your crate into multiple codegen units to parallelize (and thus speed up) compilation but at the cost of optimization. # This setting tells the compiler to use only one codegen unit, which will slow down compilation but improve optimization. codegen-units = 1 - - diff --git a/arbiter-engine/Cargo.toml b/arbiter-engine/Cargo.toml index 57d14be3..0059563a 100644 --- a/arbiter-engine/Cargo.toml +++ b/arbiter-engine/Cargo.toml @@ -2,7 +2,10 @@ name = "arbiter-engine" version = "0.1.0" edition = "2021" -authors = ["Waylon Jepsen ", "Colin Roberts "] +authors = [ + "Waylon Jepsen ", + "Colin Roberts ", +] description = "Allowing smart contract developers to do simulation driven development via an EVM emulator" license = "Apache-2.0" keywords = ["ethereum", "evm", "emulator", "testing", "smart-contracts"] @@ -12,6 +15,7 @@ homepage = "https://github.com/primitivefinance/arbiter" repository = "https://github.com/primitivefinance/arbiter" [dependencies] +arbiter-macros.workspace = true ethers.workspace = true futures-util.workspace = true async-trait.workspace = true @@ -19,14 +23,15 @@ serde_json.workspace = true serde.workspace = true tokio.workspace = true async-stream.workspace = true -anyhow = { version = "=1.0.79" } +anyhow = { version = "=1.0.79" } tracing.workspace = true -tokio-stream = "0.1.14" +tokio-stream = "0.1.14" futures = "0.3.30" crossbeam-channel.workspace = true arbiter-core.workspace = true arbiter-bindings.workspace = true thiserror.workspace = true +toml.workspace = true [dev-dependencies] arbiter-core.workspace = true diff --git a/arbiter-engine/src/agent.rs b/arbiter-engine/src/agent.rs index 7ad29c93..d7330a48 100644 --- a/arbiter-engine/src/agent.rs +++ b/arbiter-engine/src/agent.rs @@ -3,7 +3,7 @@ use std::{fmt::Debug, sync::Arc}; use arbiter_core::middleware::RevmMiddleware; -use serde::de::DeserializeOwned; +use serde::{de::DeserializeOwned, Serialize}; use thiserror::Error; use crate::{ @@ -50,7 +50,22 @@ impl Debug for Agent { } impl Agent { - /// Produces a minimal agent builder with the given identifier. + /// Creates a new [`AgentBuilder`] instance with a specified identifier. + /// + /// This method initializes an [`AgentBuilder`] with the provided `id` and + /// sets the `behavior_engines` field to `None`. The returned + /// [`AgentBuilder`] can be further configured using its methods before + /// finalizing the creation of an [`Agent`]. + /// + /// # Arguments + /// + /// * `id` - A string slice that holds the identifier for the agent being + /// built. + /// + /// # Returns + /// + /// Returns an [`AgentBuilder`] instance that can be used to configure and + /// build an [`Agent`]. pub fn builder(id: &str) -> AgentBuilder { AgentBuilder { id: id.to_owned(), @@ -73,9 +88,9 @@ pub struct AgentBuilder { impl AgentBuilder { /// Appends a behavior onto an [`AgentBuilder`]. Behaviors are initialized /// when the agent builder is added to the [`crate::world::World`] - pub fn with_behavior( + pub fn with_behavior( mut self, - behavior: impl Behavior + 'static, + behavior: impl Behavior + Serialize + DeserializeOwned + 'static, ) -> Self { let engine = Engine::new(behavior); if let Some(engines) = &mut self.behavior_engines { @@ -86,7 +101,66 @@ impl AgentBuilder { self } - /// Produces a new [`Agent`] with the given identifier. + /// Adds a state machine engine to the agent builder. + /// + /// This method allows for the addition of a custom state machine engine to + /// the agent's behavior engines. If the agent builder already has some + /// engines, the new engine is appended to the list. If no engines are + /// present, a new list is created with the provided engine as its first + /// element. + /// + /// # Parameters + /// + /// - `engine`: The state machine engine to be added to the agent builder. + /// This engine must + /// implement the `StateMachine` trait and is expected to be provided as a + /// boxed trait object to allow for dynamic dispatch. + /// + /// # Returns + /// + /// Returns the `AgentBuilder` instance to allow for method chaining. + pub fn with_engine(mut self, engine: Box) -> Self { + if let Some(engines) = &mut self.behavior_engines { + engines.push(engine); + } else { + self.behavior_engines = Some(vec![engine]); + }; + self + } + + /// Constructs and returns a new [`Agent`] instance using the provided + /// `client` and `messager`. + /// + /// This method finalizes the building process of an [`Agent`] by taking + /// ownership of the builder, and attempting to construct an `Agent` + /// with the accumulated configurations and the provided `client` and + /// `messager`. The `client` is an [`Arc`] that represents + /// the connection to the blockchain or environment, and `messager` is a + /// communication layer for the agent. + /// + /// # Parameters + /// + /// - `client`: A shared [`Arc`] instance that provides the + /// agent with access to the blockchain or environment. + /// - `messager`: A [`Messager`] instance for the agent to communicate with + /// other agents or systems. + /// + /// # Returns + /// + /// Returns a `Result` that, on success, contains the newly created + /// [`Agent`] instance. On failure, it returns an + /// [`AgentBuildError::MissingBehaviorEngines`] error indicating that the + /// agent was attempted to be built without any behavior engines + /// configured. + /// + /// # Examples + /// + /// ```ignore + /// let agent_builder = AgentBuilder::new("agent_id"); + /// let client = Arc::new(RevmMiddleware::new(...)); + /// let messager = Messager::new(...); + /// let agent = agent_builder.build(client, messager).expect("Failed to build agent"); + /// ``` pub fn build( self, client: Arc, diff --git a/arbiter-engine/src/examples/minter/agents/mod.rs b/arbiter-engine/src/examples/minter/agents/mod.rs index 2998593e..7311e561 100644 --- a/arbiter-engine/src/examples/minter/agents/mod.rs +++ b/arbiter-engine/src/examples/minter/agents/mod.rs @@ -1,3 +1,7 @@ use super::*; pub(crate) mod token_admin; pub(crate) mod token_requester; + +pub fn default_max_count() -> Option { + Some(5) +} diff --git a/arbiter-engine/src/examples/minter/agents/token_admin.rs b/arbiter-engine/src/examples/minter/agents/token_admin.rs index 300ae397..6507accf 100644 --- a/arbiter-engine/src/examples/minter/agents/token_admin.rs +++ b/arbiter-engine/src/examples/minter/agents/token_admin.rs @@ -1,15 +1,19 @@ use super::*; -#[derive(Debug)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub(crate) struct TokenAdmin { /// The identifier of the token admin. pub token_data: HashMap, + #[serde(skip)] pub tokens: Option>>, + #[serde(skip)] pub client: Option>, + #[serde(skip)] pub messager: Option, + #[serde(default)] pub count: u64, + #[serde(default = "default_max_count")] pub max_count: Option, - startup_message: Option, } impl TokenAdmin { @@ -21,7 +25,6 @@ impl TokenAdmin { messager: None, count: 0, max_count, - startup_message: None, } } diff --git a/arbiter-engine/src/examples/minter/agents/token_requester.rs b/arbiter-engine/src/examples/minter/agents/token_requester.rs index 10454717..5a3229e3 100644 --- a/arbiter-engine/src/examples/minter/agents/token_requester.rs +++ b/arbiter-engine/src/examples/minter/agents/token_requester.rs @@ -2,17 +2,21 @@ use super::*; /// The token requester is responsible for requesting tokens from the token /// admin. This agents is purely for testing purposes as far as I can tell. -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub(crate) struct TokenRequester { /// The tokens that the token requester has requested. pub token_data: TokenData, /// The agent ID to request tokens to. pub request_to: String, /// Client to have an address to receive token mint to and check balance + #[serde(skip)] pub client: Option>, /// The messaging layer for the token requester. + #[serde(skip)] pub messager: Option, + #[serde(default)] pub count: u64, + #[serde(default = "default_max_count")] pub max_count: Option, } diff --git a/arbiter-engine/src/examples/minter/behaviors/token_admin.rs b/arbiter-engine/src/examples/minter/behaviors/token_admin.rs index f7c09b37..59d02271 100644 --- a/arbiter-engine/src/examples/minter/behaviors/token_admin.rs +++ b/arbiter-engine/src/examples/minter/behaviors/token_admin.rs @@ -1,4 +1,4 @@ -use self::examples::minter::agents::token_admin::TokenAdmin; +use self::{examples::minter::agents::token_admin::TokenAdmin, machine::EventStream}; use super::*; /// Used as an action to ask what tokens are available. @@ -31,7 +31,7 @@ impl Behavior for TokenAdmin { &mut self, client: Arc, messager: Messager, - ) -> Pin + Send + Sync>> { + ) -> EventStream { self.messager = Some(messager.clone()); self.client = Some(client.clone()); for token_data in self.token_data.values_mut() { @@ -76,12 +76,9 @@ impl Behavior for TokenAdmin { token_name.clone() ); let token_data = self.token_data.get(&token_name).unwrap(); - let message = Message { - from: messager.id.clone().unwrap(), - to: To::Agent(event.from.clone()), // Reply back to sender - data: serde_json::to_string(&token_data.address).unwrap(), - }; - messager.send(message).await; + messager + .send(To::Agent(event.from.clone()), token_data.address) + .await; } TokenAdminQuery::MintRequest(mint_request) => { trace!("Minting tokens: {:?}", mint_request); diff --git a/arbiter-engine/src/examples/minter/behaviors/token_requester.rs b/arbiter-engine/src/examples/minter/behaviors/token_requester.rs index 6a125485..c662d396 100644 --- a/arbiter-engine/src/examples/minter/behaviors/token_requester.rs +++ b/arbiter-engine/src/examples/minter/behaviors/token_requester.rs @@ -2,7 +2,7 @@ use arbiter_bindings::bindings::arbiter_token::TransferFilter; use arbiter_core::data_collection::EventLogger; use token_admin::{MintRequest, TokenAdminQuery}; -use self::examples::minter::agents::token_requester::TokenRequester; +use self::{examples::minter::agents::token_requester::TokenRequester, machine::EventStream}; use super::*; #[async_trait::async_trait] @@ -12,31 +12,26 @@ impl Behavior for TokenRequester { &mut self, client: Arc, mut messager: Messager, - ) -> Pin + Send + Sync>> { - let message = Message { - from: messager.id.clone().unwrap(), - to: To::Agent(self.request_to.clone()), - data: serde_json::to_string(&TokenAdminQuery::AddressOf(self.token_data.name.clone())) - .unwrap(), - }; - messager.send(message).await; + ) -> EventStream { + messager + .send( + To::Agent(self.request_to.clone()), + &TokenAdminQuery::AddressOf(self.token_data.name.clone()), + ) + .await; let message = messager.get_next().await; let token_address = serde_json::from_str::
(&message.data).unwrap(); let token = ArbiterToken::new(token_address, client.clone()); self.token_data.address = Some(token_address); - let mint_data = serde_json::to_string(&TokenAdminQuery::MintRequest(MintRequest { + let mint_data = TokenAdminQuery::MintRequest(MintRequest { token: self.token_data.name.clone(), mint_to: client.address(), mint_amount: 1, - })) - .unwrap(); - let mint_request = Message { - from: messager.id.clone().unwrap(), - to: To::Agent(self.request_to.clone()), - data: mint_data, - }; - messager.send(mint_request).await; + }); + messager + .send(To::Agent(self.request_to.clone()), mint_data) + .await; self.messager = Some(messager.clone()); self.client = Some(client.clone()); @@ -55,17 +50,14 @@ impl Behavior for TokenRequester { let messager = self.messager.as_ref().unwrap(); while (self.count < self.max_count.unwrap()) { debug!("sending message from requester"); - let message = Message { - from: messager.id.clone().unwrap(), - to: To::Agent(self.request_to.clone()), - data: serde_json::to_string(&TokenAdminQuery::MintRequest(MintRequest { - token: self.token_data.name.clone(), - mint_to: self.client.as_ref().unwrap().address(), - mint_amount: 1, - })) - .unwrap(), - }; - messager.send(message).await; + let mint_data = TokenAdminQuery::MintRequest(MintRequest { + token: self.token_data.name.clone(), + mint_to: self.client.as_ref().unwrap().address(), + mint_amount: 1, + }); + messager + .send(To::Agent(self.request_to.clone()), mint_data) + .await; self.count += 1; } Some(MachineHalt) diff --git a/arbiter-engine/src/examples/minter/config.toml b/arbiter-engine/src/examples/minter/config.toml new file mode 100644 index 00000000..d2beefc3 --- /dev/null +++ b/arbiter-engine/src/examples/minter/config.toml @@ -0,0 +1,10 @@ +# top level id for the `TokenAdmin` agent +[[admin]] +# named struct and arguments for initializing the `TokenAdmin` agent +TokenAdmin = { max_count = 4, token_data = { "US Dollar Coin" = { name = "US Dollar Coin", symbol = "USDC", decimals = 18 } } } + + +# top level id for the `TokenRequester` agent +[[requester]] +# named struct and arguments for initializing the `TokenRequester` agent +TokenRequester = { max_count = 4, request_to = "admin", token_data = { name = "US Dollar Coin", symbol = "USDC", decimals = 18 } } diff --git a/arbiter-engine/src/examples/minter/mod.rs b/arbiter-engine/src/examples/minter/mod.rs index 428b0512..ba5ec2e3 100644 --- a/arbiter-engine/src/examples/minter/mod.rs +++ b/arbiter-engine/src/examples/minter/mod.rs @@ -1,13 +1,17 @@ use super::*; pub(crate) mod agents; pub(crate) mod behaviors; -pub(crate) mod token_minter; - -use std::pin::Pin; +use std::{pin::Pin, str::FromStr, time::Duration}; +use agents::{token_admin::TokenAdmin, token_requester::TokenRequester}; +use arbiter_core::data_collection::EventLogger; +use arbiter_macros::Behaviors; +use ethers::types::Address; use futures_util::Stream; +use tokio::time::timeout; use tracing::error; +use super::*; use crate::{ agent::Agent, machine::{Behavior, MachineHalt, MachineInstruction, StateMachine}, @@ -28,3 +32,68 @@ pub(crate) struct TokenData { pub decimals: u8, pub address: Option
, } + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn token_minter_simulation() { + let mut world = World::new("test_world"); + let client = RevmMiddleware::new(&world.environment, None).unwrap(); + + // Create the token admin agent + let token_admin = Agent::builder(TOKEN_ADMIN_ID); + let mut token_admin_behavior = TokenAdmin::new(Some(4)); + token_admin_behavior.add_token(TokenData { + name: TOKEN_NAME.to_owned(), + symbol: TOKEN_SYMBOL.to_owned(), + decimals: TOKEN_DECIMALS, + address: None, + }); + // Create the token requester agent + let token_requester = Agent::builder(REQUESTER_ID); + let mut token_requester_behavior = TokenRequester::new(Some(4)); + world.add_agent(token_requester.with_behavior(token_requester_behavior)); + + world.add_agent(token_admin.with_behavior(token_admin_behavior)); + + let arb = ArbiterToken::new( + Address::from_str("0x240a76d4c8a7dafc6286db5fa6b589e8b21fc00f").unwrap(), + client.clone(), + ); + let transfer_event = arb.transfer_filter(); + + let transfer_stream = EventLogger::builder() + .add_stream(arb.transfer_filter()) + .stream() + .unwrap(); + let mut stream = Box::pin(transfer_stream); + world.run().await; + let mut idx = 0; + + loop { + match timeout(Duration::from_secs(1), stream.next()).await { + Ok(Some(event)) => { + println!("Event received in outside world: {:?}", event); + idx += 1; + if idx == 4 { + break; + } + } + _ => { + panic!("Timeout reached. Test failed."); + } + } + } +} + +#[derive(Serialize, Deserialize, Debug, Behaviors)] +enum Behaviors { + TokenAdmin(TokenAdmin), + TokenRequester(TokenRequester), +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn config_test() { + let mut world = World::new("world"); + world.from_config::("src/examples/minter/config.toml"); + + world.run().await; +} diff --git a/arbiter-engine/src/examples/minter/token_minter.rs b/arbiter-engine/src/examples/minter/token_minter.rs deleted file mode 100644 index d1cd93c9..00000000 --- a/arbiter-engine/src/examples/minter/token_minter.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::{str::FromStr, time::Duration}; - -use agents::{token_admin::TokenAdmin, token_requester::TokenRequester}; -use arbiter_core::data_collection::EventLogger; -use ethers::types::Address; -use tokio::time::timeout; - -use super::*; -use crate::world::World; - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn token_minter_simulation() { - let mut world = World::new("test_world"); - let client = RevmMiddleware::new(&world.environment, None).unwrap(); - - // Create the token admin agent - let token_admin = Agent::builder(TOKEN_ADMIN_ID); - let mut token_admin_behavior = TokenAdmin::new(Some(4)); - token_admin_behavior.add_token(TokenData { - name: TOKEN_NAME.to_owned(), - symbol: TOKEN_SYMBOL.to_owned(), - decimals: TOKEN_DECIMALS, - address: None, - }); - // Create the token requester agent - let token_requester = Agent::builder(REQUESTER_ID); - let mut token_requester_behavior = TokenRequester::new(Some(4)); - world.add_agent(token_requester.with_behavior(token_requester_behavior)); - - world.add_agent(token_admin.with_behavior(token_admin_behavior)); - - let arb = ArbiterToken::new( - Address::from_str("0x240a76d4c8a7dafc6286db5fa6b589e8b21fc00f").unwrap(), - client.clone(), - ); - let transfer_event = arb.transfer_filter(); - - let transfer_stream = EventLogger::builder() - .add_stream(arb.transfer_filter()) - .stream() - .unwrap(); - let mut stream = Box::pin(transfer_stream); - world.run().await; - let mut idx = 0; - - loop { - match timeout(Duration::from_secs(1), stream.next()).await { - Ok(Some(event)) => { - println!("Event received in outside world: {:?}", event); - idx += 1; - if idx == 4 { - break; - } - } - _ => { - panic!("Timeout reached. Test failed."); - } - } - } -} diff --git a/arbiter-engine/src/examples/mod.rs b/arbiter-engine/src/examples/mod.rs index 07162b38..592e9e64 100644 --- a/arbiter-engine/src/examples/mod.rs +++ b/arbiter-engine/src/examples/mod.rs @@ -1,8 +1,6 @@ #![warn(missing_docs)] #![allow(unused)] - //! The examples module contains example strategies. - use std::{collections::HashMap, sync::Arc}; use arbiter_bindings::bindings::arbiter_token::ArbiterToken; @@ -11,6 +9,15 @@ use ethers::types::{transaction::eip2718::TypedTransaction, Address, Log, U256}; use futures_util::{stream, StreamExt}; use super::*; -use crate::messager::{Message, Messager}; +use crate::{ + agent::Agent, + machine::{ + Behavior, CreateStateMachine, Engine, EventStream, MachineHalt, State, StateMachine, + }, + messager::{Message, Messager, To}, + world::World, +}; +#[cfg(test)] pub(crate) mod minter; +#[cfg(test)] pub(crate) mod timed_message; diff --git a/arbiter-engine/src/examples/timed_message/config.toml b/arbiter-engine/src/examples/timed_message/config.toml new file mode 100644 index 00000000..cd82e936 --- /dev/null +++ b/arbiter-engine/src/examples/timed_message/config.toml @@ -0,0 +1,5 @@ +[[ping]] +TimedMessage = { delay = 1, send_data = "ping", receive_data = "pong", startup_message = "ping" } + +[[pong]] +TimedMessage = { delay = 1, send_data = "pong", receive_data = "ping" } diff --git a/arbiter-engine/src/examples/timed_message.rs b/arbiter-engine/src/examples/timed_message/mod.rs similarity index 77% rename from arbiter-engine/src/examples/timed_message.rs rename to arbiter-engine/src/examples/timed_message/mod.rs index 383b59c7..86da6522 100644 --- a/arbiter-engine/src/examples/timed_message.rs +++ b/arbiter-engine/src/examples/timed_message/mod.rs @@ -1,29 +1,31 @@ -#[cfg(test)] +use super::*; const AGENT_ID: &str = "agent"; use std::{pin::Pin, time::Duration}; +use arbiter_macros::Behaviors; use ethers::types::BigEndianHash; use futures_util::Stream; +use serde::*; use tokio::time::timeout; -use self::machine::MachineHalt; use super::*; -use crate::{ - agent::Agent, - machine::{Behavior, Engine, State, StateMachine}, - messager::To, - world::World, -}; - -#[derive(Debug)] + +fn default_max_count() -> Option { + Some(3) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct TimedMessage { delay: u64, receive_data: String, send_data: String, + #[serde(skip)] messager: Option, + #[serde(default)] count: u64, + #[serde(default = "default_max_count")] max_count: Option, startup_message: Option, } @@ -54,45 +56,24 @@ impl Behavior for TimedMessage { &mut self, _client: Arc, messager: Messager, - ) -> Pin + Send + Sync>> { - trace!("Starting up `TimedMessage`."); - self.messager = Some(messager.clone()); - tokio::time::sleep(std::time::Duration::from_secs(self.delay)).await; + ) -> EventStream { if let Some(startup_message) = &self.startup_message { - messager - .clone() - .send(Message { - from: messager.id.clone().unwrap(), - to: To::All, - data: startup_message.clone(), - }) - .await; + messager.send(To::All, startup_message).await; } - trace!("Started `TimedMessage`."); - return Box::pin(messager.stream()); + self.messager = Some(messager.clone()); + return messager.stream(); } async fn process(&mut self, event: Message) -> Option { - trace!("Processing event."); - let messager = self.messager.as_ref().unwrap(); - if event.data == self.receive_data { - trace!("Event matches message. Sending a new message."); - let message = Message { - from: messager.id.clone().unwrap(), - to: To::All, - data: self.send_data.clone(), - }; - messager.send(message).await; + if event.data == serde_json::to_string(&self.receive_data).unwrap() { + let messager = self.messager.clone().unwrap(); + messager.send(To::All, self.send_data.clone()).await; self.count += 1; } if self.count == self.max_count.unwrap_or(u64::MAX) { - warn!("Reached max count. Halting behavior."); return Some(MachineHalt); } - - tokio::time::sleep(std::time::Duration::from_secs(self.delay)).await; - trace!("Processed event."); - None + return None; } } @@ -145,6 +126,7 @@ async fn ping_pong() { Some("ping".to_owned()), ); let behavior_pong = TimedMessage::new(1, "ping".to_owned(), "pong".to_owned(), Some(2), None); + world.add_agent( agent .with_behavior(behavior_ping) @@ -213,3 +195,16 @@ async fn ping_pong_two_agent() { } } } + +#[derive(Serialize, Deserialize, Debug, Behaviors)] +enum Behaviors { + TimedMessage(TimedMessage), +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn config_test() { + let mut world = World::new("world"); + world.from_config::("src/examples/timed_message/config.toml"); + + world.run().await; +} diff --git a/arbiter-engine/src/machine.rs b/arbiter-engine/src/machine.rs index c0c246ac..99b5c63a 100644 --- a/arbiter-engine/src/machine.rs +++ b/arbiter-engine/src/machine.rs @@ -10,6 +10,17 @@ use serde::de::DeserializeOwned; use self::messager::Messager; use super::*; +/// A type alias for a pinned, boxed stream of events. +/// +/// This stream is capable of handling items of any type that implements the +/// `Stream` trait, and it is both sendable across threads and synchronizable +/// between threads. +/// +/// # Type Parameters +/// +/// * `E`: The type of the items in the stream. +pub type EventStream = Pin + Send + Sync>>; + /// The instructions that can be sent to a [`StateMachine`]. #[derive(Clone, Debug)] pub enum MachineInstruction { @@ -53,40 +64,95 @@ pub enum State { /// The [`Behavior`] trait is the lowest level functionality that will be used /// by a [`StateMachine`]. This constitutes what each state transition will do. #[async_trait::async_trait] -pub trait Behavior: Send + Sync + Debug + 'static { +pub trait Behavior: Serialize + DeserializeOwned + Send + Sync + Debug + 'static { /// Used to start the agent. /// This is where the agent can engage in its specific start up activities /// that it can do given the current state of the world. - async fn startup( - &mut self, - client: Arc, - messager: Messager, - ) -> Pin + Send + Sync>>; + async fn startup(&mut self, client: Arc, messager: Messager) -> EventStream; /// Used to process events. /// This is where the agent can engage in its specific processing /// of events that can lead to actions being taken. async fn process(&mut self, event: E) -> Option; } - +/// A trait for creating a state machine. +/// +/// This trait is intended to be implemented by types that can be converted into +/// a state machine. A state machine, in this context, is an entity capable of +/// executing a set of instructions or operations based on its current state and +/// inputs it receives. +/// +/// Implementers of this trait should provide the logic to initialize and return +/// a new instance of a state machine, encapsulated within a `Box`. This allows for dynamic dispatch to the state machine's +/// methods, enabling polymorphism where different types of state machines can +/// be used interchangeably at runtime. +/// +/// # Returns +/// +/// - `Box`: A boxed state machine object that can be +/// dynamically dispatched. +pub trait CreateStateMachine { + /// Creates and returns a new state machine instance. + /// + /// This method consumes the implementer and returns a new instance of a + /// state machine encapsulated within a `Box`. The + /// specific type of the state machine returned can vary, allowing for + /// flexibility and reuse of the state machine logic across + /// different contexts. + fn create_state_machine(self) -> Box; +} #[async_trait::async_trait] -pub(crate) trait StateMachine: Send + Sync + Debug + 'static { +/// A trait defining the capabilities of a state machine within the system. +/// +/// This trait is designed to be implemented by entities that can execute +/// instructions based on their current state and inputs they receive. The +/// execution of these instructions is asynchronous, allowing for non-blocking +/// operations within the state machine's logic. +/// +/// Implementers of this trait must be able to be sent across threads and shared +/// among threads safely, hence the `Send`, `Sync`, and `'static` bounds. They +/// should also support debugging through the `Debug` trait. +pub trait StateMachine: Send + Sync + Debug + 'static { + /// Executes a given instruction asynchronously. + /// + /// This method takes a mutable reference to self, allowing the state + /// machine to modify its state in response to the instruction. The + /// instruction to be executed is passed as an argument, encapsulating the + /// action to be performed by the state machine. + /// + /// # Parameters + /// + /// - `instruction`: The instruction that the state machine is to execute. + /// + /// # Returns + /// + /// This method does not return a value, but it may result in state changes + /// within the implementing type or the generation of further instructions + /// or events. async fn execute(&mut self, instruction: MachineInstruction); } -/// The idea of the [`Engine`] is that it drives the [`Behavior`] of a -/// [`StateMachine`]-based entity (like an [`agent::Agent`]). -/// The [`Engine`] specifically wraps a [`Behavior`] and a [`Receiver`] of -/// events into a cohesive unit that can listen to events and pass them onto the -/// processor stage. Since the [`Engine`] is also a [`StateMachine`], its -/// generics can be collapsed into a `dyn` trait object so that, for example, -/// [`agent::Agent`]s can own multiple [`Behavior`]s with different event `` -/// types. +/// The `Engine` struct represents the core logic unit of a state machine-based +/// entity, such as an agent. It encapsulates a behavior and manages the flow +/// of events to and from this behavior, effectively driving the entity's +/// response to external stimuli. +/// +/// The `Engine` is generic over a behavior type `B` and an event type `E`, +/// allowing it to be used with a wide variety of behaviors and event sources. +/// It is itself a state machine, capable of executing instructions that +/// manipulate its behavior or react to events. +/// +/// # Fields +/// +/// - `behavior`: An optional behavior that the engine is currently managing. +/// This is where the engine's logic is primarily executed in response to +/// events. pub struct Engine where B: Behavior, { - /// The behavior the [`Engine`] runs. + /// The behavior the `Engine` runs. pub behavior: Option, /// The current state of the [`Engine`]. @@ -95,7 +161,7 @@ where /// The receiver of events that the [`Engine`] will process. /// The [`State::Processing`] stage will attempt a decode of the [`String`]s /// into the event type ``. - event_stream: Option + Send + Sync>>>, + event_stream: Option>, phantom: std::marker::PhantomData, } @@ -132,8 +198,8 @@ where #[async_trait::async_trait] impl StateMachine for Engine where - B: Behavior + Debug, - E: DeserializeOwned + Send + Sync + Debug + 'static, + B: Behavior + Debug + Serialize + DeserializeOwned, + E: DeserializeOwned + Serialize + Send + Sync + Debug + 'static, { async fn execute(&mut self, instruction: MachineInstruction) { match instruction { @@ -143,7 +209,6 @@ where let mut behavior = self.behavior.take().unwrap(); let behavior_task = tokio::spawn(async move { let id = messager.id.clone(); - debug!("starting up stream for {:?}!", id); let stream = behavior.startup(client, messager).await; debug!("startup complete for {:?}!", id); (stream, behavior) @@ -155,7 +220,6 @@ where self.execute(MachineInstruction::Process).await; } MachineInstruction::Process => { - trace!("Behavior is processing."); let mut behavior = self.behavior.take().unwrap(); let mut stream = self.event_stream.take().unwrap(); let behavior_task = tokio::spawn(async move { diff --git a/arbiter-engine/src/messager.rs b/arbiter-engine/src/messager.rs index 15d95687..7f4032ac 100644 --- a/arbiter-engine/src/messager.rs +++ b/arbiter-engine/src/messager.rs @@ -4,9 +4,10 @@ // TODO: It might be nice to have some kind of messaging header so that we can // pipe messages to agents and pipe messages across worlds. -use futures_util::Stream; +use serde::Serialize; use tokio::sync::broadcast::{channel, Receiver, Sender}; +use self::machine::EventStream; use super::*; /// A message that can be sent between agents. @@ -99,9 +100,9 @@ impl Messager { /// Returns a stream of messages that are either sent to [`To::All`] or to /// the agent via [`To::Agent(id)`]. - pub fn stream(mut self) -> impl Stream + Send { + pub fn stream(mut self) -> EventStream { let mut receiver = self.broadcast_receiver.take().unwrap(); - async_stream::stream! { + Box::pin(async_stream::stream! { while let Ok(message) = receiver.recv().await { match &message.to { To::All => { @@ -116,12 +117,34 @@ impl Messager { } } } - } + }) } - - /// Sends a message to the messager. - pub async fn send(&self, message: Message) { + /// Asynchronously sends a message to a specified recipient. + /// + /// This method constructs a message with the provided data and sends it to + /// the specified recipient. The recipient can either be a single agent + /// or all agents, depending on the `to` parameter. The data is + /// serialized into a JSON string before being sent. + /// + /// # Type Parameters + /// + /// - `T`: The type that can be converted into a recipient specification + /// (`To`). + /// - `S`: The type of the data being sent. Must implement `Serialize`. + /// + /// # Parameters + /// + /// - `to`: The recipient of the message. Can be an individual agent's ID or + /// a broadcast to all agents. + /// - `data`: The data to be sent in the message. This data is serialized + /// into JSON format. + pub async fn send(&self, to: To, data: S) { trace!("Sending message via messager."); + let message = Message { + from: self.id.clone().unwrap(), + to, + data: serde_json::to_string(&data).unwrap(), + }; self.broadcast_sender.send(message).unwrap(); } } diff --git a/arbiter-engine/src/world.rs b/arbiter-engine/src/world.rs index 0cadd1cc..53981cdc 100644 --- a/arbiter-engine/src/world.rs +++ b/arbiter-engine/src/world.rs @@ -15,15 +15,16 @@ //! The world module contains the core world abstraction for the Arbiter Engine. -use std::collections::VecDeque; +use std::{collections::VecDeque, fmt::Debug}; use arbiter_core::{environment::Environment, middleware::RevmMiddleware}; use futures_util::future::join_all; +use serde::de::DeserializeOwned; use tokio::spawn; use self::{agent::AgentBuilder, machine::MachineInstruction}; use super::*; -use crate::{agent::Agent, messager::Messager}; +use crate::{agent::Agent, machine::CreateStateMachine, messager::Messager}; /// A world is a collection of agents that use the same type of provider, e.g., /// operate on the same blockchain or same `Environment`. The world is @@ -50,6 +51,7 @@ pub struct World { pub messager: Messager, } +use std::{fs::File, io::Read}; impl World { /// Creates a new [`World`] with the given identifier and provider. pub fn new(id: &str) -> Self { @@ -61,30 +63,149 @@ impl World { } } - /// Adds an agent to the world. + /// Builds and adds agents to the world from a configuration file. + /// + /// This method reads a configuration file specified by `config_path`, which + /// should be a TOML file containing the definitions of agents and their + /// behaviors. Each agent is identified by a unique string key, and + /// associated with a list of behaviors. These behaviors are + /// deserialized into instances that implement the `CreateStateMachine` + /// trait, allowing them to be converted into state machines that define + /// the agent's behavior within the world. + /// + /// # Type Parameters + /// + /// - `C`: The type of the behavior component that each agent will be + /// associated with. + /// This type must implement the `CreateStateMachine`, `Serialize`, + /// `DeserializeOwned`, and `Debug` traits. + /// + /// # Arguments + /// + /// - `config_path`: A string slice that holds the path to the configuration + /// file + /// relative to the current working directory. + /// + /// # Panics + /// + /// This method will panic if: + /// - The current working directory cannot be determined. + /// - The configuration file specified by `config_path` cannot be opened. + /// - The configuration file cannot be read into a string. + /// - The contents of the configuration file cannot be deserialized into the + /// expected + /// `HashMap>` format. + /// + /// # Examples + /// + /// Assuming a TOML file named `agents_config.toml` exists in the current + /// working directory with the following content: + /// + /// ```toml + /// [[agent1]] + /// BehaviorTypeA = { ... } , + /// [[agent1]] + /// BehaviorTypeB = { ... } + /// + /// [agent2] + /// BehaviorTypeC = { ... } + /// ``` + pub fn from_config( + &mut self, + config_path: &str, + ) { + let cwd = std::env::current_dir().expect("Failed to determine current working directory"); + let path = cwd.join(config_path); + let mut file = File::open(path).expect("Failed to open configuration file"); + + let mut contents = String::new(); + file.read_to_string(&mut contents) + .expect("Failed to read configuration file to string"); + + let agents_map: HashMap> = + toml::from_str(&contents).expect("Failed to deserialize configuration file"); + + for (agent, behaviors) in agents_map { + let mut next_agent = Agent::builder(&agent); + for behavior in behaviors { + let engine = behavior.create_state_machine(); + next_agent = next_agent.with_engine(engine); + } + self.add_agent(next_agent); + } + } + + /// Adds an agent, constructed from the provided `AgentBuilder`, to the + /// world. + /// + /// This method takes an `AgentBuilder` instance, extracts its identifier, + /// and uses it to create both a `RevmMiddleware` client and a + /// `Messager` specific to the agent. It then builds the `Agent` from + /// the `AgentBuilder` using these components. Finally, the newly + /// created `Agent` is inserted into the world's internal collection of + /// agents. + /// + /// # Panics + /// + /// This method will panic if: + /// - It fails to create a `RevmMiddleware` client for the agent. + /// - The `AgentBuilder` fails to build the `Agent`. + /// - The world's internal collection of agents is not initialized. + /// + /// # Examples + /// + /// Assuming you have an `AgentBuilder` instance named `agent_builder`: + /// + /// ```ignore + /// world.add_agent(agent_builder); + /// ``` + /// + /// This will add the agent defined by `agent_builder` to the world. pub fn add_agent(&mut self, agent_builder: AgentBuilder) { let id = agent_builder.id.clone(); - let client = RevmMiddleware::new(&self.environment, Some(&id)).unwrap(); + let client = RevmMiddleware::new(&self.environment, Some(&id)) + .expect("Failed to create RevmMiddleware client for agent"); let messager = self.messager.for_agent(&id); - let agent = agent_builder.build(client, messager).unwrap(); - let agents = self.agents.as_mut().unwrap(); + let agent = agent_builder + .build(client, messager) + .expect("Failed to build agent from AgentBuilder"); + let agents = self + .agents + .as_mut() + .expect("Agents collection not initialized"); agents.insert(id.to_owned(), agent); } - /// Runs all of the [`Agent`]s and their [`crate::machine::Behavior`]s in - /// the world in parallel. + /// Executes all agents and their behaviors concurrently within the world. + /// + /// This method takes all the agents registered in the world and runs their + /// associated behaviors in parallel. Each agent's behaviors are + /// executed with their respective messaging and client context. This + /// method ensures that all agents and their behaviors are started + /// simultaneously, leveraging asynchronous execution to manage concurrent + /// operations. + /// + /// # Errors + /// + /// Returns an error if no agents are found in the world, possibly + /// indicating that the world has already been run or that no agents + /// were added prior to execution. pub async fn run(&mut self) -> Result<()> { let mut tasks = vec![]; + // Retrieve the agents, erroring if none are found. let agents = self .agents .take() .ok_or_else(|| anyhow!("No agents found! Has the world already been run?"))?; + // Prepare a queue for messagers corresponding to each behavior engine. let mut messagers = VecDeque::new(); + // Populate the messagers queue. for (_, agent) in agents.iter() { for _ in &agent.behavior_engines { messagers.push_back(agent.messager.clone()); } } + // For each agent, spawn a task for each of its behavior engines. for (_, mut agent) in agents { for mut engine in agent.behavior_engines.drain(..) { let client = agent.client.clone(); @@ -96,6 +217,7 @@ impl World { })); } } + // Await the completion of all tasks. join_all(tasks).await; Ok(()) } diff --git a/arbiter-macros/Cargo.toml b/arbiter-macros/Cargo.toml new file mode 100644 index 00000000..8ea92a88 --- /dev/null +++ b/arbiter-macros/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "arbiter-macros" +version = "0.1.0" + +[lib] +proc-macro = true + +[dependencies] +syn.workspace = true +quote = "1.0.35" diff --git a/arbiter-macros/src/lib.rs b/arbiter-macros/src/lib.rs new file mode 100644 index 00000000..061335bf --- /dev/null +++ b/arbiter-macros/src/lib.rs @@ -0,0 +1,49 @@ +extern crate proc_macro; +extern crate quote; +extern crate syn; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Fields}; + +#[proc_macro_derive(Behaviors)] +pub fn create_behavior_from_enum(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = input.ident; // The name of the enum + + let enum_data = if let Data::Enum(DataEnum { variants, .. }) = input.data { + variants + } else { + // Not an enum, so panic or handle as needed + panic!("CreateBehaviorFromEnum is only defined for enums"); + }; + + let match_arms = enum_data.into_iter().map(|variant| { + let variant_name = variant.ident; + let _inner_type = if let Fields::Unnamed(fields) = variant.fields { + fields.unnamed.first().unwrap().ty.clone() + } else { + panic!("Expected unnamed fields in enum variant"); + }; + + quote! { + #name::#variant_name(inner) => { + Box::new(Engine::new(inner)) + } + } + }); + + let expanded = quote! { + + impl CreateStateMachine for #name { + fn create_state_machine(self) -> Box { + match self { + #(#match_arms,)* + } + } + } + }; + + TokenStream::from(expanded) +}