fix(rebase): resolve PR #266 conflicts against latest main

This commit is contained in:
chumyin 2026-02-16 19:33:04 +08:00
parent 34306e32d8
commit 2d6ec2fb71
7 changed files with 142 additions and 51 deletions

View file

@ -113,6 +113,7 @@ async fn auto_compact_history(
let summary_raw = provider let summary_raw = provider
.chat_with_system(Some(summarizer_system), &summarizer_user, model, 0.2) .chat_with_system(Some(summarizer_system), &summarizer_user, model, 0.2)
.await .await
.map(|resp| resp.text_or_empty().to_string())
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
// Fallback to deterministic local truncation when summarization fails. // Fallback to deterministic local truncation when summarization fails.
truncate_with_ellipsis(&transcript, COMPACTION_MAX_SUMMARY_CHARS) truncate_with_ellipsis(&transcript, COMPACTION_MAX_SUMMARY_CHARS)

View file

@ -721,6 +721,7 @@ pub async fn start_channels(config: Config) -> Result<()> {
composio_key, composio_key,
&config.browser, &config.browser,
&config.http_request, &config.http_request,
&config.workspace_dir,
&config.agents, &config.agents,
config.api_key.as_deref(), config.api_key.as_deref(),
)); ));
@ -951,7 +952,7 @@ mod tests {
use super::*; use super::*;
use crate::memory::{Memory, MemoryCategory, SqliteMemory}; use crate::memory::{Memory, MemoryCategory, SqliteMemory};
use crate::observability::NoopObserver; use crate::observability::NoopObserver;
use crate::providers::{ChatMessage, Provider}; use crate::providers::{ChatMessage, ChatResponse, Provider, ToolCall};
use crate::tools::{Tool, ToolResult}; use crate::tools::{Tool, ToolResult};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
@ -1018,27 +1019,23 @@ mod tests {
message: &str, message: &str,
_model: &str, _model: &str,
_temperature: f64, _temperature: f64,
) -> anyhow::Result<String> { ) -> anyhow::Result<ChatResponse> {
tokio::time::sleep(self.delay).await; tokio::time::sleep(self.delay).await;
Ok(format!("echo: {message}")) Ok(ChatResponse::with_text(format!("echo: {message}")))
} }
} }
struct ToolCallingProvider; struct ToolCallingProvider;
fn tool_call_payload() -> String { fn tool_call_payload() -> ChatResponse {
serde_json::json!({ ChatResponse {
"content": "", text: Some(String::new()),
"tool_calls": [{ tool_calls: vec![ToolCall {
"id": "call_1", id: "call_1".into(),
"type": "function", name: "mock_price".into(),
"function": { arguments: r#"{"symbol":"BTC"}"#.into(),
"name": "mock_price", }],
"arguments": "{\"symbol\":\"BTC\"}" }
}
}]
})
.to_string()
} }
#[async_trait::async_trait] #[async_trait::async_trait]
@ -1049,7 +1046,7 @@ mod tests {
_message: &str, _message: &str,
_model: &str, _model: &str,
_temperature: f64, _temperature: f64,
) -> anyhow::Result<String> { ) -> anyhow::Result<ChatResponse> {
Ok(tool_call_payload()) Ok(tool_call_payload())
} }
@ -1058,12 +1055,14 @@ mod tests {
messages: &[ChatMessage], messages: &[ChatMessage],
_model: &str, _model: &str,
_temperature: f64, _temperature: f64,
) -> anyhow::Result<String> { ) -> anyhow::Result<ChatResponse> {
let has_tool_results = messages let has_tool_results = messages
.iter() .iter()
.any(|msg| msg.role == "user" && msg.content.contains("[Tool results]")); .any(|msg| msg.role == "user" && msg.content.contains("[Tool results]"));
if has_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 { } else {
Ok(tool_call_payload()) Ok(tool_call_payload())
} }

View file

