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