pub mod file;
pub mod memory;
pub use echo_core::audit::*;
use futures::future::BoxFuture;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
struct ToolCallInfo {
args: Value,
started_at: std::time::Instant,
}
pub struct AuditCallback {
logger: Arc<dyn AuditLogger>,
agent_name: String,
session_id: Option<String>,
tool_calls: Mutex<HashMap<String, ToolCallInfo>>,
tool_seq: Mutex<HashMap<String, u64>>,
}
impl AuditCallback {
pub fn new(
logger: Arc<dyn AuditLogger>,
agent_name: impl Into<String>,
session_id: Option<String>,
) -> Self {
Self {
logger,
agent_name: agent_name.into(),
session_id,
tool_calls: Mutex::new(HashMap::new()),
tool_seq: Mutex::new(HashMap::new()),
}
}
fn make_tool_call_id(&self, tool: &str) -> String {
let mut seq = self.tool_seq.lock().unwrap_or_else(|e| e.into_inner());
let n = seq.entry(tool.to_string()).or_insert(0);
*n += 1;
format!("{}#{}", tool, n)
}
fn make_event(&self, event_type: AuditEventType) -> AuditEvent {
AuditEvent::now(self.session_id.clone(), self.agent_name.clone(), event_type)
}
}
impl echo_core::agent::AgentCallback for AuditCallback {
fn on_tool_start<'a>(
&'a self,
_agent: &'a str,
tool: &'a str,
args: &'a Value,
) -> BoxFuture<'a, ()> {
Box::pin(async move {
let tool_call_id = self.make_tool_call_id(tool);
if let Ok(mut calls) = self.tool_calls.lock() {
calls.insert(
tool_call_id,
ToolCallInfo {
args: args.clone(),
started_at: std::time::Instant::now(),
},
);
}
})
}
fn on_tool_end<'a>(
&'a self,
_agent: &'a str,
tool: &'a str,
result: &'a str,
) -> BoxFuture<'a, ()> {
Box::pin(async move {
let (duration_ms, input) = self
.tool_calls
.lock()
.ok()
.and_then(|mut m| {
let key = m
.keys()
.find(|k| k.starts_with(&format!("{}#", tool)))
.cloned();
key.and_then(|k| m.remove(&k))
})
.map(|info| (info.started_at.elapsed().as_millis() as u64, info.args))
.unwrap_or((0, Value::Null));
let event = self.make_event(AuditEventType::ToolCall {
tool: tool.to_string(),
input,
output: result.to_string(),
success: true,
duration_ms,
});
let _ = self.logger.log(event).await;
})
}
fn on_tool_error<'a>(
&'a self,
_agent: &'a str,
tool: &'a str,
err: &'a echo_core::error::ReactError,
) -> BoxFuture<'a, ()> {
Box::pin(async move {
let (duration_ms, input) = self
.tool_calls
.lock()
.ok()
.and_then(|mut m| {
let key = m
.keys()
.find(|k| k.starts_with(&format!("{}#", tool)))
.cloned();
key.and_then(|k| m.remove(&k))
})
.map(|info| (info.started_at.elapsed().as_millis() as u64, info.args))
.unwrap_or((0, Value::Null));
let event = self.make_event(AuditEventType::ToolCall {
tool: tool.to_string(),
input,
output: err.to_string(),
success: false,
duration_ms,
});
let _ = self.logger.log(event).await;
})
}
fn on_final_answer<'a>(&'a self, _agent: &'a str, answer: &'a str) -> BoxFuture<'a, ()> {
Box::pin(async move {
let event = self.make_event(AuditEventType::FinalAnswer {
content: answer.to_string(),
});
let _ = self.logger.log(event).await;
})
}
}