Close remaining spec compliance gaps in coding-agent-loop

- Abort now transitions to CLOSED state and returns Err(Aborted)
- Closed sessions no longer emit SessionStart before rejecting input
- Add set_reasoning_effort() for mid-session reasoning effort changes
- Fix spawn_agent truncation limit from 30k to spec-required 20k
- Add JSON Schema validation of tool arguments before execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-02-20 14:05:16 -04:00
parent 82572678c0
commit 010d09a03a
5 changed files with 884 additions and 19 deletions

600
Cargo.lock generated
View file

@ -2,6 +2,20 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"getrandom 0.3.4",
"once_cell",
"serde",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
@ -11,6 +25,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anstream"
version = "0.6.21"
@ -105,18 +125,61 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "borrow-or-share"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
[[package]]
name = "bstr"
version = "1.12.1"
@ -134,6 +197,12 @@ version = "3.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4"
[[package]]
name = "bytecount"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]]
name = "bytes"
version = "1.11.1"
@ -147,9 +216,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "1.0.4"
@ -196,16 +273,26 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "cmake"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
dependencies = [
"cc",
]
[[package]]
name = "coding-agent-loop"
version = "0.1.0"
dependencies = [
"async-trait",
"futures",
"jsonschema",
"libc",
"serde",
"serde_json",
"thiserror",
"thiserror 2.0.18",
"tokio",
"unified-llm",
"uuid",
@ -217,6 +304,16 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -243,6 +340,12 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "data-encoding"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "difflib"
version = "0.4.0"
@ -266,6 +369,21 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "email_address"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
dependencies = [
"serde",
]
[[package]]
name = "encoding_rs"
version = "0.8.35"
@ -291,6 +409,17 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "fancy-regex"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
dependencies = [
"bit-set",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "fastrand"
version = "2.3.0"
@ -312,6 +441,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "fluent-uri"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
dependencies = [
"borrow-or-share",
"ref-cast",
"serde",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -324,6 +464,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "foreign-types"
version = "0.3.2"
@ -348,6 +494,22 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fraction"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7"
dependencies = [
"lazy_static",
"num",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures"
version = "0.3.32"
@ -447,6 +609,20 @@ dependencies = [
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"wasip2",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.4.1"
@ -485,7 +661,7 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
"foldhash 0.1.5",
]
[[package]]
@ -493,6 +669,11 @@ name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "heck"
@ -766,6 +947,38 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jni"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"log",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.85"
@ -776,6 +989,41 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jsonschema"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6103dcd3815c9bd04239a6d0343dac2b7a18ee260e800d0a6e3eb5b9333504fb"
dependencies = [
"ahash",
"bytecount",
"data-encoding",
"email_address",
"fancy-regex",
"fraction",
"getrandom 0.3.4",
"idna",
"itoa",
"num-cmp",
"num-traits",
"percent-encoding",
"referencing",
"regex",
"regex-syntax",
"reqwest 0.13.2",
"rustls",
"serde",
"serde_json",
"unicode-general-category",
"uuid-simd",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leb128fmt"
version = "0.1.0"
@ -861,6 +1109,76 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-cmp"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -926,6 +1244,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "outref"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "parking_lot"
version = "0.12.5"
@ -1094,6 +1418,41 @@ dependencies = [
"bitflags",
]
[[package]]
name = "ref-cast"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "referencing"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a76b0c93f3dd49da2900cfb3c6b883602471a0e3e34b1578a3310ca1269d418"
dependencies = [
"ahash",
"fluent-uri",
"getrandom 0.3.4",
"hashbrown 0.16.1",
"parking_lot",
"percent-encoding",
"serde_json",
]
[[package]]
name = "regex"
version = "1.12.3"
@ -1166,6 +1525,45 @@ dependencies = [
"web-sys",
]
[[package]]
name = "reqwest"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "ring"
version = "0.17.14"
@ -1199,6 +1597,7 @@ version = "0.23.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
dependencies = [
"aws-lc-rs",
"once_cell",
"rustls-pki-types",
"rustls-webpki",
@ -1206,6 +1605,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.0"
@ -1215,12 +1626,40 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@ -1238,6 +1677,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.28"
@ -1464,13 +1912,33 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.18",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -1636,6 +2104,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "unicode-general-category"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@ -1660,10 +2134,10 @@ dependencies = [
"futures",
"http",
"rand",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"thiserror",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tokio-util",
@ -1726,12 +2200,34 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "uuid-simd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"vsimd",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vsimd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]]
name = "wait-timeout"
version = "0.2.1"
@ -1741,6 +2237,16 @@ dependencies = [
"libc",
]
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
@ -1890,6 +2396,24 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "windows-link"
version = "0.2.1"
@ -1925,6 +2449,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -1952,6 +2485,21 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@ -1985,6 +2533,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@ -1997,6 +2551,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@ -2009,6 +2569,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@ -2033,6 +2599,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@ -2045,6 +2617,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@ -2057,6 +2635,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@ -2069,6 +2653,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"

View file

@ -31,3 +31,4 @@ base64 = "0.22"
bytes = "1"
tokio-util = "0.7"
clap = { version = "4", features = ["derive"] }
jsonschema = "0.42"

View file

@ -18,6 +18,7 @@ tokio.workspace = true
uuid.workspace = true
futures.workspace = true
async-trait.workspace = true
jsonschema.workspace = true
[target.'cfg(unix)'.dependencies]
libc = "0.2"

View file

@ -107,11 +107,19 @@ impl Session {
self.state = SessionState::Closed;
}
pub fn set_reasoning_effort(&mut self, effort: Option<String>) {
self.config.reasoning_effort = effort;
}
pub fn history(&self) -> &History {
&self.history
}
pub async fn process_input(&mut self, input: &str) -> Result<(), AgentError> {
if self.state == SessionState::Closed {
return Err(AgentError::SessionClosed);
}
self.event_emitter.emit(
EventKind::SessionStart,
self.id.clone(),
@ -194,7 +202,8 @@ impl Session {
// Check abort flag
if self.abort_flag.load(Ordering::SeqCst) {
break;
self.state = SessionState::Closed;
return Err(AgentError::Aborted);
}
// Build request
@ -343,6 +352,19 @@ impl Session {
let registry = self.provider_profile.tool_registry();
match registry.get(tool_name) {
Some(registered_tool) => {
// Validate arguments against schema
if let Err(validation_error) =
validate_tool_args(&registered_tool.definition.parameters, arguments)
{
return ToolResult {
tool_call_id: tool_call_id.to_string(),
content: serde_json::json!(validation_error),
is_error: true,
image_data: None,
image_media_type: None,
};
}
let executor = &registered_tool.executor;
match executor(arguments.clone(), self.execution_env.clone()).await {
Ok(output) => ToolResult {
@ -429,21 +451,35 @@ impl Session {
let registry = profile.tool_registry();
let result = match registry.get(&tc.name) {
Some(registered_tool) => {
match (registered_tool.executor)(tc.arguments.clone(), env).await {
Ok(output) => ToolResult {
// Validate arguments against schema
if let Err(validation_error) = validate_tool_args(
&registered_tool.definition.parameters,
&tc.arguments,
) {
ToolResult {
tool_call_id: tc.id.clone(),
content: serde_json::json!(output),
is_error: false,
image_data: None,
image_media_type: None,
},
Err(err) => ToolResult {
tool_call_id: tc.id.clone(),
content: serde_json::json!(err),
content: serde_json::json!(validation_error),
is_error: true,
image_data: None,
image_media_type: None,
},
}
} else {
match (registered_tool.executor)(tc.arguments.clone(), env).await {
Ok(output) => ToolResult {
tool_call_id: tc.id.clone(),
content: serde_json::json!(output),
is_error: false,
image_data: None,
image_media_type: None,
},
Err(err) => ToolResult {
tool_call_id: tc.id.clone(),
content: serde_json::json!(err),
is_error: true,
image_data: None,
image_media_type: None,
},
}
}
}
None => ToolResult {
@ -625,6 +661,29 @@ fn is_auth_error(err: &SdkError) -> bool {
)
}
fn validate_tool_args(schema: &serde_json::Value, args: &serde_json::Value) -> Result<(), String> {
// Skip validation for empty/trivial schemas
if schema.is_null()
|| (schema.is_object() && schema.as_object().map_or(true, |o| o.is_empty()))
{
return Ok(());
}
let validator = jsonschema::validator_for(schema)
.map_err(|e| format!("Invalid tool schema: {e}"))?;
let errors: Vec<String> = validator.iter_errors(args).map(|e| e.to_string()).collect();
if errors.is_empty() {
Ok(())
} else {
Err(format!(
"Tool argument validation failed: {}",
errors.join("; ")
))
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -1286,7 +1345,11 @@ mod tests {
let mut session = make_session_with_tools_and_config(responses, registry, config).await;
// Set abort before processing
session.abort();
session.process_input("Do something").await.unwrap();
let result = session.process_input("Do something").await;
// Should return Aborted error and transition to Closed
assert!(matches!(result, Err(AgentError::Aborted)));
assert_eq!(session.state(), SessionState::Closed);
// Should have stopped immediately: User turn only, no LLM call
let turns = session.history().turns();
@ -1294,6 +1357,63 @@ mod tests {
assert!(matches!(&turns[0], Turn::User { .. }));
}
#[tokio::test]
async fn abort_transitions_to_closed() {
let abort_flag = Arc::new(AtomicBool::new(false));
let abort_flag_for_tool = abort_flag.clone();
// Tool that sets the abort flag when executed
let abort_tool = RegisteredTool {
definition: ToolDefinition {
name: "set_abort".into(),
description: "Sets abort flag".into(),
parameters: serde_json::json!({"type": "object"}),
},
executor: Arc::new(move |_args, _env| {
let flag = abort_flag_for_tool.clone();
Box::pin(async move {
flag.store(true, Ordering::SeqCst);
Ok("done".to_string())
})
}),
};
let mut registry = ToolRegistry::new();
registry.register(abort_tool);
let responses = vec![
tool_call_response("set_abort", "call_1", serde_json::json!({})),
text_response("Should not reach this"),
];
let provider = Arc::new(MockLlmProvider::new(responses));
let client = make_client(provider).await;
let profile = Arc::new(TestProfile::with_tools(registry));
let env = Arc::new(MemoryExecutionEnvironment::new());
let config = SessionConfig {
enable_loop_detection: false,
..Default::default()
};
let mut session = Session::new(client, profile, env, config);
// Wire the session's abort_flag to our shared one
session.abort_flag = abort_flag;
let result = session.process_input("Do something").await;
// Should return Aborted error and transition to Closed
assert!(matches!(result, Err(AgentError::Aborted)));
assert_eq!(session.state(), SessionState::Closed);
// Should have processed: User + Assistant(tool_call) + ToolResults = 3 turns
// The tool set the abort flag, so the loop breaks before the next LLM call
let turns = session.history().turns();
assert_eq!(turns.len(), 3);
assert!(matches!(&turns[0], Turn::User { .. }));
assert!(matches!(&turns[1], Turn::Assistant { tool_calls, .. } if tool_calls.len() == 1));
assert!(matches!(&turns[2], Turn::ToolResults { .. }));
}
#[tokio::test]
async fn auth_error_closes_session() {
let error_provider = Arc::new(MockErrorProvider {
@ -1347,6 +1467,26 @@ mod tests {
assert!(matches!(result.unwrap_err(), AgentError::SessionClosed));
}
#[tokio::test]
async fn closed_session_does_not_emit_session_start() {
let mut session = make_session(vec![]).await;
session.close();
let mut rx = session.subscribe();
let result = session.process_input("Hello").await;
assert!(matches!(result, Err(AgentError::SessionClosed)));
// No SessionStart event should have been emitted
let mut events = Vec::new();
while let Ok(event) = rx.try_recv() {
events.push(event.kind.clone());
}
assert!(
!events.contains(&EventKind::SessionStart),
"SessionStart should not be emitted for a closed session"
);
}
// --- Parallel execution support ---
struct ParallelTestProfile {
@ -1545,6 +1685,56 @@ mod tests {
assert!(found_warning);
}
// --- Capturing LLM Provider (captures reasoning_effort from request) ---
struct CapturingLlmProvider {
captured_effort: Arc<Mutex<Option<Option<String>>>>,
}
impl CapturingLlmProvider {
fn new(captured_effort: Arc<Mutex<Option<Option<String>>>>) -> Self {
Self { captured_effort }
}
}
#[async_trait]
impl ProviderAdapter for CapturingLlmProvider {
fn name(&self) -> &str {
"mock"
}
async fn complete(&self, request: &Request) -> Result<Response, SdkError> {
*self.captured_effort.lock().unwrap() = Some(request.reasoning_effort.clone());
Ok(text_response("captured"))
}
async fn stream(
&self,
_request: &Request,
) -> Result<StreamEventStream, SdkError> {
Err(SdkError::Configuration {
message: "streaming not supported in mock".into(),
})
}
}
#[tokio::test]
async fn set_reasoning_effort_mid_session() {
let captured_effort: Arc<Mutex<Option<Option<String>>>> = Arc::new(Mutex::new(None));
let provider = Arc::new(CapturingLlmProvider::new(captured_effort.clone()));
let client = make_client(provider).await;
let profile = Arc::new(TestProfile::new());
let env = Arc::new(MemoryExecutionEnvironment::new());
let mut session = Session::new(client, profile, env, SessionConfig::default());
// Default reasoning_effort is None
session.set_reasoning_effort(Some("high".to_string()));
session.process_input("test").await.unwrap();
let effort = captured_effort.lock().unwrap().clone();
assert_eq!(effort, Some(Some("high".to_string())));
}
#[tokio::test]
async fn context_window_no_warning_under_threshold() {
let responses = vec![text_response("OK")];
@ -1570,4 +1760,85 @@ mod tests {
}
assert!(!found_warning);
}
#[tokio::test]
async fn invalid_tool_args_returns_validation_error() {
let mut registry = ToolRegistry::new();
registry.register(RegisteredTool {
definition: ToolDefinition {
name: "strict_tool".into(),
description: "Tool with required params".into(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"text": {"type": "string"}
},
"required": ["text"]
}),
},
executor: Arc::new(|_args, _env| {
Box::pin(async move { Ok("should not reach".to_string()) })
}),
});
let responses = vec![
tool_call_response("strict_tool", "call_1", serde_json::json!({})),
text_response("Done"),
];
let mut session = make_session_with_tools(responses, registry).await;
session.process_input("Use strict tool").await.unwrap();
let turns = session.history().turns();
if let Turn::ToolResults { results, .. } = &turns[2] {
assert!(results[0].is_error);
let content_str = results[0].content.to_string();
assert!(
content_str.contains("text") && content_str.contains("required"),
"Expected validation error mentioning 'text' and 'required', got: {content_str}"
);
} else {
panic!("Expected ToolResults turn at index 2");
}
}
#[tokio::test]
async fn valid_tool_args_passes_validation() {
let mut registry = ToolRegistry::new();
registry.register(RegisteredTool {
definition: ToolDefinition {
name: "strict_tool".into(),
description: "Tool with required params".into(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"text": {"type": "string"}
},
"required": ["text"]
}),
},
executor: Arc::new(|_args, _env| {
Box::pin(async move { Ok("tool executed".to_string()) })
}),
});
let responses = vec![
tool_call_response(
"strict_tool",
"call_1",
serde_json::json!({"text": "hello"}),
),
text_response("Done"),
];
let mut session = make_session_with_tools(responses, registry).await;
session.process_input("Use strict tool").await.unwrap();
let turns = session.history().turns();
if let Turn::ToolResults { results, .. } = &turns[2] {
assert!(!results[0].is_error);
} else {
panic!("Expected ToolResults turn at index 2");
}
}
}

View file

@ -16,7 +16,7 @@ fn default_char_limits() -> HashMap<&'static str, usize> {
m.insert("edit_file", 10_000);
m.insert("write_file", 1_000);
m.insert("apply_patch", 10_000);
m.insert("spawn_agent", 30_000);
m.insert("spawn_agent", 20_000);
m
}
@ -219,6 +219,8 @@ mod tests {
assert_eq!(limits.get("glob"), Some(&20_000));
assert_eq!(limits.get("edit_file"), Some(&10_000));
assert_eq!(limits.get("write_file"), Some(&1_000));
assert_eq!(limits.get("apply_patch"), Some(&10_000));
assert_eq!(limits.get("spawn_agent"), Some(&20_000));
}
#[test]