Refactor IPC and DTO structures; remove unused code and streamline message handling

- Removed unused structs and methods from `response.rs`, `usage.rs`, and `client.rs`.
- Simplified `Connection` handling in `conn.rs` to only support Unix sockets.
- Updated `IpcServer` to exclusively use Unix sockets and removed TCP handling.
- Cleaned up `editlog.rs` by removing loading and recent entry methods.
- Refactored `memory.rs` to eliminate unused functions related to lesson promotion and retrospective creation.
- Enhanced `search.rs` to support multiple search providers and improved error handling.
- Updated chat view logic to simplify message display and improve user experience.
- Removed deprecated modules and constants from various files to streamline the codebase.
This commit is contained in:
asepharyana
2026-07-11 23:45:13 +07:00
parent 93d1bbb7c1
commit fcef85a327
51 changed files with 1431 additions and 1246 deletions
+202 -10
View File
@@ -1,8 +1,37 @@
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::super::Tool;
use super::super::ToolCtx;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SearchProvider {
Tavily,
Brave,
SerpApi,
Google,
}
impl SearchProvider {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"tavily" => Some(SearchProvider::Tavily),
"brave" => Some(SearchProvider::Brave),
"serpapi" | "serp_api" => Some(SearchProvider::SerpApi),
"google" => Some(SearchProvider::Google),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub title: String,
pub url: String,
pub snippet: String,
}
pub struct Search;
impl Tool for Search {
@@ -11,7 +40,7 @@ impl Tool for Search {
}
fn description(&self) -> &'static str {
"Search the web for information. Uses configured search provider."
"Search the web for information using a configured search provider (Tavily, Brave, SerpAPI, or Google)."
}
fn parameters(&self) -> Value {
@@ -21,6 +50,11 @@ impl Tool for Search {
"query": {
"type": "string",
"description": "Search query"
},
"num_results": {
"type": "integer",
"description": "Number of results to return (default: 5)",
"default": 5
}
},
"required": ["query"]
@@ -35,16 +69,174 @@ impl Tool for Search {
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing required argument: query"))?
.to_string();
let results = mock_search(&query);
Ok(results)
let num_results = args.get("num_results")
.and_then(|v| v.as_u64())
.unwrap_or(5) as usize;
let provider = detect_search_provider();
match provider {
Some(p) => search_with_provider(&p, &query, num_results),
None => Ok(format!(
"No search provider configured for query '{}'.\n\
Set ZESDEX_SEARCH_PROVIDER and corresponding API key env vars.\n\
Supported: tavily (ZESDEX_TAVILY_API_KEY), \
brave (ZESDEX_BRAVE_API_KEY), \
serpapi (ZESDEX_SERPAPI_KEY), \
google (ZESDEX_GOOGLE_API_KEY).",
query
)),
}
}
}
pub(crate) fn mock_search(query: &str) -> String {
format!(
"Search results for '{}':\n\n\
No search provider configured. Results are unavailable.\n\
To enable web search, configure a search provider in settings.\n\
Supported providers: tavily, brave, serpapi, google.", query
)
fn detect_search_provider() -> Option<SearchProvider> {
if std::env::var("ZESDEX_SEARCH_PROVIDER").ok().is_some() {
let provider_str = std::env::var("ZESDEX_SEARCH_PROVIDER").unwrap_or_default();
if let Some(p) = SearchProvider::from_str(&provider_str) {
return Some(p);
}
}
if std::env::var("ZESDEX_TAVILY_API_KEY").ok().filter(|k| !k.is_empty()).is_some() {
return Some(SearchProvider::Tavily);
}
if std::env::var("ZESDEX_BRAVE_API_KEY").ok().filter(|k| !k.is_empty()).is_some() {
return Some(SearchProvider::Brave);
}
if std::env::var("ZESDEX_SERPAPI_KEY").ok().filter(|k| !k.is_empty()).is_some() {
return Some(SearchProvider::SerpApi);
}
if std::env::var("ZESDEX_GOOGLE_API_KEY").ok().filter(|k| !k.is_empty()).is_some() {
return Some(SearchProvider::Google);
}
None
}
fn search_with_provider(provider: &SearchProvider, query: &str, num_results: usize) -> Result<String> {
let results = match provider {
SearchProvider::Tavily => search_tavily(query, num_results)?,
SearchProvider::Brave => search_brave(query, num_results)?,
SearchProvider::SerpApi => search_serpapi(query, num_results)?,
SearchProvider::Google => search_google(query, num_results)?,
};
if results.is_empty() {
return Ok(format!("No results found for '{}'.", query));
}
let mut output = format!("Search results for '{}':\n\n", query);
for (i, r) in results.iter().enumerate() {
output.push_str(&format!("{}. {}\n {}\n {}\n\n", i + 1, r.title, r.url, r.snippet));
}
Ok(output)
}
fn search_tavily(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
let api_key = std::env::var("ZESDEX_TAVILY_API_KEY")
.map_err(|_| anyhow!("ZESDEX_TAVILY_API_KEY not set"))?;
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(15))
.build()?;
let body = json!({
"api_key": api_key,
"query": query,
"max_results": num_results,
"include_answer": false,
"search_depth": "basic",
});
let resp = client.post("https://api.tavily.com/search")
.header("Content-Type", "application/json")
.json(&body)
.send()?;
if !resp.status().is_success() {
anyhow::bail!("Tavily API error: {}", resp.status());
}
let data: Value = resp.json()?;
let results = data["results"].as_array().cloned().unwrap_or_default();
Ok(results.iter().filter_map(|r| {
Some(SearchResult {
title: r["title"].as_str()?.to_string(),
url: r["url"].as_str()?.to_string(),
snippet: r["content"].as_str().unwrap_or("").to_string(),
})
}).collect())
}
fn search_brave(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
let api_key = std::env::var("ZESDEX_BRAVE_API_KEY")
.map_err(|_| anyhow!("ZESDEX_BRAVE_API_KEY not set"))?;
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(15))
.build()?;
let resp = client.get("https://api.search.brave.com/res/v1/web/search")
.header("Accept", "application/json")
.header("Accept-Encoding", "gzip")
.header("X-Subscription-Token", &api_key)
.query(&[("q", query), ("count", &num_results.to_string())])
.send()?;
if !resp.status().is_success() {
anyhow::bail!("Brave API error: {}", resp.status());
}
let data: Value = resp.json()?;
let results = data["web"]["results"].as_array().cloned().unwrap_or_default();
Ok(results.iter().filter_map(|r| {
Some(SearchResult {
title: r["title"].as_str()?.to_string(),
url: r["url"].as_str()?.to_string(),
snippet: r["description"].as_str().unwrap_or("").to_string(),
})
}).collect())
}
fn search_serpapi(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
let api_key = std::env::var("ZESDEX_SERPAPI_KEY")
.map_err(|_| anyhow!("ZESDEX_SERPAPI_KEY not set"))?;
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(15))
.build()?;
let resp = client.get("https://serpapi.com/search.json")
.query(&[
("q", query),
("api_key", &api_key),
("engine", "google"),
("num", &num_results.to_string()),
])
.send()?;
if !resp.status().is_success() {
anyhow::bail!("SerpAPI error: {}", resp.status());
}
let data: Value = resp.json()?;
let results = data["organic_results"].as_array().cloned().unwrap_or_default();
Ok(results.iter().filter_map(|r| {
let title = r.get("title")?.as_str()?.to_string();
let url = r.get("link")?.as_str()?.to_string();
let snippet = r.get("snippet").and_then(|s| s.as_str()).unwrap_or("").to_string();
Some(SearchResult { title, url, snippet })
}).collect())
}
fn search_google(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
let api_key = std::env::var("ZESDEX_GOOGLE_API_KEY")
.map_err(|_| anyhow!("ZESDEX_GOOGLE_API_KEY not set"))?;
let cx = std::env::var("ZESDEX_GOOGLE_CX")
.map_err(|_| anyhow!("ZESDEX_GOOGLE_CX (Custom Search Engine ID) not set"))?;
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(15))
.build()?;
let resp = client.get("https://www.googleapis.com/customsearch/v1")
.query(&[
("q", query),
("key", &api_key),
("cx", &cx),
("num", &num_results.min(10).to_string()),
])
.send()?;
if !resp.status().is_success() {
anyhow::bail!("Google Custom Search API error: {}", resp.status());
}
let data: Value = resp.json()?;
let results = data["items"].as_array().cloned().unwrap_or_default();
Ok(results.iter().filter_map(|r| {
let title = r.get("title")?.as_str()?.to_string();
let url = r.get("link")?.as_str()?.to_string();
let snippet = r.get("snippet").and_then(|s| s.as_str()).unwrap_or("").to_string();
Some(SearchResult { title, url, snippet })
}).collect())
}
+3 -14
View File
@@ -13,7 +13,6 @@ pub mod plan;
pub mod search;
pub mod seqthink;
pub mod shell;
pub mod shell_filter;
pub mod utility;
pub mod workflow;
@@ -36,7 +35,7 @@ pub struct ToolCtx {
pub workspaces: Vec<PathBuf>,
pub session_dir: PathBuf,
pub memory_dir: PathBuf,
pub download_dir: PathBuf,
pub _download_dir: PathBuf,
pub worktrees_dir: PathBuf,
pub dir_cache: std::sync::Arc<tokio::sync::RwLock<super::app::state::misc::DirCache>>,
pub internet_mode: super::model::settings::InternetMode,
@@ -89,20 +88,14 @@ impl Default for ToolCtxBuilder {
}
impl ToolCtxBuilder {
pub fn workspaces(mut self, v: Vec<PathBuf>) -> Self { self.workspaces = v; self }
pub fn session_dir(mut self, v: PathBuf) -> Self { self.session_dir = v; self }
pub fn memory_dir(mut self, v: PathBuf) -> Self { self.memory_dir = v; self }
pub fn download_dir(mut self, v: PathBuf) -> Self { self.download_dir = v; self }
pub fn worktrees_dir(mut self, v: PathBuf) -> Self { self.worktrees_dir = v; self }
pub fn internet_mode(mut self, v: super::model::settings::InternetMode) -> Self { self.internet_mode = v; self }
pub fn origin(mut self, v: crate::app::state::types::Origin) -> Self { self.origin = v; self }
pub fn graduated_checks(mut self, v: Vec<GraduatedCheck>) -> Self { self.graduated_checks = v; self }
pub fn build(self) -> ToolCtx {
ToolCtx {
workspaces: self.workspaces,
session_dir: self.session_dir,
memory_dir: self.memory_dir,
download_dir: self.download_dir,
_download_dir: self.download_dir,
worktrees_dir: self.worktrees_dir,
dir_cache: self.dir_cache,
internet_mode: self.internet_mode,
@@ -130,6 +123,7 @@ pub fn all_tools() -> Vec<Box<dyn Tool>> {
Box::new(super::tool::plan::PlanEnter),
Box::new(super::tool::plan::PlanReady),
Box::new(super::tool::workflow::WorkflowRun),
Box::new(super::tool::workflow::NoteFinding),
Box::new(super::tool::internet::fetch::Fetch),
Box::new(super::tool::internet::download::Download),
Box::new(super::tool::internet::search::Search),
@@ -162,11 +156,6 @@ pub fn tool_defs(tools: &[Box<dyn Tool>]) -> Vec<crate::dto::provider::request::
.collect()
}
pub const DEFERRED_TOOLS: &[&str] = &[
"read", "write", "edit", "bash", "grep", "glob",
"git_operator", "git_worktree", "git_cred",
];
pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result<PathBuf> {
let _parts: Vec<&str> = rel.splitn(2, '/').collect();
let (ws_idx, path) = if rel.starts_with('[') {
+37 -3
View File
@@ -11,7 +11,7 @@ impl Tool for WorkflowRun {
}
fn description(&self) -> &'static str {
"Execute a workflow script by delegating to the workflow engine"
"Execute a workflow script that can spawn multiple subagents in parallel, pipeline, or phased stages. Use when a task benefits from decomposition into independent subtasks. Simple tasks should be handled inline without this tool."
}
fn parameters(&self) -> Value {
@@ -20,11 +20,11 @@ impl Tool for WorkflowRun {
"properties": {
"script": {
"type": "string",
"description": "Workflow script content or path to a workflow file"
"description": "JSON-encoded workflow script with name, description, script (Agent/Parallel/Pipeline/Phase primitives), and options (max_concurrency, continue_on_error)"
},
"args": {
"type": "object",
"description": "Optional arguments passed to the workflow script"
"description": "Optional string key-value arguments passed to the workflow script for template substitution ({{key}} placeholders)"
}
},
"required": ["script"]
@@ -52,3 +52,37 @@ impl Tool for WorkflowRun {
crate::app::workflow::engine::run_workflow(&workflow_script, &workflow_args)
}
}
pub struct NoteFinding;
impl Tool for NoteFinding {
fn name(&self) -> &'static str {
"note_finding"
}
fn description(&self) -> &'static str {
"Share a finding with sibling agents in the same workflow_run. Findings are ephemeral to the current run and will be prepended to other agents' next tool-round context. Does not persist to memory."
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The finding to share with sibling agents"
}
},
"required": ["text"]
})
}
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
let text = args.get("text")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("missing required argument: text"))?;
crate::app::workflow::engine::note_finding(text);
Ok(format!("finding recorded: {}", text.chars().take(80).collect::<String>()))
}
}