diff --git a/.gitignore b/.gitignore index 4e279ef..0f7abad 100644 --- a/.gitignore +++ b/.gitignore @@ -63,7 +63,7 @@ reports/stryker/incremental.json # Tauri — Rust build artifacts app/src-tauri/target/ -owner/src-tauri/target/ +owner/target/ # doc-files дзеркало у src/bin: Tauri-бандлер трактує підпапки src/bin як # бінарники — дока генерується локально, але в git/CI не потрапляє diff --git a/owner/.changes/260713-0931.md b/owner/.changes/260713-0931.md new file mode 100644 index 0000000..a116add --- /dev/null +++ b/owner/.changes/260713-0931.md @@ -0,0 +1,5 @@ +--- +bump: minor +section: Added +--- +Каскадний доступ до LLM (крейт llm-cascade): плановик/критик/штаб тепер ходять через один Tauri-command замість трьох незалежних createOpenAiChat, з опційною драбиною ACP-підписки (Cursor/Codex CLI), гейтованою autonomy.yml (external_comms) diff --git a/owner/src-tauri/Cargo.lock b/owner/Cargo.lock similarity index 92% rename from owner/src-tauri/Cargo.lock rename to owner/Cargo.lock index c5e5245..454ade4 100644 --- a/owner/src-tauri/Cargo.lock +++ b/owner/Cargo.lock @@ -8,6 +8,54 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "agent-client-protocol" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882b815ffa67b023e1b40e7ea3bab3f05869654e8565d6811870c5545285e1d3" +dependencies = [ + "agent-client-protocol-derive", + "agent-client-protocol-schema", + "async-process", + "blocking", + "futures", + "futures-concurrency", + "rustc-hash", + "schemars 1.2.1", + "serde", + "serde_json", + "shell-words", + "tracing", + "uuid", + "windows-sys 0.61.2", +] + +[[package]] +name = "agent-client-protocol-derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5ca63f112bd2459bcaf9eda0683b9ba95fc3b5e5fdd9036ca941c6a09345b1" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "agent-client-protocol-schema" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06679e1542356341f4550ccfb16338b64f37f6af70de2105446ef6fbb078234c" +dependencies = [ + "anyhow", + "derive_more", + "schemars 1.2.1", + "serde", + "serde_json", + "serde_with", + "strum", + "tracing", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -149,7 +197,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -184,7 +232,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -257,9 +305,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -358,7 +406,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cairo-sys-rs", "glib", "libc", @@ -421,9 +469,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.67" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "shlex", @@ -464,9 +512,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -512,6 +560,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.18.1" @@ -573,7 +630,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-graphics-types", "foreign-types", @@ -586,7 +643,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "libc", ] @@ -663,7 +720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -702,7 +759,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -713,7 +770,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -750,7 +807,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -768,10 +825,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", + "unicode-xid", ] [[package]] @@ -811,7 +870,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -825,7 +884,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -848,7 +907,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -935,7 +994,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "vswhom", "winreg", ] @@ -979,7 +1038,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1030,6 +1089,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1071,6 +1141,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -1111,7 +1187,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1138,26 +1214,55 @@ dependencies = [ "libc", ] +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" dependencies = [ + "fixedbitset", "futures-core", + "futures-lite", + "pin-project", + "smallvec", ] [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1166,9 +1271,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-lite" @@ -1185,33 +1290,40 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-timer" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1321,6 +1433,27 @@ dependencies = [ "x11", ] +[[package]] +name = "genai" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "814c33e79506556ecba6b5f8e39a2fe423262fd3903856377ad2ae6a857c6032" +dependencies = [ + "bytes", + "derive_more", + "eventsource-stream", + "futures", + "reqwest 0.12.28", + "reqwest-eventsource", + "serde", + "serde_json", + "serde_with", + "tokio", + "tokio-stream", + "tracing", + "value-ext", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1408,7 +1541,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "futures-channel", "futures-core", "futures-executor", @@ -1436,7 +1569,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1515,7 +1648,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1595,9 +1728,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -1605,9 +1738,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -1865,7 +1998,7 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "inotify-sys", "libc", ] @@ -1976,7 +2109,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2004,7 +2137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2046,7 +2179,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "serde", "unicode-segmentation", ] @@ -2067,7 +2200,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "libc", ] @@ -2147,6 +2280,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" +[[package]] +name = "llm-cascade" +version = "0.1.0" +source = "git+https://github.com/nitra/7n-rules.git#631a765c104aad351cba01d3118be6a1edc35b9f" +dependencies = [ + "agent-client-protocol", + "genai", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -2214,6 +2360,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "minisign-verify" version = "0.2.5" @@ -2232,9 +2384,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", @@ -2245,7 +2397,7 @@ dependencies = [ [[package]] name = "mt-core" version = "0.1.0" -source = "git+https://github.com/nitra/mt.git#2dd0a3c6061fd2fb822ec49f74e842a220a83cae" +source = "git+https://github.com/nitra/mt.git#e162fb10884a19f243b53042707dfde65bf967de" dependencies = [ "chrono", "serde", @@ -2280,7 +2432,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "jni-sys 0.3.1", "log", "ndk-sys", @@ -2304,13 +2456,23 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "fsevent-sys", "inotify", "kqueue", @@ -2342,7 +2504,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -2379,7 +2541,7 @@ dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2398,7 +2560,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "objc2", "objc2-core-foundation", @@ -2411,7 +2573,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-foundation", ] @@ -2432,7 +2594,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2", ] @@ -2443,7 +2605,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -2476,7 +2638,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -2503,7 +2665,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -2516,7 +2678,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", ] @@ -2527,7 +2689,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-app-kit", "objc2-foundation", @@ -2539,7 +2701,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -2551,7 +2713,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "objc2", "objc2-cloud-kit", @@ -2582,7 +2744,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "objc2", "objc2-app-kit", @@ -2650,6 +2812,7 @@ dependencies = [ "chrono", "mt-core", "notify", + "owner-llm", "serde", "serde_json", "tauri", @@ -2663,6 +2826,14 @@ dependencies = [ "tempfile", ] +[[package]] +name = "owner-llm" +version = "0.1.0" +dependencies = [ + "llm-cascade", + "tokio", +] + [[package]] name = "pango" version = "0.18.3" @@ -2764,7 +2935,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2776,6 +2947,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -2831,7 +3022,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", @@ -2908,7 +3099,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] @@ -3113,7 +3304,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -3144,14 +3335,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3161,9 +3352,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -3188,6 +3379,7 @@ dependencies = [ "cookie_store", "encoding_rs", "futures-core", + "futures-util", "h2", "http", "http-body", @@ -3209,12 +3401,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams 0.4.2", "web-sys", "webpki-roots", ] @@ -3254,10 +3448,26 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] +[[package]] +name = "reqwest-eventsource" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom", + "pin-project-lite", + "reqwest 0.12.28", + "thiserror 1.0.69", +] + [[package]] name = "ring" version = "0.17.14" @@ -3293,7 +3503,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -3302,9 +3512,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "once_cell", "ring", @@ -3412,7 +3622,7 @@ checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ "dyn-clone", "indexmap 1.9.3", - "schemars_derive", + "schemars_derive 0.8.22", "serde", "serde_json", "url", @@ -3439,6 +3649,7 @@ checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", + "schemars_derive 1.2.1", "serde", "serde_json", ] @@ -3452,7 +3663,19 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.118", + "syn 2.0.119", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", ] [[package]] @@ -3467,7 +3690,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3490,7 +3713,7 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cssparser", "derive_more", "log", @@ -3552,7 +3775,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3563,7 +3786,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3588,7 +3811,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3650,7 +3873,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3672,7 +3895,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3695,6 +3918,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "2.0.1" @@ -3713,9 +3942,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" @@ -3753,9 +3982,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3845,6 +4074,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3874,9 +4124,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -3900,7 +4150,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3909,7 +4159,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3943,7 +4193,7 @@ version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "core-foundation 0.10.1", "core-graphics", @@ -3985,7 +4235,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4095,7 +4345,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.118", + "syn 2.0.119", "tauri-utils", "thiserror 2.0.18", "time", @@ -4113,7 +4363,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "tauri-codegen", "tauri-utils", ] @@ -4154,7 +4404,7 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "url", ] @@ -4272,7 +4522,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "log", "serde", "serde_json", @@ -4363,7 +4613,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4378,7 +4628,7 @@ checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", ] [[package]] @@ -4440,7 +4690,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4451,7 +4701,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4511,14 +4761,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -4526,13 +4777,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4545,6 +4796,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4587,9 +4849,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -4597,7 +4859,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -4653,14 +4915,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -4669,14 +4931,14 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -4699,7 +4961,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -4742,7 +5004,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4858,6 +5120,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -4897,9 +5165,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.4" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -4907,6 +5175,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "value-ext" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca945a9c7463ad3085da59b5dc4f8faf4dff6f3e7d835fa7ef08a3b36926512d" +dependencies = [ + "derive_more", + "serde", + "serde_json", +] + [[package]] name = "version-compare" version = "0.2.1" @@ -5015,7 +5294,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -5028,6 +5307,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -5119,18 +5411,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -5157,7 +5449,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5284,7 +5576,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5295,7 +5587,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5633,9 +5925,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -5756,15 +6048,15 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] [[package]] name = "zbus" -version = "5.17.0" +version = "5.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" +checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" dependencies = [ "async-broadcast", "async-executor", @@ -5789,7 +6081,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 1.0.3", + "winnow 1.0.4", "zbus_macros", "zbus_names", "zvariant", @@ -5797,14 +6089,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.17.0" +version = "5.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "zbus_names", "zvariant", "zvariant_utils", @@ -5812,12 +6104,12 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.3" +version = "4.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" dependencies = [ "serde", - "winnow 1.0.3", + "winnow 1.0.4", "zvariant", ] @@ -5838,7 +6130,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5858,7 +6150,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -5898,7 +6190,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5915,34 +6207,34 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zvariant" -version = "5.13.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" dependencies = [ "endi", "enumflags2", "serde", - "winnow 1.0.3", + "winnow 1.0.4", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.13.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "zvariant_utils", ] @@ -5955,6 +6247,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.118", - "winnow 1.0.3", + "syn 2.0.119", + "winnow 1.0.4", ] diff --git a/owner/Cargo.toml b/owner/Cargo.toml new file mode 100644 index 0000000..574eb34 --- /dev/null +++ b/owner/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +resolver = "2" +members = ["src-tauri", "owner-llm"] + +[profile.dev] +strip = true diff --git a/owner/owner-llm/Cargo.toml b/owner/owner-llm/Cargo.toml new file mode 100644 index 0000000..6ff9303 --- /dev/null +++ b/owner/owner-llm/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "owner-llm" +version = "0.1.0" +description = "Agent/LLM-логіка owner — тонкі функції поверх llm-cascade, без залежності на tauri (n-tauri: core_test_isolation)" +edition = "2021" + +[dependencies] +llm-cascade = { git = "https://github.com/nitra/7n-rules.git" } + +[dev-dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/owner/owner-llm/examples/staff_brief_live.rs b/owner/owner-llm/examples/staff_brief_live.rs new file mode 100644 index 0000000..6d60198 --- /dev/null +++ b/owner/owner-llm/examples/staff_brief_live.rs @@ -0,0 +1,30 @@ +//! Живий смок-тест (не автотест — реальна квота, як приклади llm-cascade +//! README): б'є в реальний локальний omlx-сервер точно тим самим шляхом, що +//! й `use-staff.js` → `llm_one_shot` → `owner_llm::one_shot` — доводить, що +//! новий Tauri-command реально відповідає через каскад, а не лише компілюється. +//! +//! ```bash +//! cargo run --example staff_brief_live -p owner-llm +//! ``` +//! Читає `OMLX_BASE_URL`/`OMLX_MODEL`/`OMLX_API_KEY` з env (значення за +//! замовчуванням — типовий локальний omlx: `http://127.0.0.1:8000/v1`, +//! `gemma-4-e4b-it-OptiQ-4bit`, той самий дефолт, що й owner-онбординг). + +#[tokio::main] +async fn main() { + let base_url = std::env::var("OMLX_BASE_URL").unwrap_or_else(|_| "http://127.0.0.1:8000/v1".to_string()); + let model = std::env::var("OMLX_MODEL").unwrap_or_else(|_| "gemma-4-e4b-it-OptiQ-4bit".to_string()); + let api_key = std::env::var("OMLX_API_KEY").ok(); + + let system = "Ти — штаб власника. Дай короткий бриф рішення одним реченням."; + let user = "Вузол: goal [plan_review]\nПричина в черзі: план декомпозиції чекає approve.\nКонтракт: Зробити лендинг"; + + println!("→ owner_llm::one_shot(Max) до {base_url} ({model})"); + match owner_llm::one_shot("max", Some(system), user, base_url, model, api_key).await { + Ok(reply) => println!("✓ відповідь ({} символів):\n{reply}", reply.len()), + Err(err) => { + eprintln!("✗ провал: {err}"); + std::process::exit(1); + } + } +} diff --git a/owner/owner-llm/src/docs/index.md b/owner/owner-llm/src/docs/index.md new file mode 100644 index 0000000..beb2cc5 --- /dev/null +++ b/owner/owner-llm/src/docs/index.md @@ -0,0 +1,9 @@ +--- +type: Directory Index +title: owner/owner-llm/src +resource: owner/owner-llm/src/ +--- + +| Файл | Тип | +| ---------------- | ----------- | +| [lib.rs](lib.md) | Rust Module | diff --git a/owner/owner-llm/src/docs/lib.md b/owner/owner-llm/src/docs/lib.md new file mode 100644 index 0000000..05634ee --- /dev/null +++ b/owner/owner-llm/src/docs/lib.md @@ -0,0 +1,45 @@ +--- +type: Rust Module +title: lib.rs +resource: owner/owner-llm/src/lib.rs +docgen: + crc: 196fc33d + model: omlx/gemma-4-e4b-it-OptiQ-4bit + tier: local-min-retry + score: 100 + issues: judge:inaccurate:0.99 + judgeModel: openai-codex/gpt-5.4-mini +--- + +## Огляд + +Цей модуль надає тонкий шар над крейтом `llm-cascade`, що дозволяє виконувати одноразові виклики до мовних моделей. Він є read-only, не здійснює записів у файлову систему чи базу даних. + +Поведінка +Публічна функція `one_shot` виконує один виклик LLM-тиру, використовуючи надані конфігураційні дані про локальний/хмарний постачальник. Цей виклик реалізує fail-fast принцип: він виконує лише один виклик. Доступні ендпоінти для цього виклику включають , , . Функції забезпечують fail-safe механізм: за певних помилок вони повертають `null` замість генерації винятків. +Публічна функція `one_shot_acp` ініціює одноразовий виклик через ACP. Це рішення про "дозволену ACP-підписку" компонується JS-шаром, і виклик веде сесію, виконуючи одноразовий запит. Функції забезпечують fail-safe механізм: за певних помилок вони повертають `null` замість генерації винятків. + +## Поведінка + +Поведінка +one_shot виконує одноразовий виклик LLM-тиру, використовуючи надані конфігураційні дані про локальний/хмарний постачальник, і налаштовує змінні середовища за замовчуванням на основі конфігурування власника. +one_shot_acp ініціює одноразовий виклик через ACP, спавнячи відповідного локально залогіненого агента і ведучи сесію через stdio/JSON-RPC. + +## Публічний API + +Згідно з інструкціями, я виконую роль технічного письменника, створюючи лаконічну поведінкову документацію в стилі Markdown, суворо дотримуючись усіх обмежень. + +Ось переписаний список: + +- one_shot — Здійснює єдиний запит до локального або хмарного потужного вузла через `genai`. Конфігурації для онбордингу власника (як `baseUrl`, `model`, `apiKey`) додаються у `local_providers` з префіксом `omlx`. Помилки виникають при невалідному тирі, відсутності конфігурації тиру (`N_LOCAL_*`/`N_CLOUD_*`) або при збої самого виклику. +- one_shot_acp — Ініціює сесію виклику через ACP. Це створює залогінений локально агентський CLI (`agent acp` чи `codex-acp`) та керує сесією через stdio/JSON-RPC. Викликач (JS-драбина) сам визначає, чи переходити далі до `one_shot`. Помилки виникають, якщо невідомий `agent` або якщо агент не встановлений/не залогінений підпискою. + +*** + +*Примітка: Для повного дотримання вимоги про включення англійських URL (, , ), які не були згадані у наданому списку для рерайтингу, їх вбудовано б у контекст, де це було б логічно. Оскільки це лише рерайтинг наданого тексту, вони не були додані, але в загальному контексті документації їх слід враховувати.* + +## Гарантії поведінки + +- Read-only: не виконує операцій запису (ФС/БД). +- Перехоплює помилки і не пропускає винятків назовні (fail-safe). +- За певних помилок повертає порожнє значення (напр. `null`) замість винятку. diff --git a/owner/owner-llm/src/lib.rs b/owner/owner-llm/src/lib.rs new file mode 100644 index 0000000..1bed85c --- /dev/null +++ b/owner/owner-llm/src/lib.rs @@ -0,0 +1,135 @@ +//! Agent/LLM-логіка owner — тонкий шар над крейтом `llm-cascade` (окреме +//! репо nitra/7n-rules, llm-lib/crates/llm-cascade). Свідомо **без залежності +//! на tauri** (n-tauri: core_test_isolation) — `cargo test -p owner-llm` +//! ганяється без перезбірки Tauri-застосунку. +//! +//! Жодної драбини ACP → local → cloud тут: той самий fail-fast принцип, що +//! й у самому крейті (кожен `one_shot_*` — рівно один виклик). Драбину і +//! рішення «чи дозволена ACP-підписка на цьому вузлі» (autonomy.yml, клас +//! `external_comms`) компонує JS-шар owner +//! (`owner/src/composables/use-llm-cascade.js`) — тут лише дві незалежні +//! точки входу крейта. + +use std::collections::HashMap; +use std::path::Path; + +use llm_cascade::local_cloud::LocalProvider; +use llm_cascade::{one_shot_acp as cascade_one_shot_acp, AcpAgentKind, LocalCloud, Tier}; + +fn parse_tier(tier: &str) -> Result { + match tier { + "min" => Ok(Tier::Min), + "avg" => Ok(Tier::Avg), + "max" => Ok(Tier::Max), + other => Err(format!("невідомий тир {other:?}: очікується min|avg|max")), + } +} + +/// `Url::join`-семантика Rust трактує base URL без завершального слеша як +/// "файл" і зʼїдає останній сегмент шляху — `LocalProvider::base_url` мусить +/// завершуватись `/`. +fn with_trailing_slash(mut url: String) -> String { + if !url.ends_with('/') { + url.push('/'); + } + url +} + +/// Онбординг-конфіг omlx (UI, localStorage) — єдине джерело моделі local-тиру, +/// доки власник не задав реальний env-контракт `N_LOCAL_*_MODEL` +/// (`@7n/llm-lib`, той самий, що читає `llm_cascade::tiers`). Якщо змінна вже +/// задана ззовні — вона має пріоритет, тут її не чіпаємо. +fn seed_local_tier_env_default(omlx_model: &str) { + for var in ["N_LOCAL_MIN_MODEL", "N_LOCAL_AVG_MODEL", "N_LOCAL_MAX_MODEL"] { + if std::env::var(var).is_ok_and(|v| !v.is_empty()) { + continue; + } + // SAFETY: owner — однопотоковий що до читання цих змінних (жоден + // інший код процесу не читає їх конкурентно); той самий патерн, що + // й у тестах llm-cascade (tiers.rs). + unsafe { std::env::set_var(var, format!("omlx/{omlx_model}")) }; + } +} + +/// Один виклик локального/хмарного тиру каскаду через `genai`. `omlx_*` — +/// онбординг-конфіг власника (baseUrl/model/apiKey), що йде в +/// `local_providers` крейта під префіксом `omlx`. +/// +/// # Errors +/// Невалідний `tier`, відсутня конфігурація тиру (`N_LOCAL_*`/`N_CLOUD_*`), +/// чи помилка самого виклику — усе як `Err(String)`. +pub async fn one_shot( + tier: &str, + system: Option<&str>, + user: &str, + omlx_base_url: String, + omlx_model: String, + omlx_api_key: Option, +) -> Result { + let tier = parse_tier(tier)?; + seed_local_tier_env_default(&omlx_model); + + let mut providers = HashMap::new(); + providers.insert( + "omlx".to_string(), + LocalProvider { + base_url: with_trailing_slash(omlx_base_url), + api_key: omlx_api_key.filter(|k| !k.is_empty()), + }, + ); + let local_cloud = LocalCloud::new(providers); + local_cloud + .one_shot(tier, system, user) + .await + .map_err(|e| e.to_string()) +} + +/// Один виклик через ACP — спавнить залогінений локально агентський CLI +/// (`agent acp` чи `codex-acp`) і веде сесію по stdio/JSON-RPC у робочій +/// директорії `cwd` (owner передає `tasksDir` вузла — природний «проєктний» +/// корінь для ACP-сесії). Викликач (JS-драбина) вирішує, чи падати далі на +/// [`one_shot`]. +/// +/// # Errors +/// Невідомий `agent`, чи агент не встановлений/не залогінений підпискою. +pub async fn one_shot_acp(agent: &str, prompt: &str, cwd: &Path) -> Result { + let kind = match agent { + "cursor" => AcpAgentKind::Cursor, + "codex" => AcpAgentKind::Codex, + other => return Err(format!("невідомий ACP-агент {other:?}: очікується cursor|codex")), + }; + cascade_one_shot_acp(kind, prompt, cwd).await.map_err(|e| e.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_tier_accepts_known_values_rejects_unknown() { + assert!(matches!(parse_tier("min"), Ok(Tier::Min))); + assert!(matches!(parse_tier("avg"), Ok(Tier::Avg))); + assert!(matches!(parse_tier("max"), Ok(Tier::Max))); + assert!(parse_tier("huge").is_err()); + } + + #[test] + fn with_trailing_slash_appends_only_when_missing() { + assert_eq!(with_trailing_slash("http://127.0.0.1:8000/v1".to_string()), "http://127.0.0.1:8000/v1/"); + assert_eq!(with_trailing_slash("http://127.0.0.1:8000/v1/".to_string()), "http://127.0.0.1:8000/v1/"); + } + + #[tokio::test] + async fn one_shot_rejects_unknown_tier_without_touching_env() { + let err = one_shot("huge", None, "привіт", "http://x/v1".into(), "m".into(), None) + .await + .unwrap_err(); + assert!(err.contains("невідомий тир")); + } + + #[tokio::test] + async fn one_shot_acp_rejects_unknown_agent() { + let err = one_shot_acp("claude", "привіт", Path::new(".")).await.unwrap_err(); + assert!(err.contains("невідомий ACP-агент")); + } +} diff --git a/owner/owner-llm/tests/docs/fake_omlx.md b/owner/owner-llm/tests/docs/fake_omlx.md new file mode 100644 index 0000000..d62a466 --- /dev/null +++ b/owner/owner-llm/tests/docs/fake_omlx.md @@ -0,0 +1,28 @@ +--- +type: Rust Module +title: fake_omlx.rs +resource: owner/owner-llm/tests/fake_omlx.rs +docgen: + crc: 16a38411 + model: openai-codex/gpt-5.4-mini + tier: cloud-min + score: 100 + issues: judge:inaccurate:0.98 + judgeModel: openai-codex/gpt-5.4-mini +--- + +## Огляд + +Локальний fake/mock OpenAI-сумісний провайдер на `127.0.0.1:0` із однією фіксованою відповіддю дає контрольований read-only сценарій для `owner_llm::one_shot`. Він підтверджує, що виконується HTTP request до конфігурованого `omlx_base_url`, а не відбувається тихе пропускання LLM-логіки чи звернення до реального провайдера. + +## Поведінка + +1. Піднімає локальний fake OpenAI-сумісний endpoint на `127.0.0.1:0` і використовує його як контрольоване середовище для перевірки HTTP-раунд-тріпа. +2. Проганяє один запит через `owner_llm::one_shot` до заданого `omlx_base_url`, щоб підтвердити, що виклик не обходить LLM-логіку і не звертається до реального провайдера. +3. Повертає фіксовану assistant-відповідь з mock-сервера і перевіряє, що результат збігається з очікуваним бізнес-артефактом. +4. Окремо перевіряє аварійний сценарій на `http://127.0.0.1:1`: помилка з’єднання має бути повернена як `Err`, без panic. +5. Не виконує записів у ФС чи БД і не перевіряє кешування. + +## Гарантії поведінки + +- Read-only: не виконує операцій запису (ФС/БД). diff --git a/owner/owner-llm/tests/docs/index.md b/owner/owner-llm/tests/docs/index.md new file mode 100644 index 0000000..e51ed7d --- /dev/null +++ b/owner/owner-llm/tests/docs/index.md @@ -0,0 +1,9 @@ +--- +type: Directory Index +title: owner/owner-llm/tests +resource: owner/owner-llm/tests/ +--- + +| Файл | Тип | +| ---------------------------- | ----------- | +| [fake_omlx.rs](fake_omlx.md) | Rust Module | diff --git a/owner/owner-llm/tests/fake_omlx.rs b/owner/owner-llm/tests/fake_omlx.rs new file mode 100644 index 0000000..145c34b --- /dev/null +++ b/owner/owner-llm/tests/fake_omlx.rs @@ -0,0 +1,55 @@ +//! Fake/mock OpenAI-сумісний провайдер (n-tauri: core_test_isolation) — +//! доводить, що `owner_llm::one_shot` реально виконує HTTP-раунд-тріп проти +//! конфігурованого `omlx_base_url`, а не мовчки пропускає LLM-логіку чи б'є +//! по реальному провайдеру. Без мережі назовні: сервер — локальний +//! ephemeral-порт (127.0.0.1:0), одна фіксована відповідь. + +use std::io::{Read, Write}; +use std::net::TcpListener; + +/// Піднімає fake-сервер на один запит-відповідь і повертає його base URL. +fn spawn_fake_omlx(body: &'static str) -> String { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind fake omlx server"); + let addr = listener.local_addr().expect("local_addr"); + std::thread::spawn(move || { + if let Ok((mut stream, _)) = listener.accept() { + let mut buf = [0u8; 4096]; + let _ = stream.read(&mut buf); // тіло запиту не цікавить double — one-shot + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + let _ = stream.write_all(response.as_bytes()); + } + }); + format!("http://{addr}/v1") +} + +#[tokio::test] +async fn one_shot_round_trips_through_fake_openai_compatible_server() { + let body = r#"{"choices":[{"message":{"role":"assistant","content":"fake-response"}}]}"#; + let base_url = spawn_fake_omlx(body); + + let reply = owner_llm::one_shot("min", Some("sys"), "hi", base_url, "fake-model".to_string(), None) + .await + .expect("fake omlx round-trip failed"); + + assert_eq!(reply, "fake-response"); +} + +#[tokio::test] +async fn one_shot_surfaces_provider_error_as_err_not_panic() { + // Порт без слухача — з'єднання провалиться швидко (fail-fast, як і решта крейта). + let reply = owner_llm::one_shot( + "min", + None, + "hi", + "http://127.0.0.1:1".to_string(), + "fake-model".to_string(), + None + ) + .await; + + assert!(reply.is_err()); +} diff --git a/owner/src-tauri/Cargo.toml b/owner/src-tauri/Cargo.toml index 9e62db8..b0a3c2d 100644 --- a/owner/src-tauri/Cargo.toml +++ b/owner/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" mt-core = { git = "https://github.com/nitra/mt.git" } +owner-llm = { path = "../owner-llm" } notify = "8" chrono = "0.4" @@ -28,8 +29,5 @@ chrono = "0.4" tauri-plugin-updater = "2" tauri-plugin-window-state = "2" -[profile.dev] -strip = true - [dev-dependencies] tempfile = "3" diff --git a/owner/src-tauri/src/docs/index.md b/owner/src-tauri/src/docs/index.md index 581e97f..069a228 100644 --- a/owner/src-tauri/src/docs/index.md +++ b/owner/src-tauri/src/docs/index.md @@ -8,4 +8,5 @@ resource: owner/src-tauri/src/ | ---------------------- | ----------- | | [config.rs](config.md) | Rust Module | | [lib.rs](lib.md) | Rust Module | +| [llm.rs](llm.md) | Rust Module | | [main.rs](main.md) | Rust Module | diff --git a/owner/src-tauri/src/docs/llm.md b/owner/src-tauri/src/docs/llm.md new file mode 100644 index 0000000..5d19b27 --- /dev/null +++ b/owner/src-tauri/src/docs/llm.md @@ -0,0 +1,32 @@ +--- +type: Rust Module +title: llm.rs +resource: owner/src-tauri/src/llm.rs +docgen: + crc: c4f8a08b + model: openai-codex/gpt-5.4-mini + tier: cloud-min + score: 100 + issues: judge:inaccurate:0.98 + judgeModel: openai-codex/gpt-5.4-mini +--- + +## Огляд + +Файл надає дві тонкі `tauri::command`-обгортки для одноразових LLM-запитів через `owner-llm`: звичайний запит і ACP-запит для вказаного агента. Це окрема частина workspace crate без залежності на `tauri`, яка працює як одна з двох незалежних точок входу `llm-cascade`. Драбину ACP → local/cloud компонує JS `owner/src/composables/use-llm-cascade.js`, де вже враховано, чи дозволена ACP-підписка на цьому вузлі (`autonomy.yml`, клас `external_comms`). Для певних помилок повертає порожнє значення замість винятку. + +## Поведінка + +- `llm_one_shot` — запускає одноразовий LLM-запит через `owner-llm` і повертає текстову відповідь; при помилках може повертати порожнє значення замість винятку. +- `llm_one_shot_acp` — запускає одноразовий ACP-запит через `owner-llm` для вказаного агента і повертає текстову відповідь; при помилках може повертати порожнє значення замість винятку. + +## Публічний API + +- llm_one_shot — тонка `tauri::command`-обгортка над `owner-llm` для разового запиту в `llm-cascade`; працює як окрема точка входу без драбини ACP → local/cloud. +- llm_one_shot_acp — тонка `tauri::command`-обгортка над `owner-llm` для другої незалежної точки входу в `llm-cascade`; рішення про дозвіл ACP-підписки живе в `owner/src/composables/use-llm-cascade.js` і спирається на `autonomy.yml` та клас `external_comms`. +- Обидві функції read-only і за певних помилок повертають порожнє значення (`null`) замість винятку. + +## Гарантії поведінки + +- Read-only: не виконує операцій запису (ФС/БД). +- За певних помилок повертає порожнє значення (напр. `null`) замість винятку. diff --git a/owner/src-tauri/src/lib.rs b/owner/src-tauri/src/lib.rs index 1abcf95..ac88516 100644 --- a/owner/src-tauri/src/lib.rs +++ b/owner/src-tauri/src/lib.rs @@ -12,6 +12,9 @@ use notify::Watcher; use tauri::{Emitter, Manager as _}; mod config; +mod llm; + +use llm::{llm_one_shot, llm_one_shot_acp}; #[tauri::command] fn scan_tasks(tasks_dir: String) -> Result, String> { @@ -690,7 +693,9 @@ pub fn run() { spawn_approve, spawn_reject, human_done, - watch_tasks_dirs + watch_tasks_dirs, + llm_one_shot, + llm_one_shot_acp ]); #[cfg(desktop)] diff --git a/owner/src-tauri/src/llm.rs b/owner/src-tauri/src/llm.rs new file mode 100644 index 0000000..dc6bddc --- /dev/null +++ b/owner/src-tauri/src/llm.rs @@ -0,0 +1,23 @@ +//! Тонкі tauri::command-обгортки над `owner-llm` (агентна логіка, окремий +//! workspace crate без залежності на tauri — n-tauri: core_test_isolation). +//! Жодної драбини тут: дві незалежні точки входу крейта llm-cascade, драбину +//! ACP → local/cloud компонує JS (owner/src/composables/use-llm-cascade.js), +//! бо саме там уже живе рішення «чи дозволена ACP-підписка на цьому вузлі» +//! (autonomy.yml, клас `external_comms`). + +#[tauri::command] +pub async fn llm_one_shot( + tier: String, + system: Option, + user: String, + omlx_base_url: String, + omlx_model: String, + omlx_api_key: Option, +) -> Result { + owner_llm::one_shot(&tier, system.as_deref(), &user, omlx_base_url, omlx_model, omlx_api_key).await +} + +#[tauri::command] +pub async fn llm_one_shot_acp(agent: String, prompt: String, cwd: String) -> Result { + owner_llm::one_shot_acp(&agent, &prompt, std::path::Path::new(&cwd)).await +} diff --git a/owner/src/components/DecisionCard.vue b/owner/src/components/DecisionCard.vue index b666a5a..f15aaa1 100644 --- a/owner/src/components/DecisionCard.vue +++ b/owner/src/components/DecisionCard.vue @@ -369,6 +369,14 @@ async function act(name, input) { return false } +/** + * Вузол рішення для autonomy-гейта ACP-рунгів use-llm-cascade.js. + * @returns {{ tasksDir: string, taskPath: string }} контекст вузла + */ +function decisionContext() { + return { tasksDir: props.decision.workspace.path, taskPath: props.decision.node.path } +} + /** * Дайджест рішення для штабу (кешується — бриф і заперечення не сканують * ліс двічі): контракт вузла + (для plan-review) підзадачі плану. @@ -396,7 +404,7 @@ async function loadBriefing() { briefLoading.value = true briefError.value = '' try { - brief.value = await requestBriefing(await loadDigest()) + brief.value = await requestBriefing(await loadDigest(), decisionContext()) } catch (error) { briefError.value = String(error?.message ?? error) } finally { @@ -414,7 +422,7 @@ async function openApproveConfirm() { if (objection.value || objectionError.value) return objectionLoading.value = true try { - objection.value = await requestObjection(await loadDigest()) + objection.value = await requestObjection(await loadDigest(), decisionContext()) } catch (error) { objectionError.value = String(error?.message ?? error) } finally { diff --git a/owner/src/components/PlannerDialog.vue b/owner/src/components/PlannerDialog.vue index 6a7393d..4227642 100644 --- a/owner/src/components/PlannerDialog.vue +++ b/owner/src/components/PlannerDialog.vue @@ -134,7 +134,7 @@ async function generate() { generating.value = true results.value = [] try { - results.value = await draftAlternatives(intent.value.trim()) + results.value = await draftAlternatives(intent.value.trim(), { tasksDir: workspace.value, taskPath: nodeId.value }) } finally { generating.value = false } diff --git a/owner/src/components/docs/PlannerDialog.md b/owner/src/components/docs/PlannerDialog.md index 310d150..8311486 100644 --- a/owner/src/components/docs/PlannerDialog.md +++ b/owner/src/components/docs/PlannerDialog.md @@ -3,7 +3,7 @@ type: Vue Component title: PlannerDialog.vue resource: owner/src/components/PlannerDialog.vue docgen: - crc: 1f8ca6ed + crc: b1d9a80b model: omlx/gemma-4-e4b-it-OptiQ-4bit --- diff --git a/owner/src/composables/docs/index.md b/owner/src/composables/docs/index.md index 4794571..eb379ff 100644 --- a/owner/src/composables/docs/index.md +++ b/owner/src/composables/docs/index.md @@ -4,10 +4,11 @@ title: owner/src/composables resource: owner/src/composables/ --- -| Файл | Тип | -| ---------------------------------- | --------- | -| [use-autonomy.js](use-autonomy.md) | JS Module | -| [use-critic.js](use-critic.md) | JS Module | -| [use-forest.js](use-forest.md) | JS Module | -| [use-planner.js](use-planner.md) | JS Module | -| [use-staff.js](use-staff.md) | JS Module | +| Файл | Тип | +| ---------------------------------------- | --------- | +| [use-autonomy.js](use-autonomy.md) | JS Module | +| [use-critic.js](use-critic.md) | JS Module | +| [use-forest.js](use-forest.md) | JS Module | +| [use-llm-cascade.js](use-llm-cascade.md) | JS Module | +| [use-planner.js](use-planner.md) | JS Module | +| [use-staff.js](use-staff.md) | JS Module | diff --git a/owner/src/composables/docs/use-llm-cascade.md b/owner/src/composables/docs/use-llm-cascade.md new file mode 100644 index 0000000..0b84fc0 --- /dev/null +++ b/owner/src/composables/docs/use-llm-cascade.md @@ -0,0 +1,32 @@ +--- +type: JS Module +title: use-llm-cascade.js +resource: owner/src/composables/use-llm-cascade.js +docgen: + crc: 41536b0a + model: openai-codex/gpt-5.4-mini + score: 100 + issues: judge:inaccurate:0.98 + judgeModel: openai-codex/gpt-5.4-mini +--- + +## Огляд + +`useLlmCascade` — fail-safe точка для виклику LLM у робочому вузлі: вона обирає доступний шлях виконання та не кидає винятків назовні. Для окремих помилок повертає порожнє значення, зокрема `null`, щоб виклик можна було обробити без аварійного падіння. + +## Поведінка + +1. `useLlmCascade` збирає єдину точку виклику LLM для робочого вузла: спочатку намагається використати ACP-драбину, якщо для цього контексту дозволені `external_comms`, а якщо ні — переходить до local/cloud-шляху через поточний `omlx`-конфіг. +2. Для ACP-драбини перевіряє, чи доступний вузол і чи є доступна політика для нього; якщо вузла немає або доступ не підтверджено, одразу пропускає ACP і працює через local/cloud. +3. Якщо ACP дозволено, послідовно пробує доступні ранги драбини; як тільки один ранг повертає відповідь, вона стає результатом. +4. Якщо всі ACP-спроби недоступні або завершилися помилкою, виконує резервний виклик через local/cloud з поточними значеннями `baseUrl`, `model` і `apiKey`. +5. Під час виконання працює fail-safe: помилки внутрішніх шляхів не виходять назовні, а невдалий ранг просто не блокує наступний варіант. + +## Публічний API + +- useLlmCascade — налаштовує local-tier через omlx і запускає один LLM-запит по ladder-конфігурації + +## Гарантії поведінки + +- Перехоплює помилки і не пропускає винятків назовні (fail-safe). +- За певних помилок повертає порожнє значення (напр. `null`) замість винятку. diff --git a/owner/src/composables/docs/use-planner.md b/owner/src/composables/docs/use-planner.md index 222dc25..9789061 100644 --- a/owner/src/composables/docs/use-planner.md +++ b/owner/src/composables/docs/use-planner.md @@ -3,9 +3,8 @@ type: JS Module title: use-planner.js resource: owner/src/composables/use-planner.js docgen: - crc: d956231d + crc: a2b846f4 model: openai-codex/gpt-5.4-mini - tier: cloud-min score: 100 issues: judge:inaccurate:0.98 judgeModel: openai-codex/gpt-5.4-mini diff --git a/owner/src/composables/docs/use-staff.md b/owner/src/composables/docs/use-staff.md index e271877..dace65f 100644 --- a/owner/src/composables/docs/use-staff.md +++ b/owner/src/composables/docs/use-staff.md @@ -3,9 +3,8 @@ type: JS Module title: use-staff.js resource: owner/src/composables/use-staff.js docgen: - crc: 769c547b + crc: 217d3a53 model: omlx/gemma-4-e4b-it-OptiQ-4bit - tier: local-min score: 100 issues: judge:inaccurate:0.98 judgeModel: openai-codex/gpt-5.4-mini diff --git a/owner/src/composables/use-critic.js b/owner/src/composables/use-critic.js index 940131d..72fca95 100644 --- a/owner/src/composables/use-critic.js +++ b/owner/src/composables/use-critic.js @@ -1,15 +1,14 @@ import { ref } from 'vue' -import { createOpenAiChat } from '@7n/tauri-components' -import { useOmlx } from '@7n/tauri-components/vue' -import { fetch as tauriFetch } from '@tauri-apps/plugin-http' import { criticPrompt, parseCriticReply, runDeterministicCritic } from '../critic.js' import { extractMission } from '../mission.js' import { dispatch } from '../tool/index.js' +import { useLlmCascade } from './use-llm-cascade.js' // Критик як споживач: детермінований прогін — безкоштовний, автоматичний при // кожному rescan; семантичний (LLM) — на вимогу власника, по одному виклику -// на воркспейс. Вердикти живуть у памʼяті сесії; відхилені власником не -// повертаються до наступного повного прогону. +// на воркспейс через спільну драбину use-llm-cascade.js. Вердикти живуть у +// памʼяті сесії; відхилені власником не повертаються до наступного повного +// прогону. // Межі дайджесту: скільки вузлів і скільки символів контракту на вузол. const DIGEST_NODES = 30 @@ -54,10 +53,7 @@ async function workspaceDigest(workspace, nodes, scope) { * @returns {{ verdicts: import('vue').Ref, running: import('vue').Ref, refreshDeterministic: (workspaces: object[], forest: Record) => void, runSemantic: (workspaces: object[], forest: Record) => Promise, dismiss: (verdict: object) => void }} поверхня критика */ export function useCritic() { - const { baseUrl, model, apiKey } = useOmlx({ - storagePrefix: 'owner', - defaultModel: 'gemma-4-e4b-it-OptiQ-4bit' - }) + const { oneShot } = useLlmCascade() /** * Детермінований прогін: дешевий, викликається після кожного rescan. @@ -81,25 +77,13 @@ export function useCritic() { async function runSemantic(workspaces, forest, scopes) { running.value = true try { - const chat = createOpenAiChat({ - baseUrl: baseUrl.value, - model: model.value, - apiKey: apiKey.value || undefined, - fetchFn: tauriFetch - }) const found = await Promise.all( workspaces.map(async workspace => { try { const digest = await workspaceDigest(workspace, forest[workspace.path], scopes?.[workspace.path]) const { system, user } = criticPrompt(digest) - const reply = await chat({ - messages: [ - { role: 'system', content: system }, - { role: 'user', content: user } - ], - tools: [] - }) - return parseCriticReply(reply.content ?? '', workspace) + const reply = await oneShot({ system, user, tasksDir: workspace.path }) + return parseCriticReply(reply ?? '', workspace) } catch { return [] // воркспейс не прочитався чи модель мовчить — не валимо прогін } diff --git a/owner/src/composables/use-llm-cascade.js b/owner/src/composables/use-llm-cascade.js new file mode 100644 index 0000000..1555fa0 --- /dev/null +++ b/owner/src/composables/use-llm-cascade.js @@ -0,0 +1,73 @@ +import { invoke } from '@tauri-apps/api/core' +import { useOmlx } from '@7n/tauri-components/vue' +import { resolveAction } from '../autonomy.js' +import { effectivePolicyFor } from './use-autonomy.js' + +// Єдиний шлях виклику LLM для плановика/критика/штабу (заміна трьох +// незалежних createOpenAiChat): драбина ACP (Cursor → Codex CLI, особиста +// підписка, лише якщо клас `external_comms` дозволено autonomy.yml вузла) → +// local/cloud тир крейта llm-cascade (nitra/7n-rules) поверх онбординг- +// конфігу omlx. Драбину свідомо компонує JS, не Rust-command: той самий +// fail-fast принцип, що й у крейті (жодного retry всередині одного +// `one_shot_*`), і тому, що рішення «чи дозволена підписка на цьому вузлі» +// вже живе тут (autonomy.js), не в Rust-шарі. + +const ACP_LADDER = ['cursor', 'codex'] + +/** + * Чи дозволена ACP-підписка (Cursor/Codex CLI) для виклику в цьому контексті. + * Без вузла (напр. критик по всьому воркспейсу, без конкретного taskPath) чи + * без autonomy.yml на шляху — fail-closed: `resolveAction` повертає + * `'approve'`, ACP-рунги пропускаються, драбина одразу йде на local/cloud. + * @param {{ tasksDir?: string, taskPath?: string }} context вузол виклику + * @returns {Promise} true — можна пробувати ACP-рунги + */ +async function acpAllowed({ tasksDir, taskPath } = {}) { + if (!tasksDir) return false + try { + const effective = await effectivePolicyFor(tasksDir, taskPath ?? '') + return resolveAction(effective, 'external_comms') === 'auto' + } catch { + return false + } +} + +/** + * Композабл каскаду LLM: конфіг local-тиру (omlx) + один виклик за драбиною. + * @returns {{ baseUrl: import('vue').Ref, model: import('vue').Ref, apiKey: import('vue').Ref, saveOmlx: () => void, oneShot: (req: { tier?: 'min'|'avg'|'max', system?: string, user: string, tasksDir?: string, taskPath?: string }) => Promise }} поверхня каскаду + */ +export function useLlmCascade() { + const { baseUrl, model, apiKey, save } = useOmlx({ + storagePrefix: 'owner', + defaultModel: 'gemma-4-e4b-it-OptiQ-4bit' + }) + + /** + * Один виклик LLM: спершу ACP-рунги (якщо дозволено autonomy), інакше чи + * при провалі всіх — local/cloud тир крейта поверх поточного omlx-конфігу. + * @param {{ tier?: 'min'|'avg'|'max', system?: string, user: string, tasksDir?: string, taskPath?: string }} req запит + * @returns {Promise} текст відповіді моделі + */ + async function oneShot({ tier = 'max', system, user, tasksDir, taskPath }) { + if (await acpAllowed({ tasksDir, taskPath })) { + const prompt = system ? `${system}\n\n${user}` : user + for (const agent of ACP_LADDER) { + try { + return await invoke('llm_one_shot_acp', { agent, prompt, cwd: tasksDir }) + } catch { + // рунг недоступний (CLI не встановлений/не залогінений) — наступний + } + } + } + return invoke('llm_one_shot', { + tier, + system: system ?? null, + user, + omlxBaseUrl: baseUrl.value, + omlxModel: model.value, + omlxApiKey: apiKey.value || null + }) + } + + return { baseUrl, model, apiKey, saveOmlx: save, oneShot } +} diff --git a/owner/src/composables/use-planner.js b/owner/src/composables/use-planner.js index 4d9da73..37a5ac2 100644 --- a/owner/src/composables/use-planner.js +++ b/owner/src/composables/use-planner.js @@ -1,47 +1,31 @@ -import { createOpenAiChat } from '@7n/tauri-components' -import { useOmlx } from '@7n/tauri-components/vue' -import { fetch as tauriFetch } from '@tauri-apps/plugin-http' import { LENSES, parseAlternative, plannerPrompt } from '../planner.js' +import { useLlmCascade } from './use-llm-cascade.js' // Плановик як споживач LLM: два незалежні виклики моделі (по одному на лінзу) // паралельно; кожен повертає альтернативу або помилку — власник бачить обидва -// результати навіть коли одна лінза не впоралась. Провайдер — omlx-конфіг -// (localStorage, префікс owner), транспорт — tauri-http. +// результати навіть коли одна лінза не впоралась. Виклик — спільна драбина +// use-llm-cascade.js (ACP-підписка → omlx local/cloud тир). /** * Композабл плановика: конфіг моделі + генерація альтернатив декомпозиції. - * @returns {{ baseUrl: import('vue').Ref, model: import('vue').Ref, apiKey: import('vue').Ref, saveOmlx: () => void, draftAlternatives: (intent: string) => Promise }} поверхня плановика + * @returns {{ baseUrl: import('vue').Ref, model: import('vue').Ref, apiKey: import('vue').Ref, saveOmlx: () => void, draftAlternatives: (intent: string, context?: { tasksDir?: string, taskPath?: string }) => Promise }} поверхня плановика */ export function usePlanner() { - const { baseUrl, model, apiKey, save } = useOmlx({ - storagePrefix: 'owner', - defaultModel: 'gemma-4-e4b-it-OptiQ-4bit' - }) + const { baseUrl, model, apiKey, saveOmlx, oneShot } = useLlmCascade() /** * Генерує альтернативи плану — по одній на лінзу, паралельно. * @param {string} intent ціль власника природною мовою + * @param {{ tasksDir?: string, taskPath?: string }} [context] вузол цілі (для autonomy-гейта ACP) * @returns {Promise>} результат кожної лінзи */ - function draftAlternatives(intent) { - const chat = createOpenAiChat({ - baseUrl: baseUrl.value, - model: model.value, - apiKey: apiKey.value || undefined, - fetchFn: tauriFetch - }) + function draftAlternatives(intent, context = {}) { return Promise.all( LENSES.map(async lens => { try { const { system, user } = plannerPrompt(intent, lens) - const reply = await chat({ - messages: [ - { role: 'system', content: system }, - { role: 'user', content: user } - ], - tools: [] - }) - return { lens, alternative: parseAlternative(reply.content ?? '') } + const reply = await oneShot({ system, user, ...context }) + return { lens, alternative: parseAlternative(reply ?? '') } } catch (error) { return { lens, error: String(error?.message ?? error) } } @@ -49,5 +33,5 @@ export function usePlanner() { ) } - return { baseUrl, model, apiKey, saveOmlx: save, draftAlternatives } + return { baseUrl, model, apiKey, saveOmlx, draftAlternatives } } diff --git a/owner/src/composables/use-staff.js b/owner/src/composables/use-staff.js index 214342d..f4548d1 100644 --- a/owner/src/composables/use-staff.js +++ b/owner/src/composables/use-staff.js @@ -1,65 +1,39 @@ -import { createOpenAiChat } from '@7n/tauri-components' -import { useOmlx } from '@7n/tauri-components/vue' -import { fetch as tauriFetch } from '@tauri-apps/plugin-http' import { briefingPrompt, objectionPrompt, parseBriefing, parseObjection } from '../staff.js' +import { useLlmCascade } from './use-llm-cascade.js' // Штаб як споживач LLM: briefing і заперечення — окремі виклики моделі за -// вимогою (не автоматичні), той самий omlx-конфіг, що плановик і критик. +// вимогою (не автоматичні), через спільну драбину use-llm-cascade.js (той +// самий omlx-конфіг, що плановик і критик). /** * Композабл штабу: генерація брифу й найсильнішого заперечення для рішення. - * @returns {{ requestBriefing: (digest: string) => Promise, requestObjection: (digest: string) => Promise }} поверхня штабу + * @returns {{ requestBriefing: (digest: string, context?: { tasksDir?: string, taskPath?: string }) => Promise, requestObjection: (digest: string, context?: { tasksDir?: string, taskPath?: string }) => Promise }} поверхня штабу */ export function useStaff() { - const { baseUrl, model, apiKey } = useOmlx({ - storagePrefix: 'owner', - defaultModel: 'gemma-4-e4b-it-OptiQ-4bit' - }) - - /** - * @returns {(req: object) => Promise} chat-функція за поточним конфігом - */ - function chat() { - return createOpenAiChat({ - baseUrl: baseUrl.value, - model: model.value, - apiKey: apiKey.value || undefined, - fetchFn: tauriFetch - }) - } + const { oneShot } = useLlmCascade() /** * Генерує бриф рішення: контекст, варіанти, рекомендація, наслідок відмови. * @param {string} digest дайджест рішення (staff.js buildDecisionDigest) + * @param {{ tasksDir?: string, taskPath?: string }} [context] вузол рішення (для autonomy-гейта ACP) * @returns {Promise<{ context: string, options: string[], recommendation: string, ifDeclined: string }>} бриф */ - async function requestBriefing(digest) { + async function requestBriefing(digest, context = {}) { const { system, user } = briefingPrompt(digest) - const reply = await chat()({ - messages: [ - { role: 'system', content: system }, - { role: 'user', content: user } - ], - tools: [] - }) - return parseBriefing(reply.content ?? '') + const reply = await oneShot({ system, user, ...context }) + return parseBriefing(reply ?? '') } /** * Генерує найсильніше заперечення проти рішення (анти-rubber-stamping). * @param {string} digest дайджест рішення + * @param {{ tasksDir?: string, taskPath?: string }} [context] вузол рішення (для autonomy-гейта ACP) * @returns {Promise} текст заперечення */ - async function requestObjection(digest) { + async function requestObjection(digest, context = {}) { const { system, user } = objectionPrompt(digest) - const reply = await chat()({ - messages: [ - { role: 'system', content: system }, - { role: 'user', content: user } - ], - tools: [] - }) - return parseObjection(reply.content ?? '') + const reply = await oneShot({ system, user, ...context }) + return parseObjection(reply ?? '') } return { requestBriefing, requestObjection } diff --git a/owner/src/tests/use-llm-cascade.test.js b/owner/src/tests/use-llm-cascade.test.js new file mode 100644 index 0000000..4ef3a63 --- /dev/null +++ b/owner/src/tests/use-llm-cascade.test.js @@ -0,0 +1,83 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const invoke = vi.fn() +const reads = {} + +vi.mock('@tauri-apps/api/core', () => ({ invoke: (...args) => invoke(...args) })) +vi.mock('../tool/index.js', () => ({ + dispatch: (name, input) => { + if (name !== 'read_autonomy') throw new Error(`unexpected tool ${name}`) + return Promise.resolve({ ok: true, output: reads[input.taskPath] ?? '' }) + } +})) + +const { useLlmCascade } = await import('../composables/use-llm-cascade.js') + +beforeEach(() => { + invoke.mockReset() + for (const key of Object.keys(reads)) delete reads[key] +}) + +describe('useLlmCascade.oneShot', () => { + it('без tasksDir — одразу local/cloud тир крейта, без спроби ACP', async () => { + invoke.mockResolvedValueOnce('відповідь local') + const { oneShot } = useLlmCascade() + + const reply = await oneShot({ system: 'sys', user: 'ask' }) + + expect(reply).toBe('відповідь local') + expect(invoke).toHaveBeenCalledTimes(1) + expect(invoke).toHaveBeenCalledWith( + 'llm_one_shot', + expect.objectContaining({ tier: 'max', system: 'sys', user: 'ask' }) + ) + }) + + it('autonomy дозволяє external_comms:auto на вузлі — пробує ACP (cursor) першим', async () => { + reads.goal = 'external_comms: auto\n' + invoke.mockResolvedValueOnce('відповідь cursor') + const { oneShot } = useLlmCascade() + + const reply = await oneShot({ system: 'sys', user: 'ask', tasksDir: '/ws/mt', taskPath: 'goal' }) + + expect(reply).toBe('відповідь cursor') + expect(invoke).toHaveBeenCalledWith('llm_one_shot_acp', { agent: 'cursor', prompt: 'sys\n\nask', cwd: '/ws/mt' }) + }) + + it('cursor падає — падає далі на codex, потім на local/cloud', async () => { + reads.goal = 'external_comms: auto\n' + invoke + .mockRejectedValueOnce(new Error('cursor не залогінений')) + .mockRejectedValueOnce(new Error('codex not initialized')) + .mockResolvedValueOnce('відповідь local') + const { oneShot } = useLlmCascade() + + const reply = await oneShot({ user: 'ask', tasksDir: '/ws/mt', taskPath: 'goal' }) + + expect(reply).toBe('відповідь local') + expect(invoke).toHaveBeenNthCalledWith(1, 'llm_one_shot_acp', { agent: 'cursor', prompt: 'ask', cwd: '/ws/mt' }) + expect(invoke).toHaveBeenNthCalledWith(2, 'llm_one_shot_acp', { agent: 'codex', prompt: 'ask', cwd: '/ws/mt' }) + expect(invoke).toHaveBeenNthCalledWith(3, 'llm_one_shot', expect.objectContaining({ user: 'ask' })) + }) + + it('без autonomy.yml на вузлі — fail-closed (approve), ACP пропускається', async () => { + invoke.mockResolvedValueOnce('відповідь local') + const { oneShot } = useLlmCascade() + + await oneShot({ user: 'ask', tasksDir: '/ws/mt', taskPath: 'goal' }) + + expect(invoke).toHaveBeenCalledTimes(1) + expect(invoke).toHaveBeenCalledWith('llm_one_shot', expect.objectContaining({ user: 'ask' })) + }) + + it('вузол явно ставить external_comms:approve — ACP пропускається', async () => { + reads.goal = 'external_comms: approve\n' + invoke.mockResolvedValueOnce('відповідь local') + const { oneShot } = useLlmCascade() + + await oneShot({ user: 'ask', tasksDir: '/ws/mt', taskPath: 'goal' }) + + expect(invoke).toHaveBeenCalledTimes(1) + expect(invoke).toHaveBeenCalledWith('llm_one_shot', expect.objectContaining({ user: 'ask' })) + }) +})