diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bd9ed5c..87a4ce60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,3 +24,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Multi-step execution loop with 3-iteration limit - 30-second timeout on shell commands - Context builder that combines base system prompt with skill instructions +- SQLite conversation persistence with sqlx (zeph-memory) +- Conversation history loading and message saving per session +- Claude backend via Anthropic Messages API with 429 retry (zeph-llm) +- AnyProvider enum dispatch for runtime provider selection +- CloudLlmConfig for Claude-specific settings (model, max_tokens) +- ZEPH_CLAUDE_API_KEY env var for API authentication +- ZEPH_SQLITE_PATH env var override for database location +- Provider factory in main.rs selecting Ollama or Claude from config +- Memory integration into Agent with optional SqliteStore diff --git a/Cargo.lock b/Cargo.lock index 9496b05e..9305648a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anyhow" version = "1.0.101" @@ -30,23 +36,56 @@ dependencies = [ "syn", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "bumpalo" @@ -54,6 +93,12 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -76,6 +121,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -92,6 +152,78 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -103,12 +235,36 @@ dependencies = [ "syn", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -125,6 +281,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -137,6 +315,29 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -168,6 +369,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -176,6 +378,40 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -189,12 +425,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -207,12 +467,90 @@ dependencies = [ "wasip2", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -262,6 +600,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -273,6 +612,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -307,9 +662,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -421,7 +778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -461,6 +818,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -468,6 +828,34 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.7.0", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libyml" version = "0.0.5" @@ -505,12 +893,28 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.1.1" @@ -548,6 +952,52 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[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-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "ollama-rs" version = "0.3.3" @@ -615,6 +1065,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -633,11 +1089,20 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -656,6 +1121,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -671,6 +1157,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -695,6 +1190,36 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +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 = "redox_syscall" version = "0.5.18" @@ -704,6 +1229,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -732,15 +1266,19 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", + "encoding_rs", "futures-core", + "h2", "http", "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", "log", + "mime", "native-tls", "percent-encoding", "pin-project-lite", @@ -760,6 +1298,40 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustix" version = "1.1.3" @@ -773,6 +1345,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -782,6 +1367,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -948,6 +1544,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -964,35 +1582,255 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "signal-hook-registry" -version = "1.4.8" +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ - "errno", - "libc", + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", ] [[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.2" +name = "sqlx-sqlite" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ - "libc", - "windows-sys 0.60.2", + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", ] [[package]] @@ -1007,6 +1845,23 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.114" @@ -1038,6 +1893,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.24.0" @@ -1045,7 +1921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1090,6 +1966,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.49.0" @@ -1128,6 +2019,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.9.11+spec-1.1.0" @@ -1218,6 +2143,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1275,12 +2201,45 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -1341,6 +2300,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -1410,19 +2375,76 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -1434,6 +2456,37 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -1441,58 +2494,148 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -1550,6 +2693,7 @@ dependencies = [ "tracing-subscriber", "zeph-core", "zeph-llm", + "zeph-memory", "zeph-skills", ] @@ -1564,6 +2708,7 @@ dependencies = [ "toml", "tracing", "zeph-llm", + "zeph-memory", ] [[package]] @@ -1572,13 +2717,23 @@ version = "0.1.0" dependencies = [ "anyhow", "ollama-rs", + "reqwest", "serde", + "serde_json", + "tokio", "tracing", ] [[package]] name = "zeph-memory" version = "0.1.0" +dependencies = [ + "anyhow", + "sqlx", + "tokio", + "tracing", + "zeph-llm", +] [[package]] name = "zeph-skills" @@ -1591,6 +2746,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "zerocopy" +version = "0.8.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 02b1aa12..8bd12826 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,11 @@ repository = "https://github.com/bug-ops/zeph" [workspace.dependencies] anyhow = "1.0" ollama-rs = "0.3" +reqwest = { version = "0.12", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yml = "0.0" +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } thiserror = "2.0" tokio = { version = "1", features = ["full"] } toml = "0.9" @@ -39,6 +41,7 @@ tracing.workspace = true tracing-subscriber.workspace = true zeph-core = { path = "crates/zeph-core" } zeph-llm = { path = "crates/zeph-llm" } +zeph-memory = { path = "crates/zeph-memory" } zeph-skills = { path = "crates/zeph-skills" } [lints] diff --git a/config/default.toml b/config/default.toml index d9266baa..edebc158 100644 --- a/config/default.toml +++ b/config/default.toml @@ -6,6 +6,10 @@ provider = "ollama" base_url = "http://localhost:11434" model = "mistral:7b" +[llm.cloud] +model = "claude-sonnet-4-5-20250929" +max_tokens = 4096 + [skills] paths = ["./skills"] diff --git a/crates/zeph-core/Cargo.toml b/crates/zeph-core/Cargo.toml index f5b2bf24..940f0f42 100644 --- a/crates/zeph-core/Cargo.toml +++ b/crates/zeph-core/Cargo.toml @@ -13,6 +13,7 @@ tokio.workspace = true toml.workspace = true tracing.workspace = true zeph-llm = { path = "../zeph-llm" } +zeph-memory = { path = "../zeph-memory" } [dev-dependencies] tempfile = "3" diff --git a/crates/zeph-core/src/agent.rs b/crates/zeph-core/src/agent.rs index 681677a2..f088c963 100644 --- a/crates/zeph-core/src/agent.rs +++ b/crates/zeph-core/src/agent.rs @@ -3,6 +3,7 @@ use std::time::Duration; use tokio::process::Command; use zeph_llm::provider::{LlmProvider, Message, Role}; +use zeph_memory::sqlite::{SqliteStore, role_str}; use crate::context::build_system_prompt; @@ -12,9 +13,13 @@ const SHELL_TIMEOUT: Duration = Duration::from_secs(30); pub struct Agent { provider: P, messages: Vec, + memory: Option, + conversation_id: Option, + history_limit: u32, } impl Agent

{ + #[must_use] pub fn new(provider: P, skills_prompt: &str) -> Self { let system_prompt = build_system_prompt(skills_prompt); Self { @@ -23,9 +28,40 @@ impl Agent

{ role: Role::System, content: system_prompt, }], + memory: None, + conversation_id: None, + history_limit: 50, } } + #[must_use] + pub fn with_memory(mut self, store: SqliteStore, conversation_id: i64, limit: u32) -> Self { + self.memory = Some(store); + self.conversation_id = Some(conversation_id); + self.history_limit = limit; + self + } + + /// Load conversation history from memory and inject into messages. + /// + /// # Errors + /// + /// Returns an error if loading history from `SQLite` fails. + pub async fn load_history(&mut self) -> anyhow::Result<()> { + let (Some(store), Some(cid)) = (&self.memory, self.conversation_id) else { + return Ok(()); + }; + + let history = store.load_history(cid, self.history_limit).await?; + if !history.is_empty() { + tracing::info!("restored {} message(s) from conversation {cid}", history.len()); + for msg in history { + self.messages.push(msg); + } + } + Ok(()) + } + /// Run the interactive chat loop, reading from stdin until EOF or "exit"/"quit". /// /// # Errors @@ -55,6 +91,7 @@ impl Agent

