harn_vm/llm/
protocol_violation.rs1use serde::{Deserialize, Serialize};
2
3#[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}