use agent_sdk_foundation::audit::ToolAuditRecord;
use async_trait::async_trait;
use std::sync::Arc;
#[async_trait]
pub trait ToolAuditSink: Send + Sync + 'static {
async fn record(&self, record: ToolAuditRecord);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NoopAuditSink;
#[async_trait]
impl ToolAuditSink for NoopAuditSink {
async fn record(&self, _record: ToolAuditRecord) {
}
}
#[async_trait]
impl<S: ToolAuditSink + ?Sized> ToolAuditSink for Arc<S> {
async fn record(&self, record: ToolAuditRecord) {
(**self).record(record).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use agent_sdk_foundation::audit::{AuditProvenance, ToolAuditOutcome, ToolAuditRecord};
use agent_sdk_foundation::types::{ToolResult, ToolTier};
use tokio::sync::Mutex;
#[derive(Default)]
pub struct VecSink {
pub records: Mutex<Vec<ToolAuditRecord>>,
}
#[async_trait]
impl ToolAuditSink for VecSink {
async fn record(&self, record: ToolAuditRecord) {
self.records.lock().await.push(record);
}
}
fn sample_record(outcome: ToolAuditOutcome) -> ToolAuditRecord {
ToolAuditRecord::new(agent_sdk_foundation::audit::ToolAuditRecordParams {
tool_call_id: "call_x".into(),
tool_name: "tool_x".into(),
display_name: "Tool X".into(),
tier: ToolTier::Observe,
requested_input: serde_json::json!({}),
effective_input: serde_json::json!({}),
turn: 1,
provenance: AuditProvenance::new("anthropic", "claude-sonnet-4-5-20250929"),
outcome,
})
}
#[tokio::test]
async fn noop_sink_accepts_all_variants_without_panicking() {
let sink = NoopAuditSink;
sink.record(sample_record(ToolAuditOutcome::Blocked {
reason: "nope".into(),
}))
.await;
sink.record(sample_record(ToolAuditOutcome::RequiresConfirmation {
description: "ok?".into(),
listen_context: None,
}))
.await;
sink.record(sample_record(ToolAuditOutcome::Completed {
result: ToolResult::success("ok"),
}))
.await;
}
#[tokio::test]
async fn arc_wrapped_sink_forwards_records() {
let inner = Arc::new(VecSink::default());
let sink: Arc<dyn ToolAuditSink> = inner.clone();
sink.record(sample_record(ToolAuditOutcome::Cached {
result: ToolResult::success("cached"),
}))
.await;
let kinds: Vec<&'static str> = inner
.records
.lock()
.await
.iter()
.map(ToolAuditRecord::outcome_kind)
.collect();
assert_eq!(kinds, vec!["cached"]);
}
#[tokio::test]
async fn sink_captures_every_lifecycle_variant() {
let sink = Arc::new(VecSink::default());
for outcome in [
ToolAuditOutcome::Blocked { reason: "r".into() },
ToolAuditOutcome::RequiresConfirmation {
description: "d".into(),
listen_context: None,
},
ToolAuditOutcome::Cached {
result: ToolResult::success("c"),
},
ToolAuditOutcome::Replayed {
result: ToolResult::success("r"),
},
ToolAuditOutcome::Invalidated { reason: "i".into() },
ToolAuditOutcome::Completed {
result: ToolResult::success("ok"),
},
ToolAuditOutcome::PersistenceFailed {
result: None,
error: "boom".into(),
},
] {
sink.record(sample_record(outcome)).await;
}
let kinds: Vec<&'static str> = sink
.records
.lock()
.await
.iter()
.map(ToolAuditRecord::outcome_kind)
.collect();
assert_eq!(
kinds,
vec![
"blocked",
"requires_confirmation",
"cached",
"replayed",
"invalidated",
"completed",
"persistence_failed",
],
);
}
}