From a54bb1cd4fa77f22b70253407cdac2eefb264914 Mon Sep 17 00:00:00 2001 From: jaberjaber23 Date: Mon, 2 Mar 2026 15:14:58 +0300 Subject: [PATCH] batch fixes --- Cargo.lock | 28 ++++----- Cargo.toml | 2 +- crates/openfang-channels/src/discord.rs | 15 +++-- crates/openfang-kernel/src/kernel.rs | 60 ++++++++++++------- crates/openfang-runtime/src/context_budget.rs | 40 ++++++++++++- .../openfang-runtime/src/drivers/anthropic.rs | 8 +-- .../openfang-runtime/src/drivers/fallback.rs | 34 ++++++++--- crates/openfang-runtime/src/drivers/gemini.rs | 4 +- crates/openfang-runtime/src/drivers/openai.rs | 4 +- crates/openfang-runtime/src/model_catalog.rs | 19 +----- crates/openfang-runtime/src/prompt_builder.rs | 7 +++ crates/openfang-types/src/config.rs | 6 +- 12 files changed, 148 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7778e359..d9517219 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3866,7 +3866,7 @@ dependencies = [ [[package]] name = "openfang-api" -version = "0.2.6" +version = "0.2.7" dependencies = [ "async-trait", "axum", @@ -3902,7 +3902,7 @@ dependencies = [ [[package]] name = "openfang-channels" -version = "0.2.6" +version = "0.2.7" dependencies = [ "async-trait", "axum", @@ -3933,7 +3933,7 @@ dependencies = [ [[package]] name = "openfang-cli" -version = "0.2.6" +version = "0.2.7" dependencies = [ "clap", "clap_complete", @@ -3960,7 +3960,7 @@ dependencies = [ [[package]] name = "openfang-desktop" -version = "0.2.6" +version = "0.2.7" dependencies = [ "axum", "open", @@ -3986,7 +3986,7 @@ dependencies = [ [[package]] name = "openfang-extensions" -version = "0.2.6" +version = "0.2.7" dependencies = [ "aes-gcm", "argon2", @@ -4014,7 +4014,7 @@ dependencies = [ [[package]] name = "openfang-hands" -version = "0.2.6" +version = "0.2.7" dependencies = [ "chrono", "dashmap", @@ -4031,7 +4031,7 @@ dependencies = [ [[package]] name = "openfang-kernel" -version = "0.2.6" +version = "0.2.7" dependencies = [ "async-trait", "chrono", @@ -4067,7 +4067,7 @@ dependencies = [ [[package]] name = "openfang-memory" -version = "0.2.6" +version = "0.2.7" dependencies = [ "async-trait", "chrono", @@ -4086,7 +4086,7 @@ dependencies = [ [[package]] name = "openfang-migrate" -version = "0.2.6" +version = "0.2.7" dependencies = [ "chrono", "dirs 6.0.0", @@ -4105,7 +4105,7 @@ dependencies = [ [[package]] name = "openfang-runtime" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", "async-trait", @@ -4137,7 +4137,7 @@ dependencies = [ [[package]] name = "openfang-skills" -version = "0.2.6" +version = "0.2.7" dependencies = [ "chrono", "hex", @@ -4159,7 +4159,7 @@ dependencies = [ [[package]] name = "openfang-types" -version = "0.2.6" +version = "0.2.7" dependencies = [ "async-trait", "chrono", @@ -4178,7 +4178,7 @@ dependencies = [ [[package]] name = "openfang-wire" -version = "0.2.6" +version = "0.2.7" dependencies = [ "async-trait", "chrono", @@ -8790,7 +8790,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xtask" -version = "0.2.6" +version = "0.2.7" [[package]] name = "yoke" diff --git a/Cargo.toml b/Cargo.toml index b812dffe..a9fcdb7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.2.7" +version = "0.2.8" edition = "2021" license = "Apache-2.0 OR MIT" repository = "https://github.com/RightNow-AI/openfang" diff --git a/crates/openfang-channels/src/discord.rs b/crates/openfang-channels/src/discord.rs index 0c52b6ce..465b2cd9 100644 --- a/crates/openfang-channels/src/discord.rs +++ b/crates/openfang-channels/src/discord.rs @@ -38,7 +38,7 @@ pub struct DiscordAdapter { /// SECURITY: Bot token is zeroized on drop to prevent memory disclosure. token: Zeroizing, client: reqwest::Client, - allowed_guilds: Vec, + allowed_guilds: Vec, intents: u64, shutdown_tx: Arc>, shutdown_rx: watch::Receiver, @@ -51,7 +51,7 @@ pub struct DiscordAdapter { } impl DiscordAdapter { - pub fn new(token: String, allowed_guilds: Vec, intents: u64) -> Self { + pub fn new(token: String, allowed_guilds: Vec, intents: u64) -> Self { let (shutdown_tx, shutdown_rx) = watch::channel(false); Self { token: Zeroizing::new(token), @@ -422,7 +422,7 @@ impl ChannelAdapter for DiscordAdapter { async fn parse_discord_message( d: &serde_json::Value, bot_user_id: &Arc>>, - allowed_guilds: &[u64], + allowed_guilds: &[String], ) -> Option { let author = d.get("author")?; let author_id = author["id"].as_str()?; @@ -442,8 +442,7 @@ async fn parse_discord_message( // Filter by allowed guilds if !allowed_guilds.is_empty() { if let Some(guild_id) = d["guild_id"].as_str() { - let gid: u64 = guild_id.parse().unwrap_or(0); - if !allowed_guilds.contains(&gid) { + if !allowed_guilds.iter().any(|g| g == guild_id) { return None; } } @@ -587,11 +586,11 @@ mod tests { }); // Not in allowed guilds - let msg = parse_discord_message(&d, &bot_id, &[111, 222]).await; + let msg = parse_discord_message(&d, &bot_id, &["111".into(), "222".into()]).await; assert!(msg.is_none()); // In allowed guilds - let msg = parse_discord_message(&d, &bot_id, &[999]).await; + let msg = parse_discord_message(&d, &bot_id, &["999".into()]).await; assert!(msg.is_some()); } @@ -685,7 +684,7 @@ mod tests { #[test] fn test_discord_adapter_creation() { - let adapter = DiscordAdapter::new("test-token".to_string(), vec![123, 456], 33280); + let adapter = DiscordAdapter::new("test-token".to_string(), vec!["123".to_string(), "456".to_string()], 33280); assert_eq!(adapter.name(), "discord"); assert_eq!(adapter.channel_type(), ChannelType::Discord); } diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 65059c47..0666e769 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -924,14 +924,20 @@ impl OpenFangKernel { && restored_entry.manifest.model.base_url.is_none() { let dm = &kernel.config.default_model; - if !dm.provider.is_empty() { - restored_entry.manifest.model.provider = dm.provider.clone(); - } - if !dm.model.is_empty() { - restored_entry.manifest.model.model = dm.model.clone(); - } - if dm.base_url.is_some() { - restored_entry.manifest.model.base_url = dm.base_url.clone(); + let is_default_provider = restored_entry.manifest.model.provider.is_empty() + || restored_entry.manifest.model.provider == "anthropic"; + let is_default_model = restored_entry.manifest.model.model.is_empty() + || restored_entry.manifest.model.model == "claude-sonnet-4-20250514"; + if is_default_provider && is_default_model { + if !dm.provider.is_empty() { + restored_entry.manifest.model.provider = dm.provider.clone(); + } + if !dm.model.is_empty() { + restored_entry.manifest.model.model = dm.model.clone(); + } + if dm.base_url.is_some() { + restored_entry.manifest.model.base_url = dm.base_url.clone(); + } } } @@ -998,18 +1004,26 @@ impl OpenFangKernel { } info!(agent = %name, id = %agent_id, exec_mode = ?manifest.exec_policy.as_ref().map(|p| &p.mode), "Agent exec_policy resolved"); - // Overlay kernel default_model onto agent if no custom key/url is set. - // This ensures agents respect the user's configured provider from `openfang init`. + // Overlay kernel default_model onto agent if agent didn't explicitly choose. + // Only override provider/model when the agent uses the generic defaults + // (empty or the compile-time default "anthropic"/"claude-sonnet-4-20250514"). + // This preserves explicit model choices like provider="groq", model="llama-3.3-70b". if manifest.model.api_key_env.is_none() && manifest.model.base_url.is_none() { let dm = &self.config.default_model; - if !dm.provider.is_empty() { - manifest.model.provider = dm.provider.clone(); - } - if !dm.model.is_empty() { - manifest.model.model = dm.model.clone(); - } - if dm.base_url.is_some() { - manifest.model.base_url = dm.base_url.clone(); + let is_default_provider = manifest.model.provider.is_empty() + || manifest.model.provider == "anthropic"; + let is_default_model = manifest.model.model.is_empty() + || manifest.model.model == "claude-sonnet-4-20250514"; + if is_default_provider && is_default_model { + if !dm.provider.is_empty() { + manifest.model.provider = dm.provider.clone(); + } + if !dm.model.is_empty() { + manifest.model.model = dm.model.clone(); + } + if dm.base_url.is_some() { + manifest.model.base_url = dm.base_url.clone(); + } } } @@ -1448,6 +1462,7 @@ impl OpenFangKernel { None }, peer_agents, + current_date: Some(chrono::Local::now().format("%A, %B %d, %Y (%Y-%m-%d %H:%M %Z)").to_string()), }; manifest.model.system_prompt = openfang_runtime::prompt_builder::build_system_prompt(&prompt_ctx); @@ -1916,6 +1931,7 @@ impl OpenFangKernel { None }, peer_agents, + current_date: Some(chrono::Local::now().format("%A, %B %d, %Y (%Y-%m-%d %H:%M %Z)").to_string()), }; manifest.model.system_prompt = openfang_runtime::prompt_builder::build_system_prompt(&prompt_ctx); @@ -3682,7 +3698,9 @@ impl OpenFangKernel { // If fallback models are configured, wrap in FallbackDriver if !manifest.fallback_models.is_empty() { - let mut chain = vec![primary.clone()]; + // Primary driver uses the agent's own model name (already set in request) + let mut chain: Vec<(std::sync::Arc, String)> = + vec![(primary.clone(), String::new())]; for fb in &manifest.fallback_models { let config = DriverConfig { provider: fb.provider.clone(), @@ -3693,7 +3711,7 @@ impl OpenFangKernel { base_url: fb.base_url.clone(), }; match drivers::create_driver(&config) { - Ok(d) => chain.push(d), + Ok(d) => chain.push((d, fb.model.clone())), Err(e) => { warn!("Fallback driver '{}' failed to init: {e}", fb.provider); } @@ -3701,7 +3719,7 @@ impl OpenFangKernel { } if chain.len() > 1 { return Ok(Arc::new( - openfang_runtime::drivers::fallback::FallbackDriver::new(chain), + openfang_runtime::drivers::fallback::FallbackDriver::with_models(chain), )); } } diff --git a/crates/openfang-runtime/src/context_budget.rs b/crates/openfang-runtime/src/context_budget.rs index e80751e4..cbf84490 100644 --- a/crates/openfang-runtime/src/context_budget.rs +++ b/crates/openfang-runtime/src/context_budget.rs @@ -145,7 +145,14 @@ pub fn apply_context_guard( let mut compacted = 0; for loc in &locations { if loc.char_len > single_max { + // Bounds check: indices may be stale if messages were modified concurrently + if loc.msg_idx >= messages.len() { + continue; + } if let MessageContent::Blocks(blocks) = &mut messages[loc.msg_idx].content { + if loc.block_idx >= blocks.len() { + continue; + } if let ContentBlock::ToolResult { content, .. } = &mut blocks[loc.block_idx] { let old_len = content.len(); *content = truncate_to(content, single_max); @@ -167,7 +174,13 @@ pub fn apply_context_guard( if loc.char_len <= compact_target { continue; } + if loc.msg_idx >= messages.len() { + continue; + } if let MessageContent::Blocks(blocks) = &mut messages[loc.msg_idx].content { + if loc.block_idx >= blocks.len() { + continue; + } if let ContentBlock::ToolResult { content, .. } = &mut blocks[loc.block_idx] { if content.len() > compact_target { let old_len = content.len(); @@ -188,11 +201,32 @@ fn truncate_to(content: &str, max_chars: usize) -> String { if content.len() <= max_chars { return content.to_string(); } - let keep = max_chars.saturating_sub(80); + let keep = max_chars.saturating_sub(80).min(content.len()); + // Ensure keep is a valid char boundary + let keep = if content.is_char_boundary(keep) { + keep + } else { + content[..keep] + .char_indices() + .next_back() + .map(|(i, _)| i) + .unwrap_or(0) + }; + let search_start = keep.saturating_sub(100); + // Ensure search_start is a valid char boundary + let search_start = if content.is_char_boundary(search_start) { + search_start + } else { + content[..search_start] + .char_indices() + .next_back() + .map(|(i, _)| i) + .unwrap_or(0) + }; // Try to break at newline - let break_point = content[keep.saturating_sub(100)..keep] + let break_point = content[search_start..keep] .rfind('\n') - .map(|pos| keep.saturating_sub(100) + pos) + .map(|pos| search_start + pos) .unwrap_or(keep); format!( "{}\n\n[COMPACTED: {} → {} chars by context guard]", diff --git a/crates/openfang-runtime/src/drivers/anthropic.rs b/crates/openfang-runtime/src/drivers/anthropic.rs index d99bfccb..d1c564e8 100644 --- a/crates/openfang-runtime/src/drivers/anthropic.rs +++ b/crates/openfang-runtime/src/drivers/anthropic.rs @@ -365,10 +365,10 @@ impl LlmDriver for AnthropicDriver { let mut event_type = String::new(); let mut data = String::new(); for line in event_text.lines() { - if let Some(et) = line.strip_prefix("event: ") { - event_type = et.to_string(); - } else if let Some(d) = line.strip_prefix("data: ") { - data = d.to_string(); + if let Some(et) = line.strip_prefix("event:") { + event_type = et.trim_start().to_string(); + } else if let Some(d) = line.strip_prefix("data:") { + data = d.trim_start().to_string(); } } diff --git a/crates/openfang-runtime/src/drivers/fallback.rs b/crates/openfang-runtime/src/drivers/fallback.rs index cd72ca11..c6094f06 100644 --- a/crates/openfang-runtime/src/drivers/fallback.rs +++ b/crates/openfang-runtime/src/drivers/fallback.rs @@ -12,15 +12,23 @@ use tracing::warn; /// /// On failure (including rate-limit and overload), moves to the next driver. /// Only returns an error when ALL drivers in the chain are exhausted. +/// Each driver is paired with the model name it should use. pub struct FallbackDriver { - drivers: Vec>, + drivers: Vec<(Arc, String)>, } impl FallbackDriver { - /// Create a new fallback driver from an ordered chain of drivers. + /// Create a new fallback driver from an ordered chain of (driver, model_name) pairs. /// - /// The first driver is the primary; subsequent are fallbacks. + /// The first entry is the primary; subsequent are fallbacks. pub fn new(drivers: Vec>) -> Self { + Self { + drivers: drivers.into_iter().map(|d| (d, String::new())).collect(), + } + } + + /// Create a new fallback driver with explicit model names for each driver. + pub fn with_models(drivers: Vec<(Arc, String)>) -> Self { Self { drivers } } } @@ -30,12 +38,17 @@ impl LlmDriver for FallbackDriver { async fn complete(&self, request: CompletionRequest) -> Result { let mut last_error = None; - for (i, driver) in self.drivers.iter().enumerate() { - match driver.complete(request.clone()).await { + for (i, (driver, model_name)) in self.drivers.iter().enumerate() { + let mut req = request.clone(); + if !model_name.is_empty() { + req.model = model_name.clone(); + } + match driver.complete(req).await { Ok(response) => return Ok(response), Err(e @ LlmError::RateLimited { .. }) | Err(e @ LlmError::Overloaded { .. }) => { warn!( driver_index = i, + model = %model_name, error = %e, "Driver rate-limited/overloaded, trying next fallback" ); @@ -44,6 +57,7 @@ impl LlmDriver for FallbackDriver { Err(e) => { warn!( driver_index = i, + model = %model_name, error = %e, "Fallback driver failed, trying next" ); @@ -65,12 +79,17 @@ impl LlmDriver for FallbackDriver { ) -> Result { let mut last_error = None; - for (i, driver) in self.drivers.iter().enumerate() { - match driver.stream(request.clone(), tx.clone()).await { + for (i, (driver, model_name)) in self.drivers.iter().enumerate() { + let mut req = request.clone(); + if !model_name.is_empty() { + req.model = model_name.clone(); + } + match driver.stream(req, tx.clone()).await { Ok(response) => return Ok(response), Err(e @ LlmError::RateLimited { .. }) | Err(e @ LlmError::Overloaded { .. }) => { warn!( driver_index = i, + model = %model_name, error = %e, "Driver rate-limited/overloaded (stream), trying next fallback" ); @@ -79,6 +98,7 @@ impl LlmDriver for FallbackDriver { Err(e) => { warn!( driver_index = i, + model = %model_name, error = %e, "Fallback driver (stream) failed, trying next" ); diff --git a/crates/openfang-runtime/src/drivers/gemini.rs b/crates/openfang-runtime/src/drivers/gemini.rs index c150014f..5d58b7e6 100644 --- a/crates/openfang-runtime/src/drivers/gemini.rs +++ b/crates/openfang-runtime/src/drivers/gemini.rs @@ -525,10 +525,10 @@ impl LlmDriver for GeminiDriver { let event_text = buffer[..pos].to_string(); buffer = buffer[pos + 2..].to_string(); - // Extract the data line + // Extract the data line (handle both "data: " and "data:" formats) let data = event_text .lines() - .find_map(|line| line.strip_prefix("data: ")) + .find_map(|line| line.strip_prefix("data:").map(|d| d.trim_start())) .unwrap_or(""); if data.is_empty() { diff --git a/crates/openfang-runtime/src/drivers/openai.rs b/crates/openfang-runtime/src/drivers/openai.rs index 0776771e..b7455109 100644 --- a/crates/openfang-runtime/src/drivers/openai.rs +++ b/crates/openfang-runtime/src/drivers/openai.rs @@ -660,8 +660,8 @@ impl LlmDriver for OpenAIDriver { continue; } - let data = match line.strip_prefix("data: ") { - Some(d) => d, + let data = match line.strip_prefix("data:") { + Some(d) => d.trim_start(), None => continue, }; diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index ddd68fa1..fc99d54b 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -647,8 +647,8 @@ fn builtin_aliases() -> HashMap { ("gpt4-mini", "gpt-4o-mini"), ("gpt5", "gpt-5.2"), ("gpt5-mini", "gpt-5-mini"), - ("flash", "gemini-3-flash"), - ("gemini-flash", "gemini-3-flash"), + ("flash", "gemini-2.5-flash"), + ("gemini-flash", "gemini-2.5-flash"), ("gemini-pro", "gemini-3.1-pro"), ("deepseek", "deepseek-chat"), ("llama", "llama-3.3-70b-versatile"), @@ -1052,20 +1052,7 @@ fn builtin_models() -> Vec { supports_streaming: true, aliases: vec!["gemini-pro".into()], }, - ModelCatalogEntry { - id: "gemini-3-flash".into(), - display_name: "Gemini 3 Flash".into(), - provider: "gemini".into(), - tier: ModelTier::Smart, - context_window: 1_048_576, - max_output_tokens: 65_536, - input_cost_per_m: 0.50, - output_cost_per_m: 3.0, - supports_tools: true, - supports_vision: true, - supports_streaming: true, - aliases: vec!["flash".into(), "gemini-flash".into()], - }, + // gemini-3-flash removed: model doesn't exist. Use gemini-2.5-flash instead. ModelCatalogEntry { id: "gemini-3-deep-think".into(), display_name: "Gemini 3 Deep Think".into(), diff --git a/crates/openfang-runtime/src/prompt_builder.rs b/crates/openfang-runtime/src/prompt_builder.rs index 1e9f5ea8..a2cae0df 100644 --- a/crates/openfang-runtime/src/prompt_builder.rs +++ b/crates/openfang-runtime/src/prompt_builder.rs @@ -53,6 +53,8 @@ pub struct PromptContext { pub heartbeat_md: Option, /// Peer agents visible to this agent: (name, state, model). pub peer_agents: Vec<(String, String, String)>, + /// Current date/time string for temporal awareness. + pub current_date: Option, } /// Build the complete system prompt from a `PromptContext`. @@ -66,6 +68,11 @@ pub fn build_system_prompt(ctx: &PromptContext) -> String { // Section 1 — Agent Identity (always present) sections.push(build_identity_section(ctx)); + // Section 1.5 — Current Date/Time (always present when set) + if let Some(ref date) = ctx.current_date { + sections.push(format!("## Current Date\nToday is {date}.")); + } + // Section 2 — Tool Call Behavior (skip for subagents) if !ctx.is_subagent { sections.push(TOOL_CALL_BEHAVIOR.to_string()); diff --git a/crates/openfang-types/src/config.rs b/crates/openfang-types/src/config.rs index 66a2eb65..6fb7ed13 100644 --- a/crates/openfang-types/src/config.rs +++ b/crates/openfang-types/src/config.rs @@ -746,11 +746,14 @@ impl Default for CanvasConfig { #[serde(rename_all = "lowercase")] pub enum ExecSecurityMode { /// Block all shell execution. + #[serde(alias = "none", alias = "disabled")] Deny, /// Only allow commands in safe_bins or allowed_commands. #[default] + #[serde(alias = "restricted")] Allowlist, /// Allow all commands (unsafe, dev only). + #[serde(alias = "allow", alias = "all", alias = "unrestricted")] Full, } @@ -1549,7 +1552,8 @@ pub struct DiscordConfig { /// Env var name holding the bot token (NOT the token itself). pub bot_token_env: String, /// Guild (server) IDs allowed to interact (empty = allow all). - pub allowed_guilds: Vec, + /// Accepts strings for consistency with other channel configs. + pub allowed_guilds: Vec, /// Default agent name to route messages to. pub default_agent: Option, /// Gateway intents bitmask (default: 33280 = GUILD_MESSAGES | MESSAGE_CONTENT).