refactor: consolidate hypotheses, agent_signals, and process_logs into action-based smart tools
This commit is contained in:
1 parent
79209da711
commit
3b146f91c2
17 files changed
+438
-359
No files matched your search
@@ -67,6 +67,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync instructions.md
|
||||
let instructions_src = PathBuf::from(r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\instructions.md");
|
||||
if instructions_src.exists() {
|
||||
let _ = fs::copy(&instructions_src, win_dir.join("instructions.md"));
|
||||
let _ = fs::copy(&instructions_src, wsl_dir.join("instructions.md"));
|
||||
println!(" [OK] Synchronized instructions.md to Win and WSL");
|
||||
}
|
||||
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
|
||||
println!("Schema export complete!");
|
||||
|
||||
@@ -521,7 +521,7 @@ function parseActivityPayload(item) {
|
||||
TASK_CREATE: "#1abc9c",
|
||||
TASK_UPDATE: "#1abc9c",
|
||||
CLIPBOARD: "#9b59b6",
|
||||
STICKY_NOTE: "#e67e22",
|
||||
HANDOFF_MEMO: "#e67e22",
|
||||
CHECKPOINT: "#e74c3c",
|
||||
ERROR_FIX: "#e74c3c",
|
||||
TECH_DEBT: "#d35400",
|
||||
|
||||
@@ -711,7 +711,7 @@ function parseActivityPayload(item: any): string {
|
||||
TASK_CREATE: "#1abc9c",
|
||||
TASK_UPDATE: "#1abc9c",
|
||||
CLIPBOARD: "#9b59b6",
|
||||
STICKY_NOTE: "#e67e22",
|
||||
HANDOFF_MEMO: "#e67e22",
|
||||
CHECKPOINT: "#e74c3c",
|
||||
ERROR_FIX: "#e74c3c",
|
||||
TECH_DEBT: "#d35400",
|
||||
|
||||
@@ -14,6 +14,9 @@ struct CandleEmbeddingModel {
|
||||
|
||||
impl CandleEmbeddingModel {
|
||||
fn new() -> Result<Self, String> {
|
||||
if cfg!(test) || std::env::var("MCP_OFFLINE_EMBEDDINGS").is_ok() {
|
||||
return Err("Offline embedding mode active (tests/offline flag)".to_string());
|
||||
}
|
||||
let client = hf_hub::HFClientSync::new().map_err(|e| e.to_string())?;
|
||||
let repo = client.model("sentence-transformers", "all-MiniLM-L6-v2");
|
||||
|
||||
|
||||
@@ -1223,7 +1223,7 @@ impl McpTool for SummarizeSubgraphHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::handlers::meta::{BroadcastAgentSignalHandler, QueryAgentSignalsHandler};
|
||||
use crate::handlers::meta::AgentSignalsHandler;
|
||||
use serde_json::json;
|
||||
|
||||
|
||||
@@ -1515,19 +1515,21 @@ mod tests {
|
||||
}), state.clone()).await.unwrap();
|
||||
assert_eq!(del_rel_res, "Relations deleted");
|
||||
|
||||
let bcast_handler = BroadcastAgentSignalHandler;
|
||||
let bcast_handler = AgentSignalsHandler;
|
||||
let bcast_res = bcast_handler.execute(json!({
|
||||
"action": "broadcast",
|
||||
"sender": "agent1",
|
||||
"signal_type": "task_completed",
|
||||
"payload": "fix_bug"
|
||||
}), state.clone()).await.unwrap();
|
||||
assert!(bcast_res.contains("Broadcasted signal"));
|
||||
|
||||
|
||||
let qsignal_handler = QueryAgentSignalsHandler;
|
||||
let qsignal_res = qsignal_handler.execute(json!({"sender": "agent1"}), state.clone()).await.unwrap();
|
||||
let qsignal_handler = AgentSignalsHandler;
|
||||
let qsignal_res = qsignal_handler.execute(json!({
|
||||
"action": "query",
|
||||
"sender": "agent1"
|
||||
}), state.clone()).await.unwrap();
|
||||
assert!(qsignal_res.contains("task_completed"));
|
||||
|
||||
let read_paged_handler = ReadGraphHandler;
|
||||
let paged_res = read_paged_handler
|
||||
.execute(json!({"limit": 1, "offset": 0}), state.clone())
|
||||
|
||||
+88
-74
@@ -1,90 +1,81 @@
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::{GetRecentLogsTool, WatchProcessLogsTool};
|
||||
use crate::tools::{ProcessLogAction, ProcessLogsTool};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct WatchProcessLogsHandler;
|
||||
pub struct ProcessLogsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for WatchProcessLogsHandler {
|
||||
impl McpTool for ProcessLogsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"watch_process_logs"
|
||||
"process_logs"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<WatchProcessLogsTool>(
|
||||
"watch_process_logs",
|
||||
"Tail a specific log file in the background.",
|
||||
crate::mcp::tool_def::<ProcessLogsTool>(
|
||||
"process_logs",
|
||||
"Monitor, tail, and manage process logs: watch a log file, tail recent output, or clear log files.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let tool_args: WatchProcessLogsTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let tool_args: ProcessLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let safe_path = crate::handlers::utils::validate_safe_path(&tool_args.file_path)?;
|
||||
if !safe_path.exists() {
|
||||
return Err(crate::error::AppError::Internal(format!(
|
||||
"File does not exist: {}",
|
||||
tool_args.file_path
|
||||
)));
|
||||
|
||||
match tool_args.action {
|
||||
ProcessLogAction::Watch => {
|
||||
if !safe_path.exists() {
|
||||
return Err(crate::error::AppError::Internal(format!(
|
||||
"File does not exist: {}",
|
||||
tool_args.file_path
|
||||
)));
|
||||
}
|
||||
Ok(format!("Started watching logs for {}", tool_args.file_path))
|
||||
}
|
||||
ProcessLogAction::Get => {
|
||||
let max_lines = tool_args.max_lines.unwrap_or(100);
|
||||
let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
|
||||
let mut file = File::open(&safe_path).map_err(|e| {
|
||||
crate::error::AppError::Internal(format!("Failed to open file: {}", e))
|
||||
})?;
|
||||
let len = file.metadata().map_err(|e| e.to_string())?.len();
|
||||
|
||||
let read_size = std::cmp::min(16 * 1024, len);
|
||||
file.seek(SeekFrom::End(-(read_size as i64)))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut vec_buf = Vec::new();
|
||||
file.read_to_end(&mut vec_buf)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let buffer = String::from_utf8_lossy(&vec_buf).to_string();
|
||||
let lines: Vec<&str> = buffer.lines().collect();
|
||||
let recent_lines = if lines.len() > max_lines {
|
||||
lines[lines.len() - max_lines..].join("\n")
|
||||
} else {
|
||||
buffer
|
||||
};
|
||||
|
||||
Ok(recent_lines)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
ProcessLogAction::Clear => {
|
||||
if safe_path.exists() {
|
||||
std::fs::write(&safe_path, "").map_err(|e| {
|
||||
crate::error::AppError::Internal(format!("Failed to clear log file: {}", e))
|
||||
})?;
|
||||
}
|
||||
Ok(format!("Cleared logs in {}", tool_args.file_path))
|
||||
}
|
||||
}
|
||||
Ok(format!("Started watching logs for {}", tool_args.file_path))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GetRecentLogsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for GetRecentLogsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"get_recent_logs"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<GetRecentLogsTool>(
|
||||
"get_recent_logs",
|
||||
"Get the recent logs (last 100 lines) from a watched file.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let tool_args: GetRecentLogsTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let safe_path = crate::handlers::utils::validate_safe_path(&tool_args.file_path)?;
|
||||
|
||||
let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
|
||||
let mut file = File::open(&safe_path).map_err(|e| {
|
||||
crate::error::AppError::Internal(format!("Failed to open file: {}", e))
|
||||
})?;
|
||||
let len = file.metadata().map_err(|e| e.to_string())?.len();
|
||||
|
||||
let read_size = std::cmp::min(16 * 1024, len);
|
||||
file.seek(SeekFrom::End(-(read_size as i64)))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut vec_buf = Vec::new();
|
||||
file.read_to_end(&mut vec_buf)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let buffer = String::from_utf8_lossy(&vec_buf).to_string();
|
||||
|
||||
let lines: Vec<&str> = buffer.lines().collect();
|
||||
let recent_lines = if lines.len() > 100 {
|
||||
lines[lines.len() - 100..].join("\n")
|
||||
} else {
|
||||
buffer
|
||||
};
|
||||
|
||||
Ok(recent_lines)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,15 +87,16 @@ mod tests {
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_watch_process_logs() {
|
||||
async fn test_process_logs_watch() {
|
||||
let dir = tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = WatchProcessLogsHandler;
|
||||
let handler = ProcessLogsHandler;
|
||||
|
||||
let log_file = dir.path().join("test.log");
|
||||
std::fs::write(&log_file, "line1\nline2").unwrap();
|
||||
|
||||
let args = json!({
|
||||
"action": "watch",
|
||||
"file_path": log_file.to_str().unwrap()
|
||||
});
|
||||
|
||||
@@ -117,15 +109,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_recent_logs() {
|
||||
async fn test_process_logs_get() {
|
||||
let dir = tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = GetRecentLogsHandler;
|
||||
let handler = ProcessLogsHandler;
|
||||
|
||||
let log_file = dir.path().join("test_recent.log");
|
||||
std::fs::write(&log_file, "line1\nline2\nline3").unwrap();
|
||||
|
||||
let args = json!({
|
||||
"action": "get",
|
||||
"file_path": log_file.to_str().unwrap()
|
||||
});
|
||||
|
||||
@@ -139,10 +132,31 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_recent_logs_with_large_file() {
|
||||
async fn test_process_logs_clear() {
|
||||
let dir = tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = ProcessLogsHandler;
|
||||
|
||||
let log_file = dir.path().join("test_clear.log");
|
||||
std::fs::write(&log_file, "line1\nline2\nline3").unwrap();
|
||||
|
||||
let args = json!({
|
||||
"action": "clear",
|
||||
"file_path": log_file.to_str().unwrap()
|
||||
});
|
||||
|
||||
let result = handler.execute(args, state.clone()).await.unwrap();
|
||||
assert!(result.contains("Cleared logs"));
|
||||
|
||||
let content = std::fs::read_to_string(&log_file).unwrap();
|
||||
assert_eq!(content, "");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_process_logs_get_with_large_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = GetRecentLogsHandler;
|
||||
let handler = ProcessLogsHandler;
|
||||
|
||||
let log_file = dir.path().join("large_test.log");
|
||||
let mut buffer = String::new();
|
||||
@@ -152,6 +166,7 @@ mod tests {
|
||||
std::fs::write(&log_file, buffer).unwrap();
|
||||
|
||||
let args = serde_json::json!({
|
||||
"action": "get",
|
||||
"file_path": log_file.to_str().unwrap()
|
||||
});
|
||||
|
||||
@@ -163,4 +178,3 @@ mod tests {
|
||||
assert!(result.contains("line"));
|
||||
}
|
||||
}
|
||||
|
||||
+161
-176
@@ -1384,84 +1384,73 @@ impl McpTool for GetNextActionableTasksHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LogHypothesisHandler;
|
||||
pub struct HypothesesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for LogHypothesisHandler {
|
||||
impl McpTool for HypothesesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"log_hypothesis"
|
||||
"hypotheses"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<LogHypothesisTool>(
|
||||
"log_hypothesis",
|
||||
"Log a diagnostic hypothesis and associated evidence for a task",
|
||||
crate::mcp::tool_def::<HypothesesTool>(
|
||||
"hypotheses",
|
||||
"Manage diagnostic hypotheses, tested evidence, and status during problem solving: log new hypotheses or query existing ones.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: LogHypothesisTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let hyp_id = format!(
|
||||
"HYP-{}",
|
||||
uuid::Uuid::new_v4().to_string()[..8].to_uppercase()
|
||||
);
|
||||
let timestamp = now_secs();
|
||||
let req: HypothesesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
|
||||
let record = crate::models::Hypothesis {
|
||||
id: hyp_id.clone(),
|
||||
task_id: req.task_id,
|
||||
hypothesis: req.hypothesis,
|
||||
status: req.status.unwrap_or_else(|| "unverified".to_string()),
|
||||
evidence: req.evidence,
|
||||
timestamp,
|
||||
..Default::default()
|
||||
};
|
||||
match req.action {
|
||||
HypothesisAction::Log => {
|
||||
let hyp_text = req.hypothesis.ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required 'hypothesis' for action 'log'".to_string())
|
||||
})?;
|
||||
let hyp_id = format!(
|
||||
"HYP-{}",
|
||||
uuid::Uuid::new_v4().to_string()[..8].to_uppercase()
|
||||
);
|
||||
let timestamp = now_secs();
|
||||
|
||||
state.code.hypotheses.modify(|h| h.push(record));
|
||||
let record = crate::models::Hypothesis {
|
||||
id: hyp_id.clone(),
|
||||
task_id: req.task_id,
|
||||
hypothesis: hyp_text,
|
||||
status: req.status.unwrap_or_else(|| "unverified".to_string()),
|
||||
evidence: req.evidence,
|
||||
timestamp,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(format!("Hypothesis '{}' logged successfully.", hyp_id))
|
||||
}
|
||||
}
|
||||
state.code.hypotheses.modify(|h| h.push(record));
|
||||
|
||||
pub struct QueryHypothesesHandler;
|
||||
Ok(format!("Hypothesis '{}' logged successfully.", hyp_id))
|
||||
}
|
||||
HypothesisAction::Query => {
|
||||
let hypotheses = state.code.hypotheses.read_with(|h| h.clone());
|
||||
let filtered: Vec<_> = hypotheses
|
||||
.into_iter()
|
||||
.filter(|h| {
|
||||
if let Some(tid) = &req.task_id {
|
||||
if h.task_id.as_ref() != Some(tid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(q) = &req.query {
|
||||
let lq = q.to_lowercase();
|
||||
return h.hypothesis.to_lowercase().contains(&lq)
|
||||
|| h.evidence
|
||||
.as_ref()
|
||||
.map_or(false, |e| e.to_lowercase().contains(&lq));
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for QueryHypothesesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"query_hypotheses"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<QueryHypothesesTool>(
|
||||
"query_hypotheses",
|
||||
"Query active diagnostic hypotheses and evidence by task ID or keyword",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: QueryHypothesesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
|
||||
let hypotheses = state.code.hypotheses.read_with(|h| h.clone());
|
||||
let filtered: Vec<_> = hypotheses
|
||||
.into_iter()
|
||||
.filter(|h| {
|
||||
if let Some(tid) = &req.task_id {
|
||||
if h.task_id.as_ref() != Some(tid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(q) = &req.query {
|
||||
let lq = q.to_lowercase();
|
||||
return h.hypothesis.to_lowercase().contains(&lq)
|
||||
|| h.evidence
|
||||
.as_ref()
|
||||
.map_or(false, |e| e.to_lowercase().contains(&lq));
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::to_string_pretty(&filtered)?)
|
||||
Ok(serde_json::to_string_pretty(&filtered)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1542,117 +1531,111 @@ impl McpTool for GetPreflightContextHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BroadcastAgentSignalHandler;
|
||||
pub struct AgentSignalsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for BroadcastAgentSignalHandler {
|
||||
impl McpTool for AgentSignalsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"broadcast_agent_signal"
|
||||
"agent_signals"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<BroadcastAgentSignalTool>(
|
||||
"broadcast_agent_signal",
|
||||
"Broadcast real-time inter-agent signal to peer subagents.",
|
||||
crate::mcp::tool_def::<AgentSignalsTool>(
|
||||
"agent_signals",
|
||||
"Real-time inter-agent communication bus: broadcast signals or query active signals from peer subagents.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: BroadcastAgentSignalTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let req: AgentSignalsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
match req.action {
|
||||
AgentSignalAction::Broadcast => {
|
||||
let sender = req.sender.ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required 'sender' for action 'broadcast'".to_string())
|
||||
})?;
|
||||
let signal_type = req.signal_type.ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required 'signal_type' for action 'broadcast'".to_string())
|
||||
})?;
|
||||
let payload = req.payload.ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required 'payload' for action 'broadcast'".to_string())
|
||||
})?;
|
||||
|
||||
let sig_id = format!("sig_{}", timestamp);
|
||||
let signal = crate::models::AgentSignal {
|
||||
id: sig_id.clone(),
|
||||
sender: req.sender.clone(),
|
||||
signal_type: req.signal_type.clone(),
|
||||
payload: req.payload,
|
||||
timestamp,
|
||||
ttl_seconds: req.ttl_seconds,
|
||||
..Default::default()
|
||||
};
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
state.telemetry.agent_signals.modify(|s| {
|
||||
s.retain(|sig| {
|
||||
if let Some(ttl) = sig.ttl_seconds {
|
||||
timestamp <= sig.timestamp + ttl
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
s.push(signal);
|
||||
if s.len() > 500 {
|
||||
s.remove(0);
|
||||
let sig_id = format!("sig_{}", timestamp);
|
||||
let signal = crate::models::AgentSignal {
|
||||
id: sig_id.clone(),
|
||||
sender: sender.clone(),
|
||||
signal_type: signal_type.clone(),
|
||||
payload,
|
||||
timestamp,
|
||||
ttl_seconds: req.ttl_seconds,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
state.telemetry.agent_signals.modify(|s| {
|
||||
s.retain(|sig| {
|
||||
if let Some(ttl) = sig.ttl_seconds {
|
||||
timestamp <= sig.timestamp + ttl
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
s.push(signal);
|
||||
if s.len() > 500 {
|
||||
s.remove(0);
|
||||
}
|
||||
});
|
||||
state.record_activity(
|
||||
"agent_signal",
|
||||
&format!("{}: {}", sender, signal_type),
|
||||
None,
|
||||
);
|
||||
|
||||
Ok(format!(
|
||||
"Broadcasted signal '{}' from agent '{}'.",
|
||||
sig_id, sender
|
||||
))
|
||||
}
|
||||
});
|
||||
state.record_activity(
|
||||
"agent_signal",
|
||||
&format!("{}: {}", req.sender, req.signal_type),
|
||||
None,
|
||||
);
|
||||
AgentSignalAction::Query => {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
Ok(format!(
|
||||
"Broadcasted signal '{}' from agent '{}'.",
|
||||
sig_id, req.sender
|
||||
))
|
||||
}
|
||||
}
|
||||
let filtered = state.telemetry.agent_signals.read_with(|signals| {
|
||||
signals
|
||||
.iter()
|
||||
.filter(|s| {
|
||||
if let Some(ttl) = s.ttl_seconds {
|
||||
if now > s.timestamp + ttl {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(sender) = &req.sender {
|
||||
if s.sender.to_lowercase() != sender.to_lowercase() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(st) = &req.signal_type {
|
||||
if s.signal_type.to_lowercase() != st.to_lowercase() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.take(req.limit.unwrap_or(20))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
pub struct QueryAgentSignalsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for QueryAgentSignalsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"query_agent_signals"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<QueryAgentSignalsTool>(
|
||||
"query_agent_signals",
|
||||
"Query active inter-agent signals from subagent signal bus.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: QueryAgentSignalsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
let filtered = state.telemetry.agent_signals.read_with(|signals| {
|
||||
signals
|
||||
.iter()
|
||||
.filter(|s| {
|
||||
if let Some(ttl) = s.ttl_seconds {
|
||||
if now > s.timestamp + ttl {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(sender) = &req.sender {
|
||||
if s.sender.to_lowercase() != sender.to_lowercase() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(st) = &req.signal_type {
|
||||
if s.signal_type.to_lowercase() != st.to_lowercase() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.take(req.limit.unwrap_or(20))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
Ok(serde_json::to_string_pretty(&filtered)?)
|
||||
Ok(serde_json::to_string_pretty(&filtered)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2048,11 +2031,12 @@ mod tests {
|
||||
.await;
|
||||
assert!(res_td_res.is_ok());
|
||||
|
||||
// LogHypothesis & QueryHypotheses
|
||||
let log_hyp = LogHypothesisHandler;
|
||||
let hyp_res = log_hyp
|
||||
// Hypotheses
|
||||
let hyp_handler = HypothesesHandler;
|
||||
let hyp_res = hyp_handler
|
||||
.execute(
|
||||
serde_json::json!({
|
||||
"action": "log",
|
||||
"hypothesis": "Caching improves response speed",
|
||||
"status": "testing"
|
||||
}),
|
||||
@@ -2062,9 +2046,8 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(hyp_res.contains("Hypothesis"));
|
||||
|
||||
let q_hyp = QueryHypothesesHandler;
|
||||
let q_hyp_res = q_hyp
|
||||
.execute(serde_json::json!({}), state.clone())
|
||||
let q_hyp_res = hyp_handler
|
||||
.execute(serde_json::json!({"action": "query"}), state.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(q_hyp_res.contains("Caching improves response speed"));
|
||||
@@ -2185,10 +2168,11 @@ mod tests {
|
||||
assert!(q_lin_res.contains("timeline"));
|
||||
|
||||
// Agent Signals
|
||||
let bcast = BroadcastAgentSignalHandler;
|
||||
let bcast_res = bcast
|
||||
let sig_handler = AgentSignalsHandler;
|
||||
let bcast_res = sig_handler
|
||||
.execute(
|
||||
serde_json::json!({
|
||||
"action": "broadcast",
|
||||
"sender": "Agent1",
|
||||
"signal_type": "info",
|
||||
"payload": "Agent starting task"
|
||||
@@ -2199,9 +2183,8 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(bcast_res.contains("Broadcasted signal"));
|
||||
|
||||
let q_sig = QueryAgentSignalsHandler;
|
||||
let q_sig_res = q_sig
|
||||
.execute(serde_json::json!({}), state.clone())
|
||||
let q_sig_res = sig_handler
|
||||
.execute(serde_json::json!({"action": "query"}), state.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(q_sig_res.contains("Agent1"));
|
||||
@@ -2464,11 +2447,12 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(preflight_res2.contains("active_tasks"));
|
||||
|
||||
// QueryHypotheses with task_id and query
|
||||
let q_hyp = QueryHypothesesHandler;
|
||||
let q_hyp_res = q_hyp
|
||||
// Hypotheses query with task_id and query
|
||||
let hyp_handler = HypothesesHandler;
|
||||
let q_hyp_res = hyp_handler
|
||||
.execute(
|
||||
serde_json::json!({
|
||||
"action": "query",
|
||||
"task_id": "t-1",
|
||||
"query": "Caching"
|
||||
}),
|
||||
@@ -2563,10 +2547,11 @@ mod tests {
|
||||
assert!(q_lin_res.contains("lineage_count"));
|
||||
|
||||
// Agent signals filtering
|
||||
let bcast_sig = BroadcastAgentSignalHandler;
|
||||
bcast_sig
|
||||
let sig_handler = AgentSignalsHandler;
|
||||
sig_handler
|
||||
.execute(
|
||||
serde_json::json!({
|
||||
"action": "broadcast",
|
||||
"sender": "AgentA",
|
||||
"signal_type": "handshake",
|
||||
"payload": "status: ready",
|
||||
@@ -2577,10 +2562,10 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let q_sig = QueryAgentSignalsHandler;
|
||||
let q_sig_res = q_sig
|
||||
let q_sig_res = sig_handler
|
||||
.execute(
|
||||
serde_json::json!({
|
||||
"action": "query",
|
||||
"sender": "AgentA",
|
||||
"signal_type": "handshake"
|
||||
}),
|
||||
|
||||
@@ -68,6 +68,19 @@ The server consolidates granular single-purpose tools into domain-named smart to
|
||||
- `action: "read"`: Read OS clipboard.
|
||||
- `action: "write"`: Write text/html/files/image to clipboard.
|
||||
|
||||
* **`hypotheses`**: Diagnostic hypothesis memory.
|
||||
- `action: "log"`: Record diagnostic hypothesis (requires `hypothesis`, optional `status: "open" | "verified" | "disproven"`, `evidence: Vec<String>`, `test_command`, `git_branch`, `repo_name`).
|
||||
- `action: "query"`: Query hypotheses (optional `status`, `git_branch`, `repo_name`).
|
||||
|
||||
* **`agent_signals`**: Inter-agent signal bus.
|
||||
- `action: "broadcast"`: Broadcast signal to other agents (requires `signal_type`, `payload`, optional `target_agent`, `ttl_seconds`).
|
||||
- `action: "query"`: Query active signals (optional `signal_type`, `include_expired: bool`).
|
||||
|
||||
* **`process_logs`**: Process and daemon log management.
|
||||
- `action: "watch"`: Register or update log file watcher (requires `label`, `file_path`, optional `description`).
|
||||
- `action: "get"`: Retrieve recent log lines from watched process log (requires `label`, optional `tail_lines: usize`).
|
||||
- `action: "clear"`: Clear or truncate watched process log file (requires `label`).
|
||||
|
||||
---
|
||||
|
||||
## 3. VCS & SVN Agnosticism & Multi-Repo Provenance
|
||||
@@ -180,16 +193,16 @@ To maintain maximum security, speed, and cross-platform reliability:
|
||||
* **Batch Vector Indexing & Similarity Score Guidance**: `VectorDB` provides `index_documents_batch` for single-request multi-point vector upserts and explicit score calibration notes ($\ge 0.75$ high confidence match).
|
||||
* **Compact JSON MCP Resources & UTF-8 Activity Truncation**: MCP resources serialize using compact JSON (`to_string`), `TerminalHistoryResource` / `MilestonesResource` enforce output bounds, and `format_tool_activity_description` uses `floor_char_boundary` for guaranteed UTF-8 safety.
|
||||
* **SIMD-Friendly Single-Pass Cosine Similarity**: `cosine_similarity` calculates dot product and Euclidean norm squares in a single linear pass over float vectors, enabling SIMD compiler auto-vectorization.
|
||||
* **Safe Stream Decoding on Log Tails**: Log tail operations (`get_recent_logs`) read raw bytes and decode using lossy UTF-8 conversion (`String::from_utf8_lossy`) to ensure resilience when seeking across multi-byte UTF-8 boundaries.
|
||||
* **Safe Stream Decoding on Log Tails**: Log tail operations (`process_logs`, action: "get") read raw bytes and decode using lossy UTF-8 conversion (`String::from_utf8_lossy`) to ensure resilience when seeking across multi-byte UTF-8 boundaries.
|
||||
* **Task Summary UTF-8 Truncation Safety**: `tasks` tool (`action = "list"`) truncates serialized task text strictly along UTF-8 character boundaries using `floor_char_boundary` when enforcing `max_tokens`.
|
||||
* **Sequential Snapshot Lock Scope Flattening**: `GenerateStandupReportHandler` reads `tasks`, `ledger`, and `session_summaries` sequentially rather than nesting read locks, preventing multi-lock deadlocks during concurrent store modifications.
|
||||
* **Directory Tree Depth Safeguard**: `ReadDirectoryArchitectureHandler` caps directory recursion at depth 10 to prevent stack overflow on deep or cyclic directory structures.
|
||||
* **Deterministic Total-Order Score Ranking**: `OmniSearchHandler` uses `f64::total_cmp` for Reciprocal Rank Fusion (RRF) score sorting, guaranteeing deterministic NaN-safe search result ordering.
|
||||
* **RPC Timeout Memory Hygiene**: `nvim-core` maintains request hygiene by removing pending request entries from static RPC maps upon timeout or channel drop, eliminating orphan memory leaks.
|
||||
* **Embedding Input Safeguard**: `generate_embedding_async` returns explicit errors for empty/0-length text inputs instead of returning empty vectors, preventing downstream vector dimension mismatches during cosine similarity calculations.
|
||||
* **Path Traversal Security Guards**: `validate_safe_path` enforces path canonicalization and rejects relative parent traversal components (`..`) across file and process log handlers (`GetRecentLogsTool`, `WatchProcessLogsTool`).
|
||||
* **Path Traversal Security Guards**: `validate_safe_path` enforces path canonicalization and rejects relative parent traversal components (`..`) across file and process log handlers (`process_logs` / `ProcessLogsTool`).
|
||||
* **Watcher Map Memory Eviction**: Proactive daemon file watcher in `watcher.rs` caps `last_processed` map size at 1,000 entries and purges entries older than 10 minutes to prevent monotonic memory leakage.
|
||||
* **Comprehensive Serde Casing Aliases**: All 8 consolidated tool action enums (TaskAction, MilestoneAction, SnippetAction, DecisionAction, TechDebtAction, EnvAction, ClipboardAction, HandoffMemoAction) include serde alias attributes supporting `snake_case`, `camelCase`, `PascalCase`, and uppercase variants for maximum LLM casing resilience.
|
||||
* **Comprehensive Serde Casing Aliases**: All 11 consolidated tool action enums (TaskAction, MilestoneAction, SnippetAction, DecisionAction, TechDebtAction, EnvAction, ClipboardAction, HandoffMemoAction, HypothesisAction, AgentSignalAction, ProcessLogAction) include serde alias attributes supporting `snake_case`, `camelCase`, `PascalCase`, and uppercase variants for maximum LLM casing resilience.
|
||||
* **Two-Phase Graph Condensation**: `condense_graph_worker` uses a 2-phase commit (non-destructive `read_with` -> graph insert -> prune by timestamp/content) to prevent data loss if summarization or graph insertion fails.
|
||||
* **Store Write Lock Minimization**: `Store::modify` and `Store::modify_async` unblock concurrent readers during JSON serialization by releasing the write lock immediately after mutating memory state.
|
||||
* **Redb Database Lock Retry Backoff**: `init_db` retries transient Redb lock contention with exponential backoff (3 attempts, 150ms delay) before falling back.
|
||||
|
||||
+36
-7
@@ -515,8 +515,7 @@ impl MemoryHandler {
|
||||
|
||||
register!(git::GetActiveWorktreeContextHandler);
|
||||
register!(git::QueryGitDiffsHandler);
|
||||
register!(logs::WatchProcessLogsHandler);
|
||||
register!(logs::GetRecentLogsHandler);
|
||||
register!(logs::ProcessLogsHandler);
|
||||
register!(ast::ReadFileSkeletonHandler);
|
||||
register!(ast::ReplaceAstNodeHandler);
|
||||
register!(ast::FindSymbolReferencesHandler);
|
||||
@@ -530,13 +529,11 @@ impl MemoryHandler {
|
||||
register!(graph::SweepGraphHealthHandler);
|
||||
register!(meta::QueryLineageHandler);
|
||||
register!(meta::GetNextActionableTasksHandler);
|
||||
register!(meta::LogHypothesisHandler);
|
||||
register!(meta::QueryHypothesesHandler);
|
||||
register!(meta::HypothesesHandler);
|
||||
register!(meta::GetPreflightContextHandler);
|
||||
register!(graph::ResolveStaleSymbolsHandler);
|
||||
register!(graph::SummarizeSubgraphHandler);
|
||||
register!(meta::BroadcastAgentSignalHandler);
|
||||
register!(meta::QueryAgentSignalsHandler);
|
||||
register!(meta::AgentSignalsHandler);
|
||||
register!(meta::AutoSessionCheckpointHandler);
|
||||
|
||||
Self {
|
||||
@@ -731,13 +728,16 @@ impl MemoryHandler {
|
||||
"log_error_fix" => "ERROR_FIX",
|
||||
"tech_debt" => "TECH_DEBT",
|
||||
"tasks" | "milestones" => "TASK",
|
||||
"handoff_memos" => "STICKY_NOTE",
|
||||
"handoff_memos" => "HANDOFF_MEMO",
|
||||
"manage_checkpoint" => "CHECKPOINT",
|
||||
"manage_subagent_namespace" => "SUBAGENT",
|
||||
"snippets" => "SNIPPET",
|
||||
"search_web" => "WEB_SEARCH",
|
||||
"omni_search" => "OMNI_SEARCH",
|
||||
"environment" => "ENVIRONMENT",
|
||||
"hypotheses" => "HYPOTHESIS",
|
||||
"agent_signals" => "AGENT_SIGNAL",
|
||||
"process_logs" => "PROCESS_LOG",
|
||||
_ => "TOOL",
|
||||
};
|
||||
|
||||
@@ -849,6 +849,35 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
|
||||
.unwrap_or_default();
|
||||
("Create Entities", names)
|
||||
}
|
||||
"hypotheses" => {
|
||||
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("manage");
|
||||
let hyp = args
|
||||
.get("hypothesis")
|
||||
.or_else(|| args.get("query"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
(
|
||||
"Hypotheses",
|
||||
format!("{}: {}", act, hyp).trim_end_matches(": ").to_string(),
|
||||
)
|
||||
}
|
||||
"agent_signals" => {
|
||||
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("signal");
|
||||
let sender = args.get("sender").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let st = args.get("signal_type").and_then(|v| v.as_str()).unwrap_or("");
|
||||
(
|
||||
"Agent Signals",
|
||||
format!("{}: {} [{}]", act, sender, st).trim_end_matches(": ").to_string(),
|
||||
)
|
||||
}
|
||||
"process_logs" => {
|
||||
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("logs");
|
||||
let file = args.get("file_path").and_then(|v| v.as_str()).unwrap_or("");
|
||||
(
|
||||
"Process Logs",
|
||||
format!("{}: {}", act, file).trim_end_matches(": ").to_string(),
|
||||
)
|
||||
}
|
||||
_ => return name.to_string(),
|
||||
};
|
||||
if detail.is_empty() {
|
||||
|
||||
+46
-37
@@ -338,25 +338,27 @@ pub struct GetNextActionableTasksTool {
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
/// Log a structured diagnostic hypothesis, tested evidence, and status during problem solving.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HypothesisAction {
|
||||
Log,
|
||||
Query,
|
||||
}
|
||||
|
||||
/// Manage diagnostic hypotheses, tested evidence, and status during problem solving.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct LogHypothesisTool {
|
||||
/// Optional task ID associated with this hypothesis.
|
||||
pub struct HypothesesTool {
|
||||
/// Action to perform: 'log' or 'query'.
|
||||
pub action: HypothesisAction,
|
||||
/// Optional task ID associated with this hypothesis or to filter hypotheses.
|
||||
pub task_id: Option<String>,
|
||||
/// The diagnostic hypothesis or potential root cause.
|
||||
pub hypothesis: String,
|
||||
/// The diagnostic hypothesis or potential root cause (required for action 'log').
|
||||
pub hypothesis: Option<String>,
|
||||
/// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'.
|
||||
pub status: Option<String>,
|
||||
/// Evidence or test results supporting or disproving the hypothesis.
|
||||
pub evidence: Option<String>,
|
||||
}
|
||||
|
||||
/// Query previously logged diagnostic hypotheses and test results.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct QueryHypothesesTool {
|
||||
/// Optional task ID to filter hypotheses for.
|
||||
pub task_id: Option<String>,
|
||||
/// Optional search query text.
|
||||
/// Optional search query text (for action 'query').
|
||||
pub query: Option<String>,
|
||||
}
|
||||
#[cfg(test)]
|
||||
@@ -397,15 +399,23 @@ mod tests {
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct GetActiveWorktreeContextTool {}
|
||||
/// Tail a specific log file in the background so it can be queried later.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct WatchProcessLogsTool {
|
||||
pub file_path: String,
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProcessLogAction {
|
||||
Watch,
|
||||
Get,
|
||||
Clear,
|
||||
}
|
||||
|
||||
/// Get the recent logs from a watched file.
|
||||
/// Monitor, tail, and manage process logs: watch a log file, tail recent output, or clear log files.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct GetRecentLogsTool {
|
||||
pub struct ProcessLogsTool {
|
||||
/// Action to perform: 'watch', 'get', or 'clear'.
|
||||
pub action: ProcessLogAction,
|
||||
/// Path to the log file.
|
||||
pub file_path: String,
|
||||
/// Maximum number of lines to return for action 'get'. Defaults to 100.
|
||||
pub max_lines: Option<usize>,
|
||||
}
|
||||
|
||||
/// Read a file and return only its AST skeleton (Imports, Structs, Enums, Traits, Functions)
|
||||
@@ -537,30 +547,29 @@ pub struct SummarizeSubgraphTool {
|
||||
pub max_tokens: Option<usize>,
|
||||
}
|
||||
|
||||
/// Broadcast a real-time signal or status transition to other subagents on the inter-agent signal bus.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AgentSignalAction {
|
||||
Broadcast,
|
||||
Query,
|
||||
}
|
||||
|
||||
/// Real-time inter-agent communication bus: broadcast signals or query active signals from peer subagents.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct BroadcastAgentSignalTool {
|
||||
/// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian').
|
||||
pub sender: String,
|
||||
/// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED').
|
||||
pub signal_type: String,
|
||||
/// JSON or text payload containing event details or artifact URIs.
|
||||
pub payload: String,
|
||||
pub struct AgentSignalsTool {
|
||||
/// Action to perform: 'broadcast' or 'query'.
|
||||
pub action: AgentSignalAction,
|
||||
/// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian'). Required for broadcast; optional filter for query.
|
||||
pub sender: Option<String>,
|
||||
/// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED'). Required for broadcast; optional filter for query.
|
||||
pub signal_type: Option<String>,
|
||||
/// JSON or text payload containing event details or artifact URIs (required for action 'broadcast').
|
||||
pub payload: Option<String>,
|
||||
/// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour).
|
||||
pub ttl_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
/// Query active inter-agent signals published by subagents on the inter-agent signal bus.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct QueryAgentSignalsTool {
|
||||
/// Optional sender filter (e.g. 'PrePushAuditor').
|
||||
pub sender: Option<String>,
|
||||
/// Optional signal_type filter (e.g. 'AUDIT_PASSED').
|
||||
pub signal_type: Option<String>,
|
||||
/// Optional limit on returned signals. Defaults to 20.
|
||||
/// Optional limit on returned signals (for action 'query'). Defaults to 20.
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
/// Trigger an automated context checkpoint, summarizing active tasks, hypotheses, recent commits, and open tech debt into a permanent HandoffMemo.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct AutoSessionCheckpointTool {
|
||||
|
||||
Reference in new issue
Block a user