@setoelkahfi / sigit / commits / 084eb04

Switch to local onde dependency and make tool execution async

paydii committed Apr 24, 2026 at 22:02 UTC 084eb04ebef3e88270cb47255a2fcdd05d14289a
5 files changed +59 -17
Cargo.lock
-1
index 8c9ad8c..e278d30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3799,7 +3799,6 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "onde" version = "0.1.8" -source = "git+https://github.com/ondeinference/onde?branch=development#f0bb0daad7fb07af951002f1bcae16fbd0bc876d" dependencies = [ "anyhow", "cc",
Cargo.toml
+2 -2
index 99e2b10..cc33162 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,8 @@ path = "src/main.rs" agent-client-protocol = { version = "0.10.4", features = ["unstable_session_fork"] } # Onde Inference engine (local LLM) -# For local development: onde = { path = "../onde" } -onde = { git = "https://github.com/ondeinference/onde", branch = "development" } +onde = { path = "../onde" } +# onde = { git = "https://github.com/ondeinference/onde", branch = "development" } # Async runtime async-trait = "0.1"
src/chat.rs
+20 -5
index 37108bb..b44b4d5 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -120,6 +120,8 @@ struct App { load_start: Instant, /// Display name of the model being loaded (shown in the spinner line). load_model_name: String, + /// Whether the currently loaded model supports tool calling. + tool_calling: bool, } const BANNER_ART: &str = "\ @@ -142,6 +144,11 @@ const THINKING_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", " impl App { fn new(load_model_name: String) -> Self { + let tool_calling = SIGIT_MODELS + .iter() + .find(|m| m.name == load_model_name) + .map(|m| m.tool_calling) + .unwrap_or(true); Self { messages: Vec::new(), input: String::new(), @@ -160,6 +167,7 @@ impl App { load_error: None, load_start: Instant::now(), load_model_name, + tool_calling, } } @@ -840,6 +848,7 @@ async fn exec_slash<B: ratatui::backend::Backend>( match engine.load_gguf_model(config, None, Some(sampling)).await { Ok(_) => { engine.clear_history().await; + app.tool_calling = model.tool_calling; app.messages.push(ChatMessage::system(format!( "✓ Switched to {}", model.name @@ -892,8 +901,13 @@ async fn run_inference_task( engine: Arc<ChatEngine>, text: String, tx: mpsc::Sender<InferenceUpdate>, + tools_enabled: bool, ) { - let onde_tools = build_onde_tools(); + let onde_tools = if tools_enabled { + build_onde_tools() + } else { + vec![] + }; let mut result = match engine.send_message_with_tools(&text, &onde_tools).await { Ok(r) => r, @@ -923,8 +937,8 @@ async fn run_inference_task( .send(InferenceUpdate::ToolUse(tc.function_name.clone())) .await; - // Execute the tool (synchronous / blocking-ok for file I/O). - let output = crate::tools::execute_tool(&tc.function_name, &tc.arguments); + // Execute the tool (async — read_website uses spawn_blocking internally). + let output = crate::tools::execute_tool(&tc.function_name, &tc.arguments).await; log::info!(" ← {} chars", output.len()); tool_results.push(ToolResult { @@ -985,7 +999,7 @@ pub async fn run_with<B: ratatui::backend::Backend>( engine: Arc<ChatEngine>, load_rx: std_mpsc::Receiver<Result<(), String>>, ) -> Result<()> { - let config = GgufModelConfig::platform_default(); + let config = GgufModelConfig::qwen3_4b(); let model_name = config.display_name.clone(); event_loop(terminal, engine, load_rx, model_name).await } @@ -1149,8 +1163,9 @@ async fn event_loop<B: ratatui::backend::Backend>( let engine_handle = Arc::clone(&engine); let user_text = text.clone(); + let tools_enabled = app.tool_calling; tokio::spawn(async move { - run_inference_task(engine_handle, user_text, tx).await; + run_inference_task(engine_handle, user_text, tx, tools_enabled).await; }); } }
src/main.rs
+24 -4
index 381b551..f1fbff5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -139,7 +139,16 @@ protocol-facing output and tool arguments. Create directories when needed. Run \ builds, tests, and git commands after making changes. Ground your answers in \ the actual code or fetched page content, not in guesses. +CRITICAL — you CAN access websites. You are NOT a typical LLM without internet \ +access. You have a read_website tool that fetches any http or https URL and \ +returns the page text. When the user gives you a URL or asks you to read, \ +summarize, or inspect a web page, you MUST call the read_website tool with that \ +URL. Never say \"I cannot access websites\" or \"I cannot browse the internet\". \ +You can. Use the tool. + Tool-use heuristics: +- when the user provides a URL or asks about a web page, ALWAYS call \ + read_website — never refuse or claim you lack internet access - prefer absolute paths over relative paths when you mention, return, or pass \ file and directory paths - if a path does not exist yet, create the directory before creating files in it @@ -338,8 +347,8 @@ impl Agent for SiGitAgent { tc.arguments.chars().take(120).collect::<String>() ); - // Execute the tool. - let output = tools::execute_tool(&tc.function_name, &tc.arguments); + // Execute the tool (async — read_website uses spawn_blocking internally). + let output = tools::execute_tool(&tc.function_name, &tc.arguments).await; log::info!(" ← {} chars", output.len()); @@ -374,6 +383,12 @@ impl Agent for SiGitAgent { if self.notification_tx.send(notification).await.is_err() { log::warn!("notification channel closed"); } + } else if result.tool_calls.is_empty() { + log::warn!( + "prompt({}) — model returned empty reply after {} tool round(s)", + session_id, + round + ); } log::info!("prompt({}) complete — {} tool round(s)", session_id, round); @@ -471,7 +486,11 @@ fn init_logging(is_tty: bool) { #[cfg(unix)] async fn run_interactive(tty: std::fs::File, mut cleanup_tty: std::fs::File) -> anyhow::Result<()> { let engine = Arc::new(ChatEngine::new()); - let config = GgufModelConfig::platform_default(); + let config = GgufModelConfig::qwen3_4b(); + let sampling = SamplingConfig { + max_tokens: Some(4096), + ..SamplingConfig::default() + }; // std::sync::mpsc — the loader runs on a dedicated OS thread, completely // decoupled from the tokio runtime so it can't starve the TUI draw loop. @@ -481,7 +500,8 @@ async fn run_interactive(tty: std::fs::File, mut cleanup_tty: std::fs::File) -> let system_prompt = SYSTEM_PROMPT.to_string(); std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().expect("failed to create loader runtime"); - let result = rt.block_on(loader_engine.load_gguf_model(config, Some(system_prompt), None)); + let result = + rt.block_on(loader_engine.load_gguf_model(config, Some(system_prompt), Some(sampling))); let _ = load_tx.send(result.map(|_| ()).map_err(|e| e.to_string())); });
src/tools.rs
+13 -5
index 7dc122e..116479a 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -266,12 +266,20 @@ pub fn all_tools() -> Vec<AgentTool> { /// /// Returns the tool output as a human-readable string. Errors are returned as /// descriptive strings rather than panicking. -pub fn execute_tool(name: &str, arguments: &str) -> String { +pub async fn execute_tool(name: &str, arguments: &str) -> String { match name { "read_file" => exec_read_file(arguments), "list_directory" => exec_list_directory(arguments), "search_files" => exec_search_files(arguments), - "read_website" => exec_read_website(arguments), + "read_website" => { + // reqwest::blocking panics if called inside a tokio runtime + // ("Cannot start a runtime from within a runtime"), so we + // off-load it to the blocking thread pool. + let args = arguments.to_owned(); + tokio::task::spawn_blocking(move || exec_read_website(&args)) + .await + .unwrap_or_else(|err| format!("Error: read_website task failed: {err}")) + } "create_directory" => exec_create_directory(arguments), "create_file" => exec_create_file(arguments), "edit_file" => exec_edit_file(arguments), @@ -949,9 +957,9 @@ mod tests { use super::*; use std::fs; - #[test] - fn test_execute_unknown_tool() { - let result = execute_tool("nonexistent", "{}"); + #[tokio::test] + async fn test_execute_unknown_tool() { + let result = execute_tool("nonexistent", "{}").await; assert!(result.starts_with("Unknown tool:")); }