@ -32,7 +32,10 @@ fn split_message_for_telegram(message: &str) -> Vec<String> {
pos + 1 pos + 1
} else { } else {
// Try space as fallback // 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(' ') { } else if let Some(pos) = search_area.rfind(' ') {
pos + 1 pos + 1

View file

@ -193,7 +193,8 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> {
for task in tasks { for task in tasks {
let prompt = format!("[Heartbeat Task] {task}"); let prompt = format!("[Heartbeat Task] {task}");
let temp = config.default_temperature; 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()); crate::health::mark_component_error("heartbeat", e.to_string());
tracing::warn!("Heartbeat task failed: {e}"); tracing::warn!("Heartbeat task failed: {e}");

View file

@ -70,6 +70,7 @@ async fn gateway_agent_reply(state: &AppState, message: &str) -> Result<String>
&mut history, &mut history,
state.tools_registry.as_ref(), state.tools_registry.as_ref(),
state.observer.as_ref(), state.observer.as_ref(),
"gateway",
&state.model, &state.model,
state.temperature, state.temperature,
) )
@ -262,6 +263,8 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> {
Arc::clone(&mem), Arc::clone(&mem),
composio_key, composio_key,
&config.browser, &config.browser,
&config.http_request,
&config.workspace_dir,
&config.agents, &config.agents,
config.api_key.as_deref(), config.api_key.as_deref(),
)); ));

View file

