diff --git a/CHANGELOG.md b/CHANGELOG.md index 82929649..c2ec0296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,52 @@ All notable changes to dirge are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed +- Dependency sweep. `rmcp` was on three versions at once — our own 1.7, a 2.2 + that `rig`'s unused `rmcp` feature dragged in, and a 1.8 from + `agent-client-protocol`. Nothing here ever touched rig's MCP bridge, so that + feature is gone; `agent-client-protocol` moves 0.12 → 2.0 (its types live under + `schema::v1` now, and `respond_with_error` moved from `Dispatch` to the + `Responder` that only the request variant carries); and `rmcp` goes to 3.1, + where `Annotated` is flattened to `ContentBlock`. One rmcp, latest. + + Also current: `rusqlite` 0.31 → 0.40 (no `ToSql for usize`), `sysinfo` + 0.32 → 0.39 (`RefreshKind::new` → `nothing`), `sha2` 0.10 → 0.11 (digest output + dropped `LowerHex`, so the two hex helpers encode explicitly), `jsonschema` + 0.46 → 0.49, `base64` 0.23, `compact_str` 0.10, `http` 1.5, `notify-rust` 4.18. + `tree-sitter` stays at 0.25 — 0.26 does not resolve against the grammar crates. + +### Fixed +- Reasoning is shown again for providers that stream it as `delta.reasoning` + rather than `delta.reasoning_content` (GH #745, reported against LocalAI). + rig 0.39 only deserialized `reasoning_content`, so serde dropped the field and + the panel stayed empty while text and tool calls — carried in the same delta — + worked fine. rig 0.40 added the fallback upstream; this bumps rig to 0.41. + + The bump is not small. 0.41 splits the classic runtime into `rig-agent` behind + an `agent` feature, replaces `Tool::definition` with `description`/`parameters` + across all 37 tools, drops `ToolDyn` in favour of a concrete struct, and makes + `Agent::preamble` and `Agent::model` private. Dirge's tools move to the + context-free `PortableTool` contract, which is the honest fit — the agent loop + has driven its own dispatch since 4.5h-6 and never used rig's `ToolContext`. + The erased-tool seam is now dirge's own `DynTool`; it has broken on two + consecutive rig releases and does not need to be rig's. `AnyAgentInner` holds + the completion model directly instead of a rig `Agent` it only ever read the + model back out of. + +- Cerebras no longer 400s on the turn after a reasoning turn. It streams + reasoning as `delta.reasoning` — the same field LocalAI uses — so the fix + above meant dirge started capturing it and replaying it as `reasoning_content`, + which Cerebras rejects outright. The field is renamed to `reasoning` at the + wire boundary rather than dropped, so the model keeps its own chain of thought + in context on the next turn. Backends that want `reasoning_content` are + untouched: DeepSeek needs it on tool-call turns, and llama.cpp/LocalAI chat + templates read `message.reasoning_content` back out of the assistant turn. + Only the OpenAI Responses API still has the block dropped, because it keys + reasoning to encrypted ids dirge does not retain. + ## [0.21.6] - 2026-08-04 ### Added diff --git a/Cargo.lock b/Cargo.lock index 3e37bfa2..4ceb876d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,44 +41,43 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.12.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4361ba6627e51de955b10f3c77fb9eb959c85191a236c1c2c84e32f4ff240faf" +checksum = "6d87bc7769eba641753ba5dc52f73ec3765d51022c6753bf040967125ddc86a8" dependencies = [ "agent-client-protocol-derive", "agent-client-protocol-schema", + "async-io", "async-process", "blocking", "futures", "futures-concurrency", - "jsonrpcmsg", - "rmcp", "rustc-hash 2.1.3", + "rustix", "schemars 1.2.1", "serde", "serde_json", "shell-words", - "tokio", - "tokio-util", "tracing", "uuid", + "windows-sys 0.61.2", ] [[package]] name = "agent-client-protocol-derive" -version = "0.11.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabdc9d845d08ec7ed2d0c9de1ae4a1b198301407d55855261572761be90ec9f" +checksum = "3abd4080f51e4f24f5042beb7fb7a66ede29a2dc1c2582c329532e1c27264ddc" dependencies = [ "quote", - "syn 2.0.119", + "syn 3.0.0", ] [[package]] name = "agent-client-protocol-schema" -version = "0.13.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b957d8391ac3933e2a940446171c508d2b8ffc386d8fa7d0b9c936a2575b463e" +checksum = "d5c231915b4ab578c722eca2d1bd7df4d300bfd6cac3b8e9f0d1e3ddc95b187c" dependencies = [ "anyhow", "derive_more", @@ -413,6 +412,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -427,7 +432,7 @@ checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0" dependencies = [ "blowfish", "pbkdf2", - "sha2 0.11.0", + "sha2", ] [[package]] @@ -767,7 +772,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] @@ -776,7 +781,7 @@ version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.119", @@ -821,8 +826,21 @@ dependencies = [ "itoa", "rustversion", "ryu", + "static_assertions", +] + +[[package]] +name = "compact_str" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416" +dependencies = [ + "castaway", + "cfg-if", + "itoa", "serde", "static_assertions", + "zmij", ] [[package]] @@ -1113,6 +1131,16 @@ dependencies = [ "darling_macro 0.23.0", ] +[[package]] +name = "darling" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" +dependencies = [ + "darling_core 0.24.0", + "darling_macro 0.24.0", +] + [[package]] name = "darling_core" version = "0.20.11" @@ -1123,7 +1151,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", + "strsim", "syn 2.0.119", ] @@ -1136,10 +1164,23 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", + "strsim", "syn 2.0.119", ] +[[package]] +name = "darling_core" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.0", +] + [[package]] name = "darling_macro" version = "0.20.11" @@ -1163,58 +1204,28 @@ dependencies = [ ] [[package]] -name = "data-encoding" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" - -[[package]] -name = "delegate" -version = "0.13.5" +name = "darling_macro" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" +checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" dependencies = [ - "proc-macro2", + "darling_core 0.24.0", "quote", - "syn 2.0.119", -] - -[[package]] -name = "deluxe" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed332aaf752b459088acf3dd4eca323e3ef4b83c70a84ca48fb0ec5305f1488" -dependencies = [ - "deluxe-core", - "deluxe-macros", - "once_cell", - "proc-macro2", - "syn 2.0.119", + "syn 3.0.0", ] [[package]] -name = "deluxe-core" -version = "0.5.0" +name = "data-encoding" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddada51c8576df9d6a8450c351ff63042b092c9458b8ac7d20f89cbd0ffd313" -dependencies = [ - "arrayvec", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 2.0.119", -] +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] -name = "deluxe-macros" -version = "0.5.0" +name = "delegate" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117" +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" dependencies = [ - "deluxe-core", - "heck 0.4.1", - "if_chain", - "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.119", @@ -1308,12 +1319,12 @@ dependencies = [ "ansi-to-tui", "anyhow", "async-stream", - "base64", + "base64 0.23.1", "bm25", "bytes", "chrono", "clap", - "compact_str", + "compact_str 0.10.0", "crossterm", "csv", "ctor", @@ -1349,7 +1360,7 @@ dependencies = [ "rustls", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "smallvec 1.15.2", "stop-words 0.10.0", "streaming-iterator", @@ -1484,7 +1495,7 @@ dependencies = [ "ed25519", "rand_core 0.10.1", "serde", - "sha2 0.11.0", + "sha2", "signature", "subtle", "zeroize", @@ -1647,9 +1658,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fancy-regex" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" +checksum = "476de73bddf2ef8490aa4ee8f1cf40b430bf1d56c48c22080e5186952cd580e6" dependencies = [ "bit-set", "regex-automata", @@ -2060,15 +2071,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -2102,15 +2104,6 @@ dependencies = [ "foldhash 0.2.0", ] -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "hashlink" version = "0.11.1" @@ -2121,10 +2114,13 @@ dependencies = [ ] [[package]] -name = "heck" -version = "0.4.1" +name = "hashlink" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +dependencies = [ + "hashbrown 0.17.1", +] [[package]] name = "heck" @@ -2192,9 +2188,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2283,7 +2279,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -2435,12 +2431,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "if_chain" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" - [[package]] name = "ignore" version = "0.4.30" @@ -2675,21 +2665,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonrpcmsg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d833a15225c779251e13929203518c2ff26e2fe0f322d584b213f4f4dad37bd" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "jsonschema" -version = "0.46.10" +version = "0.49.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a699d3e77675e6aa4bfffe3b907c8b5f7ed3241f9965bffb25475ad4b08d05" +checksum = "8da60094fc1968bbd091e2cfa004415cb7b19e25e592376e66a64aa832bb6039" dependencies = [ "ahash", "bytecount", @@ -2701,6 +2681,7 @@ dependencies = [ "idna", "itoa", "jsonschema-regex", + "jsonschema-value", "num-cmp", "num-traits", "percent-encoding", @@ -2710,19 +2691,34 @@ dependencies = [ "rustls", "serde", "serde_json", + "strum", "unicode-general-category", "uuid-simd", ] [[package]] name = "jsonschema-regex" -version = "0.46.10" +version = "0.49.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd1086b01b9349fd4ef9a07433965af64c8ce8159abe633a189e4ff817bd13" +checksum = "36539f34ef9e5da418433fbbf7294522d8f930ecf6a540ccd1ec6481c9ff9056" dependencies = [ "regex-syntax", ] +[[package]] +name = "jsonschema-value" +version = "0.49.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bc513dfee68c6fe29498451df95a32951ea227801b476f4a1f236433986c891" +dependencies = [ + "ahash", + "bytecount", + "fraction", + "num-cmp", + "num-traits", + "serde_json", +] + [[package]] name = "kasuari" version = "0.4.12" @@ -2804,9 +2800,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.28.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" dependencies = [ "cc", "pkg-config", @@ -3009,15 +3005,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "nanoid" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" -dependencies = [ - "rand 0.8.7", -] - [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -3057,9 +3044,9 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.17.0" +version = "4.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50ff2e74231b72c832d82982193b417f230945be6bdb5575b251d941d31adb00" +checksum = "c5b4c1b4f2aa9f25f63a7a49d3dd0ed567b3670da15330a66b29434be899b891" dependencies = [ "futures-lite", "log", @@ -3239,6 +3226,16 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3298,7 +3295,7 @@ dependencies = [ "elliptic-curve", "primefield", "primeorder", - "sha2 0.11.0", + "sha2", ] [[package]] @@ -3312,7 +3309,7 @@ dependencies = [ "fiat-crypto", "primefield", "primeorder", - "sha2 0.11.0", + "sha2", ] [[package]] @@ -3326,7 +3323,7 @@ dependencies = [ "elliptic-curve", "primefield", "primeorder", - "sha2 0.11.0", + "sha2", ] [[package]] @@ -3342,7 +3339,7 @@ dependencies = [ "futures", "log", "rand 0.10.2", - "sha2 0.11.0", + "sha2", "thiserror 2.0.19", "tokio", "windows 0.62.2", @@ -3541,7 +3538,7 @@ dependencies = [ "pbkdf2", "rand_core 0.10.1", "scrypt", - "sha2 0.11.0", + "sha2", "spki", ] @@ -3620,15 +3617,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "precomputed-hash" version = "0.1.1" @@ -3672,23 +3660,13 @@ dependencies = [ "wnaf", ] -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.13+spec-1.1.0", + "toml_edit", ] [[package]] @@ -3777,17 +3755,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rand" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" -dependencies = [ - "libc", - "rand_chacha", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.10.2" @@ -3799,16 +3766,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_core" version = "0.3.1" @@ -3829,9 +3786,6 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] [[package]] name = "rand_core" @@ -3877,7 +3831,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ "bitflags 2.13.1", - "compact_str", + "compact_str 0.9.1", "hashbrown 0.17.1", "itertools 0.14.0", "kasuari", @@ -3994,14 +3948,14 @@ dependencies = [ [[package]] name = "referencing" -version = "0.46.10" +version = "0.49.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbf332a2f81899f6836f22c03da73dae8a664c32e3016b84692c23cddadc95d" +checksum = "05915176d843da9ec5c925e5e2631be9aa61b27430acfdd562e79cae8f559725" dependencies = [ "ahash", "fluent-uri 0.4.1", "getrandom 0.3.4", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "itoa", "micromap", "parking_lot", @@ -4044,7 +3998,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-channel", @@ -4094,22 +4048,44 @@ dependencies = [ [[package]] name = "rig" -version = "0.39.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98e2e8f01c4c5bc23f577983634fa4d5244ffb070ea14c23b1ea5bd406e5cac" +checksum = "2ce03971e6115d30ef53fb3244d06718a4e62bbde82c103065600c09459b989a" dependencies = [ + "rig-agent", + "rig-core", +] + +[[package]] +name = "rig-agent" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0796bbf47d7b76670401aac975bc619cf7fba3482b22dfe14992edaa9c2e04" +dependencies = [ + "async-stream", + "fastrand", + "futures", + "http", + "indexmap 2.14.0", "rig-core", + "schemars 1.2.1", + "serde", + "serde_json", + "thiserror 2.0.19", + "tokio", + "tracing", + "tracing-futures", ] [[package]] name = "rig-core" -version = "0.39.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80a4bc7a93b329c4e1a66d5fd211d79990e7331e3c701f057c29f135f548686d" +checksum = "35f5520515ae8f6851adcbc6fde9eea8e96f657418c062e16c82cd81cce44e8e" dependencies = [ "as-any", "async-stream", - "base64", + "base64 0.22.1", "bytes", "eventsource-stream", "fastrand", @@ -4120,12 +4096,10 @@ dependencies = [ "indexmap 2.14.0", "mime", "mime_guess", - "nanoid", "ordered-float", "pin-project-lite", "reqwest", "rig-derive", - "rmcp", "schemars 1.2.1", "serde", "serde_json", @@ -4138,17 +4112,14 @@ dependencies = [ [[package]] name = "rig-derive" -version = "0.39.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5531bfa887b371eab658a92de7db35003370bbeee208ff5e68bbb81a5ae92d3d" +checksum = "eb868fcebdf3ba425e3afad2e4926bb6d9e1188a856843b00bcee2e15c07424f" dependencies = [ "convert_case 0.11.0", - "deluxe", - "indoc", - "proc-macro-crate 3.5.0", + "proc-macro-crate", "proc-macro2", "quote", - "serde_json", "syn 2.0.119", ] @@ -4168,12 +4139,12 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.8.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59" +checksum = "094c075f6698deef5a657cf4df6b684dff65157d255978b92b552ec22503f17a" dependencies = [ - "async-trait", - "base64", + "base64 0.23.1", + "bytes", "chrono", "futures", "http", @@ -4191,33 +4162,45 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", + "uuid", ] [[package]] name = "rmcp-macros" -version = "1.8.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aad0035b69380782d78ea95b508327e6deaa2235909053e596eea8f27b5e1d5" +checksum = "737d947bcfd946fae6a179a4ef6487be6dcf25c930c2393856b820f1386e52a6" dependencies = [ - "darling 0.23.0", + "darling 0.24.0", "proc-macro2", "quote", "serde_json", - "syn 2.0.119", + "syn 3.0.0", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.19", ] [[package]] name = "rusqlite" -version = "0.31.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" dependencies = [ "bitflags 2.13.1", "fallible-iterator", "fallible-streaming-iterator", - "hashlink 0.9.1", + "hashlink 0.12.1", "libsqlite3-sys", "smallvec 1.15.2", + "sqlite-wasm-rs", ] [[package]] @@ -4275,7 +4258,7 @@ dependencies = [ "scrypt", "sec1", "sha1", - "sha2 0.11.0", + "sha2", "sha3 0.12.0", "signature", "spki", @@ -4523,7 +4506,7 @@ dependencies = [ "cfg-if", "pbkdf2", "salsa20", - "sha2 0.11.0", + "sha2", ] [[package]] @@ -4662,7 +4645,7 @@ version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ - "base64", + "base64 0.22.1", "bs58", "chrono", "hex", @@ -4720,17 +4703,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - [[package]] name = "sha2" version = "0.11.0" @@ -4906,6 +4878,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "sse-stream" version = "0.2.4" @@ -4969,7 +4953,7 @@ dependencies = [ "rand_core 0.10.1", "sec1", "sha1", - "sha2 0.11.0", + "sha2", "signature", "ssh-cipher", "ssh-encoding", @@ -5036,12 +5020,6 @@ dependencies = [ "quote", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -5063,7 +5041,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.119", @@ -5119,15 +5097,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.32.1" +version = "0.39.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" dependencies = [ - "core-foundation-sys", "libc", "memchr", "ntapi", - "windows 0.57.0", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.62.2", ] [[package]] @@ -5299,7 +5278,6 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -5347,7 +5325,6 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", - "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -5362,18 +5339,12 @@ dependencies = [ "indexmap 2.14.0", "serde_core", "serde_spanned", - "toml_datetime 1.1.1+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.4", + "winnow", ] -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -5383,17 +5354,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 0.6.11", - "winnow 0.5.40", -] - [[package]] name = "toml_edit" version = "0.25.13+spec-1.1.0" @@ -5401,9 +5361,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap 2.14.0", - "toml_datetime 1.1.1+spec-1.1.0", + "toml_datetime", "toml_parser", - "winnow 1.0.4", + "winnow", ] [[package]] @@ -5412,7 +5372,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.4", + "winnow", ] [[package]] @@ -6090,16 +6050,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets", -] - [[package]] name = "windows" version = "0.61.3" @@ -6143,26 +6093,14 @@ dependencies = [ "windows-core 0.62.2", ] -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets", -] - [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -6174,8 +6112,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -6203,17 +6141,6 @@ dependencies = [ "windows-threading 0.2.1", ] -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -6225,17 +6152,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -6290,15 +6206,6 @@ dependencies = [ "windows-strings 0.5.1", ] -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -6444,15 +6351,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "1.0.4" @@ -6548,7 +6446,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 1.0.4", + "winnow", "zbus_macros", "zbus_names", "zvariant", @@ -6560,7 +6458,7 @@ version = "5.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.119", @@ -6576,7 +6474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" dependencies = [ "serde", - "winnow 1.0.4", + "winnow", "zvariant", ] @@ -6675,7 +6573,7 @@ dependencies = [ "endi", "enumflags2", "serde", - "winnow 1.0.4", + "winnow", "zvariant_derive", "zvariant_utils", ] @@ -6686,7 +6584,7 @@ version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.119", @@ -6703,5 +6601,5 @@ dependencies = [ "quote", "serde", "syn 2.0.119", - "winnow 1.0.4", + "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 78248183..66288999 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -194,13 +194,13 @@ libc = "0.2" # - Linux: needs a D-Bus backend. `z` = zbus, a pure-Rust client, so we avoid # a system libdbus build/link dependency. [target.'cfg(target_os = "macos")'.dependencies] -notify-rust = { version = "4", default-features = false } +notify-rust = { version = "4.18", default-features = false } [target.'cfg(target_os = "windows")'.dependencies] -notify-rust = { version = "4", default-features = false } +notify-rust = { version = "4.18", default-features = false } [target.'cfg(target_os = "linux")'.dependencies] -notify-rust = { version = "4", default-features = false, features = ["z"] } +notify-rust = { version = "4.18", default-features = false, features = ["z"] } [dependencies] # CVE-2026 audit (2026-05-27): rig's default features pull a @@ -216,24 +216,31 @@ notify-rust = { version = "4", default-features = false, features = ["z"] } # default → rig-lancedb → lance → tantivy → lru 0.12.5 # ← RUSTSEC-2026-0002 (unsound) # -# Dirge only uses rig's MCP transport (`rmcp` feature) and the core -# provider/agent/completion APIs. By disabling default features we -# drop ~370 transitive crates (881→508 deps) and eliminate all 6 -# cargo-audit findings. +# Dirge uses the core provider/completion APIs plus the classic agent +# runtime. By disabling default features we drop ~370 transitive crates +# (881→508 deps) and eliminate all 6 cargo-audit findings. +# +# `agent` is required: 0.41 split the classic runtime into `rig-agent`, +# and without it `rig::agent::AgentBuilder` and the `rig::tool` traits +# vanish. `rmcp` is inherited from before the split and is a candidate +# for removal — nothing here references rig's MCP bridge, and dropping +# it would also drop the duplicate `rmcp 2.2.0` that rig-agent pulls in +# alongside our own `rmcp 1.7`. +# # Pinned to an exact version, not a caret. rig ships breaking API # changes in patch/minor releases (0.37.0 -> 0.37.1 added a required # `Text.additional_params` field), and `cargo install dirge-agent` # resolves without our Cargo.lock, so a caret would let a published # dirge drift onto an incompatible rig and fail to build (GH #616). # Bump this deliberately when adopting a new rig. -rig = { version = "=0.39.0", default-features = false, features = ["rmcp"] } +rig = { version = "=0.41.0", default-features = false, features = ["agent"] } # Explicit dep to pin feature flags. Without this, rig (our only # dep on rig-core) enables `derive` and `reqwest` via its own # feature resolution, but we want `rustls` explicitly declared so # future readers understand the TLS backend choice is deliberate. # cargo-machete reports this as unused — the package.metadata # block at the top of this file suppresses that false positive. -rig-core = { version = "=0.39.0", default-features = false, features = ["reqwest", "derive"] } +rig-core = { version = "=0.41.0", default-features = false, features = ["reqwest", "derive"] } # Macros for emitting `Stream` from async generator-style # code blocks. Used by `agent_loop::rig_stream` to wrap rig's # `StreamingCompletionResponse` into our pi-style `StreamEvent` @@ -246,7 +253,7 @@ async-stream = "0.3" # undercounts emoji (e.g. ✅ takes 2 cells in most terminals); markdown # tables misaligned the right border when cells contained them. unicode-width = "0.2" -rmcp = { version = "1.7", optional = true, default-features = false, features = [ +rmcp = { version = "3.1", optional = true, default-features = false, features = [ "client", "transport-child-process", "transport-streamable-http-client-reqwest", @@ -292,16 +299,16 @@ rustls = { version = "0.23", default-features = false, features = ["ring"] } # sub-panel. `default-features = false` + only the modules we use # keeps the dep small (no disks / networks / processes — just CPU # + memory polling). -sysinfo = { version = "0.32", default-features = false, features = ["system"] } +sysinfo = { version = "0.39", default-features = false, features = ["system"] } dirs = "6" -compact_str = { version = "0.9", features = ["serde"] } +compact_str = { version = "0.10", features = ["serde"] } smallvec = "1" regex = "1" ignore = "0.4.27" pulldown-cmark = "0.13" include_dir = "0.7" -http = "1" -agent-client-protocol = { version = "0.12.0", optional = true } +http = "1.5" +agent-client-protocol = { version = "2.0.0", optional = true } tree-sitter = { version = "0.25", optional = true } tree-sitter-typescript = { version = "0.23", optional = true } tree-sitter-python = { version = "0.25", optional = true } @@ -314,7 +321,7 @@ tree-sitter-java = { version = "0.23", optional = true } tree-sitter-c = { version = "0.24", optional = true } tree-sitter-cpp = { version = "0.23", optional = true } streaming-iterator = { version = "0.1", optional = true } -jsonschema = { version = "0.46", default-features = false, features = ["resolve-http", "resolve-file", "tls-ring"] } +jsonschema = { version = "0.49", default-features = false, features = ["resolve-http", "resolve-file", "tls-ring"] } janetrs = { version = "0.8", optional = true } html2text = "0.17" indexmap = "2" @@ -326,7 +333,7 @@ lsp-types = { version = "0.97", optional = true } tree-sitter-elixir = { version = "0.3.5", optional = true } tree-sitter-sequel = { version = "0.3", optional = true } tree-sitter-dafny = { version = "0.1", optional = true } -rusqlite = { version = "0.31", features = ["bundled"] } +rusqlite = { version = "0.40", features = ["bundled"] } # DAP (Debug Adapter Protocol) — optional feature for driving # debuggers (lldb-dap, dlv, debugpy, node) to fix crashes instead # of sprinkling print statements. Types are hand-rolled in @@ -349,10 +356,10 @@ russh = { version = "0.62.5", default-features = false, features = ["ring"], opt # sha2 + hex: OCI layer digest verification for the microVM sandbox. # Every blob downloaded from the registry is SHA-256 hashed and compared # against the manifest's declared digest before extraction. -sha2 = "0.10" +sha2 = "0.11" hex = { version = "0.4", optional = true } # base64: decode OpenSSH-format public keys for host-key verification. -base64 = "0.22" +base64 = "0.23" # YAML parser for skill frontmatter (replaces hand-rolled parser in # `src/extras/skills/format.rs`). `yaml-rust2` is the maintained fork # of the abandoned `yaml-rust` crate, fully YAML 1.2 compliant. diff --git a/src/agent/agent_loop/heal.rs b/src/agent/agent_loop/heal.rs index af156495..d04f11a1 100644 --- a/src/agent/agent_loop/heal.rs +++ b/src/agent/agent_loop/heal.rs @@ -8,11 +8,19 @@ //! 1. Shrink oversized tool results (char cap, not token cap) //! 2. Fix unpaired tool calls (drops assistant.tool_calls with no //! matching tool responses + stray tool messages) -//! 3. Stamp missing `reasoning_content` on thinking-mode sessions //! //! The rationale: oversized tool results would 400 the next call //! before the user types. Unpaired tool calls would similarly //! fail API validation. +//! +//! Reasonix's third repair — stamping an empty `reasoning_content` onto +//! assistant turns that lack one — is deliberately NOT ported, and the +//! claim that it was has been removed from this list. opencode carries an +//! equivalent for DeepSeek, but only because its own transform lifts +//! reasoning out of the message content, so it has to put an empty one +//! back. Dirge replays the reasoning block as-is (see +//! `rig_stream_factory::provider_rejects_reasoning_echo`), so there is +//! nothing to restore; the DeepSeek and GLM live smoke tests cover it. use serde_json::Value; diff --git a/src/agent/agent_loop/rig_stream.rs b/src/agent/agent_loop/rig_stream.rs index 5660ee51..28f63db8 100644 --- a/src/agent/agent_loop/rig_stream.rs +++ b/src/agent/agent_loop/rig_stream.rs @@ -505,6 +505,14 @@ where }); } } + // rig 0.40 started surfacing provider output items it does + // not model (reasoning_details and friends) instead of + // dropping them. Dirge builds its own content blocks, so + // there is nothing to render — but the chunk still counted + // as forward motion for the stall detector above, which is + // the behaviour we want. Ignore the payload rather than + // guessing at a block type for it. + Ok(StreamedAssistantContent::Unknown(_)) => {} Err(err) => { let error_msg = err.to_string(); use crate::agent::recovery::classify_error; diff --git a/src/agent/agent_loop/rig_stream_factory.rs b/src/agent/agent_loop/rig_stream_factory.rs index d9731d1a..c2d0ee1c 100644 --- a/src/agent/agent_loop/rig_stream_factory.rs +++ b/src/agent/agent_loop/rig_stream_factory.rs @@ -595,7 +595,7 @@ fn value_to_rig_message_for_provider( } "assistant" => { let blocks = value.get("content").and_then(|c| c.as_array())?; - let include_reasoning = !provider_requires_openai_reasoning_ids(provider_name); + let include_reasoning = !provider_rejects_reasoning_echo(provider_name); let synthesize_call_id = provider_requires_openai_call_ids(provider_name); let assistant_contents: Vec = blocks .iter() @@ -675,7 +675,20 @@ fn value_to_rig_message(value: &Value) -> Option { value_to_rig_message_for_provider(value, None, None) } -fn provider_requires_openai_reasoning_ids(provider_name: Option<&str>) -> bool { +/// Providers that must NOT receive an echoed-back assistant reasoning block. +/// +/// Only `openai` qualifies: its Responses API wants reasoning items keyed by +/// the encrypted ids it issued, which dirge does not retain, so a bare block is +/// rejected and there is no field to rename it to. +/// +/// Every other backend keeps its reasoning. Dropping it would lose the model's +/// own chain of thought from the next turn's context, so a backend that spells +/// the field differently is handled by renaming at the wire boundary instead — +/// see `CompressingHttpClient::rewrite_provider_quirks`, which moves +/// `reasoning_content` to `reasoning` for Cerebras. DeepSeek requires the echo +/// on tool-call turns, and llama.cpp/LocalAI chat templates read +/// `message.reasoning_content` back out of the assistant turn. +fn provider_rejects_reasoning_echo(provider_name: Option<&str>) -> bool { matches!(provider_name, Some(provider) if provider.eq_ignore_ascii_case("openai")) } @@ -1127,6 +1140,49 @@ mod tests { } } + /// GH #745 fallout. Cerebras streams reasoning as `delta.reasoning` — the + /// same non-standard field LocalAI uses. rig 0.39 discarded it, so dirge + /// never had a thinking block to replay; rig 0.40+ keeps it, and replaying + /// it as `reasoning_content` makes Cerebras 400 with + /// `property 'messages.N.assistant.reasoning_content' is unsupported`. + /// + /// The block must still be BUILT here — dropping it would lose the model's + /// reasoning from the next turn's context. The field is renamed at the wire + /// boundary instead (`CompressingHttpClient::rewrite_provider_quirks`). + /// `h7_cerebras_tool_dispatch_completes_round_trip` covers the live round + /// trip; this pins the half that needs no API key. + #[test] + fn reasoning_echo_is_preserved_for_every_backend_but_openai() { + let v = serde_json::json!({ + "role": "assistant", + "content": [ + {"type": "thinking", "text": "let me think"}, + {"type": "text", "text": "the answer"}, + ], + }); + for provider in ["cerebras", "deepseek", "custom", "ollama", "glm"] { + let msg = value_to_rig_message_for_provider(&v, Some(provider), None) + .unwrap_or_else(|| panic!("{provider} must keep the reasoning block")); + match msg { + Message::Assistant { content, .. } => { + assert!( + content + .iter() + .any(|c| matches!(c, AssistantContent::Reasoning(_))), + "{provider} must retain reasoning", + ); + assert!( + content + .iter() + .any(|c| matches!(c, AssistantContent::Text(_))), + "{provider} must retain the assistant's text", + ); + } + _ => panic!("expected Assistant"), + } + } + } + #[test] fn openai_assistant_thinking_only_is_skipped() { let v = serde_json::json!({ diff --git a/src/agent/agent_loop/rig_tool.rs b/src/agent/agent_loop/rig_tool.rs index 731e4e3b..a082562d 100644 --- a/src/agent/agent_loop/rig_tool.rs +++ b/src/agent/agent_loop/rig_tool.rs @@ -31,7 +31,9 @@ use std::pin::Pin; -use rig::tool::{ToolDyn, ToolError}; +use rig::completion::ToolDefinition; +use rig::tool::{IntoToolOutput, PortableTool}; +use rig::wasm_compat::WasmBoxedFuture; use serde_json::Value; use super::result::LoopToolResult; @@ -42,6 +44,72 @@ use super::types::ToolExecutionMode; #[cfg(test)] use std::sync::Arc; +/// Object-safe erased tool surface, replacing the removed +/// `rig::tool::ToolDyn` (rig 0.41 made `ErasedTool` private and +/// exposed only the concrete `DynamicTool`). dirge drives its own +/// agent loop — no rig `ToolContext`/`ToolSet` — so it erases typed +/// tools through this trait instead: any `PortableTool` implements +/// it for free (the same blanket rig 0.39 provided for `ToolDyn`), +/// and hand-rolled tools (MCP, hook-decorated) implement it +/// directly. +pub trait DynTool: Send + Sync { + fn name(&self) -> String; + fn definition(&self) -> WasmBoxedFuture<'_, ToolDefinition>; + fn call(&self, args: String) -> WasmBoxedFuture<'_, Result>; +} + +/// Error surface for an erased tool call, mirroring rig 0.39's +/// `ToolError` (removed in rig 0.41). +#[derive(Debug, thiserror::Error)] +pub enum DynToolError { + #[error("{0}")] + ToolCallError(Box), + #[error("{0}")] + JsonError(#[from] serde_json::Error), +} + +impl DynTool for T +where + T: PortableTool + Send + Sync + 'static, +{ + fn name(&self) -> String { + T::NAME.to_string() + } + + fn definition(&self) -> WasmBoxedFuture<'_, ToolDefinition> { + Box::pin(async move { + ToolDefinition { + name: T::NAME.to_string(), + description: self.description(), + parameters: self.parameters(), + } + }) + } + + fn call(&self, args: String) -> WasmBoxedFuture<'_, Result> { + Box::pin(async move { + // Replicates rig 0.39's blanket `ToolDyn::call`: a bare + // `null` args payload falls back to `{}` so tools whose + // args are all optional still deserialize. + let parsed = match serde_json::from_str::(&args) { + Ok(args) => args, + Err(err) if args.trim() == "null" => serde_json::from_str("{}").map_err(|_| err)?, + Err(err) => return Err(err.into()), + }; + let output = ::call(self, parsed) + .await + .map_err(|e| DynToolError::ToolCallError(Box::new(e)))?; + // Replicates 0.39's output shaping: a plain string comes + // out unquoted; anything else renders as its JSON text. + // `ToolOutput::render` applies exactly that rule. + let tool_output = output + .into_tool_output() + .map_err(|e| DynToolError::ToolCallError(Box::new(e)))?; + Ok(tool_output.render()) + }) + } +} + /// Wraps a `Box` and exposes it as a `LoopTool`. /// /// Built via [`RigToolAdapter::new`] which eagerly resolves the @@ -54,7 +122,7 @@ use std::sync::Arc; /// construction time (Reasonix tools.ts:36-38). The LLM sees flat /// dot-notation keys; `prepare_arguments` re-nests them at dispatch. pub struct RigToolAdapter { - inner: Box, + inner: Box, name: String, description: String, parameters: Value, @@ -89,8 +157,8 @@ impl RigToolAdapter { /// leaves), flattens it and stores the flat variant so /// `prepare_arguments` can re-nest at dispatch (Reasonix /// tools.ts:36-38). - pub async fn new(inner: Box) -> Self { - let def = inner.definition(String::new()).await; + pub async fn new(inner: Box) -> Self { + let def = inner.definition().await; let flat_parameters = match analyze_schema(&def.parameters) { FlattenDecision { should_flatten: true, @@ -121,7 +189,7 @@ impl RigToolAdapter { /// Production callers should use `new`. #[cfg(test)] fn from_parts( - inner: Box, + inner: Box, name: String, description: String, parameters: Value, @@ -221,7 +289,7 @@ impl LoopTool for RigToolAdapter { /// repair layer; this function handles runtime tool errors. If a /// schema error leaks through (defense-in-depth), wrap it in a /// model-readable retry hint rather than leaking raw serde diagnostics. -fn format_tool_error(err: ToolError) -> String { +fn format_tool_error(err: DynToolError) -> String { let raw = err.to_string(); if raw.contains("missing field") || raw.contains("expected") || raw.contains("invalid type") { format!( @@ -238,7 +306,7 @@ fn format_tool_error(err: ToolError) -> String { mod tests { use super::*; use rig::completion::ToolDefinition; - use rig::tool::Tool; + use rig::tool::PortableTool; use serde::{Deserialize, Serialize}; /// Mock rig tool that echoes its input back. Mirrors the @@ -258,24 +326,24 @@ mod tests { Generic(String), } - impl Tool for EchoTool { + impl PortableTool for EchoTool { const NAME: &'static str = "echo"; type Error = EchoError; type Args = EchoArgs; type Output = String; - async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: "echo".to_string(), - description: "Echo the input back".to_string(), - parameters: serde_json::json!({ - "type": "object", - "properties": { - "value": {"type": "string"} - }, - "required": ["value"] - }), - } + fn description(&self) -> String { + "Echo the input back".to_string() + } + + fn parameters(&self) -> serde_json::Value { + serde_json::json!({ + "type": "object", + "properties": { + "value": {"type": "string"} + }, + "required": ["value"] + }) } async fn call(&self, args: Self::Args) -> Result { @@ -287,18 +355,18 @@ mod tests { #[derive(Debug, Clone)] struct FailingTool; - impl Tool for FailingTool { + impl PortableTool for FailingTool { const NAME: &'static str = "failing"; type Error = EchoError; type Args = EchoArgs; type Output = String; - async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: "failing".to_string(), - description: "Always fails".to_string(), - parameters: serde_json::json!({"type": "object"}), - } + fn description(&self) -> String { + "Always fails".to_string() + } + + fn parameters(&self) -> serde_json::Value { + serde_json::json!({"type": "object"}) } async fn call(&self, _args: Self::Args) -> Result { @@ -444,8 +512,8 @@ mod tests { /// returned by each path must match. #[tokio::test] async fn adapter_matches_rig_path_for_real_dirge_tool() { + use super::DynTool; use crate::agent::tools::ReadTool; - use rig::tool::ToolDyn; // Set up a temp file with known content. Reuses the // same TestDir pattern as fs_atomic tests for cleanup. @@ -465,7 +533,7 @@ mod tests { // both traits are in scope. let tool_a = ReadTool::new(None, None); let rig_args = serde_json::json!({"path": path_str}).to_string(); - let rig_output = ::call(&tool_a, rig_args) + let rig_output = ::call(&tool_a, rig_args) .await .expect("rig direct call should succeed"); diff --git a/src/agent/builder/agent_inner.rs b/src/agent/builder/agent_inner.rs index 709f5aaa..4e953097 100644 --- a/src/agent/builder/agent_inner.rs +++ b/src/agent/builder/agent_inner.rs @@ -48,6 +48,9 @@ pub async fn build_agent_inner( // caller (provider::build_agent) can attach it to AnyAgent for // session-lifecycle hook dispatch. `None` when load failed. Option>, + // rig 0.41 made `Agent::preamble` private, so hand the assembled + // system prompt back instead of reading it off the built agent. + String, ) { // The `plan_file`-keyed gate on edit/write/apply_patch was // removed: prompt-level tool restrictions now live in the @@ -316,7 +319,7 @@ pub async fn build_agent_inner( // `--no-tools`, collects MCP/semantic tools, and applies plugin hooks). // Attaching them here too only duplicated every tool construction and // double-collected MCP tools at startup [dirge-tfip]. - (builder.build(), ToolCache::new(), memory_store) + (builder.build(), ToolCache::new(), memory_store, preamble) } /// Wall-clock bound for the blocking SQLite loads in `build_agent_inner` diff --git a/src/agent/builder/loop_tools.rs b/src/agent/builder/loop_tools.rs index f7a1cd1f..2b625071 100644 --- a/src/agent/builder/loop_tools.rs +++ b/src/agent/builder/loop_tools.rs @@ -264,7 +264,7 @@ pub async fn build_rooted_writer_tools( async fn wrap(inner: T, mode: Option) -> Arc where - T: rig::tool::ToolDyn + 'static, + T: crate::agent::agent_loop::rig_tool::DynTool + 'static, { let adapter = RigToolAdapter::new(Box::new(inner)).await; Arc::new(match mode { @@ -505,7 +505,7 @@ pub async fn build_loop_tools( // is async (RigToolAdapter::new resolves it eagerly). async fn wrap(inner: T, mode: Option) -> Arc where - T: rig::tool::ToolDyn + 'static, + T: crate::agent::agent_loop::rig_tool::DynTool + 'static, { let adapter = RigToolAdapter::new(Box::new(inner)).await; let adapter = match mode { diff --git a/src/agent/builder/reminder_tests.rs b/src/agent/builder/reminder_tests.rs index 160cbec3..2610ec7b 100644 --- a/src/agent/builder/reminder_tests.rs +++ b/src/agent/builder/reminder_tests.rs @@ -536,10 +536,10 @@ async fn build_agent_inner_emits_assembled_preamble() { let client = openai::Client::new("test-key").expect("openai client builds"); let model = client.completion_model("gpt-4o"); - let (agent, _cache, _provider) = + let (_agent, _cache, _provider, agent_preamble) = build_agent_inner(model, &cli, &cfg, &context, "openai", "gpt-4o").await; - let preamble = agent.preamble.unwrap_or_default(); + let preamble = agent_preamble; // SKILLS_GUIDANCE markers (dirge-xxun). assert!( @@ -677,14 +677,14 @@ async fn preamble_lists_global_tier_skills() { let prev_home = std::env::var_os("HOME"); // SAFETY: guarded by HOME_LOCK; restored before the lock drops. unsafe { std::env::set_var("HOME", &home) }; - let (agent, _cache, _provider) = + let (_agent, _cache, _provider, agent_preamble) = build_agent_inner(model, &cli, &cfg, &context, "openai", "gpt-4o").await; match prev_home { Some(h) => unsafe { std::env::set_var("HOME", h) }, None => unsafe { std::env::remove_var("HOME") }, } - let preamble = agent.preamble.unwrap_or_default(); + let preamble = agent_preamble; assert!( preamble.contains("global-preamble-skill"), "preamble must advertise a skill from the global ~/.dirge/skills tier; got:\n{preamble}" @@ -726,25 +726,20 @@ async fn steering_fragment_tracks_active_model_not_cli() { // though the openai client/model and the CLI default are not DeepSeek. let ctx = empty_ctx(); let model = client.completion_model("gpt-4o"); - let (agent, _c, _p) = + let (_agent, _c, _p, agent_preamble) = build_agent_inner(model, &cli, &cfg, &ctx, "deepseek", "deepseek-v4-pro").await; assert!( - agent - .preamble - .unwrap_or_default() - .contains("Plan-Execute-Verify"), + agent_preamble.contains("Plan-Execute-Verify"), "DeepSeek-chat active model must inject the steering fragment" ); // Active model = a non-DeepSeek model → fragment absent. let ctx = empty_ctx(); let model = client.completion_model("gpt-4o"); - let (agent, _c, _p) = build_agent_inner(model, &cli, &cfg, &ctx, "openai", "gpt-4o").await; + let (_agent, _c, _p, agent_preamble) = + build_agent_inner(model, &cli, &cfg, &ctx, "openai", "gpt-4o").await; assert!( - !agent - .preamble - .unwrap_or_default() - .contains("Plan-Execute-Verify"), + !agent_preamble.contains("Plan-Execute-Verify"), "non-DeepSeek active model must NOT inject the steering fragment" ); } diff --git a/src/agent/review.rs b/src/agent/review.rs index 9787f320..4387d92e 100644 --- a/src/agent/review.rs +++ b/src/agent/review.rs @@ -763,11 +763,10 @@ mod tests { .build() .expect("openai client"); let model = client.completion_model("gpt-4o"); - let inner_agent = rig::agent::AgentBuilder::new(model).build(); let provider = Arc::new(RecordingEndProvider::default()); let provider_dyn: Arc = provider.clone(); let agent = AnyAgent::new( - AnyAgentInner::OpenAI(inner_agent), + AnyAgentInner::OpenAI(model), ToolCache::new(), std::time::Duration::from_secs(300), Vec::new(), @@ -871,11 +870,10 @@ mod tests { .build() .unwrap(); let model = client.completion_model("gpt-4o"); - let inner_agent = rig::agent::AgentBuilder::new(model).build(); let provider = Arc::new(RecordingSwitchProvider::default()); let provider_dyn: Arc = provider.clone(); let agent = AnyAgent::new( - AnyAgentInner::OpenAI(inner_agent), + AnyAgentInner::OpenAI(model), ToolCache::new(), std::time::Duration::from_secs(300), Vec::new(), @@ -905,9 +903,8 @@ mod tests { .build() .unwrap(); let model = client.completion_model("gpt-4o"); - let inner_agent = rig::agent::AgentBuilder::new(model).build(); let agent = AnyAgent::new( - AnyAgentInner::OpenAI(inner_agent), + AnyAgentInner::OpenAI(model), ToolCache::new(), std::time::Duration::from_secs(300), Vec::new(), @@ -929,9 +926,8 @@ mod tests { .build() .unwrap(); let model = client.completion_model("gpt-4o"); - let inner_agent = rig::agent::AgentBuilder::new(model).build(); let agent = AnyAgent::new( - AnyAgentInner::OpenAI(inner_agent), + AnyAgentInner::OpenAI(model), ToolCache::new(), std::time::Duration::from_secs(300), Vec::new(), diff --git a/src/agent/tools/apply_patch.rs b/src/agent/tools/apply_patch.rs index 3da5656a..9fb9db77 100644 --- a/src/agent/tools/apply_patch.rs +++ b/src/agent/tools/apply_patch.rs @@ -1,5 +1,4 @@ -use rig::completion::ToolDefinition; -use rig::tool::Tool; +use rig::tool::PortableTool; use serde::Deserialize; use std::path::Path; @@ -197,62 +196,62 @@ async fn apply_rename(path: &str, new_path: &str) -> Result { Ok(format!("renamed {} -> {}", path, new_path)) } -impl Tool for ApplyPatchTool { +impl PortableTool for ApplyPatchTool { const NAME: &'static str = "apply_patch"; type Error = ToolError; type Args = ApplyPatchArgs; type Output = String; - async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: "apply_patch".to_string(), - description: crate::agent::agent_loop::tool_input_repair::with_contract_hint( - "apply_patch", - "Apply multiple file operations in a single call. Supports create, update (by exact text match), delete, and rename. Operations execute in order and stop on first failure — prior operations that succeeded remain applied.", - ), - parameters: serde_json::json!({ - "type": "object", - "properties": { - "operations": { - "type": "array", - "description": "Ordered list of file operations to execute", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["create", "update", "delete", "rename"], - "description": "The type of operation" - }, - "path": { - "type": "string", - "description": "Target file path" - }, - "content": { - "type": "string", - "description": "File content (required for create)" - }, - "old_text": { - "type": "string", - "description": "Exact text to find and replace (required for update)" - }, - "new_text": { - "type": "string", - "description": "Replacement text (required for update)" - }, - "new_path": { - "type": "string", - "description": "New file path (required for rename)" - } + fn description(&self) -> String { + crate::agent::agent_loop::tool_input_repair::with_contract_hint( + "apply_patch", + "Apply multiple file operations in a single call. Supports create, update (by exact text match), delete, and rename. Operations execute in order and stop on first failure — prior operations that succeeded remain applied.", + ) + } + + fn parameters(&self) -> serde_json::Value { + serde_json::json!({ + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Ordered list of file operations to execute", + "items": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["create", "update", "delete", "rename"], + "description": "The type of operation" }, - "required": ["action", "path"] - } + "path": { + "type": "string", + "description": "Target file path" + }, + "content": { + "type": "string", + "description": "File content (required for create)" + }, + "old_text": { + "type": "string", + "description": "Exact text to find and replace (required for update)" + }, + "new_text": { + "type": "string", + "description": "Replacement text (required for update)" + }, + "new_path": { + "type": "string", + "description": "New file path (required for rename)" + } + }, + "required": ["action", "path"] } - }, - "required": ["operations"] - }), - } + } + }, + "required": ["operations"] + }) } async fn call(&self, args: ApplyPatchArgs) -> Result { @@ -597,7 +596,7 @@ mod tests { #[tokio::test] async fn test_definition_has_correct_name() { let tool = ApplyPatchTool::new(None, None); - let def = tool.definition(String::new()).await; + let def = rig::tool::tool_definition(&tool); assert_eq!(def.name, "apply_patch"); } diff --git a/src/agent/tools/bash/mod.rs b/src/agent/tools/bash/mod.rs index 46cecd0f..562ead95 100644 --- a/src/agent/tools/bash/mod.rs +++ b/src/agent/tools/bash/mod.rs @@ -1,5 +1,4 @@ -use rig::completion::ToolDefinition; -use rig::tool::Tool; +use rig::tool::PortableTool; pub(crate) mod check; pub(crate) mod exec; @@ -87,31 +86,31 @@ impl BashTool { } } -impl Tool for BashTool { +impl PortableTool for BashTool { const NAME: &'static str = "bash"; type Error = ToolError; type Args = BashArgs; type Output = String; - async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: "bash".to_string(), - description: with_contract_hint( + fn description(&self) -> String { + with_contract_hint( "bash", &("Execute a bash command in the current working directory. Returns stdout and stderr.".to_owned() + cfg!(feature = "experimental-ui-computer-use").then_some("\n\nDesktop automation: prefix commands with `computer:` to control the desktop GUI. Actions: `computer:open_url ` (opens in browser), `computer:screenshot` (captures screen), `computer:type `, `computer:key `, `computer:click