use super::records::{HookDiagnostic, HookPhase};
use crate::{
config::HookFailurePolicy,
output::{
ActivityEvent, ActivityId, ActivityKind, ActivityMetadata, ActivitySender, ActivityStatus,
redact_sensitive_text,
},
providers::ToolCall,
};
#[derive(Clone)]
pub(super) struct HookActivityTarget {
pub(super) id: ActivityId,
pub(super) parent_id: Option<ActivityId>,
pub(super) sender: ActivitySender,
pub(super) policy: HookFailurePolicy,
pub(super) label: String,
}
pub(super) fn hook_activity_id(
parent_id: Option<&ActivityId>,
phase: HookPhase,
call: &ToolCall,
index: usize,
) -> ActivityId {
let phase = phase.as_str();
if let Some(parent_id) = parent_id {
return ActivityId::new(format!("{}/{phase}-hook-{index}", parent_id.as_str()));
}
let call_key = if call.id.trim().is_empty() {
call.name.as_str()
} else {
call.id.as_str()
};
ActivityId::new(format!("hook/{call_key}/{phase}/{index}"))
}
pub(super) fn emit_hook_activity_started(
activity: &Option<HookActivityTarget>,
phase: HookPhase,
call: &ToolCall,
) {
if let Some(activity) = activity {
(activity.sender)(ActivityEvent::Started {
id: activity.id.clone(),
parent_id: activity.parent_id.clone(),
kind: ActivityKind::Hook,
status: ActivityStatus::Running,
metadata: hook_activity_metadata(
phase,
call,
&activity.label,
ActivityStatus::Running,
None,
activity.policy,
),
});
}
}
pub(super) fn emit_hook_activity_success(
activity: &Option<HookActivityTarget>,
phase: HookPhase,
call: &ToolCall,
) {
if let Some(activity) = activity {
(activity.sender)(ActivityEvent::Finished {
id: activity.id.clone(),
status: ActivityStatus::Success,
metadata: Some(hook_activity_metadata(
phase,
call,
&activity.label,
ActivityStatus::Success,
None,
activity.policy,
)),
});
}
}
pub(super) fn emit_hook_activity_failed(
activity: &Option<HookActivityTarget>,
phase: HookPhase,
call: &ToolCall,
diagnostic: &HookDiagnostic,
) {
if let Some(activity) = activity {
(activity.sender)(ActivityEvent::Finished {
id: activity.id.clone(),
status: ActivityStatus::Failed,
metadata: Some(hook_activity_metadata(
phase,
call,
&activity.label,
ActivityStatus::Failed,
Some(diagnostic.category.as_str()),
diagnostic.policy,
)),
});
}
}
pub(super) fn hook_activity_metadata(
phase: HookPhase,
call: &ToolCall,
label: &str,
status: ActivityStatus,
category: Option<&str>,
policy: HookFailurePolicy,
) -> ActivityMetadata {
let label = redact_sensitive_text(label);
let tool_name = redact_sensitive_text(&call.name);
let status_text = match status {
ActivityStatus::Queued => "queued",
ActivityStatus::Running => "running",
ActivityStatus::Writing => "writing",
ActivityStatus::Success => "success",
ActivityStatus::Failed => "failed",
ActivityStatus::Canceled => "canceled",
};
let mut fields = vec![
("phase".to_string(), phase.as_str().to_string()),
("hook_label".to_string(), label.clone()),
("target_tool".to_string(), tool_name.clone()),
("status".to_string(), status_text.to_string()),
("policy".to_string(), policy.as_str().to_string()),
];
if let Some(category) = category {
fields.push(("category".to_string(), category.to_string()));
}
let mut detail = format!(
"{} hook '{}' for tool '{}' {}",
phase.as_str(),
label,
tool_name,
status_text
);
if let Some(category) = category {
detail.push_str(&format!(" category={category} policy={}", policy.as_str()));
}
ActivityMetadata {
label: format!("{} hook {label} on {tool_name}", phase.as_str()),
detail: Some(redact_sensitive_text(&detail)),
fields,
}
}