diff --git a/tilth/Cargo.lock b/tilth/Cargo.lock index 3d8ea5fc..a8abf1ca 100644 --- a/tilth/Cargo.lock +++ b/tilth/Cargo.lock @@ -2,6 +2,54 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "agent-client-protocol" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882b815ffa67b023e1b40e7ea3bab3f05869654e8565d6811870c5545285e1d3" +dependencies = [ + "agent-client-protocol-derive", + "agent-client-protocol-schema", + "async-process", + "blocking", + "futures", + "futures-concurrency", + "rustc-hash", + "schemars 1.2.1", + "serde", + "serde_json", + "shell-words", + "tracing", + "uuid", + "windows-sys", +] + +[[package]] +name = "agent-client-protocol-derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5ca63f112bd2459bcaf9eda0683b9ba95fc3b5e5fdd9036ca941c6a09345b1" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "agent-client-protocol-schema" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06679e1542356341f4550ccfb16338b64f37f6af70de2105446ef6fbb078234c" +dependencies = [ + "anyhow", + "derive_more", + "schemars 1.2.1", + "serde", + "serde_json", + "serde_with", + "strum", + "tracing", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -11,6 +59,15 @@ 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 = "0.6.21" @@ -67,12 +124,135 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.1" @@ -84,6 +264,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "cc" version = "1.2.56" @@ -100,6 +292,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "clap" version = "4.5.60" @@ -155,6 +359,30 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -180,6 +408,40 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -194,6 +456,44 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" @@ -234,6 +534,27 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -246,12 +567,132 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[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-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + +[[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-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[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.4.2" @@ -315,6 +756,12 @@ dependencies = [ "memmap2", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -342,12 +789,54 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[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 = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "ignore" version = "0.4.25" @@ -364,6 +853,17 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -388,6 +888,17 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -436,6 +947,32 @@ dependencies = [ "libc", ] +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[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.3" @@ -448,6 +985,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot_core" version = "0.9.12" @@ -461,6 +1004,63 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "prettyplease" version = "0.2.37" @@ -524,6 +1124,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.3" @@ -553,6 +1173,21 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -566,6 +1201,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "same-file" version = "1.0.6" @@ -575,6 +1216,43 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -617,13 +1295,24 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +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 = [ - "indexmap", + "indexmap 2.13.0", "itoa", "memchr", "serde", @@ -640,12 +1329,66 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[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 = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -664,6 +1407,27 @@ 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 = "syn" version = "2.0.117" @@ -726,6 +1490,113 @@ dependencies = [ "tree-sitter-typescript", ] +[[package]] +name = "tilth-acp-agent" +version = "0.1.0" +dependencies = [ + "agent-client-protocol", + "futures-util", + "serde_json", + "tilth", + "tokio", +] + +[[package]] +name = "tilth-acp-client" +version = "0.1.0" +dependencies = [ + "agent-client-protocol", + "futures-util", + "serde_json", + "tokio", + "tokio-util", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "windows-sys", +] + +[[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-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -753,7 +1624,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime", @@ -767,6 +1638,37 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[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", +] + [[package]] name = "tree-sitter" version = "0.25.10" @@ -943,6 +1845,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -955,6 +1863,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -965,6 +1884,12 @@ dependencies = [ "winapi-util", ] +[[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.2+wasi-0.2.9" @@ -983,6 +1908,51 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -1000,7 +1970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.13.0", "wasm-encoder", "wasmparser", ] @@ -1013,7 +1983,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.13.0", "semver", ] @@ -1026,12 +1996,65 @@ dependencies = [ "windows-sys", ] +[[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-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.61.2" @@ -1078,7 +2101,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.13.0", "prettyplease", "syn", "wasm-metadata", @@ -1109,7 +2132,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap", + "indexmap 2.13.0", "log", "serde", "serde_derive", @@ -1128,7 +2151,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.13.0", "log", "semver", "serde", diff --git a/tilth/Cargo.toml b/tilth/Cargo.toml index 025a428c..6897bb02 100644 --- a/tilth/Cargo.toml +++ b/tilth/Cargo.toml @@ -85,3 +85,7 @@ inherits = "release" lto = "thin" codegen-units = 8 strip = false + +[workspace] +members = [".", "crates/acp-agent", "crates/acp-client"] +resolver = "2" diff --git a/tilth/crates/acp-agent/Cargo.toml b/tilth/crates/acp-agent/Cargo.toml new file mode 100644 index 00000000..93f0edf6 --- /dev/null +++ b/tilth/crates/acp-agent/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tilth-acp-agent" +version = "0.1.0" +edition = "2021" +description = "ACP (Agent Client Protocol) agent exposing tilth code intelligence — spawnable by Zed, JetBrains, and Eclipse Theia (aurelio-theia)." +license = "MIT" + +[[bin]] +name = "tilth-acp-agent" +path = "src/main.rs" + +[dependencies] +agent-client-protocol = "1.2.0" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "time"] } +serde_json = "1" +futures-util = { version = "0.3", default-features = false, features = ["std"] } +tilth = { path = "../.." } diff --git a/tilth/crates/acp-agent/src/main.rs b/tilth/crates/acp-agent/src/main.rs new file mode 100644 index 00000000..dc379a5e --- /dev/null +++ b/tilth/crates/acp-agent/src/main.rs @@ -0,0 +1,166 @@ +//! tilth-acp-agent — ACP agent that exposes tilth code intelligence to any +//! ACP client (Zed, JetBrains, Eclipse Theia / aurelio-theia). +//! +//! Wire-compatible with the TypeScript `AcpAgent` in aurelio-backend and the +//! `shared/acp/golden/agent` corpus: protocol version 1, NDJSON over stdio. +//! +//! Phase 3 scope: a real initialize / session/new / session/prompt / +//! session/cancel loop that routes the user's prompt through `tilth_search` +//! and streams the result back as an agent message chunk. + +use std::sync::atomic::{AtomicU64, Ordering}; + +use agent_client_protocol::schema::v1::{ + AgentCapabilities, ContentBlock, ContentChunk, Implementation, InitializeRequest, + InitializeResponse, NewSessionRequest, NewSessionResponse, PromptCapabilities, PromptRequest, + PromptResponse, SessionNotification, SessionUpdate, StopReason, TextContent, +}; +use agent_client_protocol::{ + Agent, Channel, Client, ConnectionTo, Dispatch, RawJsonRpcMessage, Result, +}; +use futures_util::StreamExt; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; + +static SESSION_SEQ: AtomicU64 = AtomicU64::new(1); + +fn text_of(blocks: &[ContentBlock]) -> String { + let mut out = String::new(); + for b in blocks { + if let ContentBlock::Text(t) = b { + if !out.is_empty() { + out.push('\n'); + } + out.push_str(&t.text); + } + } + out +} + +#[tokio::main] +async fn main() -> Result<()> { + // Transport-agnostic NDJSON pump. We own both ends of stdio so stdin EOF is + // an explicit signal: the Rust SDK's server-mode `connect_to(Stdio)` never + // resolves on a clean inbound EOF (its `run_until` waits on a + // `future::pending()` foreground), unlike the TS AgentSideConnection which + // exits on EOF. A `Channel::duplex` + two pumps gives us a clean, race-free + // termination path for pipes AND regular-file redirection. + let (agent_end, pump_end) = Channel::duplex(); + let Channel { + rx: agent_to_us, + tx: us_to_agent, + } = pump_end; + + // stdin -> agent (requests). On EOF this task returns; the agent is kept + // alive (fire-and-forget) so it can finish flushing the in-flight response + // during the grace window below. We do NOT close us_to_agent ourselves — + // the agent's connect_to is server-mode and never returns on a clean + // inbound EOF (same `run_until`/`future::pending()` footgun as Stdio), so + // waiting on it would hang. Instead we terminate the process after a grace. + let stdin_pump = tokio::spawn(async move { + let stdin = BufReader::new(tokio::io::stdin()); + let mut lines = stdin.lines(); + while let Ok(Some(line)) = lines.next_line().await { + if line.trim().is_empty() { + continue; + } + let item = match serde_json::from_str::(&line) { + Ok(m) => Ok(m), + Err(_) => Err(agent_client_protocol::Error::parse_error()), + }; + if us_to_agent.unbounded_send(item).is_err() { + break; + } + } + }); + + // agent -> stdout (responses / notifications), fire-and-forget. + tokio::spawn(async move { + let mut stdout = tokio::io::stdout(); + let mut rx = agent_to_us; + while let Some(item) = rx.next().await { + if let Ok(msg) = item { + if let Ok(mut s) = serde_json::to_string(&msg) { + s.push('\n'); + if stdout.write_all(s.as_bytes()).await.is_err() { + break; + } + let _ = stdout.flush().await; + } + } + } + }); + + let _agent_run = tokio::spawn(async move { build_agent().connect_to(agent_end).await }); + + // Terminate on stdin EOF (works for pipes and regular files): the stdin + // pump returns once the input is exhausted; a short grace lets the agent + // emit the final response; then we exit — matching TS AgentSideConnection + // EOF semantics without ever awaiting the SDK's server-mode future. + let _ = stdin_pump.await; + tokio::time::sleep(std::time::Duration::from_millis(400)).await; + std::process::exit(0); +} + +fn build_agent() -> agent_client_protocol::Builder< + Agent, + impl agent_client_protocol::HandleDispatchFrom, + impl agent_client_protocol::RunWithConnectionTo, +> { + Agent + .builder() + .name("tilth-acp-agent") + .on_receive_request( + async |req: InitializeRequest, responder, _cx| { + // Match shared/acp/golden/agent initialize: loadSession:false, + // promptCapabilities{image:false,audio:false,embeddedContext:true}. + let caps = AgentCapabilities::new() + .prompt_capabilities(PromptCapabilities::new().embedded_context(true)); + responder.respond( + InitializeResponse::new(req.protocol_version) + .agent_capabilities(caps) + .agent_info(Implementation::new("tilth-acp-agent", "0.1.0")), + ) + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + async |_req: NewSessionRequest, responder, _cx| { + let n = SESSION_SEQ.fetch_add(1, Ordering::Relaxed); + responder.respond(NewSessionResponse::new(format!("tilth-{n}"))) + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_request( + async |req: PromptRequest, responder, cx: ConnectionTo| { + let sid = req.session_id; + let query = text_of(&req.prompt); + let q = query.clone(); + let result = tokio::task::spawn_blocking(move || run_search(&q)) + .await + .unwrap_or_else(|e| Err(format!("tilth task join: {e}"))); + let body = match result { + Ok(text) => text, + Err(e) => format!("tilth error: {e}"), + }; + let chunk = SessionUpdate::AgentMessageChunk(ContentChunk::new( + ContentBlock::Text(TextContent::new(body)), + )); + let _ = cx.send_notification(SessionNotification::new(sid, chunk)); + responder.respond(PromptResponse::new(StopReason::EndTurn)) + }, + agent_client_protocol::on_receive_request!(), + ) + .on_receive_dispatch( + async |msg: Dispatch, cx| { + msg.respond_with_error( + agent_client_protocol::util::internal_error("unhandled message"), + cx, + ) + }, + agent_client_protocol::on_receive_dispatch!(), + ) +} + +fn run_search(query: &str) -> std::result::Result { + tilth::mcp::search(query) +} diff --git a/tilth/crates/acp-client/Cargo.toml b/tilth/crates/acp-client/Cargo.toml new file mode 100644 index 00000000..84eb08eb --- /dev/null +++ b/tilth/crates/acp-client/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tilth-acp-client" +version = "0.1.0" +edition = "2021" +description = "ACP (Agent Client Protocol) client — Rust counterpart of aurelio-backend's AcpClient. Drives any ACP agent (tilth-acp-agent, claude-code, gemini, kimi)." +license = "MIT" + +[[bin]] +name = "tilth-acp-client" +path = "src/main.rs" + +[dependencies] +agent-client-protocol = "1.2.0" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "time", "fs", "process"] } +tokio-util = { version = "0.7", features = ["compat"] } +serde_json = "1" +futures-util = { version = "0.3", default-features = false, features = ["std"] } diff --git a/tilth/crates/acp-client/src/main.rs b/tilth/crates/acp-client/src/main.rs new file mode 100644 index 00000000..1de5616f --- /dev/null +++ b/tilth/crates/acp-client/src/main.rs @@ -0,0 +1,276 @@ +//! tilth-acp-client — ACP client (Rust counterpart of aurelio-backend's +//! `AcpClient`). It is the side an ACP *agent* talks to: it handles the +//! agent->client requests `fs/read_text_file` and `session/request_permission`, +//! and absorbs `session/update` notifications. +//! +//! Two modes, selected by argv: +//! +//! * **Passive (default, no args)** — speak ACP over stdin/stdout as a +//! server-of-client-methods, conforming to `shared/acp/golden/client/`. +//! Used by `npm run test:tilth-client`. +//! * **Active (`--drive [args...]`)** — spawn an ACP agent over a +//! pipe, run `initialize → session/new → session/prompt`, stream each +//! `session/update` chunk to stdout as NDJSON, and print the final +//! `stopReason`. Used by `npm run test:interop-rust` (Rust client → Rust +//! agent) and is the Rust counterpart of `AcpClient::prompt`. +//! +//! Wire-compatible with the TypeScript `AcpClient` in +//! `aurelio-theia/aurelio-backend/src/acp/` against `shared/acp/golden/client/`. +//! +//! Termination note (passive mode): identical to `tilth-acp-agent`. The Rust +//! SDK's server-only `connect_to(transport)` never resolves on a clean inbound +//! EOF — internally `run_until(background, future::pending())` keeps waiting +//! after the transport actors finish. So we pump NDJSON through a +//! `Channel::duplex()` we own and terminate on stdin EOF after a short grace +//! (see Phase 3 notes in `.aurelio/knowledge/acp.md`). Active mode uses +//! `connect_with`, which returns when the driver closure finishes — no hang. + +use std::process; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use agent_client_protocol::schema::v1::{ + ContentBlock, InitializeRequest, NewSessionRequest, PromptRequest, ReadTextFileRequest, + ReadTextFileResponse, RequestPermissionOutcome, RequestPermissionRequest, + RequestPermissionResponse, SelectedPermissionOutcome, SessionNotification, StopReason, + TextContent, +}; +use agent_client_protocol::schema::ProtocolVersion; +use agent_client_protocol::{Agent, ByteStreams, Channel, Client, RawJsonRpcMessage, Result}; +use futures_util::StreamExt; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::process::Command; +use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; + +/// Grace between inbound EOF and process exit, so the SDK can flush the +/// in-flight response it has already buffered from the now-closed stdin. +const EOF_FLUSH_GRACE: Duration = Duration::from_millis(300); + +/// Optional sink invoked for each `session/update` (active mode only). +type UpdateSink = Option>; + +fn build_client( + on_update: UpdateSink, +) -> agent_client_protocol::Builder< + Client, + impl agent_client_protocol::HandleDispatchFrom, + impl agent_client_protocol::RunWithConnectionTo, +> { + Client + .builder() + // fs/read_text_file: read the requested path from disk (path is whatever + // the agent/session scoped; confinement is the agent's responsibility). + // Return the raw bytes as UTF-8 (lossy) so the agent always gets content. + .on_receive_request( + async |req: ReadTextFileRequest, responder, _cx| { + let body = match tokio::fs::read_to_string(&req.path).await { + Ok(s) => s, + // Lossy fallback for non-UTF8 or unreadable files: read raw + // bytes and re-encode; if even that fails, surface the error + // as content (never fail the JSON-RPC call itself). + Err(_) => match tokio::fs::read(&req.path).await { + Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(), + Err(e) => format!("tilth-acp-client: read failed: {e}"), + }, + }; + responder.respond(ReadTextFileResponse::new(body)) + }, + agent_client_protocol::on_receive_request!(), + ) + // session/request_permission: deterministic policy. Pick the first + // `allow_*` option (prefer `allow-once`); if none allow, cancel. + .on_receive_request( + async |req: RequestPermissionRequest, responder, _cx| { + let chosen = req + .options + .iter() + .find(|o| o.option_id.0.as_ref() == "allow-once") + .or_else(|| { + req.options + .iter() + .find(|o| o.option_id.0.as_ref().starts_with("allow")) + }) + .map(|o| o.option_id.clone()); + let outcome = match chosen { + Some(id) => { + RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(id)) + } + None => RequestPermissionOutcome::Cancelled, + }; + responder.respond(RequestPermissionResponse::new(outcome)) + }, + agent_client_protocol::on_receive_request!(), + ) + // session/update: client absorbs stream notifications. In active mode a + // sink prints each one (NDJSON) so a harness can assert a non-empty + // agent_message_chunk; in passive mode this is a no-op. + .on_receive_notification( + async move |notif: SessionNotification, _cx| { + if let Some(cb) = &on_update { + cb(¬if); + } + Ok(()) + }, + agent_client_protocol::on_receive_notification!(), + ) +} + +#[tokio::main] +async fn main() -> Result<()> { + // Mode split: `--drive [args...]` runs the active driver; + // anything else (incl. no args) is the passive conformance server. + let mut args = std::env::args().skip(1); + if matches!(args.next().as_deref(), Some("--drive")) { + let agent: Vec = args.collect(); + return drive(agent).await; + } + serve_passive().await +} + +/// Passive mode: speak ACP over stdin/stdout as a server-of-client-methods. +async fn serve_passive() -> Result<()> { + let (client_end, pump_end) = Channel::duplex(); + let Channel { + rx: client_to_us, + tx: us_to_client, + } = pump_end; + + // stdin -> client (agent->client requests). On EOF this task returns; the + // client is kept alive (fire-and-forget) so it can finish flushing the + // in-flight response during the grace window below. We do NOT await the + // client's connect_to — it is server-mode and never returns on a clean + // inbound EOF (same `run_until`/`future::pending()` footgun as Stdio), so + // waiting on it would hang. Instead we terminate the process after a grace. + let stdin_pump = tokio::spawn(async move { + let stdin = BufReader::new(tokio::io::stdin()); + let mut lines = stdin.lines(); + while let Ok(Some(line)) = lines.next_line().await { + if line.trim().is_empty() { + continue; + } + let item = match serde_json::from_str::(&line) { + Ok(m) => Ok(m), + Err(_) => Err(agent_client_protocol::Error::parse_error()), + }; + if us_to_client.unbounded_send(item).is_err() { + break; + } + } + }); + + // client -> stdout (client->agent responses / notifications), fire-and-forget. + tokio::spawn(async move { + let mut stdout = tokio::io::stdout(); + let mut rx = client_to_us; + while let Some(item) = rx.next().await { + if let Ok(msg) = item { + if let Ok(mut s) = serde_json::to_string(&msg) { + s.push('\n'); + if stdout.write_all(s.as_bytes()).await.is_err() { + break; + } + let _ = stdout.flush().await; + } + } + } + }); + + let _client_run = tokio::spawn(async move { build_client(None).connect_to(client_end).await }); + + // Terminate on stdin EOF (works for pipes and regular files): the stdin + // pump returns once the input is exhausted; a short grace lets the client + // emit the final response; then we exit — matching TS ClientSideConnection + // EOF semantics without ever awaiting the SDK's server-mode future. + let _ = stdin_pump.await; + tokio::time::sleep(EOF_FLUSH_GRACE).await; + process::exit(0); +} + +/// Active mode: spawn an ACP agent and run one prompt turn to completion. +/// +/// Prints each `session/update` notification to stdout as NDJSON (so a harness +/// can assert a non-empty `agent_message_chunk`), and finally a single +/// `{"stopReason":"end_turn"}` summary line. Exits non-zero on any protocol +/// error, a non-`EndTurn` stop, or a wall-clock timeout. +async fn drive(agent: Vec) -> Result<()> { + if agent.is_empty() { + eprintln!("tilth-acp-client --drive: missing agent command"); + process::exit(2); + } + let prompt_text = + std::env::var("INTEROP_PROMPT").unwrap_or_else(|_| "tilth_search".to_string()); + let cwd = std::env::var("INTEROP_CWD") + .map(std::path::PathBuf::from) + .unwrap_or_else(|_| std::env::current_dir().expect("cwd")); + + let mut cmd = Command::new(&agent[0]); + cmd.args(&agent[1..]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .kill_on_drop(true); + let mut child = cmd.spawn().map_err(|e| { + agent_client_protocol::Error::internal_error().data(format!("spawn {}: {e}", agent[0])) + })?; + let child_in = child.stdin.take().expect("child stdin piped"); + let child_out = child.stdout.take().expect("child stdout piped"); + let transport = ByteStreams::new(child_in.compat_write(), child_out.compat()); + + // Sink: print each session/update as NDJSON and record that we saw one. + let saw_update = Arc::new(AtomicBool::new(false)); + let sink: Arc = { + let saw = Arc::clone(&saw_update); + Arc::new(move |notif| { + saw.store(true, Ordering::Relaxed); + if let Ok(line) = serde_json::to_string(notif) { + println!("{line}"); + } + }) + }; + + // Bound the whole turn so a hung agent can't wedge CI. + let turn = build_client(Some(sink)).connect_with(transport, async move |cx| { + let init = cx + .send_request(InitializeRequest::new(ProtocolVersion::V1)) + .block_task() + .await?; + if init.protocol_version != ProtocolVersion::V1 { + return Err(agent_client_protocol::Error::internal_error().data(format!( + "unexpected protocol_version {:?}", + init.protocol_version + ))); + } + let sess = cx + .send_request(NewSessionRequest::new(cwd)) + .block_task() + .await?; + let session_id = sess.session_id; + let prompt = cx + .send_request(PromptRequest::new( + session_id, + vec![ContentBlock::Text(TextContent::new(prompt_text))], + )) + .block_task() + .await?; + Ok(prompt.stop_reason) + }); + let stop = tokio::time::timeout(Duration::from_secs(25), turn) + .await + .map_err(|_| agent_client_protocol::Error::internal_error().data("turn timed out"))??; + + // stop_reason serializes camelCase; compare structurally. + if stop != StopReason::EndTurn { + eprintln!("tilth-acp-client --drive: unexpected stopReason: {stop:?}"); + let _ = child.kill().await; + process::exit(1); + } + let saw_chunk = saw_update.load(Ordering::Relaxed); + let line = + serde_json::json!({ "stopReason": "end_turn", "sawChunk": saw_chunk }).to_string() + "\n"; + let mut out = tokio::io::stdout(); + let _ = out.write_all(line.as_bytes()).await; + let _ = out.flush().await; + let _ = child.kill().await; + Ok(()) +} diff --git a/tilth/src/mcp.rs b/tilth/src/mcp.rs index 2bd610d6..995ce6f1 100644 --- a/tilth/src/mcp.rs +++ b/tilth/src/mcp.rs @@ -562,6 +562,18 @@ fn tool_search( Ok(result) } +/// Library entry point for non-MCP callers (e.g. the ACP agent). Runs +/// `tilth_search` against the current working directory with a fresh cache / +/// session / index, returning the same formatted text the MCP tool would. +pub fn search(query: &str) -> Result { + let cache = OutlineCache::new(); + let session = Session::new(); + let index = Arc::new(SymbolIndex::new()); + let bloom = Arc::new(BloomFilterCache::new()); + let args = serde_json::json!({ "query": query }); + tool_search(&args, &cache, &session, &index, &bloom) +} + fn tool_files(args: &Value, cache: &OutlineCache) -> Result { let pattern = args .get("pattern")