fix(agent): recall search + memory_dir fallback + bersihkan dead llm_client
Hasil audit round 4 (workflow/hive_mind + memory + semantic_search). - fix(memory): recall.search selama ini TIDAK pernah dipakai — tool mengiklankan keyword search di skema tapi run() cuma list semua nama. Kini search benar-benar memfilter (cocok di name/description/content, case-insensitive), + output 'No memories match' bila kosong. - fix(memory): ToolCtxBuilder tidak punya setter memory_dir dan tak ada call-site yang mengisinya — remember/recall/forget memakai PathBuf kosong dan menulis memory ke CWD (bukan lokasi persisten). Tambah setter memory_dir + worktrees_dir, dan helper resolve_memory_dir() yang fallback ke Store::new().memory_dir bila ctx.memory_dir kosong; dipakai di ketiga tool memory. - refactor(workflow): hapus LlmClient dummy di WorkflowRun (dibuat dengan API key kosong + model default + base_url default lalu tak pernah dipakai — execute_workflow menerimanya sebagai _llm_client). Kini execute_workflow tak ambil parameter tak terpakai; LLM asli tetap lewat execute_primitive yang resolve kredensial dengan benar. - test: +2 (recall search memfilter; resolve_memory_dir fallback/eksplisit). Catatan audit yang dilaporkan (belum difix): synth_consensus hanya menggabungkan output (label Consensus menyesatkan, bukan sintesis LLM), dan semantic_search memegang Mutex index global saat full rebuild (bottleneck saat paralel) + index tidak workspace-aware. PENTING (infra): disk root 100% saat kerja. Saya bebaskan ~4.6G dari /tmp + cache aman (sekai*, verify-z, bun/npm cache). target/debug di repo = 38G — rampah, perlu cargo clean + rebuild (jangan dibiarkan).
This commit is contained in:
@@ -66,6 +66,14 @@ impl ToolCtxBuilder {
|
||||
self.session_dir = v;
|
||||
self
|
||||
}
|
||||
pub fn memory_dir(mut self, v: PathBuf) -> Self {
|
||||
self.memory_dir = v;
|
||||
self
|
||||
}
|
||||
pub fn worktrees_dir(mut self, v: PathBuf) -> Self {
|
||||
self.worktrees_dir = v;
|
||||
self
|
||||
}
|
||||
pub fn workspaces(mut self, v: Vec<PathBuf>) -> Self {
|
||||
self.workspaces = v;
|
||||
self
|
||||
|
||||
@@ -43,7 +43,8 @@ impl Tool for Forget {
|
||||
let name = crate::tools::arg_str(args, "name")?;
|
||||
info!(name, "forget invoked");
|
||||
let repo = crate::persistence::cms::memory_repo::MarkdownMemoryRepository::new();
|
||||
repo.delete(&ctx.memory_dir, &name)?;
|
||||
let memory_dir = crate::tools::memory::resolve_memory_dir(&ctx.memory_dir);
|
||||
repo.delete(&memory_dir, &name)?;
|
||||
info!(name, "memory deleted");
|
||||
Ok(format!("Memory '{}' deleted", name))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
//! Memory management tools — remember, recall, forget.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub mod forget;
|
||||
pub mod recall;
|
||||
pub mod remember;
|
||||
|
||||
/// Resolve the directory the memory tools should read/write.
|
||||
///
|
||||
/// Prefer an explicitly-configured `ToolCtx.memory_dir`. If that is empty
|
||||
/// (a `ToolCtx` is often built without setting `memory_dir`), fall back to
|
||||
/// the canonical persistent memory location from `Store` so memories are not
|
||||
/// silently written into the current working directory.
|
||||
pub fn resolve_memory_dir(ctx_memory_dir: &std::path::Path) -> PathBuf {
|
||||
if ctx_memory_dir.as_os_str().is_empty() {
|
||||
zesdex_domain::core::Store::new().memory_dir
|
||||
} else {
|
||||
ctx_memory_dir.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,21 +45,132 @@ impl Tool for Recall {
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let repo = crate::persistence::cms::memory_repo::MarkdownMemoryRepository::new();
|
||||
let memory_dir = crate::tools::memory::resolve_memory_dir(&ctx.memory_dir);
|
||||
|
||||
let specific_name = args.get("name").and_then(|v| v.as_str());
|
||||
let search = args.get("search").and_then(|v| v.as_str());
|
||||
|
||||
if let Some(name) = specific_name {
|
||||
info!(name, "recall loading specific memory");
|
||||
let memory = repo.load(&ctx.memory_dir, name)?;
|
||||
Ok(serde_json::to_string_pretty(&memory)?)
|
||||
} else {
|
||||
info!("recall listing all memories");
|
||||
let names = repo.list(&ctx.memory_dir)?;
|
||||
if names.is_empty() {
|
||||
info!("no memories found");
|
||||
return Ok("No memories saved yet".to_string());
|
||||
}
|
||||
Ok(format!("Available memories:\n{}", names.join("\n")))
|
||||
let memory = repo.load(&memory_dir, name)?;
|
||||
return Ok(serde_json::to_string_pretty(&memory)?);
|
||||
}
|
||||
|
||||
if let Some(query) = search {
|
||||
let query = query.trim().to_lowercase();
|
||||
info!(search = %query, "recall searching memories");
|
||||
if query.is_empty() {
|
||||
return Ok("Search query is empty".to_string());
|
||||
}
|
||||
let names = repo.list(&memory_dir)?;
|
||||
let mut matches: Vec<String> = Vec::new();
|
||||
for name in &names {
|
||||
// Load each memory and match against name/description/content.
|
||||
if let Ok(m) = repo.load(&memory_dir, name) {
|
||||
let haystack =
|
||||
format!("{} {} {}", m.name, m.description, m.content).to_lowercase();
|
||||
if haystack.contains(&query) {
|
||||
matches.push(m.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches.is_empty() {
|
||||
return Ok(format!("No memories match '{query}'"));
|
||||
}
|
||||
return Ok(format!(
|
||||
"Memories matching '{query}' ({}):\n{}",
|
||||
matches.len(),
|
||||
matches.join("\n")
|
||||
));
|
||||
}
|
||||
|
||||
info!("recall listing all memories");
|
||||
let names = repo.list(&memory_dir)?;
|
||||
if names.is_empty() {
|
||||
info!("no memories found");
|
||||
return Ok("No memories saved yet".to_string());
|
||||
}
|
||||
Ok(format!("Available memories:\n{}", names.join("\n")))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tools::ToolCtxBuilder;
|
||||
use zesdex_domain::cms::MemoryRepository;
|
||||
|
||||
fn save_mem(name: &str, description: &str, content: &str, dir: &std::path::Path) {
|
||||
let repo = crate::persistence::cms::memory_repo::MarkdownMemoryRepository::new();
|
||||
let memory = zesdex_domain::cms::Memory {
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
content: content.to_string(),
|
||||
kind: "reference".to_string(),
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
outcome: None,
|
||||
lifecycle: "active".to_string(),
|
||||
scope: None,
|
||||
before_snippet: None,
|
||||
after_snippet: None,
|
||||
provenances: Vec::new(),
|
||||
};
|
||||
repo.save(dir, &memory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_filters_memories_by_keyword() {
|
||||
let dir = std::env::temp_dir().join(format!("zdx-mem-test-{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
save_mem(
|
||||
"rust-concurrency",
|
||||
"tokio spawn patterns",
|
||||
"how to use async tasks",
|
||||
&dir,
|
||||
);
|
||||
save_mem(
|
||||
"docker-deploy",
|
||||
"deploy via compose",
|
||||
"container orchestration",
|
||||
&dir,
|
||||
);
|
||||
|
||||
let tool_ctx = ToolCtxBuilder::default().memory_dir(dir.clone()).build();
|
||||
let args = serde_json::json!({ "search": "tokio" });
|
||||
|
||||
let out = Recall.run(&tool_ctx, &args).unwrap();
|
||||
assert!(
|
||||
out.contains("rust-concurrency"),
|
||||
"should match rust-concurrency, got: {out}"
|
||||
);
|
||||
assert!(
|
||||
!out.contains("docker-deploy"),
|
||||
"docker-deploy should not match tokio"
|
||||
);
|
||||
|
||||
// A query with no match reports so.
|
||||
let no_match = Recall
|
||||
.run(&tool_ctx, &serde_json::json!({ "search": "zzzznope" }))
|
||||
.unwrap();
|
||||
assert!(no_match.contains("No memories match"), "{no_match}");
|
||||
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_memory_dir_falls_back_to_store_when_empty() {
|
||||
// An empty ToolCtx.memory_dir is resolved to the canonical Store path.
|
||||
let resolved = crate::tools::memory::resolve_memory_dir(std::path::Path::new(""));
|
||||
assert!(!resolved.as_os_str().is_empty());
|
||||
assert!(
|
||||
resolved.ends_with("memory"),
|
||||
"expected memory dir, got {resolved:?}"
|
||||
);
|
||||
|
||||
// An explicit memory_dir is preserved.
|
||||
let explicit =
|
||||
crate::tools::memory::resolve_memory_dir(std::path::Path::new("/tmp/custom-memory"));
|
||||
assert_eq!(explicit, std::path::Path::new("/tmp/custom-memory"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,8 @@ impl Tool for Remember {
|
||||
};
|
||||
|
||||
let repo = crate::persistence::cms::memory_repo::MarkdownMemoryRepository::new();
|
||||
repo.save(&ctx.memory_dir, &memory)?;
|
||||
let memory_dir = crate::tools::memory::resolve_memory_dir(&ctx.memory_dir);
|
||||
repo.save(&memory_dir, &memory)?;
|
||||
|
||||
info!(name, "memory saved");
|
||||
Ok(format!("Memory '{}' saved", name))
|
||||
|
||||
@@ -9,7 +9,6 @@ use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
|
||||
use crate::llm::provider::LlmClient;
|
||||
use crate::tools::{arg_str, Tool, ToolCtx};
|
||||
use crate::workflow::engine::execution::execute_workflow;
|
||||
use crate::workflow::hive_mind::cycle::execute_cycle;
|
||||
@@ -60,14 +59,8 @@ impl Tool for WorkflowRun {
|
||||
phase_names.join(", ")
|
||||
);
|
||||
|
||||
let llm_client = LlmClient::new(
|
||||
crate::llm::provider::DEFAULT_API_KEY.to_string(),
|
||||
zesdex_domain::agent::defaults::DEFAULT_MODEL.to_string(),
|
||||
None,
|
||||
);
|
||||
let rt = crate::runtime::runtime();
|
||||
let result: Vec<String> =
|
||||
rt.block_on(async { execute_workflow(&script, ctx, &llm_client).await })?;
|
||||
let result: Vec<String> = rt.block_on(async { execute_workflow(&script, ctx).await })?;
|
||||
|
||||
info!(phase_count = result.len(), "Workflow completed");
|
||||
Ok(format!(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use anyhow::Result;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use crate::llm::provider::LlmClient;
|
||||
use crate::tools::ToolCtx;
|
||||
use crate::workflow::engine::primitives::execute_primitive;
|
||||
use zesdex_domain::workflow::WorkflowScript;
|
||||
@@ -11,12 +10,8 @@ use zesdex_domain::workflow::WorkflowScript;
|
||||
/// Execute each phase of a workflow script sequentially.
|
||||
///
|
||||
/// Flow: for each phase → execute_primitive → collect result.
|
||||
#[instrument(skip(tool_ctx, _llm_client))]
|
||||
pub async fn execute_workflow(
|
||||
script: &WorkflowScript,
|
||||
tool_ctx: &ToolCtx,
|
||||
_llm_client: &LlmClient,
|
||||
) -> Result<Vec<String>> {
|
||||
#[instrument(skip(tool_ctx))]
|
||||
pub async fn execute_workflow(script: &WorkflowScript, tool_ctx: &ToolCtx) -> Result<Vec<String>> {
|
||||
info!(
|
||||
"Executing workflow: {} ({} phases)",
|
||||
script.name,
|
||||
|
||||
Reference in New Issue
Block a user