use std::sync::{Arc, Mutex};
use agent_contracts::{LoopEventSink, ToolEventSink};
use agent_types::common::ids::AgentId;
use agent_types::events::{ToolLifecycleEvent, ToolResultEvent};
use super::FileChangeDelta;
pub trait SessionDiffForwarder: Send + Sync {
fn forward_delta(&self, call_id: &str, delta: FileChangeDelta);
}
pub struct DiffComputingLoopSink {
inner: Arc<dyn LoopEventSink>,
tracker: Arc<Mutex<super::SessionDiffTracker>>,
forwarder: Arc<dyn SessionDiffForwarder>,
}
impl DiffComputingLoopSink {
pub fn new(
inner: Arc<dyn LoopEventSink>,
tracker: Arc<Mutex<super::SessionDiffTracker>>,
forwarder: Arc<dyn SessionDiffForwarder>,
) -> Self {
Self {
inner,
tracker,
forwarder,
}
}
fn compute_delta(&self, event: &ToolResultEvent) -> Option<FileChangeDelta> {
let mut tracker = match self.tracker.lock() {
Ok(guard) => guard,
Err(poisoned) => {
tracing::error!(
target: "session_diff",
call_id = %event.call_id,
tool = %event.tool_name,
is_error = event.is_error,
"SessionDiffTracker mutex poisoned; recovering and skipping delta",
);
poisoned.into_inner()
}
};
if event.is_error {
tracker.on_tool_failed(&event.call_id, None)
} else {
tracker.on_tool_completed(&event.call_id, &event.tool_name, &event.args_preview, None)
}
}
}
impl LoopEventSink for DiffComputingLoopSink {
fn on_turn_start(&self, agent_id: &AgentId, turn: u32) {
self.inner.on_turn_start(agent_id, turn);
}
fn on_assistant_message(&self, agent_id: &AgentId, text: &str) {
self.inner.on_assistant_message(agent_id, text);
}
fn on_assistant_reasoning(&self, agent_id: &AgentId, text: &str) {
self.inner.on_assistant_reasoning(agent_id, text);
}
fn on_tool_result(&self, agent_id: &AgentId, event: &ToolResultEvent) {
if let Some(delta) = self.compute_delta(event) {
self.forwarder.forward_delta(&event.call_id, delta);
}
self.inner.on_tool_result(agent_id, event);
}
fn on_loop_end(&self, agent_id: &AgentId, summary: &agent_types::events::LoopEndSummary) {
{
let mut tracker = match self.tracker.lock() {
Ok(guard) => guard,
Err(poisoned) => {
tracing::error!(
target: "session_diff",
"SessionDiffTracker mutex poisoned during on_loop_end; \
recovering and clearing per-turn state",
);
poisoned.into_inner()
}
};
tracker.clear_per_turn_state();
}
self.inner.on_loop_end(agent_id, summary);
}
}
pub struct DiffComputingToolSink {
tracker: Arc<Mutex<super::SessionDiffTracker>>,
}
impl DiffComputingToolSink {
pub fn new(tracker: Arc<Mutex<super::SessionDiffTracker>>) -> Self {
Self { tracker }
}
}
impl ToolEventSink for DiffComputingToolSink {
fn emit(&self, event: ToolLifecycleEvent) {
let inner = match event {
ToolLifecycleEvent::AgentScoped { event, .. } => *event,
other => other,
};
if let ToolLifecycleEvent::Running {
call_id,
tool_name,
args_preview,
} = inner
{
let mut tracker = match self.tracker.lock() {
Ok(guard) => guard,
Err(poisoned) => {
tracing::error!(
target: "session_diff",
call_id = %call_id,
tool = %tool_name,
"SessionDiffTracker mutex poisoned during on_tool_running; \
recovering and skipping baseline capture",
);
poisoned.into_inner()
}
};
tracker.on_tool_running(&call_id, &tool_name, &args_preview);
}
}
}