diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 5e582d04..d5682065 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -1085,7 +1085,11 @@ impl OpenFangKernel { || disk_manifest.model.model != entry.manifest.model.model || disk_manifest.capabilities.tools - != entry.manifest.capabilities.tools; + != entry.manifest.capabilities.tools + || disk_manifest.tool_allowlist + != entry.manifest.tool_allowlist + || disk_manifest.tool_blocklist + != entry.manifest.tool_blocklist; if changed { info!( agent = %name, @@ -4738,6 +4742,17 @@ impl OpenFangKernel { McpTransportEntry::Sse { url } => McpTransport::Sse { url: url.clone() }, }; + // Resolve env vars from vault/dotenv before passing to MCP subprocess. + // The MCP spawn calls env_clear() then re-adds only whitelisted vars + // from std::env — so we must ensure they're in std::env first. + for var_name in &server_config.env { + if std::env::var(var_name).is_err() { + if let Some(val) = self.resolve_credential(var_name) { + std::env::set_var(var_name, &val); + } + } + } + let mcp_config = McpServerConfig { name: server_config.name.clone(), transport, diff --git a/crates/openfang-runtime/src/agent_loop.rs b/crates/openfang-runtime/src/agent_loop.rs index 2fd481d6..9fc0c736 100644 --- a/crates/openfang-runtime/src/agent_loop.rs +++ b/crates/openfang-runtime/src/agent_loop.rs @@ -244,7 +244,9 @@ pub async fn run_agent_loop( } // Build the messages for the LLM, filtering system messages - // System prompt goes into the separate `system` field + // System prompt goes into the separate `system` field. + // NOTE: We build llm_messages BEFORE stripping images so the LLM + // sees the full image data for the current turn. let llm_messages: Vec = session .messages .iter() @@ -252,6 +254,23 @@ pub async fn run_agent_loop( .cloned() .collect(); + // Strip Image blocks from session to prevent base64 bloat. + // 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 { .. })); + if had_images { + blocks.retain(|b| !matches!(b, ContentBlock::Image { .. })); + if blocks.is_empty() { + blocks.push(ContentBlock::Text { + text: "[Image processed]".to_string(), + provider_metadata: None, + }); + } + } + } + } + // Validate and repair session history (drop orphans, merge consecutive) let mut messages = crate::session_repair::validate_and_repair(&llm_messages); @@ -421,7 +440,7 @@ 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() { + 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 { @@ -1221,6 +1240,23 @@ pub async fn run_agent_loop_streaming( .cloned() .collect(); + // Strip Image blocks from session to prevent base64 bloat. + // 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 { .. })); + if had_images { + blocks.retain(|b| !matches!(b, ContentBlock::Image { .. })); + if blocks.is_empty() { + blocks.push(ContentBlock::Text { + text: "[Image processed]".to_string(), + provider_metadata: None, + }); + } + } + } + } + // Validate and repair session history (drop orphans, merge consecutive) let mut messages = crate::session_repair::validate_and_repair(&llm_messages); @@ -1408,7 +1444,7 @@ 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() { + 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/llm_driver.rs b/crates/openfang-runtime/src/llm_driver.rs index 9178be9d..8fb59405 100644 --- a/crates/openfang-runtime/src/llm_driver.rs +++ b/crates/openfang-runtime/src/llm_driver.rs @@ -93,6 +93,17 @@ impl CompletionResponse { .collect::>() .join("") } + + /// Check if the response has any meaningful content (including Thinking blocks). + /// Used to distinguish true empty responses from thinking-only responses. + pub fn has_any_content(&self) -> bool { + self.content.iter().any(|block| match block { + ContentBlock::Text { text, .. } => !text.is_empty(), + ContentBlock::Thinking { thinking, .. } => !thinking.is_empty(), + ContentBlock::ToolUse { .. } | ContentBlock::Image { .. } => true, + _ => false, + }) + } } /// Events emitted during streaming LLM completion.