diff --git a/Cargo.lock b/Cargo.lock index 7c4172bf..8495e00c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5724,9 +5724,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 03172769..76441cd7 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -367,7 +367,11 @@ pub async fn send_message( // (not as a separate session message which the LLM may not process). let content_blocks = if !req.attachments.is_empty() { let image_blocks = resolve_attachments(&req.attachments); - if image_blocks.is_empty() { None } else { Some(image_blocks) } + if image_blocks.is_empty() { + None + } else { + Some(image_blocks) + } } else { None }; diff --git a/crates/openfang-channels/src/whatsapp.rs b/crates/openfang-channels/src/whatsapp.rs index 06156ab5..16f37b56 100644 --- a/crates/openfang-channels/src/whatsapp.rs +++ b/crates/openfang-channels/src/whatsapp.rs @@ -258,7 +258,8 @@ impl ChannelAdapter for WhatsAppAdapter { "https://graph.facebook.com/v21.0/{}/messages", self.phone_number_id ); - let resp = self.client + let resp = self + .client .post(&api_url) .bearer_auth(&*self.access_token) .json(&body) @@ -284,7 +285,8 @@ impl ChannelAdapter for WhatsAppAdapter { "https://graph.facebook.com/v21.0/{}/messages", self.phone_number_id ); - let resp = self.client + let resp = self + .client .post(&api_url) .bearer_auth(&*self.access_token) .json(&body) @@ -310,7 +312,8 @@ impl ChannelAdapter for WhatsAppAdapter { "https://graph.facebook.com/v21.0/{}/messages", self.phone_number_id ); - let resp = self.client + let resp = self + .client .post(&api_url) .bearer_auth(&*self.access_token) .json(&body) diff --git a/crates/openfang-memory/src/knowledge.rs b/crates/openfang-memory/src/knowledge.rs index 8c87b0ac..e4f5c773 100644 --- a/crates/openfang-memory/src/knowledge.rs +++ b/crates/openfang-memory/src/knowledge.rs @@ -100,11 +100,7 @@ impl KnowledgeStore { let mut idx = 1; if let Some(ref source) = pattern.source { - sql.push_str(&format!( - " AND (s.id = ?{} OR s.name = ?{})", - idx, - idx + 1 - )); + sql.push_str(&format!(" AND (s.id = ?{} OR s.name = ?{})", idx, idx + 1)); params.push(Box::new(source.clone())); params.push(Box::new(source.clone())); idx += 2; @@ -117,11 +113,7 @@ impl KnowledgeStore { idx += 1; } if let Some(ref target) = pattern.target { - sql.push_str(&format!( - " AND (t.id = ?{} OR t.name = ?{})", - idx, - idx + 1 - )); + sql.push_str(&format!(" AND (t.id = ?{} OR t.name = ?{})", idx, idx + 1)); params.push(Box::new(target.clone())); params.push(Box::new(target.clone())); idx += 2; diff --git a/crates/openfang-runtime/src/agent_loop.rs b/crates/openfang-runtime/src/agent_loop.rs index bd68752e..7a3b02c2 100644 --- a/crates/openfang-runtime/src/agent_loop.rs +++ b/crates/openfang-runtime/src/agent_loop.rs @@ -57,8 +57,15 @@ fn phantom_action_detected(text: &str) -> bool { let lower = text.to_lowercase(); let action_verbs = ["sent ", "posted ", "emailed ", "delivered ", "forwarded "]; let channel_refs = [ - "telegram", "whatsapp", "slack", "discord", "email", "channel", - "message sent", "successfully sent", "has been sent", + "telegram", + "whatsapp", + "slack", + "discord", + "email", + "channel", + "message sent", + "successfully sent", + "has been sent", ]; let has_action = action_verbs.iter().any(|v| lower.contains(v)); let has_channel = channel_refs.iter().any(|c| lower.contains(c)); @@ -272,7 +279,9 @@ pub async fn run_agent_loop( // The LLM already received them via llm_messages above. for msg in session.messages.iter_mut() { if let MessageContent::Blocks(blocks) = &mut msg.content { - let had_images = blocks.iter().any(|b| matches!(b, ContentBlock::Image { .. })); + let had_images = blocks + .iter() + .any(|b| matches!(b, ContentBlock::Image { .. })); if had_images { blocks.retain(|b| !matches!(b, ContentBlock::Image { .. })); if blocks.is_empty() { @@ -460,7 +469,10 @@ pub async fn run_agent_loop( // One-shot retry: if the LLM returns empty text with no tool use, // try once more before accepting the empty result. // Triggers on first call OR when input_tokens=0 (silently failed request). - if text.trim().is_empty() && response.tool_calls.is_empty() && !response.has_any_content() { + if text.trim().is_empty() + && response.tool_calls.is_empty() + && !response.has_any_content() + { let is_silent_failure = response.usage.input_tokens == 0 && response.usage.output_tokens == 0; if iteration == 0 || is_silent_failure { @@ -505,7 +517,10 @@ pub async fn run_agent_loop( // channel action (send, post, email, etc.) but never actually // called the corresponding tool, re-prompt once to force real // tool usage instead of hallucinated completion. - let text = if !any_tools_executed && iteration == 0 && phantom_action_detected(&text) { + let text = if !any_tools_executed + && iteration == 0 + && phantom_action_detected(&text) + { warn!(agent = %manifest.name, "Phantom action detected — re-prompting for real tool use"); messages.push(Message::assistant(text)); messages.push(Message::user( @@ -1419,7 +1434,9 @@ pub async fn run_agent_loop_streaming( // The LLM already received them via llm_messages above. for msg in session.messages.iter_mut() { if let MessageContent::Blocks(blocks) = &mut msg.content { - let had_images = blocks.iter().any(|b| matches!(b, ContentBlock::Image { .. })); + let had_images = blocks + .iter() + .any(|b| matches!(b, ContentBlock::Image { .. })); if had_images { blocks.retain(|b| !matches!(b, ContentBlock::Image { .. })); if blocks.is_empty() { @@ -1620,7 +1637,10 @@ pub async fn run_agent_loop_streaming( // One-shot retry: if the LLM returns empty text with no tool use, // try once more before accepting the empty result. // Triggers on first call OR when input_tokens=0 (silently failed request). - if text.trim().is_empty() && response.tool_calls.is_empty() && !response.has_any_content() { + if text.trim().is_empty() + && response.tool_calls.is_empty() + && !response.has_any_content() + { let is_silent_failure = response.usage.input_tokens == 0 && response.usage.output_tokens == 0; if iteration == 0 || is_silent_failure { diff --git a/crates/openfang-runtime/src/drivers/anthropic.rs b/crates/openfang-runtime/src/drivers/anthropic.rs index ad47d8b5..4dab8991 100644 --- a/crates/openfang-runtime/src/drivers/anthropic.rs +++ b/crates/openfang-runtime/src/drivers/anthropic.rs @@ -471,9 +471,8 @@ impl LlmDriver for AnthropicDriver { input_json, }) = blocks.get(block_idx) { - let input: serde_json::Value = - serde_json::from_str(input_json) - .unwrap_or_else(|_| serde_json::json!({})); + let input: serde_json::Value = serde_json::from_str(input_json) + .unwrap_or_else(|_| serde_json::json!({})); let _ = tx .send(StreamEvent::ToolUseEnd { id: id.clone(), diff --git a/crates/openfang-runtime/src/mcp.rs b/crates/openfang-runtime/src/mcp.rs index c15bb86a..b9f5f381 100644 --- a/crates/openfang-runtime/src/mcp.rs +++ b/crates/openfang-runtime/src/mcp.rs @@ -174,10 +174,7 @@ impl McpConnection { .or_else(|| strip_mcp_prefix(&self.config.name, name).map(|s| s.to_string())) .unwrap_or_else(|| name.to_string()); - let args = arguments - .as_object() - .cloned() - .unwrap_or_default(); + let args = arguments.as_object().cloned().unwrap_or_default(); debug!(tool = %raw_name, server = %self.config.name, "MCP tool call"); @@ -236,42 +233,41 @@ impl McpConnection { let args_vec: Vec = args.to_vec(); let env_list: Vec = env_whitelist.to_vec(); - let transport = - TokioChildProcess::new(Command::new(&cmd_str).configure(move |cmd| { - for arg in &args_vec { - cmd.arg(arg); + let transport = TokioChildProcess::new(Command::new(&cmd_str).configure(move |cmd| { + for arg in &args_vec { + cmd.arg(arg); + } + // Sandbox: clear environment, only pass whitelisted vars + cmd.env_clear(); + for var_name in &env_list { + if let Ok(val) = std::env::var(var_name) { + cmd.env(var_name, val); } - // Sandbox: clear environment, only pass whitelisted vars - cmd.env_clear(); - for var_name in &env_list { - if let Ok(val) = std::env::var(var_name) { - cmd.env(var_name, val); + } + // Always pass PATH for binary resolution + if let Ok(path) = std::env::var("PATH") { + cmd.env("PATH", path); + } + // On Windows, npm/node need extra vars + if cfg!(windows) { + for var in &[ + "APPDATA", + "LOCALAPPDATA", + "USERPROFILE", + "SystemRoot", + "TEMP", + "TMP", + "HOME", + "HOMEDRIVE", + "HOMEPATH", + ] { + if let Ok(val) = std::env::var(var) { + cmd.env(var, val); } } - // Always pass PATH for binary resolution - if let Ok(path) = std::env::var("PATH") { - cmd.env("PATH", path); - } - // On Windows, npm/node need extra vars - if cfg!(windows) { - for var in &[ - "APPDATA", - "LOCALAPPDATA", - "USERPROFILE", - "SystemRoot", - "TEMP", - "TMP", - "HOME", - "HOMEDRIVE", - "HOMEPATH", - ] { - if let Ok(val) = std::env::var(var) { - cmd.env(var, val); - } - } - } - })) - .map_err(|e| format!("Failed to spawn MCP server '{cmd_str}': {e}"))?; + } + })) + .map_err(|e| format!("Failed to spawn MCP server '{cmd_str}': {e}"))?; let client = client_info .serve(transport)