Skip to main content

harn_vm/llm/
protocol_violation.rs

1use serde::{Deserialize, Serialize};
2
3/// Stable classification for a text-tool protocol violation.
4///
5/// The Harn stdlib owns detection and recovery policy. Rust mirrors the
6/// structural result at the host boundary so callers cannot accidentally
7/// collapse protocol identity into presentation text.
8#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ProtocolViolationKind {
11    AmbiguousRecovery,
12    BareJson,
13    EmptyDone,
14    FenceDialect,
15    ProviderDialect,
16    RecoveredDialect,
17    StrayText,
18    UnclosedResponse,
19    UnknownTag,
20    UnparsedToolSyntax,
21    WrongToolFormat,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
25pub struct ProtocolViolation {
26    pub kind: ProtocolViolationKind,
27    pub message: String,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub excerpt: Option<String>,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub dropped_reason: Option<String>,
32}
33
34impl ProtocolViolation {
35    pub fn is_unparsed_tool_syntax(&self) -> bool {
36        self.kind == ProtocolViolationKind::UnparsedToolSyntax
37    }
38}
39
40#[cfg(test)]
41mod tests {
42    use super::{ProtocolViolation, ProtocolViolationKind};
43
44    #[test]
45    fn known_kind_round_trips_structurally() {
46        let violation = ProtocolViolation {
47            kind: ProtocolViolationKind::UnparsedToolSyntax,
48            message: "unrecognized tool syntax".to_string(),
49            excerpt: Some("<tool_call>".to_string()),
50            dropped_reason: Some("unrecognized".to_string()),
51        };
52
53        let encoded = serde_json::to_value(&violation).expect("serialize violation");
54        assert_eq!(encoded["kind"], "unparsed_tool_syntax");
55        assert_eq!(
56            serde_json::from_value::<ProtocolViolation>(encoded).expect("deserialize violation"),
57            violation
58        );
59    }
60
61    #[test]
62    fn unknown_kind_is_rejected() {
63        let encoded = serde_json::json!({
64            "kind": "future_violation",
65            "message": "unknown"
66        });
67        assert!(serde_json::from_value::<ProtocolViolation>(encoded).is_err());
68    }
69}