diff --git a/.gitignore b/.gitignore index 5c0baf53..a30fcfa8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ node_modules/ # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +docs/** # Generated by cargo mutants # Contains mutation testing data diff --git a/Cargo.lock b/Cargo.lock index 96cb8fb4..12c617aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4125,8 +4125,7 @@ dependencies = [ "aws-smithy-types", "base64 0.22.1", "rig-core", - "rig-derive", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "tokio", @@ -4156,7 +4155,7 @@ dependencies = [ "pin-project-lite", "reqwest", "rig-derive", - "schemars", + "schemars 1.1.0", "serde", "serde_json", "thiserror 2.0.12", @@ -4406,6 +4405,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive 0.8.22", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "1.1.0" @@ -4414,11 +4425,23 @@ checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", - "schemars_derive", + "schemars_derive 1.1.0", "serde", "serde_json", ] +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[package]] name = "schemars_derive" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 77a3f1fa..982b332f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,7 @@ futures-util = "0.3" # Agent dependencies (using Rig - LLM application framework) rig-core = { version = "0.27", features = ["derive"] } -rig-bedrock = "0.3" # AWS Bedrock provider for Rig +rig-bedrock = { path = "vendor/rig-bedrock" } # Vendored with fix for extended thinking + tool calls # Diff rendering for file confirmation UI similar = "2.6" diff --git a/patches/rig-bedrock/.cargo-ok b/patches/rig-bedrock/.cargo-ok deleted file mode 100644 index 5f8b7958..00000000 --- a/patches/rig-bedrock/.cargo-ok +++ /dev/null @@ -1 +0,0 @@ -{"v":1} \ No newline at end of file diff --git a/patches/rig-bedrock/.cargo_vcs_info.json b/patches/rig-bedrock/.cargo_vcs_info.json deleted file mode 100644 index 435db7a9..00000000 --- a/patches/rig-bedrock/.cargo_vcs_info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "git": { - "sha1": "133cad1671e703f42449e52650a993a11a433fd6" - }, - "path_in_vcs": "rig-integrations/rig-bedrock" -} \ No newline at end of file diff --git a/patches/rig-bedrock/Cargo.lock b/patches/rig-bedrock/Cargo.lock deleted file mode 100644 index 69f4b425..00000000 --- a/patches/rig-bedrock/Cargo.lock +++ /dev/null @@ -1,3074 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "as-any" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063" - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -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-config" -version = "1.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c478f5b10ce55c9a33f87ca3404ca92768b144fc1bfdede7c0121214a8283a25" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sso", - "aws-sdk-ssooidc", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "hex", - "http 1.3.1", - "ring", - "time", - "tokio", - "tracing", - "url", - "zeroize", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1541072f81945fa1251f8795ef6c92c4282d74d59f88498ae7d4bf00f0ebdad9" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - -[[package]] -name = "aws-lc-rs" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "aws-runtime" -version = "1.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c034a1bc1d70e16e7f4e4caf7e9f7693e4c9c24cd91cf17c2a0b21abaebc7c8b" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-bedrockruntime" -version = "1.104.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1574d1fad8f4bbf71aeb5dbb16653e7db48463f031ae77fdc161621019364d4a" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "hyper 0.14.32", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sso" -version = "1.81.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ede098271e3471036c46957cba2ba30888f53bda2515bf04b560614a30a36e" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ssooidc" -version = "1.82.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43326f724ba2cc957e6f3deac0ca1621a3e5d4146f5970c24c8a108dac33070f" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.83.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5468593c47efc31fdbe6c902d1a5fde8d9c82f78a3f8ccfe907b1e9434748cb" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084c34162187d39e3740cb635acd73c4e3a551a36146ad6fe8883c929c9f876c" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.3.1", - "percent-encoding", - "sha2", - "time", - "tracing", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604c7aec361252b8f1c871a7641d5e0ba3a7f5a586e51b66bc9510a5519594d9" -dependencies = [ - "aws-smithy-types", - "bytes", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.62.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c4dacf2d38996cf729f55e7a762b30918229917eca115de45dfa8dfb97796c9" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http-client" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2 0.3.26", - "h2 0.4.10", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper 1.6.0", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", - "hyper-util", - "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.28", - "rustls-native-certs 0.8.1", - "rustls-pki-types", - "tokio", - "tower", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.61.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a16e040799d29c17412943bdbf488fd75db04112d0c0d4b9290bacf5ae0014b9" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-observability" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" -dependencies = [ - "aws-smithy-runtime-api", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e107ce0783019dbff59b3a244aa0c114e4a8c9d93498af9162608cd5474e796" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-http-client", - "aws-smithy-observability", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "http-body 1.0.1", - "pin-project-lite", - "pin-utils", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75d52251ed4b9776a3e8487b2a01ac915f73b2da3af8fc1e77e0fce697a550d4" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.12", - "http 1.3.1", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" -dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", -] - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", - "which", -] - -[[package]] -name = "bitflags" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - -[[package]] -name = "cc" -version = "1.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" -dependencies = [ - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - -[[package]] -name = "convert_case" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" -dependencies = [ - "unicode-segmentation", -] - -[[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.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "deluxe" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed332aaf752b459088acf3dd4eca323e3ef4b83c70a84ca48fb0ec5305f1488" -dependencies = [ - "deluxe-core", - "deluxe-macros", - "once_cell", - "proc-macro2", - "syn", -] - -[[package]] -name = "deluxe-core" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddada51c8576df9d6a8450c351ff63042b092c9458b8ac7d20f89cbd0ffd313" -dependencies = [ - "arrayvec", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "deluxe-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117" -dependencies = [ - "deluxe-core", - "heck", - "if_chain", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[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 = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[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.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "eventsource-stream" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" -dependencies = [ - "futures-core", - "nom", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[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.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "glob" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.3.1", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.3.1", -] - -[[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 1.3.1", - "http-body 1.0.1", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.10", - "http 1.3.1", - "http-body 1.0.1", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.21.12", - "rustls-native-certs 0.6.3", - "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.3.1", - "hyper 1.6.0", - "hyper-util", - "rustls 0.23.28", - "rustls-native-certs 0.8.1", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.6.0", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[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 = "if_chain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" - -[[package]] -name = "indexmap" -version = "2.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jobserver" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" -dependencies = [ - "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 = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "libc" -version = "0.2.177" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" - -[[package]] -name = "libloading" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" -dependencies = [ - "cfg-if", - "windows-targets 0.53.2", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "ordered-float" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2c1f9f56e534ac6a9b8a4600bdf0f530fb393b5f393e7b4d03489c3cf0c3f01" -dependencies = [ - "num-traits", -] - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "potential_utf" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" -dependencies = [ - "zerovec", -] - -[[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.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -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 = "redox_syscall" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" -dependencies = [ - "bitflags", -] - -[[package]] -name = "ref-cast" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - -[[package]] -name = "regex-syntax" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3160422bbd54dd5ecfdca71e5fd59b7b8fe2b1697ab2baf64f6d05dcc66d298" - -[[package]] -name = "reqwest" -version = "0.12.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "js-sys", - "log", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "rig-bedrock" -version = "0.3.9" -dependencies = [ - "anyhow", - "async-stream", - "aws-config", - "aws-sdk-bedrockruntime", - "aws-smithy-types", - "base64 0.22.1", - "reqwest", - "rig-core", - "rig-derive", - "schemars", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "rig-core" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3799afd8ba38d90d9886be5bf596b0159043f88598b40e1f5aa08aad488f2223" -dependencies = [ - "as-any", - "async-stream", - "base64 0.22.1", - "bytes", - "eventsource-stream", - "fastrand", - "futures", - "futures-timer", - "glob", - "http 1.3.1", - "mime", - "mime_guess", - "ordered-float", - "pin-project-lite", - "reqwest", - "schemars", - "serde", - "serde_json", - "thiserror", - "tokio", - "tracing", - "tracing-futures", - "url", -] - -[[package]] -name = "rig-derive" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f4b48f1449fa214d5cb11d0d0d952fd4c13b7ca5d1eaac64c87ce03cfb9e24" -dependencies = [ - "convert_case", - "deluxe", - "indoc", - "proc-macro2", - "quote", - "serde_json", - "syn", -] - -[[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.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[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 = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.23.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" -dependencies = [ - "aws-lc-rs", - "once_cell", - "rustls-pki-types", - "rustls-webpki 0.103.3", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "schannel", - "security-framework 2.11.1", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework 3.2.0", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "schemars" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" -dependencies = [ - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - -[[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_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.143" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[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 = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[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.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" -dependencies = [ - "libc", -] - -[[package]] -name = "slab" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[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.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -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 = "thiserror" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" -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.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" -dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "slab", - "socket2 0.6.0", - "tokio-macros", - "windows-sys 0.59.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls 0.23.28", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "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.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "futures", - "futures-task", - "pin-project", - "tracing", -] - -[[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.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "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 = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -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 = "uuid" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = [ - "getrandom 0.3.3", - "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 = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[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 = "wasi" -version = "0.14.2+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[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.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -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.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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] - -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/patches/rig-bedrock/Cargo.toml b/patches/rig-bedrock/Cargo.toml deleted file mode 100644 index 3c05aaf1..00000000 --- a/patches/rig-bedrock/Cargo.toml +++ /dev/null @@ -1,128 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2024" -name = "rig-bedrock" -version = "0.3.9" -build = false -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "AWS Bedrock model provider for Rig integration." -readme = "README.md" -license = "MIT" - -[lib] -name = "rig_bedrock" -path = "src/lib.rs" - -[[example]] -name = "agent_with_bedrock" -path = "examples/agent_with_bedrock.rs" - -[[example]] -name = "document_with_bedrock" -path = "examples/document_with_bedrock.rs" - -[[example]] -name = "embedding_with_bedrock" -path = "examples/embedding_with_bedrock.rs" - -[[example]] -name = "extractor_with_bedrock" -path = "examples/extractor_with_bedrock.rs" - -[[example]] -name = "image_generator" -path = "examples/image_generator.rs" - -[[example]] -name = "image_with_bedrock" -path = "examples/image_with_bedrock.rs" - -[[example]] -name = "rag_with_bedrock" -path = "examples/rag_with_bedrock.rs" - -[[example]] -name = "streaming_with_bedrock" -path = "examples/streaming_with_bedrock.rs" - -[[example]] -name = "streaming_with_bedrock_and_tools" -path = "examples/streaming_with_bedrock_and_tools.rs" - -[dependencies.async-stream] -version = "0.3.6" - -[dependencies.aws-config] -version = "1.8.5" -features = ["behavior-version-latest"] - -[dependencies.aws-sdk-bedrockruntime] -version = "1.102.0" - -[dependencies.aws-smithy-types] -version = "1.3.2" - -[dependencies.base64] -version = "0.22.1" - -[dependencies.rig-core] -version = "0.27.0" -features = ["image"] -default-features = false - -[dependencies.rig-derive] -version = "0.1.10" - -[dependencies.schemars] -version = "1.0.4" - -[dependencies.serde] -version = "1.0.219" -features = ["derive"] - -[dependencies.serde_json] -version = "1.0.140" - -[dependencies.tokio] -version = "1.45.1" -features = ["full"] - -[dependencies.tracing] -version = "0.1.41" - -[dependencies.uuid] -version = "1.17.0" -features = ["v4"] - -[dev-dependencies.anyhow] -version = "1.0.98" - -[dev-dependencies.reqwest] -version = "0.12.20" -features = [ - "json", - "stream", -] -default-features = false - -[dev-dependencies.tracing-subscriber] -version = "0.3.19" - -[lints.clippy] -dbg_macro = "forbid" -todo = "forbid" -unimplemented = "forbid" diff --git a/patches/rig-bedrock/Cargo.toml.orig b/patches/rig-bedrock/Cargo.toml.orig deleted file mode 100644 index b784d4ed..00000000 --- a/patches/rig-bedrock/Cargo.toml.orig +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "rig-bedrock" -version = "0.3.9" -edition = { workspace = true } -license = "MIT" -readme = "README.md" -description = "AWS Bedrock model provider for Rig integration." - -[lints] -workspace = true - -[dependencies] -async-stream = { workspace = true } -aws-config = { workspace = true, features = ["behavior-version-latest"] } -aws-sdk-bedrockruntime = { workspace = true } -aws-smithy-types = { workspace = true } -base64 = { workspace = true } -rig-core = { path = "../../rig/rig-core", version = "0.27.0", default-features = false, features = [ - "image", -] } -rig-derive = { path = "../../rig/rig-derive", version = "0.1.10" } -schemars = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true } -uuid = { workspace = true, features = ["v4"] } - -[dev-dependencies] -anyhow = { workspace = true } -reqwest = { workspace = true, features = ["json", "stream"] } -tracing-subscriber = { workspace = true } diff --git a/src/agent/commands.rs b/src/agent/commands.rs index 3be831e2..9ebb19de 100644 --- a/src/agent/commands.rs +++ b/src/agent/commands.rs @@ -72,6 +72,12 @@ pub const SLASH_COMMANDS: &[SlashCommand] = &[ description: "Manage provider profiles (multiple configs)", auto_execute: true, }, + SlashCommand { + name: "plans", + alias: None, + description: "Show incomplete plans and continue", + auto_execute: true, + }, SlashCommand { name: "exit", alias: Some("q"), @@ -80,13 +86,32 @@ pub const SLASH_COMMANDS: &[SlashCommand] = &[ }, ]; +/// Whether a token count is actual (from API) or approximate (estimated) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum TokenCountType { + /// Actual count from API response + Actual, + /// Approximate count estimated from character count (~chars/4) + #[default] + Approximate, +} + /// Token usage statistics for /cost command +/// Tracks actual vs approximate tokens similar to Forge #[derive(Debug, Default, Clone)] pub struct TokenUsage { /// Total prompt/input tokens pub prompt_tokens: u64, - /// Total completion/output tokens + /// Total completion/output tokens pub completion_tokens: u64, + /// Cache read tokens (prompt caching) + pub cache_read_tokens: u64, + /// Cache creation tokens (prompt caching) + pub cache_creation_tokens: u64, + /// Thinking/reasoning tokens (extended thinking models) + pub thinking_tokens: u64, + /// Whether the counts are actual or approximate + pub count_type: TokenCountType, /// Number of requests made pub request_count: u64, /// Session start time @@ -101,23 +126,100 @@ impl TokenUsage { } } - /// Add tokens from a request - pub fn add_request(&mut self, prompt: u64, completion: u64) { + /// Add actual tokens from API response + pub fn add_actual(&mut self, input: u64, output: u64) { + self.prompt_tokens += input; + self.completion_tokens += output; + self.request_count += 1; + // If we have any actual counts, mark as actual + if input > 0 || output > 0 { + self.count_type = TokenCountType::Actual; + } + } + + /// Add actual tokens with cache and thinking info + pub fn add_actual_extended( + &mut self, + input: u64, + output: u64, + cache_read: u64, + cache_creation: u64, + thinking: u64, + ) { + self.prompt_tokens += input; + self.completion_tokens += output; + self.cache_read_tokens += cache_read; + self.cache_creation_tokens += cache_creation; + self.thinking_tokens += thinking; + self.request_count += 1; + self.count_type = TokenCountType::Actual; + } + + /// Add estimated tokens (when API doesn't return actual counts) + /// Only updates if we don't already have actual counts for this session + pub fn add_estimated(&mut self, prompt: u64, completion: u64) { self.prompt_tokens += prompt; self.completion_tokens += completion; self.request_count += 1; + // Keep as Approximate unless we've received actual counts + } + + /// Legacy method for compatibility - adds estimated tokens + pub fn add_request(&mut self, prompt: u64, completion: u64) { + self.add_estimated(prompt, completion); } /// Estimate token count from text (rough approximation: ~4 chars per token) + /// Matches Forge's approach: char_count.div_ceil(4) pub fn estimate_tokens(text: &str) -> u64 { - (text.len() as f64 / 4.0).ceil() as u64 + text.len().div_ceil(4) as u64 } - /// Get total tokens + /// Get total tokens (input + output, excluding cache/thinking) pub fn total_tokens(&self) -> u64 { self.prompt_tokens + self.completion_tokens } + /// Get total tokens including cache reads (effective context size) + pub fn total_with_cache(&self) -> u64 { + self.prompt_tokens + self.completion_tokens + self.cache_read_tokens + } + + /// Format total tokens for display (with ~ prefix if approximate) + pub fn format_total(&self) -> String { + match self.count_type { + TokenCountType::Actual => format!("{}", self.total_tokens()), + TokenCountType::Approximate => format!("~{}", self.total_tokens()), + } + } + + /// Get a short display string like Forge: "~1.2k" or "15k" + pub fn format_compact(&self) -> String { + let total = self.total_tokens(); + let prefix = match self.count_type { + TokenCountType::Actual => "", + TokenCountType::Approximate => "~", + }; + + if total >= 1_000_000 { + format!("{}{:.1}M", prefix, total as f64 / 1_000_000.0) + } else if total >= 1_000 { + format!("{}{:.1}k", prefix, total as f64 / 1_000.0) + } else { + format!("{}{}", prefix, total) + } + } + + /// Check if we have cache hits (prompt caching is working) + pub fn has_cache_hits(&self) -> bool { + self.cache_read_tokens > 0 + } + + /// Check if we have thinking tokens (extended thinking enabled) + pub fn has_thinking(&self) -> bool { + self.thinking_tokens > 0 + } + /// Get session duration pub fn session_duration(&self) -> std::time::Duration { self.session_start @@ -151,13 +253,19 @@ impl TokenUsage { let duration = self.session_duration(); let (input_cost, output_cost, total_cost) = self.estimate_cost(model); + // Determine accuracy indicator + let accuracy_note = match self.count_type { + TokenCountType::Actual => format!("{}actual counts{}", ansi::SUCCESS, ansi::RESET), + TokenCountType::Approximate => format!("{}~approximate{}", ansi::DIM, ansi::RESET), + }; + println!(); println!(" {}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", ansi::PURPLE, ansi::RESET); println!(" {}💰 Session Cost & Usage{}", ansi::PURPLE, ansi::RESET); println!(" {}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", ansi::PURPLE, ansi::RESET); println!(); println!(" {}Model:{} {}", ansi::DIM, ansi::RESET, model); - println!(" {}Duration:{} {:02}:{:02}:{:02}", + println!(" {}Duration:{} {:02}:{:02}:{:02}", ansi::DIM, ansi::RESET, duration.as_secs() / 3600, (duration.as_secs() % 3600) / 60, @@ -165,17 +273,47 @@ impl TokenUsage { ); println!(" {}Requests:{} {}", ansi::DIM, ansi::RESET, self.request_count); println!(); - println!(" {}Tokens:{}", ansi::CYAN, ansi::RESET); - println!(" Input: {:>10} tokens", self.prompt_tokens); - println!(" Output: {:>10} tokens", self.completion_tokens); - println!(" {}Total: {:>10} tokens{}", ansi::BOLD, self.total_tokens(), ansi::RESET); + println!(" {}Tokens{} ({}){}:", ansi::CYAN, ansi::RESET, accuracy_note, ansi::RESET); + println!(" Input: {:>10} tokens", self.prompt_tokens); + println!(" Output: {:>10} tokens", self.completion_tokens); + + // Show cache tokens if present + if self.cache_read_tokens > 0 || self.cache_creation_tokens > 0 { + println!(); + println!(" {}Cache:{}", ansi::CYAN, ansi::RESET); + if self.cache_read_tokens > 0 { + println!(" Read: {:>10} tokens {}(saved){}", self.cache_read_tokens, ansi::SUCCESS, ansi::RESET); + } + if self.cache_creation_tokens > 0 { + println!(" Created: {:>10} tokens", self.cache_creation_tokens); + } + } + + // Show thinking tokens if present + if self.thinking_tokens > 0 { + println!(); + println!(" {}Thinking:{}", ansi::CYAN, ansi::RESET); + println!(" Reasoning:{:>10} tokens", self.thinking_tokens); + } + + println!(); + println!(" {}Total: {:>10} tokens{}", ansi::BOLD, self.format_total(), ansi::RESET); println!(); println!(" {}Estimated Cost:{}", ansi::SUCCESS, ansi::RESET); println!(" Input: ${:.4}", input_cost); println!(" Output: ${:.4}", output_cost); println!(" {}Total: ${:.4}{}", ansi::BOLD, total_cost, ansi::RESET); println!(); - println!(" {}(Estimates based on public API pricing){}", ansi::DIM, ansi::RESET); + + // Show note about accuracy + match self.count_type { + TokenCountType::Actual => { + println!(" {}(Based on actual API usage){}", ansi::DIM, ansi::RESET); + } + TokenCountType::Approximate => { + println!(" {}(Estimates based on ~4 chars/token){}", ansi::DIM, ansi::RESET); + } + } println!(); } } diff --git a/src/agent/history.rs b/src/agent/history.rs index 5fde1f0d..365f3f55 100644 --- a/src/agent/history.rs +++ b/src/agent/history.rs @@ -111,8 +111,9 @@ impl ConversationHistory { }) } - /// Estimate tokens in a string - fn estimate_tokens(text: &str) -> usize { + /// Estimate tokens in a string (~4 characters per token) + /// Public so it can be used for pre-request context size estimation + pub fn estimate_tokens(text: &str) -> usize { text.len() / CHARS_PER_TOKEN } @@ -360,6 +361,25 @@ impl ConversationHistory { )) } + /// Emergency compaction - more aggressive than normal + /// Used when "input too long" error occurs and we need to reduce context urgently. + /// Temporarily switches to aggressive config, compacts, then restores original. + pub fn emergency_compact(&mut self) -> Option { + // Switch to aggressive config temporarily + let original_config = self.compact_config.clone(); + self.compact_config = CompactConfig { + retention_window: 3, // Keep only 3 most recent turns + eviction_window: 0.9, // Evict 90% of context + thresholds: CompactThresholds::aggressive(), + }; + + let result = self.compact(); + + // Restore original config + self.compact_config = original_config; + result + } + /// Convert history to Rig Message format for the agent /// Uses structured summary frames to preserve context pub fn to_messages(&self) -> Vec { diff --git a/src/agent/ide/client.rs b/src/agent/ide/client.rs index c42ff69e..a3af43b4 100644 --- a/src/agent/ide/client.rs +++ b/src/agent/ide/client.rs @@ -498,6 +498,64 @@ impl IdeClient { } } + /// Get diagnostics from the IDE's language servers + /// + /// This queries the IDE for all diagnostic messages (errors, warnings, etc.) + /// from the active language servers (rust-analyzer, ESLint, TypeScript, etc.) + /// + /// If `file_path` is provided, returns diagnostics only for that file. + /// Otherwise returns all diagnostics across the workspace. + pub async fn get_diagnostics(&self, file_path: Option<&str>) -> Result { + if !self.is_connected() { + return Err(IdeError::ConnectionFailed("Not connected to IDE".to_string())); + } + + let params = serde_json::to_value(ToolCallParams { + name: "getDiagnostics".to_string(), + arguments: serde_json::to_value(GetDiagnosticsArgs { + uri: file_path.map(|p| format!("file://{}", p)), + }) + .unwrap(), + }) + .unwrap(); + + let response = self.send_request("tools/call", params).await?; + + // Parse the response + if let Some(result) = response.result { + if let Ok(tool_result) = serde_json::from_value::(result) { + // Look for the text content with diagnostics + for content in tool_result.content { + if content.content_type == "text" { + if let Some(text) = content.text { + // Try to parse as DiagnosticsResponse + if let Ok(diag_response) = serde_json::from_str::(&text) { + return Ok(diag_response); + } + // Try parsing as raw array of diagnostics + if let Ok(diagnostics) = serde_json::from_str::>(&text) { + let total_errors = diagnostics.iter().filter(|d| d.severity == DiagnosticSeverity::Error).count() as u32; + let total_warnings = diagnostics.iter().filter(|d| d.severity == DiagnosticSeverity::Warning).count() as u32; + return Ok(DiagnosticsResponse { + diagnostics, + total_errors, + total_warnings, + }); + } + } + } + } + } + } + + // No diagnostics found - return empty response + Ok(DiagnosticsResponse { + diagnostics: Vec::new(), + total_errors: 0, + total_warnings: 0, + }) + } + /// Disconnect from the IDE pub async fn disconnect(&mut self) { // Close any pending diffs diff --git a/src/agent/ide/mod.rs b/src/agent/ide/mod.rs index 14f24366..ff6a76f8 100644 --- a/src/agent/ide/mod.rs +++ b/src/agent/ide/mod.rs @@ -9,3 +9,4 @@ pub mod client; pub use client::{IdeClient, DiffResult, IdeError}; pub use detect::{IdeInfo, detect_ide, get_ide_process_info}; +pub use types::{Diagnostic, DiagnosticSeverity, DiagnosticsResponse}; diff --git a/src/agent/ide/types.rs b/src/agent/ide/types.rs index 2594d335..39da6dac 100644 --- a/src/agent/ide/types.rs +++ b/src/agent/ide/types.rs @@ -173,3 +173,75 @@ pub struct CloseDiffArgs { #[serde(rename = "suppressNotification", skip_serializing_if = "Option::is_none")] pub suppress_notification: Option, } + +/// Get diagnostics request arguments +#[derive(Debug, Serialize)] +pub struct GetDiagnosticsArgs { + /// Optional file URI to get diagnostics for. If not provided, gets all diagnostics. + #[serde(skip_serializing_if = "Option::is_none")] + pub uri: Option, +} + +/// Diagnostic severity levels (matches VS Code DiagnosticSeverity) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +pub enum DiagnosticSeverity { + Error = 0, + Warning = 1, + Information = 2, + Hint = 3, +} + +impl DiagnosticSeverity { + pub fn from_number(n: u8) -> Self { + match n { + 0 => Self::Error, + 1 => Self::Warning, + 2 => Self::Information, + _ => Self::Hint, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::Error => "error", + Self::Warning => "warning", + Self::Information => "info", + Self::Hint => "hint", + } + } +} + +/// A diagnostic message from the language server +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Diagnostic { + /// The file path where the diagnostic occurred + pub file: String, + /// Line number (1-based) + pub line: u32, + /// Column number (1-based) + pub column: u32, + /// End line number (1-based) + #[serde(rename = "endLine")] + pub end_line: Option, + /// End column number (1-based) + #[serde(rename = "endColumn")] + pub end_column: Option, + /// Severity level + pub severity: DiagnosticSeverity, + /// The diagnostic message + pub message: String, + /// Source of the diagnostic (e.g., "rust-analyzer", "eslint") + #[serde(default)] + pub source: Option, + /// Diagnostic code + #[serde(default)] + pub code: Option, +} + +/// Response from getDiagnostics +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct DiagnosticsResponse { + pub diagnostics: Vec, + pub total_errors: u32, + pub total_warnings: u32, +} diff --git a/src/agent/mod.rs b/src/agent/mod.rs index ee2e179a..168f870d 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -38,7 +38,6 @@ pub mod prompts; pub mod session; pub mod tools; pub mod ui; - use colored::Colorize; use history::{ConversationHistory, ToolCallRecord}; use ide::IdeClient; @@ -47,7 +46,7 @@ use rig::{ completion::Prompt, providers::{anthropic, openai}, }; -use session::ChatSession; +use session::{ChatSession, PlanMode}; use commands::TokenUsage; use std::path::Path; use std::sync::Arc; @@ -101,8 +100,13 @@ pub enum AgentError { pub type AgentResult = Result; -/// Get the system prompt for the agent based on query type -fn get_system_prompt(project_path: &Path, query: Option<&str>) -> String { +/// Get the system prompt for the agent based on query type and plan mode +fn get_system_prompt(project_path: &Path, query: Option<&str>, plan_mode: PlanMode) -> String { + // In planning mode, use the read-only exploration prompt + if plan_mode.is_planning() { + return prompts::get_planning_prompt(project_path); + } + if let Some(q) = query { // First check if it's a code development task (highest priority) if prompts::is_code_development_query(q) { @@ -169,16 +173,51 @@ pub async fn run_interactive( session.print_banner(); + // Raw Rig messages for multi-turn - preserves Reasoning blocks for thinking + // Our ConversationHistory only stores text summaries, but rig needs full Message structure + let mut raw_chat_history: Vec = Vec::new(); + + // Pending input for auto-continue after plan creation + let mut pending_input: Option = None; + // Auto-accept mode for plan execution (skips write confirmations) + let mut auto_accept_writes = false; + loop { // Show conversation status if we have history if !conversation_history.is_empty() { println!("{}", format!(" 💬 Context: {}", conversation_history.status()).dimmed()); } - // Read user input - let input = match session.read_input() { - Ok(input) => input, - Err(_) => break, + // Check for pending input (from plan menu selection) + let input = if let Some(pending) = pending_input.take() { + // Show what we're executing + println!("{} {}", "→".cyan(), pending.dimmed()); + pending + } else { + // New user turn - reset auto-accept mode from previous plan execution + auto_accept_writes = false; + + // Read user input (returns InputResult) + let input_result = match session.read_input() { + Ok(result) => result, + Err(_) => break, + }; + + // Handle the input result + match input_result { + ui::InputResult::Submit(text) => ChatSession::process_submitted_text(&text), + ui::InputResult::Cancel | ui::InputResult::Exit => break, + ui::InputResult::TogglePlanMode => { + // Toggle planning mode - minimal feedback, no extra newlines + let new_mode = session.toggle_plan_mode(); + if new_mode.is_planning() { + println!("{}", "★ plan mode".yellow()); + } else { + println!("{}", "▶ standard mode".green()); + } + continue; + } + } }; if input.is_empty() { @@ -190,6 +229,7 @@ pub async fn run_interactive( // Special handling for /clear to also clear conversation history if input.trim().to_lowercase() == "/clear" || input.trim().to_lowercase() == "/c" { conversation_history.clear(); + raw_chat_history.clear(); } match session.process_command(&input) { Ok(true) => continue, @@ -215,6 +255,26 @@ pub async fn run_interactive( } } + // Pre-request check: estimate if we're approaching context limit + // Check raw_chat_history (actual messages) not conversation_history + // because conversation_history may be out of sync + let estimated_input_tokens = estimate_raw_history_tokens(&raw_chat_history) + + input.len() / 4 // New input + + 5000; // System prompt overhead estimate + + if estimated_input_tokens > 150_000 { + println!("{}", " ⚠ Large context detected. Pre-truncating...".yellow()); + + let old_count = raw_chat_history.len(); + // Keep last 20 messages when approaching limit + if raw_chat_history.len() > 20 { + let drain_count = raw_chat_history.len() - 20; + raw_chat_history.drain(0..drain_count); + conversation_history.clear(); // Stay in sync + println!("{}", format!(" ✓ Truncated {} → {} messages", old_count, raw_chat_history.len()).dimmed()); + } + } + // Retry loop for automatic error recovery // MAX_RETRIES is for failures without progress // MAX_CONTINUATIONS is for truncations WITH progress (more generous) @@ -242,12 +302,13 @@ pub async fn run_interactive( let hook = ToolDisplayHook::new(); let project_path_buf = session.project_path.clone(); - // Select prompt based on query type (analysis vs generation) - let preamble = get_system_prompt(&session.project_path, Some(¤t_input)); + // Select prompt based on query type (analysis vs generation) and plan mode + let preamble = get_system_prompt(&session.project_path, Some(¤t_input), session.plan_mode); let is_generation = prompts::is_generation_query(¤t_input); + let is_planning = session.plan_mode.is_planning(); - // Convert conversation history to Rig Message format - let mut chat_history = conversation_history.to_messages(); + // Note: using raw_chat_history directly which preserves Reasoning blocks + // This is needed for extended thinking to work with multi-turn conversations let response = match session.provider { ProviderType::OpenAI => { @@ -279,10 +340,16 @@ pub async fn run_interactive( .tool(ReadFileTool::new(project_path_buf.clone())) .tool(ListDirectoryTool::new(project_path_buf.clone())); - // Add generation tools if this is a generation query - if is_generation { - // Create file tools with IDE client if connected - let (write_file_tool, write_files_tool) = if let Some(ref client) = ide_client { + // Add tools based on mode + if is_planning { + // Plan mode: read-only shell + plan creation tools + builder = builder + .tool(ShellTool::new(project_path_buf.clone()).with_read_only(true)) + .tool(PlanCreateTool::new(project_path_buf.clone())) + .tool(PlanListTool::new(project_path_buf.clone())); + } else if is_generation { + // Standard mode + generation query: all tools including file writes and plan execution + let (mut write_file_tool, mut write_files_tool) = if let Some(ref client) = ide_client { ( WriteFileTool::new(project_path_buf.clone()) .with_ide_client(client.clone()), @@ -295,10 +362,18 @@ pub async fn run_interactive( WriteFilesTool::new(project_path_buf.clone()), ) }; + // Disable confirmations if auto-accept mode is enabled (from plan menu) + if auto_accept_writes { + write_file_tool = write_file_tool.without_confirmation(); + write_files_tool = write_files_tool.without_confirmation(); + } builder = builder .tool(write_file_tool) .tool(write_files_tool) - .tool(ShellTool::new(project_path_buf.clone())); + .tool(ShellTool::new(project_path_buf.clone())) + .tool(PlanListTool::new(project_path_buf.clone())) + .tool(PlanNextTool::new(project_path_buf.clone())) + .tool(PlanUpdateTool::new(project_path_buf.clone())); } if let Some(params) = reasoning_params { @@ -310,7 +385,7 @@ pub async fn run_interactive( // Use hook to display tool calls as they happen // Pass conversation history for context continuity agent.prompt(¤t_input) - .with_history(&mut chat_history) + .with_history(&mut raw_chat_history) .with_hook(hook.clone()) .multi_turn(50) .await @@ -338,10 +413,16 @@ pub async fn run_interactive( .tool(ReadFileTool::new(project_path_buf.clone())) .tool(ListDirectoryTool::new(project_path_buf.clone())); - // Add generation tools if this is a generation query - if is_generation { - // Create file tools with IDE client if connected - let (write_file_tool, write_files_tool) = if let Some(ref client) = ide_client { + // Add tools based on mode + if is_planning { + // Plan mode: read-only shell + plan creation tools + builder = builder + .tool(ShellTool::new(project_path_buf.clone()).with_read_only(true)) + .tool(PlanCreateTool::new(project_path_buf.clone())) + .tool(PlanListTool::new(project_path_buf.clone())); + } else if is_generation { + // Standard mode + generation query: all tools including file writes and plan execution + let (mut write_file_tool, mut write_files_tool) = if let Some(ref client) = ide_client { ( WriteFileTool::new(project_path_buf.clone()) .with_ide_client(client.clone()), @@ -354,10 +435,18 @@ pub async fn run_interactive( WriteFilesTool::new(project_path_buf.clone()), ) }; + // Disable confirmations if auto-accept mode is enabled (from plan menu) + if auto_accept_writes { + write_file_tool = write_file_tool.without_confirmation(); + write_files_tool = write_files_tool.without_confirmation(); + } builder = builder .tool(write_file_tool) .tool(write_files_tool) - .tool(ShellTool::new(project_path_buf.clone())); + .tool(ShellTool::new(project_path_buf.clone())) + .tool(PlanListTool::new(project_path_buf.clone())) + .tool(PlanNextTool::new(project_path_buf.clone())) + .tool(PlanUpdateTool::new(project_path_buf.clone())); } let agent = builder.build(); @@ -366,7 +455,7 @@ pub async fn run_interactive( // Use hook to display tool calls as they happen // Pass conversation history for context continuity agent.prompt(¤t_input) - .with_history(&mut chat_history) + .with_history(&mut raw_chat_history) .with_hook(hook.clone()) .multi_turn(50) .await @@ -377,7 +466,9 @@ pub async fn run_interactive( // Extended thinking for Claude models via Bedrock // This enables Claude to show its reasoning process before responding. - // Requires patched rig-bedrock that preserves Reasoning blocks with tool calls. + // Requires vendored rig-bedrock that preserves Reasoning blocks with tool calls. + // Extended thinking budget - reduced to help with rate limits + // 8000 is enough for most tasks, increase to 16000 for complex analysis let thinking_params = serde_json::json!({ "thinking": { "type": "enabled", @@ -388,7 +479,7 @@ pub async fn run_interactive( let mut builder = client .agent(&session.model) .preamble(&preamble) - .max_tokens(16000) // Higher for thinking + response + .max_tokens(64000) // Max output tokens for Claude Sonnet on Bedrock .tool(AnalyzeTool::new(project_path_buf.clone())) .tool(SecurityScanTool::new(project_path_buf.clone())) .tool(VulnerabilitiesTool::new(project_path_buf.clone())) @@ -399,10 +490,16 @@ pub async fn run_interactive( .tool(ReadFileTool::new(project_path_buf.clone())) .tool(ListDirectoryTool::new(project_path_buf.clone())); - // Add generation tools if this is a generation query - if is_generation { - // Create file tools with IDE client if connected - let (write_file_tool, write_files_tool) = if let Some(ref client) = ide_client { + // Add tools based on mode + if is_planning { + // Plan mode: read-only shell + plan creation tools + builder = builder + .tool(ShellTool::new(project_path_buf.clone()).with_read_only(true)) + .tool(PlanCreateTool::new(project_path_buf.clone())) + .tool(PlanListTool::new(project_path_buf.clone())); + } else if is_generation { + // Standard mode + generation query: all tools including file writes and plan execution + let (mut write_file_tool, mut write_files_tool) = if let Some(ref client) = ide_client { ( WriteFileTool::new(project_path_buf.clone()) .with_ide_client(client.clone()), @@ -415,10 +512,18 @@ pub async fn run_interactive( WriteFilesTool::new(project_path_buf.clone()), ) }; + // Disable confirmations if auto-accept mode is enabled (from plan menu) + if auto_accept_writes { + write_file_tool = write_file_tool.without_confirmation(); + write_files_tool = write_files_tool.without_confirmation(); + } builder = builder .tool(write_file_tool) .tool(write_files_tool) - .tool(ShellTool::new(project_path_buf.clone())); + .tool(ShellTool::new(project_path_buf.clone())) + .tool(PlanListTool::new(project_path_buf.clone())) + .tool(PlanNextTool::new(project_path_buf.clone())) + .tool(PlanUpdateTool::new(project_path_buf.clone())); } // Add thinking params for extended reasoning @@ -428,7 +533,7 @@ pub async fn run_interactive( // Use same multi-turn pattern as OpenAI/Anthropic agent.prompt(¤t_input) - .with_history(&mut chat_history) + .with_history(&mut raw_chat_history) .with_hook(hook.clone()) .multi_turn(50) .await @@ -441,10 +546,33 @@ pub async fn run_interactive( println!(); ResponseFormatter::print_response(&text); - // Track token usage (estimate since Rig doesn't expose exact counts) - let prompt_tokens = TokenUsage::estimate_tokens(&input); - let completion_tokens = TokenUsage::estimate_tokens(&text); - session.token_usage.add_request(prompt_tokens, completion_tokens); + // Track token usage - use actual from hook if available, else estimate + let hook_usage = hook.get_usage().await; + if hook_usage.has_data() { + // Use actual token counts from API response + session.token_usage.add_actual(hook_usage.input_tokens, hook_usage.output_tokens); + } else { + // Fall back to estimation when API doesn't provide usage + let prompt_tokens = TokenUsage::estimate_tokens(&input); + let completion_tokens = TokenUsage::estimate_tokens(&text); + session.token_usage.add_estimated(prompt_tokens, completion_tokens); + } + // Reset hook usage for next request batch + hook.reset_usage().await; + + // Show context indicator like Forge: [model/~tokens] + let model_short = session.model.split('/').last() + .unwrap_or(&session.model) + .split(':').next() + .unwrap_or(&session.model); + println!(); + println!( + " {}[{}/{}]{}", + ui::colors::ansi::DIM, + model_short, + session.token_usage.format_compact(), + ui::colors::ansi::RESET + ); // Extract tool calls from the hook state for history tracking let tool_calls = extract_tool_calls_from_hook(&hook).await; @@ -457,7 +585,7 @@ pub async fn run_interactive( } // Add to conversation history with tool call records - conversation_history.add_turn(input.clone(), text.clone(), tool_calls); + conversation_history.add_turn(input.clone(), text.clone(), tool_calls.clone()); // Check if this heavy turn requires immediate compaction // This helps prevent context overflow in subsequent requests @@ -470,8 +598,53 @@ pub async fn run_interactive( // Also update legacy session history for compatibility session.history.push(("user".to_string(), input.clone())); - session.history.push(("assistant".to_string(), text)); - succeeded = true; + session.history.push(("assistant".to_string(), text.clone())); + + // Check if plan_create was called - show interactive menu + if let Some(plan_info) = find_plan_create_call(&tool_calls) { + println!(); // Space before menu + + // Show the plan action menu (don't switch modes yet - let user choose) + match ui::show_plan_action_menu(&plan_info.0, plan_info.1) { + ui::PlanActionResult::ExecuteAutoAccept => { + // Now switch to standard mode for execution + if session.plan_mode.is_planning() { + session.plan_mode = session.plan_mode.toggle(); + } + auto_accept_writes = true; + pending_input = Some(format!( + "Execute the plan at '{}'. Use plan_next to get tasks and execute them in order. Auto-accept all file writes.", + plan_info.0 + )); + succeeded = true; + } + ui::PlanActionResult::ExecuteWithReview => { + // Now switch to standard mode for execution + if session.plan_mode.is_planning() { + session.plan_mode = session.plan_mode.toggle(); + } + pending_input = Some(format!( + "Execute the plan at '{}'. Use plan_next to get tasks and execute them in order.", + plan_info.0 + )); + succeeded = true; + } + ui::PlanActionResult::ChangePlan(feedback) => { + // Stay in plan mode for modifications + pending_input = Some(format!( + "Please modify the plan at '{}'. User feedback: {}", + plan_info.0, feedback + )); + succeeded = true; + } + ui::PlanActionResult::Cancel => { + // Just complete normally, don't execute + succeeded = true; + } + } + } else { + succeeded = true; + } } Err(e) => { let err_str = e.to_string(); @@ -556,12 +729,56 @@ pub async fn run_interactive( // Brief delay before continuation tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; continue; // Continue the loop without incrementing retry_attempt - } else if err_str.contains("rate") || err_str.contains("Rate") || err_str.contains("429") { + } else if err_str.contains("rate") || err_str.contains("Rate") || err_str.contains("429") + || err_str.contains("Too many tokens") || err_str.contains("please wait") + || err_str.contains("throttl") || err_str.contains("Throttl") { eprintln!("{}", "⚠ Rate limited by API provider.".yellow()); - // Wait before retry for rate limits + // Wait before retry for rate limits (longer wait for "too many tokens") + retry_attempt += 1; + let wait_secs = if err_str.contains("Too many tokens") { 30 } else { 5 }; + eprintln!("{}", format!(" Waiting {} seconds before retry ({}/{})...", wait_secs, retry_attempt, MAX_RETRIES).dimmed()); + tokio::time::sleep(tokio::time::Duration::from_secs(wait_secs)).await; + } else if is_input_too_long_error(&err_str) { + // Context too large - truncate raw_chat_history directly + // NOTE: We truncate raw_chat_history (actual messages) not conversation_history + // because conversation_history may be empty/stale during errors + eprintln!("{}", "⚠ Context too large for model. Truncating history...".yellow()); + + let old_token_count = estimate_raw_history_tokens(&raw_chat_history); + let old_msg_count = raw_chat_history.len(); + + // Strategy: Keep only the last N messages (user/assistant pairs) + // More aggressive truncation on each retry: 10 → 6 → 4 messages + let keep_count = match retry_attempt { + 0 => 10, + 1 => 6, + _ => 4, + }; + + if raw_chat_history.len() > keep_count { + // Drain older messages, keep the most recent ones + let drain_count = raw_chat_history.len() - keep_count; + raw_chat_history.drain(0..drain_count); + } + + let new_token_count = estimate_raw_history_tokens(&raw_chat_history); + eprintln!("{}", format!( + " ✓ Truncated: {} messages (~{} tokens) → {} messages (~{} tokens)", + old_msg_count, old_token_count, raw_chat_history.len(), new_token_count + ).green()); + + // Also clear conversation_history to stay in sync + conversation_history.clear(); + + // Retry with truncated context retry_attempt += 1; - eprintln!("{}", format!(" Waiting 5 seconds before retry ({}/{})...", retry_attempt, MAX_RETRIES).dimmed()); - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + if retry_attempt < MAX_RETRIES { + eprintln!("{}", format!(" → Retrying with truncated context ({}/{})...", retry_attempt, MAX_RETRIES).dimmed()); + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + } else { + eprintln!("{}", "Context still too large after truncation. Try /clear to reset.".red()); + break; + } } else if is_truncation_error(&err_str) { // Truncation error - try intelligent continuation let completed_tools = extract_tool_calls_from_hook(&hook).await; @@ -715,6 +932,121 @@ fn truncate_string(s: &str, max_len: usize) -> String { } } +/// Estimate token count from raw rig Messages +/// This is used for context length management to prevent "input too long" errors. +/// Estimates ~4 characters per token. +fn estimate_raw_history_tokens(messages: &[rig::completion::Message]) -> usize { + use rig::completion::message::{AssistantContent, UserContent}; + + messages.iter().map(|msg| -> usize { + match msg { + rig::completion::Message::User { content } => { + content.iter().map(|c| -> usize { + match c { + UserContent::Text(t) => t.text.len() / 4, + _ => 100, // Estimate for images/documents + } + }).sum::() + } + rig::completion::Message::Assistant { content, .. } => { + content.iter().map(|c| -> usize { + match c { + AssistantContent::Text(t) => t.text.len() / 4, + AssistantContent::ToolCall(tc) => { + // arguments is serde_json::Value, convert to string for length estimate + let args_len = tc.function.arguments.to_string().len(); + (tc.function.name.len() + args_len) / 4 + } + _ => 100, + } + }).sum::() + } + } + }).sum() +} + +/// Find a plan_create tool call in the list and extract plan info +/// Returns (plan_path, task_count) if found +fn find_plan_create_call(tool_calls: &[ToolCallRecord]) -> Option<(String, usize)> { + for tc in tool_calls { + if tc.tool_name == "plan_create" { + // Try to parse the result_summary as JSON to extract plan_path + // Note: result_summary may be truncated, so we have multiple fallbacks + let plan_path = if let Ok(result) = serde_json::from_str::(&tc.result_summary) { + result.get("plan_path") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + } else { + None + }; + + // If JSON parsing failed, find the most recently created plan file + // This is more reliable than trying to reconstruct the path from truncated args + let plan_path = plan_path.unwrap_or_else(|| { + find_most_recent_plan_file().unwrap_or_else(|| "plans/plan.md".to_string()) + }); + + // Count tasks by reading the plan file directly + let task_count = count_tasks_in_plan_file(&plan_path).unwrap_or(0); + + return Some((plan_path, task_count)); + } + } + None +} + +/// Find the most recently created plan file in the plans directory +fn find_most_recent_plan_file() -> Option { + let plans_dir = std::env::current_dir().ok()?.join("plans"); + if !plans_dir.exists() { + return None; + } + + let mut newest: Option<(std::path::PathBuf, std::time::SystemTime)> = None; + + for entry in std::fs::read_dir(&plans_dir).ok()?.flatten() { + let path = entry.path(); + if path.extension().map(|e| e == "md").unwrap_or(false) { + if let Ok(metadata) = entry.metadata() { + if let Ok(modified) = metadata.modified() { + if newest.as_ref().map(|(_, t)| modified > *t).unwrap_or(true) { + newest = Some((path, modified)); + } + } + } + } + } + + newest.map(|(path, _)| { + // Return relative path + path.strip_prefix(std::env::current_dir().unwrap_or_default()) + .map(|p| p.display().to_string()) + .unwrap_or_else(|_| path.display().to_string()) + }) +} + +/// Count tasks (checkbox items) in a plan file +fn count_tasks_in_plan_file(plan_path: &str) -> Option { + use regex::Regex; + + // Try both relative and absolute paths + let path = std::path::Path::new(plan_path); + let content = if path.exists() { + std::fs::read_to_string(path).ok()? + } else { + // Try with current directory + std::fs::read_to_string(std::env::current_dir().ok()?.join(plan_path)).ok()? + }; + + // Count task checkboxes: - [ ], - [x], - [~], - [!] + let task_regex = Regex::new(r"^\s*-\s*\[[ x~!]\]").ok()?; + let count = content.lines() + .filter(|line| task_regex.is_match(line)) + .count(); + + Some(count) +} + /// Check if an error is a truncation/JSON parsing error that can be recovered via continuation fn is_truncation_error(err_str: &str) -> bool { err_str.contains("JsonError") @@ -723,6 +1055,18 @@ fn is_truncation_error(err_str: &str) -> bool { || err_str.contains("unexpected end") } +/// Check if error is "input too long" - context exceeds model limit +/// This happens when conversation history grows beyond what the model can handle. +/// Recovery: compact history and retry with reduced context. +fn is_input_too_long_error(err_str: &str) -> bool { + err_str.contains("too long") + || err_str.contains("Too long") + || err_str.contains("context length") + || err_str.contains("maximum context") + || err_str.contains("exceeds the model") + || err_str.contains("Input is too long") +} + /// Build a continuation prompt that tells the AI what work was completed /// and asks it to continue from where it left off fn build_continuation_prompt( @@ -840,7 +1184,8 @@ pub async fn run_query( let project_path_buf = project_path.to_path_buf(); // Select prompt based on query type (analysis vs generation) - let preamble = get_system_prompt(project_path, Some(query)); + // For single queries (non-interactive), always use standard mode + let preamble = get_system_prompt(project_path, Some(query), PlanMode::default()); let is_generation = prompts::is_generation_query(query); match provider { @@ -941,14 +1286,14 @@ pub async fn run_query( let thinking_params = serde_json::json!({ "thinking": { "type": "enabled", - "budget_tokens": 8000 + "budget_tokens": 16000 } }); let mut builder = client .agent(model_name) .preamble(&preamble) - .max_tokens(16000) // Higher for thinking + response + .max_tokens(64000) // Max output tokens for Claude Sonnet on Bedrock .tool(AnalyzeTool::new(project_path_buf.clone())) .tool(SecurityScanTool::new(project_path_buf.clone())) .tool(VulnerabilitiesTool::new(project_path_buf.clone())) diff --git a/src/agent/prompts/mod.rs b/src/agent/prompts/mod.rs index 30958f71..f35c2999 100644 --- a/src/agent/prompts/mod.rs +++ b/src/agent/prompts/mod.rs @@ -51,11 +51,28 @@ const NON_NEGOTIABLE_RULES: &str = r#" - Do what has been asked; nothing more, nothing less - NEVER create files unless absolutely necessary for the goal - ALWAYS prefer editing existing files over creating new ones -- NEVER create documentation files unless explicitly requested +- NEVER create documentation files (*.md, *.txt, README, CHANGELOG, CONTRIBUTING, etc.) unless explicitly requested by the user + - "Explicitly requested" means the user asks for a specific document BY NAME + - Instead of creating docs, explain in your reply or use code comments + - This includes: summaries, migration guides, HOWTOs, explanatory files - User may tag files with @ - do NOT reread those files - Only use emojis if explicitly requested - Cite code references as: `filepath:line` or `filepath:startLine-endLine` + +**CRITICAL**: When a tool returns `"cancelled": true`, you MUST: +1. STOP immediately - do NOT try the same operation again +2. Do NOT create alternative/similar files +3. Read the `user_feedback` field for what the user wants instead +4. If feedback says "no", "stop", "WTF", or similar - STOP ALL file creation +5. Ask the user what they want instead + +When user cancels/rejects a file: +- The entire batch of related files should stop +- Do NOT create README, GUIDE, or SUMMARY files as alternatives +- Wait for explicit user instruction before creating any more files + + When users say ANY of these patterns, you MUST create files: - "put your findings in X" → create files in X - "generate a Dockerfile" → create the Dockerfile @@ -138,8 +155,35 @@ You have access to tools to help analyze and understand the project: **Generation Tools:** - write_file - Write content to a file (creates parent directories automatically) - write_files - Write multiple files at once + +**Plan Execution Tools:** +- plan_list - List available plans in plans/ directory +- plan_next - Get next pending task from a plan, mark it in-progress +- plan_update - Mark a task as done or failed + +When the user says "execute the plan", "continue", "resume" or similar: +1. Use `plan_list` to find available/incomplete plans, or use the plan path they specify +2. Use `plan_next` to get the next pending task - this marks it `[~]` IN_PROGRESS + - If continuing a previous plan, `plan_next` automatically finds where you left off + - Tasks already marked `[x]` or `[!]` are skipped +3. Execute the task using appropriate tools (write_file, shell, etc.) +4. Use `plan_update` to mark the task `[x]` DONE (or `[!]` FAILED with reason) +5. Repeat: call `plan_next` for the next task until all complete + +**IMPORTANT for continuation:** Plans are resumable! If execution was interrupted: +- The plan file preserves task states (`[x]` done, `[~]` in-progress, `[ ]` pending) +- User just needs to say "continue" or "continue the plan at plans/X.md" +- `plan_next` will return the next `[ ]` pending task automatically + +Task status in plan files: +- `[ ]` PENDING - Not started +- `[~]` IN_PROGRESS - Currently working on (may need to re-run if interrupted) +- `[x]` DONE - Completed successfully +- `[!]` FAILED - Failed (includes reason) + + 1. Use tools to gather information - don't guess about project structure 2. Be concise but thorough in explanations @@ -180,8 +224,22 @@ pub fn get_code_development_prompt(project_path: &std::path::Path) -> String { - write_file - Write or update a single file - write_files - Write multiple files at once - shell - Run shell commands (build, test, lint) + +**Plan Execution Tools:** +- plan_list - List available plans in plans/ directory +- plan_next - Get next pending task from a plan, mark it in-progress +- plan_update - Mark a task as done or failed + +When the user says "execute the plan" or similar: +1. Use `plan_list` to find available plans, or use the plan path they specify +2. Use `plan_next` to get the first pending task - this marks it `[~]` IN_PROGRESS +3. Execute the task using appropriate tools (write_file, shell, etc.) +4. Use `plan_update` to mark the task `[x]` DONE (or `[!]` FAILED with reason) +5. Repeat: call `plan_next` for the next task until all complete + + 1. **Quick Analysis** (1-3 tool calls max): - Read the most relevant existing files @@ -248,8 +306,22 @@ pub fn get_devops_prompt(project_path: &std::path::Path) -> String { **Validation Tools:** - shell - Execute validation commands (docker build, terraform validate, helm lint) + +**Plan Execution Tools:** +- plan_list - List available plans in plans/ directory +- plan_next - Get next pending task from a plan, mark it in-progress +- plan_update - Mark a task as done or failed + +When the user says "execute the plan" or similar: +1. Use `plan_list` to find available plans, or use the plan path they specify +2. Use `plan_next` to get the first pending task - this marks it `[~]` IN_PROGRESS +3. Execute the task using appropriate tools (write_file, shell, etc.) +4. Use `plan_update` to mark the task `[x]` DONE (or `[!]` FAILED with reason) +5. Repeat: call `plan_next` for the next task until all complete + + **Dockerfile Standards:** - Multi-stage builds (builder + final stages) @@ -388,6 +460,107 @@ pub fn is_generation_query(query: &str) -> bool { generation_keywords.iter().any(|kw| query_lower.contains(kw)) } +/// Get the planning mode prompt (read-only exploration) +pub fn get_planning_prompt(project_path: &std::path::Path) -> String { + format!( + r#"{system_info} + +{agent_identity} + +{tool_usage} + + +**PLAN MODE ACTIVE** - You are in read-only exploration mode. + +## What You CAN Do: +- Read and analyze files using read_file +- List directories using list_directory +- Run read-only shell commands: ls, cat, head, tail, grep, find, git status, git log, git diff +- Analyze project structure and patterns +- Explain code and architecture +- **CREATE STRUCTURED PLANS** using plan_create tool +- Answer questions about the codebase + +## What You CANNOT Do: +- Create or modify source files (write_file, write_files are disabled) +- Run write commands (rm, mv, cp, mkdir, echo >, etc.) +- Execute build/test commands that modify state + +## Your Role in Plan Mode: +1. Research thoroughly - read relevant files, understand patterns +2. Analyze the user's request +3. Create a structured plan using the `plan_create` tool with task checkboxes +4. Tell user to switch to standard mode (Shift+Tab) and say "execute the plan" + +## Creating Plans: +Use the `plan_create` tool to create executable plans. Each task must use checkbox format: + +```markdown +# Feature Name Plan + +## Overview +Brief description of what we're implementing. + +## Tasks + +- [ ] First task - create/modify this file +- [ ] Second task - implement this feature +- [ ] Third task - add tests +- [ ] Fourth task - validate everything works +``` + +Task status markers: +- `[ ]` PENDING - Not started +- `[~]` IN_PROGRESS - Currently being worked on +- `[x]` DONE - Completed +- `[!]` FAILED - Failed with reason + + + +**Available Tools (Plan Mode):** +- read_file - Read file contents +- list_directory - List files and directories +- shell - Run read-only commands only (ls, cat, grep, find, git status/log/diff) +- analyze_project - Analyze project architecture, dependencies +- hadolint - Lint Dockerfiles (read-only analysis) +- **plan_create** - Create structured plan files with task checkboxes +- **plan_list** - List existing plans in plans/ directory + +**NOT Available in Plan Mode:** +- write_file, write_files - File creation/modification disabled +- Shell commands that modify files - Blocked +"#, + system_info = get_system_info(project_path), + agent_identity = AGENT_IDENTITY, + tool_usage = TOOL_USAGE_INSTRUCTIONS + ) +} + +/// Detect if a query is asking to continue/resume an incomplete plan +pub fn is_plan_continuation_query(query: &str) -> bool { + let query_lower = query.to_lowercase(); + let continuation_keywords = [ + "continue", "resume", "pick up", "carry on", + "where we left off", "where i left off", "where it left off", + "finish the plan", "complete the plan", + "continue the plan", "resume the plan", + ]; + + let plan_keywords = ["plan", "task", "tasks"]; + + // Direct continuation phrases + if continuation_keywords.iter().any(|kw| query_lower.contains(kw)) { + return true; + } + + // "continue" + plan-related word + if query_lower.contains("continue") && plan_keywords.iter().any(|kw| query_lower.contains(kw)) { + return true; + } + + false +} + /// Detect if a query is specifically about code development (not DevOps) pub fn is_code_development_query(query: &str) -> bool { let query_lower = query.to_lowercase(); diff --git a/src/agent/session.rs b/src/agent/session.rs index fe0cde4e..f036af1a 100644 --- a/src/agent/session.rs +++ b/src/agent/session.rs @@ -18,6 +18,108 @@ use std::path::Path; const ROBOT: &str = "🤖"; +/// Information about an incomplete plan +#[derive(Debug, Clone)] +pub struct IncompletePlan { + pub path: String, + pub filename: String, + pub done: usize, + pub pending: usize, + pub total: usize, +} + +/// Find incomplete plans in the plans/ directory +pub fn find_incomplete_plans(project_path: &std::path::Path) -> Vec { + use regex::Regex; + + let plans_dir = project_path.join("plans"); + if !plans_dir.exists() { + return Vec::new(); + } + + let task_regex = Regex::new(r"^\s*-\s*\[([ x~!])\]").unwrap(); + let mut incomplete = Vec::new(); + + if let Ok(entries) = std::fs::read_dir(&plans_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().map(|e| e == "md").unwrap_or(false) { + if let Ok(content) = std::fs::read_to_string(&path) { + let mut done = 0; + let mut pending = 0; + let mut in_progress = 0; + + for line in content.lines() { + if let Some(caps) = task_regex.captures(line) { + match caps.get(1).map(|m| m.as_str()) { + Some("x") => done += 1, + Some(" ") => pending += 1, + Some("~") => in_progress += 1, + Some("!") => done += 1, // Failed counts as "attempted" + _ => {} + } + } + } + + let total = done + pending + in_progress; + if total > 0 && (pending > 0 || in_progress > 0) { + let rel_path = path.strip_prefix(project_path) + .map(|p| p.display().to_string()) + .unwrap_or_else(|_| path.display().to_string()); + + incomplete.push(IncompletePlan { + path: rel_path, + filename: path.file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(), + done, + pending: pending + in_progress, + total, + }); + } + } + } + } + } + + // Sort by most recently modified (newest first) + incomplete.sort_by(|a, b| b.filename.cmp(&a.filename)); + incomplete +} + +/// Planning mode state - toggles between standard and plan mode +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum PlanMode { + /// Standard mode - all tools available, normal operation + #[default] + Standard, + /// Planning mode - read-only exploration, no file modifications + Planning, +} + +impl PlanMode { + /// Toggle between Standard and Planning mode + pub fn toggle(&self) -> Self { + match self { + PlanMode::Standard => PlanMode::Planning, + PlanMode::Planning => PlanMode::Standard, + } + } + + /// Check if in planning mode + pub fn is_planning(&self) -> bool { + matches!(self, PlanMode::Planning) + } + + /// Get display name for the mode + pub fn display_name(&self) -> &'static str { + match self { + PlanMode::Standard => "standard mode", + PlanMode::Planning => "plan mode", + } + } +} + /// Available models per provider pub fn get_available_models(provider: ProviderType) -> Vec<(&'static str, &'static str)> { match provider { @@ -50,6 +152,8 @@ pub struct ChatSession { pub project_path: std::path::PathBuf, pub history: Vec<(String, String)>, // (role, content) pub token_usage: TokenUsage, + /// Current planning mode state + pub plan_mode: PlanMode, } impl ChatSession { @@ -59,16 +163,28 @@ impl ChatSession { ProviderType::Anthropic => "claude-sonnet-4-5-20250929".to_string(), ProviderType::Bedrock => "global.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string(), }; - + Self { provider, model: model.unwrap_or(default_model), project_path: project_path.to_path_buf(), history: Vec::new(), token_usage: TokenUsage::new(), + plan_mode: PlanMode::default(), } } + /// Toggle planning mode and return the new mode + pub fn toggle_plan_mode(&mut self) -> PlanMode { + self.plan_mode = self.plan_mode.toggle(); + self.plan_mode + } + + /// Check if currently in planning mode + pub fn is_planning(&self) -> bool { + self.plan_mode.is_planning() + } + /// Check if API key is configured for a provider (env var OR config file) pub fn has_api_key(provider: ProviderType) -> bool { // Check environment variable first @@ -1000,6 +1116,47 @@ impl ChatSession { Ok(()) } + /// Handle /plans command - show incomplete plans and offer to continue + pub fn handle_plans_command(&self) -> AgentResult<()> { + let incomplete = find_incomplete_plans(&self.project_path); + + if incomplete.is_empty() { + println!("\n{}", "No incomplete plans found.".dimmed()); + println!("{}", "Create a plan using plan mode (Shift+Tab) and the plan_create tool.".dimmed()); + return Ok(()); + } + + println!("\n{}", "📋 Incomplete Plans".cyan().bold()); + println!(); + + for (i, plan) in incomplete.iter().enumerate() { + let progress = format!("{}/{}", plan.done, plan.total); + let percent = if plan.total > 0 { + (plan.done as f64 / plan.total as f64 * 100.0) as usize + } else { + 0 + }; + + println!( + " {} {} {} ({} - {}%)", + format!("[{}]", i + 1).cyan(), + plan.filename.white().bold(), + format!("({} pending)", plan.pending).yellow(), + progress.dimmed(), + percent + ); + println!(" {}", plan.path.dimmed()); + } + + println!(); + println!("{}", "To continue a plan, say:".dimmed()); + println!(" {}", "\"continue the plan at plans/FILENAME.md\"".cyan()); + println!(" {}", "or just \"continue\" to resume the most recent one".cyan()); + println!(); + + Ok(()) + } + /// List all profiles fn list_profiles(&self, config: &crate::config::types::AgentConfig) { let active = config.active_profile.as_deref(); @@ -1122,6 +1279,36 @@ impl ChatSession { " {}", "Your AI-powered code analysis assistant".dimmed() ); + + // Check for incomplete plans and show a hint + let incomplete_plans = find_incomplete_plans(&self.project_path); + if !incomplete_plans.is_empty() { + println!(); + if incomplete_plans.len() == 1 { + let plan = &incomplete_plans[0]; + println!( + " {} {} ({}/{} done)", + "📋 Incomplete plan:".yellow(), + plan.filename.white(), + plan.done, + plan.total + ); + println!( + " {} \"{}\" {}", + "→".cyan(), + "continue".cyan().bold(), + "to resume".dimmed() + ); + } else { + println!( + " {} {} incomplete plans found. Use {} to see them.", + "📋".yellow(), + incomplete_plans.len(), + "/plans".cyan() + ); + } + } + println!(); println!( " {} Type your questions. Use {} to exit.\n", @@ -1169,6 +1356,9 @@ impl ChatSession { "/profile" => { self.handle_profile_command()?; } + "/plans" => { + self.handle_plans_command()?; + } _ => { if cmd.starts_with('/') { // Unknown command - interactive picker already handled in read_input @@ -1218,26 +1408,26 @@ impl ChatSession { /// Read user input with prompt - with interactive file picker support /// Uses custom terminal handling for @ file references and / commands - pub fn read_input(&self) -> io::Result { - use crate::agent::ui::input::{read_input_with_file_picker, InputResult}; - - match read_input_with_file_picker("You:", &self.project_path) { - InputResult::Submit(text) => { - let trimmed = text.trim(); - // Handle case where full suggestion was submitted (e.g., "/model Description") - // Extract just the command if it looks like a suggestion format - if trimmed.starts_with('/') && trimmed.contains(" ") { - // This looks like a suggestion format, extract just the command - if let Some(cmd) = trimmed.split_whitespace().next() { - return Ok(cmd.to_string()); - } - } - // Strip @ prefix from file references before sending to AI - // The @ is for UI autocomplete, but the AI should see just the path - Ok(Self::strip_file_references(trimmed)) + /// Returns InputResult which the main loop should handle + pub fn read_input(&self) -> io::Result { + use crate::agent::ui::input::read_input_with_file_picker; + + Ok(read_input_with_file_picker("You:", &self.project_path, self.plan_mode.is_planning())) + } + + /// Process a submitted input text - strips @ references and handles suggestion format + pub fn process_submitted_text(text: &str) -> String { + let trimmed = text.trim(); + // Handle case where full suggestion was submitted (e.g., "/model Description") + // Extract just the command if it looks like a suggestion format + if trimmed.starts_with('/') && trimmed.contains(" ") { + // This looks like a suggestion format, extract just the command + if let Some(cmd) = trimmed.split_whitespace().next() { + return cmd.to_string(); } - InputResult::Cancel => Ok("exit".to_string()), // Ctrl+C exits - InputResult::Exit => Ok("exit".to_string()), } + // Strip @ prefix from file references before sending to AI + // The @ is for UI autocomplete, but the AI should see just the path + Self::strip_file_references(trimmed) } } diff --git a/src/agent/tools/diagnostics.rs b/src/agent/tools/diagnostics.rs new file mode 100644 index 00000000..27a4ad35 --- /dev/null +++ b/src/agent/tools/diagnostics.rs @@ -0,0 +1,673 @@ +//! Diagnostics tool for detecting code errors via IDE/LSP integration +//! +//! This tool queries the IDE's language servers (via MCP) or falls back to +//! running language-specific commands to detect errors in the code. +//! +//! ## Usage +//! +//! The agent can use this tool after writing or modifying files to check +//! for compilation errors, type errors, linting issues, etc. +//! +//! ## Supported Methods +//! +//! 1. **IDE Integration (preferred)**: If connected to an IDE via MCP, +//! queries language servers directly (rust-analyzer, TypeScript, ESLint, etc.) +//! +//! 2. **Command Fallback**: If no IDE is connected, runs language-specific +//! commands based on detected project type: +//! - Rust: `cargo check` +//! - JavaScript/TypeScript: `npm run lint` or `eslint` +//! - Python: `python -m py_compile` or `pylint` +//! - Go: `go build` + +use crate::agent::ide::{Diagnostic, DiagnosticSeverity, DiagnosticsResponse, IdeClient}; +use rig::completion::ToolDefinition; +use rig::tool::Tool; +use serde::Deserialize; +use serde_json::json; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::process::Command; +use tokio::sync::Mutex; + +#[derive(Debug, Deserialize)] +pub struct DiagnosticsArgs { + /// Optional file path to check. If not provided, checks all files. + pub path: Option, + /// Whether to include warnings (default: true) + pub include_warnings: Option, + /// Maximum number of diagnostics to return (default: 50) + pub limit: Option, +} + +#[derive(Debug, thiserror::Error)] +#[error("Diagnostics error: {0}")] +pub struct DiagnosticsError(String); + +#[derive(Debug, Clone)] +pub struct DiagnosticsTool { + project_path: PathBuf, + /// Optional IDE client for LSP integration + ide_client: Option>>, +} + +impl DiagnosticsTool { + pub fn new(project_path: PathBuf) -> Self { + Self { + project_path, + ide_client: None, + } + } + + /// Set the IDE client for LSP integration + pub fn with_ide_client(mut self, ide_client: Arc>) -> Self { + self.ide_client = Some(ide_client); + self + } + + /// Detect project type based on files present + fn detect_project_type(&self) -> ProjectType { + let cargo_toml = self.project_path.join("Cargo.toml"); + let package_json = self.project_path.join("package.json"); + let go_mod = self.project_path.join("go.mod"); + let pyproject_toml = self.project_path.join("pyproject.toml"); + let requirements_txt = self.project_path.join("requirements.txt"); + + if cargo_toml.exists() { + ProjectType::Rust + } else if package_json.exists() { + ProjectType::JavaScript + } else if go_mod.exists() { + ProjectType::Go + } else if pyproject_toml.exists() || requirements_txt.exists() { + ProjectType::Python + } else { + ProjectType::Unknown + } + } + + /// Get diagnostics from IDE via MCP + async fn get_ide_diagnostics( + &self, + file_path: Option<&str>, + ) -> Option { + let client = self.ide_client.as_ref()?; + let guard = client.lock().await; + + if !guard.is_connected() { + return None; + } + + guard.get_diagnostics(file_path).await.ok() + } + + /// Run fallback command-based diagnostics + async fn get_command_diagnostics(&self) -> Result { + let project_type = self.detect_project_type(); + + match project_type { + ProjectType::Rust => self.run_cargo_check().await, + ProjectType::JavaScript => self.run_npm_lint().await, + ProjectType::Go => self.run_go_build().await, + ProjectType::Python => self.run_python_check().await, + ProjectType::Unknown => Ok(DiagnosticsResponse { + diagnostics: Vec::new(), + total_errors: 0, + total_warnings: 0, + }), + } + } + + /// Run cargo check and parse output + async fn run_cargo_check(&self) -> Result { + let output = Command::new("cargo") + .args(["check", "--message-format=json"]) + .current_dir(&self.project_path) + .output() + .await + .map_err(|e| DiagnosticsError(format!("Failed to run cargo check: {}", e)))?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut diagnostics = Vec::new(); + + for line in stdout.lines() { + if let Ok(msg) = serde_json::from_str::(line) { + if msg.get("reason").and_then(|r| r.as_str()) == Some("compiler-message") { + if let Some(message) = msg.get("message") { + if let Some(diag) = self.parse_cargo_message(message) { + diagnostics.push(diag); + } + } + } + } + } + + let total_errors = diagnostics + .iter() + .filter(|d| d.severity == DiagnosticSeverity::Error) + .count() as u32; + let total_warnings = diagnostics + .iter() + .filter(|d| d.severity == DiagnosticSeverity::Warning) + .count() as u32; + + Ok(DiagnosticsResponse { + diagnostics, + total_errors, + total_warnings, + }) + } + + /// Parse a cargo compiler message into a Diagnostic + fn parse_cargo_message(&self, message: &serde_json::Value) -> Option { + let level = message.get("level")?.as_str()?; + let msg = message.get("message")?.as_str()?; + + let severity = match level { + "error" => DiagnosticSeverity::Error, + "warning" => DiagnosticSeverity::Warning, + "note" | "help" => DiagnosticSeverity::Hint, + _ => DiagnosticSeverity::Information, + }; + + // Get the primary span + let spans = message.get("spans")?.as_array()?; + let span = spans.iter().find(|s| { + s.get("is_primary").and_then(|v| v.as_bool()).unwrap_or(false) + }).or_else(|| spans.first())?; + + let file = span.get("file_name")?.as_str()?; + let line = span.get("line_start")?.as_u64()? as u32; + let column = span.get("column_start")?.as_u64()? as u32; + let end_line = span.get("line_end").and_then(|v| v.as_u64()).map(|v| v as u32); + let end_column = span.get("column_end").and_then(|v| v.as_u64()).map(|v| v as u32); + + let code = message + .get("code") + .and_then(|c| c.get("code")) + .and_then(|c| c.as_str()) + .map(|s| s.to_string()); + + Some(Diagnostic { + file: file.to_string(), + line, + column, + end_line, + end_column, + severity, + message: msg.to_string(), + source: Some("rustc".to_string()), + code, + }) + } + + /// Run npm lint and parse output + async fn run_npm_lint(&self) -> Result { + // Try npm run lint first + let output = Command::new("npm") + .args(["run", "lint", "--", "--format=json"]) + .current_dir(&self.project_path) + .output() + .await; + + if let Ok(output) = output { + if output.status.success() || !output.stdout.is_empty() { + let stdout = String::from_utf8_lossy(&output.stdout); + if let Ok(results) = serde_json::from_str::>(&stdout) { + return Ok(self.parse_eslint_output(&results)); + } + } + } + + // If that fails, try npx eslint + let output = Command::new("npx") + .args(["eslint", ".", "--format=json"]) + .current_dir(&self.project_path) + .output() + .await + .map_err(|e| DiagnosticsError(format!("Failed to run eslint: {}", e)))?; + + let stdout = String::from_utf8_lossy(&output.stdout); + if let Ok(results) = serde_json::from_str::>(&stdout) { + return Ok(self.parse_eslint_output(&results)); + } + + // Return empty if we couldn't parse + Ok(DiagnosticsResponse { + diagnostics: Vec::new(), + total_errors: 0, + total_warnings: 0, + }) + } + + /// Parse ESLint JSON output + fn parse_eslint_output(&self, results: &[serde_json::Value]) -> DiagnosticsResponse { + let mut diagnostics = Vec::new(); + + for file_result in results { + let file = file_result + .get("filePath") + .and_then(|f| f.as_str()) + .unwrap_or(""); + + if let Some(messages) = file_result.get("messages").and_then(|m| m.as_array()) { + for msg in messages { + let severity = match msg.get("severity").and_then(|s| s.as_u64()) { + Some(2) => DiagnosticSeverity::Error, + Some(1) => DiagnosticSeverity::Warning, + _ => DiagnosticSeverity::Information, + }; + + let message = msg + .get("message") + .and_then(|m| m.as_str()) + .unwrap_or("") + .to_string(); + let line = msg.get("line").and_then(|l| l.as_u64()).unwrap_or(1) as u32; + let column = msg.get("column").and_then(|c| c.as_u64()).unwrap_or(1) as u32; + let end_line = msg.get("endLine").and_then(|l| l.as_u64()).map(|v| v as u32); + let end_column = msg.get("endColumn").and_then(|c| c.as_u64()).map(|v| v as u32); + let code = msg.get("ruleId").and_then(|r| r.as_str()).map(|s| s.to_string()); + + diagnostics.push(Diagnostic { + file: file.to_string(), + line, + column, + end_line, + end_column, + severity, + message, + source: Some("eslint".to_string()), + code, + }); + } + } + } + + let total_errors = diagnostics + .iter() + .filter(|d| d.severity == DiagnosticSeverity::Error) + .count() as u32; + let total_warnings = diagnostics + .iter() + .filter(|d| d.severity == DiagnosticSeverity::Warning) + .count() as u32; + + DiagnosticsResponse { + diagnostics, + total_errors, + total_warnings, + } + } + + /// Run go build and parse output + async fn run_go_build(&self) -> Result { + let output = Command::new("go") + .args(["build", "-o", "/dev/null", "./..."]) + .current_dir(&self.project_path) + .output() + .await + .map_err(|e| DiagnosticsError(format!("Failed to run go build: {}", e)))?; + + let stderr = String::from_utf8_lossy(&output.stderr); + let mut diagnostics = Vec::new(); + + // Parse go build output: "file.go:line:col: message" + for line in stderr.lines() { + if let Some(diag) = self.parse_go_error(line) { + diagnostics.push(diag); + } + } + + let total_errors = diagnostics + .iter() + .filter(|d| d.severity == DiagnosticSeverity::Error) + .count() as u32; + let total_warnings = diagnostics + .iter() + .filter(|d| d.severity == DiagnosticSeverity::Warning) + .count() as u32; + + Ok(DiagnosticsResponse { + diagnostics, + total_errors, + total_warnings, + }) + } + + /// Parse a Go error line + fn parse_go_error(&self, line: &str) -> Option { + // Format: file.go:line:col: message + let parts: Vec<&str> = line.splitn(4, ':').collect(); + if parts.len() < 4 { + return None; + } + + let file = parts[0].to_string(); + let line_num = parts[1].parse::().ok()?; + let column = parts[2].parse::().ok()?; + let message = parts[3].trim().to_string(); + + Some(Diagnostic { + file, + line: line_num, + column, + end_line: None, + end_column: None, + severity: DiagnosticSeverity::Error, + message, + source: Some("go".to_string()), + code: None, + }) + } + + /// Run Python syntax check + async fn run_python_check(&self) -> Result { + // Try pylint first + let output = Command::new("pylint") + .args(["--output-format=json", "."]) + .current_dir(&self.project_path) + .output() + .await; + + if let Ok(output) = output { + let stdout = String::from_utf8_lossy(&output.stdout); + if let Ok(results) = serde_json::from_str::>(&stdout) { + return Ok(self.parse_pylint_output(&results)); + } + } + + // Fallback: just return empty + Ok(DiagnosticsResponse { + diagnostics: Vec::new(), + total_errors: 0, + total_warnings: 0, + }) + } + + /// Parse pylint JSON output + fn parse_pylint_output(&self, results: &[serde_json::Value]) -> DiagnosticsResponse { + let mut diagnostics = Vec::new(); + + for msg in results { + let msg_type = msg.get("type").and_then(|t| t.as_str()).unwrap_or(""); + let severity = match msg_type { + "error" | "fatal" => DiagnosticSeverity::Error, + "warning" => DiagnosticSeverity::Warning, + "convention" | "refactor" => DiagnosticSeverity::Hint, + _ => DiagnosticSeverity::Information, + }; + + let file = msg + .get("path") + .and_then(|p| p.as_str()) + .unwrap_or("") + .to_string(); + let line = msg.get("line").and_then(|l| l.as_u64()).unwrap_or(1) as u32; + let column = msg.get("column").and_then(|c| c.as_u64()).unwrap_or(1) as u32; + let message = msg + .get("message") + .and_then(|m| m.as_str()) + .unwrap_or("") + .to_string(); + let code = msg + .get("message-id") + .and_then(|m| m.as_str()) + .map(|s| s.to_string()); + + diagnostics.push(Diagnostic { + file, + line, + column, + end_line: None, + end_column: None, + severity, + message, + source: Some("pylint".to_string()), + code, + }); + } + + let total_errors = diagnostics + .iter() + .filter(|d| d.severity == DiagnosticSeverity::Error) + .count() as u32; + let total_warnings = diagnostics + .iter() + .filter(|d| d.severity == DiagnosticSeverity::Warning) + .count() as u32; + + DiagnosticsResponse { + diagnostics, + total_errors, + total_warnings, + } + } + + /// Filter diagnostics based on user preferences + fn filter_diagnostics( + &self, + mut response: DiagnosticsResponse, + include_warnings: bool, + limit: usize, + file_path: Option<&str>, + ) -> DiagnosticsResponse { + // Filter by file if specified + if let Some(path) = file_path { + response.diagnostics.retain(|d| d.file.contains(path)); + } + + // Filter out warnings if not requested + if !include_warnings { + response.diagnostics.retain(|d| d.severity == DiagnosticSeverity::Error); + } + + // Apply limit + response.diagnostics.truncate(limit); + + // Recalculate totals + response.total_errors = response + .diagnostics + .iter() + .filter(|d| d.severity == DiagnosticSeverity::Error) + .count() as u32; + response.total_warnings = response + .diagnostics + .iter() + .filter(|d| d.severity == DiagnosticSeverity::Warning) + .count() as u32; + + response + } +} + +#[derive(Debug, Clone, Copy)] +enum ProjectType { + Rust, + JavaScript, + Go, + Python, + Unknown, +} + +impl Tool for DiagnosticsTool { + const NAME: &'static str = "diagnostics"; + + type Error = DiagnosticsError; + type Args = DiagnosticsArgs; + type Output = String; + + async fn definition(&self, _prompt: String) -> ToolDefinition { + ToolDefinition { + name: Self::NAME.to_string(), + description: r#"Check for code errors, warnings, and linting issues. + +This tool queries language servers or runs language-specific commands to detect: +- Compilation errors +- Type errors +- Syntax errors +- Linting warnings +- Best practice violations + +Use this tool after writing or modifying code to verify there are no errors. + +The tool automatically detects the project type and uses appropriate checking: +- Rust: Uses rust-analyzer or cargo check +- JavaScript/TypeScript: Uses ESLint or TypeScript compiler +- Go: Uses gopls or go build +- Python: Uses pylint or pyright + +Returns a list of diagnostics with file locations, severity, and messages."# + .to_string(), + parameters: json!({ + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Optional file path to check. If not provided, checks all files in the project." + }, + "include_warnings": { + "type": "boolean", + "description": "Whether to include warnings in addition to errors (default: true)" + }, + "limit": { + "type": "integer", + "description": "Maximum number of diagnostics to return (default: 50)" + } + } + }), + } + } + + async fn call(&self, args: Self::Args) -> Result { + let include_warnings = args.include_warnings.unwrap_or(true); + let limit = args.limit.unwrap_or(50); + let file_path = args.path.as_deref(); + + // Try IDE first (better real-time diagnostics) + let response = if let Some(ide_response) = self.get_ide_diagnostics(file_path).await { + ide_response + } else { + // Fall back to command-based diagnostics + self.get_command_diagnostics().await? + }; + + // Filter and limit results + let filtered = self.filter_diagnostics(response, include_warnings, limit, file_path); + + // Format output + let result = if filtered.diagnostics.is_empty() { + json!({ + "success": true, + "message": "No errors or warnings found", + "total_errors": 0, + "total_warnings": 0, + "diagnostics": [] + }) + } else { + let formatted_diagnostics: Vec = filtered + .diagnostics + .iter() + .map(|d| { + json!({ + "file": d.file, + "line": d.line, + "column": d.column, + "severity": d.severity.as_str(), + "message": d.message, + "source": d.source, + "code": d.code + }) + }) + .collect(); + + json!({ + "success": filtered.total_errors == 0, + "total_errors": filtered.total_errors, + "total_warnings": filtered.total_warnings, + "diagnostics": formatted_diagnostics + }) + }; + + serde_json::to_string_pretty(&result) + .map_err(|e| DiagnosticsError(format!("Failed to serialize: {}", e))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + + #[tokio::test] + async fn test_diagnostics_tool_creation() { + let tool = DiagnosticsTool::new(PathBuf::from(".")); + assert_eq!(tool.project_path, PathBuf::from(".")); + } + + #[test] + fn test_project_type_detection() { + // This test would need a proper test directory setup + let tool = DiagnosticsTool::new(env::current_dir().unwrap()); + let project_type = tool.detect_project_type(); + // Current project is Rust + assert!(matches!(project_type, ProjectType::Rust)); + } + + #[test] + fn test_parse_go_error() { + let tool = DiagnosticsTool::new(PathBuf::from(".")); + let line = "main.go:10:5: undefined: foo"; + let diag = tool.parse_go_error(line); + assert!(diag.is_some()); + let diag = diag.unwrap(); + assert_eq!(diag.file, "main.go"); + assert_eq!(diag.line, 10); + assert_eq!(diag.column, 5); + assert_eq!(diag.message, "undefined: foo"); + } + + #[test] + fn test_filter_diagnostics() { + let tool = DiagnosticsTool::new(PathBuf::from(".")); + let response = DiagnosticsResponse { + diagnostics: vec![ + Diagnostic { + file: "src/main.rs".to_string(), + line: 1, + column: 1, + end_line: None, + end_column: None, + severity: DiagnosticSeverity::Error, + message: "error".to_string(), + source: None, + code: None, + }, + Diagnostic { + file: "src/lib.rs".to_string(), + line: 1, + column: 1, + end_line: None, + end_column: None, + severity: DiagnosticSeverity::Warning, + message: "warning".to_string(), + source: None, + code: None, + }, + ], + total_errors: 1, + total_warnings: 1, + }; + + // Filter to errors only + let filtered = tool.filter_diagnostics(response.clone(), false, 50, None); + assert_eq!(filtered.diagnostics.len(), 1); + assert_eq!(filtered.total_errors, 1); + assert_eq!(filtered.total_warnings, 0); + + // Filter by file + let filtered = tool.filter_diagnostics(response, true, 50, Some("main.rs")); + assert_eq!(filtered.diagnostics.len(), 1); + assert_eq!(filtered.diagnostics[0].file, "src/main.rs"); + } +} diff --git a/src/agent/tools/file_ops.rs b/src/agent/tools/file_ops.rs index c31f253b..4a272946 100644 --- a/src/agent/tools/file_ops.rs +++ b/src/agent/tools/file_ops.rs @@ -7,10 +7,18 @@ //! - Listing directories (ListDirectoryTool) //! //! File write operations include interactive diff confirmation before applying changes. +//! +//! ## Truncation Limits +//! +//! Tool outputs are truncated to prevent context overflow: +//! - File reads: Max 2000 lines (use start_line/end_line for specific sections) +//! - Directory listings: Max 500 entries +//! - Long lines: Truncated at 2000 characters use crate::agent::ide::IdeClient; use crate::agent::ui::confirmation::ConfirmationResult; use crate::agent::ui::diff::{confirm_file_write, confirm_file_write_with_ide}; +use super::truncation::{truncate_file_content, truncate_dir_listing, TruncationLimits}; use rig::completion::ToolDefinition; use rig::tool::Tool; use serde::{Deserialize, Serialize}; @@ -104,7 +112,7 @@ impl Tool for ReadFileTool { let metadata = fs::metadata(&file_path) .map_err(|e| ReadFileError(format!("Cannot read file: {}", e)))?; - + const MAX_SIZE: u64 = 1024 * 1024; if metadata.len() > MAX_SIZE { return Ok(json!({ @@ -116,10 +124,11 @@ impl Tool for ReadFileTool { .map_err(|e| ReadFileError(format!("Failed to read file: {}", e)))?; let output = if let Some(start) = args.start_line { + // User requested specific line range - respect it exactly let lines: Vec<&str> = content.lines().collect(); let start_idx = (start as usize).saturating_sub(1); let end_idx = args.end_line.map(|e| (e as usize).min(lines.len())).unwrap_or(lines.len()); - + if start_idx >= lines.len() { return Ok(json!({ "error": format!("Start line {} exceeds file length ({})", start, lines.len()) @@ -142,10 +151,16 @@ impl Tool for ReadFileTool { "content": selected.join("\n") }) } else { + // Full file read - apply truncation to prevent context overflow + let limits = TruncationLimits::default(); + let truncated = truncate_file_content(&content, &limits); + json!({ "file": args.path, - "total_lines": content.lines().count(), - "content": content + "total_lines": truncated.total_lines, + "lines_returned": truncated.returned_lines, + "truncated": truncated.was_truncated, + "content": truncated.content }) }; @@ -285,11 +300,26 @@ impl Tool for ListDirectoryTool { let mut entries = Vec::new(); self.list_entries(&dir_path, &dir_path, recursive, 0, 3, &mut entries)?; - let result = json!({ - "path": path_str, - "entries": entries, - "total_count": entries.len() - }); + // Apply truncation to prevent context overflow + let limits = TruncationLimits::default(); + let truncated = truncate_dir_listing(entries, limits.max_dir_entries); + + let result = if truncated.was_truncated { + json!({ + "path": path_str, + "entries": truncated.entries, + "entries_returned": truncated.entries.len(), + "total_count": truncated.total_entries, + "truncated": true, + "note": format!("Showing first {} of {} entries. Use a more specific path to see others.", truncated.entries.len(), truncated.total_entries) + }) + } else { + json!({ + "path": path_str, + "entries": truncated.entries, + "total_count": truncated.total_entries + }) + }; serde_json::to_string_pretty(&result) .map_err(|e| ListDirectoryError(format!("Failed to serialize: {}", e))) @@ -530,27 +560,42 @@ The tool will create parent directories automatically if they don't exist."#.to_ self.allowed_patterns.allow(pattern); } ConfirmationResult::Modify(feedback) => { - // Return feedback to the agent + // Return feedback to the agent - make it VERY clear to stop let result = json!({ "cancelled": true, + "STOP": "Do NOT create this file or any similar files. Wait for user instruction.", "reason": "User requested changes", "user_feedback": feedback, - "original_path": args.path + "original_path": args.path, + "action_required": "Read the user_feedback and respond accordingly. Do NOT try to create alternative files." }); return serde_json::to_string_pretty(&result) .map_err(|e| WriteFileError(format!("Failed to serialize: {}", e))); } ConfirmationResult::Cancel => { - // User cancelled + // User cancelled - make it absolutely clear to stop let result = json!({ "cancelled": true, + "STOP": "User has rejected this operation. Do NOT create this file or any alternative files.", "reason": "User cancelled the operation", - "original_path": args.path + "original_path": args.path, + "action_required": "Stop creating files. Ask the user what they want instead." }); return serde_json::to_string_pretty(&result) .map_err(|e| WriteFileError(format!("Failed to serialize: {}", e))); } } + } else { + // Auto-accept mode: show the diff without requiring confirmation + use crate::agent::ui::diff::{render_diff, render_new_file}; + use colored::Colorize; + + if let Some(old) = &old_content { + render_diff(old, &args.content, &args.path); + } else { + render_new_file(&args.content, &args.path); + } + println!(" {} Auto-accepted", "✓".green()); } // Create parent directories if needed @@ -748,7 +793,6 @@ All files are written atomically. Parent directories are created automatically." let mut results = Vec::new(); let mut total_bytes = 0usize; let mut total_lines = 0usize; - let mut skipped_files = Vec::new(); for file in &args.files { let requested_path = PathBuf::from(&file.path); @@ -806,21 +850,44 @@ All files are written atomically. Parent directories are created automatically." self.allowed_patterns.allow(pattern); } ConfirmationResult::Modify(feedback) => { - skipped_files.push(json!({ - "path": file.path, + // User provided feedback - stop ALL remaining files immediately + let result = json!({ + "cancelled": true, + "STOP": "User provided feedback. Stop creating all remaining files in this batch.", "reason": "User requested changes", - "feedback": feedback - })); - continue; + "user_feedback": feedback, + "skipped_file": file.path, + "files_written_before_cancel": results.len(), + "action_required": "Read the user_feedback. Do NOT continue with remaining files." + }); + return serde_json::to_string_pretty(&result) + .map_err(|e| WriteFilesError(format!("Failed to serialize: {}", e))); } ConfirmationResult::Cancel => { - skipped_files.push(json!({ - "path": file.path, - "reason": "User cancelled" - })); - continue; + // User cancelled - stop ALL remaining files immediately + let result = json!({ + "cancelled": true, + "STOP": "User cancelled. Stop creating all files immediately.", + "reason": "User cancelled the operation", + "skipped_file": file.path, + "files_written_before_cancel": results.len(), + "action_required": "Stop all file creation. Ask the user what they want instead." + }); + return serde_json::to_string_pretty(&result) + .map_err(|e| WriteFilesError(format!("Failed to serialize: {}", e))); } } + } else { + // Auto-accept mode: show the diff without requiring confirmation + use crate::agent::ui::diff::{render_diff, render_new_file}; + use colored::Colorize; + + if let Some(old) = &old_content { + render_diff(old, &file.content, &file.path); + } else { + render_new_file(&file.content, &file.path); + } + println!(" {} Auto-accepted", "✓".green()); } // Create parent directories if needed @@ -850,25 +917,15 @@ All files are written atomically. Parent directories are created automatically." })); } - let result = if skipped_files.is_empty() { - json!({ - "success": true, - "files_written": results.len(), - "total_lines": total_lines, - "total_bytes": total_bytes, - "files": results - }) - } else { - json!({ - "success": results.len() > 0, - "files_written": results.len(), - "files_skipped": skipped_files.len(), - "total_lines": total_lines, - "total_bytes": total_bytes, - "files": results, - "skipped": skipped_files - }) - }; + // If we get here, all files were written successfully + // (cancellations return early with immediate stop message) + let result = json!({ + "success": true, + "files_written": results.len(), + "total_lines": total_lines, + "total_bytes": total_bytes, + "files": results + }); serde_json::to_string_pretty(&result) .map_err(|e| WriteFilesError(format!("Failed to serialize: {}", e))) diff --git a/src/agent/tools/mod.rs b/src/agent/tools/mod.rs index 70fe0d03..8c5c6680 100644 --- a/src/agent/tools/mod.rs +++ b/src/agent/tools/mod.rs @@ -20,6 +20,9 @@ //! ### Linting //! - `HadolintTool` - Native Dockerfile linting (best practices, security) //! +//! ### Diagnostics +//! - `DiagnosticsTool` - Check for code errors via IDE/LSP or language-specific commands +//! //! ### Terraform //! - `TerraformFmtTool` - Format Terraform configuration files //! - `TerraformValidateTool` - Validate Terraform configurations @@ -28,16 +31,29 @@ //! ### Shell //! - `ShellTool` - Execute validation commands (docker build, terraform validate, helm lint) //! +//! ### Planning (Forge-style workflow) +//! - `PlanCreateTool` - Create structured plan files with task checkboxes +//! - `PlanNextTool` - Get next pending task and mark it in-progress +//! - `PlanUpdateTool` - Update task status (done, failed) +//! - `PlanListTool` - List all available plan files +//! mod analyze; +mod diagnostics; mod file_ops; mod hadolint; +mod plan; mod security; mod shell; mod terraform; +mod truncation; + +pub use truncation::TruncationLimits; pub use analyze::AnalyzeTool; +pub use diagnostics::DiagnosticsTool; pub use file_ops::{ListDirectoryTool, ReadFileTool, WriteFileTool, WriteFilesTool}; pub use hadolint::HadolintTool; +pub use plan::{PlanCreateTool, PlanListTool, PlanNextTool, PlanUpdateTool}; pub use security::{SecurityScanTool, VulnerabilitiesTool}; pub use shell::ShellTool; pub use terraform::{TerraformFmtTool, TerraformInstallTool, TerraformValidateTool}; diff --git a/src/agent/tools/plan.rs b/src/agent/tools/plan.rs new file mode 100644 index 00000000..e79c6192 --- /dev/null +++ b/src/agent/tools/plan.rs @@ -0,0 +1,719 @@ +//! Plan tools for Forge-style planning workflow +//! +//! Provides tools for creating and executing structured plans: +//! - `PlanCreateTool` - Create plan files with task checkboxes +//! - `PlanNextTool` - Get next pending task and mark it in-progress +//! - `PlanUpdateTool` - Update task status (done, failed) +//! +//! ## Task Status Format +//! +//! ```markdown +//! - [ ] Task description (PENDING) +//! - [~] Task description (IN_PROGRESS) +//! - [x] Task description (DONE) +//! - [!] Task description (FAILED: reason) +//! ``` + +use chrono::Local; +use regex::Regex; +use rig::completion::ToolDefinition; +use rig::tool::Tool; +use serde::Deserialize; +use serde_json::json; +use std::fs; +use std::path::PathBuf; + +// ============================================================================ +// Task Status Types +// ============================================================================ + +/// Task status in a plan file +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TaskStatus { + Pending, // [ ] + InProgress, // [~] + Done, // [x] + Failed, // [!] +} + +impl TaskStatus { + fn marker(&self) -> &'static str { + match self { + TaskStatus::Pending => "[ ]", + TaskStatus::InProgress => "[~]", + TaskStatus::Done => "[x]", + TaskStatus::Failed => "[!]", + } + } + + fn from_marker(s: &str) -> Option { + match s { + "[ ]" => Some(TaskStatus::Pending), + "[~]" => Some(TaskStatus::InProgress), + "[x]" => Some(TaskStatus::Done), + "[!]" => Some(TaskStatus::Failed), + _ => None, + } + } +} + +/// A task parsed from a plan file +#[derive(Debug, Clone)] +pub struct PlanTask { + pub index: usize, // 1-based index + pub status: TaskStatus, + pub description: String, + pub line_number: usize, // Line number in file (1-based) +} + +// ============================================================================ +// Plan Parser +// ============================================================================ + +/// Parse tasks from plan file content +fn parse_plan_tasks(content: &str) -> Vec { + let task_regex = Regex::new(r"^(\s*)-\s*\[([ x~!])\]\s*(.+)$").unwrap(); + let mut tasks = Vec::new(); + let mut task_index = 0; + + for (line_idx, line) in content.lines().enumerate() { + if let Some(caps) = task_regex.captures(line) { + task_index += 1; + let marker_char = caps.get(2).map(|m| m.as_str()).unwrap_or(" "); + let description = caps.get(3).map(|m| m.as_str()).unwrap_or("").to_string(); + + let status = match marker_char { + " " => TaskStatus::Pending, + "~" => TaskStatus::InProgress, + "x" => TaskStatus::Done, + "!" => TaskStatus::Failed, + _ => TaskStatus::Pending, + }; + + tasks.push(PlanTask { + index: task_index, + status, + description, + line_number: line_idx + 1, + }); + } + } + + tasks +} + +/// Update a task's status in the plan file content +fn update_task_status(content: &str, task_index: usize, new_status: TaskStatus, note: Option<&str>) -> Option { + let task_regex = Regex::new(r"^(\s*)-\s*\[[ x~!]\]\s*(.+)$").unwrap(); + let mut current_index = 0; + let mut lines: Vec = content.lines().map(String::from).collect(); + + for (line_idx, line) in content.lines().enumerate() { + if task_regex.is_match(line) { + current_index += 1; + if current_index == task_index { + // Found the task to update + let caps = task_regex.captures(line)?; + let indent = caps.get(1).map(|m| m.as_str()).unwrap_or(""); + let desc = caps.get(2).map(|m| m.as_str()).unwrap_or(""); + + // Build new line with updated status + let new_line = if new_status == TaskStatus::Failed { + let fail_note = note.unwrap_or("unknown reason"); + format!("{}- {} {} (FAILED: {})", indent, new_status.marker(), desc, fail_note) + } else { + format!("{}- {} {}", indent, new_status.marker(), desc) + }; + + lines[line_idx] = new_line; + return Some(lines.join("\n")); + } + } + } + + None // Task not found +} + +// ============================================================================ +// Plan Create Tool +// ============================================================================ + +#[derive(Debug, Deserialize)] +pub struct PlanCreateArgs { + /// Short name for the plan (e.g., "auth-feature", "refactor-db") + pub plan_name: String, + /// Version identifier (e.g., "v1", "draft") + pub version: Option, + /// Markdown content with task checkboxes (- [ ] task description) + pub content: String, +} + +#[derive(Debug, thiserror::Error)] +#[error("Plan create error: {0}")] +pub struct PlanCreateError(String); + +#[derive(Debug, Clone)] +pub struct PlanCreateTool { + project_path: PathBuf, +} + +impl PlanCreateTool { + pub fn new(project_path: PathBuf) -> Self { + Self { project_path } + } +} + +impl Tool for PlanCreateTool { + const NAME: &'static str = "plan_create"; + + type Error = PlanCreateError; + type Args = PlanCreateArgs; + type Output = String; + + async fn definition(&self, _prompt: String) -> ToolDefinition { + ToolDefinition { + name: Self::NAME.to_string(), + description: r#"Create a structured plan file with task checkboxes. Use this in plan mode to document implementation steps. + +The plan file will be created in the `plans/` directory with format: {date}-{plan_name}-{version}.md + +IMPORTANT: Each task MUST use the checkbox format: `- [ ] Task description` + +Example content: +```markdown +# Authentication Feature Plan + +## Overview +Add user authentication to the application. + +## Tasks + +- [ ] Create User model in src/models/user.rs +- [ ] Add password hashing with bcrypt +- [ ] Create login endpoint at POST /api/login +- [ ] Add JWT token generation +- [ ] Create authentication middleware +- [ ] Write tests for auth flow +``` + +The task status markers are: +- `[ ]` - PENDING (not started) +- `[~]` - IN_PROGRESS (currently being worked on) +- `[x]` - DONE (completed) +- `[!]` - FAILED (failed with reason)"#.to_string(), + parameters: json!({ + "type": "object", + "properties": { + "plan_name": { + "type": "string", + "description": "Short kebab-case name for the plan (e.g., 'auth-feature', 'refactor-db')" + }, + "version": { + "type": "string", + "description": "Optional version identifier (e.g., 'v1', 'draft'). Defaults to 'v1'" + }, + "content": { + "type": "string", + "description": "Markdown content with task checkboxes. Each task must be: '- [ ] Task description'" + } + }, + "required": ["plan_name", "content"] + }), + } + } + + async fn call(&self, args: Self::Args) -> Result { + // Validate plan name (kebab-case) + let plan_name = args.plan_name.trim().to_lowercase().replace(' ', "-"); + if plan_name.is_empty() { + return Err(PlanCreateError("Plan name cannot be empty".to_string())); + } + + // Validate content has at least one task + let tasks = parse_plan_tasks(&args.content); + if tasks.is_empty() { + return Err(PlanCreateError( + "Plan must contain at least one task with format: '- [ ] Task description'".to_string() + )); + } + + // Build filename: {date}-{plan_name}-{version}.md + let version = args.version.unwrap_or_else(|| "v1".to_string()); + let date = Local::now().format("%Y-%m-%d"); + let filename = format!("{}-{}-{}.md", date, plan_name, version); + + // Create plans directory if it doesn't exist + let plans_dir = self.project_path.join("plans"); + if !plans_dir.exists() { + fs::create_dir_all(&plans_dir) + .map_err(|e| PlanCreateError(format!("Failed to create plans directory: {}", e)))?; + } + + // Check if file already exists + let file_path = plans_dir.join(&filename); + if file_path.exists() { + return Err(PlanCreateError(format!( + "Plan file already exists: {}. Use a different name or version.", + filename + ))); + } + + // Write the plan file + fs::write(&file_path, &args.content) + .map_err(|e| PlanCreateError(format!("Failed to write plan file: {}", e)))?; + + // Get relative path for display + let rel_path = file_path.strip_prefix(&self.project_path) + .map(|p| p.display().to_string()) + .unwrap_or_else(|_| file_path.display().to_string()); + + let result = json!({ + "success": true, + "plan_path": rel_path, + "filename": filename, + "task_count": tasks.len(), + "tasks": tasks.iter().map(|t| json!({ + "index": t.index, + "description": t.description, + "status": "pending" + })).collect::>(), + "next_steps": "Plan created successfully. Choose an execution option from the menu." + }); + + serde_json::to_string_pretty(&result) + .map_err(|e| PlanCreateError(format!("Failed to serialize: {}", e))) + } +} + +// ============================================================================ +// Plan Next Tool - Get next pending task +// ============================================================================ + +#[derive(Debug, Deserialize)] +pub struct PlanNextArgs { + /// Path to the plan file (relative or absolute) + pub plan_path: String, +} + +#[derive(Debug, thiserror::Error)] +#[error("Plan next error: {0}")] +pub struct PlanNextError(String); + +#[derive(Debug, Clone)] +pub struct PlanNextTool { + project_path: PathBuf, +} + +impl PlanNextTool { + pub fn new(project_path: PathBuf) -> Self { + Self { project_path } + } + + fn resolve_path(&self, path: &str) -> PathBuf { + let p = PathBuf::from(path); + if p.is_absolute() { + p + } else { + self.project_path.join(p) + } + } +} + +impl Tool for PlanNextTool { + const NAME: &'static str = "plan_next"; + + type Error = PlanNextError; + type Args = PlanNextArgs; + type Output = String; + + async fn definition(&self, _prompt: String) -> ToolDefinition { + ToolDefinition { + name: Self::NAME.to_string(), + description: r#"Get the next pending task from a plan file and mark it as in-progress. + +This tool: +1. Reads the plan file +2. Finds the first `[ ]` (PENDING) task +3. Updates it to `[~]` (IN_PROGRESS) in the file +4. Returns the task description for you to execute + +After executing the task, use `plan_update` to mark it as done or failed. + +Returns null task if all tasks are complete."#.to_string(), + parameters: json!({ + "type": "object", + "properties": { + "plan_path": { + "type": "string", + "description": "Path to the plan file (e.g., 'plans/2025-01-15-auth-feature-v1.md')" + } + }, + "required": ["plan_path"] + }), + } + } + + async fn call(&self, args: Self::Args) -> Result { + let file_path = self.resolve_path(&args.plan_path); + + // Read plan file + let content = fs::read_to_string(&file_path) + .map_err(|e| PlanNextError(format!("Failed to read plan file: {}", e)))?; + + // Parse tasks + let tasks = parse_plan_tasks(&content); + if tasks.is_empty() { + return Err(PlanNextError("No tasks found in plan file".to_string())); + } + + // Find first pending task + let pending_task = tasks.iter().find(|t| t.status == TaskStatus::Pending); + + match pending_task { + Some(task) => { + // Update task to in-progress + let updated_content = update_task_status(&content, task.index, TaskStatus::InProgress, None) + .ok_or_else(|| PlanNextError("Failed to update task status".to_string()))?; + + // Write updated content + fs::write(&file_path, &updated_content) + .map_err(|e| PlanNextError(format!("Failed to write plan file: {}", e)))?; + + // Count task states + let done_count = tasks.iter().filter(|t| t.status == TaskStatus::Done).count(); + let pending_count = tasks.iter().filter(|t| t.status == TaskStatus::Pending).count() - 1; // -1 for current + let failed_count = tasks.iter().filter(|t| t.status == TaskStatus::Failed).count(); + + let result = json!({ + "has_task": true, + "task_index": task.index, + "task_description": task.description, + "total_tasks": tasks.len(), + "completed": done_count, + "pending": pending_count, + "failed": failed_count, + "progress": format!("{}/{}", done_count, tasks.len()), + "instructions": "Execute this task using appropriate tools, then call plan_update to mark it done." + }); + + serde_json::to_string_pretty(&result) + .map_err(|e| PlanNextError(format!("Failed to serialize: {}", e))) + } + None => { + // No pending tasks - check if all done + let done_count = tasks.iter().filter(|t| t.status == TaskStatus::Done).count(); + let failed_count = tasks.iter().filter(|t| t.status == TaskStatus::Failed).count(); + let in_progress = tasks.iter().filter(|t| t.status == TaskStatus::InProgress).count(); + + let result = json!({ + "has_task": false, + "total_tasks": tasks.len(), + "completed": done_count, + "failed": failed_count, + "in_progress": in_progress, + "status": if in_progress > 0 { + "Task in progress - complete it before getting next" + } else if failed_count > 0 { + "Plan completed with failures" + } else { + "All tasks completed successfully!" + } + }); + + serde_json::to_string_pretty(&result) + .map_err(|e| PlanNextError(format!("Failed to serialize: {}", e))) + } + } + } +} + +// ============================================================================ +// Plan Update Tool - Update task status +// ============================================================================ + +#[derive(Debug, Deserialize)] +pub struct PlanUpdateArgs { + /// Path to the plan file + pub plan_path: String, + /// 1-based task index to update + pub task_index: usize, + /// New status: "done", "failed", or "pending" + pub status: String, + /// Optional note for failed tasks + pub note: Option, +} + +#[derive(Debug, thiserror::Error)] +#[error("Plan update error: {0}")] +pub struct PlanUpdateError(String); + +#[derive(Debug, Clone)] +pub struct PlanUpdateTool { + project_path: PathBuf, +} + +impl PlanUpdateTool { + pub fn new(project_path: PathBuf) -> Self { + Self { project_path } + } + + fn resolve_path(&self, path: &str) -> PathBuf { + let p = PathBuf::from(path); + if p.is_absolute() { + p + } else { + self.project_path.join(p) + } + } +} + +impl Tool for PlanUpdateTool { + const NAME: &'static str = "plan_update"; + + type Error = PlanUpdateError; + type Args = PlanUpdateArgs; + type Output = String; + + async fn definition(&self, _prompt: String) -> ToolDefinition { + ToolDefinition { + name: Self::NAME.to_string(), + description: r#"Update the status of a task in a plan file. + +Use this after completing or failing a task to update its status: +- "done" - Mark task as completed `[x]` +- "failed" - Mark task as failed `[!]` (include a note explaining why) +- "pending" - Reset task to pending `[ ]` + +After marking a task done, call `plan_next` to get the next task."#.to_string(), + parameters: json!({ + "type": "object", + "properties": { + "plan_path": { + "type": "string", + "description": "Path to the plan file" + }, + "task_index": { + "type": "integer", + "description": "1-based index of the task to update" + }, + "status": { + "type": "string", + "enum": ["done", "failed", "pending"], + "description": "New status for the task" + }, + "note": { + "type": "string", + "description": "Optional note explaining failure (required for 'failed' status)" + } + }, + "required": ["plan_path", "task_index", "status"] + }), + } + } + + async fn call(&self, args: Self::Args) -> Result { + let file_path = self.resolve_path(&args.plan_path); + + // Read plan file + let content = fs::read_to_string(&file_path) + .map_err(|e| PlanUpdateError(format!("Failed to read plan file: {}", e)))?; + + // Parse status + let new_status = match args.status.to_lowercase().as_str() { + "done" => TaskStatus::Done, + "failed" => TaskStatus::Failed, + "pending" => TaskStatus::Pending, + _ => return Err(PlanUpdateError(format!( + "Invalid status '{}'. Use: done, failed, or pending", + args.status + ))), + }; + + // Require note for failed status + if new_status == TaskStatus::Failed && args.note.is_none() { + return Err(PlanUpdateError( + "A note is required when marking a task as failed".to_string() + )); + } + + // Update task status + let updated_content = update_task_status( + &content, + args.task_index, + new_status, + args.note.as_deref(), + ).ok_or_else(|| PlanUpdateError(format!( + "Task {} not found in plan", + args.task_index + )))?; + + // Write updated content + fs::write(&file_path, &updated_content) + .map_err(|e| PlanUpdateError(format!("Failed to write plan file: {}", e)))?; + + // Parse updated tasks for summary + let tasks = parse_plan_tasks(&updated_content); + let done_count = tasks.iter().filter(|t| t.status == TaskStatus::Done).count(); + let pending_count = tasks.iter().filter(|t| t.status == TaskStatus::Pending).count(); + let failed_count = tasks.iter().filter(|t| t.status == TaskStatus::Failed).count(); + + let result = json!({ + "success": true, + "task_index": args.task_index, + "new_status": args.status, + "progress": format!("{}/{}", done_count, tasks.len()), + "summary": { + "total": tasks.len(), + "done": done_count, + "pending": pending_count, + "failed": failed_count + }, + "next_action": if pending_count > 0 { + "Call plan_next to get the next pending task" + } else if failed_count > 0 { + "Plan complete with failures. Review failed tasks." + } else { + "All tasks completed! Plan execution finished." + } + }); + + serde_json::to_string_pretty(&result) + .map_err(|e| PlanUpdateError(format!("Failed to serialize: {}", e))) + } +} + +// ============================================================================ +// Plan List Tool - List available plans +// ============================================================================ + +#[derive(Debug, Deserialize)] +pub struct PlanListArgs { + /// Optional filter by status (e.g., "incomplete" to show plans with pending tasks) + pub filter: Option, +} + +#[derive(Debug, thiserror::Error)] +#[error("Plan list error: {0}")] +pub struct PlanListError(String); + +#[derive(Debug, Clone)] +pub struct PlanListTool { + project_path: PathBuf, +} + +impl PlanListTool { + pub fn new(project_path: PathBuf) -> Self { + Self { project_path } + } +} + +impl Tool for PlanListTool { + const NAME: &'static str = "plan_list"; + + type Error = PlanListError; + type Args = PlanListArgs; + type Output = String; + + async fn definition(&self, _prompt: String) -> ToolDefinition { + ToolDefinition { + name: Self::NAME.to_string(), + description: r#"List all plan files in the plans/ directory with their status summary. + +Shows each plan with: +- Filename and path +- Task counts (done/pending/failed) +- Overall status"#.to_string(), + parameters: json!({ + "type": "object", + "properties": { + "filter": { + "type": "string", + "enum": ["all", "incomplete", "complete"], + "description": "Filter plans: 'all' (default), 'incomplete' (has pending), 'complete' (no pending)" + } + } + }), + } + } + + async fn call(&self, args: Self::Args) -> Result { + let plans_dir = self.project_path.join("plans"); + + if !plans_dir.exists() { + let result = json!({ + "plans": [], + "message": "No plans directory found. Create a plan first with plan_create." + }); + return serde_json::to_string_pretty(&result) + .map_err(|e| PlanListError(format!("Failed to serialize: {}", e))); + } + + let filter = args.filter.as_deref().unwrap_or("all"); + let mut plans = Vec::new(); + + let entries = fs::read_dir(&plans_dir) + .map_err(|e| PlanListError(format!("Failed to read plans directory: {}", e)))?; + + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().map(|e| e == "md").unwrap_or(false) { + if let Ok(content) = fs::read_to_string(&path) { + let tasks = parse_plan_tasks(&content); + let done = tasks.iter().filter(|t| t.status == TaskStatus::Done).count(); + let pending = tasks.iter().filter(|t| t.status == TaskStatus::Pending).count(); + let in_progress = tasks.iter().filter(|t| t.status == TaskStatus::InProgress).count(); + let failed = tasks.iter().filter(|t| t.status == TaskStatus::Failed).count(); + + // Apply filter + let include = match filter { + "incomplete" => pending > 0 || in_progress > 0, + "complete" => pending == 0 && in_progress == 0, + _ => true, + }; + + if include { + let rel_path = path.strip_prefix(&self.project_path) + .map(|p| p.display().to_string()) + .unwrap_or_else(|_| path.display().to_string()); + + plans.push(json!({ + "path": rel_path, + "filename": path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(), + "tasks": { + "total": tasks.len(), + "done": done, + "pending": pending, + "in_progress": in_progress, + "failed": failed + }, + "progress": format!("{}/{}", done, tasks.len()), + "status": if pending == 0 && in_progress == 0 { + if failed > 0 { "completed_with_failures" } else { "complete" } + } else if in_progress > 0 { + "in_progress" + } else { + "pending" + } + })); + } + } + } + } + + // Sort by filename (most recent first due to date prefix) + plans.sort_by(|a, b| { + let a_name = a.get("filename").and_then(|v| v.as_str()).unwrap_or(""); + let b_name = b.get("filename").and_then(|v| v.as_str()).unwrap_or(""); + b_name.cmp(a_name) + }); + + let result = json!({ + "plans": plans, + "total": plans.len(), + "filter": filter + }); + + serde_json::to_string_pretty(&result) + .map_err(|e| PlanListError(format!("Failed to serialize: {}", e))) + } +} diff --git a/src/agent/tools/shell.rs b/src/agent/tools/shell.rs index 9ce46e9c..a004ad18 100644 --- a/src/agent/tools/shell.rs +++ b/src/agent/tools/shell.rs @@ -7,9 +7,17 @@ //! - Kubernetes dry-run //! //! Includes interactive confirmation before execution and streaming output display. +//! +//! ## Output Truncation +//! +//! Shell outputs are truncated using prefix/suffix strategy: +//! - First 200 lines + last 200 lines are kept +//! - Middle content is summarized with line count +//! - Long lines (>2000 chars) are truncated use crate::agent::ui::confirmation::{confirm_shell_command, AllowedCommands, ConfirmationResult}; use crate::agent::ui::shell_output::StreamingShellOutput; +use super::truncation::{truncate_shell_output, TruncationLimits}; use rig::completion::ToolDefinition; use rig::tool::Tool; use serde::Deserialize; @@ -51,6 +59,47 @@ const ALLOWED_COMMANDS: &[&str] = &[ "shellcheck", ]; +/// Read-only commands allowed in plan mode +/// These commands only read/analyze and don't modify the filesystem +const READ_ONLY_COMMANDS: &[&str] = &[ + // File listing/reading + "ls", + "cat", + "head", + "tail", + "less", + "more", + "wc", + "file", + // Search/find + "grep", + "find", + "locate", + "which", + "whereis", + // Git read-only + "git status", + "git log", + "git diff", + "git show", + "git branch", + "git remote", + "git tag", + // Directory navigation + "pwd", + "tree", + // System info + "uname", + "env", + "printenv", + "echo", + // Code analysis + "hadolint", + "tflint", + "yamllint", + "shellcheck", +]; + #[derive(Debug, Deserialize)] pub struct ShellArgs { /// The command to execute @@ -72,6 +121,8 @@ pub struct ShellTool { allowed_commands: Arc, /// Whether to require confirmation before executing commands require_confirmation: bool, + /// Whether in read-only mode (plan mode) - only allows read-only commands + read_only: bool, } impl ShellTool { @@ -80,6 +131,7 @@ impl ShellTool { project_path, allowed_commands: Arc::new(AllowedCommands::new()), require_confirmation: true, + read_only: false, } } @@ -89,6 +141,7 @@ impl ShellTool { project_path, allowed_commands, require_confirmation: true, + read_only: false, } } @@ -98,6 +151,12 @@ impl ShellTool { self } + /// Enable read-only mode (for plan mode) - only allows read-only commands + pub fn with_read_only(mut self, read_only: bool) -> Self { + self.read_only = read_only; + self + } + fn is_command_allowed(&self, command: &str) -> bool { let trimmed = command.trim(); ALLOWED_COMMANDS.iter().any(|allowed| { @@ -105,6 +164,58 @@ impl ShellTool { }) } + /// Check if a command is read-only (safe for plan mode) + fn is_read_only_command(&self, command: &str) -> bool { + let trimmed = command.trim(); + + // Block output redirection (writes to files) + if trimmed.contains(" > ") || trimmed.contains(" >> ") { + return false; + } + + // Block dangerous commands explicitly + let dangerous = ["rm ", "rm\t", "rmdir", "mv ", "cp ", "mkdir ", "touch ", "chmod ", "chown ", "npm install", "yarn install", "pnpm install"]; + for d in dangerous { + if trimmed.contains(d) { + return false; + } + } + + // Split on && and || to check each command in chain + // Also split on | for pipes + let separators = ["&&", "||", "|", ";"]; + let mut parts: Vec<&str> = vec![trimmed]; + for sep in separators { + parts = parts.iter() + .flat_map(|p| p.split(sep)) + .collect(); + } + + // Each part must be a read-only command + for part in parts { + let part = part.trim(); + if part.is_empty() { + continue; + } + + // Skip "cd" commands - they don't modify anything + if part.starts_with("cd ") || part == "cd" { + continue; + } + + // Check if this part starts with a read-only command + let is_allowed = READ_ONLY_COMMANDS.iter().any(|allowed| { + part.starts_with(allowed) || part == *allowed + }); + + if !is_allowed { + return false; + } + } + + true + } + fn validate_working_dir(&self, dir: &Option) -> Result { let canonical_project = self.project_path.canonicalize() .map_err(|e| ShellError(format!("Invalid project path: {}", e)))?; @@ -179,12 +290,27 @@ Use this to validate generated configurations: } async fn call(&self, args: Self::Args) -> Result { - // Validate command is allowed - if !self.is_command_allowed(&args.command) { - return Err(ShellError(format!( - "Command not allowed. Allowed commands are: {}", - ALLOWED_COMMANDS.join(", ") - ))); + // In read-only mode (plan mode), only allow read-only commands + if self.read_only { + if !self.is_read_only_command(&args.command) { + let result = json!({ + "error": true, + "reason": "Plan mode is active - only read-only commands allowed", + "blocked_command": args.command, + "allowed_commands": READ_ONLY_COMMANDS, + "hint": "Exit plan mode (Shift+Tab) to run write commands" + }); + return serde_json::to_string_pretty(&result) + .map_err(|e| ShellError(format!("Failed to serialize: {}", e))); + } + } else { + // Validate command is allowed (standard mode) + if !self.is_command_allowed(&args.command) { + return Err(ShellError(format!( + "Command not allowed. Allowed commands are: {}", + ALLOWED_COMMANDS.join(", ") + ))); + } } // Validate and get working directory @@ -329,35 +455,22 @@ Use this to validate generated configurations: // Finalize display stream_display.finish(status.success(), status.code()); - // Truncate output if too long - const MAX_OUTPUT: usize = 10000; - let stdout_truncated = if stdout_content.len() > MAX_OUTPUT { - format!( - "{}...\n[Output truncated, {} total bytes]", - &stdout_content[..MAX_OUTPUT], - stdout_content.len() - ) - } else { - stdout_content - }; - - let stderr_truncated = if stderr_content.len() > MAX_OUTPUT { - format!( - "{}...\n[Output truncated, {} total bytes]", - &stderr_content[..MAX_OUTPUT], - stderr_content.len() - ) - } else { - stderr_content - }; + // Apply smart truncation: prefix + suffix strategy + // This keeps the first N and last M lines, hiding the middle + let limits = TruncationLimits::default(); + let truncated = truncate_shell_output(&stdout_content, &stderr_content, &limits); let result = json!({ "command": args.command, "working_dir": working_dir_str, "exit_code": status.code(), "success": status.success(), - "stdout": stdout_truncated, - "stderr": stderr_truncated + "stdout": truncated.stdout, + "stderr": truncated.stderr, + "stdout_total_lines": truncated.stdout_total_lines, + "stderr_total_lines": truncated.stderr_total_lines, + "stdout_truncated": truncated.stdout_truncated, + "stderr_truncated": truncated.stderr_truncated }); serde_json::to_string_pretty(&result) diff --git a/src/agent/tools/truncation.rs b/src/agent/tools/truncation.rs new file mode 100644 index 00000000..a3c9e004 --- /dev/null +++ b/src/agent/tools/truncation.rs @@ -0,0 +1,297 @@ +//! Truncation utilities for tool outputs +//! +//! Limits the size of tool outputs to prevent context overflow. +//! Based on Forge's approach: truncate proactively BEFORE sending to the LLM. + +/// Configuration for output truncation limits +pub struct TruncationLimits { + /// Maximum lines to return from file reads (default: 2000) + pub max_file_lines: usize, + /// Lines to keep from start of shell output (default: 200) + pub shell_prefix_lines: usize, + /// Lines to keep from end of shell output (default: 200) + pub shell_suffix_lines: usize, + /// Maximum characters per line (default: 2000) + pub max_line_length: usize, + /// Maximum directory entries to return (default: 500) + pub max_dir_entries: usize, +} + +impl Default for TruncationLimits { + fn default() -> Self { + Self { + max_file_lines: 2000, + shell_prefix_lines: 200, + shell_suffix_lines: 200, + max_line_length: 2000, + max_dir_entries: 500, + } + } +} + +/// Result of truncating file content +pub struct TruncatedFileContent { + /// The (possibly truncated) content + pub content: String, + /// Total lines in original file + pub total_lines: usize, + /// Lines actually returned + pub returned_lines: usize, + /// Whether content was truncated + pub was_truncated: bool, + /// Number of lines with truncated characters + pub lines_char_truncated: usize, +} + +/// Truncate file content to max lines, with per-line character limit +pub fn truncate_file_content(content: &str, limits: &TruncationLimits) -> TruncatedFileContent { + let lines: Vec<&str> = content.lines().collect(); + let total_lines = lines.len(); + + let (selected_lines, was_truncated) = if total_lines <= limits.max_file_lines { + (lines.clone(), false) + } else { + // Take first max_file_lines lines + (lines[..limits.max_file_lines].to_vec(), true) + }; + + let mut lines_char_truncated = 0; + let processed: Vec = selected_lines + .iter() + .map(|line| { + if line.chars().count() > limits.max_line_length { + lines_char_truncated += 1; + let truncated: String = line.chars().take(limits.max_line_length).collect(); + let extra = line.chars().count() - limits.max_line_length; + format!("{}...[{} chars truncated]", truncated, extra) + } else { + line.to_string() + } + }) + .collect(); + + let returned_lines = processed.len(); + let mut result = processed.join("\n"); + + // Add truncation notice at the end + if was_truncated { + result.push_str(&format!( + "\n\n[OUTPUT TRUNCATED: Showing first {} of {} lines. Use start_line/end_line to read specific sections.]", + returned_lines, total_lines + )); + } + + TruncatedFileContent { + content: result, + total_lines, + returned_lines, + was_truncated, + lines_char_truncated, + } +} + +/// Result of truncating shell output +pub struct TruncatedShellOutput { + /// The truncated stdout + pub stdout: String, + /// The truncated stderr + pub stderr: String, + /// Total stdout lines + pub stdout_total_lines: usize, + /// Total stderr lines + pub stderr_total_lines: usize, + /// Whether stdout was truncated + pub stdout_truncated: bool, + /// Whether stderr was truncated + pub stderr_truncated: bool, +} + +/// Truncate shell output using prefix/suffix strategy +/// Shows first N lines + last M lines, hiding the middle +pub fn truncate_shell_output( + stdout: &str, + stderr: &str, + limits: &TruncationLimits, +) -> TruncatedShellOutput { + let stdout_result = truncate_stream( + stdout, + limits.shell_prefix_lines, + limits.shell_suffix_lines, + limits.max_line_length, + ); + + let stderr_result = truncate_stream( + stderr, + limits.shell_prefix_lines, + limits.shell_suffix_lines, + limits.max_line_length, + ); + + TruncatedShellOutput { + stdout: stdout_result.0, + stderr: stderr_result.0, + stdout_total_lines: stdout_result.1, + stderr_total_lines: stderr_result.1, + stdout_truncated: stdout_result.2, + stderr_truncated: stderr_result.2, + } +} + +/// Truncate a single stream (stdout or stderr) with prefix/suffix strategy +fn truncate_stream( + content: &str, + prefix_lines: usize, + suffix_lines: usize, + max_line_length: usize, +) -> (String, usize, bool) { + let lines: Vec<&str> = content.lines().collect(); + let total_lines = lines.len(); + let max_total = prefix_lines + suffix_lines; + + if total_lines <= max_total { + // No truncation needed, just apply character limit + let processed: Vec = lines + .iter() + .map(|line| truncate_line(line, max_line_length)) + .collect(); + return (processed.join("\n"), total_lines, false); + } + + // Need truncation: take prefix + suffix + let mut result = Vec::new(); + + // Add prefix lines + for line in lines.iter().take(prefix_lines) { + result.push(truncate_line(line, max_line_length)); + } + + // Add truncation marker + let hidden = total_lines - prefix_lines - suffix_lines; + result.push(format!( + "\n... [{} lines hidden, showing first {} and last {} of {} total] ...\n", + hidden, prefix_lines, suffix_lines, total_lines + )); + + // Add suffix lines + for line in lines.iter().skip(total_lines - suffix_lines) { + result.push(truncate_line(line, max_line_length)); + } + + (result.join("\n"), total_lines, true) +} + +/// Truncate a single line if it exceeds max length +fn truncate_line(line: &str, max_length: usize) -> String { + if line.chars().count() <= max_length { + line.to_string() + } else { + let truncated: String = line.chars().take(max_length).collect(); + let extra = line.chars().count() - max_length; + format!("{}...[{} chars]", truncated, extra) + } +} + +/// Result of truncating directory listing +pub struct TruncatedDirListing { + /// The (possibly truncated) entries + pub entries: Vec, + /// Total entries in directory + pub total_entries: usize, + /// Whether list was truncated + pub was_truncated: bool, +} + +/// Truncate directory listing to max entries +pub fn truncate_dir_listing( + entries: Vec, + max_entries: usize, +) -> TruncatedDirListing { + let total_entries = entries.len(); + + if total_entries <= max_entries { + TruncatedDirListing { + entries, + total_entries, + was_truncated: false, + } + } else { + TruncatedDirListing { + entries: entries.into_iter().take(max_entries).collect(), + total_entries, + was_truncated: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_truncate_file_no_truncation_needed() { + let content = "line1\nline2\nline3"; + let limits = TruncationLimits::default(); + let result = truncate_file_content(content, &limits); + + assert_eq!(result.total_lines, 3); + assert_eq!(result.returned_lines, 3); + assert!(!result.was_truncated); + assert_eq!(result.content, content); + } + + #[test] + fn test_truncate_file_exceeds_limit() { + let lines: Vec = (0..100).map(|i| format!("line {}", i)).collect(); + let content = lines.join("\n"); + let limits = TruncationLimits { + max_file_lines: 10, + ..Default::default() + }; + let result = truncate_file_content(&content, &limits); + + assert_eq!(result.total_lines, 100); + assert_eq!(result.returned_lines, 10); + assert!(result.was_truncated); + assert!(result.content.contains("[OUTPUT TRUNCATED")); + } + + #[test] + fn test_truncate_shell_prefix_suffix() { + let lines: Vec = (0..500).map(|i| format!("output line {}", i)).collect(); + let stdout = lines.join("\n"); + let limits = TruncationLimits { + shell_prefix_lines: 5, + shell_suffix_lines: 5, + ..Default::default() + }; + let result = truncate_shell_output(&stdout, "", &limits); + + assert_eq!(result.stdout_total_lines, 500); + assert!(result.stdout_truncated); + assert!(result.stdout.contains("output line 0")); + assert!(result.stdout.contains("output line 499")); + assert!(result.stdout.contains("lines hidden")); + } + + #[test] + fn test_truncate_long_line() { + let long_line = "x".repeat(3000); + let result = truncate_line(&long_line, 100); + + assert!(result.len() < 200); // Should be truncated + assert!(result.contains("chars]")); + } + + #[test] + fn test_truncate_dir_listing() { + let entries: Vec = (0..100) + .map(|i| serde_json::json!({"name": format!("file{}", i)})) + .collect(); + + let result = truncate_dir_listing(entries, 10); + + assert_eq!(result.total_entries, 100); + assert_eq!(result.entries.len(), 10); + assert!(result.was_truncated); + } +} diff --git a/src/agent/ui/hooks.rs b/src/agent/ui/hooks.rs index eb521110..ca48c686 100644 --- a/src/agent/ui/hooks.rs +++ b/src/agent/ui/hooks.rs @@ -10,7 +10,7 @@ use crate::agent::ui::colors::ansi; use colored::Colorize; use rig::agent::CancelSignal; -use rig::completion::{CompletionModel, CompletionResponse, Message}; +use rig::completion::{CompletionModel, CompletionResponse, Message, Usage}; use rig::message::{AssistantContent, Reasoning}; use std::io::{self, Write}; use std::sync::Arc; @@ -32,6 +32,28 @@ pub struct ToolCallState { pub status_ok: bool, } +/// Accumulated usage from API responses +#[derive(Debug, Default, Clone)] +pub struct AccumulatedUsage { + pub input_tokens: u64, + pub output_tokens: u64, + pub total_tokens: u64, +} + +impl AccumulatedUsage { + /// Add usage from a completion response + pub fn add(&mut self, usage: &Usage) { + self.input_tokens += usage.input_tokens; + self.output_tokens += usage.output_tokens; + self.total_tokens += usage.total_tokens; + } + + /// Check if we have any actual usage data + pub fn has_data(&self) -> bool { + self.input_tokens > 0 || self.output_tokens > 0 || self.total_tokens > 0 + } +} + /// Shared state for the display #[derive(Debug, Default)] pub struct DisplayState { @@ -39,6 +61,8 @@ pub struct DisplayState { pub agent_messages: Vec, pub current_tool_index: Option, pub last_expandable_index: Option, + /// Accumulated token usage from API responses + pub usage: AccumulatedUsage, } /// A hook that shows Claude Code style tool execution @@ -58,6 +82,18 @@ impl ToolDisplayHook { pub fn state(&self) -> Arc> { self.state.clone() } + + /// Get accumulated usage (blocks on lock) + pub async fn get_usage(&self) -> AccumulatedUsage { + let state = self.state.lock().await; + state.usage.clone() + } + + /// Reset usage counter (e.g., at start of a new request batch) + pub async fn reset_usage(&self) { + let mut state = self.state.lock().await; + state.usage = AccumulatedUsage::default(); + } } impl Default for ToolDisplayHook { @@ -146,6 +182,9 @@ where ) -> impl std::future::Future + Send { let state = self.state.clone(); + // Capture usage from response for token tracking + let usage = response.usage.clone(); + // Check if response contains tool calls - if so, any text is "thinking" // If no tool calls, this is the final response - don't show as thinking let has_tool_calls = response.choice.iter().any(|content| { @@ -187,6 +226,12 @@ where .collect(); async move { + // Accumulate usage tokens from this response + { + let mut s = state.lock().await; + s.usage.add(&usage); + } + // First, show reasoning content if available (GPT-5.2 thinking) if !reasoning_parts.is_empty() { let thinking_text = reasoning_parts.join("\n"); diff --git a/src/agent/ui/input.rs b/src/agent/ui/input.rs index eb78170a..6b1987d8 100644 --- a/src/agent/ui/input.rs +++ b/src/agent/ui/input.rs @@ -26,6 +26,8 @@ pub enum InputResult { Cancel, /// User wants to exit Exit, + /// User toggled planning mode (Shift+Tab) + TogglePlanMode, } /// Suggestion item @@ -56,10 +58,12 @@ struct InputState { rendered_lines: usize, /// Number of wrapped lines the input text occupied in last render prev_wrapped_lines: usize, + /// Whether in plan mode (shows ★ indicator) + plan_mode: bool, } impl InputState { - fn new(project_path: PathBuf) -> Self { + fn new(project_path: PathBuf, plan_mode: bool) -> Self { Self { text: String::new(), cursor: 0, @@ -70,6 +74,7 @@ impl InputState { project_path, rendered_lines: 0, prev_wrapped_lines: 1, + plan_mode, } } @@ -475,8 +480,9 @@ fn render(state: &mut InputState, prompt: &str, stdout: &mut io::Stdout) -> io:: let (term_width, _) = terminal::size().unwrap_or((80, 24)); let term_width = term_width as usize; - // Calculate prompt length - let prompt_len = prompt.len() + 1; // +1 for space + // Calculate prompt length (include ★ prefix if in plan mode) + let mode_prefix_len = if state.plan_mode { 2 } else { 0 }; // "★ " = 2 chars + let prompt_len = prompt.len() + 1 + mode_prefix_len; // +1 for space after prompt // Move up to clear previous rendered lines, then to column 0 if state.prev_wrapped_lines > 1 { @@ -487,10 +493,14 @@ fn render(state: &mut InputState, prompt: &str, stdout: &mut io::Stdout) -> io:: // Clear from cursor to end of screen execute!(stdout, Clear(ClearType::FromCursorDown))?; - // Print prompt and input text + // Print prompt and input text with mode indicator if in plan mode // In raw mode, \n doesn't return to column 0, so we need \r\n let display_text = state.text.replace('\n', "\r\n"); - print!("{}{}{} {}", ansi::SUCCESS, prompt, ansi::RESET, display_text); + if state.plan_mode { + print!("{}★{} {}{}{} {}", ansi::ORANGE, ansi::RESET, ansi::SUCCESS, prompt, ansi::RESET, display_text); + } else { + print!("{}{}{} {}", ansi::SUCCESS, prompt, ansi::RESET, display_text); + } stdout.flush()?; // Calculate how many lines the text spans (counting newlines + wrapping) @@ -587,7 +597,8 @@ fn clear_suggestions(num_lines: usize, stdout: &mut io::Stdout) -> io::Result<() } /// Read user input with Claude Code-style @ file picker -pub fn read_input_with_file_picker(prompt: &str, project_path: &PathBuf) -> InputResult { +/// If `plan_mode` is true, shows the plan mode indicator below the prompt +pub fn read_input_with_file_picker(prompt: &str, project_path: &PathBuf, plan_mode: bool) -> InputResult { let mut stdout = io::stdout(); // Enable raw mode @@ -598,12 +609,16 @@ pub fn read_input_with_file_picker(prompt: &str, project_path: &PathBuf) -> Inpu // Enable bracketed paste mode to detect paste vs keypress let _ = execute!(stdout, EnableBracketedPaste); - // Print initial prompt and capture start row for absolute positioning - print!("{}{}{} ", ansi::SUCCESS, prompt, ansi::RESET); + // Print prompt with mode indicator inline (no separate line) + if plan_mode { + print!("{}★{} {}{}{} ", ansi::ORANGE, ansi::RESET, ansi::SUCCESS, prompt, ansi::RESET); + } else { + print!("{}{}{} ", ansi::SUCCESS, prompt, ansi::RESET); + } let _ = stdout.flush(); // Create state after printing prompt so start_row is correct - let mut state = InputState::new(project_path.clone()); + let mut state = InputState::new(project_path.clone(), plan_mode); let result = loop { match event::read() { @@ -640,6 +655,12 @@ pub fn read_input_with_file_picker(prompt: &str, project_path: &PathBuf) -> Inpu state.accept_selection(); } } + KeyCode::BackTab => { + // Shift+Tab toggles planning mode + print!("\r\n"); + let _ = stdout.flush(); + break InputResult::TogglePlanMode; + } KeyCode::Esc => { if state.showing_suggestions { state.close_suggestions(); diff --git a/src/agent/ui/mod.rs b/src/agent/ui/mod.rs index 1b7c335e..dcda1864 100644 --- a/src/agent/ui/mod.rs +++ b/src/agent/ui/mod.rs @@ -17,6 +17,7 @@ pub mod diff; pub mod hadolint_display; pub mod hooks; pub mod input; +pub mod plan_menu; pub mod response; pub mod shell_output; pub mod spinner; @@ -30,6 +31,7 @@ pub use diff::*; pub use hadolint_display::*; pub use hooks::*; pub use input::*; +pub use plan_menu::*; pub use response::*; pub use shell_output::*; pub use spinner::*; diff --git a/src/agent/ui/plan_menu.rs b/src/agent/ui/plan_menu.rs new file mode 100644 index 00000000..ce1a436e --- /dev/null +++ b/src/agent/ui/plan_menu.rs @@ -0,0 +1,153 @@ +//! Interactive menu for post-plan actions +//! +//! Displays after a plan is created with options: +//! 1. Execute and auto-accept changes +//! 2. Execute and review each change +//! 3. Change something - provide feedback + +use colored::Colorize; +use inquire::ui::{Color, IndexPrefix, RenderConfig, StyleSheet, Styled}; +use inquire::{InquireError, Select, Text}; + +/// Result of the plan action menu +#[derive(Debug, Clone)] +pub enum PlanActionResult { + /// Execute plan, auto-accept all file writes + ExecuteAutoAccept, + /// Execute plan, require confirmation for each file write + ExecuteWithReview, + /// User wants to change the plan, includes feedback + ChangePlan(String), + /// User cancelled (Esc or Ctrl+C) + Cancel, +} + +/// Get custom render config for plan menu +fn get_plan_menu_render_config() -> RenderConfig<'static> { + RenderConfig::default() + .with_highlighted_option_prefix(Styled::new("▸ ").with_fg(Color::LightCyan)) + .with_option_index_prefix(IndexPrefix::Simple) + .with_selected_option(Some(StyleSheet::new().with_fg(Color::LightCyan))) + .with_scroll_up_prefix(Styled::new("▲ ")) + .with_scroll_down_prefix(Styled::new("▼ ")) +} + +/// Display plan summary box +fn display_plan_box(plan_path: &str, task_count: usize) { + let term_width = term_size::dimensions().map(|(w, _)| w).unwrap_or(80); + let box_width = term_width.min(70); + let inner_width = box_width - 4; + + // Top border with title + println!( + "{}", + format!( + "{}{}{}", + "┌─ Plan Created ".bright_green(), + "─".repeat(inner_width.saturating_sub(15)).dimmed(), + "┐".dimmed() + ) + ); + + // Plan path + let path_display = format!(" {}", plan_path); + println!( + "{}{}{}{}", + "│".dimmed(), + path_display.cyan(), + " ".repeat(inner_width.saturating_sub(path_display.len())), + "│".dimmed() + ); + + // Task count + let tasks_display = format!(" {} tasks ready to execute", task_count); + println!( + "{}{}{}{}", + "│".dimmed(), + tasks_display.white(), + " ".repeat(inner_width.saturating_sub(tasks_display.len())), + "│".dimmed() + ); + + // Bottom border + println!( + "{}", + format!( + "{}{}{}", + "└".dimmed(), + "─".repeat(box_width - 2).dimmed(), + "┘".dimmed() + ) + ); + println!(); +} + +/// Show the post-plan action menu +/// +/// Displays after a plan is created, offering execution options: +/// 1. Execute and auto-accept - runs all tasks without confirmation prompts +/// 2. Execute and review - requires confirmation for each file write +/// 3. Change something - lets user provide feedback to modify the plan +pub fn show_plan_action_menu(plan_path: &str, task_count: usize) -> PlanActionResult { + display_plan_box(plan_path, task_count); + + let options = vec![ + "Execute and auto-accept changes".to_string(), + "Execute and review each change".to_string(), + "Change something in the plan".to_string(), + ]; + + println!("{}", "What would you like to do?".white()); + + let selection = Select::new("", options.clone()) + .with_render_config(get_plan_menu_render_config()) + .with_page_size(3) + .with_help_message("↑↓ to move, Enter to select, Esc to cancel") + .prompt(); + + match selection { + Ok(answer) => { + if answer == options[0] { + println!("{}", "→ Will execute plan with auto-accept".green()); + PlanActionResult::ExecuteAutoAccept + } else if answer == options[1] { + println!("{}", "→ Will execute plan with review for each change".yellow()); + PlanActionResult::ExecuteWithReview + } else { + // User wants to change the plan + println!(); + match Text::new("What should be changed in the plan?") + .with_help_message("Press Enter to submit, Esc to cancel") + .prompt() + { + Ok(feedback) if !feedback.trim().is_empty() => { + PlanActionResult::ChangePlan(feedback) + } + _ => PlanActionResult::Cancel, + } + } + } + Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => { + println!("{}", "Plan execution cancelled.".dimmed()); + PlanActionResult::Cancel + } + Err(_) => PlanActionResult::Cancel, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Note: Interactive tests require manual testing + // These are placeholder tests for non-interactive functionality + + #[test] + fn test_plan_action_result_variants() { + // Ensure all variants are constructible + let _ = PlanActionResult::ExecuteAutoAccept; + let _ = PlanActionResult::ExecuteWithReview; + let _ = PlanActionResult::ChangePlan("test".to_string()); + let _ = PlanActionResult::Cancel; + } +} diff --git a/patches/rig-bedrock/CHANGELOG.md b/vendor/rig-bedrock/CHANGELOG.md similarity index 100% rename from patches/rig-bedrock/CHANGELOG.md rename to vendor/rig-bedrock/CHANGELOG.md diff --git a/vendor/rig-bedrock/Cargo.toml b/vendor/rig-bedrock/Cargo.toml new file mode 100644 index 00000000..a5e7f9d1 --- /dev/null +++ b/vendor/rig-bedrock/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "rig-bedrock" +version = "0.3.9" +edition = "2024" +license = "MIT" +readme = "README.md" +description = "AWS Bedrock model provider for Rig integration (vendored with extended thinking fix)" + +[dependencies] +async-stream = "0.3" +aws-config = { version = "1", features = ["behavior-version-latest"] } +aws-sdk-bedrockruntime = "1" +aws-smithy-types = "1" +base64 = "0.22" +rig-core = { version = "0.27", default-features = false, features = ["image"] } +schemars = "0.8" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["full"] } +tracing = "0.1" +uuid = { version = "1", features = ["v4"] } + +[dev-dependencies] +anyhow = "1" +reqwest = { version = "0.12", features = ["json", "stream"] } +tracing-subscriber = "0.3" diff --git a/patches/rig-bedrock/README.md b/vendor/rig-bedrock/README.md similarity index 100% rename from patches/rig-bedrock/README.md rename to vendor/rig-bedrock/README.md diff --git a/patches/rig-bedrock/examples/agent_with_bedrock.rs b/vendor/rig-bedrock/examples/agent_with_bedrock.rs similarity index 100% rename from patches/rig-bedrock/examples/agent_with_bedrock.rs rename to vendor/rig-bedrock/examples/agent_with_bedrock.rs diff --git a/patches/rig-bedrock/examples/common/mod.rs b/vendor/rig-bedrock/examples/common/mod.rs similarity index 100% rename from patches/rig-bedrock/examples/common/mod.rs rename to vendor/rig-bedrock/examples/common/mod.rs diff --git a/patches/rig-bedrock/examples/document_with_bedrock.rs b/vendor/rig-bedrock/examples/document_with_bedrock.rs similarity index 100% rename from patches/rig-bedrock/examples/document_with_bedrock.rs rename to vendor/rig-bedrock/examples/document_with_bedrock.rs diff --git a/patches/rig-bedrock/examples/embedding_with_bedrock.rs b/vendor/rig-bedrock/examples/embedding_with_bedrock.rs similarity index 100% rename from patches/rig-bedrock/examples/embedding_with_bedrock.rs rename to vendor/rig-bedrock/examples/embedding_with_bedrock.rs diff --git a/patches/rig-bedrock/examples/extractor_with_bedrock.rs b/vendor/rig-bedrock/examples/extractor_with_bedrock.rs similarity index 100% rename from patches/rig-bedrock/examples/extractor_with_bedrock.rs rename to vendor/rig-bedrock/examples/extractor_with_bedrock.rs diff --git a/patches/rig-bedrock/examples/image_generator.rs b/vendor/rig-bedrock/examples/image_generator.rs similarity index 100% rename from patches/rig-bedrock/examples/image_generator.rs rename to vendor/rig-bedrock/examples/image_generator.rs diff --git a/patches/rig-bedrock/examples/image_with_bedrock.rs b/vendor/rig-bedrock/examples/image_with_bedrock.rs similarity index 100% rename from patches/rig-bedrock/examples/image_with_bedrock.rs rename to vendor/rig-bedrock/examples/image_with_bedrock.rs diff --git a/patches/rig-bedrock/examples/rag_with_bedrock.rs b/vendor/rig-bedrock/examples/rag_with_bedrock.rs similarity index 100% rename from patches/rig-bedrock/examples/rag_with_bedrock.rs rename to vendor/rig-bedrock/examples/rag_with_bedrock.rs diff --git a/patches/rig-bedrock/examples/streaming_with_bedrock.rs b/vendor/rig-bedrock/examples/streaming_with_bedrock.rs similarity index 100% rename from patches/rig-bedrock/examples/streaming_with_bedrock.rs rename to vendor/rig-bedrock/examples/streaming_with_bedrock.rs diff --git a/patches/rig-bedrock/examples/streaming_with_bedrock_and_tools.rs b/vendor/rig-bedrock/examples/streaming_with_bedrock_and_tools.rs similarity index 100% rename from patches/rig-bedrock/examples/streaming_with_bedrock_and_tools.rs rename to vendor/rig-bedrock/examples/streaming_with_bedrock_and_tools.rs diff --git a/patches/rig-bedrock/src/client.rs b/vendor/rig-bedrock/src/client.rs similarity index 100% rename from patches/rig-bedrock/src/client.rs rename to vendor/rig-bedrock/src/client.rs diff --git a/patches/rig-bedrock/src/completion.rs b/vendor/rig-bedrock/src/completion.rs similarity index 100% rename from patches/rig-bedrock/src/completion.rs rename to vendor/rig-bedrock/src/completion.rs diff --git a/patches/rig-bedrock/src/embedding.rs b/vendor/rig-bedrock/src/embedding.rs similarity index 100% rename from patches/rig-bedrock/src/embedding.rs rename to vendor/rig-bedrock/src/embedding.rs diff --git a/patches/rig-bedrock/src/image.rs b/vendor/rig-bedrock/src/image.rs similarity index 100% rename from patches/rig-bedrock/src/image.rs rename to vendor/rig-bedrock/src/image.rs diff --git a/patches/rig-bedrock/src/lib.rs b/vendor/rig-bedrock/src/lib.rs similarity index 100% rename from patches/rig-bedrock/src/lib.rs rename to vendor/rig-bedrock/src/lib.rs diff --git a/patches/rig-bedrock/src/streaming.rs b/vendor/rig-bedrock/src/streaming.rs similarity index 100% rename from patches/rig-bedrock/src/streaming.rs rename to vendor/rig-bedrock/src/streaming.rs diff --git a/patches/rig-bedrock/src/types/assistant_content.rs b/vendor/rig-bedrock/src/types/assistant_content.rs similarity index 65% rename from patches/rig-bedrock/src/types/assistant_content.rs rename to vendor/rig-bedrock/src/types/assistant_content.rs index 4e1c23ef..5e647af6 100644 --- a/patches/rig-bedrock/src/types/assistant_content.rs +++ b/vendor/rig-bedrock/src/types/assistant_content.rs @@ -17,6 +17,19 @@ pub struct AwsConverseOutput(pub InternalConverseOutput); impl TryFrom for completion::CompletionResponse { type Error = CompletionError; + /// Converts AWS Bedrock Converse API output to a Rig CompletionResponse. + /// + /// This preserves ALL content blocks from the assistant response including: + /// - Text content + /// - ToolCall/ToolUse blocks + /// - Reasoning blocks (for extended thinking) + /// + /// When extended thinking is enabled, Claude returns content in order: + /// [Reasoning, ToolCall] or [Reasoning, Text] + /// + /// AWS Bedrock requires that when replaying conversation history with thinking enabled, + /// assistant messages MUST start with thinking/reasoning blocks before any tool_use blocks. + /// By preserving the full choice, we ensure proper conversation history replay. fn try_from(value: AwsConverseOutput) -> Result { let message: RigMessage = value .to_owned() @@ -51,14 +64,6 @@ impl TryFrom for completion::CompletionResponse for RigAssistantContent { } } +/// Sanitize tool name to match Bedrock's required pattern: [a-zA-Z0-9_-]+ +/// Invalid characters are replaced with underscores. +/// This handles cases where the model hallucinates invalid tool names like "$FUNCTION_NAME". +fn sanitize_tool_name(name: &str) -> String { + if name.is_empty() { + return "unknown_tool".to_string(); + } + + let sanitized: String = name + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '_' || c == '-' { + c + } else { + '_' + } + }) + .collect(); + + // Log warning if name was sanitized + if sanitized != name { + tracing::warn!( + original_name = %name, + sanitized_name = %sanitized, + "Tool name contained invalid characters and was sanitized for Bedrock API" + ); + } + + // Ensure the result isn't empty after sanitization + if sanitized.is_empty() || sanitized.chars().all(|c| c == '_') { + return "unknown_tool".to_string(); + } + + sanitized +} + impl TryFrom for aws_bedrock::ContentBlock { type Error = CompletionError; @@ -109,11 +150,13 @@ impl TryFrom for aws_bedrock::ContentBlock { match value.0 { AssistantContent::Text(text) => Ok(aws_bedrock::ContentBlock::Text(text.text)), AssistantContent::ToolCall(tool_call) => { + // Sanitize tool name to match Bedrock's pattern: [a-zA-Z0-9_-]+ + let sanitized_name = sanitize_tool_name(&tool_call.function.name); let doc: AwsDocument = tool_call.function.arguments.into(); Ok(aws_bedrock::ContentBlock::ToolUse( aws_bedrock::ToolUseBlock::builder() .tool_use_id(tool_call.id) - .name(tool_call.function.name) + .name(sanitized_name) .input(doc.0) .build() .map_err(|e| CompletionError::ProviderError(e.to_string()))?, @@ -309,4 +352,115 @@ mod tests { _ => panic!("Expected ContentBlock::ReasoningContent"), } } + + #[test] + fn aws_converse_output_preserves_reasoning_with_tool_call() { + // Test that when extended thinking is enabled and Claude returns both + // Reasoning and ToolCall, BOTH are preserved in the response. + // This is critical for AWS Bedrock's requirement that assistant messages + // must start with thinking blocks when thinking is enabled. + + // Build a message with both Reasoning and ToolUse content blocks + let reasoning_block = aws_bedrock::ReasoningTextBlock::builder() + .text("Let me think about this...") + .signature("sig_test_123") + .build() + .unwrap(); + + let tool_use_block = aws_bedrock::ToolUseBlock::builder() + .tool_use_id("tool_123") + .name("analyze_project") + .input(aws_smithy_types::Document::Object(std::collections::HashMap::new())) + .build() + .unwrap(); + + let message = aws_bedrock::Message::builder() + .role(aws_bedrock::ConversationRole::Assistant) + .content(aws_bedrock::ContentBlock::ReasoningContent( + aws_bedrock::ReasoningContentBlock::ReasoningText(reasoning_block), + )) + .content(aws_bedrock::ContentBlock::ToolUse(tool_use_block)) + .build() + .unwrap(); + + let output = aws_bedrock::ConverseOutput::Message(message); + let converse_output = + aws_sdk_bedrockruntime::operation::converse::ConverseOutput::builder() + .output(output) + .stop_reason(aws_bedrock::StopReason::ToolUse) + .build() + .unwrap(); + + let converse_output: Result = + converse_output.try_into(); + assert!(converse_output.is_ok()); + + let completion: Result, _> = + AwsConverseOutput(converse_output.unwrap()).try_into(); + assert!(completion.is_ok()); + + let completion = completion.unwrap(); + + // Verify we have BOTH content blocks preserved + let contents: Vec<_> = completion.choice.iter().collect(); + assert_eq!(contents.len(), 2, "Expected both Reasoning and ToolCall to be preserved"); + + // First should be Reasoning + match &contents[0] { + AssistantContent::Reasoning(reasoning) => { + assert_eq!(reasoning.reasoning, vec!["Let me think about this..."]); + assert_eq!(reasoning.signature, Some("sig_test_123".to_string())); + } + _ => panic!("Expected first content to be Reasoning, got {:?}", contents[0]), + } + + // Second should be ToolCall + match &contents[1] { + AssistantContent::ToolCall(tool_call) => { + assert_eq!(tool_call.id, "tool_123"); + assert_eq!(tool_call.function.name, "analyze_project"); + } + _ => panic!("Expected second content to be ToolCall, got {:?}", contents[1]), + } + } + + #[test] + fn test_sanitize_tool_name_valid() { + use super::sanitize_tool_name; + + // Valid names should pass through unchanged + assert_eq!(sanitize_tool_name("read_file"), "read_file"); + assert_eq!(sanitize_tool_name("analyze-project"), "analyze-project"); + assert_eq!(sanitize_tool_name("tool123"), "tool123"); + assert_eq!(sanitize_tool_name("My_Tool-Name_123"), "My_Tool-Name_123"); + } + + #[test] + fn test_sanitize_tool_name_invalid_chars() { + use super::sanitize_tool_name; + + // Invalid characters should be replaced with underscores + assert_eq!(sanitize_tool_name("$FUNCTION_NAME"), "_FUNCTION_NAME"); + assert_eq!(sanitize_tool_name("tool.name"), "tool_name"); + assert_eq!(sanitize_tool_name("tool name"), "tool_name"); + assert_eq!(sanitize_tool_name("tool@name#test"), "tool_name_test"); + assert_eq!(sanitize_tool_name("hello/world"), "hello_world"); + } + + #[test] + fn test_sanitize_tool_name_edge_cases() { + use super::sanitize_tool_name; + + // Empty string + assert_eq!(sanitize_tool_name(""), "unknown_tool"); + + // All invalid characters + assert_eq!(sanitize_tool_name("$@#!"), "unknown_tool"); + + // Single valid character + assert_eq!(sanitize_tool_name("a"), "a"); + + // Unicode characters get replaced + assert_eq!(sanitize_tool_name("tøøl"), "t__l"); + } } diff --git a/patches/rig-bedrock/src/types/completion_request.rs b/vendor/rig-bedrock/src/types/completion_request.rs similarity index 100% rename from patches/rig-bedrock/src/types/completion_request.rs rename to vendor/rig-bedrock/src/types/completion_request.rs diff --git a/patches/rig-bedrock/src/types/converse_output.rs b/vendor/rig-bedrock/src/types/converse_output.rs similarity index 100% rename from patches/rig-bedrock/src/types/converse_output.rs rename to vendor/rig-bedrock/src/types/converse_output.rs diff --git a/patches/rig-bedrock/src/types/document.rs b/vendor/rig-bedrock/src/types/document.rs similarity index 100% rename from patches/rig-bedrock/src/types/document.rs rename to vendor/rig-bedrock/src/types/document.rs diff --git a/patches/rig-bedrock/src/types/errors.rs b/vendor/rig-bedrock/src/types/errors.rs similarity index 100% rename from patches/rig-bedrock/src/types/errors.rs rename to vendor/rig-bedrock/src/types/errors.rs diff --git a/patches/rig-bedrock/src/types/image.rs b/vendor/rig-bedrock/src/types/image.rs similarity index 100% rename from patches/rig-bedrock/src/types/image.rs rename to vendor/rig-bedrock/src/types/image.rs diff --git a/patches/rig-bedrock/src/types/json.rs b/vendor/rig-bedrock/src/types/json.rs similarity index 100% rename from patches/rig-bedrock/src/types/json.rs rename to vendor/rig-bedrock/src/types/json.rs diff --git a/patches/rig-bedrock/src/types/media_types.rs b/vendor/rig-bedrock/src/types/media_types.rs similarity index 100% rename from patches/rig-bedrock/src/types/media_types.rs rename to vendor/rig-bedrock/src/types/media_types.rs diff --git a/patches/rig-bedrock/src/types/message.rs b/vendor/rig-bedrock/src/types/message.rs similarity index 100% rename from patches/rig-bedrock/src/types/message.rs rename to vendor/rig-bedrock/src/types/message.rs diff --git a/patches/rig-bedrock/src/types/mod.rs b/vendor/rig-bedrock/src/types/mod.rs similarity index 100% rename from patches/rig-bedrock/src/types/mod.rs rename to vendor/rig-bedrock/src/types/mod.rs diff --git a/patches/rig-bedrock/src/types/text_to_image.rs b/vendor/rig-bedrock/src/types/text_to_image.rs similarity index 100% rename from patches/rig-bedrock/src/types/text_to_image.rs rename to vendor/rig-bedrock/src/types/text_to_image.rs diff --git a/patches/rig-bedrock/src/types/tool.rs b/vendor/rig-bedrock/src/types/tool.rs similarity index 100% rename from patches/rig-bedrock/src/types/tool.rs rename to vendor/rig-bedrock/src/types/tool.rs diff --git a/patches/rig-bedrock/src/types/user_content.rs b/vendor/rig-bedrock/src/types/user_content.rs similarity index 100% rename from patches/rig-bedrock/src/types/user_content.rs rename to vendor/rig-bedrock/src/types/user_content.rs