mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 06:32:17 +00:00
- #834: Remove 3 decommissioned Groq models (gemma2-9b-it, llama-3.2-1b/3b-preview) - #805: Ollama streaming parser now checks both reasoning_content and reasoning fields - #820: Browser Hand checks python3 before python, fix optional dep logic - #848: Hand continuous interval changed from 60s to 3600s to prevent credit waste - #826: Doctor command no longer reports all_ok when provider key is rejected - #836: WebSocket tool events now include tool call ID for concurrent call correlation All 825+ tests passing. Verified live with daemon.
This commit is contained in:
Generated
+14
-14
@@ -3812,7 +3812,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-api"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3852,7 +3852,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-channels"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"async-trait",
|
||||
@@ -3889,7 +3889,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-cli"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
@@ -3916,7 +3916,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-desktop"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"open",
|
||||
@@ -3942,7 +3942,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-extensions"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
@@ -3970,7 +3970,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-hands"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dashmap",
|
||||
@@ -3987,7 +3987,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-kernel"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4025,7 +4025,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-memory"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4044,7 +4044,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-migrate"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 6.0.0",
|
||||
@@ -4063,7 +4063,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-runtime"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4097,7 +4097,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-skills"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
@@ -4120,7 +4120,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-types"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4139,7 +4139,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-wire"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -8818,7 +8818,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.4.9"
|
||||
version = "0.5.1"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
|
||||
@@ -998,11 +998,12 @@ async fn handle_command(
|
||||
fn map_stream_event(event: &StreamEvent, verbose: VerboseLevel) -> Option<serde_json::Value> {
|
||||
match event {
|
||||
StreamEvent::TextDelta { .. } => None, // Handled by debounce buffer
|
||||
StreamEvent::ToolUseStart { name, .. } => Some(serde_json::json!({
|
||||
StreamEvent::ToolUseStart { id, name, .. } => Some(serde_json::json!({
|
||||
"type": "tool_start",
|
||||
"id": id,
|
||||
"tool": name,
|
||||
})),
|
||||
StreamEvent::ToolUseEnd { name, input, .. } if name == "canvas_present" => {
|
||||
StreamEvent::ToolUseEnd { id, name, input, .. } if name == "canvas_present" => {
|
||||
let html = input.get("html").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let title = input
|
||||
.get("title")
|
||||
@@ -1010,12 +1011,13 @@ fn map_stream_event(event: &StreamEvent, verbose: VerboseLevel) -> Option<serde_
|
||||
.unwrap_or("Canvas");
|
||||
Some(serde_json::json!({
|
||||
"type": "canvas",
|
||||
"id": id,
|
||||
"canvas_id": uuid::Uuid::new_v4().to_string(),
|
||||
"html": html,
|
||||
"title": title,
|
||||
}))
|
||||
}
|
||||
StreamEvent::ToolUseEnd { name, input, .. } => match verbose {
|
||||
StreamEvent::ToolUseEnd { id, name, input, .. } => match verbose {
|
||||
VerboseLevel::Off => None,
|
||||
VerboseLevel::On => {
|
||||
let input_preview: String = serde_json::to_string(input)
|
||||
@@ -1025,6 +1027,7 @@ fn map_stream_event(event: &StreamEvent, verbose: VerboseLevel) -> Option<serde_
|
||||
.collect();
|
||||
Some(serde_json::json!({
|
||||
"type": "tool_end",
|
||||
"id": id,
|
||||
"tool": name,
|
||||
"input": input_preview,
|
||||
}))
|
||||
@@ -1037,18 +1040,21 @@ fn map_stream_event(event: &StreamEvent, verbose: VerboseLevel) -> Option<serde_
|
||||
.collect();
|
||||
Some(serde_json::json!({
|
||||
"type": "tool_end",
|
||||
"id": id,
|
||||
"tool": name,
|
||||
"input": input_preview,
|
||||
}))
|
||||
}
|
||||
},
|
||||
StreamEvent::ToolExecutionResult {
|
||||
id,
|
||||
name,
|
||||
result_preview,
|
||||
is_error,
|
||||
} => match verbose {
|
||||
VerboseLevel::Off => Some(serde_json::json!({
|
||||
"type": "tool_result",
|
||||
"id": id,
|
||||
"tool": name,
|
||||
"is_error": is_error,
|
||||
})),
|
||||
@@ -1056,6 +1062,7 @@ fn map_stream_event(event: &StreamEvent, verbose: VerboseLevel) -> Option<serde_
|
||||
let truncated: String = result_preview.chars().take(200).collect();
|
||||
Some(serde_json::json!({
|
||||
"type": "tool_result",
|
||||
"id": id,
|
||||
"tool": name,
|
||||
"result": truncated,
|
||||
"is_error": is_error,
|
||||
@@ -1063,6 +1070,7 @@ fn map_stream_event(event: &StreamEvent, verbose: VerboseLevel) -> Option<serde_
|
||||
}
|
||||
VerboseLevel::Full => Some(serde_json::json!({
|
||||
"type": "tool_result",
|
||||
"id": id,
|
||||
"tool": name,
|
||||
"result": result_preview,
|
||||
"is_error": is_error,
|
||||
|
||||
@@ -2414,11 +2414,14 @@ decay_rate = 0.05
|
||||
if !json {
|
||||
ui::provider_status(name, env_var, true);
|
||||
}
|
||||
} else if !json {
|
||||
ui::check_warn(&format!("{name} ({env_var}) - key rejected (401/403)"));
|
||||
} else {
|
||||
if !json {
|
||||
ui::check_fail(&format!("{name} ({env_var}) - key rejected (401/403)"));
|
||||
}
|
||||
all_ok = false;
|
||||
}
|
||||
any_key_set = true;
|
||||
checks.push(serde_json::json!({"check": "provider", "name": name, "env_var": env_var, "status": if valid { "ok" } else { "warn" }, "live_test": !valid}));
|
||||
checks.push(serde_json::json!({"check": "provider", "name": name, "env_var": env_var, "status": if valid { "ok" } else { "fail" }, "live_test": !valid}));
|
||||
} else {
|
||||
if !json {
|
||||
ui::provider_status(name, env_var, false);
|
||||
@@ -2921,7 +2924,9 @@ decay_rate = 0.05
|
||||
println!();
|
||||
if all_ok {
|
||||
ui::success("All checks passed! OpenFang is ready.");
|
||||
ui::hint("Start the daemon: openfang start");
|
||||
if find_daemon().is_none() {
|
||||
ui::hint("Start the daemon: openfang start");
|
||||
}
|
||||
} else if repaired {
|
||||
ui::success("Repairs applied. Re-run `openfang doctor` to verify.");
|
||||
} else {
|
||||
|
||||
@@ -153,6 +153,7 @@ impl StandaloneChat {
|
||||
name,
|
||||
result_preview,
|
||||
is_error,
|
||||
..
|
||||
} => {
|
||||
self.chat.tool_result(&name, &result_preview, is_error);
|
||||
}
|
||||
|
||||
@@ -1183,6 +1183,7 @@ impl App {
|
||||
name,
|
||||
result_preview,
|
||||
is_error,
|
||||
..
|
||||
} => {
|
||||
self.chat.tool_result(&name, &result_preview, is_error);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ tools = [
|
||||
|
||||
[[requires]]
|
||||
key = "python3"
|
||||
label = "Python 3 must be installed"
|
||||
label = "Python 3 must be installed (python3 or python)"
|
||||
requirement_type = "binary"
|
||||
check_value = "python3"
|
||||
description = "Python 3 is required for installing and running the Playwright browser automation library. Python 3.8 or newer is recommended."
|
||||
description = "Python 3 is required for installing and running the Playwright browser automation library. Python 3.8 or newer is recommended. Either 'python3' or 'python' (pointing to Python 3) will be detected."
|
||||
|
||||
[requires.install]
|
||||
macos = "brew install python3"
|
||||
@@ -26,7 +26,6 @@ windows = "winget install Python.Python.3.12"
|
||||
linux_apt = "sudo apt install python3"
|
||||
linux_dnf = "sudo dnf install python3"
|
||||
linux_pacman = "sudo pacman -S python"
|
||||
pip = "python3 --version"
|
||||
manual_url = "https://www.python.org/downloads/"
|
||||
estimated_time = "1-3 min"
|
||||
|
||||
|
||||
@@ -379,7 +379,12 @@ impl HandRegistry {
|
||||
pub fn readiness(&self, hand_id: &str) -> Option<HandReadiness> {
|
||||
let reqs = self.check_requirements(hand_id).ok()?;
|
||||
|
||||
let requirements_met = reqs.iter().all(|(_, ok)| *ok);
|
||||
// Only non-optional requirements gate readiness.
|
||||
// Optional requirements (e.g. chromium for browser hand) are nice-to-have;
|
||||
// missing them results in "degraded" status but not "requirements not met".
|
||||
let requirements_met = reqs
|
||||
.iter()
|
||||
.all(|(req, ok)| *ok || req.optional);
|
||||
|
||||
// A hand is active if at least one instance is in Active status.
|
||||
let active = self
|
||||
@@ -424,10 +429,13 @@ impl Default for HandRegistry {
|
||||
fn check_requirement(req: &HandRequirement) -> bool {
|
||||
match req.requirement_type {
|
||||
RequirementType::Binary => {
|
||||
// Special handling for python3: must actually run the command and verify
|
||||
// the output contains "Python 3", because Windows ships a python3.exe
|
||||
// Store shim that exists on PATH but doesn't actually work.
|
||||
if req.check_value == "python3" {
|
||||
// Special handling for python3 / python: must actually run the command
|
||||
// and verify the output contains "Python 3", because:
|
||||
// - Windows ships a python3.exe Store shim that doesn't actually work
|
||||
// - Most modern Linux distros only ship "python3", not "python"
|
||||
// - Some Docker images only have "python" pointing to Python 3
|
||||
// Matches the detection logic in python_runtime.rs find_python_interpreter().
|
||||
if req.check_value == "python3" || req.check_value == "python" {
|
||||
return check_python3_available();
|
||||
}
|
||||
// Check if binary exists on PATH.
|
||||
@@ -838,17 +846,26 @@ mod tests {
|
||||
let reg = HandRegistry::new();
|
||||
reg.load_bundled();
|
||||
|
||||
// Browser hand requires python3 + chromium. Activate it — if either
|
||||
// requirement is unmet on this machine, it will show as degraded.
|
||||
// Browser hand requires python3 (non-optional) + chromium (optional).
|
||||
// requirements_met only reflects non-optional requirements.
|
||||
// degraded = active + any requirement (including optional) unsatisfied.
|
||||
let instance = reg.activate("browser", HashMap::new()).unwrap();
|
||||
let r = reg.readiness("browser").unwrap();
|
||||
assert!(r.active);
|
||||
|
||||
// If any requirement is not satisfied, degraded should be true
|
||||
if !r.requirements_met {
|
||||
assert!(r.degraded);
|
||||
// Check individual requirements
|
||||
let reqs = reg.check_requirements("browser").unwrap();
|
||||
let python_met = reqs.iter().any(|(req, ok)| req.key == "python3" && *ok);
|
||||
let chromium_met = reqs.iter().any(|(req, ok)| req.key == "chromium" && *ok);
|
||||
|
||||
// requirements_met only gates on non-optional (python3)
|
||||
assert_eq!(r.requirements_met, python_met);
|
||||
|
||||
// degraded = active + any requirement unsatisfied
|
||||
if python_met && chromium_met {
|
||||
assert!(!r.degraded); // all met, not degraded
|
||||
} else {
|
||||
assert!(!r.degraded);
|
||||
assert!(r.degraded); // something is missing, degraded
|
||||
}
|
||||
|
||||
reg.deactivate(instance.instance_id).unwrap();
|
||||
|
||||
@@ -3302,9 +3302,10 @@ impl OpenFangKernel {
|
||||
}),
|
||||
// Autonomous hands must run in Continuous mode so the background loop picks them up.
|
||||
// Reactive (default) only fires on incoming messages, so autonomous hands would be inert.
|
||||
// Default to 3600s (1 hour) to avoid wasting credits — see issue #848.
|
||||
schedule: if def.agent.max_iterations.is_some() {
|
||||
ScheduleMode::Continuous {
|
||||
check_interval_secs: 60,
|
||||
check_interval_secs: 3600,
|
||||
}
|
||||
} else {
|
||||
ScheduleMode::default()
|
||||
|
||||
@@ -1780,6 +1780,7 @@ pub async fn run_agent_loop_streaming(
|
||||
let preview: String = final_content.chars().take(300).collect();
|
||||
if stream_tx
|
||||
.send(StreamEvent::ToolExecutionResult {
|
||||
id: tool_call.id.clone(),
|
||||
name: tool_call.name.clone(),
|
||||
result_preview: preview,
|
||||
is_error: result.is_error,
|
||||
|
||||
@@ -1150,7 +1150,7 @@ impl LlmDriver for OpenAIDriver {
|
||||
}
|
||||
|
||||
// Reasoning/thinking content delta (DeepSeek-R1, Qwen3 via LM Studio/Ollama)
|
||||
if let Some(reasoning) = delta["reasoning_content"].as_str() {
|
||||
if let Some(reasoning) = delta["reasoning_content"].as_str().or_else(|| delta["reasoning"].as_str()) {
|
||||
if !reasoning.is_empty() {
|
||||
reasoning_content.push_str(reasoning);
|
||||
let _ = tx
|
||||
|
||||
@@ -135,6 +135,7 @@ pub enum StreamEvent {
|
||||
},
|
||||
/// Tool execution completed with result (emitted by agent loop, not LLM driver).
|
||||
ToolExecutionResult {
|
||||
id: String,
|
||||
name: String,
|
||||
result_preview: String,
|
||||
is_error: bool,
|
||||
|
||||
@@ -1548,34 +1548,6 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "llama-3.2-3b-preview".into(),
|
||||
display_name: "Llama 3.2 3B".into(),
|
||||
provider: "groq".into(),
|
||||
tier: ModelTier::Fast,
|
||||
context_window: 128_000,
|
||||
max_output_tokens: 8_192,
|
||||
input_cost_per_m: 0.06,
|
||||
output_cost_per_m: 0.06,
|
||||
supports_tools: true,
|
||||
supports_vision: false,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "llama-3.2-1b-preview".into(),
|
||||
display_name: "Llama 3.2 1B".into(),
|
||||
provider: "groq".into(),
|
||||
tier: ModelTier::Fast,
|
||||
context_window: 128_000,
|
||||
max_output_tokens: 8_192,
|
||||
input_cost_per_m: 0.04,
|
||||
output_cost_per_m: 0.04,
|
||||
supports_tools: true,
|
||||
supports_vision: false,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "mixtral-8x7b-32768".into(),
|
||||
display_name: "Mixtral 8x7B".into(),
|
||||
@@ -1590,20 +1562,6 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_streaming: true,
|
||||
aliases: vec!["mixtral".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gemma2-9b-it".into(),
|
||||
display_name: "Gemma 2 9B".into(),
|
||||
provider: "groq".into(),
|
||||
tier: ModelTier::Fast,
|
||||
context_window: 8_192,
|
||||
max_output_tokens: 4_096,
|
||||
input_cost_per_m: 0.02,
|
||||
output_cost_per_m: 0.02,
|
||||
supports_tools: false,
|
||||
supports_vision: false,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "qwen-qwq-32b".into(),
|
||||
display_name: "Qwen QWQ 32B".into(),
|
||||
@@ -3924,7 +3882,7 @@ mod tests {
|
||||
let anthropic = catalog.get_provider("anthropic").unwrap();
|
||||
assert_eq!(anthropic.model_count, 7);
|
||||
let groq = catalog.get_provider("groq").unwrap();
|
||||
assert_eq!(groq.model_count, 10);
|
||||
assert_eq!(groq.model_count, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user