diff --git a/Cargo.lock b/Cargo.lock index 566ef8c8..24b181cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/crates/openfang-api/src/ws.rs b/crates/openfang-api/src/ws.rs index a73f3a8b..4052fe16 100644 --- a/crates/openfang-api/src/ws.rs +++ b/crates/openfang-api/src/ws.rs @@ -998,11 +998,12 @@ async fn handle_command( fn map_stream_event(event: &StreamEvent, verbose: VerboseLevel) -> Option { 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 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 Option 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 Option Some(serde_json::json!({ "type": "tool_result", + "id": id, "tool": name, "result": result_preview, "is_error": is_error, diff --git a/crates/openfang-cli/src/main.rs b/crates/openfang-cli/src/main.rs index a09e012d..8219f913 100644 --- a/crates/openfang-cli/src/main.rs +++ b/crates/openfang-cli/src/main.rs @@ -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 { diff --git a/crates/openfang-cli/src/tui/chat_runner.rs b/crates/openfang-cli/src/tui/chat_runner.rs index 9ad032f1..f10a5942 100644 --- a/crates/openfang-cli/src/tui/chat_runner.rs +++ b/crates/openfang-cli/src/tui/chat_runner.rs @@ -153,6 +153,7 @@ impl StandaloneChat { name, result_preview, is_error, + .. } => { self.chat.tool_result(&name, &result_preview, is_error); } diff --git a/crates/openfang-cli/src/tui/mod.rs b/crates/openfang-cli/src/tui/mod.rs index dce70434..109c8506 100644 --- a/crates/openfang-cli/src/tui/mod.rs +++ b/crates/openfang-cli/src/tui/mod.rs @@ -1183,6 +1183,7 @@ impl App { name, result_preview, is_error, + .. } => { self.chat.tool_result(&name, &result_preview, is_error); } diff --git a/crates/openfang-hands/bundled/browser/HAND.toml b/crates/openfang-hands/bundled/browser/HAND.toml index 08ee0b6c..eb2ee452 100644 --- a/crates/openfang-hands/bundled/browser/HAND.toml +++ b/crates/openfang-hands/bundled/browser/HAND.toml @@ -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" diff --git a/crates/openfang-hands/src/registry.rs b/crates/openfang-hands/src/registry.rs index 54240f04..461470e1 100644 --- a/crates/openfang-hands/src/registry.rs +++ b/crates/openfang-hands/src/registry.rs @@ -379,7 +379,12 @@ impl HandRegistry { pub fn readiness(&self, hand_id: &str) -> Option { 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(); diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 3bb4a3fc..699028a8 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -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() diff --git a/crates/openfang-runtime/src/agent_loop.rs b/crates/openfang-runtime/src/agent_loop.rs index c377584f..f2cdc936 100644 --- a/crates/openfang-runtime/src/agent_loop.rs +++ b/crates/openfang-runtime/src/agent_loop.rs @@ -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, diff --git a/crates/openfang-runtime/src/drivers/openai.rs b/crates/openfang-runtime/src/drivers/openai.rs index 7d54c432..b6dd5d21 100644 --- a/crates/openfang-runtime/src/drivers/openai.rs +++ b/crates/openfang-runtime/src/drivers/openai.rs @@ -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 diff --git a/crates/openfang-runtime/src/llm_driver.rs b/crates/openfang-runtime/src/llm_driver.rs index 8fb59405..17af1b84 100644 --- a/crates/openfang-runtime/src/llm_driver.rs +++ b/crates/openfang-runtime/src/llm_driver.rs @@ -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, diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index 6bfa9914..0cbe4d1f 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -1548,34 +1548,6 @@ fn builtin_models() -> Vec { 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 { 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]