refactor: consolidate hypotheses, agent_signals, and process_logs into action-based smart tools

This commit is contained in:
Riz Ashraf committed 2026-10-07 12:00:20 +01:00
1 parent 79209da711
commit 3b146f91c2
17 files changed
+438 -359

No files matched your search

+8 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}),