Skip to main content

response_validator/
types.rs

1//! Core types for response validation.
2
3use serde::{Deserialize, Serialize};
4
5/// A content block from an LLM response.
6///
7/// This is a minimal representation — consumers can convert from their own
8/// content block types into this for validation.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(tag = "type")]
11pub enum ContentBlock {
12    /// A text content block.
13    #[serde(rename = "text")]
14    Text { text: String },
15
16    /// A tool use content block.
17    #[serde(rename = "tool_use")]
18    ToolUse {
19        id: String,
20        name: String,
21        input: serde_json::Value,
22    },
23
24    /// A tool result content block.
25    #[serde(rename = "tool_result")]
26    ToolResult {
27        tool_use_id: String,
28        content: String,
29    },
30}
31
32/// Result of validating a response for hallucinated turn markers.
33#[derive(Debug, Clone)]
34pub struct ValidationResult {
35    /// The (possibly truncated) text content.
36    pub text: String,
37    /// Whether hallucinated turn markers were detected.
38    pub was_truncated: bool,
39    /// The marker that triggered truncation, if any.
40    pub detected_marker: Option<String>,
41    /// Character position where truncation occurred.
42    pub truncation_offset: Option<usize>,
43}
44
45/// Category of action an LLM claims to have performed.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum ActionCategory {
48    /// File write/update/modify/create/delete.
49    FileOperation,
50    /// Shell command execution.
51    CommandExecution,
52    /// General completion claim ("Done! I've ...").
53    GeneralCompletion,
54}
55
56impl ActionCategory {
57    /// Check whether a tool name is consistent with this action category.
58    pub fn matches_tool(&self, tool_name: &str) -> bool {
59        let lower = tool_name.to_lowercase();
60        match self {
61            Self::FileOperation => {
62                lower.contains("write")
63                    || lower.contains("edit")
64                    || lower.contains("notebook")
65                    || lower.contains("file")
66            }
67            Self::CommandExecution => {
68                lower.contains("bash")
69                    || lower.contains("shell")
70                    || lower.contains("exec")
71                    || lower.contains("command")
72            }
73            Self::GeneralCompletion => true,
74        }
75    }
76}
77
78impl std::fmt::Display for ActionCategory {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            Self::FileOperation => write!(f, "file_operation"),
82            Self::CommandExecution => write!(f, "command_execution"),
83            Self::GeneralCompletion => write!(f, "general_completion"),
84        }
85    }
86}
87
88/// A detected action claim in response text.
89#[derive(Debug, Clone)]
90pub struct ActionClaim {
91    /// The matched text fragment.
92    pub matched_text: String,
93    /// Human-readable description of the pattern that matched.
94    pub description: &'static str,
95    /// Category of the claimed action.
96    pub category: ActionCategory,
97    /// Confidence score (0.0–1.0).
98    pub confidence: f32,
99    /// Character offset in the source text.
100    pub offset: usize,
101}
102
103/// Result of validating action claims against actual tool usage.
104#[derive(Debug, Clone, Default)]
105pub struct ActionClaimValidation {
106    /// All detected action claims.
107    pub claims: Vec<ActionClaim>,
108    /// Claims with no matching tool usage (potential hallucinations).
109    pub unmatched_claims: Vec<ActionClaim>,
110}
111
112impl ActionClaimValidation {
113    /// Whether any unmatched (potentially hallucinated) claims were found.
114    pub fn has_warnings(&self) -> bool {
115        !self.unmatched_claims.is_empty()
116    }
117}