diff --git a/Cargo.lock b/Cargo.lock index fb09eae..208395c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -215,6 +224,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "async-task" version = "4.7.1" @@ -831,6 +862,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -6805,9 +6847,12 @@ dependencies = [ "tokio-stream", "tracing", "uuid", + "zclaw-hands", "zclaw-memory", + "zclaw-protocols", "zclaw-runtime", "zclaw-types", + "zip", ] [[package]] @@ -6837,6 +6882,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", + "uuid", "zclaw-types", ] @@ -6844,6 +6890,7 @@ dependencies = [ name = "zclaw-runtime" version = "0.1.0" dependencies = [ + "async-stream", "async-trait", "chrono", "futures", @@ -6966,12 +7013,41 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.13.0", + "memchr", + "thiserror 2.0.18", + "zopfli", +] + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zvariant" version = "5.10.0" diff --git a/desktop/src-tauri/src/kernel_commands.rs b/desktop/src-tauri/src/kernel_commands.rs index 5bb98dd..925b23d 100644 --- a/desktop/src-tauri/src/kernel_commands.rs +++ b/desktop/src-tauri/src/kernel_commands.rs @@ -4,9 +4,10 @@ //! eliminating the need for external OpenFang process. use std::sync::Arc; -use tauri::{AppHandle, Manager, State}; +use tauri::{AppHandle, Emitter, Manager, State}; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; +use futures::StreamExt; use zclaw_kernel::Kernel; use zclaw_types::{AgentConfig, AgentId, AgentInfo, AgentState}; @@ -342,6 +343,102 @@ pub async fn agent_chat( }) } +/// Streaming chat event for Tauri emission +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", tag = "type")] +pub enum StreamChatEvent { + /// Text delta received + Delta { delta: String }, + /// Tool use started + ToolStart { name: String, input: serde_json::Value }, + /// Tool use completed + ToolEnd { name: String, output: serde_json::Value }, + /// Stream completed + Complete { input_tokens: u32, output_tokens: u32 }, + /// Error occurred + Error { message: String }, +} + +/// Streaming chat request +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StreamChatRequest { + /// Agent ID + pub agent_id: String, + /// Session ID (for event routing) + pub session_id: String, + /// Message content + pub message: String, +} + +/// Send a message to an agent with streaming response +/// +/// This command initiates a streaming chat session. Events are emitted +/// via Tauri's event system with the name "stream:chunk" and include +/// the session_id for routing. +#[tauri::command] +pub async fn agent_chat_stream( + app: AppHandle, + state: State<'_, KernelState>, + request: StreamChatRequest, +) -> Result<(), String> { + // Parse agent ID first + let id: AgentId = request.agent_id.parse() + .map_err(|_| "Invalid agent ID format".to_string())?; + + let session_id = request.session_id.clone(); + let message = request.message; + + // Get the streaming receiver while holding the lock, then release it + let mut rx = { + let kernel_lock = state.lock().await; + let kernel = kernel_lock.as_ref() + .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; + + // Start the stream - this spawns a background task + kernel.send_message_stream(&id, message) + .await + .map_err(|e| format!("Failed to start streaming: {}", e))? + }; + // Lock is released here + + // Spawn a task to process stream events + tokio::spawn(async move { + use zclaw_runtime::LoopEvent; + + while let Some(event) = rx.recv().await { + let stream_event = match event { + LoopEvent::Delta(delta) => { + StreamChatEvent::Delta { delta } + } + LoopEvent::ToolStart { name, input } => { + StreamChatEvent::ToolStart { name, input } + } + LoopEvent::ToolEnd { name, output } => { + StreamChatEvent::ToolEnd { name, output } + } + LoopEvent::Complete(result) => { + StreamChatEvent::Complete { + input_tokens: result.input_tokens, + output_tokens: result.output_tokens, + } + } + LoopEvent::Error(message) => { + StreamChatEvent::Error { message } + } + }; + + // Emit the event with session_id for routing + let _ = app.emit("stream:chunk", serde_json::json!({ + "sessionId": session_id, + "event": stream_event + })); + } + }); + + Ok(()) +} + /// Create the kernel state for Tauri pub fn create_kernel_state() -> KernelState { Arc::new(Mutex::new(None)) diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index ebadf44..1b0f0b2 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1332,6 +1332,7 @@ pub fn run() { kernel_commands::agent_get, kernel_commands::agent_delete, kernel_commands::agent_chat, + kernel_commands::agent_chat_stream, // OpenFang commands (new naming) openfang_status, openfang_start, @@ -1347,7 +1348,6 @@ pub fn run() { openfang_process_logs, openfang_version, // Health check commands - openfang_health_check, openfang_ping, // Backward-compatible aliases (OpenClaw naming) gateway_status,