use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use super::TaskStatus;
use crate::traits::MessageAnnotation;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionStartData {
pub channel: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionEndData {
pub reason: SessionEndReason,
pub duration_secs: u64,
pub event_count: u32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionEndReason {
UserEnded,
Timeout,
Shutdown,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserMessageData {
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_id: Option<String>,
#[serde(default)]
pub has_attachments: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub annotations: Vec<MessageAnnotation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_role: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantResponseData {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallInfo>>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub annotations: Vec<MessageAnnotation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallInfo {
pub id: String,
pub name: String,
pub arguments: JsonValue,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extra_content: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallData {
pub tool_call_id: String,
pub name: String,
pub arguments: JsonValue,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy_rev: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub risk_score: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResultData {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message_id: Option<String>,
pub tool_call_id: String,
pub name: String,
pub result: String,
pub success: bool,
pub duration_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub annotations: Vec<MessageAnnotation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThinkingStartData {
pub iteration: u32,
pub task_id: String,
#[serde(default)]
pub total_tool_calls: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyDecisionData {
pub task_id: String,
pub old_model: String,
pub new_model: String,
pub old_tier: String,
pub new_profile: String,
pub diverged: bool,
pub policy_enforce: bool,
pub risk_score: f32,
pub uncertainty_score: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyMetricsData {
pub tool_exposure_samples: u64,
pub tool_exposure_before_sum: u64,
pub tool_exposure_after_sum: u64,
pub tool_schema_contract_rejections_total: u64,
pub ambiguity_detected_total: u64,
pub uncertainty_clarify_total: u64,
pub context_refresh_total: u64,
pub escalation_total: u64,
pub fallback_expansion_total: u64,
pub response_direct_return_total: u64,
pub response_fallthrough_total: u64,
pub orchestration_route_clarification_required_total: u64,
pub orchestration_route_tools_required_total: u64,
pub orchestration_route_short_correction_direct_reply_total: u64,
pub orchestration_route_acknowledgment_direct_reply_total: u64,
pub orchestration_route_default_continue_total: u64,
pub context_bleed_prevented_total: u64,
pub context_mismatch_preflight_drop_total: u64,
pub followup_mode_overrides_total: u64,
pub cross_scope_blocked_total: u64,
pub route_drift_alert_total: u64,
pub route_drift_failsafe_activation_total: u64,
pub route_failsafe_active_turn_total: u64,
pub tokens_failed_tasks_total: u64,
pub est_input_token_samples: u64,
pub est_input_tokens_total: u64,
pub est_msg_tokens_total: u64,
pub est_tool_tokens_total: u64,
pub est_tool_tokens_high_share_total: u64,
pub est_tool_tokens_high_abs_total: u64,
pub no_progress_iterations_total: u64,
pub deferred_no_tool_forced_required_total: u64,
pub deferred_no_tool_deferral_detected_total: u64,
pub deferred_no_tool_model_switch_total: u64,
pub deferred_no_tool_error_marker_total: u64,
pub llm_payload_invalid_total: u64,
pub llm_payload_invalid_breakdown: Vec<LlmPayloadInvalidMetric>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmPayloadInvalidMetric {
pub provider: String,
pub model: String,
pub reason: String,
pub count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionPointData {
pub decision_type: DecisionType,
pub task_id: String,
pub iteration: u32,
#[serde(default)]
pub severity: DiagnosticSeverity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
pub metadata: JsonValue,
pub summary: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticSeverity {
#[default]
Info,
Warning,
Error,
}
impl DiagnosticSeverity {
pub fn as_str(self) -> &'static str {
match self {
DiagnosticSeverity::Info => "info",
DiagnosticSeverity::Warning => "warning",
DiagnosticSeverity::Error => "error",
}
}
pub fn is_warning_or_higher(self) -> bool {
matches!(
self,
DiagnosticSeverity::Warning | DiagnosticSeverity::Error
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionType {
SkillMatch,
MemoryRetrieval,
IntentGate,
ExecutionPlanningGate,
ExecutionCritiquePass,
ExecutionBudgetSelection,
ExecutionStateSnapshot,
EvidenceGate,
ExecutionFailureClassification,
PostExecutionValidation,
RepetitiveCallDetection,
ConsecutiveSameToolDetection,
AlternatingPatternDetection,
ToolBudgetBlock,
RouteDriftAlert,
StoppingCondition,
InstructionsSnapshot,
BudgetAutoExtension,
}
impl DecisionType {
pub fn as_str(self) -> &'static str {
match self {
DecisionType::SkillMatch => "skill_match",
DecisionType::MemoryRetrieval => "memory_retrieval",
DecisionType::IntentGate => "intent_gate",
DecisionType::ExecutionPlanningGate => "execution_planning_gate",
DecisionType::ExecutionCritiquePass => "execution_critique_pass",
DecisionType::ExecutionBudgetSelection => "execution_budget_selection",
DecisionType::ExecutionStateSnapshot => "execution_state_snapshot",
DecisionType::EvidenceGate => "evidence_gate",
DecisionType::ExecutionFailureClassification => "execution_failure_classification",
DecisionType::PostExecutionValidation => "post_execution_validation",
DecisionType::RepetitiveCallDetection => "repetitive_call_detection",
DecisionType::ConsecutiveSameToolDetection => "consecutive_same_tool_detection",
DecisionType::AlternatingPatternDetection => "alternating_pattern_detection",
DecisionType::ToolBudgetBlock => "tool_budget_block",
DecisionType::RouteDriftAlert => "route_drift_alert",
DecisionType::StoppingCondition => "stopping_condition",
DecisionType::InstructionsSnapshot => "instructions_snapshot",
DecisionType::BudgetAutoExtension => "budget_auto_extension",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FailureCategory {
BadAssumption,
MissingContext,
ToolFailure,
SandboxPermissionBlock,
InvalidEditPatch,
DependencyRuntimeMismatch,
PartialCompletionRegression,
AgentLoop,
ProviderError,
IncorrectResult,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceRef {
pub event_id: i64,
pub event_type: String,
pub timestamp: String,
pub summary: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RootCauseCandidate {
pub category: FailureCategory,
pub confidence: f32,
pub description: String,
pub evidence: Vec<EvidenceRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub why_previous_step_looked_valid: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskStartData {
pub task_id: String,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_task_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskEndData {
pub task_id: String,
pub status: TaskStatus,
pub duration_secs: u64,
pub iterations: u32,
pub tool_calls_count: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorData {
pub message: String,
pub error_type: ErrorType,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(default)]
pub recovered: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorType {
ToolError,
LlmError,
Timeout,
RateLimit,
PermissionDenied,
Internal,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubAgentSpawnData {
pub child_session_id: String,
pub mission: String,
pub task: String,
pub depth: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_task_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubAgentCompleteData {
pub child_session_id: String,
pub success: bool,
pub result_summary: String,
pub duration_secs: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_task_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalRequestedData {
pub command: String,
pub risk_level: String,
#[serde(default)]
pub warnings: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalGrantedData {
pub command: String,
pub approval_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalDeniedData {
pub command: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
}
impl ToolCallData {
pub fn from_tool_call(
tool_call_id: impl Into<String>,
name: impl Into<String>,
arguments: JsonValue,
task_id: Option<String>,
) -> Self {
let name = name.into();
let summary = Self::generate_summary(&name, &arguments);
Self {
tool_call_id: tool_call_id.into(),
name,
arguments,
summary: Some(summary),
task_id,
idempotency_key: None,
policy_rev: None,
risk_score: None,
}
}
pub fn with_policy_metadata(
mut self,
idempotency_key: Option<String>,
policy_rev: Option<u32>,
risk_score: Option<f32>,
) -> Self {
self.idempotency_key = idempotency_key;
self.policy_rev = policy_rev;
self.risk_score = risk_score;
self
}
fn generate_summary(name: &str, arguments: &JsonValue) -> String {
use crate::utils::truncate_str;
match name {
"terminal" => {
if let Some(cmd) = arguments.get("command").and_then(|v| v.as_str()) {
format!("`{}`", truncate_str(cmd, 50))
} else {
"terminal command".to_string()
}
}
"web_search" => {
if let Some(query) = arguments.get("query").and_then(|v| v.as_str()) {
format!("\"{}\"", query)
} else {
"web search".to_string()
}
}
"web_fetch" => {
if let Some(url) = arguments.get("url").and_then(|v| v.as_str()) {
truncate_str(url, 40)
} else {
"fetch URL".to_string()
}
}
_ => {
if let Some(obj) = arguments.as_object() {
if let Some((_, first_val)) = obj.iter().next() {
if let Some(s) = first_val.as_str() {
return truncate_str(s, 30);
}
}
}
name.to_string()
}
}
}
}
impl ErrorData {
pub fn tool_error(
tool_name: impl Into<String>,
message: impl Into<String>,
task_id: Option<String>,
) -> Self {
Self {
message: message.into(),
error_type: ErrorType::ToolError,
context: None,
recovered: false,
task_id,
tool_name: Some(tool_name.into()),
}
}
pub fn llm_error(message: impl Into<String>, task_id: Option<String>) -> Self {
Self {
message: message.into(),
error_type: ErrorType::LlmError,
context: None,
recovered: false,
task_id,
tool_name: None,
}
}
pub fn with_recovered(mut self) -> Self {
self.recovered = true;
self
}
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context = Some(context.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn tool_call_data_defaults_policy_metadata_to_none() {
let data: ToolCallData = serde_json::from_value(json!({
"tool_call_id": "c1",
"name": "read_file",
"arguments": {"path":"README.md"}
}))
.expect("deserialize ToolCallData");
assert!(data.idempotency_key.is_none());
assert!(data.policy_rev.is_none());
assert!(data.risk_score.is_none());
}
#[test]
fn tool_call_data_with_policy_metadata_sets_optional_fields() {
let data = ToolCallData::from_tool_call(
"c1",
"write_file",
json!({"path":"notes.txt"}),
Some("task-1".to_string()),
)
.with_policy_metadata(Some("idem-task-1-c1".to_string()), Some(3), Some(0.72));
assert_eq!(data.idempotency_key.as_deref(), Some("idem-task-1-c1"));
assert_eq!(data.policy_rev, Some(3));
assert_eq!(data.risk_score, Some(0.72));
}
#[test]
fn decision_point_data_serde_roundtrip() {
let data = DecisionPointData {
decision_type: DecisionType::IntentGate,
task_id: "task-123".to_string(),
iteration: 1,
severity: DiagnosticSeverity::Warning,
code: Some("intent_gate".to_string()),
metadata: json!({"needs_tools": true, "can_answer_now": false}),
summary: "Intent gate requested tool mode".to_string(),
};
let serialized = serde_json::to_string(&data).expect("serialize");
let parsed: DecisionPointData = serde_json::from_str(&serialized).expect("deserialize");
assert_eq!(parsed.task_id, "task-123");
assert_eq!(parsed.iteration, 1);
assert_eq!(parsed.decision_type, DecisionType::IntentGate);
assert_eq!(parsed.severity, DiagnosticSeverity::Warning);
assert_eq!(parsed.code.as_deref(), Some("intent_gate"));
}
#[test]
fn decision_point_data_defaults_new_fields_for_older_rows() {
let data: DecisionPointData = serde_json::from_value(json!({
"decision_type": "intent_gate",
"task_id": "task-123",
"iteration": 1,
"metadata": {"needs_tools": true},
"summary": "Intent gate requested tool mode"
}))
.expect("deserialize DecisionPointData");
assert_eq!(data.severity, DiagnosticSeverity::Info);
assert!(data.code.is_none());
}
#[test]
fn failure_category_serde_roundtrip() {
let cat = FailureCategory::SandboxPermissionBlock;
let serialized = serde_json::to_string(&cat).expect("serialize");
let parsed: FailureCategory = serde_json::from_str(&serialized).expect("deserialize");
assert_eq!(parsed, FailureCategory::SandboxPermissionBlock);
}
}