use serde::{Deserialize, Serialize};
use agent_sdk_core::{
AgentError, EntityKind, EntityRef, PolicyRef, PrivacyClass, RetentionClass, RunTrace,
SessionTimeline, TurnTrace,
};
use crate::EvaluationScope;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceRole {
Input,
Context,
Tool,
Model,
Output,
Effect,
Policy,
ExpectedOutcome,
Baseline,
Other,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct EvidenceItem {
pub evidence_ref: EntityRef,
pub role: EvidenceRole,
pub redacted_summary: String,
pub privacy_class: PrivacyClass,
pub retention_class: RetentionClass,
pub derived_from: Vec<EntityRef>,
}
impl EvidenceItem {
pub fn new(
evidence_ref: EntityRef,
role: EvidenceRole,
redacted_summary: impl Into<String>,
) -> Self {
Self {
evidence_ref,
role,
redacted_summary: redacted_summary.into(),
privacy_class: PrivacyClass::ContentRefsOnly,
retention_class: RetentionClass::RunScoped,
derived_from: Vec::new(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SupportRefValidation {
pub accepted_refs: Vec<EntityRef>,
pub rejected_refs: Vec<EntityRef>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct EvidenceBundle {
pub scope: EvaluationScope,
pub items: Vec<EvidenceItem>,
pub outcome_ref: Option<EntityRef>,
pub redacted_summary: String,
pub policy_refs: Vec<PolicyRef>,
pub privacy_class: PrivacyClass,
pub retention_class: RetentionClass,
}
impl EvidenceBundle {
pub fn new(scope: EvaluationScope, redacted_summary: impl Into<String>) -> Self {
Self {
scope,
items: Vec::new(),
outcome_ref: None,
redacted_summary: redacted_summary.into(),
policy_refs: Vec::new(),
privacy_class: PrivacyClass::ContentRefsOnly,
retention_class: RetentionClass::RunScoped,
}
}
pub fn from_turn_trace(trace: &TurnTrace) -> Result<Self, AgentError> {
let turn_id = trace.turn_id.clone().ok_or_else(|| {
AgentError::contract_violation("turn trace is missing turn id for evaluation")
})?;
let mut bundle = Self::new(
EvaluationScope::Turn {
session_id: trace.session_id.clone(),
turn_id: turn_id.clone(),
},
"turn trace evidence",
);
bundle.outcome_ref = trace.run_ids.first().cloned().map(EntityRef::run);
bundle.push(EvidenceItem::new(
EntityRef::new(EntityKind::Turn, turn_id),
EvidenceRole::Input,
"turn envelope",
));
for run_id in &trace.run_ids {
bundle.push(EvidenceItem::new(
EntityRef::run(run_id.clone()),
EvidenceRole::Output,
"run associated with turn",
));
}
for attempt_id in &trace.attempt_ids {
bundle.push(EvidenceItem::new(
EntityRef::new(EntityKind::Attempt, attempt_id.clone()),
EvidenceRole::Model,
"model attempt",
));
}
for message_id in &trace.message_ids {
bundle.push(EvidenceItem::new(
EntityRef::message(message_id.clone()),
EvidenceRole::Input,
"message envelope",
));
}
for projection_id in &trace.context_projection_ids {
bundle.push(EvidenceItem::new(
EntityRef::new(EntityKind::ContextProjection, projection_id.clone()),
EvidenceRole::Context,
"context projection",
));
}
for effect_id in &trace.effect_ids {
bundle.push(EvidenceItem::new(
EntityRef::new(EntityKind::Effect, effect_id.clone()),
EvidenceRole::Effect,
"effect evidence",
));
}
for tool_call_id in &trace.tool_call_ids {
bundle.push(EvidenceItem::new(
EntityRef::new(EntityKind::ToolCall, tool_call_id.clone()),
EvidenceRole::Tool,
"tool call evidence",
));
}
Ok(bundle)
}
pub fn from_run_trace(trace: &RunTrace) -> Result<Self, AgentError> {
let run_id = trace.run_id.clone().ok_or_else(|| {
AgentError::contract_violation("run trace is missing run id for evaluation")
})?;
let mut bundle = Self::new(
EvaluationScope::Run {
run_id: run_id.clone(),
},
"run trace evidence",
);
bundle.outcome_ref = Some(EntityRef::run(run_id.clone()));
bundle.push(EvidenceItem::new(
EntityRef::run(run_id),
EvidenceRole::Output,
"run envelope",
));
for turn in &trace.turn_traces {
let turn_bundle = Self::from_turn_trace(turn)?;
for item in turn_bundle.items {
bundle.push(item);
}
}
Ok(bundle)
}
pub fn from_session_timeline(timeline: &SessionTimeline) -> Result<Self, AgentError> {
let mut bundle = Self::new(
EvaluationScope::Session {
session_id: timeline.session_id.clone(),
},
"session timeline evidence",
);
for turn in &timeline.turns {
let turn_bundle = Self::from_turn_trace(turn)?;
if bundle.outcome_ref.is_none() {
bundle.outcome_ref = turn_bundle.outcome_ref.clone();
}
for item in turn_bundle.items {
bundle.push(item);
}
}
Ok(bundle)
}
pub fn with_item(mut self, item: EvidenceItem) -> Self {
self.push(item);
self
}
pub fn validate_support_refs(
&self,
support_refs: impl IntoIterator<Item = EntityRef>,
max_support_refs: usize,
) -> SupportRefValidation {
let mut accepted_refs = Vec::new();
let mut rejected_refs = Vec::new();
for cited_ref in support_refs.into_iter().take(max_support_refs) {
if let Some(available_ref) = self
.items
.iter()
.map(|item| &item.evidence_ref)
.find(|available_ref| same_entity_ref(available_ref, &cited_ref))
{
push_unique(&mut accepted_refs, available_ref.clone());
} else {
push_unique(&mut rejected_refs, cited_ref);
}
}
SupportRefValidation {
accepted_refs,
rejected_refs,
}
}
fn push(&mut self, item: EvidenceItem) {
if !self
.items
.iter()
.any(|existing| same_entity_ref(&existing.evidence_ref, &item.evidence_ref))
{
self.items.push(item);
}
}
}
fn push_unique(items: &mut Vec<EntityRef>, value: EntityRef) {
if !items
.iter()
.any(|existing| same_entity_ref(existing, &value))
{
items.push(value);
}
}
pub(crate) fn same_entity_ref(left: &EntityRef, right: &EntityRef) -> bool {
left.kind == right.kind && left.id.as_str() == right.id.as_str()
}