batch fixes

This commit is contained in:
jaberjaber23
2026-03-02 15:14:58 +03:00
parent 6f3c4e7778
commit a54bb1cd4f
12 changed files with 148 additions and 79 deletions
Generated
+14 -14
View File
@@ -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"
+1 -1
View File
@@ -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"
+7 -8
View File
@@ -38,7 +38,7 @@ pub struct DiscordAdapter {
/// SECURITY: Bot token is zeroized on drop to prevent memory disclosure.
token: Zeroizing<String>,
client: reqwest::Client,
allowed_guilds: Vec<u64>,
allowed_guilds: Vec<String>,
intents: u64,
shutdown_tx: Arc<watch::Sender<bool>>,
shutdown_rx: watch::Receiver<bool>,
@@ -51,7 +51,7 @@ pub struct DiscordAdapter {
}
impl DiscordAdapter {
pub fn new(token: String, allowed_guilds: Vec<u64>, intents: u64) -> Self {
pub fn new(token: String, allowed_guilds: Vec<String>, 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<RwLock<Option<String>>>,
allowed_guilds: &[u64],
allowed_guilds: &[String],
) -> Option<ChannelMessage> {
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);
}
+39 -21
View File
@@ -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<dyn openfang_runtime::llm_driver::LlmDriver>, 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),
));
}
}
+37 -3
View File
@@ -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]",
@@ -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();
}
}
@@ -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<Arc<dyn LlmDriver>>,
drivers: Vec<(Arc<dyn LlmDriver>, 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<Arc<dyn LlmDriver>>) -> 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<dyn LlmDriver>, String)>) -> Self {
Self { drivers }
}
}
@@ -30,12 +38,17 @@ impl LlmDriver for FallbackDriver {
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse, LlmError> {
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<CompletionResponse, LlmError> {
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"
);
@@ -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() {
@@ -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,
};
+3 -16
View File
@@ -647,8 +647,8 @@ fn builtin_aliases() -> HashMap<String, String> {
("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<ModelCatalogEntry> {
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(),
@@ -53,6 +53,8 @@ pub struct PromptContext {
pub heartbeat_md: Option<String>,
/// 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<String>,
}
/// 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());
+5 -1
View File
@@ -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<u64>,
/// Accepts strings for consistency with other channel configs.
pub allowed_guilds: Vec<String>,
/// Default agent name to route messages to.
pub default_agent: Option<String>,
/// Gateway intents bitmask (default: 33280 = GUILD_MESSAGES | MESSAGE_CONTENT).