community fixes

This commit is contained in:
jaberjaber23
2026-03-14 22:49:43 +03:00
parent d55e1b8545
commit 52bacf0946
11 changed files with 267 additions and 19 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ members = [
]
[workspace.package]
version = "0.4.0"
version = "0.4.1"
edition = "2021"
license = "Apache-2.0 OR MIT"
repository = "https://github.com/RightNow-AI/openfang"
+7 -1
View File
@@ -7007,8 +7007,8 @@ pub async fn set_provider_key(
"\n[default_model]\nprovider = \"{}\"\nmodel = \"{}\"\napi_key_env = \"{}\"\n",
name, model_id, env_var
);
backup_config(&config_path);
if let Ok(existing) = std::fs::read_to_string(&config_path) {
// Remove existing [default_model] section if present, then append
let cleaned = remove_toml_section(&existing, "default_model");
let _ = std::fs::write(&config_path, format!("{}\n{}", cleaned.trim(), update_toml));
} else {
@@ -10839,6 +10839,12 @@ pub async fn auth_check(
}
/// Remove a `[section]` and its contents from a TOML string.
#[allow(dead_code)]
fn backup_config(config_path: &std::path::Path) {
let backup = config_path.with_extension("toml.bak");
let _ = std::fs::copy(config_path, backup);
}
fn remove_toml_section(content: &str, section: &str) -> String {
let header = format!("[{}]", section);
let mut result = String::new();
+11 -1
View File
@@ -393,7 +393,7 @@ function agentsPage() {
},
// ── Multi-step wizard navigation ──
openSpawnWizard() {
async openSpawnWizard() {
this.showSpawnModal = true;
this.spawnStep = 1;
this.spawnMode = 'wizard';
@@ -401,8 +401,18 @@ function agentsPage() {
this.selectedPreset = '';
this.soulContent = '';
this.spawnForm.name = '';
this.spawnForm.provider = 'groq';
this.spawnForm.model = 'llama-3.3-70b-versatile';
this.spawnForm.systemPrompt = 'You are a helpful assistant.';
this.spawnForm.profile = 'full';
try {
var res = await fetch('/api/status');
if (res.ok) {
var status = await res.json();
if (status.default_provider) this.spawnForm.provider = status.default_provider;
if (status.default_model) this.spawnForm.model = status.default_model;
}
} catch(e) { /* keep hardcoded defaults */ }
},
nextStep() {
+72 -2
View File
@@ -780,7 +780,7 @@ async fn dispatch_message(
send_lifecycle_reaction(adapter, &message.sender, msg_id, AgentPhase::Error).await;
}
warn!("Agent error for {agent_id}: {e}");
let err_msg = format!("Agent error: {e}");
let err_msg = sanitize_agent_error(&e.to_string());
send_response(
adapter,
&message.sender,
@@ -803,6 +803,76 @@ async fn dispatch_message(
}
}
fn sanitize_agent_error(raw: &str) -> String {
let lower = raw.to_lowercase();
if lower.contains("rate limit")
|| lower.contains("rate_limit")
|| lower.contains("429")
|| lower.contains("too many requests")
|| lower.contains("resource_exhausted")
{
return "Rate limit reached, please try again later.".to_string();
}
if lower.contains("authentication")
|| lower.contains("unauthorized")
|| lower.contains("invalid api key")
|| lower.contains("invalid x-goog-api-key")
|| lower.contains("incorrect api key")
|| lower.contains("permission denied")
|| lower.contains("billing")
|| lower.contains("quota exceeded")
{
return "Service temporarily unavailable.".to_string();
}
if lower.contains("context length")
|| lower.contains("token limit")
|| lower.contains("too many tokens")
|| lower.contains("maximum context")
|| lower.contains("max_tokens")
|| lower.contains("context window")
{
return "Message too long, try a shorter request.".to_string();
}
if lower.contains("overloaded")
|| lower.contains("503")
|| lower.contains("502")
|| lower.contains("server error")
|| lower.contains("internal error")
{
return "The AI service is temporarily overloaded, please try again shortly.".to_string();
}
if lower.contains("timeout") || lower.contains("timed out") || lower.contains("deadline") {
return "Request timed out, please try again.".to_string();
}
if lower.contains("model not found") || lower.contains("model_not_found") {
return "The requested model is currently unavailable.".to_string();
}
let cleaned = raw
.strip_prefix("LLM driver error: ")
.or_else(|| raw.strip_prefix("Agent error: "))
.unwrap_or(raw);
if let Some(first_sentence_end) = cleaned.find(". ") {
let first = &cleaned[..=first_sentence_end];
if first.len() < cleaned.len() / 2 {
return format!("Agent error: {first}");
}
}
if cleaned.contains('{') || cleaned.len() > 200 {
return "Something went wrong processing your request. Please try again.".to_string();
}
format!("Agent error: {cleaned}")
}
/// Detect image format from the first few magic bytes.
///
/// Returns `Some("image/...")` for JPEG, PNG, GIF, and WebP.
@@ -1012,7 +1082,7 @@ async fn dispatch_with_blocks(
send_lifecycle_reaction(adapter, &message.sender, msg_id, AgentPhase::Error).await;
}
warn!("Agent error for {agent_id}: {e}");
let err_msg = format!("Agent error: {e}");
let err_msg = sanitize_agent_error(&e.to_string());
send_response(
adapter,
&message.sender,
+31 -2
View File
@@ -794,11 +794,36 @@ enum SystemCommands {
},
}
fn config_log_level() -> String {
let config_path = if let Ok(home) = std::env::var("OPENFANG_HOME") {
std::path::PathBuf::from(home).join("config.toml")
} else {
dirs::home_dir()
.unwrap_or_else(std::env::temp_dir)
.join(".openfang")
.join("config.toml")
};
if let Ok(content) = std::fs::read_to_string(config_path) {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("log_level") {
if let Some(val) = trimmed.split('=').nth(1) {
let level = val.trim().trim_matches('"').trim_matches('\'');
if !level.is_empty() {
return level.to_string();
}
}
}
}
}
"info".to_string()
}
fn init_tracing_stderr() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(config_log_level())),
)
.init();
}
@@ -824,7 +849,7 @@ fn init_tracing_file() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(config_log_level())),
)
.with_writer(std::sync::Mutex::new(file))
.with_ansi(false)
@@ -4691,6 +4716,8 @@ fn cmd_config_set(key: &str, value: &str) {
std::process::exit(1);
});
let _ = std::fs::copy(&config_path, config_path.with_extension("toml.bak"));
std::fs::write(&config_path, &serialized).unwrap_or_else(|e| {
ui::error(&format!("Failed to write config: {e}"));
std::process::exit(1);
@@ -4757,6 +4784,8 @@ fn cmd_config_unset(key: &str) {
std::process::exit(1);
});
let _ = std::fs::copy(&config_path, config_path.with_extension("toml.bak"));
std::fs::write(&config_path, &serialized).unwrap_or_else(|e| {
ui::error(&format!("Failed to write config: {e}"));
std::process::exit(1);
+2 -2
View File
@@ -1,6 +1,6 @@
//! Compile-time embedded Hand definitions.
use crate::{HandDefinition, HandError};
use crate::{parse_hand_toml, HandDefinition, HandError};
/// Returns all bundled hand definitions as (id, HAND.toml content, SKILL.md content).
pub fn bundled_hands() -> Vec<(&'static str, &'static str, &'static str)> {
@@ -55,7 +55,7 @@ pub fn parse_bundled(
skill_content: &str,
) -> Result<HandDefinition, HandError> {
let mut def: HandDefinition =
toml::from_str(toml_content).map_err(|e| HandError::TomlParse(e.to_string()))?;
parse_hand_toml(toml_content).map_err(|e| HandError::TomlParse(e.to_string()))?;
if !skill_content.is_empty() {
def.skill_content = Some(skill_content.to_string());
}
+60
View File
@@ -306,6 +306,20 @@ fn default_temperature() -> f32 {
0.7
}
#[derive(Deserialize)]
struct HandTomlWrapper {
hand: HandDefinition,
}
/// Parse HAND.toml content, supporting both flat format and `[hand]` table format.
pub fn parse_hand_toml(content: &str) -> Result<HandDefinition, toml::de::Error> {
if let Ok(def) = toml::from_str::<HandDefinition>(content) {
return Ok(def);
}
let wrapper: HandTomlWrapper = toml::from_str(content)?;
Ok(wrapper.hand)
}
/// Complete Hand definition — parsed from HAND.toml.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandDefinition {
@@ -798,4 +812,50 @@ metrics = []
assert!(install.macos.is_none());
assert!(install.windows.is_none());
}
#[test]
fn parse_hand_toml_flat_format() {
let toml_str = r#"
id = "test"
name = "Test Hand"
description = "A test hand"
category = "content"
tools = ["shell_exec"]
[agent]
name = "test-hand"
description = "Test agent"
system_prompt = "You are a test agent."
[dashboard]
metrics = []
"#;
let def = parse_hand_toml(toml_str).unwrap();
assert_eq!(def.id, "test");
assert_eq!(def.name, "Test Hand");
}
#[test]
fn parse_hand_toml_wrapped_format() {
let toml_str = r#"
[hand]
id = "test"
name = "Test Hand"
description = "A test hand"
category = "content"
tools = ["shell_exec"]
[hand.agent]
name = "test-hand"
description = "Test agent"
system_prompt = "You are a test agent."
[hand.dashboard]
metrics = []
"#;
let def = parse_hand_toml(toml_str).unwrap();
assert_eq!(def.id, "test");
assert_eq!(def.name, "Test Hand");
assert_eq!(def.agent.name, "test-hand");
}
}
+54 -2
View File
@@ -1602,7 +1602,7 @@ impl OpenFangKernel {
label: None,
});
// Check if auto-compaction is needed: message-count OR token-count trigger
// Check if auto-compaction is needed: message-count OR token-count OR quota-headroom trigger
let needs_compact = {
use openfang_runtime::compactor::{
estimate_token_count, needs_compaction as check_compact,
@@ -1624,7 +1624,23 @@ impl OpenFangKernel {
"Token-based compaction triggered (messages below threshold but tokens above)"
);
}
by_messages || by_tokens
let by_quota = if let Some(headroom) = self.scheduler.token_headroom(agent_id) {
let threshold = (headroom as f64 * 0.8) as u64;
if estimated as u64 > threshold && session.messages.len() > 4 {
info!(
agent_id = %agent_id,
estimated_tokens = estimated,
quota_headroom = headroom,
"Quota-headroom compaction triggered (session would consume >80% of remaining quota)"
);
true
} else {
false
}
} else {
false
};
by_messages || by_tokens || by_quota
};
let tools = self.available_tools(agent_id);
@@ -2094,6 +2110,42 @@ impl OpenFangKernel {
label: None,
});
// Pre-emptive compaction: compact before LLM call if session is large or quota headroom is low
{
use openfang_runtime::compactor::{
estimate_token_count, needs_compaction as check_compact,
needs_compaction_by_tokens, CompactionConfig,
};
let config = CompactionConfig::default();
let by_messages = check_compact(&session, &config);
let estimated = estimate_token_count(
&session.messages,
Some(&entry.manifest.model.system_prompt),
None,
);
let by_tokens = needs_compaction_by_tokens(estimated, &config);
let by_quota = if let Some(headroom) = self.scheduler.token_headroom(agent_id) {
let threshold = (headroom as f64 * 0.8) as u64;
estimated as u64 > threshold && session.messages.len() > 4
} else {
false
};
if by_messages || by_tokens || by_quota {
info!(agent_id = %agent_id, messages = session.messages.len(), estimated_tokens = estimated, "Pre-emptive compaction before LLM call");
match self.compact_agent_session(agent_id).await {
Ok(msg) => {
info!(agent_id = %agent_id, "{msg}");
if let Ok(Some(reloaded)) = self.memory.get_session(session.id) {
session = reloaded;
}
}
Err(e) => {
warn!(agent_id = %agent_id, "Pre-emptive compaction failed: {e}");
}
}
}
}
let messages_before = session.messages.len();
let tools = self.available_tools(agent_id);
+13
View File
@@ -130,6 +130,19 @@ impl AgentScheduler {
.get(&agent_id)
.map(|t| (t.total_tokens, t.tool_calls))
}
/// Returns remaining token headroom before quota is hit.
/// Returns `None` if no token quota is configured (unlimited).
pub fn token_headroom(&self, agent_id: AgentId) -> Option<u64> {
let quota = self.quotas.get(&agent_id)?;
if quota.max_llm_tokens_per_hour == 0 {
return None;
}
let mut tracker = self.usage.get_mut(&agent_id)?;
tracker.reset_if_expired();
let used = tracker.total_tokens;
Some(quota.max_llm_tokens_per_hour.saturating_sub(used))
}
}
impl Default for AgentScheduler {
+15 -7
View File
@@ -275,12 +275,18 @@ pub fn build_canonical_context_message(ctx: &PromptContext) -> Option<String> {
///
/// Also used by `agent_loop.rs` to append recalled memories after DB lookup.
pub fn build_memory_section(memories: &[(String, String)]) -> String {
let mut out = String::from(
"## Memory\n\
- When the user asks about something from a previous conversation, use memory_recall first.\n\
- Store important preferences, decisions, and context with memory_store for future use.",
);
if !memories.is_empty() {
let mut out = String::from("## Memory\n");
if memories.is_empty() {
out.push_str(
"- When the user asks about something from a previous conversation, use memory_recall first.\n\
- Store important preferences, decisions, and context with memory_store for future use.",
);
} else {
out.push_str(
"- Use the recalled memories below to inform your responses.\n\
- Only call memory_recall if you need information not already shown here.\n\
- Store important preferences, decisions, and context with memory_store for future use.",
);
out.push_str("\n\nRecalled memories:\n");
for (key, content) in memories.iter().take(5) {
let capped = cap_str(content, 500);
@@ -728,7 +734,7 @@ mod tests {
fn test_memory_section_empty() {
let section = build_memory_section(&[]);
assert!(section.contains("## Memory"));
assert!(section.contains("memory_recall"));
assert!(section.contains("use memory_recall first"));
assert!(!section.contains("Recalled memories"));
}
@@ -742,6 +748,8 @@ mod tests {
assert!(section.contains("Recalled memories"));
assert!(section.contains("[pref] User likes dark mode"));
assert!(section.contains("[ctx] Working on Rust project"));
assert!(section.contains("Use the recalled memories below"));
assert!(!section.contains("use memory_recall first"));
}
#[test]
+1 -1
View File
@@ -72,7 +72,7 @@ pub enum OpenFangError {
Serialization(String),
/// The agent loop exceeded the maximum iteration count.
#[error("Max iterations exceeded: {0}")]
#[error("Max iterations exceeded ({0}). Configure a higher limit in agent.toml under [autonomous] max_iterations")]
MaxIterationsExceeded(u32),
/// The kernel is shutting down.