diff --git a/Cargo.toml b/Cargo.toml index 4557ee5..0d87400 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = [ "lmm", "lmm-derive", - "lmm-agent", + "lmm-agent" ] exclude = [ "target", diff --git a/README.md b/README.md index 8d00029..9bf9902 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![LMM](https://wiseai.dev/assets/logo.png)](https://wiseai.dev) [![Work In Progress](https://img.shields.io/badge/Work%20In%20Progress-orange)](https://github.com/wiseaidotdev/lmm) +[![ASI](https://img.shields.io/badge/ASI-10.71%25-brown)](https://arcprize.org/replay/8471c865-4c54-40c5-a523-dcaa681aa4f1) [![Crates.io](https://img.shields.io/crates/v/lmm.svg)](https://crates.io/crates/lmm) [![Docs.rs](https://docs.rs/lmm/badge.svg)](https://docs.rs/lmm) [![Crates.io Downloads](https://img.shields.io/crates/d/lmm)](https://crates.io/crates/lmm) diff --git a/examples/arc-lmm-agent/Cargo.lock b/examples/arc-lmm-agent/Cargo.lock new file mode 100644 index 0000000..5f1c2d4 --- /dev/null +++ b/examples/arc-lmm-agent/Cargo.lock @@ -0,0 +1,2924 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-agi-rs" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "reqwest", + "serde", + "serde_json", + "urlencoding", + "uuid", +] + +[[package]] +name = "arc-lmm-agent" +version = "0.1.0" +dependencies = [ + "anyhow", + "arc-agi-rs", + "clap", + "indicatif", + "lmm-agent", + "owo-colors", + "rand 0.8.6", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lmm" +version = "0.2.6" +dependencies = [ + "anyhow", + "getrandom 0.4.2", + "phf", + "rand 0.10.1", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "lmm-agent" +version = "0.1.1" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "futures", + "lmm", + "lmm-derive", + "phf", + "rand 0.10.1", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "lmm-derive" +version = "0.0.2" +dependencies = [ + "async-trait", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustls" +version = "0.23.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/arc-lmm-agent/Cargo.toml b/examples/arc-lmm-agent/Cargo.toml new file mode 100644 index 0000000..e66d4cf --- /dev/null +++ b/examples/arc-lmm-agent/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "arc-lmm-agent" +version = "0.1.0" +edition = "2024" +authors = ["Mahmoud Harmouch "] +license = "MIT" +description = "ARC-AGI game agent powered by lmm equation-based intelligence." +repository = "https://github.com/wiseaidotdev/lmm" +homepage = "https://wiseai.dev" +keywords = ["arc-agi", "agent", "lmm", "ai", "games"] +categories = ["science", "games"] + +[[bin]] +name = "arc-lmm-agent" +path = "src/main.rs" + +[dependencies] +lmm-agent = { path = "../../lmm-agent" } +arc-agi-rs = { version = "0.1.0" } +tokio = { version = "1.52.1", features = ["full"] } +anyhow = "1.0.102" +thiserror = "2.0.18" +serde_json = "1.0.149" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } +clap = { version = "4.6.1", features = ["derive"] } +rand = "0.8.5" +indicatif = "0.18" +owo-colors = "4" diff --git a/examples/arc-lmm-agent/README.md b/examples/arc-lmm-agent/README.md new file mode 100644 index 0000000..49d98db --- /dev/null +++ b/examples/arc-lmm-agent/README.md @@ -0,0 +1,105 @@ +
+ +# 🕹️ arc-lmm-agent + +[![ASI](https://img.shields.io/badge/ASI-10.71%25-brown)](https://arcprize.org/replay/8471c865-4c54-40c5-a523-dcaa681aa4f1) +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE) + +[![ls20-arc-lmm.gif](./assets/ls20-arc-lmm.gif)](https://arcprize.org/replay/8471c865-4c54-40c5-a523-dcaa681aa4f1) + +> `arc-lmm-agent` is an autonomous navigation solver for ARC-AGI interactive environments (`ls20` game atm). It uses an episodic framework, progressive strategy learning, and robust world modeling to dynamically maneuver through complex grids, interact with rotation modifiers, systematically collect step-boosters, and reach the target zones across escalating levels. + +
+ +## 🤔 Core Challenges Solved + +The `ls20` environment is an intricate, partially observable continuous-exploration puzzle requiring multiple stages of logical sequential progression within constrained action budgets: + +1. **Fog of War**: The grid is only discovered as the agent moves. False walls and passages must be robustly classified. +1. **Sequential Configuration Objectives**: The final target zone cannot be successfully entered until the agent's avatar matches the exact expected rotation scheme and shape footprint. This is achieved by first locating and interacting with isolated `+` rotation modifiers, then deliberately combining step boosters to formulate the right geometric structure. +1. **Budget Exhaustion**: Random walks immediately fail. The agent must memorize prior paths, actively backtrack through explored terrain, and utilize A* routing where possible to minimize wasted step limits. + +## 👷🏻‍♀️ Agent Architecture + +`arc-lmm-agent` overcomes these constraints by bridging standard graph theory with the deeper `lmm-agent` cognitive stack (Motivation drives, Semantic Knowledge Index, tabular Q-Learning). + +```mermaid +flowchart TD + subgraph Engine["Runner (Event Loop)"] + direction TB + STEP["client.step()"] --> STATE["FrameContext (State Parsing)"] + STATE --> POLICY_DECIDE["policy.decide()"] + end + + subgraph Perception["Cognitive Perception"] + HASH["ui_hash()\nDetects Rotation Modifiers (+)"] + BONUS["bonus_positions()\nDetects Step Boosters (*)"] + TARGET["target_pos()\nGoal Box Tracking"] + end + + STATE --> Perception + Perception --> POLICY_DECIDE + + subgraph Memory["LMM Cognitive Stack"] + DRIVE["InternalDrive\nCuriosity & Incoherence Signals"] + INDEX["KnowledgeIndex\nCross-Level Strategy Transfer"] + HELM["LearningEngine (HELM)\nQ-Table & Reward Shaping"] + WMAP["WorldMapGraph\nwalls, passages, milestones"] + end + + POLICY_DECIDE --> Memory + + subgraph DecisionForest["Tiered Routing Policy"] + direction TB + ESCAPE["1. Stuck-Escape Trigger (Frontier Edge break)"] + PLAN["2. Sequence execution (BFS/A*/Backtrack)"] + ROUTE["3. Route to Modifiers / Uncollected Bonuses"] + TARGET["4. March to Locked Final Target"] + BFS_MOD["5. Global Milestone BFS Rescue"] + NOVELTY["6. Outward Novelty Exploration & Q-Learning"] + + ESCAPE --> PLAN --> ROUTE --> TARGET --> BFS_MOD --> NOVELTY + end + + POLICY_DECIDE --> DecisionForest + DecisionForest --> ACTION["Action (1=UP, 2=DOWN, 3=LEFT, 4=RIGHT)"] + ACTION --> STEP +``` + +## 🧠 Generalized Tiered Navigation + +The agent employs a pure routing dispatcher (`LmmPolicy::decide()`). At each step, it drops through a prioritized list of strategies, taking the first valid action it finds: + +### 1. Stuck-Escape Protocol +If the agent detects heavy oscillation (re-visiting the same grid coordinates repeatedly without discovering new terrain), it bypasses naive A* and fires a BFS to target the nearest globally un-visited grid coordinate or frontier edge, effectively "breaking" local optima loops. + +### 2. Strategic Routing (Modifiers -> Boosters -> Target) +The agent inherently learns an ordered priority sequence based on what it perceives in the current grid: +- **Modifier Discovery**: Before anything else, the agent seeks out the `+` modifier. +- **Boosters/Treats Collection**: If the modifier has been activated, the agent immediately pivot to acquiring any known step-boosters (yellow treats). +- **Backtracking**: The agent employs a tactical backtracking queue. After picking up a booster, the agent *reverses the path it took from the modifier*, ensuring it safely retraces known, cleared passageways rather than risking new dead-ends. When necessary, it intentionally crosses the modifier a second time to configure the target shape. +- **Final Assault**: Once all visible bonuses are collected, the agent locks onto target coordinates and deploys a ruthless march straight into the goal. + +### 3. Progressive BFS & Milestone Memories +Every time the agent identifies a modifier or starts a new level, it marks the exact state hash as a **Milestone**. If the agent is entirely lost, it can drop into a rescue fallback that BFS routes directly to these known milestones across the entire level `WorldMap`. + +### 4. Novelty Exploration +When all else fails (no plan, no known targets, nothing visible on radar), the agent relies on raw exploration: +- Sorts neighbors by how many times they have been globally visited. +- Seeks out absolute "novel" states. +- Applies a fallback to the `LearningEngine` (Q-Table recommendation) to guess the most historically profitable direction based on reinforcement gradients. + +## 🛠 `lmm-agent` Core Integrations + +The solver natively utilizes the overarching `lmm-agent` architecture for generalized intelligence logic: + +1. **`InternalDrive`**: The agent fires intrinsic reward/motivation signals. If the agent finds a new bonus position or discovers a completely unvisited tile, the `Curiosity` drive spikes. If the agent bumps into a newly discovered wall and loses a turn, the `Incoherence` drive registers the penalty, adjusting future behavioral tolerances. +2. **`KnowledgeIndex` (Cross-Level Transfer)**: As the agent completes `Level N`, it synthesizes the trial's metadata into narrative English (e.g. *"Level 0 completed after 1 mod interactions and 0 bonuses... "*). This raw text is dynamically ingested into the localized `KnowledgeIndex`. When `Level N+1` begins, this long-term semantic memory primes the agent about the nature of the puzzles it will likely encounter. +3. **`LearningEngine` (HELM)**: Traditional tabular Q-learning shapes underlying values. The agent emits a continuous localized Bellman reward stream (+10 for activating a modifier, +50 for moving closer to the target post-modifier, -1.0 for wall collisions) to fine-tune the `NOVELTY` fallback recommendations. + + +## 🕹️ Run the agent + +```sh +cargo run --release -- --base-url "https://three.arcprize.org" --api-key "your-api-key" +``` diff --git a/examples/arc-lmm-agent/assets/ls20-arc-lmm.gif b/examples/arc-lmm-agent/assets/ls20-arc-lmm.gif new file mode 100644 index 0000000..e5054f0 Binary files /dev/null and b/examples/arc-lmm-agent/assets/ls20-arc-lmm.gif differ diff --git a/examples/arc-lmm-agent/src/config.rs b/examples/arc-lmm-agent/src/config.rs new file mode 100644 index 0000000..999b54d --- /dev/null +++ b/examples/arc-lmm-agent/src/config.rs @@ -0,0 +1,61 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! # `AgentConfig` - CLI and runtime configuration. +//! +//! Parsed from command-line arguments via `clap`. The same struct can be +//! constructed programmatically (e.g. in tests) using [`AgentConfig::default`]. + +use clap::Parser; + +/// Command-line configuration for the ARC-LMM agent. +/// +/// Every flag has a sensible default so the agent can be run with zero +/// arguments against a local ARC-AGI server. +#[derive(Debug, Clone, Parser)] +#[command( + name = "arc-lmm-agent", + about = "ARC-AGI game agent powered by lmm equation-based intelligence." +)] +pub struct AgentConfig { + /// ARC-AGI game identifier to play. + #[arg(long, default_value = "ls20")] + pub game_id: String, + + /// Base URL of the ARC-AGI server. + #[arg(long, default_value = "http://localhost:8001")] + pub base_url: String, + + /// API key for the ARC-AGI server. Leave empty for local offline mode. + #[arg(long, default_value = "")] + pub api_key: String, + + /// Maximum number of actions the agent may take per trial. + #[arg(long, default_value_t = 500)] + pub max_actions: usize, + + /// Number of trials (server resets) the agent is allowed. + #[arg(long, default_value_t = 1)] + pub max_trials: usize, + + /// Comma-separated scorecard tags submitted with the run. + #[arg(long, default_value = "agent,lmm", value_delimiter = ',')] + pub tags: Vec, +} + +impl Default for AgentConfig { + fn default() -> Self { + Self { + game_id: "ls20".to_string(), + base_url: "http://localhost:8001".to_string(), + api_key: String::new(), + max_actions: 500, + max_trials: 1, + tags: vec!["agent".to_string(), "lmm".to_string()], + } + } +} diff --git a/examples/arc-lmm-agent/src/display.rs b/examples/arc-lmm-agent/src/display.rs new file mode 100644 index 0000000..c38f53b --- /dev/null +++ b/examples/arc-lmm-agent/src/display.rs @@ -0,0 +1,402 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! # `display` – production-ready terminal output for arc-lmm-agent. +//! +//! Provides structured, coloured, human-readable prints that surface the agent's +//! inner cognitive mechanics. All output goes to **stderr** so it does not +//! interfere with structured logs or stdout redirection. + +use indicatif::{ProgressBar, ProgressStyle}; +use owo_colors::OwoColorize; +use std::time::Duration; + +const HEADER_WIDTH: usize = 70; + +/// Creates a transient spinner that disappears when [`Spinner::finish`] is called. +/// +/// Use for operations with uncertain duration (ThinkLoop, network calls). +pub struct Spinner(ProgressBar); + +impl Spinner { + pub fn new(msg: &str) -> Self { + let pb = ProgressBar::new_spinner(); + pb.enable_steady_tick(Duration::from_millis(80)); + pb.set_style( + ProgressStyle::with_template("{spinner:.cyan} {msg}") + .unwrap() + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]), + ); + pb.set_message(msg.to_string()); + Self(pb) + } + + /// Clears the spinner and prints a completion line. + pub fn finish_ok(self, msg: &str) { + self.0.finish_and_clear(); + eprintln!(" {} {}", "✔".green().bold(), msg.dimmed()); + } + + /// Clears the spinner silently. + pub fn finish_silent(self) { + self.0.finish_and_clear(); + } +} + +/// Prints a top-level banner, e.g. at game start. +pub fn print_banner(game_id: &str, base_url: &str, max_actions: usize) { + let title = format!( + " ARC-LMM-Agent · game={} · url={} · budget={} ", + game_id, base_url, max_actions + ); + let width = title.len().max(HEADER_WIDTH); + let border = "═".repeat(width); + eprintln!("\n╔{}╗", border.cyan()); + eprintln!("║{} ║", title.bold().cyan()); + eprintln!("╚{}╝\n", border.cyan()); +} + +/// Prints a section divider for a new trial. +pub fn print_trial_start(trial: usize, epsilon: f64, q_states: usize) { + let label = format!(" Trial #{trial} ε={epsilon:.3} Q-states={q_states} "); + let dashes = "─".repeat(HEADER_WIDTH.saturating_sub(label.len()).max(2)); + eprintln!( + "\n{}{}{}", + "┌─".yellow(), + label.yellow().bold(), + dashes.yellow() + ); +} + +/// Prints a trial-end summary line. +pub fn print_trial_end(trial: usize, steps: usize, epsilon: f64, states: usize) { + eprintln!( + "{} trial={} steps={} ε={:.3} states={}", + "└─ End".yellow(), + trial.yellow().bold(), + steps.bright_white(), + epsilon, + states + ); +} + +/// Prints the compact per-step state digest. +#[allow(clippy::too_many_arguments)] +pub fn print_step_state( + trial: usize, + step: usize, + pos: (i32, i32), + level: u32, + win_levels: u32, + trial_visits: u32, + global_visits: u32, + global_states: usize, + walls: usize, + passages: usize, + milestones: usize, + ui_mods: usize, + plan_len: usize, +) { + eprintln!( + "{} {} pos={} lvl={}/{} tv={} gv={} Σ={} walls={} pass={} milestones={} mods={} plan={}", + format!("[t{trial}·s{step}]").bright_cyan().bold(), + "▸".dimmed(), + format!("({},{})", pos.0, pos.1).bright_white(), + level.bright_green(), + win_levels.dimmed(), + trial_visits.bright_yellow(), + global_visits.dimmed(), + global_states, + walls.bright_red(), + passages.green(), + milestones, + ui_mods.magenta(), + if plan_len > 0 { + format!("{plan_len}").bright_blue().to_string() + } else { + "0".dimmed().to_string() + }, + ); +} + +/// Prints action direction chosen. +pub fn print_action(action: u32, mode: &str) { + let dir = match action { + 1 => "↑ UP", + 2 => "↓ DN", + 3 => "← LT", + 4 => "→ RT", + _ => "· NOP", + }; + eprintln!( + " {} {} {}", + "→".bright_white().bold(), + dir.bright_white().bold(), + format!("[{mode}]").dimmed(), + ); +} + +/// Prints the current plan execution status. +pub fn print_plan_step(action: u32, remaining: usize) { + let dir = match action { + 1 => "↑", + 2 => "↓", + 3 => "←", + 4 => "→", + _ => "·", + }; + eprintln!( + " {} {} {}", + "📋".dimmed(), + format!("PLAN {dir}").bright_blue(), + format!("({remaining} remaining)").dimmed() + ); +} + +/// Prints a plan being loaded. +pub fn print_plan_loaded(label: &str, steps: usize) { + eprintln!( + " {} {} {}", + "📌".dimmed(), + format!("Plan loaded: {label}").bright_blue().bold(), + format!("({steps} steps)").dimmed() + ); +} + +/// Prints a routing target decision. +pub fn print_routing(label: &str, target: (usize, usize), path_len: usize, action: u32) { + let dir = match action { + 1 => "↑", + 2 => "↓", + 3 => "←", + 4 => "→", + _ => "·", + }; + eprintln!( + " {} {} {} {} {}", + "🧭".dimmed(), + format!("Route→{label}").bright_magenta(), + format!("target=({},{})", target.0, target.1).dimmed(), + format!("path={path_len}").dimmed(), + dir.to_string().bright_white().bold(), + ); +} + +/// Prints a stuck-escape event. +pub fn print_stuck(mode: &str, action: u32, escape_count: u32) { + eprintln!( + " {} {} {} {}", + "⚠".bright_yellow().bold(), + "STUCK".bright_yellow().bold(), + format!("[{mode}]").bright_yellow(), + format!("action={action} count={escape_count}").dimmed() + ); +} + +/// Prints a modifier (plus sign) reaching event. +pub fn print_modifier_reached(pos: (usize, usize), visit_num: u32) { + eprintln!( + " {} {} {} {}", + "✚".bright_green().bold(), + "MODIFIER ACTIVATED".bright_green().bold(), + format!("at ({},{})", pos.0, pos.1).bright_white(), + format!("(visit #{visit_num})").dimmed() + ); +} + +/// Prints a newly discovered bonus (step booster). +pub fn print_bonus_found(pos: (usize, usize)) { + eprintln!( + " {} {} {}", + "⭐".yellow(), + "Booster found".yellow().bold(), + format!("at ({},{})", pos.0, pos.1).bright_white() + ); +} + +/// Prints a bonus collection event. +pub fn print_bonus_collected(pos: (usize, usize), remaining: usize) { + eprintln!( + " {} {} {} {}", + "🍬".dimmed(), + "Booster collected".bright_yellow().bold(), + format!("({},{})", pos.0, pos.1).bright_white(), + format!("- {remaining} remaining").dimmed() + ); +} + +/// Prints a backtrack event. +pub fn print_backtrack(steps: usize) { + eprintln!( + " {} {} {}", + "↩".bright_cyan().bold(), + "Backtracking to modifier".bright_cyan().bold(), + format!("({steps} steps)").dimmed() + ); +} + +/// Prints the second modifier pass status. +pub fn print_second_mod_pass(complete: bool) { + if complete { + eprintln!( + " {} {}", + "✚✚".bright_green().bold(), + "Second modifier pass complete".bright_green() + ); + } else { + eprintln!( + " {} {}", + "✚".bright_green(), + "Second modifier pass - routing to modifier".dimmed() + ); + } +} + +/// Prints when all bonuses are consumed and agent marches to target. +pub fn print_all_bonuses_consumed() { + eprintln!( + " {} {}", + "🏁".dimmed(), + "All boosters collected - marching to final target ↓" + .bright_green() + .bold() + ); +} + +/// Prints when the final target is locked. +pub fn print_target_locked(pos: (usize, usize)) { + eprintln!( + " {} {} {}", + "🔒".dimmed(), + "Final target locked".bright_magenta().bold(), + format!("at ({},{})", pos.0, pos.1).bright_white() + ); +} + +/// Prints a level completion / progressive learning event. +pub fn print_level_advance(from: u32, to: u32, mod_visits: u32, bonuses: usize) { + eprintln!( + "\n {} {} {}", + "🎉".dimmed(), + format!("Level {from} → {to}").bright_green().bold(), + format!("[mod_visits={mod_visits} bonuses_collected={bonuses}]").dimmed() + ); +} + +/// Prints a milestone recording event. +pub fn print_milestone(trial: usize, step: usize, levels: u32, state: u64) { + eprintln!( + " {} {}", + "📍".dimmed(), + format!("Milestone trial={trial} step={step} lvl={levels} state={state:x}").dimmed() + ); +} + +/// Prints a dominant internal drive signal when its magnitude crosses the threshold. +pub fn print_drive(name: &str, magnitude: f64) { + let bar_len = (magnitude * 10.0) as usize; + let bar: String = "█".repeat(bar_len) + &"░".repeat(10 - bar_len); + eprintln!( + " {} {} [{}] {:.2}", + "💡".dimmed(), + format!("Drive:{name}").bright_magenta(), + bar.magenta(), + magnitude + ); +} + +/// Returns a spinner to display while the ThinkLoop is running. +pub fn think_spinner() -> Spinner { + Spinner::new("🧠 Cognitive ThinkLoop ...") +} + +/// Prints the end-of-run summary box. +pub fn print_run_summary( + game_id: &str, + final_state: &str, + levels_completed: u32, + win_levels: u32, + steps_taken: usize, + card_id: &str, +) { + let w = HEADER_WIDTH; + eprintln!("\n{}", "─".repeat(w).cyan()); + eprintln!( + "{:^width$}", + "Run Complete".bold().bright_white(), + width = w + ); + eprintln!("{}", "─".repeat(w).cyan()); + eprintln!(" {:20} {}", "Game ID".dimmed(), game_id.bright_white()); + eprintln!( + " {:20} {}", + "Final State".dimmed(), + final_state.bright_green().bold() + ); + eprintln!( + " {:20} {}", + "Levels Completed".dimmed(), + format!("{levels_completed} / {win_levels}").bright_green() + ); + eprintln!( + " {:20} {}", + "Steps Taken".dimmed(), + steps_taken.to_string().bright_yellow() + ); + eprintln!(" {:20} {}", "Scorecard ID".dimmed(), card_id.bright_cyan()); + eprintln!("{}\n", "─".repeat(w).cyan()); +} + +/// Printed once per step in the runner: choice + server confirmation. +#[allow(clippy::too_many_arguments)] +pub fn print_step_summary( + trial: usize, + step: usize, + total: usize, + action_id: u32, + epsilon: f64, + q_states: usize, + state: &str, + levels: u32, + win_levels: u32, + server_action: u32, +) { + let dir = match action_id { + 1 => "↑", + 2 => "↓", + 3 => "←", + 4 => "→", + _ => "·", + }; + let ok = if action_id == server_action { + "✔".green().to_string() + } else { + "≠".yellow().to_string() + }; + eprintln!( + " {} t{}·s{} tot={} {dir} {} ε={:.3} Q={} lvl={}/{} state={}", + ok, + trial, + step, + total, + format!("[{action_id}→{server_action}]").dimmed(), + epsilon, + q_states, + levels, + win_levels, + state.dimmed() + ); +} + +/// Plan invalidated notification. +pub fn print_plan_invalidated() { + eprintln!( + " {} {}", + "✖".bright_red(), + "Plan invalidated (wall/unavailable)".dimmed() + ); +} diff --git a/examples/arc-lmm-agent/src/error.rs b/examples/arc-lmm-agent/src/error.rs new file mode 100644 index 0000000..c18b165 --- /dev/null +++ b/examples/arc-lmm-agent/src/error.rs @@ -0,0 +1,32 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! # `ArcAgentError` - unified error type. +//! +//! All fallible operations in `arc-lmm-agent` return this error via `?`. + +use thiserror::Error; + +/// The error type for all fallible operations in the ARC-LMM agent. +#[derive(Debug, Error)] +pub enum ArcAgentError { + /// An error originating from the ARC-AGI HTTP client. + #[error("ARC-AGI client error: {0}")] + Client(#[from] arc_agi_rs::ArcAgiError), + + /// An error propagated from agent reasoning or knowledge ingestion. + #[error("Agent reasoning error: {0}")] + Reasoning(#[from] anyhow::Error), + + /// The available action list was empty and no action could be chosen. + #[error("No valid action available in frame")] + NoValidAction, + + /// The server session ended without reaching a terminal game state. + #[error("Game session ended unexpectedly without a terminal state")] + UnexpectedSessionEnd, +} diff --git a/examples/arc-lmm-agent/src/frame.rs b/examples/arc-lmm-agent/src/frame.rs new file mode 100644 index 0000000..9563879 --- /dev/null +++ b/examples/arc-lmm-agent/src/frame.rs @@ -0,0 +1,361 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! # `FrameContext` - per-frame game-state parsing. +//! +//! Wraps the raw [`FrameData`] returned by the ARC-AGI server and exposes +//! higher-level accessors that the policy uses to make decisions: +//! player position, modifier position, bonus positions, target position, and +//! stable image-hash state keys. + +use arc_agi_rs::models::{FrameData, GameState}; +use std::collections::HashSet; + +/// The context of a single frame of the game, providing utility methods for state analysis and entity detection. +pub struct FrameContext<'a> { + pub inner: &'a FrameData, +} + +impl<'a> FrameContext<'a> { + /// Creates a new frame context from the provided frame data. + pub fn new(frame: &'a FrameData) -> Self { + Self { inner: frame } + } + + /// Determines whether the current frame is a terminal frame (Win or GameOver). + pub fn is_terminal(&self) -> bool { + matches!(self.inner.state, GameState::Win | GameState::GameOver) + } + + /// Calculates the grid dimensions dynamically. + pub fn grid_dims(&self) -> (usize, usize) { + let grid = self.grid_values(); + let rows = grid.len(); + let cols = grid.first().map_or(0, |r| r.len()); + (rows, cols) + } + + /// Generates a uniquely identifiable state key hash for the agent position, excluding the varying bottom ui strip. + pub fn state_key(&self) -> u64 { + let grid = self.grid_values(); + let (rows, _) = self.grid_dims(); + let safe_rows = rows.saturating_sub(10); + let mut combined = format!("L{}|", self.inner.levels_completed); + for row in grid.iter().take(safe_rows) { + for &v in row { + combined.push((v as u8 + b'0') as char); + } + } + fnv1a_hash(&combined) + } + + /// Locates the top-left coordinate of the player block, characterized by a specific pixel value of 12. + pub fn player_pos(&self) -> Option<(usize, usize)> { + let grid = self.grid_values(); + let (rows, cols) = self.grid_dims(); + for row in 0..rows { + for col in 0..cols { + if grid + .get(row) + .and_then(|r| r.get(col)) + .copied() + .unwrap_or(-1) + == 12 + { + return Some((col, row)); + } + } + } + None + } + + /// Generates a hash representing the bottom piece-display area exclusively used for detecting rotation/modification. + pub fn ui_hash(&self) -> Option { + let piece = self.bottom_left_piece()?; + let mut s = String::with_capacity(128); + for row in piece { + for v in row { + s.push((v.max(0) as u8 + b'0') as char); + } + } + Some(fnv1a_hash(&s)) + } + + /// Extracts the matrix layout of the player piece displayed in the bottom-left corner of the UI. + pub fn bottom_left_piece(&self) -> Option>> { + self.player_pos()?; + let grid = self.grid_values(); + let (rows, _) = self.grid_dims(); + let display_start_row = rows.saturating_sub(10); + let mut piece = Vec::new(); + for row in grid.iter().skip(display_start_row).take(7) { + let row_slice = row.iter().take(12).copied().collect(); + piece.push(row_slice); + } + Some(piece) + } + + /// Extracts the matrix layout of the final target destination piece displayed in the bottom-right corner of the UI. + pub fn target_piece(&self) -> Option>> { + let grid = self.grid_values(); + let (rows, cols) = self.grid_dims(); + let display_start_row = rows.saturating_sub(10); + let target_col_start = cols.saturating_sub(16).max(12); + let mut piece = Vec::new(); + for row in grid.iter().skip(display_start_row).take(7) { + let row_slice = row + .iter() + .skip(target_col_start) + .take(12) + .copied() + .collect(); + piece.push(row_slice); + } + Some(piece) + } + + /// Determines if the current dynamic bottom-left piece identically matches the rotation and shape of the destination target piece. + pub fn player_piece_matches_target(&self) -> bool { + match (self.bottom_left_piece(), self.target_piece()) { + (Some(bl), Some(tr)) => { + if bl.iter().flatten().all(|&v| v <= 0) { + return false; + } + bl == tr + } + _ => false, + } + } + + /// Detects the target pixel coordinate of the modifier cell matching a cross-shaped pattern of any active block color. + #[allow(clippy::needless_range_loop)] + pub fn modifier_pos(&self) -> Option<(usize, usize)> { + let grid = self.grid_values(); + let (rows, cols) = self.grid_dims(); + if rows < 3 || cols < 3 { + return None; + } + + let mut clusters: Vec> = Vec::new(); + for r in 1..rows.saturating_sub(10) { + for c in 1..cols.saturating_sub(1) { + let v = grid[r][c]; + if v > 0 + && v != 3 + && v != 4 + && v != 5 + && v != 8 + && v != 9 + && v != 11 + && v != 12 + && v != 14 + { + let mut added = false; + for cluster in &mut clusters { + if cluster + .iter() + .any(|&(cr, cc)| cr.abs_diff(r) < 8 && cc.abs_diff(c) < 8) + { + cluster.push((r, c)); + added = true; + break; + } + } + if !added { + clusters.push(vec![(r, c)]); + } + } + } + } // ignore bottom UI row + + for cluster in clusters { + let min_r = cluster.iter().map(|&(r, _)| r).min().unwrap(); + let max_r = cluster.iter().map(|&(r, _)| r).max().unwrap(); + let min_c = cluster.iter().map(|&(_, c)| c).min().unwrap(); + let max_c = cluster.iter().map(|&(_, c)| c).max().unwrap(); + + if max_r.abs_diff(min_r) < 6 && max_c.abs_diff(min_c) < 6 { + let sum_r: usize = cluster.iter().map(|&(r, _)| r).sum(); + let sum_c: usize = cluster.iter().map(|&(_, c)| c).sum(); + return Some((sum_c / cluster.len(), sum_r / cluster.len())); + } + } + None + } + + /// Identifies the internal top-left coordinate of the destination target box matching a uniform value 3 border. + pub fn target_pos(&self) -> Option<(usize, usize)> { + let grid = self.grid_values(); + let (rows, cols) = self.grid_dims(); + for row in 0..rows { + for col in 0..cols { + if grid + .get(row) + .and_then(|r| r.get(col)) + .copied() + .unwrap_or(-1) + == 3 + && grid + .get(row) + .and_then(|r| r.get(col + 1)) + .copied() + .unwrap_or(-1) + == 3 + { + return Some((col + 2, row + 2)); + } + } + } + None + } + + /// Detects and returns all known collectible bonus positions in the grid matching known border signatures. + pub fn bonus_positions(&self) -> Vec<(usize, usize)> { + let grid = self.grid_values(); + let (rows, cols) = self.grid_dims(); + let mut found = Vec::new(); + let mut tagged: HashSet<(usize, usize)> = Default::default(); + for row in 0..rows { + for col in 0..cols { + if tagged.contains(&(col, row)) { + continue; + } + let v = grid + .get(row) + .and_then(|r| r.get(col)) + .copied() + .unwrap_or(-1); + if v == 14 { + let horiz = (col..col + 3) + .filter(|&c| { + grid.get(row).and_then(|r| r.get(c)).copied().unwrap_or(-1) == 14 + }) + .count(); + let vert = (row..row + 3) + .filter(|&r| { + grid.get(r) + .and_then(|rr| rr.get(col)) + .copied() + .unwrap_or(-1) + == 14 + }) + .count(); + if horiz >= 3 && vert >= 3 { + found.push((col + 5, row + 5)); + tagged.insert((col, row)); + continue; + } + } + if v == 11 && col + 2 < cols && row + 2 < rows { + let top_row = (col..col + 3) + .filter(|&c| { + grid.get(row).and_then(|r| r.get(c)).copied().unwrap_or(-1) == 11 + }) + .count(); + let bot_row = (col..col + 3) + .filter(|&c| { + grid.get(row + 2) + .and_then(|r| r.get(c)) + .copied() + .unwrap_or(-1) + == 11 + }) + .count(); + let left_col = grid + .get(row + 1) + .and_then(|r| r.get(col)) + .copied() + .unwrap_or(-1) + == 11; + let right_col = grid + .get(row + 1) + .and_then(|r| r.get(col + 2)) + .copied() + .unwrap_or(-1) + == 11; + let center = grid + .get(row + 1) + .and_then(|r| r.get(col + 1)) + .copied() + .unwrap_or(-1); + if top_row == 3 && bot_row == 3 && left_col && right_col && center != 11 { + found.push((col + 1, row + 1)); + for dy in 0..3 { + for dx in 0..3 { + tagged.insert((col + dx, row + dy)); + } + } + } + } + } + } + found + } + + /// Provides a list of viable action integer states avoiding the 0 reset state. + pub fn available_non_reset(&self) -> Vec { + self.inner + .available_actions + .iter() + .copied() + .filter(|&a| a != 0) + .collect() + } + + /// Generates a standardized textual description of the frame observables for reasoning contexts. + pub fn encode_observation(&self) -> String { + let pos = self + .player_pos() + .map(|(x, y)| format!("x={} y={}", x, y)) + .unwrap_or_else(|| "pos=unknown".into()); + format!( + "game={} state={} levels={} win={} {} actions=[{}]", + self.inner.game_id, + self.inner.state.as_str(), + self.inner.levels_completed, + self.inner.win_levels, + pos, + self.inner + .available_actions + .iter() + .map(|a| a.to_string()) + .collect::>() + .join(","), + ) + } + + /// Projects the hierarchical JSON layer frame representation into a straightforward two dimensional array of integers. + pub fn grid_values(&self) -> Vec> { + self.inner + .frame + .first() + .and_then(|layer| layer.as_array()) + .map(|rows| { + rows.iter() + .map(|row| { + row.as_array() + .map(|cols| cols.iter().map(|v| v.as_i64().unwrap_or(-1)).collect()) + .unwrap_or_default() + }) + .collect() + }) + .unwrap_or_default() + } +} + +/// Computes equality strictly based upon the internal frame representation structures. +pub fn frames_equal(a: &FrameData, b: &FrameData) -> bool { + a.frame == b.frame +} + +/// Applies a 64-bit FNV-1a non-cryptographic hash mapping to a standard string. +fn fnv1a_hash(s: &str) -> u64 { + const BASIS: u64 = 0xcbf29ce484222325; + const PRIME: u64 = 0x100000001b3; + s.bytes() + .fold(BASIS, |hash, byte| (hash ^ byte as u64).wrapping_mul(PRIME)) +} diff --git a/examples/arc-lmm-agent/src/lib.rs b/examples/arc-lmm-agent/src/lib.rs new file mode 100644 index 0000000..eff8f27 --- /dev/null +++ b/examples/arc-lmm-agent/src/lib.rs @@ -0,0 +1,32 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! # `arc-lmm-agent` +//! +//! An autonomous ARC-AGI navigation solver backed by equation-based intelligence +//! from the `lmm` framework. +//! +//! ## Module layout +//! +//! | Module | Purpose | +//! |--------|---------| +//! | [`config`] | CLI and runtime configuration | +//! | [`error`] | Unified error type | +//! | [`frame`] | Game-frame parsing and entity detection | +//! | [`world`] | Learned spatial model (`WorldMap`) | +//! | [`tools`] | Stateless pathfinding algorithms (`PathfindingTool`) | +//! | [`policy`] | Tiered navigation policy (`LmmPolicy`) | +//! | [`runner`] | Game-loop orchestrator (`ArcGameRunner`) | + +pub mod config; +pub mod display; +pub mod error; +pub mod frame; +pub mod policy; +pub mod runner; +pub mod tools; +pub mod world; diff --git a/examples/arc-lmm-agent/src/main.rs b/examples/arc-lmm-agent/src/main.rs new file mode 100644 index 0000000..77fa109 --- /dev/null +++ b/examples/arc-lmm-agent/src/main.rs @@ -0,0 +1,58 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use clap::Parser; +use tracing::info; +use tracing_subscriber::EnvFilter; + +use arc_lmm_agent::config::AgentConfig; +use arc_lmm_agent::display; +use arc_lmm_agent::runner::ArcGameRunner; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into())) + .with_target(false) + .compact() + .init(); + + let config = AgentConfig::parse(); + + info!( + game_id = %config.game_id, + base_url = %config.base_url, + max_actions = config.max_actions, + "Starting ARC-LMM-Agent" + ); + + display::print_banner(&config.game_id, &config.base_url, config.max_actions); + + let mut runner = ArcGameRunner::new(config)?; + let summary = runner.run().await?; + + info!( + game_id = %summary.game_id, + final_state = %summary.final_state, + levels_completed = summary.levels_completed, + win_levels = summary.win_levels, + steps_taken = summary.steps_taken, + card_id = %summary.card_id, + "Run complete" + ); + + display::print_run_summary( + &summary.game_id, + summary.final_state.as_str(), + summary.levels_completed, + summary.win_levels, + summary.steps_taken, + &summary.card_id, + ); + + Ok(()) +} diff --git a/examples/arc-lmm-agent/src/policy.rs b/examples/arc-lmm-agent/src/policy.rs new file mode 100644 index 0000000..9c6d1d7 --- /dev/null +++ b/examples/arc-lmm-agent/src/policy.rs @@ -0,0 +1,1367 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! # `LmmPolicy` - learned navigation policy for ARC-AGI. +//! +//! `LmmPolicy` is the central decision-making component. Its [`decide`] method +//! is called once per game frame and returns the next action integer. +//! +//! ## Behavioral hierarchy (in priority order) +//! +//! 1. **Stuck escape** - triggered when `trial_visits` for the current state +//! exceeds [`STUCK_THRESHOLD`]. Bypassed when a modifier has been reached so that +//! target routing is never interrupted by stuck logic. +//! 2. **Follow active plan** - execute the next step of an already-computed +//! BFS / A\* plan held in `plan`. +//! 3. **Route to active target** - determined by what the agent knows: +//! - *No modifier reached*: route to known modifier position. +//! - *Modifier reached, bonuses remain*: route to nearest uncollected bonus; +//! after collecting the first, **backtrack through the recorded outbound +//! path** before routing to subsequent bonuses. +//! - *All bonuses collected or none exist*: route to final target. +//! 4. **Milestone BFS** - use the global `WorldMap` to BFS back to a known +//! milestone (modifier, level-start) when no spatial route is available. +//! 5. **Novelty exploration** - expand the map frontier, preferring globally +//! unvisited neighbours; falls back to a minimum-visit greedy walk. +//! +//! ## Learning integration +//! +//! - [`LearningEngine`] (HELM / Q-table) records per-step rewards and is +//! consulted in the exploration fallback via `recommend_action`. +//! - [`KnowledgeIndex`] stores cross-level strategic knowledge (modifier and +//! bonus positions, target coordinates) as ingested text, making discoveries +//! from level N available when level N+1 begins. +//! - [`InternalDrive`] emits curiosity signals when novel states or bonuses are +//! found, and incoherence signals when stuck. + +use std::collections::{HashMap, HashSet, VecDeque}; + +use crate::display; +use crate::error::ArcAgentError; +use crate::frame::FrameContext; +use crate::tools::PathfindingTool; +use crate::world::WorldMap; +use anyhow::Context; +use lmm_agent::agent::LmmAgent; +use lmm_agent::cognition::drive::DriveSignal; +use lmm_agent::cognition::knowledge::KnowledgeIndex; +use lmm_agent::cognition::knowledge::KnowledgeSource; +use lmm_agent::cognition::learning::config::LearningConfig; +use lmm_agent::cognition::learning::engine::LearningEngine; +use lmm_agent::cognition::learning::q_table::ActionKey; +use lmm_agent::cognition::memory::ColdStore; +use lmm_agent::cognition::signal::CognitionSignal; +use lmm_agent::types::Message; +use rand::Rng; +use rand::seq::SliceRandom; +use std::iter::repeat_n; +use tracing::info; + +/// Threshold of per-trial visits to the same state before stuck-escape logic fires. +const STUCK_THRESHOLD: u32 = 12; + +/// Maps a raw game action integer to the [`ActionKey`] variant used by the Q-table. +fn action_to_key(action: u32) -> ActionKey { + match action { + 1 => ActionKey::Expand, + 2 => ActionKey::Narrow, + 3 => ActionKey::Pivot, + 4 => ActionKey::Broaden, + _ => ActionKey::Repeat, + } +} + +/// Builds a minimal [`CognitionSignal`] carrying only the reward value. +/// +/// `step` and the placeholder observation strings are required by the signal +/// type but are not used during Q-table updates in this agent. +fn reward_signal(step: usize, reward: f64) -> CognitionSignal { + let mut s = CognitionSignal::new(step, "g".into(), "g".into(), 1.0, 0.0); + s.reward = reward; + s +} + +/// The learned navigation policy for an ARC-AGI agent. +/// +/// `LmmPolicy` orchestrates the full cognitive stack: world-model updates, +/// reward shaping, drive signals, Q-table learning, and tiered action selection. +/// All search is delegated to [`PathfindingTool`]; raw map data lives in [`WorldMap`]. +/// +/// Construct with [`LmmPolicy::new`]. +#[derive(Debug)] +pub struct LmmPolicy { + /// LMM agent facade - holds hot memory, LTM, knowledge index, and internal drive. + agent: LmmAgent, + + /// HELM learning engine - tabular Q-learning over discrete state hashes. + engine: LearningEngine, + + /// Learned spatial model of the current level. + world: WorldMap, + + /// Completed level count from the previous frame (used to detect level transitions). + prev_levels: u32, + + /// State hash of the previous frame. + prev_state_key: Option, + + /// Q-table action key chosen in the previous frame. + prev_action_key: Option, + + /// Raw integer action sent in the previous frame. + prev_raw_action: Option, + + /// UI hash from the previous frame (used to detect modifier activation). + prev_ui_hash: Option, + + /// Player pixel position from the previous frame. + prev_player_pos: Option<(usize, usize)>, + + /// How many steps have elapsed within the current trial. + step: usize, + + /// How many complete trials have elapsed in this level. + trial: usize, + + /// Planned action sequence to execute before re-evaluating. + plan: VecDeque, + + /// Visit counts per state for the current trial (reset each trial). + trial_visits: HashMap, + + /// Consecutive-stuck counts per state (used to escalate escape strategy). + stuck_counts: HashMap, + + /// Milestones already reached in the current trial. + milestones_reached_this_trial: HashSet, + + /// Global visit counts across all trials and levels. + global_visits: HashMap, + + /// Level index at which each milestone was first recorded. + milestone_levels: HashMap, + + /// Pixel positions of all known modifier (+) cells. + known_modifiers: HashSet<(usize, usize)>, + + /// Whether the modifier has been activated in the current trial. + local_modifier_reached: bool, + + /// Zero-indexed level the agent is currently navigating. + current_level_idx: u32, + + /// All known collectible bonus (yellow) positions found during exploration. + known_bonuses: Vec<(usize, usize)>, + + /// How many modifiers have been activated in the current trial. + trial_mod_visits: u32, + + /// Bonus positions collected in the current trial. + trial_bonuses_consumed: HashSet<(usize, usize)>, + + /// Observed maximum step budgets per level index. + level_step_limits: HashMap, + + /// Actions recorded from modifier activation to first bonus collection. + /// Reversed to produce the backtrack plan when subsequent bonuses must be reached. + outbound_path_to_first_bonus: Vec, + + /// Whether the agent is currently executing a return-path after claiming the first bonus. + backtracking_from_first_bonus: bool, + + /// Set after backtracking completes: forces one more modifier visit before routing to second bonus. + needs_second_modifier_pass: bool, + + /// Step at which the modifier was first activated in the current trial. + modifier_reached_step: Option, + + /// Locked final target position (set once all bonuses are consumed, never changed until level reset). + locked_final_target: Option<(usize, usize)>, +} + +impl LmmPolicy { + /// Constructs a fresh [`LmmPolicy`] with default HELM hyper-parameters. + /// + /// The embedded [`LmmAgent`] is initialised with the ARC-AGI solver persona + /// and an empty knowledge index ready to receive cross-level strategic facts. + pub fn new() -> Self { + let learning_config = LearningConfig::builder() + .alpha(0.35) + .gamma(0.95) + .epsilon(1.0) + .epsilon_decay(0.3) + .epsilon_min(0.05) + .build(); + + Self { + agent: LmmAgent::builder() + .persona("ARC-AGI Solver") + .behavior( + "Navigate the grid. Activate modifier cells. Collect step boosters. \ + Reach the target zone.", + ) + .learning_engine(LearningEngine::new(learning_config.clone())) + .build(), + engine: LearningEngine::new(learning_config), + world: WorldMap::new(), + prev_levels: 0, + prev_state_key: None, + prev_action_key: None, + prev_raw_action: None, + prev_ui_hash: None, + prev_player_pos: None, + step: 0, + trial: 0, + plan: VecDeque::new(), + trial_visits: HashMap::new(), + stuck_counts: HashMap::new(), + milestones_reached_this_trial: HashSet::new(), + global_visits: HashMap::new(), + milestone_levels: HashMap::new(), + known_modifiers: HashSet::new(), + local_modifier_reached: false, + current_level_idx: 0, + known_bonuses: Vec::new(), + trial_mod_visits: 0, + trial_bonuses_consumed: HashSet::new(), + level_step_limits: HashMap::new(), + outbound_path_to_first_bonus: Vec::new(), + backtracking_from_first_bonus: false, + needs_second_modifier_pass: false, + modifier_reached_step: None, + locked_final_target: None, + } + } + + /// Records the maximum step budget for the current level after observing a reset. + /// + /// Persists the constraint to the agent's long-term memory so it can be + /// referenced in subsequent trials. + /// + /// # Time complexity: O(1) + /// # Space complexity: O(1) + pub fn record_level_step_limit(&mut self, limit: usize) { + self.level_step_limits.insert(self.current_level_idx, limit); + self.agent.add_ltm_message(Message::new( + "level_limit", + format!( + "Level {} max steps learned: {}", + self.current_level_idx, limit + ), + )); + } + + /// Main frame decision entry-point. + /// + /// Updates the world model, emits HELM reward signals, runs the ThinkLoop + /// for cognitive reflection, then delegates to [`choose`] for action selection. + /// + /// Returns the chosen action integer (`1`=up, `2`=down, `3`=left, `4`=right, + /// `0`=no-op when no valid action is available). + /// + /// # Time complexity: O(V + E) amortised - dominated by ThinkLoop overhead + /// # Space complexity: O(S) where S = known states + pub async fn decide( + &mut self, + context: &FrameContext<'_>, + _prev_frame: Option<&arc_agi_rs::models::FrameData>, + ) -> Result { + let current_state = context.state_key(); + + if self.step == 0 && self.current_level_idx == 0 && self.plan.is_empty() { + self.plan = vec![3, 3, 3].into(); + } + + let moved = self.prev_state_key != Some(current_state); + + self.update_world_model(context, current_state, moved); + if let Some(strategy) = self.handle_level_transition(context, current_state) { + let _ = self.agent.ingest(KnowledgeSource::RawText(strategy)).await; + } + self.update_known_bonuses(context); + self.update_modifier_detection(context); + + let ui_changed_for_reward = self.detect_ui_change(context); + self.emit_reward(context, current_state, moved, ui_changed_for_reward); + self.emit_drive_signals(context, moved); + + let observation = context.encode_observation(); + let _ = self + .agent + .think_with(&observation, 2, 0.5, 1.0, 0.05) + .await + .context("ThinkLoop")?; + + let available = context.available_non_reset(); + if available.is_empty() { + self.step += 1; + return Ok(0); + } + + *self.trial_visits.entry(current_state).or_insert(0) += 1; + *self.global_visits.entry(current_state).or_insert(0) += 1; + + self.check_bonus_proximity(context); + + let (_, map_walls, map_passages) = self.world.stats(); + let (px, py) = context + .player_pos() + .map(|(x, y)| (x as i32, y as i32)) + .unwrap_or((-1, -1)); + display::print_step_state( + self.trial, + self.step, + (px, py), + context.inner.levels_completed, + context.inner.win_levels, + self.trial_visits.get(¤t_state).copied().unwrap_or(0), + self.global_visits.get(¤t_state).copied().unwrap_or(0), + self.global_visits.len(), + map_walls, + map_passages, + self.world.milestones.len(), + self.known_modifiers.len(), + self.plan.len(), + ); + + let chosen = self.choose(current_state, &available, context); + display::print_action(chosen, "chosen"); + + if self.local_modifier_reached + && self.trial_bonuses_consumed.is_empty() + && !self.backtracking_from_first_bonus + { + self.outbound_path_to_first_bonus.push(chosen); + } + + self.prev_levels = context.inner.levels_completed; + self.prev_state_key = Some(current_state); + self.prev_action_key = Some(action_to_key(chosen)); + self.prev_raw_action = Some(chosen); + self.prev_player_pos = context.player_pos(); + self.step += 1; + Ok(chosen) + } + + /// Updates the `WorldMap` with passage or wall data from the previous action. + /// + /// # Time complexity: O(1) amortised + fn update_world_model(&mut self, context: &FrameContext<'_>, current_state: u64, moved: bool) { + if self.step == 0 { + return; + } + if let (Some(prev_sk), Some(prev_ga)) = (self.prev_state_key, self.prev_raw_action) { + if moved { + self.world.record_passage(prev_sk, prev_ga, current_state); + let reverse = PathfindingTool::reverse_action(prev_ga); + self.world + .record_reverse_passage(prev_sk, prev_ga, current_state, reverse); + if let Some(pos) = context.player_pos() { + self.world.state_positions.insert(current_state, pos); + } + } else { + self.world.record_wall(prev_sk, prev_ga); + self.agent.internal_drive.record_incoherence(0.5); + self.plan.clear(); + } + } + } + + /// Resets level-scoped state when the agent advances to a new level. + /// + /// Persists learned strategy text as the return value so the async caller + /// (`decide`) can hand it to `agent.ingest()` without blocking. + /// + /// Returns `Some(strategy_text)` on a level transition, `None` otherwise. + /// + /// # Time complexity: O(S) where S = known states + fn handle_level_transition( + &mut self, + context: &FrameContext<'_>, + current_state: u64, + ) -> Option { + if context.inner.levels_completed <= self.prev_levels { + return None; + } + + let strategy_text = format!( + "Level {} completed after {} mod interactions and {} bonus collections. \ + Modifier positions: {:?}. Bonus positions: {:?}.", + self.prev_levels, + self.trial_mod_visits, + self.trial_bonuses_consumed.len(), + self.known_modifiers.iter().collect::>(), + self.known_bonuses, + ); + self.agent + .add_ltm_message(Message::new("learned_strategy", strategy_text.clone())); + + display::print_level_advance( + self.prev_levels, + context.inner.levels_completed, + self.trial_mod_visits, + self.trial_bonuses_consumed.len(), + ); + + self.world.clear(); + self.known_modifiers.clear(); + self.global_visits.clear(); + self.trial_visits.clear(); + self.milestones_reached_this_trial.clear(); + self.local_modifier_reached = false; + self.prev_ui_hash = None; + self.plan.clear(); + self.known_bonuses.clear(); + self.current_level_idx = context.inner.levels_completed; + self.engine.reset_epsilon(1.0); + self.trial = 0; + self.step = 0; + self.trial_mod_visits = 0; + self.trial_bonuses_consumed.clear(); + self.outbound_path_to_first_bonus.clear(); + self.backtracking_from_first_bonus = false; + self.needs_second_modifier_pass = false; + self.locked_final_target = None; + self.modifier_reached_step = None; + + self.world.record_milestone(current_state); + self.milestone_levels + .insert(current_state, context.inner.levels_completed); + self.milestones_reached_this_trial.insert(current_state); + self.agent.add_ltm_message(Message::new( + "milestone", + format!( + "trial={} step={} state={:x} levels={}", + self.trial, self.step, current_state, context.inner.levels_completed + ), + )); + display::print_milestone( + self.trial, + self.step, + context.inner.levels_completed, + current_state, + ); + + Some(strategy_text) + } + + /// Scans the current frame for bonus positions not yet in `known_bonuses`. + /// + /// # Time complexity: O(B * K) where B = grid bonuses, K = known bonuses count + fn update_known_bonuses(&mut self, context: &FrameContext<'_>) { + for bonus in context.bonus_positions() { + if !self.known_bonuses.contains(&bonus) { + display::print_bonus_found(bonus); + self.known_bonuses.push(bonus); + self.agent.internal_drive.record_residual(0.8); + } + } + } + + /// Detects modifier activation via spatial proximity or piece-match heuristic. + /// + /// Sets `local_modifier_reached` and records the modifier position in + /// `known_modifiers` on the first detection per trial. + /// + /// # Time complexity: O(1) + fn update_modifier_detection(&mut self, context: &FrameContext<'_>) { + if self.local_modifier_reached { + return; + } + let mut activated = false; + let mut activated_pos: Option<(usize, usize)> = None; + + if let Some(modifier_pos) = context.modifier_pos() + && let Some(player_pos) = context.player_pos() + { + let dx = player_pos.0.abs_diff(modifier_pos.0); + let dy = player_pos.1.abs_diff(modifier_pos.1); + if dx <= 7 && dy <= 7 { + activated = true; + activated_pos = Some(modifier_pos); + } + } + if context.player_piece_matches_target() && self.step > 0 { + activated = true; + activated_pos = activated_pos.or(context.player_pos()); + } + + if activated { + self.local_modifier_reached = true; + self.modifier_reached_step = Some(self.step); + self.plan.clear(); + self.outbound_path_to_first_bonus.clear(); + self.backtracking_from_first_bonus = false; + self.needs_second_modifier_pass = false; + self.locked_final_target = None; + + if let Some(pos) = activated_pos { + self.known_modifiers.insert(pos); + self.trial_mod_visits += 1; + display::print_modifier_reached(pos, self.trial_mod_visits); + self.agent.add_ltm_message(Message::new( + "modifier_reached", + format!( + "Modifier activated at {:?} on trial={} step={}", + pos, self.trial, self.step + ), + )); + self.agent.internal_drive.record_residual(1.0); + } + } + + if let Some(ui_hash) = context.ui_hash() { + self.prev_ui_hash = Some(ui_hash); + } + } + + /// Returns `true` when the UI hash changed between this frame and the previous one. + /// + /// A UI change indicates the player piece was rotated by a modifier cell. + /// + /// # Time complexity: O(1) + fn detect_ui_change(&self, context: &FrameContext<'_>) -> bool { + match (self.prev_ui_hash, context.ui_hash()) { + (Some(prev), Some(now)) => prev != now, + _ => false, + } + } + + /// Records a HELM reward signal for the previous action. + /// + /// # Time complexity: O(1) + fn emit_reward( + &mut self, + context: &FrameContext<'_>, + current_state: u64, + moved: bool, + ui_changed: bool, + ) { + if let (Some(prev_sk), Some(prev_ak)) = (self.prev_state_key, self.prev_action_key) { + let r = self.compute_reward(context, moved, ui_changed); + self.engine.record_step( + &reward_signal(self.step, r), + prev_sk, + prev_ak, + current_state, + ); + } + } + + /// Fires `InternalDrive` signals appropriate to the agent's current situation. + /// + /// - **Curiosity** when the agent moves to a globally new state. + /// - **Incoherence** when the agent is blocked (did not move on the last action). + /// + /// # Time complexity: O(1) + fn emit_drive_signals(&mut self, context: &FrameContext<'_>, moved: bool) { + if moved { + if let Some(state) = self.prev_state_key { + let visits = self.global_visits.get(&state).copied().unwrap_or(0); + if visits == 0 { + self.agent.internal_drive.record_residual(0.9); + } + } + } else { + self.agent.internal_drive.record_incoherence(0.3); + } + let drive_state = self.agent.internal_drive.tick(); + if let Some(dominant) = drive_state.dominant_drive() + && dominant.magnitude() > 0.5 + { + let signal_name = dominant.name(); + let magnitude = dominant.magnitude(); + display::print_drive(signal_name, magnitude); + if matches!(dominant, DriveSignal::Curiosity(_)) { + let _ = context; + } + } + } + + /// Checks whether the player is adjacent to any known bonus and marks it consumed. + /// + /// When the first bonus is consumed, the recorded `outbound_path_to_first_bonus` + /// is reversed and loaded into `plan` if more bonuses remain, enabling the agent + /// to backtrack safely through already-explored territory. + /// + /// # Time complexity: O(K) where K = known bonuses + fn check_bonus_proximity(&mut self, context: &FrameContext<'_>) { + let Some((px, py)) = context.player_pos() else { + return; + }; + + let newly_consumed: Vec<(usize, usize)> = self + .known_bonuses + .iter() + .filter(|&&(bx, by)| { + (px + 5).abs_diff(bx) < 5 + && (py + 5).abs_diff(by) < 5 + && !self.trial_bonuses_consumed.contains(&(bx, by)) + }) + .copied() + .collect(); + + for bonus in newly_consumed { + self.trial_bonuses_consumed.insert(bonus); + self.agent.add_ltm_message(Message::new( + "booster_knowledge", + format!( + "Collected bonus at {:?} on trial={} step={}. Steps extended.", + bonus, self.trial, self.step + ), + )); + let remaining_after = self + .known_bonuses + .iter() + .filter(|b| !self.trial_bonuses_consumed.contains(*b)) + .count(); + display::print_bonus_collected(bonus, remaining_after); + + let remaining_bonuses = self + .known_bonuses + .iter() + .filter(|b| !self.trial_bonuses_consumed.contains(b)) + .count(); + + if remaining_bonuses == 0 { + self.plan = repeat_n(2u32, 50).collect(); + self.locked_final_target = None; + } else if !self.outbound_path_to_first_bonus.is_empty() + && !self.backtracking_from_first_bonus + { + let return_path = PathfindingTool::reverse_path(&self.outbound_path_to_first_bonus); + display::print_backtrack(return_path.len()); + self.plan = return_path.into_iter().collect(); + self.backtracking_from_first_bonus = true; + self.needs_second_modifier_pass = true; + self.outbound_path_to_first_bonus.clear(); + } + } + } + + /// Computes the scalar reward for the previous action using distance gradients + /// and novelty bonuses. + /// + /// Reward components: + /// - Base step cost: -0.1 + /// - Level completion: +15.0 + /// - UI change (modifier rotation): +10.0 + /// - Wall hit (no movement): -1.5 + /// - Novel state visit: +2.0 + /// - Revisit penalty: -0.2 × visits + /// - Proximity to target after modifier: +50 / (1 + Manhattan distance) + /// + /// # Time complexity: O(1) + /// # Space complexity: O(1) + fn compute_reward(&self, ctx: &FrameContext<'_>, moved: bool, ui_changed: bool) -> f64 { + let mut reward = -0.1; + if ctx.inner.levels_completed > self.prev_levels { + return reward + 15.0; + } + if ui_changed { + reward += 10.0; + } + if !moved { + reward -= 1.5; + } else { + let visits = self + .global_visits + .get(&ctx.state_key()) + .copied() + .unwrap_or(0); + reward += if visits == 0 { + 2.0 + } else { + -0.2 * visits as f64 + }; + + if self.local_modifier_reached + && let (Some((px, py)), Some((tx, ty))) = (ctx.player_pos(), ctx.target_pos()) + { + let distance = px.abs_diff(tx) + py.abs_diff(ty); + reward += 50.0 / (1.0 + distance as f64); + } + } + reward + } + + /// Selects the next action using the full behavioral hierarchy. + /// + /// This is a pure routing dispatcher: it calls specialised helpers in + /// priority order and returns the first valid action found. + /// + /// # Time complexity: O(V + E) worst-case (BFS fallback) + /// # Space complexity: O(V) + fn choose(&mut self, state: u64, avail: &[u32], context: &FrameContext<'_>) -> u32 { + let trial_visits_here = self.trial_visits.get(&state).copied().unwrap_or(0); + + if trial_visits_here >= STUCK_THRESHOLD + && !self.local_modifier_reached + && let Some(action) = self.escape_stuck(state, avail) + { + return action; + } + + if let Some(action) = self.follow_plan(state, avail) { + return action; + } + + if let Some(action) = self.route_to_active_target(state, avail, context) { + return action; + } + + if self.trial > 0 + && let Some(action) = self.route_via_milestone_bfs(state, avail) + { + return action; + } + + self.explore(state, avail) + } + + /// Attempts to escape a stuck state by BFS-navigating to the nearest unvisited + /// or frontier cell. + /// + /// Clears the `plan` buffer and escalates the escape strategy on repeated invocations + /// for the same state. Does not fire when `local_modifier_reached` is true, so that + /// the post-modifier target route is never interrupted. + /// + /// # Time complexity: O(V + E) + /// # Space complexity: O(V) + fn escape_stuck(&mut self, state: u64, avail: &[u32]) -> Option { + self.plan.clear(); + *self.stuck_counts.entry(state).or_insert(0) += 1; + let escape_count = self.stuck_counts[&state]; + + let mut queue: VecDeque = VecDeque::from([state]); + let mut seen: HashSet = HashSet::from([state]); + let mut path_parents: HashMap = HashMap::new(); + let mut escape_target: Option = None; + + 'bfs: while let Some(current) = queue.pop_front() { + let blocked = self.world.walls.get(¤t); + for action in [1u32, 2, 3, 4] { + if blocked.is_some_and(|w| w.contains(&action)) { + continue; + } + if let Some(next_state) = self.world.predict(current, action) { + if self.global_visits.get(&next_state).copied().unwrap_or(0) == 0 { + escape_target = Some(current); + break 'bfs; + } + if seen.insert(next_state) { + path_parents.insert(next_state, (current, action)); + queue.push_back(next_state); + } + } else if !self.world.is_wall(current, action) { + escape_target = Some(current); + break 'bfs; + } + } + } + + if let Some(target) = escape_target { + if target != state { + let path = PathfindingTool::reconstruct_path(&path_parents, state, target); + if let Some(&first) = path.first() + && avail.contains(&first) + { + eprintln!( + " [mode=STUCK-escape] target={:x} steps={}", + target, + path.len() + ); + self.plan = path.into_iter().skip(1).collect(); + return Some(first); + } + } else { + for &action in avail { + if self.world.predict(state, action).is_none() + && !self.world.is_wall(state, action) + { + let sc = self.stuck_counts.get(&state).copied().unwrap_or(0); + display::print_stuck("frontier-edge", action, sc); + return Some(action); + } + } + if escape_count >= 4 { + let mut rng = rand::thread_rng(); + let action = avail[rng.gen_range(0..avail.len())]; + eprintln!( + " [mode=STUCK-random-break] action={} escape={}", + action, escape_count + ); + return Some(action); + } + let best = avail.iter().min_by_key(|&&a| { + self.world + .predict(state, a) + .map(|ns| self.global_visits.get(&ns).copied().unwrap_or(0)) + .unwrap_or(0) + }); + if let Some(&action) = best { + let sc = self.stuck_counts.get(&state).copied().unwrap_or(0); + display::print_stuck("min-visit", action, sc); + return Some(action); + } + } + } + None + } + + /// Executes the next step from `self.plan` if the plan is valid. + /// + /// Clears the plan and returns `None` if the next planned action is blocked. + /// + /// # Time complexity: O(1) + /// # Space complexity: O(1) + fn follow_plan(&mut self, state: u64, avail: &[u32]) -> Option { + if let Some(&next) = self.plan.front() { + if !avail.contains(&next) || self.world.is_wall(state, next) { + display::print_plan_invalidated(); + self.plan.clear(); + return None; + } + self.plan.pop_front(); + display::print_plan_step(next, self.plan.len()); + return Some(next); + } + None + } + + /// Determines the current active target and routes toward it. + /// + /// Target priority: + /// 1. If modifier not yet reached: route to modifier position. + /// 2. If modifier reached and uncollected bonuses exist: route to nearest bonus. + /// After first bonus consumed, the backtrack plan takes over via `follow_plan`. + /// Once backtrack completes, route forward to next bonus. + /// 3. If modifier reached and all bonuses collected: route to final target. + /// + /// # Time complexity: O(N log N) worst-case (spatial A\*) + /// # Space complexity: O(N) + fn route_to_active_target( + &mut self, + state: u64, + avail: &[u32], + context: &FrameContext<'_>, + ) -> Option { + let player_pos = context.player_pos(); + let target_pos = context.target_pos(); + let known_mod_pos = self.known_modifiers.iter().next().copied(); + + let modifier_pos = if self.trial > 0 || self.step > 0 { + context.modifier_pos().or(known_mod_pos) + } else { + known_mod_pos + }; + + let uncollected_bonuses: Vec<(usize, usize)> = self + .known_bonuses + .iter() + .filter(|b| !self.trial_bonuses_consumed.contains(*b)) + .copied() + .collect(); + + let active_target: Option<(usize, usize)>; + let target_label: &str; + + if self.current_level_idx == 0 { + if self.local_modifier_reached || context.player_piece_matches_target() { + active_target = target_pos; + target_label = "Target"; + } else if self.trial > 0 && known_mod_pos.is_some() { + active_target = known_mod_pos; + target_label = "Target"; + } else { + active_target = None; + target_label = "None"; + } + } else if !self.local_modifier_reached { + active_target = modifier_pos; + target_label = "Modifier"; + } else if self.needs_second_modifier_pass && self.plan.is_empty() { + if let Some(mod_pos) = known_mod_pos.or(modifier_pos) { + if let Some((px, py)) = player_pos { + let dx = px.abs_diff(mod_pos.0); + let dy = py.abs_diff(mod_pos.1); + if dx < 5 && dy < 5 { + display::print_second_mod_pass(true); + self.needs_second_modifier_pass = false; + if !uncollected_bonuses.is_empty() { + let nearest = uncollected_bonuses + .iter() + .min_by_key(|&&(bx, by)| px.abs_diff(bx) + py.abs_diff(by)) + .copied(); + active_target = nearest; + target_label = "Bonus"; + } else { + active_target = target_pos; + target_label = "Target"; + } + } else { + display::print_second_mod_pass(false); + active_target = Some(mod_pos); + target_label = "Modifier2"; + } + } else { + active_target = Some(mod_pos); + target_label = "Modifier2"; + } + } else { + self.needs_second_modifier_pass = false; + active_target = target_pos; + target_label = "Target"; + } + } else if !uncollected_bonuses.is_empty() { + if let Some((px, py)) = player_pos { + let nearest = uncollected_bonuses + .iter() + .min_by_key(|&&(bx, by)| px.abs_diff(bx) + py.abs_diff(by)) + .copied(); + active_target = nearest; + target_label = "Bonus"; + } else { + active_target = target_pos; + target_label = "Target"; + } + } else { + if self.locked_final_target.is_none() + && let Some(tp) = target_pos + { + self.locked_final_target = Some(tp); + display::print_target_locked(tp); + } + active_target = self.locked_final_target.or(target_pos); + target_label = "Target"; + } + + let (goal_x, goal_y) = active_target?; + let (px, py) = player_pos?; + + let at_goal = if target_label == "Target" && uncollected_bonuses.is_empty() { + px.abs_diff(goal_x) < 5 && py.abs_diff(goal_y) < 5 + } else { + px == goal_x && py == goal_y + }; + if at_goal { + return None; + } + + let pos_walls = self.world.pos_walls(); + let visited_coords = self.world.visited_pixel_coords(); + + if let Some(path) = + PathfindingTool::spatial_astar(px, py, goal_x, goal_y, &pos_walls, &visited_coords) + && let Some(&first) = path.first() + && avail.contains(&first) + && !self.world.is_wall(state, first) + { + let mode = format!("GENERAL→{target_label}_Cartesian"); + display::print_routing(&mode, (goal_x, goal_y), path.len(), first); + if path.len() > 1 { + self.plan = path.into_iter().skip(1).collect(); + } + return Some(first); + } + + let dx = (goal_x as i32 - px as i32).unsigned_abs() as usize; + let dy = (goal_y as i32 - py as i32).unsigned_abs() as usize; + let vertical_action = if (goal_y as i32) < (py as i32) { 1 } else { 2 }; + let horizontal_action = if (goal_x as i32) < (px as i32) { 3 } else { 4 }; + + let preferred: Vec = if dy >= dx { + vec![vertical_action, horizontal_action] + } else { + vec![horizontal_action, vertical_action] + }; + + for action in &preferred { + if avail.contains(action) && !self.world.is_wall(state, *action) { + let pred_visits = self + .world + .predict(state, *action) + .map(|ns| self.trial_visits.get(&ns).copied().unwrap_or(0)) + .unwrap_or(0); + if pred_visits == 0 { + let mode = format!("GENERAL→{target_label}"); + display::print_routing(&mode, (goal_x, goal_y), 1, *action); + return Some(*action); + } + } + } + + let best = avail + .iter() + .copied() + .filter(|&a| !self.world.is_wall(state, a)) + .min_by_key(|&a| { + let (nx, ny) = PathfindingTool::action_next_pos(px, py, a); + let distance = (nx.abs_diff(goal_x) + ny.abs_diff(goal_y)) as u32; + let visits = self + .world + .predict(state, a) + .map(|ns| self.trial_visits.get(&ns).copied().unwrap_or(0)) + .unwrap_or(0); + distance + visits * 50 + }); + + if let Some(action) = best { + let mode = format!("GENERAL→{target_label}"); + display::print_routing(&mode, (goal_x, goal_y), 1, action); + return Some(action); + } + + None + } + + /// Routes to the highest-priority unvisited milestone via graph BFS. + /// + /// Only fires in trial ≥ 1 when the spatial routes have failed. + /// + /// # Time complexity: O(V + E) + /// # Space complexity: O(V) + fn route_via_milestone_bfs(&mut self, state: u64, avail: &[u32]) -> Option { + let best_milestone = self + .world + .milestones + .iter() + .filter(|&&m| !self.milestones_reached_this_trial.contains(&m)) + .max_by_key(|&&m| self.milestone_levels.get(&m).copied().unwrap_or(0)) + .copied(); + + if let Some(target) = best_milestone + && target != state + && let Some(path) = PathfindingTool::bfs(state, target, &self.world.transitions) + && let Some(&first) = path.first() + && avail.contains(&first) + { + display::print_routing("BFS→milestone", (0, 0), path.len(), first); + self.plan = path.into_iter().skip(1).collect(); + return Some(first); + } + + if let Some((win_state, _)) = self.world.win_predecessor + && state != win_state + && let Some(path) = PathfindingTool::bfs(state, win_state, &self.world.transitions) + && let Some(&first) = path.first() + && avail.contains(&first) + { + display::print_routing("BFS→win", (0, 0), path.len(), first); + self.plan = path.into_iter().skip(1).collect(); + return Some(first); + } + + None + } + + /// Novelty-driven map exploration fallback. + /// + /// Preference order: + /// 1. Any neighbour leading to a globally unvisited state. + /// 2. Any neighbour leading to a trial-unvisited state. + /// 3. BFS to the nearest frontier cell (an explored state that has unknown neighbours). + /// 4. Minimum global-visit greedy walk. + /// + /// Back-tracking (reversing the previous action) is penalised unless unavoidable. + /// + /// # Time complexity: O(V + E) for BFS sub-path; O(1) for greedy steps + /// # Space complexity: O(V) + fn explore(&mut self, state: u64, avail: &[u32]) -> u32 { + let mut rng = rand::thread_rng(); + + let mut candidates: Vec = avail + .iter() + .copied() + .filter(|&a| !self.world.is_wall(state, a)) + .collect(); + if candidates.is_empty() { + candidates = avail.to_vec(); + } + + if let Some(prev) = self.prev_raw_action { + let reverse = PathfindingTool::reverse_action(prev); + let no_rev: Vec = candidates + .iter() + .copied() + .filter(|&a| a != reverse) + .collect(); + if !no_rev.is_empty() { + candidates = no_rev; + } + } + + let globally_new: Vec = candidates + .iter() + .copied() + .filter(|&a| { + self.world + .predict(state, a) + .map(|ns| self.global_visits.get(&ns).copied().unwrap_or(0) == 0) + .unwrap_or(true) + }) + .collect(); + if !globally_new.is_empty() { + display::print_action(0, &format!("RANDOM-new choices={}", globally_new.len())); + return *globally_new.choose(&mut rng).unwrap(); + } + + let trial_new: Vec = candidates + .iter() + .copied() + .filter(|&a| { + self.world + .predict(state, a) + .map(|ns| self.trial_visits.get(&ns).copied().unwrap_or(0) == 0) + .unwrap_or(true) + }) + .collect(); + if !trial_new.is_empty() { + display::print_action(0, &format!("RANDOM-trial-new choices={}", trial_new.len())); + return *trial_new.choose(&mut rng).unwrap(); + } + + let min_gv = candidates + .iter() + .map(|&a| { + self.world + .predict(state, a) + .map(|ns| self.global_visits.get(&ns).copied().unwrap_or(0)) + .unwrap_or(0) + }) + .min() + .unwrap_or(0); + candidates.retain(|&a| { + self.world + .predict(state, a) + .map(|ns| self.global_visits.get(&ns).copied().unwrap_or(0)) + .unwrap_or(0) + == min_gv + }); + + if min_gv > 0 + && let Some(action) = self.frontier_bfs(state, avail, &candidates) + { + return action; + } + + display::print_action( + 0, + &format!( + "RANDOM-exploit min_gv={} choices={}", + min_gv, + candidates.len() + ), + ); + *candidates.choose(&mut rng).unwrap() + } + + /// Performs a BFS from `state` to find the nearest frontier cell (a cell with an + /// unknown passable neighbour) and returns the first action toward it. + /// + /// # Time complexity: O(V + E) + /// # Space complexity: O(V) + fn frontier_bfs(&mut self, state: u64, avail: &[u32], candidates: &[u32]) -> Option { + let mut queue: VecDeque = VecDeque::from([state]); + let mut seen: HashSet = HashSet::from([state]); + let mut path_parents: HashMap = HashMap::new(); + let mut frontier_target: Option = None; + + 'outer: while let Some(current) = queue.pop_front() { + let local_avail: Vec = if current == state { + avail.to_vec() + } else { + self.world + .walls + .get(¤t) + .map_or(vec![1, 2, 3, 4], |w| { + [1u32, 2, 3, 4] + .into_iter() + .filter(|a| !w.contains(a)) + .collect() + }) + }; + for action in local_avail { + if let Some(next_state) = self.world.predict(current, action) { + if self.global_visits.get(&next_state).copied().unwrap_or(0) == 0 { + frontier_target = Some(current); + break 'outer; + } + if seen.insert(next_state) { + path_parents.insert(next_state, (current, action)); + queue.push_back(next_state); + } + } else if !self.world.is_wall(current, action) { + frontier_target = Some(current); + break 'outer; + } + } + } + + if let Some(target) = frontier_target { + if target != state { + let path = PathfindingTool::reconstruct_path(&path_parents, state, target); + if let Some(&first) = path.first() + && avail.contains(&first) + { + display::print_routing("BFS→frontier", (0, 0), path.len(), first); + self.plan = path.into_iter().skip(1).collect(); + return Some(first); + } + } else { + for &action in avail { + if self.world.predict(state, action).is_none() + && !self.world.is_wall(state, action) + { + display::print_action(action, "RANDOM-frontier-edge"); + return Some(action); + } + } + } + } + + let recommended = candidates + .iter() + .filter_map(|&a| { + let ns = self.world.predict(state, a)?; + Some((a, self.global_visits.get(&ns).copied().unwrap_or(0))) + }) + .min_by_key(|&(_, v)| v) + .map(|(a, _)| a); + + if let Some(action) = recommended { + display::print_action(action, "BFS→recommended"); + return Some(action); + } + + None + } + + /// Finalises the current trial. + /// + /// Persists Q-table state, writes telemetry to long-term memory, resets all + /// trial-scoped fields, and increments the trial counter. + /// + /// # Time complexity: O(S) where S = Q-table state count + /// # Space complexity: O(1) additional + pub fn end_trial(&mut self) { + self.engine.end_of_episode( + &ColdStore::default(), + &mut KnowledgeIndex::new(), + "arc-agi-ls20", + 0.5, + ); + + let (states, walls, passages) = self.world.stats(); + self.agent.add_ltm_message(Message::new( + "trial_end", + format!( + "trial={} steps={} g_states={} walls={} passages={} milestones={}", + self.trial, + self.step, + self.global_visits.len(), + walls, + passages, + self.world.milestones.len(), + ), + )); + + info!( + trial = self.trial, + eps = %format!("{:.3}", self.engine.q_table().epsilon), + g_states = self.global_visits.len(), + states, walls, passages, + milestones = self.world.milestones.len(), + "Trial ended" + ); + + self.trial += 1; + self.step = 0; + self.prev_state_key = None; + self.prev_action_key = None; + self.prev_raw_action = None; + self.prev_ui_hash = None; + self.plan.clear(); + self.trial_visits.clear(); + self.milestones_reached_this_trial.clear(); + self.local_modifier_reached = false; + self.stuck_counts.clear(); + self.trial_mod_visits = 0; + self.trial_bonuses_consumed.clear(); + self.outbound_path_to_first_bonus.clear(); + self.backtracking_from_first_bonus = false; + self.needs_second_modifier_pass = false; + self.locked_final_target = None; + self.modifier_reached_step = None; + } + + /// Records a terminal reward for the final state of a trial and stores the + /// winning predecessor state for BFS replay in future trials. + /// + /// # Time complexity: O(1) + /// # Space complexity: O(1) + pub fn record_terminal_reward(&mut self, ctx: &FrameContext<'_>) { + if let (Some(prev_sk), Some(prev_ak)) = (self.prev_state_key, self.prev_action_key) { + let current = ctx.state_key(); + if ctx.inner.state == arc_agi_rs::models::GameState::Win + && let Some(prev_ga) = self.prev_raw_action + { + self.world.win_predecessor = Some((prev_sk, prev_ga)); + let _ = prev_sk; // No log needed, display:: handles this at run summary + } + let r: f64 = if ctx.inner.levels_completed > 0 { + 20.0 + } else { + -2.0 + }; + for _ in 0..5 { + self.engine + .record_step(&reward_signal(self.step, r), prev_sk, prev_ak, current); + } + } + } + + /// Persists the current HELM Q-table to disk. + /// + /// # Time complexity: O(S) where S = Q-table state count + /// # Space complexity: O(S) + pub fn save_learning(&self, path: &std::path::Path) -> Result<(), ArcAgentError> { + self.agent.save_learning(path).context("save")?; + Ok(()) + } + + /// Returns the current trial index. + pub fn trial(&self) -> usize { + self.trial + } + + /// Returns the current step index within the active trial. + pub fn step(&self) -> usize { + self.step + } + + /// Returns the number of distinct states in the Q-table. + pub fn q_state_count(&self) -> usize { + self.engine.q_table().state_count() + } + + /// Returns the current epsilon value of the greedy-epsilon exploration policy. + pub fn epsilon(&self) -> f64 { + self.engine.q_table().epsilon + } + + /// Returns the zero-indexed current level. + pub fn current_level(&self) -> u32 { + self.current_level_idx + } +} + +impl Default for LmmPolicy { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/arc-lmm-agent/src/runner.rs b/examples/arc-lmm-agent/src/runner.rs new file mode 100644 index 0000000..f324f9b --- /dev/null +++ b/examples/arc-lmm-agent/src/runner.rs @@ -0,0 +1,290 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! # `ArcGameRunner` - outer game loop. +//! +//! Owns the ARC-AGI HTTP client, manages trial resets, calls `policy.decide()` +//! each frame, and assembles the final [`RunSummary`] after all trials complete. + +use arc_agi_rs::client::Client; +use arc_agi_rs::models::{FrameData, GameState}; +use arc_agi_rs::params::{MakeParams, ScorecardParams, StepParams}; +use tracing::{info, warn}; + +use crate::config::AgentConfig; +use crate::display; +use crate::error::ArcAgentError; +use crate::frame::FrameContext; +use crate::policy::LmmPolicy; + +/// Summary of a completed agent run returned from [`ArcGameRunner::run`]. +#[derive(Debug)] +pub struct RunSummary { + /// The game identifier used for this run. + pub game_id: String, + + /// Terminal state when the run ended (`Win`, `GameOver`, or `NotFinished`). + pub final_state: GameState, + + /// Number of levels successfully completed. + pub levels_completed: u32, + + /// Total levels required to win. + pub win_levels: u32, + + /// Total action steps consumed across all trials. + pub steps_taken: usize, + + /// Scorecard identifier assigned by the server. + pub card_id: String, +} + +/// Game-loop orchestrator that drives [`LmmPolicy`] through a series of trials. +/// +/// Construct with [`ArcGameRunner::new`] and drive with [`ArcGameRunner::run`]. +pub struct ArcGameRunner { + /// HTTP client connected to the ARC-AGI game server. + client: Client, + + /// The navigation policy being evaluated. + policy: LmmPolicy, + + /// Runtime configuration (server URL, trial budget, etc.). + config: AgentConfig, +} + +impl ArcGameRunner { + /// Constructs a new runner from the given configuration. + /// + /// Initialises the server client and an empty [`LmmPolicy`]. + /// + /// # Errors + /// + /// Returns [`ArcAgentError::Client`] when the HTTP client cannot be created. + pub fn new(config: AgentConfig) -> Result { + let client = Client::builder() + .base_url(&config.base_url) + .api_key(&config.api_key) + .cookie_store(true) + .build()?; + + Ok(Self { + client, + policy: LmmPolicy::new(), + config, + }) + } + + /// Runs the agent for up to `config.max_trials` trials and returns a [`RunSummary`]. + /// + /// Each trial: + /// 1. Resets the game session. + /// 2. Steps through frames until a terminal state, step budget exhaustion, or + /// an implicit server reset (player sprite disappears). + /// 3. Calls `policy.end_trial()` to finalise Q-table and reset trial state. + /// + /// # Errors + /// + /// Propagates [`ArcAgentError`] on client communication failures. + pub async fn run(&mut self) -> Result { + let resolved_game_id = { + let envs = self.client.list_environments().await.unwrap_or_default(); + let prefix = &self.config.game_id; + envs.into_iter() + .find(|e| e.game_id == *prefix || e.game_id.starts_with(&format!("{}-", prefix))) + .map(|e| e.game_id) + .unwrap_or_else(|| prefix.clone()) + }; + + info!(resolved_game_id = %resolved_game_id, "Resolved full game_id from server"); + + let card_id = self + .client + .open_scorecard(Some( + ScorecardParams::new() + .tags(self.config.tags.clone()) + .source_url("https://github.com/wiseaidotdev/lmm"), + )) + .await?; + + info!(card_id = %card_id, game_id = %resolved_game_id, "Scorecard opened"); + + let mut final_frame: Option = None; + let mut total_steps = 0usize; + + for trial in 0..self.config.max_trials { + display::print_trial_start(trial, self.policy.epsilon(), self.policy.q_state_count()); + info!( + trial = trial, + max_trials = self.config.max_trials, + epsilon = %format!("{:.4}", self.policy.epsilon()), + q_states = self.policy.q_state_count(), + "Starting trial" + ); + + let initial_frame = self + .client + .reset(MakeParams::new(&resolved_game_id, &card_id)) + .await?; + + let guid = initial_frame + .guid + .clone() + .ok_or(ArcAgentError::UnexpectedSessionEnd)?; + + info!( + trial = trial, + guid = %guid, + state = %initial_frame.state, + "Trial session started" + ); + + let mut current_frame = initial_frame; + let mut current_guid = guid; + let mut trial_steps = 0usize; + let mut prev_frame: Option = None; + + while trial_steps < self.config.max_actions { + let ctx = FrameContext::new(¤t_frame); + + if ctx.is_terminal() { + self.policy.record_terminal_reward(&ctx); + break; + } + + // let available = current_frame.available_actions.clone(); + let spinner = display::think_spinner(); + let action_id = self.policy.decide(&ctx, prev_frame.as_ref()).await?; + spinner.finish_silent(); + + let step_params = + StepParams::new(&resolved_game_id, &card_id, ¤t_guid, action_id) + .reasoning(serde_json::json!({ + "agent": "arc-lmm-agent", + "trial": self.policy.trial(), + "step": trial_steps, + "action_id": action_id, + "epsilon": self.policy.epsilon(), + })); + + let next_frame = self.client.step(step_params).await?; + + if let Some(ref new_guid) = next_frame.guid { + current_guid = new_guid.clone(); + } + + let server_action = next_frame.action_input.as_ref().map(|a| a.id).unwrap_or(0); + + let next_ctx = FrameContext::new(&next_frame); + if ctx.player_pos().is_some() + && next_ctx.player_pos().is_none() + && next_frame.levels_completed == current_frame.levels_completed + { + info!( + trial = trial, + steps = trial_steps, + "Implicit server reset detected (player wiped). Recording constraint and restarting loop natively." + ); + self.policy.record_level_step_limit(trial_steps + 1); + self.policy.end_trial(); + trial_steps = 0; + prev_frame = Some(current_frame); + current_frame = next_frame; + continue; + } + + display::print_step_summary( + self.policy.trial(), + self.policy.step().saturating_sub(1), + total_steps + trial_steps, + action_id, + self.policy.epsilon(), + self.policy.q_state_count(), + next_frame.state.as_str(), + next_frame.levels_completed, + next_frame.win_levels, + server_action, + ); + + info!( + trial = self.policy.trial(), + step = self.policy.step(), + total_steps = total_steps + trial_steps, + action_id = action_id, + state = %next_frame.state, + levels_completed = next_frame.levels_completed, + win_levels = next_frame.win_levels, + "Step completed" + ); + + prev_frame = Some(current_frame); + current_frame = next_frame; + trial_steps += 1; + } + + total_steps += trial_steps; + + if trial_steps >= self.config.max_actions { + warn!( + trial = trial, + max_actions = self.config.max_actions, + "Action budget exhausted for trial" + ); + } + + final_frame = Some(current_frame.clone()); + + if current_frame.state == GameState::Win { + info!(trial = trial, state = %current_frame.state, "Game Won."); + self.policy.end_trial(); + break; + } else if current_frame.state == GameState::GameOver { + info!( + trial = trial, + steps = trial_steps, + "Trial exhausted steps (GameOver)" + ); + self.policy.record_level_step_limit(trial_steps); + self.policy.end_trial(); + continue; + } + + self.policy.end_trial(); + display::print_trial_end( + trial, + trial_steps, + self.policy.epsilon(), + self.policy.q_state_count(), + ); + } + + let last = final_frame.unwrap_or_default(); + let summary_state = last.state.clone(); + let summary_levels = last.levels_completed; + let summary_win = last.win_levels; + + let _final_card = match self.client.close_scorecard(&card_id).await { + Ok(card) => { + info!(score = card.score, "Scorecard closed"); + Some(card) + } + Err(e) => { + warn!(error = %e, "Failed to close scorecard"); + None + } + }; + + Ok(RunSummary { + game_id: self.config.game_id.clone(), + final_state: summary_state, + levels_completed: summary_levels, + win_levels: summary_win, + steps_taken: total_steps, + card_id, + }) + } +} diff --git a/examples/arc-lmm-agent/src/tools.rs b/examples/arc-lmm-agent/src/tools.rs new file mode 100644 index 0000000..26b051a --- /dev/null +++ b/examples/arc-lmm-agent/src/tools.rs @@ -0,0 +1,28 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! # `tools` - Agent-callable algorithmic tools. +//! +//! This module provides pure-function tools that the agent policy +//! dispatches to at runtime. Every tool operates on data passed by +//! reference; none of them hold mutable state. +//! +//! ## Available tools +//! +//! - [`PathfindingTool`] - graph-based BFS and Cartesian A\* pathfinding over +//! the learned world map. +//! +//! ## Design principles +//! +//! Tools are **stateless** structs with associated methods. The agent stores +//! an instance of each tool it needs and calls it with slices of its own +//! world data. Keeping algorithms here makes `policy.rs` a pure behavioral +//! orchestrator with no search logic embedded inside it. + +pub mod pathfinding; + +pub use pathfinding::PathfindingTool; diff --git a/examples/arc-lmm-agent/src/tools/pathfinding.rs b/examples/arc-lmm-agent/src/tools/pathfinding.rs new file mode 100644 index 0000000..5aa4f63 --- /dev/null +++ b/examples/arc-lmm-agent/src/tools/pathfinding.rs @@ -0,0 +1,270 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! # `PathfindingTool` - graph-based BFS and Cartesian A\* for the ARC agent. +//! +//! All search algorithms live here as **pure functions**. They receive data as +//! immutable references; they never mutate world state. The agent policy +//! calls these methods and applies the resulting action sequences. +//! +//! ## Algorithms +//! +//! | Method | Algorithm | Time | Space | +//! |--------|-----------|------|-------| +//! | [`PathfindingTool::bfs`] | Breadth-first search on the state-transition graph | O(V + E) | O(V) | +//! | [`PathfindingTool::spatial_astar`] | A\* with Manhattan heuristic on a 2-D pixel grid | O(N log N) | O(N) | +//! | [`PathfindingTool::reconstruct_path`] | Backtrack through parent map | O(L) | O(L) | +//! | [`PathfindingTool::action_next_pos`] | Constant-time next-pixel projection | O(1) | O(1) | +//! +//! where V = visited states, E = edges (transitions), N = grid cells, L = path length. + +use std::cmp::Reverse; +use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; + +/// A stateless pathfinding tool the agent policy delegates search to. +/// +/// Instantiate once and hold as a field; every method borrows world-map data +/// passed in by the caller so there is no internal allocation between calls. +/// +/// # Examples +/// +/// ```rust +/// use arc_lmm_agent::tools::PathfindingTool; +/// use std::collections::HashMap; +/// +/// let mut transitions: HashMap> = HashMap::new(); +/// transitions.entry(1).or_default().insert(4, 2); +/// transitions.entry(2).or_default().insert(4, 3); +/// +/// let path = PathfindingTool::bfs(1, 3, &transitions); +/// assert_eq!(path, Some(vec![4, 4])); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct PathfindingTool; + +impl PathfindingTool { + /// Returns the shortest action sequence from `from` to `to` using BFS over + /// the supplied state-transition graph. + /// + /// Returns `None` when `to` is unreachable from `from`. + /// + /// # Parameters + /// + /// - `from` - starting state hash. + /// - `to` - goal state hash. + /// - `transitions` - map of `state → (action → next_state)` collected by the agent. + /// + /// # Time complexity: O(V + E) + /// # Space complexity: O(V) + pub fn bfs( + from: u64, + to: u64, + transitions: &HashMap>, + ) -> Option> { + if from == to { + return Some(vec![]); + } + let mut queue: VecDeque = VecDeque::from([from]); + let mut seen: HashSet = HashSet::from([from]); + let mut parents: HashMap = HashMap::new(); + + while let Some(current) = queue.pop_front() { + for (&action, &next) in transitions.get(¤t).into_iter().flatten() { + if seen.insert(next) { + parents.insert(next, (current, action)); + if next == to { + return Some(Self::reconstruct_path(&parents, from, to)); + } + queue.push_back(next); + } + } + } + None + } + + /// Reconstructs an action sequence from the BFS `parents` map. + /// + /// Traces from `to` back to `from` following the parent pointers, then + /// reverses the accumulated actions to produce a forward-going plan. + /// + /// # Parameters + /// + /// - `parents` - map of `state → (predecessor_state, action_taken)`. + /// - `from` - origin state hash. + /// - `to` - terminal state hash. + /// + /// # Time complexity: O(L) where L = path length + /// # Space complexity: O(L) + pub fn reconstruct_path(parents: &HashMap, from: u64, to: u64) -> Vec { + let mut path = Vec::new(); + let mut node = to; + while node != from { + if let Some(&(prev, action)) = parents.get(&node) { + path.push(action); + node = prev; + } else { + break; + } + } + path.reverse(); + path + } + + /// Finds the optimal action sequence from pixel position `(start_x, start_y)` to + /// `(goal_x, goal_y)` using the A\* algorithm with a Manhattan heuristic. + /// + /// The search operates directly on pixel coordinates with 5-pixel grid steps. + /// Known wall positions penalise expansion. Unvisited coordinates receive a large + /// penalty to encourage the agent to stay on previously explored paths. + /// + /// Returns `None` when the goal is unreachable within the map bounds. + /// + /// # Parameters + /// + /// - `start_x`, `start_y` - current player pixel coordinates. + /// - `goal_x`, `goal_y` - target pixel coordinates. + /// - `pos_walls` - set of `((x, y), action)` pairs marking known walls. + /// - `visited_coords` - set of pixel coordinates the agent has previously stood on. + /// + /// # Time complexity: O(N log N) where N = grid cells within bounds + /// # Space complexity: O(N) + pub fn spatial_astar( + start_x: usize, + start_y: usize, + goal_x: usize, + goal_y: usize, + pos_walls: &HashSet<((usize, usize), u32)>, + visited_coords: &HashSet<(usize, usize)>, + ) -> Option> { + if start_x == goal_x && start_y == goal_y { + return Some(vec![]); + } + + let heuristic = |x: usize, y: usize| -> usize { x.abs_diff(goal_x) + y.abs_diff(goal_y) }; + + type AStarNode = Reverse<(usize, usize, usize, usize, u32)>; + let mut open: BinaryHeap = BinaryHeap::new(); + let mut g_scores: HashMap<(usize, usize), usize> = HashMap::new(); + let mut parents: HashMap<(usize, usize), ((usize, usize), u32)> = HashMap::new(); + + g_scores.insert((start_x, start_y), 0); + open.push(Reverse(( + heuristic(start_x, start_y), + 0, + start_x, + start_y, + 0, + ))); + + while let Some(Reverse((_, cost, cx, cy, prev_action))) = open.pop() { + if cx.abs_diff(goal_x) < 5 && cy.abs_diff(goal_y) < 5 { + let mut path: Vec = Vec::new(); + let mut curr = (cx, cy); + while curr != (start_x, start_y) { + if let Some(&(prev, action)) = parents.get(&curr) { + path.push(action); + curr = prev; + } else { + break; + } + } + path.reverse(); + return Some(path); + } + + let best_known = *g_scores.get(&(cx, cy)).unwrap_or(&usize::MAX); + if cost > best_known { + continue; + } + + for action in 1u32..=4 { + if pos_walls.contains(&((cx, cy), action)) { + continue; + } + let (nx, ny) = Self::action_next_pos(cx, cy, action); + if nx > 200 || ny > 200 { + continue; + } + + let unvisited_penalty = if visited_coords.contains(&(nx, ny)) { + 0 + } else { + 5 + }; + let turn_penalty = if prev_action != 0 && prev_action != action { + 5 + } else { + 0 + }; + let ng = cost + 1 + unvisited_penalty + turn_penalty; + + if ng < *g_scores.get(&(nx, ny)).unwrap_or(&usize::MAX) { + g_scores.insert((nx, ny), ng); + parents.insert((nx, ny), ((cx, cy), action)); + open.push(Reverse((ng + heuristic(nx, ny), ng, nx, ny, action))); + } + } + } + None + } + + /// Projects the next pixel position after taking `action` from `(px, py)`. + /// + /// Uses the game's 5-pixel grid increment convention: + /// - `1` → up (y decreases) + /// - `2` → down (y increases) + /// - `3` → left (x decreases) + /// - `4` → right (x increases) + /// + /// # Time complexity: O(1) + /// # Space complexity: O(1) + #[inline] + pub fn action_next_pos(px: usize, py: usize, action: u32) -> (usize, usize) { + match action { + 1 => (px, py.saturating_sub(5)), + 2 => (px, py + 5), + 3 => (px.saturating_sub(5), py), + 4 => (px + 5, py), + _ => (px, py), + } + } + + /// Returns the action that is the inverse (opposite direction) of `action`. + /// + /// Used to reverse a recorded path segment: up↔down, left↔right. + /// + /// # Time complexity: O(1) + /// # Space complexity: O(1) + #[inline] + pub fn reverse_action(action: u32) -> u32 { + match action { + 1 => 2, + 2 => 1, + 3 => 4, + 4 => 3, + _ => 0, + } + } + + /// Reverses a forward action sequence into the equivalent backtracking sequence. + /// + /// Each action is replaced with its opposite and the resulting list is reversed so + /// that replaying it traces the original route in the opposite direction. + /// + /// # Parameters + /// + /// - `path` - forward action sequence to invert. + /// + /// # Time complexity: O(L) + /// # Space complexity: O(L) + pub fn reverse_path(path: &[u32]) -> Vec { + path.iter() + .rev() + .map(|&a| Self::reverse_action(a)) + .collect() + } +} diff --git a/examples/arc-lmm-agent/src/world.rs b/examples/arc-lmm-agent/src/world.rs new file mode 100644 index 0000000..8b9a8a9 --- /dev/null +++ b/examples/arc-lmm-agent/src/world.rs @@ -0,0 +1,203 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! # `WorldMap` - learned spatial model of the ARC-AGI environment. +//! +//! `WorldMap` accumulates observed state transitions, wall constraints, milestone +//! states, and pixel-coordinate annotations as the agent explores the grid. It is +//! the agent's primary long-term spatial memory within a single level. +//! +//! ## Lifecycle +//! +//! - **Cleared** when the game advances to a new level (the geometry changes). +//! - **Persisted** across virtual trials within the same level so BFS can replay +//! previously discovered routes without re-exploring. +//! +//! ## Usage +//! +//! Pass slices of `WorldMap` data to [`PathfindingTool`] methods; do not embed +//! search logic in `WorldMap` itself. +//! +//! [`PathfindingTool`]: crate::tools::PathfindingTool + +use std::collections::{HashMap, HashSet}; + +/// Learned spatial model of a single ARC-AGI level. +/// +/// The map is built incrementally from observed `(state, action) → next_state` +/// tuples. Wall constraints are added whenever the player attempts an action +/// and the state does not change. +/// +/// # Invariants +/// +/// - A `(from, action)` pair is removed from `transitions` when added to `walls`. +/// - `state_positions` is only set when a transition is first recorded so that +/// the stored coordinate corresponds to the observed player location. +#[derive(Debug, Clone, Default)] +pub struct WorldMap { + /// Observed passable transitions: `state → (action → next_state)`. + pub transitions: HashMap>, + + /// Known wall directions per state: `state → {blocked_action, ...}`. + pub walls: HashMap>, + + /// States where the player triggered a significant environment event + /// (e.g. rotation modifier, level entry). Used to guide BFS in later trials. + pub milestones: Vec, + + /// The state/action pair that immediately preceded a Win - used to replay + /// a winning route in subsequent trials. + pub win_predecessor: Option<(u64, u32)>, + + /// Pixel coordinate of each known state, set when a transition is first observed. + pub state_positions: HashMap, +} + +impl WorldMap { + /// Creates an empty world map. + /// + /// # Time complexity: O(1) + /// # Space complexity: O(1) + pub fn new() -> Self { + Self::default() + } + + /// Records a passable transition `from --action--> to`. + /// + /// Only **observed** transitions are stored; the map never infers reverse + /// transitions automatically because open-grid pixel changes mean that + /// `from --action--> to` does not imply `to --reverse_action--> from`. + /// + /// # Time complexity: O(1) amortised + /// # Space complexity: O(1) amortised + pub fn record_passage(&mut self, from: u64, action: u32, to: u64) { + self.transitions.entry(from).or_default().insert(action, to); + } + + /// Records a reverse (backtrack) transition `to --reverse_action--> from`. + /// + /// Call this only when the geometry guarantees that the reverse step is safe + /// (e.g. a simple bidirectional corridor, not a teleport or stair tile). + /// + /// # Time complexity: O(1) amortised + /// # Space complexity: O(1) amortised + pub fn record_reverse_passage(&mut self, from: u64, action: u32, to: u64, reverse_action: u32) { + self.transitions + .entry(to) + .or_default() + .insert(reverse_action, from); + if let Some(tr) = self.walls.get_mut(&to) { + tr.remove(&reverse_action); + } + let _ = (from, action); + } + + /// Records that `action` from `state` is blocked by a wall. + /// + /// Any previously inferred transition for the same `(state, action)` pair is + /// removed to maintain consistency. + /// + /// # Time complexity: O(1) amortised + /// # Space complexity: O(1) amortised + pub fn record_wall(&mut self, state: u64, action: u32) { + self.walls.entry(state).or_default().insert(action); + if let Some(tr) = self.transitions.get_mut(&state) { + tr.remove(&action); + } + } + + /// Records `state` as a milestone (e.g. modifier activated, level start). + /// + /// Duplicates are silently ignored. + /// + /// # Time complexity: O(M) where M = current milestone count + /// # Space complexity: O(1) + pub fn record_milestone(&mut self, state: u64) { + if !self.milestones.contains(&state) { + self.milestones.push(state); + } + } + + /// Returns `true` when `action` from `state` is known to be blocked. + /// + /// # Time complexity: O(1) + /// # Space complexity: O(1) + pub fn is_wall(&self, state: u64, action: u32) -> bool { + self.walls.get(&state).is_some_and(|w| w.contains(&action)) + } + + /// Returns the predicted next state for `(state, action)`, or `None` when unknown. + /// + /// # Time complexity: O(1) + /// # Space complexity: O(1) + pub fn predict(&self, state: u64, action: u32) -> Option { + self.transitions.get(&state)?.get(&action).copied() + } + + /// Computes summary statistics: `(unique_states, total_walls, total_passages)`. + /// + /// Used for logging and agent introspection. + /// + /// # Time complexity: O(S) where S = total states in the map + /// # Space complexity: O(S) + pub fn stats(&self) -> (usize, usize, usize) { + let mut states: HashSet = HashSet::new(); + for (&s, tr) in &self.transitions { + states.insert(s); + states.extend(tr.values()); + } + states.extend(self.walls.keys()); + let total_walls = self.walls.values().map(|v| v.len()).sum(); + let total_passages = self.transitions.values().map(|v| v.len()).sum(); + (states.len(), total_walls, total_passages) + } + + /// Collects wall data in the format required by [`PathfindingTool::spatial_astar`]. + /// + /// Returns a set of `((x, y), action)` entries for every wall whose state has + /// a known pixel position. + /// + /// # Time complexity: O(W) where W = total wall entries + /// # Space complexity: O(W) + pub fn pos_walls(&self) -> HashSet<((usize, usize), u32)> { + let mut result: HashSet<((usize, usize), u32)> = HashSet::new(); + for (&state, blocked) in &self.walls { + if let Some(&pos) = self.state_positions.get(&state) { + for &action in blocked { + result.insert((pos, action)); + } + } + } + result + } + + /// Returns the set of all pixel coordinates that have a known state mapping. + /// + /// Used as the `visited_coords` argument to [`PathfindingTool::spatial_astar`] so + /// the planner strongly prefers previously explored cells. + /// + /// # Time complexity: O(P) where P = state_positions entries + /// # Space complexity: O(P) + pub fn visited_pixel_coords(&self) -> HashSet<(usize, usize)> { + self.state_positions.values().copied().collect() + } + + /// Resets all learned map data. + /// + /// Called when the agent advances to a new level whose geometry differs entirely + /// from the previous one. + /// + /// # Time complexity: O(S + W) where S = states, W = walls + /// # Space complexity: O(1) + pub fn clear(&mut self) { + self.transitions.clear(); + self.walls.clear(); + self.milestones.clear(); + self.win_predecessor = None; + self.state_positions.clear(); + } +} diff --git a/examples/arc-lmm-agent/tests/pathfinding.rs b/examples/arc-lmm-agent/tests/pathfinding.rs new file mode 100644 index 0000000..22ac270 --- /dev/null +++ b/examples/arc-lmm-agent/tests/pathfinding.rs @@ -0,0 +1,77 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use arc_lmm_agent::tools::PathfindingTool; +use std::collections::{HashMap, HashSet}; + +fn simple_graph() -> HashMap> { + let mut t: HashMap> = HashMap::new(); + t.entry(1).or_default().insert(4, 2); + t.entry(2).or_default().insert(4, 3); + t.entry(3).or_default().insert(4, 4); + t +} + +#[test] +fn bfs_returns_path_in_simple_graph() { + let t = simple_graph(); + let path = PathfindingTool::bfs(1, 4, &t); + assert_eq!(path, Some(vec![4, 4, 4])); +} + +#[test] +fn bfs_same_state_returns_empty() { + let t = simple_graph(); + let path = PathfindingTool::bfs(1, 1, &t); + assert_eq!(path, Some(vec![])); +} + +#[test] +fn bfs_returns_none_when_unreachable() { + let t = simple_graph(); + let path = PathfindingTool::bfs(1, 99, &t); + assert!(path.is_none()); +} + +#[test] +fn spatial_astar_straight_line_right() { + let walls: HashSet<((usize, usize), u32)> = HashSet::new(); + let visited: HashSet<(usize, usize)> = (0..=60) + .step_by(5) + .flat_map(|x| (0..=60usize).step_by(5).map(move |y| (x, y))) + .collect(); + + let path = PathfindingTool::spatial_astar(0, 0, 20, 0, &walls, &visited); + assert!(path.is_some()); + let path = path.unwrap(); + assert!(path.iter().all(|&a| a == 4)); +} + +#[test] +fn reconstruct_path_correctness() { + let mut parents: HashMap = HashMap::new(); + parents.insert(2, (1, 4)); + parents.insert(3, (2, 4)); + parents.insert(4, (3, 4)); + let path = PathfindingTool::reconstruct_path(&parents, 1, 4); + assert_eq!(path, vec![4, 4, 4]); +} + +#[test] +fn reverse_path_is_correct() { + let forward = vec![4, 4, 2, 4]; + let rev = PathfindingTool::reverse_path(&forward); + assert_eq!(rev, vec![3, 1, 3, 3]); +} + +#[test] +fn action_next_pos_all_directions() { + assert_eq!(PathfindingTool::action_next_pos(10, 10, 1), (10, 5)); + assert_eq!(PathfindingTool::action_next_pos(10, 10, 2), (10, 15)); + assert_eq!(PathfindingTool::action_next_pos(10, 10, 3), (5, 10)); + assert_eq!(PathfindingTool::action_next_pos(10, 10, 4), (15, 10)); +} diff --git a/examples/arc-lmm-agent/tests/world.rs b/examples/arc-lmm-agent/tests/world.rs new file mode 100644 index 0000000..3beef7b --- /dev/null +++ b/examples/arc-lmm-agent/tests/world.rs @@ -0,0 +1,51 @@ +// Copyright 2026 Mahmoud Harmouch. +// +// Licensed under the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use arc_lmm_agent::world::WorldMap; + +#[test] +fn record_passage_and_predict() { + let mut map = WorldMap::new(); + map.record_passage(1, 4, 2); + assert_eq!(map.predict(1, 4), Some(2)); + assert_eq!(map.predict(1, 3), None); +} + +#[test] +fn record_wall_removes_transition() { + let mut map = WorldMap::new(); + map.record_passage(1, 4, 2); + map.record_wall(1, 4); + assert!(map.is_wall(1, 4)); + assert_eq!(map.predict(1, 4), None); +} + +#[test] +fn record_milestone_no_duplicates() { + let mut map = WorldMap::new(); + map.record_milestone(1); + map.record_milestone(1); + assert_eq!(map.milestones.len(), 1); +} + +#[test] +fn stats_counts_correctly() { + let mut map = WorldMap::new(); + map.record_passage(1, 4, 2); + map.record_passage(2, 4, 3); + map.record_wall(3, 1); + let (states, walls, passages) = map.stats(); + assert_eq!(states, 3); + assert_eq!(walls, 1); + assert_eq!(passages, 2); +} + +#[test] +fn predict_returns_none_for_unknown_state() { + let map = WorldMap::new(); + assert!(map.predict(999, 2).is_none()); +} diff --git a/lmm-agent/examples/causal_reasoning.rs b/lmm-agent/examples/causal_reasoning.rs index 8a17af5..8bc8d8c 100644 --- a/lmm-agent/examples/causal_reasoning.rs +++ b/lmm-agent/examples/causal_reasoning.rs @@ -88,7 +88,7 @@ async fn main() { println!("Step 1b: '{direct_cause}' is the direct cause. Tracing it to root causes..."); let report_ghg = agent.attribute_causes(&graph, direct_cause).unwrap(); if report_ghg.weights.is_empty() { - println!(" (no further parents — '{direct_cause}' is a root variable)"); + println!(" (no further parents - '{direct_cause}' is a root variable)"); } else { for (var, weight) in &report_ghg.weights { println!(" {var:<25} {weight:.3} [{}]", print_bar(*weight)); diff --git a/lmm-agent/src/cognition/learning/engine.rs b/lmm-agent/src/cognition/learning/engine.rs index eb00fc6..6593a57 100644 --- a/lmm-agent/src/cognition/learning/engine.rs +++ b/lmm-agent/src/cognition/learning/engine.rs @@ -137,6 +137,11 @@ impl LearningEngine { &self.q_table } + /// Resets the Q-table exploration epsilon. + pub fn reset_epsilon(&mut self, epsilon: f64) { + self.q_table.reset_epsilon(epsilon); + } + /// Returns the total number of completed episodes. pub fn episode_count(&self) -> usize { self.episode_count diff --git a/lmm-agent/src/cognition/learning/q_table.rs b/lmm-agent/src/cognition/learning/q_table.rs index e11b892..66be4ae 100644 --- a/lmm-agent/src/cognition/learning/q_table.rs +++ b/lmm-agent/src/cognition/learning/q_table.rs @@ -295,6 +295,11 @@ impl QTable { self.epsilon = (self.epsilon * self.epsilon_decay).max(self.epsilon_min); } + /// Resets ε to a specific value, useful for phase transitions or new levels. + pub fn reset_epsilon(&mut self, new_epsilon: f64) { + self.epsilon = new_epsilon.clamp(0.0, 1.0); + } + /// Returns an iterator over `(state, action, q_value)` triples. pub fn entries(&self) -> impl Iterator + '_ { self.table