From 2d6ec2fb71a4ad162e505d7c58676519b4f6da03 Mon Sep 17 00:00:00 2001 From: chumyin Date: Mon, 16 Feb 2026 19:33:04 +0800 Subject: [PATCH] fix(rebase): resolve PR #266 conflicts against latest main --- src/agent/loop_.rs | 1 + src/channels/mod.rs | 37 +++++++-------- src/channels/telegram.rs | 5 +- src/daemon/mod.rs | 3 +- src/gateway/mod.rs | 3 ++ src/tools/git_operations.rs | 95 +++++++++++++++++++++++++++---------- src/tools/mod.rs | 49 +++++++++++++++++-- 7 files changed, 142 insertions(+), 51 deletions(-) diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index 45b37d2..4698032 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -113,6 +113,7 @@ async fn auto_compact_history( let summary_raw = provider .chat_with_system(Some(summarizer_system), &summarizer_user, model, 0.2) .await + .map(|resp| resp.text_or_empty().to_string()) .unwrap_or_else(|_| { // Fallback to deterministic local truncation when summarization fails. truncate_with_ellipsis(&transcript, COMPACTION_MAX_SUMMARY_CHARS) diff --git a/src/channels/mod.rs b/src/channels/mod.rs index f0399da..aa1fc6b 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -721,6 +721,7 @@ pub async fn start_channels(config: Config) -> Result<()> { composio_key, &config.browser, &config.http_request, + &config.workspace_dir, &config.agents, config.api_key.as_deref(), )); @@ -951,7 +952,7 @@ mod tests { use super::*; use crate::memory::{Memory, MemoryCategory, SqliteMemory}; use crate::observability::NoopObserver; - use crate::providers::{ChatMessage, Provider}; + use crate::providers::{ChatMessage, ChatResponse, Provider, ToolCall}; use crate::tools::{Tool, ToolResult}; use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -1018,27 +1019,23 @@ mod tests { message: &str, _model: &str, _temperature: f64, - ) -> anyhow::Result { + ) -> anyhow::Result { tokio::time::sleep(self.delay).await; - Ok(format!("echo: {message}")) + Ok(ChatResponse::with_text(format!("echo: {message}"))) } } struct ToolCallingProvider; - fn tool_call_payload() -> String { - serde_json::json!({ - "content": "", - "tool_calls": [{ - "id": "call_1", - "type": "function", - "function": { - "name": "mock_price", - "arguments": "{\"symbol\":\"BTC\"}" - } - }] - }) - .to_string() + fn tool_call_payload() -> ChatResponse { + ChatResponse { + text: Some(String::new()), + tool_calls: vec![ToolCall { + id: "call_1".into(), + name: "mock_price".into(), + arguments: r#"{"symbol":"BTC"}"#.into(), + }], + } } #[async_trait::async_trait] @@ -1049,7 +1046,7 @@ mod tests { _message: &str, _model: &str, _temperature: f64, - ) -> anyhow::Result { + ) -> anyhow::Result { Ok(tool_call_payload()) } @@ -1058,12 +1055,14 @@ mod tests { messages: &[ChatMessage], _model: &str, _temperature: f64, - ) -> anyhow::Result { + ) -> anyhow::Result { let has_tool_results = messages .iter() .any(|msg| msg.role == "user" && msg.content.contains("[Tool results]")); if has_tool_results { - Ok("BTC is currently around $65,000 based on latest tool output.".to_string()) + Ok(ChatResponse::with_text( + "BTC is currently around $65,000 based on latest tool output.", + )) } else { Ok(tool_call_payload()) } diff --git a/src/channels/telegram.rs b/src/channels/telegram.rs index 40193fe..5b1435c 100644 --- a/src/channels/telegram.rs +++ b/src/channels/telegram.rs @@ -32,7 +32,10 @@ fn split_message_for_telegram(message: &str) -> Vec { pos + 1 } else { // Try space as fallback - search_area.rfind(' ').unwrap_or(TELEGRAM_MAX_MESSAGE_LENGTH) + 1 + search_area + .rfind(' ') + .unwrap_or(TELEGRAM_MAX_MESSAGE_LENGTH) + + 1 } } else if let Some(pos) = search_area.rfind(' ') { pos + 1 diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index af3b861..f1bc4a1 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -193,7 +193,8 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> { for task in tasks { let prompt = format!("[Heartbeat Task] {task}"); let temp = config.default_temperature; - if let Err(e) = crate::agent::run(config.clone(), Some(prompt), None, None, temp).await + if let Err(e) = + crate::agent::run(config.clone(), Some(prompt), None, None, temp, false).await { crate::health::mark_component_error("heartbeat", e.to_string()); tracing::warn!("Heartbeat task failed: {e}"); diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index acf62a4..8eaa57c 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -70,6 +70,7 @@ async fn gateway_agent_reply(state: &AppState, message: &str) -> Result &mut history, state.tools_registry.as_ref(), state.observer.as_ref(), + "gateway", &state.model, state.temperature, ) @@ -262,6 +263,8 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> { Arc::clone(&mem), composio_key, &config.browser, + &config.http_request, + &config.workspace_dir, &config.agents, config.api_key.as_deref(), )); diff --git a/src/tools/git_operations.rs b/src/tools/git_operations.rs index 774115b..bf4e62c 100644 --- a/src/tools/git_operations.rs +++ b/src/tools/git_operations.rs @@ -14,7 +14,10 @@ pub struct GitOperationsTool { impl GitOperationsTool { pub fn new(security: Arc, workspace_dir: std::path::PathBuf) -> Self { - Self { security, workspace_dir } + Self { + security, + workspace_dir, + } } /// Sanitize git arguments to prevent injection attacks @@ -48,7 +51,10 @@ impl GitOperationsTool { /// Check if an operation is read-only fn is_read_only(&self, operation: &str) -> bool { - matches!(operation, "status" | "diff" | "log" | "show" | "branch" | "rev-parse") + matches!( + operation, + "status" | "diff" | "log" | "show" | "branch" | "rev-parse" + ) } async fn run_git_command(&self, args: &[&str]) -> anyhow::Result { @@ -67,7 +73,9 @@ impl GitOperationsTool { } async fn git_status(&self, _args: serde_json::Value) -> anyhow::Result { - let output = self.run_git_command(&["status", "--porcelain=2", "--branch"]).await?; + let output = self + .run_git_command(&["status", "--porcelain=2", "--branch"]) + .await?; // Parse git status output into structured format let mut result = serde_json::Map::new(); @@ -105,7 +113,10 @@ impl GitOperationsTool { result.insert("staged".to_string(), json!(staged)); result.insert("unstaged".to_string(), json!(unstaged)); result.insert("untracked".to_string(), json!(untracked)); - result.insert("clean".to_string(), json!(staged.is_empty() && unstaged.is_empty() && untracked.is_empty())); + result.insert( + "clean".to_string(), + json!(staged.is_empty() && unstaged.is_empty() && untracked.is_empty()), + ); Ok(ToolResult { success: true, @@ -116,7 +127,10 @@ impl GitOperationsTool { async fn git_diff(&self, args: serde_json::Value) -> anyhow::Result { let files = args.get("files").and_then(|v| v.as_str()).unwrap_or("."); - let cached = args.get("cached").and_then(|v| v.as_bool()).unwrap_or(false); + let cached = args + .get("cached") + .and_then(|v| v.as_bool()) + .unwrap_or(false); let mut git_args = vec!["diff", "--unified=3"]; if cached { @@ -191,12 +205,14 @@ impl GitOperationsTool { let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize; let limit_str = limit.to_string(); - let output = self.run_git_command(&[ - "log", - &format!("-{limit_str}"), - "--pretty=format:%H|%an|%ae|%ad|%s", - "--date=iso", - ]).await?; + let output = self + .run_git_command(&[ + "log", + &format!("-{limit_str}"), + "--pretty=format:%H|%an|%ae|%ad|%s", + "--date=iso", + ]) + .await?; let mut commits = Vec::new(); @@ -215,13 +231,16 @@ impl GitOperationsTool { Ok(ToolResult { success: true, - output: serde_json::to_string_pretty(&json!({ "commits": commits })).unwrap_or_default(), + output: serde_json::to_string_pretty(&json!({ "commits": commits })) + .unwrap_or_default(), error: None, }) } async fn git_branch(&self, _args: serde_json::Value) -> anyhow::Result { - let output = self.run_git_command(&["branch", "--format=%(refname:short)|%(HEAD)"]).await?; + let output = self + .run_git_command(&["branch", "--format=%(refname:short)|%(HEAD)"]) + .await?; let mut branches = Vec::new(); let mut current = String::new(); @@ -244,18 +263,21 @@ impl GitOperationsTool { output: serde_json::to_string_pretty(&json!({ "current": current, "branches": branches - })).unwrap_or_default(), + })) + .unwrap_or_default(), error: None, }) } async fn git_commit(&self, args: serde_json::Value) -> anyhow::Result { - let message = args.get("message") + let message = args + .get("message") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing 'message' parameter"))?; // Sanitize commit message - let sanitized = message.lines() + let sanitized = message + .lines() .map(|l| l.trim()) .filter(|l| !l.is_empty()) .collect::>() @@ -289,7 +311,8 @@ impl GitOperationsTool { } async fn git_add(&self, args: serde_json::Value) -> anyhow::Result { - let paths = args.get("paths") + let paths = args + .get("paths") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing 'paths' parameter"))?; @@ -310,7 +333,8 @@ impl GitOperationsTool { } async fn git_checkout(&self, args: serde_json::Value) -> anyhow::Result { - let branch = args.get("branch") + let branch = args + .get("branch") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing 'branch' parameter"))?; @@ -345,15 +369,22 @@ impl GitOperationsTool { } async fn git_stash(&self, args: serde_json::Value) -> anyhow::Result { - let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("push"); + let action = args + .get("action") + .and_then(|v| v.as_str()) + .unwrap_or("push"); let output = match action { - "push" | "save" => self.run_git_command(&["stash", "push", "-m", "auto-stash"]).await, + "push" | "save" => { + self.run_git_command(&["stash", "push", "-m", "auto-stash"]) + .await + } "pop" => self.run_git_command(&["stash", "pop"]).await, "list" => self.run_git_command(&["stash", "list"]).await, "drop" => { let index = args.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as i32; - self.run_git_command(&["stash", "drop", &format!("stash@{{{index}}}")]).await + self.run_git_command(&["stash", "drop", &format!("stash@{{{index}}}")]) + .await } _ => anyhow::bail!("Unknown stash action: {action}. Use: push, pop, list, drop"), }; @@ -470,7 +501,9 @@ impl Tool for GitOperationsTool { return Ok(ToolResult { success: false, output: String::new(), - error: Some("Action blocked: git write operations require higher autonomy level".into()), + error: Some( + "Action blocked: git write operations require higher autonomy level".into(), + ), }); } @@ -606,7 +639,11 @@ mod tests { .unwrap(); assert!(!result.success); // can_act() returns false for ReadOnly, so we get the "higher autonomy level" message - assert!(result.error.as_deref().unwrap_or("").contains("higher autonomy")); + assert!(result + .error + .as_deref() + .unwrap_or("") + .contains("higher autonomy")); } #[tokio::test] @@ -632,7 +669,11 @@ mod tests { let result = tool.execute(json!({})).await.unwrap(); assert!(!result.success); - assert!(result.error.as_deref().unwrap_or("").contains("Missing 'operation'")); + assert!(result + .error + .as_deref() + .unwrap_or("") + .contains("Missing 'operation'")); } #[tokio::test] @@ -649,6 +690,10 @@ mod tests { let result = tool.execute(json!({"operation": "push"})).await.unwrap(); assert!(!result.success); - assert!(result.error.as_deref().unwrap_or("").contains("Unknown operation")); + assert!(result + .error + .as_deref() + .unwrap_or("") + .contains("Unknown operation")); } } diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 95660b3..22e8d1a 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -101,7 +101,10 @@ pub fn all_tools_with_runtime( Box::new(MemoryStoreTool::new(memory.clone())), Box::new(MemoryRecallTool::new(memory.clone())), Box::new(MemoryForgetTool::new(memory)), - Box::new(GitOperationsTool::new(security.clone(), workspace_dir.to_path_buf())), + Box::new(GitOperationsTool::new( + security.clone(), + workspace_dir.to_path_buf(), + )), ]; if browser_config.enabled { @@ -184,7 +187,16 @@ mod tests { }; let http = crate::config::HttpRequestConfig::default(); - let tools = all_tools(&security, mem, None, &browser, &http, tmp.path(), &HashMap::new(), None); + let tools = all_tools( + &security, + mem, + None, + &browser, + &http, + tmp.path(), + &HashMap::new(), + None, + ); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(!names.contains(&"browser_open")); } @@ -208,7 +220,16 @@ mod tests { }; let http = crate::config::HttpRequestConfig::default(); - let tools = all_tools(&security, mem, None, &browser, &http, tmp.path(), &HashMap::new(), None); + let tools = all_tools( + &security, + mem, + None, + &browser, + &http, + tmp.path(), + &HashMap::new(), + None, + ); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(names.contains(&"browser_open")); } @@ -334,7 +355,16 @@ mod tests { }, ); - let tools = all_tools(&security, mem, None, &browser, &http, tmp.path(), &agents, Some("sk-test")); + let tools = all_tools( + &security, + mem, + None, + &browser, + &http, + tmp.path(), + &agents, + Some("sk-test"), + ); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(names.contains(&"delegate")); } @@ -353,7 +383,16 @@ mod tests { let browser = BrowserConfig::default(); let http = crate::config::HttpRequestConfig::default(); - let tools = all_tools(&security, mem, None, &browser, &http, tmp.path(), &HashMap::new(), None); + let tools = all_tools( + &security, + mem, + None, + &browser, + &http, + tmp.path(), + &HashMap::new(), + None, + ); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(!names.contains(&"delegate")); }