From 5b3f11d70b8e166168eee25196fee0fdd3cc6aa0 Mon Sep 17 00:00:00 2001 From: AndreaBozzo Date: Mon, 26 Jan 2026 11:55:29 +0100 Subject: [PATCH 1/9] feat: initialize Rust SDK --- rust/Cargo.toml | 6 ++++++ rust/src/lib.rs | 14 ++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 rust/Cargo.toml create mode 100644 rust/src/lib.rs diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000000..1c61e9e1e4 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "rust" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000000..b93cf3ffd9 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 749297f31b832c300c1f925a63b2bbcadc8dda20 Mon Sep 17 00:00:00 2001 From: AndreaBozzo Date: Mon, 26 Jan 2026 11:58:39 +0100 Subject: [PATCH 2/9] feat: add types and dependencies for Rust SDK --- rust/Cargo.toml | 4 ++++ rust/src/lib.rs | 15 +-------------- rust/src/types.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 rust/src/types.rs diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 1c61e9e1e4..e617cc11c6 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -4,3 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "2.0" +async-trait = "0.1" diff --git a/rust/src/lib.rs b/rust/src/lib.rs index b93cf3ffd9..cd408564ea 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,14 +1 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod types; diff --git a/rust/src/types.rs b/rust/src/types.rs new file mode 100644 index 0000000000..3943e23ba4 --- /dev/null +++ b/rust/src/types.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum ConnectionState { + Disconnected, + Connecting, + Connected, + Disconnecting, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LogMessage { + pub level: LogLevel, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + Debug, + Info, + Warn, + Error, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentConfig { + pub agent_id: String, + pub agent_name: String, + pub version: String, + #[serde(default)] + pub capabilities: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProtocolEvent { + pub event: String, + #[serde(flatten)] + pub payload: serde_json::Value, +} From 3aa16daeecaad12849a247668729e5bde94e9a9a Mon Sep 17 00:00:00 2001 From: AndreaBozzo Date: Mon, 26 Jan 2026 12:01:45 +0100 Subject: [PATCH 3/9] feat: add .gitignore and Cargo.lock for Rust SDK --- rust/.gitignore | 1 + rust/Cargo.lock | 140 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 rust/.gitignore create mode 100644 rust/Cargo.lock diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000000..b83d22266a --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000000..00dcb53e54 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,140 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rust" +version = "0.1.0" +dependencies = [ + "async-trait", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" From 57ac9dddb18451e9dd23d042935ddde8e4aa4ceb Mon Sep 17 00:00:00 2001 From: AndreaBozzo Date: Mon, 26 Jan 2026 12:11:33 +0100 Subject: [PATCH 4/9] update Cargo.toml and Cargo.lock with new dependencies for Rust SDK --- rust/Cargo.lock | 430 ++++++++++++++++++++++++++++++++++++++++++++++++ rust/Cargo.toml | 3 + 2 files changed, 433 insertions(+) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 00dcb53e54..15afcd976a 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2,6 +2,65 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -13,18 +72,187 @@ dependencies = [ "syn", ] +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jiff" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -43,16 +271,63 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "rust" version = "0.1.0" dependencies = [ "async-trait", + "env_logger", + "log", "serde", "serde_json", "thiserror", + "tokio", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -96,6 +371,32 @@ dependencies = [ "zmij", ] +[[package]] +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 = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[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 = "syn" version = "2.0.114" @@ -127,12 +428,141 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +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", +] + +[[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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "zmij" version = "1.0.17" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e617cc11c6..c7036639e7 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -8,3 +8,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "2.0" async-trait = "0.1" +tokio = { version = "1.49.0", features = ["full"] } +log = "0.4" +env_logger = "0.11" From 290d8559cf3659bb247d13974eb192b15cd351ba Mon Sep 17 00:00:00 2001 From: AndreaBozzo Date: Mon, 26 Jan 2026 12:14:58 +0100 Subject: [PATCH 5/9] feat: implement JsonRpcTransport for asynchronous JSON-RPC communication --- rust/Cargo.lock | 7 ++++++ rust/Cargo.toml | 1 + rust/src/jsonrpc.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++ rust/src/lib.rs | 1 + 4 files changed, 62 insertions(+) create mode 100644 rust/src/jsonrpc.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 15afcd976a..e5bc26335c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -61,6 +61,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "async-trait" version = "0.1.89" @@ -313,6 +319,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" name = "rust" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "env_logger", "log", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index c7036639e7..9caa98883f 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -11,3 +11,4 @@ async-trait = "0.1" tokio = { version = "1.49.0", features = ["full"] } log = "0.4" env_logger = "0.11" +anyhow = "1.0.100" diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs new file mode 100644 index 0000000000..a3ce48d97d --- /dev/null +++ b/rust/src/jsonrpc.rs @@ -0,0 +1,53 @@ +use anyhow::Result; +use serde::{de::DeserializeOwned, Serialize}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::sync::mpsc; + +pub struct JsonRpcTransport { + write_tx: mpsc::Sender, +} + +impl JsonRpcTransport { + pub fn new(incoming_handler: mpsc::Sender) -> Self + where + In: DeserializeOwned + Send + 'static, + { + let (write_tx, mut write_rx) = mpsc::channel::(32); + + tokio::spawn(async move { + let mut stdout = tokio::io::stdout(); + while let Some(msg) = write_rx.recv().await { + let _ = stdout.write_all(msg.as_bytes()).await; + let _ = stdout.write_all(b"\n").await; + let _ = stdout.flush().await; + } + }); + + tokio::spawn(async move { + let stdin = tokio::io::stdin(); + let mut reader = BufReader::new(stdin).lines(); + + while let Ok(Some(line)) = reader.next_line().await { + if line.trim().is_empty() { continue; } + + match serde_json::from_str::(&line) { + Ok(event) => { + if incoming_handler.send(event).await.is_err() { + break; + } + } + Err(e) => { + eprintln!("Error parsing JSON input: {} | Line: {}", e, line); + } + } + } + }); + + Self { write_tx } + } + + pub async fn send(&self, message: &T) -> Result<()> { + let json = serde_json::to_string(message)?; + self.write_tx.send(json).await.map_err(|e| anyhow::anyhow!("Channel closed: {}", e)) + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index cd408564ea..b27c13424e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1 +1,2 @@ pub mod types; +pub mod jsonrpc; From 48f92a10b95b6272a10140b1b3e58a26e3e5b65e Mon Sep 17 00:00:00 2001 From: AndreaBozzo Date: Mon, 26 Jan 2026 12:18:49 +0100 Subject: [PATCH 6/9] feat: implement Client struct for JSON-RPC communication in Rust SDK --- rust/src/client.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++ rust/src/lib.rs | 1 + 2 files changed, 72 insertions(+) create mode 100644 rust/src/client.rs diff --git a/rust/src/client.rs b/rust/src/client.rs new file mode 100644 index 0000000000..df6e4ab9bd --- /dev/null +++ b/rust/src/client.rs @@ -0,0 +1,71 @@ +use crate::jsonrpc::JsonRpcTransport; +use crate::types::{AgentConfig, LogLevel, LogMessage, ProtocolEvent}; +use anyhow::Result; +use serde_json::json; +use tokio::sync::mpsc; + +// prototype, must be improved +const PROTOCOL_VERSION: &str = "1.0.0"; + +pub struct Client { + transport: JsonRpcTransport, + incoming_events: Option>, +} + +impl Client { + pub fn new() -> Self { + let (tx, rx) = mpsc::channel(100); + + let transport = JsonRpcTransport::new(tx); + + Self { + transport, + incoming_events: Some(rx), + } + } + + pub async fn initialize(&self, config: AgentConfig) -> Result<()> { + let init_params = json!({ + "agentInfo": { + "id": config.agent_id, + "name": config.agent_name, + "version": config.version, + "capabilities": config.capabilities + }, + "sdkVersion": env!("CARGO_PKG_VERSION"), + "protocolVersion": PROTOCOL_VERSION + }); + + + let message = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": init_params + }); + + self.transport.send(&message).await?; + + Ok(()) + } + + pub async fn log(&self, level: LogLevel, message: &str) -> Result<()> { + let log_payload = LogMessage { + level, + message: message.to_string(), + metadata: None, + }; + + let notification = json!({ + "jsonrpc": "2.0", + "method": "logMessage", + "params": log_payload + }); + + self.transport.send(¬ification).await + } + + pub fn take_event_receiver(&mut self) -> Option> { + self.incoming_events.take() + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index b27c13424e..a3c476199d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,2 +1,3 @@ pub mod types; pub mod jsonrpc; +pub mod client; From 8c93df53e1b999b270eef18ae5d5b47c0edbff94 Mon Sep 17 00:00:00 2001 From: AndreaBozzo Date: Mon, 26 Jan 2026 12:31:35 +0100 Subject: [PATCH 7/9] feat: enhance Client and JsonRpcTransport for improved session management and request handling --- rust/src/client.rs | 145 ++++++++++++++++++++++++++++++++------------ rust/src/jsonrpc.rs | 11 +++- rust/src/lib.rs | 5 +- rust/src/session.rs | 44 ++++++++++++++ 4 files changed, 160 insertions(+), 45 deletions(-) create mode 100644 rust/src/session.rs diff --git a/rust/src/client.rs b/rust/src/client.rs index df6e4ab9bd..91528dac7e 100644 --- a/rust/src/client.rs +++ b/rust/src/client.rs @@ -1,71 +1,136 @@ use crate::jsonrpc::JsonRpcTransport; -use crate::types::{AgentConfig, LogLevel, LogMessage, ProtocolEvent}; -use anyhow::Result; -use serde_json::json; -use tokio::sync::mpsc; +use crate::types::{AgentConfig, ProtocolEvent}; +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use tokio::sync::{Mutex, mpsc, oneshot}; -// prototype, must be improved -const PROTOCOL_VERSION: &str = "1.0.0"; +#[derive(Debug, Deserialize)] +struct JsonRpcResponse { + id: Option, + result: Option, + error: Option, + method: Option, + params: Option, +} pub struct Client { transport: JsonRpcTransport, - incoming_events: Option>, + next_id: AtomicU64, + pending_requests: Arc>>>>, + events_tx: mpsc::Sender, + events_rx: Option>, } impl Client { pub fn new() -> Self { - let (tx, rx) = mpsc::channel(100); + let (events_tx, events_rx) = mpsc::channel(100); + let pending_requests: Arc>>>> = + Arc::new(Mutex::new(HashMap::new())); + let (transport_in_tx, mut transport_in_rx) = mpsc::channel::(100); + + let transport = JsonRpcTransport::new(transport_in_tx); + let pending_requests_clone = pending_requests.clone(); + let events_tx_clone = events_tx.clone(); - let transport = JsonRpcTransport::new(tx); + tokio::spawn(async move { + while let Some(raw_msg) = transport_in_rx.recv().await { + if let Ok(msg) = serde_json::from_str::(&raw_msg) { + if let Some(id) = msg.id { + let mut map = pending_requests_clone.lock().await; + if let Some(tx) = map.remove(&id) { + let result = if let Some(err) = msg.error { + Err(anyhow::anyhow!("RPC Error: {:?}", err)) + } else { + Ok(msg.result.unwrap_or(Value::Null)) + }; + let _ = tx.send(result); + continue; + } + } + + if let Some(method) = msg.method { + let event = ProtocolEvent { + event: method, + payload: msg.params.unwrap_or(Value::Null), + }; + let _ = events_tx_clone.send(event).await; + } + } + } + }); Self { transport, - incoming_events: Some(rx), + next_id: AtomicU64::new(1), + pending_requests, + events_tx, + events_rx: Some(events_rx), } } - pub async fn initialize(&self, config: AgentConfig) -> Result<()> { - let init_params = json!({ - "agentInfo": { - "id": config.agent_id, - "name": config.agent_name, - "version": config.version, - "capabilities": config.capabilities - }, - "sdkVersion": env!("CARGO_PKG_VERSION"), - "protocolVersion": PROTOCOL_VERSION - }); + pub async fn send_request( + &self, + method: &str, + params: P, + ) -> Result { + let id = self.next_id.fetch_add(1, Ordering::SeqCst); + let (tx, rx) = oneshot::channel(); + { + let mut map = self.pending_requests.lock().await; + map.insert(id, tx); + } - let message = json!({ + let req = serde_json::json!({ "jsonrpc": "2.0", - "id": 1, - "method": "initialize", - "params": init_params + "id": id, + "method": method, + "params": params }); - self.transport.send(&message).await?; + if let Err(e) = self.transport.send(&req).await { + let mut map = self.pending_requests.lock().await; + map.remove(&id); + return Err(e.into()); + } - Ok(()) + let response_value = rx.await.context("Client dropped or connection closed")??; + serde_json::from_value(response_value).map_err(Into::into) } - pub async fn log(&self, level: LogLevel, message: &str) -> Result<()> { - let log_payload = LogMessage { - level, - message: message.to_string(), - metadata: None, - }; + pub async fn initialize(&self, config: AgentConfig) -> Result<()> { + let _resp: Value = self + .send_request( + "initialize", + serde_json::json!({ + "agentInfo": { + "id": config.agent_id, + "name": config.agent_name, + "version": config.version, + "capabilities": config.capabilities + }, + "sdkVersion": env!("CARGO_PKG_VERSION"), + "protocolVersion": "1.0.0" + }), + ) + .await?; - let notification = json!({ - "jsonrpc": "2.0", - "method": "logMessage", - "params": log_payload - }); + self.transport + .send(&serde_json::json!({ + "jsonrpc": "2.0", + "method": "initialized", + "params": {} + })) + .await?; - self.transport.send(¬ification).await + Ok(()) } pub fn take_event_receiver(&mut self) -> Option> { - self.incoming_events.take() + self.events_rx.take() } } diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs index a3ce48d97d..5a61f59b36 100644 --- a/rust/src/jsonrpc.rs +++ b/rust/src/jsonrpc.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::sync::mpsc; @@ -28,7 +28,9 @@ impl JsonRpcTransport { let mut reader = BufReader::new(stdin).lines(); while let Ok(Some(line)) = reader.next_line().await { - if line.trim().is_empty() { continue; } + if line.trim().is_empty() { + continue; + } match serde_json::from_str::(&line) { Ok(event) => { @@ -48,6 +50,9 @@ impl JsonRpcTransport { pub async fn send(&self, message: &T) -> Result<()> { let json = serde_json::to_string(message)?; - self.write_tx.send(json).await.map_err(|e| anyhow::anyhow!("Channel closed: {}", e)) + self.write_tx + .send(json) + .await + .map_err(|e| anyhow::anyhow!("Channel closed: {}", e)) } } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index a3c476199d..bf30e4e8ae 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,3 +1,4 @@ -pub mod types; -pub mod jsonrpc; pub mod client; +pub mod jsonrpc; +pub mod session; +pub mod types; diff --git a/rust/src/session.rs b/rust/src/session.rs new file mode 100644 index 0000000000..9d3f8e03e9 --- /dev/null +++ b/rust/src/session.rs @@ -0,0 +1,44 @@ +use crate::client::Client; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionConfig { + pub system_message: Option, +} + +pub struct Session { + client: Arc, + pub id: String, +} + +impl Session { + pub async fn create(client: Arc, config: SessionConfig) -> Result { + #[derive(Deserialize)] + struct CreateSessionResponse { + id: String, + } + + let resp: CreateSessionResponse = client.send_request("session/create", config).await?; + + Ok(Self { + client, + id: resp.id, + }) + } + + pub async fn send_message(&self, content: &str) -> Result<()> { + let _resp: serde_json::Value = self + .client + .send_request( + "session/send", + serde_json::json!({ + "sessionId": self.id, + "message": content + }), + ) + .await?; + Ok(()) + } +} From db17890af745389fca2d1f4b445c8a7d2311c199 Mon Sep 17 00:00:00 2001 From: AndreaBozzo Date: Mon, 26 Jan 2026 14:09:52 +0100 Subject: [PATCH 8/9] Refactor JSON-RPC client and session management - Introduced a new `JsonRpcClient` struct to handle JSON-RPC communication with improved message handling and response management. - Updated `Session` struct to utilize the new `JsonRpcClient` for sending messages and managing session events. - Enhanced session configuration with additional fields and improved debug implementations for better logging. - Added support for message options, including attachments and delivery modes. - Improved error handling and response parsing for session-related operations. --- rust/Cargo.lock | 118 +++++++++ rust/Cargo.toml | 1 + rust/src/client.rs | 410 ++++++++++++++++++++++--------- rust/src/jsonrpc.rs | 448 ++++++++++++++++++++++++++++++--- rust/src/session.rs | 144 ++++++++--- rust/src/types.rs | 584 +++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 1522 insertions(+), 183 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e5bc26335c..eddeddef94 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -84,6 +84,12 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + [[package]] name = "bytes" version = "1.11.0" @@ -135,6 +141,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -171,6 +189,16 @@ dependencies = [ "syn", ] +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.180" @@ -209,6 +237,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -277,6 +311,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -327,8 +367,15 @@ dependencies = [ "serde_json", "thiserror", "tokio", + "uuid", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "scopeguard" version = "1.2.0" @@ -475,12 +522,77 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -570,6 +682,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + [[package]] name = "zmij" version = "1.0.17" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9caa98883f..fdeea517cf 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -12,3 +12,4 @@ tokio = { version = "1.49.0", features = ["full"] } log = "0.4" env_logger = "0.11" anyhow = "1.0.100" +uuid = { version = "1.11.0", features = ["v4"] } diff --git a/rust/src/client.rs b/rust/src/client.rs index 91528dac7e..a39064f19f 100644 --- a/rust/src/client.rs +++ b/rust/src/client.rs @@ -1,136 +1,328 @@ -use crate::jsonrpc::JsonRpcTransport; -use crate::types::{AgentConfig, ProtocolEvent}; -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; -use serde_json::Value; +use crate::jsonrpc::JsonRpcClient; +use crate::session::Session; +use crate::types::*; +use anyhow::{Context, Result, anyhow}; +use serde_json::json; use std::collections::HashMap; -use std::sync::Arc; -use std::sync::atomic::{AtomicU64, Ordering}; -use tokio::sync::{Mutex, mpsc, oneshot}; - -#[derive(Debug, Deserialize)] -struct JsonRpcResponse { - id: Option, - result: Option, - error: Option, - method: Option, - params: Option, -} +use std::io::{BufRead, BufReader, Write}; +use std::process::{Child, Command, Stdio}; +use std::sync::{Arc, Mutex as StdMutex}; + +// Type alias for the boxed RPC client type to reduce complexity +type BoxedRpcClient = Arc, Box>>; +/// Main client for interacting with the Copilot CLI pub struct Client { - transport: JsonRpcTransport, - next_id: AtomicU64, - pending_requests: Arc>>>>, - events_tx: mpsc::Sender, - events_rx: Option>, + options: ClientOptions, + rpc_client: Option, + cli_process: Option, + state: Arc>, + sessions: Arc>>>, } impl Client { - pub fn new() -> Self { - let (events_tx, events_rx) = mpsc::channel(100); - let pending_requests: Arc>>>> = - Arc::new(Mutex::new(HashMap::new())); - let (transport_in_tx, mut transport_in_rx) = mpsc::channel::(100); - - let transport = JsonRpcTransport::new(transport_in_tx); - let pending_requests_clone = pending_requests.clone(); - let events_tx_clone = events_tx.clone(); - - tokio::spawn(async move { - while let Some(raw_msg) = transport_in_rx.recv().await { - if let Ok(msg) = serde_json::from_str::(&raw_msg) { - if let Some(id) = msg.id { - let mut map = pending_requests_clone.lock().await; - if let Some(tx) = map.remove(&id) { - let result = if let Some(err) = msg.error { - Err(anyhow::anyhow!("RPC Error: {:?}", err)) - } else { - Ok(msg.result.unwrap_or(Value::Null)) - }; - let _ = tx.send(result); - continue; - } - } - - if let Some(method) = msg.method { - let event = ProtocolEvent { - event: method, - payload: msg.params.unwrap_or(Value::Null), - }; - let _ = events_tx_clone.send(event).await; - } - } - } - }); + /// Create a new client with the given options + pub fn new(options: Option) -> Self { + let options = options.unwrap_or_default(); Self { - transport, - next_id: AtomicU64::new(1), - pending_requests, - events_tx, - events_rx: Some(events_rx), + options, + rpc_client: None, + cli_process: None, + state: Arc::new(StdMutex::new(ConnectionState::Disconnected)), + sessions: Arc::new(StdMutex::new(HashMap::new())), } } - pub async fn send_request( - &self, - method: &str, - params: P, - ) -> Result { - let id = self.next_id.fetch_add(1, Ordering::SeqCst); - let (tx, rx) = oneshot::channel(); + /// Start the client and connect to the CLI server + pub fn start(&mut self) -> Result<()> { + { + let mut state = self.state.lock().unwrap(); + if *state != ConnectionState::Disconnected { + return Err(anyhow!("Client is already started or connecting")); + } + *state = ConnectionState::Connecting; + } + // Start CLI server if needed + if let Some(cli_url) = self.options.cli_url.clone() { + // Connect to external server + self.connect_to_external_server(&cli_url)?; + } else if self.options.use_stdio { + // Start CLI server with stdio + self.start_cli_server()?; + } else { + // Start CLI server with TCP (not yet implemented) + return Err(anyhow!("TCP transport not yet implemented")); + } + + // Set state to connected { - let mut map = self.pending_requests.lock().await; - map.insert(id, tx); + let mut state = self.state.lock().unwrap(); + *state = ConnectionState::Connected; + } + + Ok(()) + } + + /// Stop the client and cleanup + pub fn stop(&mut self) -> Vec { + let mut errors = Vec::new(); + + // Stop RPC client + if let Some(ref rpc_client) = self.rpc_client { + rpc_client.stop(); + } + + // Stop CLI process + if let Some(ref mut process) = self.cli_process { + if let Err(e) = process.kill() { + errors.push(anyhow!("Failed to kill CLI process: {}", e)); + } + if let Err(e) = process.wait() { + errors.push(anyhow!("Failed to wait for CLI process: {}", e)); + } + } + + self.rpc_client = None; + self.cli_process = None; + + let mut state = self.state.lock().unwrap(); + *state = ConnectionState::Disconnected; + + errors + } + + /// Forcefully stop the client + pub fn force_stop(&mut self) { + let _ = self.stop(); + } + + /// Get the current connection state + pub fn get_state(&self) -> ConnectionState { + *self.state.lock().unwrap() + } + + /// Send a ping request + pub async fn ping(&self, message: &str) -> Result { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let result = rpc_client + .request("ping", json!({ "message": message })) + .await + .map_err(|e| anyhow!("Ping failed: {}", e))?; + + serde_json::from_value(result).context("Failed to parse ping response") + } + + /// Get server status + pub async fn get_status(&self) -> Result { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let result = rpc_client + .request("status.get", json!({})) + .await + .map_err(|e| anyhow!("Get status failed: {}", e))?; + + serde_json::from_value(result).context("Failed to parse status response") + } + + /// Get authentication status + pub async fn get_auth_status(&self) -> Result { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let result = rpc_client + .request("auth.getStatus", json!({})) + .await + .map_err(|e| anyhow!("Get auth status failed: {}", e))?; + + serde_json::from_value(result).context("Failed to parse auth status response") + } + + /// List available models + pub async fn list_models(&self) -> Result> { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let result = rpc_client + .request("models.list", json!({})) + .await + .map_err(|e| anyhow!("List models failed: {}", e))?; + + let response: GetModelsResponse = + serde_json::from_value(result).context("Failed to parse models response")?; + + Ok(response.models) + } + + /// Create a new session + pub async fn create_session(&self, config: Option) -> Result> { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let config = config.unwrap_or_default(); + + // Build session create params + let mut params = json!({}); + if let Some(ref session_id) = config.session_id { + params["sessionId"] = json!(session_id); + } + if let Some(ref model) = config.model { + params["model"] = json!(model); + } + if let Some(ref system_message) = config.system_message { + params["systemMessage"] = serde_json::to_value(system_message)?; } + if config.streaming { + params["streaming"] = json!(true); + } + + let result = rpc_client + .request("session.create", params) + .await + .map_err(|e| anyhow!("Create session failed: {}", e))?; - let req = serde_json::json!({ - "jsonrpc": "2.0", - "id": id, - "method": method, - "params": params + let response: SessionCreateResponse = + serde_json::from_value(result).context("Failed to parse session create response")?; + + let session = Arc::new(Session::new( + response.session_id.clone(), + Arc::clone(rpc_client), + response.workspace_path, + )); + + // Store session + let mut sessions = self.sessions.lock().unwrap(); + sessions.insert(response.session_id.clone(), Arc::clone(&session)); + + Ok(session) + } + + /// Resume an existing session + pub async fn resume_session(&self, session_id: &str) -> Result> { + self.resume_session_with_options(session_id, None).await + } + + /// Resume a session with configuration options + pub async fn resume_session_with_options( + &self, + session_id: &str, + config: Option, + ) -> Result> { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let mut params = json!({ + "sessionId": session_id }); - if let Err(e) = self.transport.send(&req).await { - let mut map = self.pending_requests.lock().await; - map.remove(&id); - return Err(e.into()); + if let Some(config) = config + && config.streaming + { + params["streaming"] = json!(true); } - let response_value = rx.await.context("Client dropped or connection closed")??; - serde_json::from_value(response_value).map_err(Into::into) + let result = rpc_client + .request("session.resume", params) + .await + .map_err(|e| anyhow!("Resume session failed: {}", e))?; + + let response: SessionCreateResponse = + serde_json::from_value(result).context("Failed to parse session resume response")?; + + let session = Arc::new(Session::new( + response.session_id.clone(), + Arc::clone(rpc_client), + response.workspace_path, + )); + + // Store session + let mut sessions = self.sessions.lock().unwrap(); + sessions.insert(response.session_id.clone(), Arc::clone(&session)); + + Ok(session) } - pub async fn initialize(&self, config: AgentConfig) -> Result<()> { - let _resp: Value = self - .send_request( - "initialize", - serde_json::json!({ - "agentInfo": { - "id": config.agent_id, - "name": config.agent_name, - "version": config.version, - "capabilities": config.capabilities - }, - "sdkVersion": env!("CARGO_PKG_VERSION"), - "protocolVersion": "1.0.0" - }), - ) - .await?; - - self.transport - .send(&serde_json::json!({ - "jsonrpc": "2.0", - "method": "initialized", - "params": {} - })) - .await?; + // ======================================================================== + // Private helper methods + // ======================================================================== + + fn start_cli_server(&mut self) -> Result<()> { + let cli_path = self.options.cli_path.as_deref().unwrap_or("copilot"); + + let mut cmd = Command::new(cli_path); + cmd.arg("agent") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()); + + // Set working directory + if let Some(ref cwd) = self.options.cwd { + cmd.current_dir(cwd); + } + + // Set log level + if let Some(ref log_level) = self.options.log_level { + cmd.arg("--log-level").arg(log_level); + } + + // Set environment variables + if let Some(ref env_vars) = self.options.env { + for (key, value) in env_vars { + cmd.env(key, value); + } + } + + // Spawn the process + let mut child = cmd.spawn().context("Failed to spawn CLI process")?; + + // Get stdin and stdout handles + let stdin = child + .stdin + .take() + .ok_or_else(|| anyhow!("Failed to get stdin"))?; + let stdout = child + .stdout + .take() + .ok_or_else(|| anyhow!("Failed to get stdout"))?; + + // Create RPC client with boxed writers/readers + let boxed_writer: Box = Box::new(stdin); + let boxed_reader: Box = Box::new(BufReader::new(stdout)); + let rpc_client = JsonRpcClient::new(boxed_writer, boxed_reader); + + // Start the RPC client + rpc_client.start(); + + self.rpc_client = Some(Arc::new(rpc_client)); + self.cli_process = Some(child); Ok(()) } - pub fn take_event_receiver(&mut self) -> Option> { - self.events_rx.take() + fn connect_to_external_server(&mut self, _url: &str) -> Result<()> { + // TODO: Implement TCP connection to external server + Err(anyhow!("External server connection not yet implemented")) + } +} + +impl Drop for Client { + fn drop(&mut self) { + let _ = self.stop(); } } diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs index 5a61f59b36..90900967a0 100644 --- a/rust/src/jsonrpc.rs +++ b/rust/src/jsonrpc.rs @@ -1,58 +1,428 @@ -use anyhow::Result; -use serde::{Serialize, de::DeserializeOwned}; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::sync::mpsc; +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::io::{BufRead, BufReader, Write}; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, +}; +use tokio::sync::{mpsc, oneshot}; +use uuid::Uuid; -pub struct JsonRpcTransport { - write_tx: mpsc::Sender, +// ============================================================================ +// JSON-RPC Types +// ============================================================================ + +/// JSON-RPC error +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcError { + pub code: i32, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, } -impl JsonRpcTransport { - pub fn new(incoming_handler: mpsc::Sender) -> Self - where - In: DeserializeOwned + Send + 'static, - { - let (write_tx, mut write_rx) = mpsc::channel::(32); +impl std::fmt::Display for JsonRpcError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "JSON-RPC Error {}: {}", self.code, self.message) + } +} - tokio::spawn(async move { - let mut stdout = tokio::io::stdout(); - while let Some(msg) = write_rx.recv().await { - let _ = stdout.write_all(msg.as_bytes()).await; - let _ = stdout.write_all(b"\n").await; - let _ = stdout.flush().await; - } - }); +impl std::error::Error for JsonRpcError {} + +/// JSON-RPC request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcRequest { + pub jsonrpc: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + pub method: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +/// JSON-RPC response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcResponse { + pub jsonrpc: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// JSON-RPC notification (request without ID) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcNotification { + pub jsonrpc: String, + pub method: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +// ============================================================================ +// Handler Types +// ============================================================================ + +/// Handler for incoming notifications +pub type NotificationHandler = Arc; + +/// Handler for incoming requests from the server +pub type RequestHandler = Arc Result + Send + Sync>; + +// ============================================================================ +// JSON-RPC Client +// ============================================================================ + +type ResponseSender = oneshot::Sender>; + +pub struct JsonRpcClient { + writer: Arc>, + reader: Arc>, + pending_requests: Arc>>, + request_handlers: Arc>>, + notification_handler: Arc>>, + running: Arc, + stop_tx: mpsc::Sender<()>, + stop_rx: Arc>>>, +} + +impl JsonRpcClient { + /// Create a new JSON-RPC client + pub fn new(writer: W, reader: R) -> Self { + let (stop_tx, stop_rx) = mpsc::channel(1); + + Self { + writer: Arc::new(Mutex::new(writer)), + reader: Arc::new(Mutex::new(reader)), + pending_requests: Arc::new(Mutex::new(HashMap::new())), + request_handlers: Arc::new(Mutex::new(HashMap::new())), + notification_handler: Arc::new(Mutex::new(None)), + running: Arc::new(AtomicBool::new(false)), + stop_tx, + stop_rx: Arc::new(Mutex::new(Some(stop_rx))), + } + } + + /// Start the client and begin processing messages + pub fn start(&self) { + if self + .running + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_err() + { + return; // Already running + } + + let reader = Arc::clone(&self.reader); + let writer = Arc::clone(&self.writer); + let pending_requests = Arc::clone(&self.pending_requests); + let request_handlers = Arc::clone(&self.request_handlers); + let notification_handler = Arc::clone(&self.notification_handler); + let running = Arc::clone(&self.running); + let stop_rx = Arc::clone(&self.stop_rx); - tokio::spawn(async move { - let stdin = tokio::io::stdin(); - let mut reader = BufReader::new(stdin).lines(); + std::thread::spawn(move || { + let mut stop_rx = stop_rx.lock().unwrap().take().unwrap(); - while let Ok(Some(line)) = reader.next_line().await { - if line.trim().is_empty() { - continue; + loop { + // Check if we should stop + if !running.load(Ordering::SeqCst) { + break; } - match serde_json::from_str::(&line) { - Ok(event) => { - if incoming_handler.send(event).await.is_err() { - break; - } + // Try to receive stop signal (non-blocking) + match stop_rx.try_recv() { + Ok(_) | Err(mpsc::error::TryRecvError::Disconnected) => break, + Err(mpsc::error::TryRecvError::Empty) => {} + } + + // Read next message + let message_result = { + let mut reader_guard = reader.lock().unwrap(); + Self::read_message(&mut *reader_guard) + }; + + match message_result { + Ok(Some(msg)) => { + Self::handle_message( + msg, + &writer, + &pending_requests, + &request_handlers, + ¬ification_handler, + ); } + Ok(None) => continue, Err(e) => { - eprintln!("Error parsing JSON input: {} | Line: {}", e, line); + if running.load(Ordering::SeqCst) { + eprintln!("Error reading message: {}", e); + } + break; } } } + + running.store(false, Ordering::SeqCst); }); + } - Self { write_tx } + /// Stop the client + pub fn stop(&self) { + self.running.store(false, Ordering::SeqCst); + let _ = self.stop_tx.try_send(()); } - pub async fn send(&self, message: &T) -> Result<()> { - let json = serde_json::to_string(message)?; - self.write_tx - .send(json) - .await - .map_err(|e| anyhow::anyhow!("Channel closed: {}", e)) + /// Set handler for incoming notifications + pub fn set_notification_handler(&self, handler: NotificationHandler) { + let mut guard = self.notification_handler.lock().unwrap(); + *guard = Some(handler); + } + + /// Set handler for incoming server requests + pub fn set_request_handler(&self, method: String, handler: RequestHandler) { + let mut guard = self.request_handlers.lock().unwrap(); + guard.insert(method, handler); + } + + /// Remove handler for a specific method + pub fn remove_request_handler(&self, method: &str) { + let mut guard = self.request_handlers.lock().unwrap(); + guard.remove(method); + } + + /// Send a request and wait for response + pub async fn request(&self, method: &str, params: Value) -> Result { + let request_id = Uuid::new_v4().to_string(); + let (tx, rx) = oneshot::channel(); + + // Register pending request + { + let mut pending = self.pending_requests.lock().unwrap(); + pending.insert(request_id.clone(), tx); + } + + // Send request + let request = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + id: Some(Value::String(request_id.clone())), + method: method.to_string(), + params: Some(params), + }; + + if let Err(e) = self.send_message(&request) { + // Clean up on error + let mut pending = self.pending_requests.lock().unwrap(); + pending.remove(&request_id); + return Err(JsonRpcError { + code: -32000, + message: format!("Failed to send request: {}", e), + data: None, + }); + } + + // Wait for response + match rx.await { + Ok(result) => result, + Err(_) => Err(JsonRpcError { + code: -32000, + message: "Response channel closed".to_string(), + data: None, + }), + } + } + + /// Send a notification (no response expected) + pub fn notify(&self, method: &str, params: Value) -> Result<()> { + let notification = JsonRpcNotification { + jsonrpc: "2.0".to_string(), + method: method.to_string(), + params: Some(params), + }; + + self.send_message(¬ification) + } + + /// Send a message (internal helper) + fn send_message(&self, message: &T) -> Result<()> { + let data = serde_json::to_vec(message)?; + let header = format!("Content-Length: {}\r\n\r\n", data.len()); + + let mut writer = self.writer.lock().unwrap(); + writer + .write_all(header.as_bytes()) + .context("Failed to write header")?; + writer.write_all(&data).context("Failed to write message")?; + writer.flush().context("Failed to flush writer")?; + + Ok(()) + } + + /// Read a single message from the reader + fn read_message(reader: &mut R) -> Result> { + // Read headers + let mut content_length = 0; + loop { + let mut line = String::new(); + let bytes_read = reader.read_line(&mut line)?; + if bytes_read == 0 { + return Ok(None); // EOF + } + + let line = line.trim(); + if line.is_empty() { + break; // End of headers + } + + if let Some(value) = line.strip_prefix("Content-Length:") { + content_length = value.trim().parse()?; + } + } + + if content_length == 0 { + return Ok(None); + } + + // Read body + let mut body = vec![0u8; content_length]; + reader.read_exact(&mut body)?; + + let value: Value = serde_json::from_slice(&body)?; + Ok(Some(value)) + } + + /// Handle an incoming message + fn handle_message( + msg: Value, + writer: &Arc>, + pending_requests: &Arc>>, + request_handlers: &Arc>>, + notification_handler: &Arc>>, + ) { + // Try to parse as response first + if let Ok(response) = serde_json::from_value::(msg.clone()) { + if let Some(id) = &response.id { + let id_str = match id { + Value::String(s) => s.clone(), + Value::Number(n) => n.to_string(), + _ => return, + }; + + let mut pending = pending_requests.lock().unwrap(); + if let Some(tx) = pending.remove(&id_str) { + let result = if let Some(error) = response.error { + Err(error) + } else { + Ok(response.result.unwrap_or(Value::Null)) + }; + let _ = tx.send(result); + } + } + return; + } + + // Try to parse as request + if let Ok(request) = serde_json::from_value::(msg.clone()) + && request.id.is_some() + { + Self::handle_request(request, writer, request_handlers); + return; + } + + // Try to parse as notification + if let Ok(notification) = serde_json::from_value::(msg) { + Self::handle_notification(notification, notification_handler); + } + } + + /// Handle an incoming request from the server + fn handle_request( + request: JsonRpcRequest, + writer: &Arc>, + request_handlers: &Arc>>, + ) { + let handler = { + let handlers = request_handlers.lock().unwrap(); + handlers.get(&request.method).cloned() + }; + + let response = if let Some(handler) = handler { + let result = handler(request.params.unwrap_or(Value::Null)); + match result { + Ok(value) => JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id.clone(), + result: Some(value), + error: None, + }, + Err(error) => JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id.clone(), + result: None, + error: Some(error), + }, + } + } else { + JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id.clone(), + result: None, + error: Some(JsonRpcError { + code: -32601, + message: format!("Method not found: {}", request.method), + data: None, + }), + } + }; + + // Send response + let data = match serde_json::to_vec(&response) { + Ok(d) => d, + Err(e) => { + eprintln!("Failed to serialize response: {}", e); + return; + } + }; + + let header = format!("Content-Length: {}\r\n\r\n", data.len()); + let mut writer = writer.lock().unwrap(); + if let Err(e) = writer.write_all(header.as_bytes()) { + eprintln!("Failed to write response header: {}", e); + return; + } + if let Err(e) = writer.write_all(&data) { + eprintln!("Failed to write response body: {}", e); + return; + } + let _ = writer.flush(); + } + + /// Handle an incoming notification + fn handle_notification( + notification: JsonRpcNotification, + notification_handler: &Arc>>, + ) { + let handler = notification_handler.lock().unwrap().clone(); + if let Some(handler) = handler { + handler( + notification.method, + notification.params.unwrap_or(Value::Null), + ); + } + } +} + +// ============================================================================ +// Convenience type for stdio transport +// ============================================================================ + +pub type StdioJsonRpcClient = JsonRpcClient>; + +impl StdioJsonRpcClient { + pub fn new_stdio() -> Self { + Self::new(std::io::stdout(), BufReader::new(std::io::stdin())) } } diff --git a/rust/src/session.rs b/rust/src/session.rs index 9d3f8e03e9..4d019b43dd 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -1,44 +1,128 @@ -use crate::client::Client; -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionConfig { - pub system_message: Option, -} +use crate::jsonrpc::JsonRpcClient; +use crate::types::{MessageOptions, SessionEvent, SessionEventHandler, SessionGetMessagesResponse}; +use anyhow::{Context, Result, anyhow}; +use serde_json::json; +use std::io::{BufRead, Write}; +use std::sync::{Arc, Mutex}; + +// Type alias for the boxed RPC client type (matches client.rs) +type BoxedRpcClient = Arc, Box>>; +/// Represents a conversation session with the Copilot CLI pub struct Session { - client: Arc, pub id: String, + workspace_path: Option, + rpc_client: BoxedRpcClient, + event_handlers: Arc>>, } impl Session { - pub async fn create(client: Arc, config: SessionConfig) -> Result { - #[derive(Deserialize)] - struct CreateSessionResponse { - id: String, + /// Create a new session + pub fn new(id: String, rpc_client: BoxedRpcClient, workspace_path: Option) -> Self { + Self { + id, + workspace_path, + rpc_client, + event_handlers: Arc::new(Mutex::new(Vec::new())), + } + } + + /// Get the workspace path for infinite sessions + pub fn workspace_path(&self) -> Option<&str> { + self.workspace_path.as_deref() + } + + /// Send a message to this session + pub async fn send(&self, options: MessageOptions) -> Result { + let mut params = json!({ + "sessionId": self.id, + "message": { + "prompt": options.prompt + } + }); + + if !options.attachments.is_empty() { + params["message"]["attachments"] = serde_json::to_value(&options.attachments)?; + } + + if let Some(ref mode) = options.mode { + params["message"]["mode"] = json!(mode); } - let resp: CreateSessionResponse = client.send_request("session/create", config).await?; + let result = self + .rpc_client + .request("session.send", params) + .await + .map_err(|e| anyhow!("Failed to send message: {}", e))?; + + let response: serde_json::Value = result; + let message_id = response + .get("messageId") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("Response missing messageId"))?; - Ok(Self { - client, - id: resp.id, - }) + Ok(message_id.to_string()) } - pub async fn send_message(&self, content: &str) -> Result<()> { - let _resp: serde_json::Value = self - .client - .send_request( - "session/send", - serde_json::json!({ - "sessionId": self.id, - "message": content - }), - ) - .await?; + /// Subscribe to session events + pub fn on(&self, handler: SessionEventHandler) -> impl FnOnce() { + let mut handlers = self.event_handlers.lock().unwrap(); + let index = handlers.len(); + handlers.push(handler); + + // Return unsubscribe function + let event_handlers = Arc::clone(&self.event_handlers); + move || { + let mut handlers = event_handlers.lock().unwrap(); + if index < handlers.len() { + let _ = handlers.remove(index); + } + } + } + + /// Get messages from the session + pub async fn get_messages(&self) -> Result> { + let params = json!({ + "sessionId": self.id + }); + + let result = self + .rpc_client + .request("session.getMessages", params) + .await + .map_err(|e| anyhow!("Failed to get messages: {}", e))?; + + let response: SessionGetMessagesResponse = + serde_json::from_value(result).context("Failed to parse get messages response")?; + + Ok(response.events) + } + + /// Destroy the session + pub async fn destroy(&self) -> Result<()> { + let params = json!({ + "sessionId": self.id + }); + + self.rpc_client + .request("session.destroy", params) + .await + .map_err(|e| anyhow!("Failed to destroy session: {}", e))?; + + Ok(()) + } + + /// Abort the current operation + pub async fn abort(&self) -> Result<()> { + let params = json!({ + "sessionId": self.id + }); + + self.rpc_client + .request("session.abort", params) + .await + .map_err(|e| anyhow!("Failed to abort session: {}", e))?; + Ok(()) } } diff --git a/rust/src/types.rs b/rust/src/types.rs index 3943e23ba4..bf4b703797 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1,15 +1,542 @@ use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::collections::HashMap; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] +// ============================================================================ +// Connection State +// ============================================================================ + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] pub enum ConnectionState { Disconnected, Connecting, Connected, - Disconnecting, + Error, +} + +// ============================================================================ +// Client Configuration +// ============================================================================ + +/// ClientOptions configures the Copilot client +#[derive(Debug, Clone, Default)] +pub struct ClientOptions { + /// Path to the Copilot CLI executable (default: "copilot") + pub cli_path: Option, + /// Working directory for the CLI process (default: inherit from current process) + pub cwd: Option, + /// Port for TCP transport (default: 0 = random port) + pub port: Option, + /// Enable stdio transport instead of TCP (default: true) + pub use_stdio: bool, + /// URL of an existing Copilot CLI server to connect to over TCP + /// Format: "host:port", "http://host:port", or just "port" (defaults to localhost) + /// Examples: "localhost:8080", "http://127.0.0.1:9000", "8080" + /// Mutually exclusive with cli_path, use_stdio + pub cli_url: Option, + /// Log level for the CLI server + pub log_level: Option, + /// Automatically starts the CLI server on first use (default: true) + pub auto_start: Option, + /// Automatically restarts the CLI server if it crashes (default: true) + pub auto_restart: Option, + /// Environment variables for the CLI process (default: inherits from current process) + pub env: Option>, +} + +impl ClientOptions { + pub fn new() -> Self { + Self { + cli_path: Some("copilot".to_string()), + use_stdio: true, + log_level: Some("info".to_string()), + auto_start: Some(true), + auto_restart: Some(true), + ..Default::default() + } + } +} + +// ============================================================================ +// System Message Configuration +// ============================================================================ + +/// System message configuration for session creation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SystemMessageConfig { + /// Mode: "append" or "replace" + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + /// Content: additional instructions (append) or complete system message (replace) + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, +} + +// ============================================================================ +// Permission Types +// ============================================================================ + +/// Permission request from the server +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionRequest { + pub kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, + #[serde(flatten)] + pub extra: HashMap, } +/// Result of a permission request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PermissionRequestResult { + pub kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub rules: Option>, +} + +/// Context for permission invocation +#[derive(Debug, Clone)] +pub struct PermissionInvocation { + pub session_id: String, +} + +/// Handler for permission requests +pub type PermissionHandler = Box< + dyn Fn(PermissionRequest, PermissionInvocation) -> Result + + Send + + Sync, +>; + +// ============================================================================ +// MCP Server Configuration +// ============================================================================ + +/// Configuration for a local/stdio MCP server +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPLocalServerConfig { + pub tools: Vec, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub server_type: Option, // "local" or "stdio" + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option, + pub command: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub args: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub env: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cwd: Option, +} + +/// Configuration for a remote MCP server (HTTP or SSE) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPRemoteServerConfig { + pub tools: Vec, + #[serde(rename = "type")] + pub server_type: String, // "http" or "sse" + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option, + pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub headers: Option>, +} + +/// MCP server configuration (can be local or remote) +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MCPServerConfig { + Local(MCPLocalServerConfig), + Remote(MCPRemoteServerConfig), + Raw(HashMap), +} + +// ============================================================================ +// Custom Agent Configuration +// ============================================================================ + +/// Configuration for a custom agent +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomAgentConfig { + /// Unique name of the custom agent + pub name: String, + /// Display name for UI purposes + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Description of what the agent does + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// List of tool names the agent can use (None for all tools) + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, + /// Prompt content for the agent + pub prompt: String, + /// MCP servers specific to this agent + #[serde(skip_serializing_if = "Option::is_none")] + pub mcp_servers: Option>, + /// Whether the agent should be available for model inference + #[serde(skip_serializing_if = "Option::is_none")] + pub infer: Option, +} + +// ============================================================================ +// Infinite Session Configuration +// ============================================================================ + +/// Configuration for infinite sessions with automatic context compaction +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InfiniteSessionConfig { + /// Controls whether infinite sessions are enabled (default: true) + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + /// Context utilization (0.0-1.0) at which background compaction starts (default: 0.80) + #[serde(skip_serializing_if = "Option::is_none")] + pub background_compaction_threshold: Option, + /// Context utilization (0.0-1.0) at which the session blocks until compaction completes (default: 0.95) + #[serde(skip_serializing_if = "Option::is_none")] + pub buffer_exhaustion_threshold: Option, +} + +// ============================================================================ +// Provider Configuration +// ============================================================================ + +/// Azure-specific provider options +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AzureProviderOptions { + /// Azure API version (default: "2024-10-21") + #[serde(skip_serializing_if = "Option::is_none")] + pub api_version: Option, +} + +/// Configuration for a custom model provider +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderConfig { + /// Provider type: "openai", "azure", or "anthropic" (default: "openai") + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub provider_type: Option, + /// API format (openai/azure only): "completions" or "responses" (default: "completions") + #[serde(skip_serializing_if = "Option::is_none")] + pub wire_api: Option, + /// API endpoint URL + pub base_url: String, + /// API key (optional for local providers like Ollama) + #[serde(skip_serializing_if = "Option::is_none")] + pub api_key: Option, + /// Bearer token for authentication (takes precedence over api_key) + #[serde(skip_serializing_if = "Option::is_none")] + pub bearer_token: Option, + /// Azure-specific options + #[serde(skip_serializing_if = "Option::is_none")] + pub azure: Option, +} + +// ============================================================================ +// Tool Types +// ============================================================================ + +/// A tool that can be invoked by Copilot +#[derive(Clone)] +pub struct Tool { + pub name: String, + pub description: Option, + pub parameters: HashMap, + pub handler: ToolHandler, +} + +impl std::fmt::Debug for Tool { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tool") + .field("name", &self.name) + .field("description", &self.description) + .field("parameters", &self.parameters) + .field("handler", &"") + .finish() + } +} + +/// Context for a tool invocation +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolInvocation { + pub session_id: String, + pub tool_call_id: String, + pub tool_name: String, + pub arguments: Value, +} + +/// Handler for tool invocations +pub type ToolHandler = + std::sync::Arc Result + Send + Sync>; + +/// Binary result for tools +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolBinaryResult { + pub data: String, + pub mime_type: String, + #[serde(rename = "type")] + pub result_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +/// Result of a tool invocation +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolResult { + pub text_result_for_llm: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub binary_results_for_llm: Option>, + pub result_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub session_log: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_telemetry: Option>, +} + +// ============================================================================ +// Session Configuration +// ============================================================================ + +/// Configuration for creating a new session +#[derive(Default)] +pub struct SessionConfig { + /// Optional custom session ID + pub session_id: Option, + /// Model to use for this session + pub model: Option, + /// Override the default configuration directory location + pub config_dir: Option, + /// Caller-implemented tools to expose to the CLI + pub tools: Vec, + /// System message customization + pub system_message: Option, + /// List of tool names to allow (takes precedence over excluded_tools) + pub available_tools: Option>, + /// List of tool names to disable + pub excluded_tools: Option>, + /// Handler for permission requests + pub on_permission_request: Option, + /// Enable streaming of assistant message and reasoning chunks + pub streaming: bool, + /// Custom model provider configuration (BYOK) + pub provider: Option, + /// MCP servers for the session + pub mcp_servers: Option>, + /// Custom agents for the session + pub custom_agents: Option>, + /// Directories to load skills from + pub skill_directories: Option>, + /// Skill names to disable + pub disabled_skills: Option>, + /// Infinite sessions configuration + pub infinite_sessions: Option, +} + +/// Configuration for resuming a session +#[derive(Default)] +pub struct ResumeSessionConfig { + /// Caller-implemented tools to expose to the CLI + pub tools: Vec, + /// Custom model provider configuration + pub provider: Option, + /// Handler for permission requests + pub on_permission_request: Option, + /// Enable streaming + pub streaming: bool, + /// MCP servers for the session + pub mcp_servers: Option>, + /// Custom agents for the session + pub custom_agents: Option>, + /// Directories to load skills from + pub skill_directories: Option>, + /// Skill names to disable + pub disabled_skills: Option>, +} + +// ============================================================================ +// Message Types +// ============================================================================ + +/// Attachment type +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AttachmentType { + File, + Directory, +} + +/// Attachment for a message +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Attachment { + pub display_name: String, + pub path: String, + #[serde(rename = "type")] + pub attachment_type: AttachmentType, +} + +/// Options for sending a message +#[derive(Debug, Clone)] +pub struct MessageOptions { + /// The message prompt + pub prompt: String, + /// File or directory attachments + pub attachments: Vec, + /// Message delivery mode (default: "enqueue") + pub mode: Option, +} + +// ============================================================================ +// Response Types +// ============================================================================ + +/// Response from a ping request +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PingResponse { + pub message: String, + pub timestamp: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub protocol_version: Option, +} + +/// Response from session.create +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionCreateResponse { + pub session_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_path: Option, +} + +/// Response from session.send +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionSendResponse { + pub message_id: String, +} + +/// Response from status.get +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetStatusResponse { + pub version: String, + pub protocol_version: i32, +} + +/// Response from auth.getStatus +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAuthStatusResponse { + pub is_authenticated: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub host: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub login: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_message: Option, +} + +/// Vision-specific limits +#[derive(Debug, Clone, Deserialize)] +pub struct ModelVisionLimits { + pub supported_media_types: Vec, + pub max_prompt_images: i32, + pub max_prompt_image_size: i32, +} + +/// Model limits +#[derive(Debug, Clone, Deserialize)] +pub struct ModelLimits { + #[serde(skip_serializing_if = "Option::is_none")] + pub max_prompt_tokens: Option, + pub max_context_window_tokens: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub vision: Option, +} + +/// Model support flags +#[derive(Debug, Clone, Deserialize)] +pub struct ModelSupports { + pub vision: bool, +} + +/// Model capabilities +#[derive(Debug, Clone, Deserialize)] +pub struct ModelCapabilities { + pub supports: ModelSupports, + pub limits: ModelLimits, +} + +/// Model policy +#[derive(Debug, Clone, Deserialize)] +pub struct ModelPolicy { + pub state: String, + pub terms: String, +} + +/// Model billing information +#[derive(Debug, Clone, Deserialize)] +pub struct ModelBilling { + pub multiplier: f64, +} + +/// Information about a model +#[derive(Debug, Clone, Deserialize)] +pub struct ModelInfo { + pub id: String, + pub name: String, + pub capabilities: ModelCapabilities, + #[serde(skip_serializing_if = "Option::is_none")] + pub policy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub billing: Option, +} + +/// Response from models.list +#[derive(Debug, Clone, Deserialize)] +pub struct GetModelsResponse { + pub models: Vec, +} + +/// Response from session.getMessages +#[derive(Debug, Clone, Deserialize)] +pub struct SessionGetMessagesResponse { + pub events: Vec, +} + +// ============================================================================ +// Session Events (simplified for now) +// ============================================================================ + +/// Session event (simplified - will be expanded with generated types later) +#[derive(Debug, Clone, Deserialize)] +pub struct SessionEvent { + #[serde(rename = "type")] + pub event_type: String, + pub id: String, + #[serde(flatten)] + pub data: Value, +} + +/// Handler for session events +pub type SessionEventHandler = Box; + +// ============================================================================ +// Logging and Events +// ============================================================================ + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LogMessage { @@ -35,12 +562,59 @@ pub struct AgentConfig { pub agent_name: String, pub version: String, #[serde(default)] - pub capabilities: serde_json::Value, + pub capabilities: Value, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProtocolEvent { pub event: String, #[serde(flatten)] - pub payload: serde_json::Value, + pub payload: Value, +} + +// ============================================================================ +// Manual Debug Implementations for types with function pointers +// ============================================================================ + +impl std::fmt::Debug for SessionConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SessionConfig") + .field("session_id", &self.session_id) + .field("model", &self.model) + .field("config_dir", &self.config_dir) + .field("tools", &format!("[{} tools]", self.tools.len())) + .field("system_message", &self.system_message) + .field("available_tools", &self.available_tools) + .field("excluded_tools", &self.excluded_tools) + .field( + "on_permission_request", + &self.on_permission_request.as_ref().map(|_| ""), + ) + .field("streaming", &self.streaming) + .field("provider", &self.provider) + .field("mcp_servers", &self.mcp_servers) + .field("custom_agents", &self.custom_agents) + .field("skill_directories", &self.skill_directories) + .field("disabled_skills", &self.disabled_skills) + .field("infinite_sessions", &self.infinite_sessions) + .finish() + } +} + +impl std::fmt::Debug for ResumeSessionConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResumeSessionConfig") + .field("tools", &format!("[{} tools]", self.tools.len())) + .field("provider", &self.provider) + .field( + "on_permission_request", + &self.on_permission_request.as_ref().map(|_| ""), + ) + .field("streaming", &self.streaming) + .field("mcp_servers", &self.mcp_servers) + .field("custom_agents", &self.custom_agents) + .field("skill_directories", &self.skill_directories) + .field("disabled_skills", &self.disabled_skills) + .finish() + } } From 399b2ffb075f1402c36bdfac876a38f23b19d48a Mon Sep 17 00:00:00 2001 From: AndreaBozzo Date: Mon, 26 Jan 2026 15:25:50 +0100 Subject: [PATCH 9/9] add notification handler for session events and dispatch mechanism --- rust/src/client.rs | 18 ++++++++++++++++++ rust/src/session.rs | 14 ++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/rust/src/client.rs b/rust/src/client.rs index a39064f19f..352d0c9df3 100644 --- a/rust/src/client.rs +++ b/rust/src/client.rs @@ -306,6 +306,24 @@ impl Client { let boxed_reader: Box = Box::new(BufReader::new(stdout)); let rpc_client = JsonRpcClient::new(boxed_writer, boxed_reader); + // Set up notification handler for session events + let sessions_for_handler = Arc::clone(&self.sessions); + rpc_client.set_notification_handler(Arc::new(move |method, params| { + if method == "session.event" { + // Extract sessionId and event from params + if let Some(session_id) = params.get("sessionId").and_then(|v| v.as_str()) + && let Some(event_value) = params.get("event") + && let Ok(event) = serde_json::from_value::(event_value.clone()) + { + // Dispatch to the session + let sessions = sessions_for_handler.lock().unwrap(); + if let Some(session) = sessions.get(session_id) { + session.dispatch_event(event); + } + } + } + })); + // Start the RPC client rpc_client.start(); diff --git a/rust/src/session.rs b/rust/src/session.rs index 4d019b43dd..a248b89517 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -125,4 +125,18 @@ impl Session { Ok(()) } + + /// Dispatch an event to all registered handlers + pub fn dispatch_event(&self, event: SessionEvent) { + let handlers = self.event_handlers.lock().unwrap(); + for handler in handlers.iter() { + // Call handler - catch panics to prevent crashing the dispatcher + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + handler(event.clone()); + })); + if let Err(e) = result { + eprintln!("Error in session event handler: {:?}", e); + } + } + } }