From 7f039d2c08281f8597f4a9ad36246421936569a8 Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Mon, 1 Jun 2026 13:02:22 -0500 Subject: [PATCH 1/2] Applying additional cache fixes --- Cargo.lock | 501 +++++++++++------- app/src/ai/bedrock/convert.rs | 10 +- app/src/ai/mod.rs | 1 + app/src/ai/prompt_builder/context.rs | 48 ++ app/src/ai/prompt_builder/mod.rs | 192 +++++++ app/src/ai/prompt_builder/mode.rs | 66 +++ .../ai/prompt_builder/prompts/anthropic.rs | 160 ++++++ app/src/ai/prompt_builder/prompts/mod.rs | 40 ++ app/src/ai/prompt_builder/prompts/provider.rs | 28 + app/src/ai/prompt_builder/tests.rs | 131 +++++ app/src/ai/prompt_builder/tools.rs | 331 ++++++++++++ script/install-galaxy.sh | 5 +- 12 files changed, 1326 insertions(+), 187 deletions(-) create mode 100644 app/src/ai/prompt_builder/context.rs create mode 100644 app/src/ai/prompt_builder/mod.rs create mode 100644 app/src/ai/prompt_builder/mode.rs create mode 100644 app/src/ai/prompt_builder/prompts/anthropic.rs create mode 100644 app/src/ai/prompt_builder/prompts/mod.rs create mode 100644 app/src/ai/prompt_builder/prompts/provider.rs create mode 100644 app/src/ai/prompt_builder/tests.rs create mode 100644 app/src/ai/prompt_builder/tools.rs diff --git a/Cargo.lock b/Cargo.lock index f50c1118..c03bd85d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,7 +201,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "generic-array", ] @@ -213,7 +213,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.9", ] [[package]] @@ -257,7 +257,7 @@ dependencies = [ "cmac", "ctr", "dbl", - "digest", + "digest 0.10.7", "zeroize", ] @@ -342,7 +342,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "sha2", + "sha2 0.10.9", "shellexpand", "streaming-iterator", "string-offset", @@ -1153,7 +1153,7 @@ dependencies = [ "galaxy_util", "proc-macro2", "quote", - "sha2", + "sha2 0.10.9", "syn 2.0.117", ] @@ -1484,8 +1484,8 @@ dependencies = [ "aws-sdk-ssooidc", "aws-sdk-sts", "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-http 0.62.6", + "aws-smithy-json 0.61.9", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -1494,11 +1494,11 @@ dependencies = [ "bytes", "fastrand 2.3.0", "hex", - "http 1.1.0", + "http 1.4.1", "p256", "rand 0.8.5", "ring", - "sha2", + "sha2 0.10.9", "time", "tokio", "tracing", @@ -1509,9 +1509,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.11" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -1543,23 +1543,24 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.17" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81b5b2898f6798ad58f484856768bca817e3cd9de0974c24ae0f1113fe88f1b" +checksum = "77ed8e8c52d2dc2390ad9f15647fe663f71e9780b4262c190fbb823a32721566" dependencies = [ "aws-credential-types", "aws-sigv4", "aws-smithy-async", "aws-smithy-eventstream", - "aws-smithy-http", + "aws-smithy-http 0.63.6", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", "bytes", + "bytes-utils", "fastrand 2.3.0", - "http 0.2.12", - "http-body 0.4.6", + "http 1.4.1", + "http-body 1.0.1", "percent-encoding", "pin-project-lite", "tracing", @@ -1575,8 +1576,8 @@ dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-http 0.62.6", + "aws-smithy-json 0.61.9", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -1590,17 +1591,19 @@ dependencies = [ [[package]] name = "aws-sdk-bedrockruntime" -version = "1.120.0" +version = "1.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b8dcf42378ab2d5accac1652cdd059114fb071baf53250ceafb76fcdde347f" +checksum = "41a2940faeb61f4f579a434bc3a546e9ab49a89596e94527d329281ef55fd44d" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-sigv4", "aws-smithy-async", "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-http 0.63.6", + "aws-smithy-json 0.62.6", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -1608,7 +1611,8 @@ dependencies = [ "bytes", "fastrand 2.3.0", "http 0.2.12", - "hyper 0.14.32", + "http 1.4.1", + "http-body-util", "regex-lite", "tracing", ] @@ -1622,8 +1626,8 @@ dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-http 0.62.6", + "aws-smithy-json 0.61.9", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -1644,8 +1648,8 @@ dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-http 0.62.6", + "aws-smithy-json 0.61.9", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -1666,8 +1670,8 @@ dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-http 0.62.6", + "aws-smithy-json 0.61.9", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -1688,8 +1692,8 @@ dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", + "aws-smithy-http 0.62.6", + "aws-smithy-json 0.61.9", "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -1704,32 +1708,32 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.3.7" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c" +checksum = "b7083fb918b38474ac65ffbf8a69fc8792d36879f4ac5f1667b43aec61efe9a5" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", - "aws-smithy-http", + "aws-smithy-http 0.63.6", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", "form_urlencoded", "hex", - "hmac", + "hmac 0.13.0", "http 0.2.12", - "http 1.1.0", + "http 1.4.1", "percent-encoding", - "sha2", + "sha2 0.11.0", "time", "tracing", ] [[package]] name = "aws-smithy-async" -version = "1.2.7" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee19095c7c4dda59f1697d028ce704c24b2d33c6718790c7f1d5a3015b4107c" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" dependencies = [ "futures-util", "pin-project-lite", @@ -1738,9 +1742,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.14" +version = "0.60.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc12f8b310e38cad85cf3bef45ad236f470717393c613266ce0a89512286b650" +checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" dependencies = [ "aws-smithy-types", "bytes", @@ -1753,7 +1757,6 @@ version = "0.62.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" dependencies = [ - "aws-smithy-eventstream", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", @@ -1761,7 +1764,7 @@ dependencies = [ "futures-core", "futures-util", "http 0.2.12", - "http 1.1.0", + "http 1.4.1", "http-body 0.4.6", "percent-encoding", "pin-project-lite", @@ -1770,10 +1773,32 @@ dependencies = [ ] [[package]] -name = "aws-smithy-http-client" -version = "1.1.5" +name = "aws-smithy-http" +version = "0.63.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e62db736db19c488966c8d787f52e6270be565727236fd5579eaa301e7bc4a" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.1", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -1781,7 +1806,7 @@ dependencies = [ "h2 0.3.27", "h2 0.4.12", "http 0.2.12", - "http 1.1.0", + "http 1.4.1", "http-body 0.4.6", "hyper 0.14.32", "hyper 1.8.1", @@ -1809,10 +1834,21 @@ dependencies = [ ] [[package]] -name = "aws-smithy-observability" -version = "0.2.0" +name = "aws-smithy-json" +version = "0.62.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1fcbefc7ece1d70dcce29e490f269695dfca2d2bacdeaf9e5c3f799e4e6a42" +checksum = "517089205f18ab4adc5a3e02888cb139bbbbb2e168eac9f396216925d1fbeaf5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" dependencies = [ "aws-smithy-runtime-api", ] @@ -1829,22 +1865,24 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.8" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb5b6167fcdf47399024e81ac08e795180c576a20e4d4ce67949f9a88ae37dc1" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" dependencies = [ "aws-smithy-async", - "aws-smithy-http", + "aws-smithy-http 0.63.6", "aws-smithy-http-client", "aws-smithy-observability", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "bytes", "fastrand 2.3.0", "http 0.2.12", - "http 1.1.0", + "http 1.4.1", "http-body 0.4.6", - "http-body 1.0.0", + "http-body 1.0.1", + "http-body-util", "pin-project-lite", "pin-utils", "tokio", @@ -1853,15 +1891,16 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.10.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efce7aaaf59ad53c5412f14fc19b2d5c6ab2c3ec688d272fd31f76ec12f44fb0" +checksum = "dc117c179ecf39a62a0a3f49f600e9ac26a7ad7dd172177999f83933af776c32" dependencies = [ "aws-smithy-async", + "aws-smithy-runtime-api-macros", "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.1.0", + "http 1.4.1", "pin-project-lite", "tokio", "tracing", @@ -1869,19 +1908,41 @@ dependencies = [ ] [[package]] -name = "aws-smithy-types" -version = "1.3.6" +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f172bcb02424eb94425db8aed1b6d583b5104d4d5ddddf22402c661a320048" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.1", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056b66dbce2f81cc0c1e2b05bb402eb58f8a3530479d650efadd5bbae9a4050b" dependencies = [ "base64-simd", "bytes", "bytes-utils", "futures-core", "http 0.2.12", - "http 1.1.0", + "http 1.4.1", "http-body 0.4.6", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", "itoa", "num-integer", @@ -1905,13 +1966,14 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.11" +version = "1.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" +checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" dependencies = [ "aws-credential-types", "aws-smithy-async", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "rustc_version", "tracing", @@ -1927,8 +1989,8 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "http 1.4.1", + "http-body 1.0.1", "http-body-util", "hyper 1.8.1", "hyper-util", @@ -1959,8 +2021,8 @@ checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", "futures-core", - "http 1.1.0", - "http-body 1.0.0", + "http 1.4.1", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -1981,8 +2043,8 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "http 1.4.1", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -2233,6 +2295,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -2677,7 +2748,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.9", ] [[package]] @@ -2755,7 +2826,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", "zeroize", ] @@ -2837,7 +2908,7 @@ checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" dependencies = [ "cipher", "dbl", - "digest", + "digest 0.10.7", ] [[package]] @@ -2849,6 +2920,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cocoa" version = "0.25.0" @@ -3125,6 +3202,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -3377,6 +3460,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crash-context" version = "0.6.3" @@ -3557,6 +3649,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cstr" version = "0.2.11" @@ -3617,6 +3718,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "cursor-icon" version = "1.1.0" @@ -3630,7 +3740,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.9", "curve25519-dalek-derive", "fiat-crypto", "rustc_version", @@ -3875,7 +3985,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -4083,12 +4193,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "directories" version = "6.0.0" @@ -4294,7 +4416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -4315,7 +4437,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -5305,7 +5427,7 @@ dependencies = [ "gloo", "handlebars", "hex", - "http 1.1.0", + "http 1.4.1", "http_client", "http_server", "hyper 1.8.1", @@ -5401,7 +5523,7 @@ dependencies = [ "session-sharing-protocol", "settings", "settings_value", - "sha2", + "sha2 0.10.9", "shell-words", "shellexpand", "shlex", @@ -5545,7 +5667,7 @@ dependencies = [ "galaxyui", "galaxyui_extras", "getset", - "http 1.1.0", + "http 1.4.1", "instant", "inventory", "itertools 0.14.0", @@ -5669,7 +5791,7 @@ dependencies = [ "galaxy_core", "galaxy_graphql_schema", "graphql-ws-client", - "http 1.1.0", + "http 1.4.1", "http_client", "instant", "log", @@ -6071,7 +6193,7 @@ dependencies = [ "security-framework 2.9.2", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 2.0.17", "toml_edit 0.25.6+spec-1.1.0", @@ -6719,7 +6841,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.1.0", + "http 1.4.1", "indexmap 2.12.0", "slab", "tokio", @@ -6864,7 +6986,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -6873,7 +6995,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -6911,12 +7042,12 @@ dependencies = [ "aead", "aes-gcm", "chacha20poly1305", - "digest", + "digest 0.10.7", "generic-array", "hkdf", - "hmac", + "hmac 0.12.1", "rand_core 0.9.3", - "sha2", + "sha2 0.10.9", "subtle", "x25519-dalek", "zeroize", @@ -6961,12 +7092,11 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -6983,12 +7113,12 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http 1.4.1", ] [[package]] @@ -6999,8 +7129,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.1.0", - "http-body 1.0.0", + "http 1.4.1", + "http-body 1.0.1", "pin-project-lite", ] @@ -7022,7 +7152,7 @@ dependencies = [ "futures", "galaxy_core", "gloo", - "http 1.1.0", + "http 1.4.1", "log", "oauth2", "prevent_sleep", @@ -7065,6 +7195,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -7100,8 +7239,8 @@ dependencies = [ "futures-channel", "futures-core", "h2 0.4.12", - "http 1.1.0", - "http-body 1.0.0", + "http 1.4.1", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -7133,7 +7272,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.1.0", + "http 1.4.1", "hyper 1.8.1", "hyper-util", "rustls 0.23.39", @@ -7156,8 +7295,8 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "http 1.4.1", + "http-body 1.0.1", "hyper 1.8.1", "ipnet", "libc", @@ -7646,17 +7785,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipc" version = "0.1.0" @@ -7776,9 +7904,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jaq-all" @@ -8422,7 +8550,7 @@ dependencies = [ "repo_metadata", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "simple_logger", "strum", "strum_macros", @@ -8929,8 +9057,8 @@ dependencies = [ "bytes", "colored", "futures-core", - "http 1.1.0", - "http-body 1.0.0", + "http 1.4.1", + "http-body 1.0.1", "http-body-util", "hyper 1.8.1", "hyper-util", @@ -9176,7 +9304,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tar", "zip 2.4.2", ] @@ -9424,16 +9552,16 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.22.1", + "base64 0.21.7", "chrono", "getrandom 0.2.16", - "http 1.1.0", + "http 1.4.1", "rand 0.8.5", "reqwest", "serde", "serde_json", "serde_path_to_error", - "sha2", + "sha2 0.10.9", "thiserror 1.0.63", "url", ] @@ -10058,7 +10186,7 @@ checksum = "e2aba9f5c7c479925205799216e7e5d07cc1d4fa76ea8058c60a9a30f6a4e890" dependencies = [ "flate2", "pkg-config", - "sha2", + "sha2 0.10.9", "tar", "ureq", ] @@ -10081,7 +10209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.45.0", ] [[package]] @@ -10142,7 +10270,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -10254,8 +10382,8 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", ] [[package]] @@ -10511,7 +10639,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.9", "opaque-debug", "universal-hash", ] @@ -10523,7 +10651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.9", "opaque-debug", "universal-hash", ] @@ -10750,9 +10878,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -11592,8 +11720,8 @@ dependencies = [ "futures-core", "futures-util", "h2 0.4.12", - "http 1.1.0", - "http-body 1.0.0", + "http 1.4.1", + "http-body 1.0.1", "http-body-util", "hyper 1.8.1", "hyper-rustls 0.27.7", @@ -11664,7 +11792,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -11729,7 +11857,7 @@ dependencies = [ "base64 0.22.1", "chrono", "futures", - "http 1.1.0", + "http 1.4.1", "oauth2", "pastey 0.2.1", "pin-project-lite", @@ -11883,7 +12011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" dependencies = [ "globset", - "sha2", + "sha2 0.10.9", "walkdir", ] @@ -11894,7 +12022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" dependencies = [ "globset", - "sha2", + "sha2 0.10.9", "walkdir", ] @@ -12126,9 +12254,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "safe_arch" @@ -12301,7 +12429,7 @@ dependencies = [ "num", "once_cell", "serde", - "sha2", + "sha2 0.10.9", "zbus", ] @@ -12799,8 +12927,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.9", + "digest 0.10.7", ] [[package]] @@ -12810,8 +12938,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.9", + "digest 0.10.7", ] [[package]] @@ -12821,8 +12949,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.9", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -12891,7 +13030,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -13165,7 +13304,7 @@ checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" dependencies = [ "bytes", "futures-util", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", "pin-project-lite", ] @@ -13890,14 +14029,14 @@ name = "tink-core" version = "0.3.0" source = "git+https://github.com/warpdotdev/tink-rust?branch=warpdotdev%2Fmain#0141035f04a5e262b955c450857b689cff877469" dependencies = [ - "digest", + "digest 0.10.7", "hkdf", "lazy_static", "rand 0.8.5", "serde", "serde_json", "sha-1", - "sha2", + "sha2 0.10.9", "subtle", "tink-proto", ] @@ -13945,11 +14084,11 @@ checksum = "78d1cabf040b08759a32d8b2a830c707b7c578fc92f68a3eeca4231cd6dfca33" dependencies = [ "aes", "cmac", - "digest", + "digest 0.10.7", "hkdf", - "hmac", + "hmac 0.12.1", "sha-1", - "sha2", + "sha2 0.10.9", "tink-core", "tink-proto", ] @@ -14103,29 +14242,26 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" 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", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -14165,9 +14301,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -14175,7 +14311,6 @@ dependencies = [ "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -14347,8 +14482,8 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "http 1.4.1", + "http-body 1.0.1", "http-body-util", "http-range-header", "httpdate", @@ -14379,9 +14514,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -14391,9 +14526,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -14402,9 +14537,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -14501,7 +14636,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.1.0", + "http 1.4.1", "httparse", "log", "rand 0.8.5", @@ -14544,9 +14679,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.17.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "typetag" @@ -14744,7 +14879,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "subtle", ] @@ -14782,7 +14917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59db78ad1923f2b1be62b6da81fe80b173605ca0d57f85da2e005382adf693f7" dependencies = [ "base64 0.22.1", - "http 1.1.0", + "http 1.4.1", "httparse", "log", ] @@ -15473,7 +15608,7 @@ dependencies = [ "futures-test-sink", "futures-util", "graphql-ws-client", - "http 1.1.0", + "http 1.4.1", "http-body-util", "hyper 1.8.1", "hyper-util", @@ -16701,7 +16836,7 @@ dependencies = [ "displaydoc", "flate2", "getrandom 0.3.4", - "hmac", + "hmac 0.12.1", "indexmap 2.12.0", "lzma-rs", "memchr", diff --git a/app/src/ai/bedrock/convert.rs b/app/src/ai/bedrock/convert.rs index 58884f1a..fa9ecbd3 100644 --- a/app/src/ai/bedrock/convert.rs +++ b/app/src/ai/bedrock/convert.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; use aws_sdk_bedrockruntime::types::{ - CachePointBlock, CachePointType, ContentBlock, ConversationRole, InferenceConfiguration, - Message as BedrockMessage, SystemContentBlock, Tool, ToolConfiguration, ToolInputSchema, - ToolResultBlock, ToolResultContentBlock, ToolResultStatus, ToolSpecification, ToolUseBlock, + CachePointBlock, CachePointType, CacheTtl, ContentBlock, ConversationRole, + InferenceConfiguration, Message as BedrockMessage, SystemContentBlock, Tool, + ToolConfiguration, ToolInputSchema, ToolResultBlock, ToolResultContentBlock, ToolResultStatus, + ToolSpecification, ToolUseBlock, }; use aws_smithy_types::Document; use serde_json::Value as JsonValue; @@ -220,6 +221,7 @@ fn convert_messages(messages: Vec) -> Vec { content.push(ContentBlock::CachePoint( CachePointBlock::builder() .r#type(CachePointType::Default) + .ttl(CacheTtl::OneHour) .build() .expect("valid cache point"), )); @@ -273,6 +275,7 @@ fn convert_system_prompt(system_prompt: Option) -> Vec) -> Option { tool_specs.push(Tool::CachePoint( CachePointBlock::builder() .r#type(CachePointType::Default) + .ttl(CacheTtl::OneHour) .build() .expect("valid cache point"), )); diff --git a/app/src/ai/mod.rs b/app/src/ai/mod.rs index fe61270e..c91d8b11 100644 --- a/app/src/ai/mod.rs +++ b/app/src/ai/mod.rs @@ -17,6 +17,7 @@ pub(crate) mod attachment_utils; pub mod aws_credentials; #[cfg(not(target_family = "wasm"))] pub mod bedrock; +pub mod prompt_builder; pub(crate) mod block_context; pub(crate) mod blocklist; pub mod control_code_parser; diff --git a/app/src/ai/prompt_builder/context.rs b/app/src/ai/prompt_builder/context.rs new file mode 100644 index 00000000..8c6042da --- /dev/null +++ b/app/src/ai/prompt_builder/context.rs @@ -0,0 +1,48 @@ +//! Environment context that gets injected into system prompts. + +/// Environment context about the user's machine and project. +#[derive(Debug, Clone, Default)] +pub struct PromptContext { + pub working_dir: String, + pub home_dir: String, + pub os: String, + pub shell: String, + pub git_branch: String, + pub current_time: String, +} + +impl PromptContext { + pub fn new() -> Self { + Self::default() + } + + pub fn with_working_dir(mut self, dir: impl Into) -> Self { + self.working_dir = dir.into(); + self + } + + pub fn with_home_dir(mut self, dir: impl Into) -> Self { + self.home_dir = dir.into(); + self + } + + pub fn with_os(mut self, os: impl Into) -> Self { + self.os = os.into(); + self + } + + pub fn with_shell(mut self, shell: impl Into) -> Self { + self.shell = shell.into(); + self + } + + pub fn with_git_branch(mut self, branch: impl Into) -> Self { + self.git_branch = branch.into(); + self + } + + pub fn with_current_time(mut self, time: impl Into) -> Self { + self.current_time = time.into(); + self + } +} \ No newline at end of file diff --git a/app/src/ai/prompt_builder/mod.rs b/app/src/ai/prompt_builder/mod.rs new file mode 100644 index 00000000..01108fe5 --- /dev/null +++ b/app/src/ai/prompt_builder/mod.rs @@ -0,0 +1,192 @@ +//! Prompt Builder Module +//! +//! Centralized system for constructing AI model prompts based on the current +//! operational mode (code, plan, review, etc.) and provider (Anthropic, etc.). +//! +//! # Architecture +//! +//! The prompt builder separates concerns into: +//! - **Mode**: What the agent is doing (coding, planning, reviewing, etc.) +//! - **Provider**: Which LLM is being used (Anthropic Claude, future: Gemini, GPT, etc.) +//! - **Context**: Environment info (working directory, OS, shell, git state, etc.) +//! - **Rules**: Project-specific rules from AGENTS.md / GALAXY.md files +//! - **Tools**: Which tools are available (varies by mode) +//! +//! The builder composes these layers to produce the final system prompt and tool +//! definitions for each request. + +mod context; +mod mode; +mod prompts; +mod tools; + +pub use context::PromptContext; +pub use mode::Mode; +pub use prompts::provider::Provider; +pub use tools::ToolSet; + +use crate::ai::bedrock::convert::ToolDefinition; + +/// The fully-resolved prompt configuration ready to send to a model. +#[derive(Debug, Clone)] +pub struct ResolvedPrompt { + /// The system prompt text. + pub system_prompt: String, + /// The tool definitions available for this request. + pub tools: Vec, +} + +/// The main prompt builder. Composes mode, provider, context, and rules +/// into a final system prompt and tool set. +pub struct PromptBuilder { + mode: Mode, + provider: Provider, + context: PromptContext, + project_rules: Vec, + mcp_tools: Vec, +} + +/// A project rule loaded from AGENTS.md or GALAXY.md files. +#[derive(Debug, Clone)] +pub struct ProjectRule { + pub root_path: String, + pub content: String, +} + +impl PromptBuilder { + /// Create a new prompt builder with the given mode and provider. + pub fn new(mode: Mode, provider: Provider) -> Self { + Self { + mode, + provider, + context: PromptContext::default(), + project_rules: Vec::new(), + mcp_tools: Vec::new(), + } + } + + /// Set the environment context. + pub fn with_context(mut self, context: PromptContext) -> Self { + self.context = context; + self + } + + /// Add project rules (from AGENTS.md, GALAXY.md, etc.). + pub fn with_project_rules(mut self, rules: Vec) -> Self { + self.project_rules = rules; + self + } + + /// Add MCP tools from connected servers. + pub fn with_mcp_tools(mut self, tools: Vec) -> Self { + self.mcp_tools = tools; + self + } + + /// Build the final resolved prompt. + pub fn build(&self) -> ResolvedPrompt { + let system_prompt = self.build_system_prompt(); + let tools = self.build_tools(); + ResolvedPrompt { + system_prompt, + tools, + } + } + + fn build_system_prompt(&self) -> String { + let mut parts: Vec = Vec::with_capacity(8); + + // 1. Identity + mode-specific base prompt + parts.push(self.identity_prompt()); + + // 2. Environment context + if let Some(env) = self.environment_section() { + parts.push(env); + } + + // 3. Project rules + if let Some(rules) = self.rules_section() { + parts.push(rules); + } + + // 4. Mode-specific instructions + parts.push(self.mode_instructions()); + + // 5. Tool usage guidelines + parts.push(self.tool_usage_section()); + + parts.join("\n\n") + } + + fn identity_prompt(&self) -> String { + let base = prompts::base_identity(&self.provider); + let mode_identity = prompts::mode_identity(&self.mode); + format!("{base}\n\n{mode_identity}") + } + + fn environment_section(&self) -> Option { + let ctx = &self.context; + if ctx.working_dir.is_empty() && ctx.os.is_empty() { + return None; + } + + let mut lines = vec!["## Environment".to_string()]; + + if !ctx.working_dir.is_empty() { + lines.push(format!("- Working directory: {}", ctx.working_dir)); + } + if !ctx.home_dir.is_empty() { + lines.push(format!("- Home directory: {}", ctx.home_dir)); + } + if !ctx.os.is_empty() { + lines.push(format!("- OS: {}", ctx.os)); + } + if !ctx.shell.is_empty() { + lines.push(format!("- Shell: {}", ctx.shell)); + } + if !ctx.git_branch.is_empty() { + lines.push(format!("- Git branch: {}", ctx.git_branch)); + } + if !ctx.current_time.is_empty() { + lines.push(format!("- Current time (UTC): {}", ctx.current_time)); + } + + Some(lines.join("\n")) + } + + fn rules_section(&self) -> Option { + if self.project_rules.is_empty() { + return None; + } + + let mut section = String::from("## Project Rules\n"); + for rule in &self.project_rules { + if !rule.root_path.is_empty() { + section.push_str(&format!("### Rules from {}\n", rule.root_path)); + } + section.push_str(&rule.content); + section.push('\n'); + } + Some(section) + } + + fn mode_instructions(&self) -> String { + prompts::mode_instructions(&self.mode, &self.provider) + } + + fn tool_usage_section(&self) -> String { + prompts::tool_usage_guidelines(&self.mode, &self.provider) + } + + fn build_tools(&self) -> Vec { + let mut tool_set = tools::tools_for_mode(&self.mode); + + // Append MCP tools + tool_set.extend(self.mcp_tools.clone()); + + tool_set + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/app/src/ai/prompt_builder/mode.rs b/app/src/ai/prompt_builder/mode.rs new file mode 100644 index 00000000..269106d5 --- /dev/null +++ b/app/src/ai/prompt_builder/mode.rs @@ -0,0 +1,66 @@ +//! Operational modes that determine prompt behavior and tool availability. + +/// The operational mode the agent is currently in. +/// +/// Each mode provides different system prompt instructions and makes +/// different tools available to the model. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Mode { + /// General coding mode - the default. Full tool access, focused on + /// implementing changes, debugging, and exploring codebases. + Code, + + /// Planning mode - read-only exploration and design. The model should + /// NOT make edits, only analyze, research, and produce a plan. + Plan, + + /// Code review mode - focused on reviewing diffs/changes and providing + /// feedback on correctness, style, and potential issues. + Review, + + /// Conversation summary mode - condensing conversation history while + /// preserving key decisions and context. + Summarize, + + /// Title generation mode - producing a short title for a conversation. + Title, +} + +impl Mode { + /// Whether this mode allows file edits. + pub fn allows_edits(&self) -> bool { + matches!(self, Mode::Code) + } + + /// Whether this mode allows shell command execution. + pub fn allows_shell(&self) -> bool { + matches!(self, Mode::Code) + } + + /// Whether this mode allows read-only exploration tools. + pub fn allows_read(&self) -> bool { + matches!(self, Mode::Code | Mode::Plan | Mode::Review) + } + + /// Whether this mode should use sub-agents. + pub fn allows_subagents(&self) -> bool { + matches!(self, Mode::Code | Mode::Plan) + } + + /// Human-readable label for logging/diagnostics. + pub fn label(&self) -> &'static str { + match self { + Mode::Code => "code", + Mode::Plan => "plan", + Mode::Review => "review", + Mode::Summarize => "summarize", + Mode::Title => "title", + } + } +} + +impl std::fmt::Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.label()) + } +} \ No newline at end of file diff --git a/app/src/ai/prompt_builder/prompts/anthropic.rs b/app/src/ai/prompt_builder/prompts/anthropic.rs new file mode 100644 index 00000000..f03a9dc4 --- /dev/null +++ b/app/src/ai/prompt_builder/prompts/anthropic.rs @@ -0,0 +1,160 @@ +//! Anthropic Claude-specific prompt templates. +//! +//! These are tuned for Claude's instruction-following style and capabilities. + +use crate::ai::prompt_builder::mode::Mode; + +pub const BASE_IDENTITY: &str = r#"You are Galaxy, an AI coding assistant embedded in a terminal application. You help users with software engineering tasks including writing code, debugging, explaining concepts, and navigating codebases. + +You are concise, direct, and to the point. Your output is displayed in a terminal — use GitHub-flavored markdown for formatting. Output text to communicate with the user; only use tools to complete tasks. + +If you cannot or will not help with something, say so briefly (1-2 sentences) and offer alternatives if possible. Do not add unnecessary preamble or postamble unless the user asks for detail."#; + +pub fn mode_instructions(mode: &Mode) -> String { + match mode { + Mode::Code => CODE_INSTRUCTIONS.to_string(), + Mode::Plan => PLAN_INSTRUCTIONS.to_string(), + Mode::Review => REVIEW_INSTRUCTIONS.to_string(), + Mode::Summarize => SUMMARIZE_INSTRUCTIONS.to_string(), + Mode::Title => TITLE_INSTRUCTIONS.to_string(), + } +} + +pub fn tool_usage_guidelines(mode: &Mode) -> String { + match mode { + Mode::Code => CODE_TOOL_GUIDELINES.to_string(), + Mode::Plan => PLAN_TOOL_GUIDELINES.to_string(), + Mode::Review => REVIEW_TOOL_GUIDELINES.to_string(), + Mode::Summarize => String::new(), + Mode::Title => String::new(), + } +} + +const CODE_INSTRUCTIONS: &str = r#"## Instructions + +### Doing Tasks +The user will primarily request software engineering tasks: solving bugs, adding features, refactoring, explaining code, and more. For these tasks: +1. Use search tools to understand the codebase and the user's query. Search extensively — both in parallel and sequentially. +2. Implement the solution using all tools available to you. +3. Verify the solution if possible with tests. NEVER assume a specific test framework — check the project first. +4. After making changes, run lint/typecheck/build commands if you know them, to ensure correctness. + +NEVER commit changes unless the user explicitly asks you to. + +### Following Conventions +When making changes, first understand the file's code conventions. Mimic code style, use existing libraries, and follow existing patterns. +- NEVER assume a library is available. Check the project's dependency files (package.json, Cargo.toml, requirements.txt, etc.) first. +- When creating new components, look at existing ones to understand conventions. +- When editing code, look at surrounding context (imports, patterns) to ensure idiomatic changes. +- Always follow security best practices. Never introduce code that exposes or logs secrets. + +### Proactiveness +Strike a balance between doing the right thing when asked (including follow-up actions) and not surprising the user with unrequested actions. If the user asks HOW to do something, explain first — don't immediately take action. + +Do not add code explanation summaries after making changes unless asked."#; + +const PLAN_INSTRUCTIONS: &str = r#"## Instructions + +CRITICAL: You are in READ-ONLY planning mode. You MUST NOT: +- Edit, create, or delete any files +- Run any mutating shell commands +- Make commits or change configuration + +You MAY: +- Read files +- Search the codebase (grep, glob) +- Run read-only shell commands (ls, cat, git log, git status) +- Ask the user clarifying questions + +### Planning Workflow +1. **Understand**: Explore the codebase to understand the current state and the user's goal. +2. **Research**: Look at relevant files, patterns, and dependencies. +3. **Design**: Produce a clear, concise implementation plan with: + - The approach and rationale + - Key files that need modification + - Potential risks or tradeoffs + - Verification strategy (how to test the changes) +4. **Clarify**: Ask the user questions about ambiguities or tradeoffs before finalizing. + +Present your plan in a structured, scannable format. Focus on the recommended approach — don't enumerate every alternative."#; + +const REVIEW_INSTRUCTIONS: &str = r#"## Instructions + +You are reviewing code changes. Your primary focus is identifying: +1. **Bugs and correctness issues** — logic errors, off-by-ones, race conditions, null handling +2. **Security risks** — exposed secrets, injection vulnerabilities, unsafe operations +3. **Behavioral regressions** — changes that break existing functionality +4. **Missing tests** — untested edge cases or new code paths + +Present findings ordered by severity with file/line references. Keep summaries brief — findings are the primary focus. + +Also note: +- Style issues (only if they meaningfully impact readability) +- Performance concerns (only if significant) +- Suggestions for better approaches + +If no issues are found, state that explicitly and mention any residual risks or testing gaps."#; + +const SUMMARIZE_INSTRUCTIONS: &str = r#"Summarize the conversation so far. Preserve: +- Key decisions made +- Code changes (files modified, what was changed and why) +- Important context (file paths, function names, architectural choices) +- Outstanding tasks or next steps +- Any errors encountered and how they were resolved + +Be concise but retain all information needed to continue the work without re-reading the full history."#; + +const TITLE_INSTRUCTIONS: &str = r#"Rules: +- Output ONLY a title, nothing else +- Under 50 characters +- Use the same language as the user's message +- Focus on what the user wants to accomplish +- Keep technical terms, filenames, and numbers exact +- Never use tools"#; + +const CODE_TOOL_GUIDELINES: &str = r#"## Tool Usage +You have been given every tool you need to complete your tasks. Use them to achieve results with as few calls and as little back-and-forth as possible. + +**How to choose tools:** +- For reading, writing, searching, and navigating files on the local filesystem, use your filesystem tools (`read_files`, `file_glob`, `grep`, `apply_file_diffs`). +- For running commands, installing packages, building, testing, and any shell operation, use `run_shell_command`. +- For tasks that require interacting with external services, web UIs, or capabilities not covered by your filesystem and shell tools, use your MCP tools. +- For complex multi-step tasks where a single script would replace many tool calls, write code (Python, Node, bash) via `run_shell_command` to reduce round-trips. But never use scripts for simple operations that a single command handles. + +**Critical rules:** +- Use ONLY the tools in your tool configuration. Never invent or guess tool names. +- ALWAYS pass `--no-pager` (or equivalent) flags to CLI tools like git, less, man, etc. Tools that lock stdin will freeze the session. +- Output text directly in your response instead of using `echo` — echo requires user approval and adds unnecessary friction. +- Use absolute paths based on the working directory shown above. +- Be logical in your tool choices. Read files before making claims about code. List files before assuming project structure. +- Be concise and direct. + +When making function calls using tools that accept array or object parameters ensure those are structured using JSON. For example: +```json +{"parameter": [{"color": "orange", "options": {"option_key_1": true}}]} +``` + +Answer the user's request using the relevant tool(s), if they are available. Check that all the required parameters for each tool call are provided or can reasonably be inferred from context. IF there are no relevant tools or there are missing values for required parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the user provides a specific value for a parameter (for example provided in quotes), make sure to use that value EXACTLY. DO NOT make up values for or ask about optional parameters. + +If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in the same block, otherwise you MUST wait for previous calls to finish first to determine the dependent values (do NOT use placeholders or guess missing parameters)."#; + +const PLAN_TOOL_GUIDELINES: &str = r#"## Tool Usage +You have access to read-only tools for exploring the codebase: +- `read_files` — Read file contents +- `grep` — Search for patterns in files +- `file_glob` — Find files by glob pattern +- `search_codebase` — Semantic code search +- `run_shell_command` — ONLY for read-only commands (ls, git log, git status, etc.) +- `ask_user_question` — Ask the user for clarification + +You MUST NOT use `apply_file_diffs`, `create_documents`, `edit_documents`, or any command that modifies files or state. Any attempt to edit is a critical violation of planning mode."#; + +const REVIEW_TOOL_GUIDELINES: &str = r#"## Tool Usage +You have access to tools for examining the code under review: +- `read_files` — Read file contents to understand context +- `grep` — Search for patterns to find related code +- `file_glob` — Find related files +- `search_codebase` — Semantic search for related implementations +- `run_shell_command` — For read-only commands (git diff, git log, etc.) + +Use these tools to gather context needed for a thorough review. You should read the files being changed and their surrounding context before providing feedback."#; \ No newline at end of file diff --git a/app/src/ai/prompt_builder/prompts/mod.rs b/app/src/ai/prompt_builder/prompts/mod.rs new file mode 100644 index 00000000..474976dd --- /dev/null +++ b/app/src/ai/prompt_builder/prompts/mod.rs @@ -0,0 +1,40 @@ +//! Prompt text templates organized by provider and mode. + +pub mod provider; + +mod anthropic; + +use crate::ai::prompt_builder::mode::Mode; +use provider::Provider; + +/// The base identity statement shared across all modes. +pub fn base_identity(provider: &Provider) -> &'static str { + match provider { + Provider::Anthropic => anthropic::BASE_IDENTITY, + } +} + +/// Mode-specific identity/role description. +pub fn mode_identity(mode: &Mode) -> &'static str { + match mode { + Mode::Code => "You are in coding mode. Your primary role is to help the user implement changes, debug issues, explore codebases, and complete software engineering tasks.", + Mode::Plan => "You are in planning mode. Your role is to analyze, research, and design an implementation approach. You must NOT make any edits or run any mutating commands — only read, search, and think.", + Mode::Review => "You are in code review mode. Your role is to review code changes and provide detailed feedback on correctness, style, potential bugs, and improvements.", + Mode::Summarize => "You are summarizing a conversation. Preserve key decisions, code changes, file paths, and important context. Be concise but retain all information needed to continue the work.", + Mode::Title => "Generate a brief conversation title. Output ONLY the title — no explanation, no quotes. Keep it under 50 characters.", + } +} + +/// Mode-specific behavioral instructions. +pub fn mode_instructions(mode: &Mode, provider: &Provider) -> String { + match provider { + Provider::Anthropic => anthropic::mode_instructions(mode), + } +} + +/// Tool usage guidelines tailored to mode and provider. +pub fn tool_usage_guidelines(mode: &Mode, provider: &Provider) -> String { + match provider { + Provider::Anthropic => anthropic::tool_usage_guidelines(mode), + } +} \ No newline at end of file diff --git a/app/src/ai/prompt_builder/prompts/provider.rs b/app/src/ai/prompt_builder/prompts/provider.rs new file mode 100644 index 00000000..f0077873 --- /dev/null +++ b/app/src/ai/prompt_builder/prompts/provider.rs @@ -0,0 +1,28 @@ +//! LLM provider definitions. +//! +//! Each provider may have different prompting strategies, tone preferences, +//! and tool schema requirements. New providers are added here as variants. + +/// The LLM provider being targeted. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Provider { + /// Anthropic Claude models (Sonnet, Opus, Haiku, etc.) + Anthropic, + // Future: + // Gemini, + // OpenAI, +} + +impl Provider { + pub fn label(&self) -> &'static str { + match self { + Provider::Anthropic => "anthropic", + } + } +} + +impl std::fmt::Display for Provider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.label()) + } +} \ No newline at end of file diff --git a/app/src/ai/prompt_builder/tests.rs b/app/src/ai/prompt_builder/tests.rs new file mode 100644 index 00000000..82cc9dda --- /dev/null +++ b/app/src/ai/prompt_builder/tests.rs @@ -0,0 +1,131 @@ +#[cfg(test)] +mod prompt_builder_tests { + use super::*; + + #[test] + fn test_code_mode_builds_successfully() { + let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic) + .with_context( + PromptContext::new() + .with_working_dir("/home/user/project") + .with_os("Linux") + .with_shell("zsh 5.9"), + ) + .build(); + + assert!(prompt.system_prompt.contains("Galaxy")); + assert!(prompt.system_prompt.contains("coding mode")); + assert!(prompt.system_prompt.contains("/home/user/project")); + assert!(!prompt.tools.is_empty()); + // Code mode should have apply_file_diffs + assert!(prompt.tools.iter().any(|t| t.name == "apply_file_diffs")); + } + + #[test] + fn test_plan_mode_has_no_edit_tools() { + let prompt = PromptBuilder::new(Mode::Plan, Provider::Anthropic).build(); + + assert!(prompt.system_prompt.contains("planning mode")); + assert!(prompt.system_prompt.contains("READ-ONLY")); + // Plan mode should NOT have apply_file_diffs + assert!(!prompt.tools.iter().any(|t| t.name == "apply_file_diffs")); + // But should have read_files + assert!(prompt.tools.iter().any(|t| t.name == "read_files")); + } + + #[test] + fn test_review_mode_is_read_only() { + let prompt = PromptBuilder::new(Mode::Review, Provider::Anthropic).build(); + + assert!(prompt.system_prompt.contains("code review mode")); + assert!(!prompt.tools.iter().any(|t| t.name == "apply_file_diffs")); + assert!(!prompt.tools.iter().any(|t| t.name == "create_documents")); + assert!(prompt.tools.iter().any(|t| t.name == "read_files")); + assert!(prompt.tools.iter().any(|t| t.name == "grep")); + } + + #[test] + fn test_summarize_mode_has_no_tools() { + let prompt = PromptBuilder::new(Mode::Summarize, Provider::Anthropic).build(); + + assert!(prompt.system_prompt.contains("summariz")); + assert!(prompt.tools.is_empty()); + } + + #[test] + fn test_title_mode_has_no_tools() { + let prompt = PromptBuilder::new(Mode::Title, Provider::Anthropic).build(); + + assert!(prompt.system_prompt.contains("title")); + assert!(prompt.tools.is_empty()); + } + + #[test] + fn test_project_rules_included() { + let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic) + .with_project_rules(vec![ProjectRule { + root_path: "/home/user/project".to_string(), + content: "Always use snake_case for function names.".to_string(), + }]) + .build(); + + assert!(prompt.system_prompt.contains("Project Rules")); + assert!(prompt.system_prompt.contains("snake_case")); + } + + #[test] + fn test_mcp_tools_appended() { + use crate::ai::bedrock::convert::ToolDefinition; + + let mcp_tool = ToolDefinition { + name: "mcp__github__create_pr".to_string(), + description: "Create a pull request".to_string(), + input_schema: serde_json::json!({"type": "object", "properties": {}}), + }; + + let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic) + .with_mcp_tools(vec![mcp_tool.clone()]) + .build(); + + assert!(prompt.tools.iter().any(|t| t.name == "mcp__github__create_pr")); + } + + #[test] + fn test_mode_allows_edits() { + assert!(Mode::Code.allows_edits()); + assert!(!Mode::Plan.allows_edits()); + assert!(!Mode::Review.allows_edits()); + assert!(!Mode::Summarize.allows_edits()); + assert!(!Mode::Title.allows_edits()); + } + + #[test] + fn test_mode_allows_shell() { + assert!(Mode::Code.allows_shell()); + assert!(!Mode::Plan.allows_shell()); + assert!(!Mode::Review.allows_shell()); + } + + #[test] + fn test_empty_context_omits_environment_section() { + let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic).build(); + + // With default (empty) context, no Environment section + assert!(!prompt.system_prompt.contains("## Environment")); + } + + #[test] + fn test_context_populates_environment_section() { + let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic) + .with_context( + PromptContext::new() + .with_working_dir("/projects/myapp") + .with_git_branch("feature/new-thing"), + ) + .build(); + + assert!(prompt.system_prompt.contains("## Environment")); + assert!(prompt.system_prompt.contains("/projects/myapp")); + assert!(prompt.system_prompt.contains("feature/new-thing")); + } +} \ No newline at end of file diff --git a/app/src/ai/prompt_builder/tools.rs b/app/src/ai/prompt_builder/tools.rs new file mode 100644 index 00000000..2bd1f7f4 --- /dev/null +++ b/app/src/ai/prompt_builder/tools.rs @@ -0,0 +1,331 @@ +//! Tool definitions filtered by mode. +//! +//! Each mode has a different set of tools available. Code mode gets everything, +//! Plan mode gets read-only tools, Review mode gets read + search, etc. + +use crate::ai::bedrock::convert::ToolDefinition; +use crate::ai::prompt_builder::mode::Mode; + +/// Returns the tool definitions available for the given mode. +pub fn tools_for_mode(mode: &Mode) -> Vec { + match mode { + Mode::Code => code_tools(), + Mode::Plan => plan_tools(), + Mode::Review => review_tools(), + Mode::Summarize => vec![], + Mode::Title => vec![], + } +} + +/// Full tool set for coding mode. +fn code_tools() -> Vec { + vec![ + run_shell_command(), + read_files(), + apply_file_diffs(), + grep(), + file_glob(), + search_codebase(), + write_to_long_running_shell_command(), + read_shell_command_output(), + read_mcp_resource(), + read_documents(), + create_documents(), + edit_documents(), + start_agent(), + send_message_to_agent(), + ask_user_question(), + read_skill(), + fetch_conversation(), + ] +} + +/// Read-only tools for planning mode. +fn plan_tools() -> Vec { + vec![ + read_files(), + grep(), + file_glob(), + search_codebase(), + run_shell_command_readonly(), + ask_user_question(), + start_agent(), + send_message_to_agent(), + read_skill(), + fetch_conversation(), + ] +} + +/// Tools for code review mode. +fn review_tools() -> Vec { + vec![ + read_files(), + grep(), + file_glob(), + search_codebase(), + run_shell_command_readonly(), + ] +} + +// ─── Tool Definitions ──────────────────────────────────────────────────────── + +fn run_shell_command() -> ToolDefinition { + ToolDefinition { + name: "run_shell_command".to_string(), + description: "Execute a shell command in the user's terminal and return its output. Use for running builds, tests, git operations, installing packages, or any shell operation. Commands run in the user's actual shell with their environment. Set is_read_only=true for read-only commands (ls, cat, git status) to enable auto-execution. Always use --no-pager for git commands.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "command": { "type": "string", "description": "The shell command to execute" }, + "is_read_only": { "type": "boolean", "description": "True if command only reads data and makes no changes" }, + "is_risky": { "type": "boolean", "description": "True if command is destructive or irreversible (rm -rf, git push --force)" } + }, + "required": ["command"] + }), + } +} + +fn run_shell_command_readonly() -> ToolDefinition { + ToolDefinition { + name: "run_shell_command".to_string(), + description: "Execute a READ-ONLY shell command and return its output. Only use for commands that inspect state (ls, cat, git log, git status, find, etc.). Do NOT use for commands that modify files or state. Always use --no-pager for git commands.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "command": { "type": "string", "description": "The read-only shell command to execute" }, + "is_read_only": { "type": "boolean", "description": "Must be true — only read-only commands are allowed in this mode" } + }, + "required": ["command"] + }), + } +} + +fn read_files() -> ToolDefinition { + ToolDefinition { + name: "read_files".to_string(), + description: "Read the contents of one or more files. Pass ALL file paths you need in a single call for efficiency. Returns file contents with path headers. Binary files are detected and skipped. Use absolute paths.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "files": { "type": "array", "items": { "type": "string" }, "description": "Absolute file paths to read" } + }, + "required": ["files"] + }), + } +} + +fn apply_file_diffs() -> ToolDefinition { + ToolDefinition { + name: "apply_file_diffs".to_string(), + description: "Apply search/replace edits to files. Creates files if they don't exist (use empty search string). The search string must uniquely match one location in the file. Include enough surrounding context for uniqueness. For new files, use search=\"\" and put full content in replace.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "summary": { "type": "string", "description": "A brief summary of what these edits accomplish (e.g. 'Add error handling to parse_config')" }, + "diffs": { "type": "array", "items": { "type": "object", "properties": { "file_path": { "type": "string", "description": "Absolute path to the file" }, "search": { "type": "string", "description": "Exact text to find (must match uniquely). Empty string to create a new file." }, "replace": { "type": "string", "description": "Text to replace with" } }, "required": ["file_path", "search", "replace"] }, "description": "Array of file edits to apply" } + }, + "required": ["summary", "diffs"] + }), + } +} + +fn grep() -> ToolDefinition { + ToolDefinition { + name: "grep".to_string(), + description: "Search for regex patterns in files. Uses git grep in git repos (respects .gitignore) or ripgrep otherwise. Returns file paths and matching line numbers. Use read_files afterward to see context around matches. Pass ALL patterns you need in one call.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "queries": { "type": "array", "items": { "type": "string" }, "description": "Regex patterns to search for" }, + "path": { "type": "string", "description": "Directory to scope the search to" } + }, + "required": ["queries"] + }), + } +} + +fn file_glob() -> ToolDefinition { + ToolDefinition { + name: "file_glob".to_string(), + description: "Find files matching glob patterns. Uses git ls-files in git repos. Returns absolute file paths of matches. Common patterns: '**/*.rs', 'src/**/*.ts', '**/Cargo.toml'. Pass ALL patterns in one call.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "patterns": { "type": "array", "items": { "type": "string" }, "description": "Glob patterns to match files" }, + "path": { "type": "string", "description": "Directory to search from" } + }, + "required": ["patterns"] + }), + } +} + +fn search_codebase() -> ToolDefinition { + ToolDefinition { + name: "search_codebase".to_string(), + description: "Semantic code search across the indexed codebase. Use for finding relevant code by meaning rather than exact text match. Better than grep for conceptual queries like 'authentication logic' or 'error handling for database connections'.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "query": { "type": "string", "description": "Natural language search query describing what you're looking for" }, + "path": { "type": "string", "description": "Optional directory path to narrow search scope" } + }, + "required": ["query"] + }), + } +} + +fn write_to_long_running_shell_command() -> ToolDefinition { + ToolDefinition { + name: "write_to_long_running_shell_command".to_string(), + description: "Send input (stdin) to a currently running shell command. Use this to interact with commands that are waiting for input, like interactive prompts, REPLs, or commands that accept piped input.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "input": { "type": "string", "description": "Text to send as stdin to the running command" } + }, + "required": ["input"] + }), + } +} + +fn read_shell_command_output() -> ToolDefinition { + ToolDefinition { + name: "read_shell_command_output".to_string(), + description: "Read the latest output from a previously started long-running shell command. Use to check progress or get results from commands that are still running.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": {}, + "required": [] + }), + } +} + +fn read_mcp_resource() -> ToolDefinition { + ToolDefinition { + name: "read_mcp_resource".to_string(), + description: "Read a resource from a connected MCP (Model Context Protocol) server. Resources provide context like database schemas, API docs, or live system state.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "server_id": { "type": "string", "description": "MCP server identifier" }, + "uri": { "type": "string", "description": "Resource URI to read" } + }, + "required": ["server_id", "uri"] + }), + } +} + +fn read_documents() -> ToolDefinition { + ToolDefinition { + name: "read_documents".to_string(), + description: "Read the contents of one or more Galaxy notebook documents by their IDs.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "document_ids": { "type": "array", "items": { "type": "string" }, "description": "Document IDs to read" } + }, + "required": ["document_ids"] + }), + } +} + +fn create_documents() -> ToolDefinition { + ToolDefinition { + name: "create_documents".to_string(), + description: "Create new Galaxy notebook documents with the specified title and content.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "documents": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "content": { "type": "string" } }, "required": ["title", "content"] }, "description": "Documents to create" } + }, + "required": ["documents"] + }), + } +} + +fn edit_documents() -> ToolDefinition { + ToolDefinition { + name: "edit_documents".to_string(), + description: "Edit existing Galaxy notebook documents using search/replace diffs.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "diffs": { "type": "array", "items": { "type": "object", "properties": { "document_id": { "type": "string" }, "search": { "type": "string" }, "replace": { "type": "string" } }, "required": ["document_id", "search", "replace"] }, "description": "Edits to apply to documents" } + }, + "required": ["diffs"] + }), + } +} + +fn start_agent() -> ToolDefinition { + ToolDefinition { + name: "start_agent".to_string(), + description: "Start a sub-agent to handle a specific task autonomously. Use for delegating independent work that can run in parallel. The agent gets its own conversation context and tool access.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "name": { "type": "string", "description": "Name for the sub-agent (used for identification)" }, + "prompt": { "type": "string", "description": "The task/instructions for the sub-agent to execute" } + }, + "required": ["name", "prompt"] + }), + } +} + +fn send_message_to_agent() -> ToolDefinition { + ToolDefinition { + name: "send_message_to_agent".to_string(), + description: "Send a message to a running sub-agent. Use to provide additional context, ask for updates, or redirect the agent's work.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "agent_id": { "type": "string", "description": "ID of the target sub-agent" }, + "message": { "type": "string", "description": "Message to send to the agent" } + }, + "required": ["agent_id", "message"] + }), + } +} + +fn ask_user_question() -> ToolDefinition { + ToolDefinition { + name: "ask_user_question".to_string(), + description: "Ask the user a question when you need clarification or a decision. Present clear options when possible. Use sparingly \u{2014} prefer making reasonable assumptions.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "question": { "type": "string", "description": "The question to ask the user" }, + "options": { "type": "array", "items": { "type": "string" }, "description": "Optional multiple-choice options to present" } + }, + "required": ["question"] + }), + } +} + +fn read_skill() -> ToolDefinition { + ToolDefinition { + name: "read_skill".to_string(), + description: "Read a skill definition to understand available capabilities and how to use them.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "skill": { "type": "string", "description": "Skill identifier to read" } + }, + "required": ["skill"] + }), + } +} + +fn fetch_conversation() -> ToolDefinition { + ToolDefinition { + name: "fetch_conversation".to_string(), + description: "Fetch the contents of a previous conversation for context. Use when the user references prior work or you need history from another session.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "conversation_id": { "type": "string", "description": "ID of the conversation to fetch" } + }, + "required": ["conversation_id"] + }), + } +} \ No newline at end of file diff --git a/script/install-galaxy.sh b/script/install-galaxy.sh index 2c8688d7..0a3be6c0 100755 --- a/script/install-galaxy.sh +++ b/script/install-galaxy.sh @@ -89,8 +89,11 @@ else fi # ---------- 5. Build ---------- -info "Building and bundling $APP_NAME (release)..." +info "Fetching dependencies..." pushd "$CLONE_DIR" > /dev/null +cargo fetch + +info "Building and bundling $APP_NAME (release)..." cargo bundle --release --bin "$BUNDLE_BIN" --package "$BUNDLE_PKG" popd > /dev/null From 54e4e8abf584dd45c4da5f83fc4cbd80f0b8ecdb Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Mon, 1 Jun 2026 15:21:03 -0500 Subject: [PATCH 2/2] Additional cleanup of caching --- app/src/ai/agent/api.rs | 4 +++ app/src/ai/agent/api/impl.rs | 1 + app/src/ai/agent/api/impl_tests.rs | 1 + app/src/ai/agent/conversation.rs | 15 ++++++++ app/src/ai/bedrock/client.rs | 2 ++ app/src/ai/bedrock/convert.rs | 46 ++++++++++++++++--------- app/src/ai/bedrock/convert_tests.rs | 28 ++++++++------- app/src/ai/bedrock/e2e_tests.rs | 16 +++++++++ app/src/ai/bedrock/integration_tests.rs | 2 ++ app/src/ai/bedrock/translator.rs | 6 +++- app/src/ai/blocklist/controller.rs | 36 +++++-------------- app/src/ai/prompt_builder/mod.rs | 2 +- 12 files changed, 100 insertions(+), 59 deletions(-) diff --git a/app/src/ai/agent/api.rs b/app/src/ai/agent/api.rs index b6062f2d..2d57a5dc 100644 --- a/app/src/ai/agent/api.rs +++ b/app/src/ai/agent/api.rs @@ -134,6 +134,9 @@ pub struct RequestParams { /// Full Bedrock conversation history for direct Bedrock calls. /// When present, the Bedrock path uses this instead of extracting from task_context. pub bedrock_message_history: Vec, + /// Compacted summary from a prior summarization pass. Injected into the system + /// prompt so it benefits from system-level caching. + pub bedrock_compact_summary: Option, /// Populated by the Bedrock path after building the message list. /// Contains the full messages sent (old history + new input) so the controller /// can store them back into the conversation for the next request cycle. @@ -328,6 +331,7 @@ impl RequestParams { parent_agent_id: None, agent_name: None, bedrock_message_history: Vec::new(), + bedrock_compact_summary: None, bedrock_messages_sent: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), is_summarization: false, } diff --git a/app/src/ai/agent/api/impl.rs b/app/src/ai/agent/api/impl.rs index 2d598f28..6e50210f 100644 --- a/app/src/ai/agent/api/impl.rs +++ b/app/src/ai/agent/api/impl.rs @@ -153,6 +153,7 @@ pub async fn generate_multi_agent_output( model_id, root_task_id: params.root_task_id.clone(), bedrock_message_history: params.bedrock_message_history.clone(), + bedrock_compact_summary: params.bedrock_compact_summary.clone(), bedrock_messages_sent: params.bedrock_messages_sent.clone(), is_summarization: params.is_summarization, }; diff --git a/app/src/ai/agent/api/impl_tests.rs b/app/src/ai/agent/api/impl_tests.rs index 14693ac7..70b3bc97 100644 --- a/app/src/ai/agent/api/impl_tests.rs +++ b/app/src/ai/agent/api/impl_tests.rs @@ -41,6 +41,7 @@ fn request_params_with_ask_user_question_enabled(ask_user_question_enabled: bool parent_agent_id: None, agent_name: None, bedrock_message_history: Vec::new(), + bedrock_compact_summary: None, bedrock_messages_sent: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), is_summarization: false, } diff --git a/app/src/ai/agent/conversation.rs b/app/src/ai/agent/conversation.rs index a2b5d839..b1bcb11f 100644 --- a/app/src/ai/agent/conversation.rs +++ b/app/src/ai/agent/conversation.rs @@ -241,6 +241,11 @@ pub struct AIConversation { /// context window size, NOT a cumulative total. current_context_tokens: u32, + /// When set, contains the compacted summary of prior conversation history. + /// Injected into the system prompt (not the messages array) so it benefits + /// from system-level caching at the 1-hour TTL. + compact_summary: Option, + /// Guards against repeated auto-compact triggers within the same high-usage window. /// Set to true when auto-compact fires; reset when summarization completes. has_pending_auto_compact: bool, @@ -298,6 +303,7 @@ impl AIConversation { is_remote_child: false, last_event_sequence: None, bedrock_message_history: Vec::new(), + compact_summary: None, current_context_tokens: 0, has_pending_auto_compact: false, subagent_retry_count: 0, @@ -498,6 +504,7 @@ impl AIConversation { is_remote_child: false, last_event_sequence, bedrock_message_history, + compact_summary: None, current_context_tokens: 0, has_pending_auto_compact: false, subagent_retry_count: 0, @@ -525,6 +532,14 @@ impl AIConversation { self.bedrock_message_history.extend(messages); } + pub fn compact_summary(&self) -> Option<&str> { + self.compact_summary.as_deref() + } + + pub fn set_compact_summary(&mut self, summary: Option) { + self.compact_summary = summary; + } + /// Assigns fresh exchange IDs to all exchanges in this conversation. /// Used when forking conversations to avoid ID collisions with persisted blocks. pub fn reassign_exchange_ids(&mut self) { diff --git a/app/src/ai/bedrock/client.rs b/app/src/ai/bedrock/client.rs index 9756a974..7d76bbee 100644 --- a/app/src/ai/bedrock/client.rs +++ b/app/src/ai/bedrock/client.rs @@ -133,6 +133,7 @@ impl BedrockClient { needs_create_task: bool, messages: Vec, system_prompt: Option, + compact_summary: Option, tools: Vec, max_tokens: i32, temperature: Option, @@ -158,6 +159,7 @@ impl BedrockClient { let converted = build_converse_request( messages.clone(), system_prompt.clone(), + compact_summary, tools.clone(), max_tokens, temperature, diff --git a/app/src/ai/bedrock/convert.rs b/app/src/ai/bedrock/convert.rs index fa9ecbd3..e57d4766 100644 --- a/app/src/ai/bedrock/convert.rs +++ b/app/src/ai/bedrock/convert.rs @@ -59,7 +59,7 @@ pub enum ContentPart { }, } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct ToolDefinition { pub name: String, pub description: String, @@ -69,6 +69,7 @@ pub struct ToolDefinition { pub fn build_converse_request( messages: Vec, system_prompt: Option, + compact_summary: Option, tools: Vec, max_tokens: i32, temperature: Option, @@ -76,7 +77,7 @@ pub fn build_converse_request( stop_sequences: Option>, ) -> ConvertedRequest { let bedrock_messages = convert_messages(messages); - let system = convert_system_prompt(system_prompt); + let system = convert_system_prompt(system_prompt, compact_summary); let inference_config = build_inference_config(max_tokens, temperature, top_p, stop_sequences); let tool_config = build_tool_config(tools); @@ -267,22 +268,35 @@ fn coalesce_consecutive_roles(messages: Vec) -> Vec) -> Vec { - match system_prompt { - Some(prompt) if !prompt.is_empty() => { - vec![ - SystemContentBlock::Text(prompt), - SystemContentBlock::CachePoint( - CachePointBlock::builder() - .r#type(CachePointType::Default) - .ttl(CacheTtl::OneHour) - .build() - .expect("valid cache point"), - ), - ] +fn convert_system_prompt( + system_prompt: Option, + compact_summary: Option, +) -> Vec { + let mut blocks = Vec::new(); + + if let Some(prompt) = system_prompt { + if !prompt.is_empty() { + blocks.push(SystemContentBlock::Text(prompt)); } - _ => vec![], } + + if let Some(summary) = compact_summary { + blocks.push(SystemContentBlock::Text(format!( + "\n{summary}\n" + ))); + } + + if !blocks.is_empty() { + blocks.push(SystemContentBlock::CachePoint( + CachePointBlock::builder() + .r#type(CachePointType::Default) + .ttl(CacheTtl::OneHour) + .build() + .expect("valid cache point"), + )); + } + + blocks } fn build_inference_config( diff --git a/app/src/ai/bedrock/convert_tests.rs b/app/src/ai/bedrock/convert_tests.rs index 26389a80..efb1ff6b 100644 --- a/app/src/ai/bedrock/convert_tests.rs +++ b/app/src/ai/bedrock/convert_tests.rs @@ -10,7 +10,7 @@ fn test_text_message_converts_to_single_block() { content: MessageContent::Text("Hello".to_string()), }]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, None, vec![], 4096, None, None, None); assert_eq!(result.messages.len(), 1); assert_eq!(result.messages[0].role(), &ConversationRole::User); @@ -29,7 +29,7 @@ fn test_tool_use_produces_valid_json_input() { }, }]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, None, vec![], 4096, None, None, None); assert_eq!(result.messages.len(), 1); assert_eq!(result.messages[0].role(), &ConversationRole::Assistant); @@ -53,7 +53,7 @@ fn test_tool_result_with_matching_id() { }, }]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, None, vec![], 4096, None, None, None); assert_eq!(result.messages.len(), 1); match &result.messages[0].content()[0] { @@ -75,7 +75,7 @@ fn test_tool_result_error_status() { }, }]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, None, vec![], 4096, None, None, None); match &result.messages[0].content()[0] { ContentBlock::ToolResult(block) => { @@ -101,7 +101,7 @@ fn test_consecutive_same_role_messages_coalesced() { }, ]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, None, vec![], 4096, None, None, None); assert_eq!(result.messages.len(), 1); assert_eq!(result.messages[0].content().len(), 2); @@ -126,7 +126,7 @@ fn test_alternating_roles_not_coalesced() { }, ]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, None, vec![], 4096, None, None, None); assert_eq!(result.messages.len(), 3); assert_eq!(result.messages[0].role(), &ConversationRole::User); @@ -144,6 +144,7 @@ fn test_system_prompt_separated_from_messages() { let result = build_converse_request( messages, Some("You are a helpful assistant.".to_string()), + None, vec![], 4096, None, @@ -159,16 +160,16 @@ fn test_system_prompt_separated_from_messages() { #[test] fn test_empty_system_prompt_produces_empty_vec() { let result = - build_converse_request(vec![], Some("".to_string()), vec![], 4096, None, None, None); + build_converse_request(vec![], Some("".to_string()), None, vec![], 4096, None, None, None); assert!(result.system.is_empty()); - let result2 = build_converse_request(vec![], None, vec![], 4096, None, None, None); + let result2 = build_converse_request(vec![], None, None, vec![], 4096, None, None, None); assert!(result2.system.is_empty()); } #[test] fn test_empty_tools_produce_none_config() { - let result = build_converse_request(vec![], None, vec![], 4096, None, None, None); + let result = build_converse_request(vec![], None, None, vec![], 4096, None, None, None); assert!(result.tool_config.is_none()); } @@ -186,7 +187,7 @@ fn test_tool_definitions_produce_tool_config() { }), }]; - let result = build_converse_request(vec![], None, tools, 4096, None, None, None); + let result = build_converse_request(vec![], None, None, tools, 4096, None, None, None); assert!(result.tool_config.is_some()); let config = result.tool_config.unwrap(); @@ -195,7 +196,7 @@ fn test_tool_definitions_produce_tool_config() { #[test] fn test_inference_config_max_tokens_only() { - let result = build_converse_request(vec![], None, vec![], 8192, None, None, None); + let result = build_converse_request(vec![], None, None, vec![], 8192, None, None, None); assert_eq!(result.inference_config.max_tokens(), Some(8192)); assert_eq!(result.inference_config.temperature(), None); assert_eq!(result.inference_config.top_p(), None); @@ -207,6 +208,7 @@ fn test_inference_config_all_params() { let result = build_converse_request( vec![], None, + None, vec![], 4096, Some(0.7), @@ -233,7 +235,7 @@ fn test_multipart_content_produces_multiple_blocks() { ]), }]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, None, vec![], 4096, None, None, None); assert_eq!(result.messages[0].content().len(), 2); assert!(matches!( @@ -271,7 +273,7 @@ fn test_tool_result_after_tool_use_coalesced_into_user_message() { }, ]; - let result = build_converse_request(messages, None, vec![], 4096, None, None, None); + let result = build_converse_request(messages, None, None, vec![], 4096, None, None, None); assert_eq!(result.messages.len(), 3); assert_eq!(result.messages[0].role(), &ConversationRole::User); diff --git a/app/src/ai/bedrock/e2e_tests.rs b/app/src/ai/bedrock/e2e_tests.rs index edbd150d..880fd3de 100644 --- a/app/src/ai/bedrock/e2e_tests.rs +++ b/app/src/ai/bedrock/e2e_tests.rs @@ -550,6 +550,7 @@ impl AgentSimulation { needs_create_task, self.conversation.clone(), Some(self.system_prompt.clone()), + None, self.tools.clone(), 8192, None, @@ -557,6 +558,7 @@ impl AgentSimulation { None, None, Arc::new(Mutex::new(Vec::new())), + false, ) .await .expect("converse_stream should succeed"); @@ -1137,6 +1139,7 @@ async fn test_reasoning_model_produces_substantial_output() { true, messages, system, + None, agent_tools(), 8192, None, @@ -1144,6 +1147,7 @@ async fn test_reasoning_model_produces_substantial_output() { None, None, Arc::new(Mutex::new(Vec::new())), + false, ) .await .expect("converse_stream should succeed"); @@ -1260,6 +1264,7 @@ async fn test_event_sequence_matches_controller_expectations() { true, messages, None, + None, vec![], 100, None, @@ -1267,6 +1272,7 @@ async fn test_event_sequence_matches_controller_expectations() { None, None, Arc::new(Mutex::new(Vec::new())), + false, ) .await .expect("should connect"); @@ -1375,6 +1381,7 @@ async fn test_followup_turn_does_not_send_create_task() { false, messages, None, + None, vec![], 100, None, @@ -1382,6 +1389,7 @@ async fn test_followup_turn_does_not_send_create_task() { None, None, Arc::new(Mutex::new(Vec::new())), + false, ) .await .expect("should connect"); @@ -1459,6 +1467,7 @@ async fn run_slash_command_test( true, messages, system_prompt, + None, tools, 4096, None, @@ -1466,6 +1475,7 @@ async fn run_slash_command_test( None, None, Arc::new(Mutex::new(Vec::new())), + false, ) .await .expect("stream should connect"); @@ -1687,6 +1697,7 @@ async fn test_slash_resume_conversation() { true, messages, None, + None, vec![], 256, None, @@ -1694,6 +1705,7 @@ async fn test_slash_resume_conversation() { None, None, Arc::new(Mutex::new(Vec::new())), + false, ) .await .expect("stream should connect"); @@ -1799,6 +1811,7 @@ async fn test_empty_messages_safety_check() { true, messages, None, + None, vec![], 100, None, @@ -1806,6 +1819,7 @@ async fn test_empty_messages_safety_check() { None, None, Arc::new(Mutex::new(Vec::new())), + false, ) .await .expect("safety fallback message should work"); @@ -1960,6 +1974,7 @@ async fn test_full_proto_round_trip_with_tool_history() { false, messages, system_prompt, + None, tools, 1024, None, @@ -1967,6 +1982,7 @@ async fn test_full_proto_round_trip_with_tool_history() { None, None, Arc::new(Mutex::new(Vec::new())), + false, ) .await; diff --git a/app/src/ai/bedrock/integration_tests.rs b/app/src/ai/bedrock/integration_tests.rs index da0cbe77..486df1ec 100644 --- a/app/src/ai/bedrock/integration_tests.rs +++ b/app/src/ai/bedrock/integration_tests.rs @@ -59,6 +59,7 @@ async fn collect_stream_output( true, messages, system_prompt, + None, tools, 8192, None, @@ -66,6 +67,7 @@ async fn collect_stream_output( None, None, Arc::new(Mutex::new(Vec::new())), + false, ) .await .expect("converse_stream should succeed"); diff --git a/app/src/ai/bedrock/translator.rs b/app/src/ai/bedrock/translator.rs index f3b729fb..331f2366 100644 --- a/app/src/ai/bedrock/translator.rs +++ b/app/src/ai/bedrock/translator.rs @@ -13,6 +13,7 @@ pub struct TranslatorRequest { pub model_id: String, pub root_task_id: Option, pub bedrock_message_history: Vec, + pub bedrock_compact_summary: Option, pub bedrock_messages_sent: Arc>>, pub is_summarization: bool, } @@ -71,12 +72,14 @@ pub async fn execute( request_translator::sanitize_messages_for_bedrock(&mut messages); let system_prompt = request_translator::extract_system_prompt(request); + let compact_summary = params.bedrock_compact_summary; let tools = request_translator::extract_tools(request); log::info!( - "[bedrock] Sending {} messages, system_prompt={}, tools={}", + "[bedrock] Sending {} messages, system_prompt={}, compact_summary={}, tools={}", messages.len(), system_prompt.is_some(), + compact_summary.is_some(), tools.len() ); @@ -94,6 +97,7 @@ pub async fn execute( needs_create_task, messages.clone(), system_prompt, + compact_summary, tools, 64000, None, diff --git a/app/src/ai/blocklist/controller.rs b/app/src/ai/blocklist/controller.rs index 655c7835..13ec52d5 100644 --- a/app/src/ai/blocklist/controller.rs +++ b/app/src/ai/blocklist/controller.rs @@ -1963,6 +1963,7 @@ impl BlocklistAIController { parent_agent_id, agent_name, bedrock_history, + bedrock_compact_summary, ) = { let Some(conversation) = history_model .as_ref(ctx) @@ -1986,6 +1987,7 @@ impl BlocklistAIController { conversation.parent_agent_id().map(str::to_string), conversation.agent_name().map(str::to_string), conversation.bedrock_message_history().to_vec(), + conversation.compact_summary().map(str::to_string), ) }; @@ -2053,6 +2055,7 @@ impl BlocklistAIController { request_params.parent_agent_id = parent_agent_id; request_params.agent_name = agent_name; request_params.bedrock_message_history = bedrock_history; + request_params.bedrock_compact_summary = bedrock_compact_summary; request_params.is_summarization = request_input .all_inputs() .any(|input| matches!(input, AIAgentInput::SummarizeConversation { .. })); @@ -2415,37 +2418,14 @@ impl BlocklistAIController { }); if let Some(summary) = summary_text { - use crate::ai::bedrock::convert::{ - ConversationMessage, MessageContent, - MessageRole, - }; - let assistant_reply = "Understood. I have the context from our previous conversation. How can I help you next?"; - let user_msg = format!( - "Here is a summary of our conversation so far:\n\n{summary}" - ); - let compacted = vec![ - ConversationMessage { - role: MessageRole::User, - content: MessageContent::Text(user_msg.clone()), - }, - ConversationMessage { - role: MessageRole::Assistant, - content: MessageContent::Text( - assistant_reply.to_string() - ), - }, - ]; log::info!( - "[bedrock] Compacted conversation history from {} messages to {} (summary)", - new_history.len(), - compacted.len() + "[bedrock] Compacted conversation history from {} messages to system-level summary", + new_history.len() ); - *conversation.bedrock_message_history_mut() = - compacted; + conversation.set_compact_summary(Some(summary.clone())); + *conversation.bedrock_message_history_mut() = Vec::new(); - // Estimate new context size from the compacted content. - // ~4 chars per token is a reasonable approximation. - let estimated_tokens = ((user_msg.len() + assistant_reply.len()) / 4) as u32; + let estimated_tokens = (summary.len() / 4) as u32; let max_context = crate::ai::bedrock::response_translator::context_window_for_model("claude-opus-4-6-20250514[1m]"); let new_usage = estimated_tokens as f32 / max_context as f32; conversation.set_context_window_usage(new_usage); diff --git a/app/src/ai/prompt_builder/mod.rs b/app/src/ai/prompt_builder/mod.rs index 01108fe5..f97219b7 100644 --- a/app/src/ai/prompt_builder/mod.rs +++ b/app/src/ai/prompt_builder/mod.rs @@ -23,7 +23,7 @@ mod tools; pub use context::PromptContext; pub use mode::Mode; pub use prompts::provider::Provider; -pub use tools::ToolSet; +pub use tools::tools_for_mode; use crate::ai::bedrock::convert::ToolDefinition;