{ role: Role::User, content: trimmed.to_string(), }); + self.persist_message(Role::User, trimmed).await; if let Err(e) = self.process_response().await { tracing::error!("LLM error: {e:#}"); @@ -75,6 +112,7 @@ impl Agent

{ role: Role::Assistant, content: response.clone(), }); + self.persist_message(Role::Assistant, &response).await; let Some(output) = extract_and_execute_bash(&response).await else { return Ok(()); @@ -82,14 +120,25 @@ impl Agent

{ println!("[shell output]\n{output}"); + let shell_msg = format!("[shell output]\n{output}"); self.messages.push(Message { role: Role::User, - content: format!("[shell output]\n{output}"), + content: shell_msg.clone(), }); + self.persist_message(Role::User, &shell_msg).await; } Ok(()) } + + async fn persist_message(&self, role: Role, content: &str) { + let (Some(store), Some(cid)) = (&self.memory, self.conversation_id) else { + return; + }; + if let Err(e) = store.save_message(cid, role_str(role), content).await { + tracing::error!("failed to persist message: {e:#}"); + } + } } fn extract_bash_blocks(text: &str) -> Vec<&str> { diff --git a/crates/zeph-core/src/config.rs b/crates/zeph-core/src/config.rs index 1290ff0d..69464b49 100644 --- a/crates/zeph-core/src/config.rs +++ b/crates/zeph-core/src/config.rs @@ -21,6 +21,13 @@ pub struct LlmConfig { pub provider: String, pub base_url: String, pub model: String, + pub cloud: Option, +} + +#[derive(Debug, Deserialize)] +pub struct CloudLlmConfig { + pub model: String, + pub max_tokens: u32, } #[derive(Debug, Deserialize)] @@ -31,7 +38,7 @@ pub struct SkillsConfig { #[derive(Debug, Deserialize)] pub struct MemoryConfig { pub sqlite_path: String, - pub history_limit: usize, + pub history_limit: u32, } impl Config { @@ -65,6 +72,9 @@ impl Config { if let Ok(v) = std::env::var("ZEPH_LLM_MODEL") { self.llm.model = v; } + if let Ok(v) = std::env::var("ZEPH_SQLITE_PATH") { + self.memory.sqlite_path = v; + } } fn default() -> Self { @@ -76,6 +86,7 @@ impl Config { provider: "ollama".into(), base_url: "http://localhost:11434".into(), model: "mistral:7b".into(), + cloud: None, }, skills: SkillsConfig { paths: vec!["./skills".into()], @@ -102,6 +113,7 @@ mod tests { assert_eq!(config.llm.model, "mistral:7b"); assert_eq!(config.agent.name, "Zeph"); assert_eq!(config.memory.history_limit, 50); + assert!(config.llm.cloud.is_none()); } #[test] @@ -131,7 +143,7 @@ history_limit = 10 .unwrap(); // Remove any ZEPH_ env vars that could interfere - for key in ["ZEPH_LLM_PROVIDER", "ZEPH_LLM_BASE_URL", "ZEPH_LLM_MODEL"] { + for key in ["ZEPH_LLM_PROVIDER", "ZEPH_LLM_BASE_URL", "ZEPH_LLM_MODEL", "ZEPH_SQLITE_PATH"] { unsafe { std::env::remove_var(key) }; } @@ -142,6 +154,47 @@ history_limit = 10 assert_eq!(config.memory.history_limit, 10); } + #[test] + fn parse_toml_with_cloud() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("cloud.toml"); + let mut f = std::fs::File::create(&path).unwrap(); + write!( + f, + r#" +[agent] +name = "Zeph" + +[llm] +provider = "claude" +base_url = "http://localhost:11434" +model = "mistral:7b" + +[llm.cloud] +model = "claude-sonnet-4-5-20250929" +max_tokens = 4096 + +[skills] +paths = ["./skills"] + +[memory] +sqlite_path = "./data/zeph.db" +history_limit = 50 +"# + ) + .unwrap(); + + for key in ["ZEPH_LLM_PROVIDER", "ZEPH_LLM_BASE_URL", "ZEPH_LLM_MODEL", "ZEPH_SQLITE_PATH"] { + unsafe { std::env::remove_var(key) }; + } + + let config = Config::load(&path).unwrap(); + assert_eq!(config.llm.provider, "claude"); + let cloud = config.llm.cloud.unwrap(); + assert_eq!(cloud.model, "claude-sonnet-4-5-20250929"); + assert_eq!(cloud.max_tokens, 4096); + } + #[test] fn env_overrides() { let mut config = Config::default(); diff --git a/crates/zeph-llm/Cargo.toml b/crates/zeph-llm/Cargo.toml index 67dfb6a5..2e7bd5fc 100644 --- a/crates/zeph-llm/Cargo.toml +++ b/crates/zeph-llm/Cargo.toml @@ -9,7 +9,10 @@ repository.workspace = true [dependencies] anyhow.workspace = true ollama-rs.workspace = true +reqwest.workspace = true serde.workspace = true +serde_json.workspace = true +tokio.workspace = true tracing.workspace = true [lints] diff --git a/crates/zeph-llm/src/any.rs b/crates/zeph-llm/src/any.rs new file mode 100644 index 00000000..d140a288 --- /dev/null +++ b/crates/zeph-llm/src/any.rs @@ -0,0 +1,25 @@ +use crate::claude::ClaudeProvider; +use crate::ollama::OllamaProvider; +use crate::provider::{LlmProvider, Message}; + +#[derive(Debug)] +pub enum AnyProvider { + Ollama(OllamaProvider), + Claude(ClaudeProvider), +} + +impl LlmProvider for AnyProvider { + async fn chat(&self, messages: &[Message]) -> anyhow::Result { + match self { + Self::Ollama(p) => p.chat(messages).await, + Self::Claude(p) => p.chat(messages).await, + } + } + + fn name(&self) -> &'static str { + match self { + Self::Ollama(p) => p.name(), + Self::Claude(p) => p.name(), + } + } +} diff --git a/crates/zeph-llm/src/claude.rs b/crates/zeph-llm/src/claude.rs new file mode 100644 index 00000000..33a564a9 --- /dev/null +++ b/crates/zeph-llm/src/claude.rs @@ -0,0 +1,217 @@ +use std::time::Duration; + +use anyhow::{Context, bail}; +use serde::{Deserialize, Serialize}; + +use crate::provider::{LlmProvider, Message, Role}; + +const API_URL: &str = "https://api.anthropic.com/v1/messages"; +const ANTHROPIC_VERSION: &str = "2023-06-01"; + +#[derive(Debug)] +pub struct ClaudeProvider { + client: reqwest::Client, + api_key: String, + model: String, + max_tokens: u32, +} + +impl ClaudeProvider { + #[must_use] + pub fn new(api_key: String, model: String, max_tokens: u32) -> Self { + Self { + client: reqwest::Client::new(), + api_key, + model, + max_tokens, + } + } + + async fn send_request(&self, messages: &[Message]) -> anyhow::Result { + let (system, chat_messages) = split_messages(messages); + + let body = RequestBody { + model: &self.model, + max_tokens: self.max_tokens, + system: system.as_deref(), + messages: &chat_messages, + }; + + let response = self + .client + .post(API_URL) + .header("x-api-key", &self.api_key) + .header("anthropic-version", ANTHROPIC_VERSION) + .header("content-type", "application/json") + .json(&body) + .send() + .await + .context("failed to send request to Claude API")?; + + let status = response.status(); + let text = response.text().await.context("failed to read response body")?; + + if status == reqwest::StatusCode::TOO_MANY_REQUESTS { + return Err(anyhow::anyhow!("rate_limited")); + } + + if !status.is_success() { + bail!("Claude API error {status}: {text}"); + } + + let resp: ApiResponse = + serde_json::from_str(&text).context("failed to parse Claude API response")?; + + resp.content + .first() + .map(|c| c.text.clone()) + .context("empty response from Claude API") + } +} + +impl LlmProvider for ClaudeProvider { + async fn chat(&self, messages: &[Message]) -> anyhow::Result { + match self.send_request(messages).await { + Ok(text) => Ok(text), + Err(e) if e.to_string().contains("rate_limited") => { + tracing::warn!("Claude rate limited, retrying in 1s"); + tokio::time::sleep(Duration::from_secs(1)).await; + self.send_request(messages).await + } + Err(e) => Err(e), + } + } + + fn name(&self) -> &'static str { + "claude" + } +} + +fn split_messages(messages: &[Message]) -> (Option, Vec>) { + let mut system_parts = Vec::new(); + let mut chat = Vec::new(); + + for msg in messages { + match msg.role { + Role::System => system_parts.push(msg.content.as_str()), + Role::User => chat.push(ApiMessage { + role: "user", + content: &msg.content, + }), + Role::Assistant => chat.push(ApiMessage { + role: "assistant", + content: &msg.content, + }), + } + } + + let system = if system_parts.is_empty() { + None + } else { + Some(system_parts.join("\n\n")) + }; + + (system, chat) +} + +#[derive(Serialize)] +struct RequestBody<'a> { + model: &'a str, + max_tokens: u32, + #[serde(skip_serializing_if = "Option::is_none")] + system: Option<&'a str>, + messages: &'a [ApiMessage<'a>], +} + +#[derive(Serialize)] +struct ApiMessage<'a> { + role: &'a str, + content: &'a str, +} + +#[derive(Deserialize)] +struct ApiResponse { + content: Vec, +} + +#[derive(Deserialize)] +struct ContentBlock { + text: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn split_messages_extracts_system() { + let messages = vec![ + Message { + role: Role::System, + content: "You are helpful.".into(), + }, + Message { + role: Role::User, + content: "Hi".into(), + }, + ]; + + let (system, chat) = split_messages(&messages); + assert_eq!(system.unwrap(), "You are helpful."); + assert_eq!(chat.len(), 1); + assert_eq!(chat[0].role, "user"); + } + + #[test] + fn split_messages_no_system() { + let messages = vec![Message { + role: Role::User, + content: "Hi".into(), + }]; + + let (system, chat) = split_messages(&messages); + assert!(system.is_none()); + assert_eq!(chat.len(), 1); + } + + #[test] + fn split_messages_multiple_system() { + let messages = vec![ + Message { + role: Role::System, + content: "Part 1".into(), + }, + Message { + role: Role::System, + content: "Part 2".into(), + }, + Message { + role: Role::User, + content: "Hi".into(), + }, + ]; + + let (system, _) = split_messages(&messages); + assert_eq!(system.unwrap(), "Part 1\n\nPart 2"); + } + + #[tokio::test] + #[ignore] // Requires ZEPH_CLAUDE_API_KEY env var + async fn integration_claude_chat() { + let api_key = std::env::var("ZEPH_CLAUDE_API_KEY") + .expect("ZEPH_CLAUDE_API_KEY must be set"); + let provider = ClaudeProvider::new( + api_key, + "claude-sonnet-4-5-20250929".into(), + 256, + ); + + let messages = vec![Message { + role: Role::User, + content: "Reply with exactly: pong".into(), + }]; + + let response = provider.chat(&messages).await.unwrap(); + assert!(response.to_lowercase().contains("pong")); + } +} diff --git a/crates/zeph-llm/src/lib.rs b/crates/zeph-llm/src/lib.rs index df0497dc..b6c8029f 100644 --- a/crates/zeph-llm/src/lib.rs +++ b/crates/zeph-llm/src/lib.rs @@ -1,4 +1,6 @@ //! LLM provider abstraction and backend implementations. +pub mod any; +pub mod claude; pub mod ollama; pub mod provider; diff --git a/crates/zeph-memory/Cargo.toml b/crates/zeph-memory/Cargo.toml index 0d53674d..356f8817 100644 --- a/crates/zeph-memory/Cargo.toml +++ b/crates/zeph-memory/Cargo.toml @@ -7,6 +7,11 @@ license.workspace = true repository.workspace = true [dependencies] +anyhow.workspace = true +sqlx.workspace = true +tokio.workspace = true +tracing.workspace = true +zeph-llm = { path = "../zeph-llm" } [lints] workspace = true diff --git a/crates/zeph-memory/migrations/001_init.sql b/crates/zeph-memory/migrations/001_init.sql new file mode 100644 index 00000000..289014c9 --- /dev/null +++ b/crates/zeph-memory/migrations/001_init.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS conversations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + conversation_id INTEGER NOT NULL REFERENCES conversations(id), + role TEXT NOT NULL, + content TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); diff --git a/crates/zeph-memory/src/lib.rs b/crates/zeph-memory/src/lib.rs index c964950c..e3f97491 100644 --- a/crates/zeph-memory/src/lib.rs +++ b/crates/zeph-memory/src/lib.rs @@ -1 +1,3 @@ //! SQLite-backed conversation persistence. + +pub mod sqlite; diff --git a/crates/zeph-memory/src/sqlite.rs b/crates/zeph-memory/src/sqlite.rs new file mode 100644 index 00000000..e4984f88 --- /dev/null +++ b/crates/zeph-memory/src/sqlite.rs @@ -0,0 +1,239 @@ +use anyhow::Context; +use sqlx::SqlitePool; +use sqlx::sqlite::SqlitePoolOptions; +use zeph_llm::provider::{Message, Role}; + +const INIT_SQL: &str = "\ +CREATE TABLE IF NOT EXISTS conversations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + conversation_id INTEGER NOT NULL REFERENCES conversations(id), + role TEXT NOT NULL, + content TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +);"; + +#[derive(Debug)] +pub struct SqliteStore { + pool: SqlitePool, +} + +impl SqliteStore { + /// Open (or create) the `SQLite` database and run migrations. + /// + /// # Errors + /// + /// Returns an error if the database cannot be opened or migrations fail. + pub async fn new(path: &str) -> anyhow::Result { + if path != ":memory:" + && let Some(parent) = std::path::Path::new(path).parent() + { + std::fs::create_dir_all(parent) + .context("failed to create database directory")?; + } + + let url = if path == ":memory:" { + "sqlite::memory:".to_string() + } else { + format!("sqlite:{path}?mode=rwc") + }; + + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect(&url) + .await + .context("failed to open SQLite database")?; + + sqlx::query(INIT_SQL) + .execute(&pool) + .await + .context("failed to run migrations")?; + + Ok(Self { pool }) + } + + /// Create a new conversation and return its ID. + /// + /// # Errors + /// + /// Returns an error if the insert fails. + pub async fn create_conversation(&self) -> anyhow::Result { + let row: (i64,) = + sqlx::query_as("INSERT INTO conversations DEFAULT VALUES RETURNING id") + .fetch_one(&self.pool) + .await + .context("failed to create conversation")?; + Ok(row.0) + } + + /// Save a message to the given conversation. + /// + /// # Errors + /// + /// Returns an error if the insert fails. + pub async fn save_message( + &self, + conversation_id: i64, + role: &str, + content: &str, + ) -> anyhow::Result<()> { + sqlx::query("INSERT INTO messages (conversation_id, role, content) VALUES (?, ?, ?)") + .bind(conversation_id) + .bind(role) + .bind(content) + .execute(&self.pool) + .await + .context("failed to save message")?; + Ok(()) + } + + /// Load the most recent messages for a conversation, up to `limit`. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub async fn load_history( + &self, + conversation_id: i64, + limit: u32, + ) -> anyhow::Result> { + let rows: Vec<(String, String)> = sqlx::query_as( + "SELECT role, content FROM messages \ + WHERE conversation_id = ? \ + ORDER BY id ASC \ + LIMIT ?", + ) + .bind(conversation_id) + .bind(limit) + .fetch_all(&self.pool) + .await + .context("failed to load history")?; + + let messages = rows + .into_iter() + .map(|(role_str, content)| Message { + role: parse_role(&role_str), + content, + }) + .collect(); + Ok(messages) + } + + /// Return the ID of the most recent conversation, if any. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub async fn latest_conversation_id(&self) -> anyhow::Result> { + let row: Option<(i64,)> = + sqlx::query_as("SELECT id FROM conversations ORDER BY id DESC LIMIT 1") + .fetch_optional(&self.pool) + .await + .context("failed to fetch latest conversation")?; + Ok(row.map(|r| r.0)) + } +} + +fn parse_role(s: &str) -> Role { + match s { + "assistant" => Role::Assistant, + "system" => Role::System, + _ => Role::User, + } +} + +#[must_use] +pub fn role_str(role: Role) -> &'static str { + match role { + Role::System => "system", + Role::User => "user", + Role::Assistant => "assistant", + } +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn test_store() -> SqliteStore { + SqliteStore::new(":memory:").await.unwrap() + } + + #[tokio::test] + async fn create_conversation_returns_id() { + let store = test_store().await; + let id1 = store.create_conversation().await.unwrap(); + let id2 = store.create_conversation().await.unwrap(); + assert_eq!(id1, 1); + assert_eq!(id2, 2); + } + + #[tokio::test] + async fn save_and_load_messages() { + let store = test_store().await; + let cid = store.create_conversation().await.unwrap(); + + store.save_message(cid, "user", "hello").await.unwrap(); + store + .save_message(cid, "assistant", "hi there") + .await + .unwrap(); + + let history = store.load_history(cid, 50).await.unwrap(); + assert_eq!(history.len(), 2); + assert_eq!(history[0].role, Role::User); + assert_eq!(history[0].content, "hello"); + assert_eq!(history[1].role, Role::Assistant); + assert_eq!(history[1].content, "hi there"); + } + + #[tokio::test] + async fn load_history_respects_limit() { + let store = test_store().await; + let cid = store.create_conversation().await.unwrap(); + + for i in 0..10 { + store + .save_message(cid, "user", &format!("msg {i}")) + .await + .unwrap(); + } + + let history = store.load_history(cid, 3).await.unwrap(); + assert_eq!(history.len(), 3); + } + + #[tokio::test] + async fn latest_conversation_id_empty() { + let store = test_store().await; + assert!(store.latest_conversation_id().await.unwrap().is_none()); + } + + #[tokio::test] + async fn latest_conversation_id_returns_newest() { + let store = test_store().await; + store.create_conversation().await.unwrap(); + let id2 = store.create_conversation().await.unwrap(); + assert_eq!(store.latest_conversation_id().await.unwrap(), Some(id2)); + } + + #[tokio::test] + async fn messages_isolated_per_conversation() { + let store = test_store().await; + let cid1 = store.create_conversation().await.unwrap(); + let cid2 = store.create_conversation().await.unwrap(); + + store.save_message(cid1, "user", "conv1").await.unwrap(); + store.save_message(cid2, "user", "conv2").await.unwrap(); + + let h1 = store.load_history(cid1, 50).await.unwrap(); + let h2 = store.load_history(cid2, 50).await.unwrap(); + assert_eq!(h1.len(), 1); + assert_eq!(h1[0].content, "conv1"); + assert_eq!(h2.len(), 1); + assert_eq!(h2[0].content, "conv2"); + } +} diff --git a/src/main.rs b/src/main.rs index b3460342..e90f67b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,12 @@ use std::path::{Path, PathBuf}; +use anyhow::{Context, bail}; use zeph_core::agent::Agent; use zeph_core::config::Config; +use zeph_llm::any::AnyProvider; +use zeph_llm::claude::ClaudeProvider; use zeph_llm::ollama::OllamaProvider; +use zeph_memory::sqlite::SqliteStore; use zeph_skills::prompt::format_skills_prompt; use zeph_skills::registry::SkillRegistry; @@ -11,7 +15,8 @@ async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let config = Config::load(Path::new("config/default.toml"))?; - let provider = OllamaProvider::new(&config.llm.base_url, config.llm.model.clone()); + + let provider = create_provider(&config)?; let skill_paths: Vec = config.skills.paths.iter().map(PathBuf::from).collect(); let registry = SkillRegistry::load(&skill_paths); @@ -21,6 +26,40 @@ async fn main() -> anyhow::Result<()> { println!("zeph v{}", env!("CARGO_PKG_VERSION")); - let mut agent = Agent::new(provider, &skills_prompt); + let store = SqliteStore::new(&config.memory.sqlite_path).await?; + let conversation_id = match store.latest_conversation_id().await? { + Some(id) => id, + None => store.create_conversation().await?, + }; + + tracing::info!("conversation id: {conversation_id}"); + + let mut agent = Agent::new(provider, &skills_prompt) + .with_memory(store, conversation_id, config.memory.history_limit); + agent.load_history().await?; agent.run().await } + +fn create_provider(config: &Config) -> anyhow::Result { + match config.llm.provider.as_str() { + "ollama" => { + let provider = + OllamaProvider::new(&config.llm.base_url, config.llm.model.clone()); + Ok(AnyProvider::Ollama(provider)) + } + "claude" => { + let cloud = config + .llm + .cloud + .as_ref() + .context("llm.cloud config section required for Claude provider")?; + + let api_key = std::env::var("ZEPH_CLAUDE_API_KEY") + .context("ZEPH_CLAUDE_API_KEY env var required for Claude provider")?; + + let provider = ClaudeProvider::new(api_key, cloud.model.clone(), cloud.max_tokens); + Ok(AnyProvider::Claude(provider)) + } + other => bail!("unknown LLM provider: {other}"), + } +}