@hej / 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
@@ -3799,7 +3799,6 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
3799 [[package]]
3800 name = "onde"
3801 version = "0.1.8"
3802 -source = "git+https://github.com/ondeinference/onde?branch=development#f0bb0daad7fb07af951002f1bcae16fbd0bc876d"
3802 dependencies = [
3803 "anyhow",
3804 "cc",
Cargo.toml
+2 -2
@@ -21,8 +21,8 @@ path = "src/main.rs"
21 agent-client-protocol = { version = "0.10.4", features = ["unstable_session_fork"] }
22
23 # Onde Inference engine (local LLM)
24 -# For local development: onde = { path = "../onde" }
25 -onde = { git = "https://github.com/ondeinference/onde", branch = "development" }
24 +onde = { path = "../onde" }
25 +# onde = { git = "https://github.com/ondeinference/onde", branch = "development" }
26
27 # Async runtime
28 async-trait = "0.1"
src/chat.rs
+20 -5
@@ -120,6 +120,8 @@ struct App {
120 load_start: Instant,
121 /// Display name of the model being loaded (shown in the spinner line).
122 load_model_name: String,
123 + /// Whether the currently loaded model supports tool calling.
124 + tool_calling: bool,
125 }
126
127 const BANNER_ART: &str = "\
@@ -142,6 +144,11 @@ const THINKING_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "
144
145 impl App {
146 fn new(load_model_name: String) -> Self {
147 + let tool_calling = SIGIT_MODELS
148 + .iter()
149 + .find(|m| m.name == load_model_name)
150 + .map(|m| m.tool_calling)
151 + .unwrap_or(true);
152 Self {
153 messages: Vec::new(),
154 input: String::new(),
@@ -160,6 +167,7 @@ impl App {
167 load_error: None,
168 load_start: Instant::now(),
169 load_model_name,
170 + tool_calling,
171 }
172 }
173
@@ -840,6 +848,7 @@ async fn exec_slash<B: ratatui::backend::Backend>(
848 match engine.load_gguf_model(config, None, Some(sampling)).await {
849 Ok(_) => {
850 engine.clear_history().await;
851 + app.tool_calling = model.tool_calling;
852 app.messages.push(ChatMessage::system(format!(
853 "✓ Switched to {}",
854 model.name
@@ -892,8 +901,13 @@ async fn run_inference_task(
901 engine: Arc<ChatEngine>,
902 text: String,
903 tx: mpsc::Sender<InferenceUpdate>,
904 + tools_enabled: bool,
905 ) {
896 - let onde_tools = build_onde_tools();
906 + let onde_tools = if tools_enabled {
907 + build_onde_tools()
908 + } else {
909 + vec![]
910 + };
911
912 let mut result = match engine.send_message_with_tools(&text, &onde_tools).await {
913 Ok(r) => r,
@@ -923,8 +937,8 @@ async fn run_inference_task(
937 .send(InferenceUpdate::ToolUse(tc.function_name.clone()))
938 .await;
939
926 - // Execute the tool (synchronous / blocking-ok for file I/O).
927 - let output = crate::tools::execute_tool(&tc.function_name, &tc.arguments);
940 + // Execute the tool (async — read_website uses spawn_blocking internally).
941 + let output = crate::tools::execute_tool(&tc.function_name, &tc.arguments).await;
942 log::info!(" ← {} chars", output.len());
943
944 tool_results.push(ToolResult {
@@ -985,7 +999,7 @@ pub async fn run_with<B: ratatui::backend::Backend>(
999 engine: Arc<ChatEngine>,
1000 load_rx: std_mpsc::Receiver<Result<(), String>>,
1001 ) -> Result<()> {
988 - let config = GgufModelConfig::platform_default();
1002 + let config = GgufModelConfig::qwen3_4b();
1003 let model_name = config.display_name.clone();
1004 event_loop(terminal, engine, load_rx, model_name).await
1005 }
@@ -1149,8 +1163,9 @@ async fn event_loop<B: ratatui::backend::Backend>(
1163
1164 let engine_handle = Arc::clone(&engine);
1165 let user_text = text.clone();
1166 + let tools_enabled = app.tool_calling;
1167 tokio::spawn(async move {
1153 - run_inference_task(engine_handle, user_text, tx).await;
1168 + run_inference_task(engine_handle, user_text, tx, tools_enabled).await;
1169 });
1170 }
1171 }
src/main.rs
+24 -4
@@ -139,7 +139,16 @@ protocol-facing output and tool arguments. Create directories when needed. Run \
139 builds, tests, and git commands after making changes. Ground your answers in \
140 the actual code or fetched page content, not in guesses.
141
142 +CRITICAL — you CAN access websites. You are NOT a typical LLM without internet \
143 +access. You have a read_website tool that fetches any http or https URL and \
144 +returns the page text. When the user gives you a URL or asks you to read, \
145 +summarize, or inspect a web page, you MUST call the read_website tool with that \
146 +URL. Never say \"I cannot access websites\" or \"I cannot browse the internet\". \
147 +You can. Use the tool.
148 +
149 Tool-use heuristics:
150 +- when the user provides a URL or asks about a web page, ALWAYS call \
151 + read_website — never refuse or claim you lack internet access
152 - prefer absolute paths over relative paths when you mention, return, or pass \
153 file and directory paths
154 - if a path does not exist yet, create the directory before creating files in it
@@ -338,8 +347,8 @@ impl Agent for SiGitAgent {
347 tc.arguments.chars().take(120).collect::<String>()
348 );
349
341 - // Execute the tool.
342 - let output = tools::execute_tool(&tc.function_name, &tc.arguments);
350 + // Execute the tool (async — read_website uses spawn_blocking internally).
351 + let output = tools::execute_tool(&tc.function_name, &tc.arguments).await;
352
353 log::info!(" ← {} chars", output.len());
354
@@ -374,6 +383,12 @@ impl Agent for SiGitAgent {
383 if self.notification_tx.send(notification).await.is_err() {
384 log::warn!("notification channel closed");
385 }
386 + } else if result.tool_calls.is_empty() {
387 + log::warn!(
388 + "prompt({}) — model returned empty reply after {} tool round(s)",
389 + session_id,
390 + round
391 + );
392 }
393
394 log::info!("prompt({}) complete — {} tool round(s)", session_id, round);
@@ -471,7 +486,11 @@ fn init_logging(is_tty: bool) {
486 #[cfg(unix)]
487 async fn run_interactive(tty: std::fs::File, mut cleanup_tty: std::fs::File) -> anyhow::Result<()> {
488 let engine = Arc::new(ChatEngine::new());
474 - let config = GgufModelConfig::platform_default();
489 + let config = GgufModelConfig::qwen3_4b();
490 + let sampling = SamplingConfig {
491 + max_tokens: Some(4096),
492 + ..SamplingConfig::default()
493 + };
494
495 // std::sync::mpsc — the loader runs on a dedicated OS thread, completely
496 // 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) ->
500 let system_prompt = SYSTEM_PROMPT.to_string();
501 std::thread::spawn(move || {
502 let rt = tokio::runtime::Runtime::new().expect("failed to create loader runtime");
484 - let result = rt.block_on(loader_engine.load_gguf_model(config, Some(system_prompt), None));
503 + let result =
504 + rt.block_on(loader_engine.load_gguf_model(config, Some(system_prompt), Some(sampling)));
505 let _ = load_tx.send(result.map(|_| ()).map_err(|e| e.to_string()));
506 });
507
src/tools.rs
+13 -5
@@ -266,12 +266,20 @@ pub fn all_tools() -> Vec<AgentTool> {
266 ///
267 /// Returns the tool output as a human-readable string. Errors are returned as
268 /// descriptive strings rather than panicking.
269 -pub fn execute_tool(name: &str, arguments: &str) -> String {
269 +pub async fn execute_tool(name: &str, arguments: &str) -> String {
270 match name {
271 "read_file" => exec_read_file(arguments),
272 "list_directory" => exec_list_directory(arguments),
273 "search_files" => exec_search_files(arguments),
274 - "read_website" => exec_read_website(arguments),
274 + "read_website" => {
275 + // reqwest::blocking panics if called inside a tokio runtime
276 + // ("Cannot start a runtime from within a runtime"), so we
277 + // off-load it to the blocking thread pool.
278 + let args = arguments.to_owned();
279 + tokio::task::spawn_blocking(move || exec_read_website(&args))
280 + .await
281 + .unwrap_or_else(|err| format!("Error: read_website task failed: {err}"))
282 + }
283 "create_directory" => exec_create_directory(arguments),
284 "create_file" => exec_create_file(arguments),
285 "edit_file" => exec_edit_file(arguments),
@@ -949,9 +957,9 @@ mod tests {
957 use super::*;
958 use std::fs;
959
952 - #[test]
953 - fn test_execute_unknown_tool() {
954 - let result = execute_tool("nonexistent", "{}");
960 + #[tokio::test]
961 + async fn test_execute_unknown_tool() {
962 + let result = execute_tool("nonexistent", "{}").await;
963 assert!(result.starts_with("Unknown tool:"));
964 }
965