agentmux_common/
transcript_request.rs1use serde::{Deserialize, Serialize};
29
30pub const TRANSCRIPT_REQUEST_TYPE: &str = "transcript_request";
31pub const TRANSCRIPT_RESPONSE_TYPE: &str = "transcript_response";
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct TranscriptRequest {
35 #[serde(rename = "type")]
36 pub msg_type: String,
37 pub request_id: String,
38 pub max_lines: usize,
39}
40
41impl TranscriptRequest {
42 pub fn new(request_id: impl Into<String>, max_lines: usize) -> Self {
43 Self {
44 msg_type: TRANSCRIPT_REQUEST_TYPE.to_string(),
45 request_id: request_id.into(),
46 max_lines,
47 }
48 }
49
50 pub fn to_message(&self) -> String {
51 serde_json::to_string(self).unwrap_or_default()
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(tag = "status", rename_all = "lowercase")]
57pub enum TranscriptResponseStatus {
58 Ok { lines: Vec<String> },
59 Denied,
60 Error { error: String },
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub struct TranscriptResponse {
65 #[serde(rename = "type")]
66 pub msg_type: String,
67 pub request_id: String,
68 #[serde(flatten)]
69 pub status: TranscriptResponseStatus,
70}
71
72impl TranscriptResponse {
73 pub fn ok(request_id: impl Into<String>, lines: Vec<String>) -> Self {
74 Self {
75 msg_type: TRANSCRIPT_RESPONSE_TYPE.to_string(),
76 request_id: request_id.into(),
77 status: TranscriptResponseStatus::Ok { lines },
78 }
79 }
80
81 pub fn denied(request_id: impl Into<String>) -> Self {
82 Self {
83 msg_type: TRANSCRIPT_RESPONSE_TYPE.to_string(),
84 request_id: request_id.into(),
85 status: TranscriptResponseStatus::Denied,
86 }
87 }
88
89 pub fn error(request_id: impl Into<String>, error: impl Into<String>) -> Self {
90 Self {
91 msg_type: TRANSCRIPT_RESPONSE_TYPE.to_string(),
92 request_id: request_id.into(),
93 status: TranscriptResponseStatus::Error { error: error.into() },
94 }
95 }
96
97 pub fn to_message(&self) -> String {
98 serde_json::to_string(self).unwrap_or_default()
99 }
100}
101
102pub fn parse_transcript_request(message: &str) -> Option<TranscriptRequest> {
109 let value: serde_json::Value = serde_json::from_str(message).ok()?;
110 if value.get("type")?.as_str()? != TRANSCRIPT_REQUEST_TYPE {
111 return None;
112 }
113 serde_json::from_value(value).ok()
114}
115
116pub fn parse_transcript_response(message: &str) -> Option<TranscriptResponse> {
119 let value: serde_json::Value = serde_json::from_str(message).ok()?;
120 if value.get("type")?.as_str()? != TRANSCRIPT_RESPONSE_TYPE {
121 return None;
122 }
123 serde_json::from_value(value).ok()
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn request_round_trips_through_message_encoding() {
132 let req = TranscriptRequest::new("req-1", 100);
133 let message = req.to_message();
134 let parsed = parse_transcript_request(&message).unwrap();
135 assert_eq!(parsed, req);
136 }
137
138 #[test]
139 fn ordinary_free_text_never_parses_as_a_request() {
140 assert!(parse_transcript_request("just chatting with you").is_none());
141 }
142
143 #[test]
144 fn arbitrary_json_that_isnt_this_shape_never_parses_as_a_request() {
145 assert!(parse_transcript_request(r#"{"type":"something_else","foo":"bar"}"#).is_none());
146 assert!(parse_transcript_request(r#"{"foo":"bar"}"#).is_none());
147 assert!(parse_transcript_request("[1,2,3]").is_none());
148 }
149
150 #[test]
151 fn malformed_json_never_parses_as_a_request_and_never_panics() {
152 assert!(parse_transcript_request("{not json").is_none());
153 }
154
155 #[test]
156 fn ok_response_round_trips() {
157 let resp = TranscriptResponse::ok("req-1", vec!["line 1".to_string(), "line 2".to_string()]);
158 let message = resp.to_message();
159 let parsed = parse_transcript_response(&message).unwrap();
160 assert_eq!(parsed, resp);
161 }
162
163 #[test]
164 fn denied_response_round_trips() {
165 let resp = TranscriptResponse::denied("req-1");
166 let message = resp.to_message();
167 let parsed = parse_transcript_response(&message).unwrap();
168 assert_eq!(parsed, resp);
169 }
170
171 #[test]
172 fn error_response_round_trips() {
173 let resp = TranscriptResponse::error("req-1", "something broke");
174 let message = resp.to_message();
175 let parsed = parse_transcript_response(&message).unwrap();
176 assert_eq!(parsed, resp);
177 }
178
179 #[test]
180 fn a_request_never_parses_as_a_response_and_vice_versa() {
181 let req = TranscriptRequest::new("req-1", 100);
182 assert!(parse_transcript_response(&req.to_message()).is_none());
183 let resp = TranscriptResponse::denied("req-1");
184 assert!(parse_transcript_request(&resp.to_message()).is_none());
185 }
186}