@ -14,7 +14,10 @@ pub struct GitOperationsTool {
impl GitOperationsTool { impl GitOperationsTool {
pub fn new(security: Arc<SecurityPolicy>, workspace_dir: std::path::PathBuf) -> Self { pub fn new(security: Arc<SecurityPolicy>, workspace_dir: std::path::PathBuf) -> Self {
Self { security, workspace_dir } Self {
security,
workspace_dir,
}
} }
/// Sanitize git arguments to prevent injection attacks /// Sanitize git arguments to prevent injection attacks
@ -48,7 +51,10 @@ impl GitOperationsTool {
/// Check if an operation is read-only /// Check if an operation is read-only
fn is_read_only(&self, operation: &str) -> bool { 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<String> { async fn run_git_command(&self, args: &[&str]) -> anyhow::Result<String> {
@ -67,7 +73,9 @@ impl GitOperationsTool {
} }
async fn git_status(&self, _args: serde_json::Value) -> anyhow::Result<ToolResult> { async fn git_status(&self, _args: serde_json::Value) -> anyhow::Result<ToolResult> {
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 // Parse git status output into structured format
let mut result = serde_json::Map::new(); let mut result = serde_json::Map::new();
@ -105,7 +113,10 @@ impl GitOperationsTool {
result.insert("staged".to_string(), json!(staged)); result.insert("staged".to_string(), json!(staged));
result.insert("unstaged".to_string(), json!(unstaged)); result.insert("unstaged".to_string(), json!(unstaged));
result.insert("untracked".to_string(), json!(untracked)); 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 { Ok(ToolResult {
success: true, success: true,
@ -116,7 +127,10 @@ impl GitOperationsTool {
async fn git_diff(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> { async fn git_diff(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let files = args.get("files").and_then(|v| v.as_str()).unwrap_or("."); 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"]; let mut git_args = vec!["diff", "--unified=3"];
if cached { 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 = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
let limit_str = limit.to_string(); let limit_str = limit.to_string();
let output = self.run_git_command(&[ let output = self
"log", .run_git_command(&[
&format!("-{limit_str}"), "log",
"--pretty=format:%H|%an|%ae|%ad|%s", &format!("-{limit_str}"),
"--date=iso", "--pretty=format:%H|%an|%ae|%ad|%s",
]).await?; "--date=iso",
])
.await?;
let mut commits = Vec::new(); let mut commits = Vec::new();
@ -215,13 +231,16 @@ impl GitOperationsTool {
Ok(ToolResult { Ok(ToolResult {
success: true, 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, error: None,
}) })
} }
async fn git_branch(&self, _args: serde_json::Value) -> anyhow::Result<ToolResult> { async fn git_branch(&self, _args: serde_json::Value) -> anyhow::Result<ToolResult> {
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 branches = Vec::new();
let mut current = String::new(); let mut current = String::new();
@ -244,18 +263,21 @@ impl GitOperationsTool {
output: serde_json::to_string_pretty(&json!({ output: serde_json::to_string_pretty(&json!({
"current": current, "current": current,
"branches": branches "branches": branches
})).unwrap_or_default(), }))
.unwrap_or_default(),
error: None, error: None,
}) })
} }
async fn git_commit(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> { async fn git_commit(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let message = args.get("message") let message = args
.get("message")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'message' parameter"))?; .ok_or_else(|| anyhow::anyhow!("Missing 'message' parameter"))?;
// Sanitize commit message // Sanitize commit message
let sanitized = message.lines() let sanitized = message
.lines()
.map(|l| l.trim()) .map(|l| l.trim())
.filter(|l| !l.is_empty()) .filter(|l| !l.is_empty())
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -289,7 +311,8 @@ impl GitOperationsTool {
} }
async fn git_add(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> { async fn git_add(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let paths = args.get("paths") let paths = args
.get("paths")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'paths' parameter"))?; .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<ToolResult> { async fn git_checkout(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let branch = args.get("branch") let branch = args
.get("branch")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'branch' parameter"))?; .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<ToolResult> { async fn git_stash(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
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 { 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, "pop" => self.run_git_command(&["stash", "pop"]).await,
"list" => self.run_git_command(&["stash", "list"]).await, "list" => self.run_git_command(&["stash", "list"]).await,
"drop" => { "drop" => {
let index = args.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as i32; 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"), _ => anyhow::bail!("Unknown stash action: {action}. Use: push, pop, list, drop"),
}; };
@ -470,7 +501,9 @@ impl Tool for GitOperationsTool {
return Ok(ToolResult { return Ok(ToolResult {
success: false, success: false,
output: String::new(), 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(); .unwrap();
assert!(!result.success); assert!(!result.success);
// can_act() returns false for ReadOnly, so we get the "higher autonomy level" message // 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] #[tokio::test]
@ -632,7 +669,11 @@ mod tests {
let result = tool.execute(json!({})).await.unwrap(); let result = tool.execute(json!({})).await.unwrap();
assert!(!result.success); 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] #[tokio::test]
@ -649,6 +690,10 @@ mod tests {
let result = tool.execute(json!({"operation": "push"})).await.unwrap(); let result = tool.execute(json!({"operation": "push"})).await.unwrap();
assert!(!result.success); assert!(!result.success);
assert!(result.error.as_deref().unwrap_or("").contains("Unknown operation")); assert!(result
.error
.as_deref()
.unwrap_or("")
.contains("Unknown operation"));
} }
} }

View file

@ -101,7 +101,10 @@ pub fn all_tools_with_runtime(
Box::new(MemoryStoreTool::new(memory.clone())), Box::new(MemoryStoreTool::new(memory.clone())),
Box::new(MemoryRecallTool::new(memory.clone())), Box::new(MemoryRecallTool::new(memory.clone())),
Box::new(MemoryForgetTool::new(memory)), 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 { if browser_config.enabled {
@ -184,7 +187,16 @@ mod tests {
}; };
let http = crate::config::HttpRequestConfig::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(); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert!(!names.contains(&"browser_open")); assert!(!names.contains(&"browser_open"));
} }
@ -208,7 +220,16 @@ mod tests {
}; };
let http = crate::config::HttpRequestConfig::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(); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert!(names.contains(&"browser_open")); 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(); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert!(names.contains(&"delegate")); assert!(names.contains(&"delegate"));
} }
@ -353,7 +383,16 @@ mod tests {
let browser = BrowserConfig::default(); let browser = BrowserConfig::default();
let http = crate::config::HttpRequestConfig::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(); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert!(!names.contains(&"delegate")); assert!(!names.contains(&"delegate"));
} }