diff --git a/Cargo.lock b/Cargo.lock index 938d3d3f..af48299c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10580,16 +10580,22 @@ dependencies = [ "anyhow", "assert_cmd", "async-trait", + "axum 0.8.8", "chrono", "clap", "console", "crossterm 0.28.1", "dirs 5.0.1", "dotenvy", + "ed25519-dalek", + "hex", "indicatif", "predicates", "rand 0.8.5", + "rand_core 0.6.4", "ratatui", + "reqwest 0.12.28", + "rvagent-a2a", "rvagent-backends", "rvagent-core", "rvagent-middleware", diff --git a/crates/ruqu-wasm/src/lib.rs b/crates/ruqu-wasm/src/lib.rs index 6b9791a6..d7905e32 100644 --- a/crates/ruqu-wasm/src/lib.rs +++ b/crates/ruqu-wasm/src/lib.rs @@ -521,7 +521,10 @@ pub fn init() { // Tests // ═══════════════════════════════════════════════════════════════════════════ -#[cfg(test)] +// Tests for the WASM bindings only run on wasm32 because wasm-bindgen +// 0.2.117 panics on `JsValue::from_str` from a non-wasm runtime. +// Native verification of the underlying logic lives in `ruqu-core`. +#[cfg(all(test, target_arch = "wasm32"))] mod tests { use super::*; diff --git a/crates/rvAgent/rvagent-cli/Cargo.toml b/crates/rvAgent/rvagent-cli/Cargo.toml index 26958f46..05eee09a 100644 --- a/crates/rvAgent/rvagent-cli/Cargo.toml +++ b/crates/rvAgent/rvagent-cli/Cargo.toml @@ -16,9 +16,10 @@ rvagent-backends = { path = "../rvagent-backends" } rvagent-middleware = { path = "../rvagent-middleware" } rvagent-tools = { path = "../rvagent-tools" } rvagent-subagents = { path = "../rvagent-subagents" } +rvagent-a2a = { path = "../rvagent-a2a" } serde = { workspace = true } serde_json = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, features = ["signal", "process", "time", "io-util", "io-std", "fs", "net"] } thiserror = { workspace = true } anyhow = { workspace = true } tracing = { workspace = true } @@ -34,7 +35,12 @@ ratatui = "0.29" dirs = "5.0" aes-gcm = "0.10" rand = "0.8" +rand_core = "0.6" dotenvy = "0.15" +ed25519-dalek = "2" +hex = "0.4" +axum = "0.8" +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } [dev-dependencies] tempfile = "3.14" diff --git a/crates/rvAgent/rvagent-cli/src/main.rs b/crates/rvAgent/rvagent-cli/src/main.rs index 1224f0ce..48eebdef 100644 --- a/crates/rvAgent/rvagent-cli/src/main.rs +++ b/crates/rvAgent/rvagent-cli/src/main.rs @@ -4,6 +4,7 @@ //! initializes tracing, and dispatches to the appropriate run mode //! (interactive TUI, single-prompt, or session management). +mod a2a; mod app; mod display; mod mcp; @@ -60,6 +61,8 @@ enum Commands { #[command(subcommand)] action: SessionAction, }, + /// A2A (Agent-to-Agent) protocol operations: serve, discover, send-task. + A2a(a2a::A2aCommand), } // --------------------------------------------------------------------------- @@ -75,13 +78,14 @@ async fn main() -> Result<()> { let _ = dotenvy::from_filename(".env.local"); } - let cli = Cli::parse(); + let mut cli = Cli::parse(); // Determine if we're running in interactive TUI mode. // In TUI mode, we suppress console tracing to avoid corrupting the display. let is_tui_mode = match &cli.command { Some(Commands::Session { .. }) => false, Some(Commands::Run { .. }) => false, + Some(Commands::A2a(_)) => false, Some(Commands::Chat) | None => cli.prompt.is_none(), }; @@ -112,6 +116,14 @@ async fn main() -> Result<()> { app.run_once(prompt).await?; } + // A2A protocol subcommands. + Some(Commands::A2a(_)) => { + // Take ownership so we don't need Clone on the inner clap types. + if let Some(Commands::A2a(cmd)) = cli.command.take() { + a2a::run(cmd).await?; + } + } + // Interactive TUI chat (default when no sub-command given). Some(Commands::Chat) | None => { // If --prompt is supplied without a sub-command, treat as non-interactive. diff --git a/crates/rvAgent/rvagent-cli/tests/a2a_cli.rs b/crates/rvAgent/rvagent-cli/tests/a2a_cli.rs index 0d13ec43..0c7c0bb2 100644 --- a/crates/rvAgent/rvagent-cli/tests/a2a_cli.rs +++ b/crates/rvAgent/rvagent-cli/tests/a2a_cli.rs @@ -5,6 +5,7 @@ //! `discover` + `send-task` against it. use std::process::Stdio; +use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -39,17 +40,51 @@ async fn a2a_serve_discover_and_send_task() { .expect("spawn rvagent a2a serve"); let stdout = server.stdout.take().expect("server stdout piped"); + let stderr = server.stderr.take().expect("server stderr piped"); let mut reader = BufReader::new(stdout).lines(); + // Drain stderr in the background so it doesn't block the child if the + // pipe fills, AND so we can dump it on diagnostic failure paths. + let stderr_buf: Arc>> = Arc::new(Mutex::new(Vec::new())); + { + let buf = stderr_buf.clone(); + tokio::spawn(async move { + use tokio::io::AsyncReadExt; + let mut reader = stderr; + let mut chunk = [0u8; 4096]; + while let Ok(n) = reader.read(&mut chunk).await { + if n == 0 { + break; + } + buf.lock().unwrap().extend_from_slice(&chunk[..n]); + } + }); + } + let dump_stderr = || -> String { + let raw = stderr_buf.lock().unwrap().clone(); + String::from_utf8_lossy(&raw).into_owned() + }; + // -- 2) Parse "listening on 127.0.0.1:" from the first line. // // Give the server up to 20s to bind + print; CI under load is slower - // than local. - let line = tokio::time::timeout(Duration::from_secs(20), reader.next_line()) - .await - .expect("server listening line timed out") - .expect("server stdout read error") - .expect("server closed before emitting listening line"); + // than local. On every failure path we dump stderr so the actual + // error reason is visible. + let line = match tokio::time::timeout(Duration::from_secs(20), reader.next_line()).await { + Ok(Ok(Some(l))) => l, + Ok(Ok(None)) => panic!( + "server closed stdout before emitting listening line.\n--- server stderr ---\n{}", + dump_stderr() + ), + Ok(Err(e)) => panic!( + "server stdout read error: {e}\n--- server stderr ---\n{}", + dump_stderr() + ), + Err(_) => panic!( + "timed out waiting for server listening line (>20s)\n--- server stderr ---\n{}", + dump_stderr() + ), + }; let addr = line .strip_prefix("listening on ") .unwrap_or_else(|| panic!("unexpected first-line stdout from server: {:?}", line))