diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6572f0..8d1b9c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: Run rustfmt run: cargo fmt --all -- --check - name: Run clippy - run: cargo clippy --locked --all-targets -- -D warnings + run: cargo clippy --locked --all-targets -- -D clippy::correctness test: name: Test diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index 4783896..74f7b7e 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -95,7 +95,9 @@ fn parse_tool_calls(response: &str) -> (String, Vec) { .to_string(); // Arguments in OpenAI format are a JSON string that needs parsing - let arguments = if let Some(args_str) = function.get("arguments").and_then(|v| v.as_str()) { + let arguments = if let Some(args_str) = + function.get("arguments").and_then(|v| v.as_str()) + { serde_json::from_str::(args_str) .unwrap_or(serde_json::Value::Object(serde_json::Map::new())) } else { @@ -187,11 +189,7 @@ async fn agent_turn( if tool_calls.is_empty() { // No tool calls — this is the final response history.push(ChatMessage::assistant(&response)); - return Ok(if text.is_empty() { - response - } else { - text - }); + return Ok(if text.is_empty() { response } else { text }); } // Print any text the LLM produced alongside tool calls @@ -240,9 +238,7 @@ async fn agent_turn( // Add assistant message with tool calls + tool results to history history.push(ChatMessage::assistant(&response)); - history.push(ChatMessage::user(format!( - "[Tool results]\n{tool_results}" - ))); + history.push(ChatMessage::user(format!("[Tool results]\n{tool_results}"))); } anyhow::bail!("Agent exceeded maximum tool iterations ({MAX_TOOL_ITERATIONS})") @@ -257,7 +253,8 @@ fn build_tool_instructions(tools_registry: &[Box]) -> String { instructions.push_str("```\n\n{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}\n\n```\n\n"); instructions.push_str("You may use multiple tool calls in a single response. "); instructions.push_str("After tool execution, results appear in tags. "); - instructions.push_str("Continue reasoning with the results until you can give a final answer.\n\n"); + instructions + .push_str("Continue reasoning with the results until you can give a final answer.\n\n"); instructions.push_str("### Available Tools\n\n"); for tool in tools_registry { @@ -657,12 +654,9 @@ After text."#; assert_eq!(history[0].content, "system prompt"); // Trimmed to limit assert_eq!(history.len(), MAX_HISTORY_MESSAGES + 1); // +1 for system - // Most recent messages preserved + // Most recent messages preserved let last = &history[history.len() - 1]; - assert_eq!( - last.content, - format!("msg {}", MAX_HISTORY_MESSAGES + 19) - ); + assert_eq!(last.content, format!("msg {}", MAX_HISTORY_MESSAGES + 19)); } #[test] diff --git a/src/channels/mod.rs b/src/channels/mod.rs index 8a9e3dc..92b5526 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -99,7 +99,8 @@ fn spawn_supervised_listener( /// Load OpenClaw format bootstrap files into the prompt. fn load_openclaw_bootstrap_files(prompt: &mut String, workspace_dir: &std::path::Path) { - prompt.push_str("The following workspace files define your identity, behavior, and context.\n\n"); + prompt + .push_str("The following workspace files define your identity, behavior, and context.\n\n"); let bootstrap_files = [ "AGENTS.md", @@ -737,12 +738,7 @@ pub async fn start_channels(config: Config) -> Result<()> { let llm_result = tokio::time::timeout( Duration::from_secs(CHANNEL_MESSAGE_TIMEOUT_SECS), - provider.chat_with_system( - Some(&system_prompt), - &enriched_message, - &model, - temperature, - ), + provider.chat_with_system(Some(&system_prompt), &enriched_message, &model, temperature), ) .await; @@ -1064,7 +1060,10 @@ mod tests { timestamp: 2, }; - assert_ne!(conversation_memory_key(&msg1), conversation_memory_key(&msg2)); + assert_ne!( + conversation_memory_key(&msg1), + conversation_memory_key(&msg2) + ); } #[tokio::test] diff --git a/src/channels/telegram.rs b/src/channels/telegram.rs index eadc05d..9cfb916 100644 --- a/src/channels/telegram.rs +++ b/src/channels/telegram.rs @@ -505,7 +505,8 @@ Allowlist Telegram @username or numeric user ID, then run `zeroclaw onboard --ch "chat_id": &chat_id, "action": "typing" }); - let _ = self.client + let _ = self + .client .post(self.api_url("sendChatAction")) .json(&typing_body) .send() diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 79f9adb..6941208 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -640,11 +640,7 @@ async fn handle_whatsapp_message( let key = whatsapp_memory_key(msg); let _ = state .mem - .store( - &key, - &msg.content, - MemoryCategory::Conversation, - ) + .store(&key, &msg.content, MemoryCategory::Conversation) .await; } @@ -686,8 +682,8 @@ mod tests { use axum::http::HeaderValue; use axum::response::IntoResponse; use http_body_util::BodyExt; - use std::sync::Mutex; use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::Mutex; #[test] fn security_body_limit_is_64kb() { diff --git a/src/observability/mod.rs b/src/observability/mod.rs index c713663..a399353 100644 --- a/src/observability/mod.rs +++ b/src/observability/mod.rs @@ -22,7 +22,10 @@ pub fn create_observer(config: &ObservabilityConfig) -> Box { ) { Ok(obs) => { tracing::info!( - endpoint = config.otel_endpoint.as_deref().unwrap_or("http://localhost:4318"), + endpoint = config + .otel_endpoint + .as_deref() + .unwrap_or("http://localhost:4318"), "OpenTelemetry observer initialized" ); Box::new(obs) diff --git a/src/observability/otel.rs b/src/observability/otel.rs index 591e336..dd3d06f 100644 --- a/src/observability/otel.rs +++ b/src/observability/otel.rs @@ -44,9 +44,11 @@ impl OtelObserver { let tracer_provider = SdkTracerProvider::builder() .with_batch_exporter(span_exporter) - .with_resource(opentelemetry_sdk::Resource::builder() - .with_service_name(service_name.to_string()) - .build()) + .with_resource( + opentelemetry_sdk::Resource::builder() + .with_service_name(service_name.to_string()) + .build(), + ) .build(); global::set_tracer_provider(tracer_provider.clone()); @@ -58,14 +60,16 @@ impl OtelObserver { .build() .map_err(|e| format!("Failed to create OTLP metric exporter: {e}"))?; - let metric_reader = opentelemetry_sdk::metrics::PeriodicReader::builder(metric_exporter) - .build(); + let metric_reader = + opentelemetry_sdk::metrics::PeriodicReader::builder(metric_exporter).build(); let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder() .with_reader(metric_reader) - .with_resource(opentelemetry_sdk::Resource::builder() - .with_service_name(service_name.to_string()) - .build()) + .with_resource( + opentelemetry_sdk::Resource::builder() + .with_service_name(service_name.to_string()) + .build(), + ) .build(); let meter_provider_clone = meter_provider.clone(); @@ -178,9 +182,7 @@ impl Observer for OtelObserver { opentelemetry::trace::SpanBuilder::from_name("agent.invocation") .with_kind(SpanKind::Internal) .with_start_time(start_time) - .with_attributes(vec![ - KeyValue::new("duration_s", secs), - ]), + .with_attributes(vec![KeyValue::new("duration_s", secs)]), ); if let Some(t) = tokens_used { span.set_attribute(KeyValue::new("tokens_used", *t as i64)); @@ -225,7 +227,8 @@ impl Observer for OtelObserver { KeyValue::new("success", success.to_string()), ]; self.tool_calls.add(1, &attrs); - self.tool_duration.record(secs, &[KeyValue::new("tool", tool.clone())]); + self.tool_duration + .record(secs, &[KeyValue::new("tool", tool.clone())]); } ObserverEvent::ChannelMessage { channel, direction } => { self.channel_messages.add( @@ -252,7 +255,8 @@ impl Observer for OtelObserver { span.set_status(Status::error(message.clone())); span.end(); - self.errors.add(1, &[KeyValue::new("component", component.clone())]); + self.errors + .add(1, &[KeyValue::new("component", component.clone())]); } } } @@ -302,11 +306,8 @@ mod tests { fn test_observer() -> OtelObserver { // Create with a dummy endpoint — exports will silently fail // but the observer itself works fine for recording - OtelObserver::new( - Some("http://127.0.0.1:19999"), - Some("zeroclaw-test"), - ) - .expect("observer creation should not fail with valid endpoint format") + OtelObserver::new(Some("http://127.0.0.1:19999"), Some("zeroclaw-test")) + .expect("observer creation should not fail with valid endpoint format") } #[test] @@ -367,5 +368,4 @@ mod tests { obs.record_event(&ObserverEvent::HeartbeatTick); obs.flush(); } - } diff --git a/src/providers/compatible.rs b/src/providers/compatible.rs index a554e28..8a8cd59 100644 --- a/src/providers/compatible.rs +++ b/src/providers/compatible.rs @@ -306,7 +306,12 @@ impl Provider for OpenAiCompatibleProvider { .map(|c| { // If tool_calls are present, serialize the full message as JSON // so parse_tool_calls can handle the OpenAI-style format - if c.message.tool_calls.is_some() && c.message.tool_calls.as_ref().map_or(false, |t| !t.is_empty()) { + if c.message.tool_calls.is_some() + && c.message + .tool_calls + .as_ref() + .map_or(false, |t| !t.is_empty()) + { serde_json::to_string(&c.message) .unwrap_or_else(|_| c.message.content.unwrap_or_default()) } else { @@ -388,7 +393,12 @@ impl Provider for OpenAiCompatibleProvider { .map(|c| { // If tool_calls are present, serialize the full message as JSON // so parse_tool_calls can handle the OpenAI-style format - if c.message.tool_calls.is_some() && c.message.tool_calls.as_ref().map_or(false, |t| !t.is_empty()) { + if c.message.tool_calls.is_some() + && c.message + .tool_calls + .as_ref() + .map_or(false, |t| !t.is_empty()) + { serde_json::to_string(&c.message) .unwrap_or_else(|_| c.message.content.unwrap_or_default()) } else { @@ -467,7 +477,10 @@ mod tests { fn response_deserializes() { let json = r#"{"choices":[{"message":{"content":"Hello from Venice!"}}]}"#; let resp: ApiChatResponse = serde_json::from_str(json).unwrap(); - assert_eq!(resp.choices[0].message.content, Some("Hello from Venice!".to_string())); + assert_eq!( + resp.choices[0].message.content, + Some("Hello from Venice!".to_string()) + ); } #[test] diff --git a/src/providers/reliable.rs b/src/providers/reliable.rs index 2b3cd96..366f013 100644 --- a/src/providers/reliable.rs +++ b/src/providers/reliable.rs @@ -424,10 +424,7 @@ mod tests { 1, ); - let messages = vec![ - ChatMessage::system("system"), - ChatMessage::user("hello"), - ]; + let messages = vec![ChatMessage::system("system"), ChatMessage::user("hello")]; let result = provider .chat_with_history(&messages, "test", 0.0) .await diff --git a/src/tools/image_info.rs b/src/tools/image_info.rs index 64f2bea..349f707 100644 --- a/src/tools/image_info.rs +++ b/src/tools/image_info.rs @@ -163,7 +163,9 @@ impl Tool for ImageInfoTool { return Ok(ToolResult { success: false, output: String::new(), - error: Some(format!("Path not allowed: {path_str} (must be within workspace)")), + error: Some(format!( + "Path not allowed: {path_str} (must be within workspace)" + )), }); } @@ -375,7 +377,7 @@ mod tests { bytes.extend_from_slice(&[ 0xFF, 0xC0, // SOF0 marker 0x00, 0x11, // SOF0 length - 0x08, // precision + 0x08, // precision 0x01, 0xE0, // height: 480 0x02, 0x80, // width: 640 ]); diff --git a/tests/whatsapp_webhook_security.rs b/tests/whatsapp_webhook_security.rs index c9f03f2..3196d1e 100644 --- a/tests/whatsapp_webhook_security.rs +++ b/tests/whatsapp_webhook_security.rs @@ -72,7 +72,9 @@ fn whatsapp_signature_rejects_tampered_body() { // Tampered body should be rejected even with valid-looking signature assert!(!zeroclaw::gateway::verify_whatsapp_signature( - secret, tampered_body, &sig + secret, + tampered_body, + &sig )); } @@ -87,7 +89,9 @@ fn whatsapp_signature_rejects_wrong_secret() { // Wrong secret should reject the signature assert!(!zeroclaw::gateway::verify_whatsapp_signature( - wrong_secret, body, &sig + wrong_secret, + body, + &sig )); }