Rust SDK
The Diminuendo Rust SDK provides a fully async WebSocket client designed for Tauri v2 desktop applications. It is Send + Sync safe, non-blocking, and communicates with the gateway using the same wire protocol as the TypeScript SDK.
Installation
Add the SDK to your Cargo.toml:
[dependencies]
diminuendo-sdk = { path = "sdk/rust" }
Dependencies
The SDK depends on the following crates:
| Crate | Purpose |
|---|
tokio | Async runtime |
tokio-tungstenite | WebSocket client |
serde / serde_json | JSON serialization with snake_case to camelCase mapping |
thiserror | Typed error derivation |
tracing | Structured logging |
futures-util | Stream/Sink extensions for WebSocket I/O |
DiminuendoClient
The client is constructed via the async connect() associated function, which returns a tuple of (DiminuendoClient, mpsc::UnboundedReceiver<ServerEvent>). All server events arrive through the channel receiver; the client handle is used to send messages.
use diminuendo_sdk::{DiminuendoClient, ClientOptions, ServerEvent};
let options = ClientOptions {
url: "ws://localhost:8080/ws".to_string(),
token: "your-jwt-token".to_string(), // Empty string for dev mode
};
let (client, mut events) = DiminuendoClient::connect(options).await?;
The connect() function spawns a background tokio task that handles all WebSocket I/O. The task reads from the WebSocket and forwards parsed events to the events channel. Commands sent via the client are forwarded to the WebSocket write half via an internal command channel.
Event Delivery
All server events arrive through the mpsc::UnboundedReceiver<ServerEvent> channel returned by connect(). The consumer should spawn a dedicated task to process events:
tokio::spawn(async move {
while let Some(event) = events.recv().await {
match event {
ServerEvent::Authenticated { identity } => {
println!("Authenticated as {}", identity.email);
}
ServerEvent::StateSnapshot { session_id, session, .. } => {
println!("Joined session {} (status: {})", session_id, session.status);
}
ServerEvent::TextDelta { text, session_id, .. } => {
print!("{}", text);
}
ServerEvent::TurnComplete { session_id, .. } => {
println!("\nTurn complete for {}", session_id);
}
ServerEvent::ToolCall { tool_name, args, .. } => {
println!("Tool call: {}({:?})", tool_name, args);
}
ServerEvent::TurnError { message, .. } => {
eprintln!("Error: {}", message);
}
_ => {}
}
}
println!("Event channel closed");
});
Client Methods
All client methods are fire-and-forget — they serialize the message to JSON and send it to the background WebSocket task via an internal command channel. Each returns Result<(), ClientError>.
Session Management
// List sessions
client.list_sessions(false)?; // include_archived = false
client.list_sessions(true)?; // include archived sessions
// Create a session
client.create_session("coding-agent", Some("My Session"))?;
client.create_session("echo", None)?; // No name
// Rename, archive, unarchive, delete
client.rename_session("session-id", "New Name")?;
client.archive_session("session-id")?;
client.unarchive_session("session-id")?;
client.delete_session("session-id")?;
Session Interaction
// Join a session (subscribe to events)
client.join_session("session-id", None)?;
client.join_session("session-id", Some(42))?; // Resume from seq 42
// Leave a session
client.leave_session("session-id")?;
// Start a turn
client.run_turn("session-id", "Fix the bug", None)?;
client.run_turn("session-id", "Add tests", Some("client-turn-123"))?;
// Stop the current turn
client.stop_turn("session-id")?;
// Mid-turn guidance
client.steer("session-id", "Focus on error handling")?;
// Answer a question from the agent
let mut answers = std::collections::HashMap::new();
answers.insert("language".to_string(), "Rust".to_string());
client.answer_question("session-id", "request-id", answers, false)?;
// Get history and events
client.get_history("session-id", Some(0), Some(50))?;
client.get_events("session-id", Some(0), Some(200))?;
File Access
client.list_files("session-id", None, None)?;
client.list_files("session-id", Some("src/"), Some(2))?;
client.read_file("session-id", "src/main.rs")?;
client.file_history("session-id", "src/main.rs")?;
client.file_at_iteration("session-id", "src/main.rs", 3)?;
Member Management
client.manage_members("list", None, None)?;
client.manage_members("set_role", Some("user-id"), Some("admin"))?;
client.manage_members("remove", Some("user-id"), None)?;
Utility
// Ping the gateway
client.ping()?;
// Close the connection
client.close();
Types
ClientMessage
The ClientMessage enum represents all messages a client can send to the gateway. It is serialized with #[serde(tag = "type", rename_all = "snake_case")], producing JSON with a type discriminator and camelCase field names:
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage {
Authenticate { token: String },
ListSessions { include_archived: Option<bool> },
CreateSession { agent_type: String, name: Option<String> },
JoinSession { session_id: String, after_seq: Option<i64> },
LeaveSession { session_id: String },
RunTurn { session_id: String, text: String, client_turn_id: Option<String> },
StopTurn { session_id: String },
Steer { session_id: String, content: String },
AnswerQuestion { session_id: String, request_id: String, answers: HashMap<String, String>, dismissed: Option<bool> },
// ... and more
Ping { ts: i64 },
}
ServerEvent
The ServerEvent enum represents all events the gateway can send. It uses #[serde(tag = "type", rename_all = "snake_case")] for deserialization, with an Unknown catch-all variant for forward compatibility:
Session Events
Turn Events
Tool Events
Other Events
ServerEvent::SessionList { sessions: Vec<SessionMeta> }
ServerEvent::SessionCreated { session: SessionMeta }
ServerEvent::SessionUpdated { session: SessionMeta }
ServerEvent::SessionArchived { session: SessionMeta }
ServerEvent::SessionUnarchived { session: SessionMeta }
ServerEvent::SessionDeleted { session_id: String }
ServerEvent::StateSnapshot { session_id, session, current_turn, recent_history, subscriber_count, sandbox }
ServerEvent::TurnStarted { session_id, turn_id, seq, ts }
ServerEvent::TextDelta { session_id, turn_id, text, seq, ts }
ServerEvent::TurnComplete { session_id, turn_id, final_text, seq, ts }
ServerEvent::TurnError { session_id, turn_id, message, code, seq, ts }
ServerEvent::ToolCallStart { session_id, turn_id, tool_call_id, tool_name, seq, ts }
ServerEvent::ToolCallDelta { session_id, turn_id, tool_call_id, delta, seq, ts }
ServerEvent::ToolCall { session_id, turn_id, tool_call_id, tool_name, args, seq, ts }
ServerEvent::ToolResult { session_id, turn_id, tool_call_id, status, output, seq, ts }
ServerEvent::ToolError { session_id, turn_id, tool_call_id, error, seq, ts }
ServerEvent::ThinkingStart { session_id, turn_id, seq, ts }
ServerEvent::ThinkingProgress { session_id, turn_id, text, seq, ts }
ServerEvent::ThinkingComplete { session_id, turn_id, seq, ts }
ServerEvent::TerminalStream { session_id, turn_id, data, seq, ts }
ServerEvent::TerminalComplete { session_id, turn_id, exit_code, seq, ts }
ServerEvent::UsageUpdate { session_id, turn_id, input_tokens, output_tokens, ... }
ServerEvent::Unknown // Catch-all for unrecognized event types
Supporting Types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionMeta {
pub id: String,
pub tenant_id: String,
pub name: Option<String>,
pub agent_type: String,
pub status: String,
pub archived: bool,
pub created_at: i64,
pub updated_at: i64,
pub last_activity_at: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Identity {
pub user_id: String,
pub email: String,
pub tenant_id: String,
}
Serde Mapping
Rust fields use snake_case while the JSON wire format uses camelCase. This mapping is handled transparently by #[serde(rename)] attributes:
| Rust Field | JSON Field |
|---|
session_id | sessionId |
agent_type | agentType |
tool_call_id | toolCallId |
after_seq | afterSeq |
include_archived | includeArchived |
client_turn_id | clientTurnId |
Error Types
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("WebSocket error: {0}")]
WebSocket(String),
#[error("Connection failed: {0}")]
Connection(String),
#[error("Authentication timed out")]
AuthTimeout,
#[error("Request timed out: {0}")]
RequestTimeout(String),
#[error("Not connected")]
NotConnected,
#[error("Serialization error: {0}")]
Serde(#[from] serde_json::Error),
}
Complete Example
use diminuendo_sdk::{DiminuendoClient, ClientOptions, ServerEvent};
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = ClientOptions {
url: "ws://localhost:8080/ws".to_string(),
token: std::env::var("GATEWAY_TOKEN").unwrap_or_default(),
};
let (client, mut events) = DiminuendoClient::connect(options).await?;
// Spawn event handler
let event_client = client.clone(); // Note: clone not available — use Arc if needed
tokio::spawn(async move {
while let Some(event) = events.recv().await {
match event {
ServerEvent::Authenticated { identity } => {
println!("Authenticated as {}", identity.email);
}
ServerEvent::SessionCreated { session } => {
println!("Created session: {}", session.id);
}
ServerEvent::StateSnapshot { session, .. } => {
println!("Session status: {}", session.status);
}
ServerEvent::TurnStarted { turn_id, .. } => {
println!("Turn started: {}", turn_id);
}
ServerEvent::TextDelta { text, .. } => {
print!("{}", text);
}
ServerEvent::ToolCall { tool_name, args, .. } => {
println!("\nTool: {}({:?})", tool_name, args);
}
ServerEvent::ToolResult { status, output, .. } => {
println!("Result [{}]: {:?}", status, output);
}
ServerEvent::TurnComplete { .. } => {
println!("\nTurn complete.");
}
ServerEvent::TurnError { message, .. } => {
eprintln!("Error: {}", message);
}
_ => {}
}
}
});
// Wait for authentication (events arrive via the spawned task)
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
// Create and join a session
client.create_session("coding-agent", Some("Bug Fix"))?;
// Wait for session_created event, then join
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Start a turn
// In practice, you'd extract the session ID from the SessionCreated event
// client.run_turn(&session_id, "Fix the authentication bug", None)?;
// Keep alive
tokio::signal::ctrl_c().await?;
client.close();
Ok(())
}
In a Tauri application, the DiminuendoClient is typically held in application state (wrapped in Arc<Mutex<...>> or managed by Tauri’s state management). Tauri commands invoke client methods, and the event receiver task emits Tauri events via app_handle.emit("gateway-event", &event) for the frontend to consume.