1use crate::input::{Input, PeerConvention};
12use meerkat_core::types::HandlingMode;
13
14#[derive(Debug, Clone, thiserror::Error)]
16#[non_exhaustive]
17pub enum PeerHandlingModeError {
18 #[error("handling_mode is forbidden on ResponseProgress peer inputs")]
20 ForbiddenForResponseProgress,
21 #[error("steer handling_mode cannot carry injected context on peer inputs")]
25 SteerCannotCarryInjectedContext,
26}
27
28pub fn validate_peer_handling_mode(input: &Input) -> Result<(), PeerHandlingModeError> {
30 let Input::Peer(peer) = input else {
31 return Ok(());
32 };
33 if peer.handling_mode == Some(HandlingMode::Steer) && !peer.injected_context.is_empty() {
34 return Err(PeerHandlingModeError::SteerCannotCarryInjectedContext);
35 }
36 if peer.handling_mode.is_none() {
37 return Ok(());
38 }
39 match &peer.convention {
40 Some(PeerConvention::ResponseProgress { .. }) => {
41 Err(PeerHandlingModeError::ForbiddenForResponseProgress)
42 }
43 _ => Ok(()),
44 }
45}
46
47#[cfg(test)]
48#[allow(clippy::unwrap_used)]
49mod tests {
50 use super::*;
51 use crate::input::*;
52 use chrono::Utc;
53 use meerkat_core::lifecycle::InputId;
54 use meerkat_core::types::HandlingMode;
55
56 fn make_header() -> InputHeader {
57 InputHeader {
58 id: InputId::new(),
59 timestamp: Utc::now(),
60 source: InputOrigin::Peer {
61 peer_id: "peer-1".into(),
62 display_identity: None,
63 runtime_id: None,
64 },
65 durability: InputDurability::Durable,
66 visibility: InputVisibility::default(),
67 idempotency_key: None,
68 supersession_key: None,
69 correlation_id: None,
70 }
71 }
72
73 #[test]
74 fn response_progress_with_handling_mode_rejected() {
75 let input = Input::Peer(PeerInput {
76 directed_interaction_id: None,
77 objective_id: None,
78 system_prompts: Vec::new(),
79 injected_context: Vec::new(),
80 sender_taint: None,
81 header: make_header(),
82 convention: Some(PeerConvention::ResponseProgress {
83 request_id: "r".into(),
84 phase: ResponseProgressPhase::InProgress,
85 }),
86 content: "working".into(),
87 payload: Some(serde_json::json!({"progress": "working"})),
88 handling_mode: Some(HandlingMode::Queue),
89 });
90 let err = validate_peer_handling_mode(&input).unwrap_err();
91 assert!(matches!(
92 err,
93 PeerHandlingModeError::ForbiddenForResponseProgress
94 ));
95 }
96
97 #[test]
98 fn response_terminal_with_handling_mode_accepted() {
99 let input = Input::Peer(PeerInput {
100 directed_interaction_id: None,
101 objective_id: None,
102 system_prompts: Vec::new(),
103 injected_context: Vec::new(),
104 sender_taint: None,
105 header: make_header(),
106 convention: Some(PeerConvention::ResponseTerminal {
107 request_id: "r".into(),
108 status: ResponseTerminalStatus::Completed,
109 }),
110 content: "done".into(),
111 payload: Some(serde_json::json!({"ok": true})),
112 handling_mode: Some(HandlingMode::Steer),
113 });
114 assert!(validate_peer_handling_mode(&input).is_ok());
115 }
116
117 #[test]
118 fn response_terminal_with_queue_handling_mode_accepted() {
119 let input = Input::Peer(PeerInput {
120 directed_interaction_id: None,
121 objective_id: None,
122 system_prompts: Vec::new(),
123 injected_context: Vec::new(),
124 sender_taint: None,
125 header: make_header(),
126 convention: Some(PeerConvention::ResponseTerminal {
127 request_id: "r".into(),
128 status: ResponseTerminalStatus::Completed,
129 }),
130 content: "done".into(),
131 payload: Some(serde_json::json!({"ok": true})),
132 handling_mode: Some(HandlingMode::Queue),
133 });
134 assert!(validate_peer_handling_mode(&input).is_ok());
135 }
136
137 #[test]
138 fn message_with_handling_mode_accepted() {
139 let input = Input::Peer(PeerInput {
140 directed_interaction_id: None,
141 objective_id: None,
142 system_prompts: Vec::new(),
143 injected_context: Vec::new(),
144 sender_taint: None,
145 header: make_header(),
146 convention: Some(PeerConvention::Message),
147 content: "hi".into(),
148 payload: None,
149 handling_mode: Some(HandlingMode::Queue),
150 });
151 assert!(validate_peer_handling_mode(&input).is_ok());
152 }
153
154 #[test]
155 fn request_with_handling_mode_accepted() {
156 let input = Input::Peer(PeerInput {
157 directed_interaction_id: None,
158 objective_id: None,
159 system_prompts: Vec::new(),
160 injected_context: Vec::new(),
161 sender_taint: None,
162 header: make_header(),
163 convention: Some(PeerConvention::Request {
164 request_id: "r".into(),
165 intent: "i".into(),
166 }),
167 content: "do it".into(),
168 payload: Some(serde_json::json!({"subject": "x"})),
169 handling_mode: Some(HandlingMode::Steer),
170 });
171 assert!(validate_peer_handling_mode(&input).is_ok());
172 }
173
174 #[test]
175 fn no_convention_with_handling_mode_accepted() {
176 let input = Input::Peer(PeerInput {
177 directed_interaction_id: None,
178 objective_id: None,
179 system_prompts: Vec::new(),
180 injected_context: Vec::new(),
181 sender_taint: None,
182 header: make_header(),
183 convention: None,
184 content: "hi".into(),
185 payload: None,
186 handling_mode: Some(HandlingMode::Queue),
187 });
188 assert!(validate_peer_handling_mode(&input).is_ok());
189 }
190
191 #[test]
194 fn steer_with_injected_context_rejected() {
195 let input = Input::Peer(PeerInput {
196 directed_interaction_id: None,
197 objective_id: None,
198 system_prompts: Vec::new(),
199 injected_context: vec![meerkat_core::types::ContentInput::Text(
200 "ambient".to_string(),
201 )],
202 sender_taint: None,
203 header: make_header(),
204 convention: Some(PeerConvention::Message),
205 content: "urgent".into(),
206 payload: None,
207 handling_mode: Some(HandlingMode::Steer),
208 });
209 let err = validate_peer_handling_mode(&input).unwrap_err();
210 assert!(matches!(
211 err,
212 PeerHandlingModeError::SteerCannotCarryInjectedContext
213 ));
214 }
215
216 #[test]
219 fn queue_with_injected_context_accepted() {
220 let input = Input::Peer(PeerInput {
221 directed_interaction_id: None,
222 objective_id: None,
223 system_prompts: Vec::new(),
224 injected_context: vec![meerkat_core::types::ContentInput::Text(
225 "ambient".to_string(),
226 )],
227 sender_taint: None,
228 header: make_header(),
229 convention: Some(PeerConvention::Message),
230 content: "work".into(),
231 payload: None,
232 handling_mode: Some(HandlingMode::Queue),
233 });
234 assert!(validate_peer_handling_mode(&input).is_ok());
235 }
236
237 #[test]
238 fn peer_without_handling_mode_always_accepted() {
239 for convention in [
240 Some(PeerConvention::Message),
241 Some(PeerConvention::Request {
242 request_id: "r".into(),
243 intent: "i".into(),
244 }),
245 Some(PeerConvention::ResponseProgress {
246 request_id: "r".into(),
247 phase: ResponseProgressPhase::InProgress,
248 }),
249 Some(PeerConvention::ResponseTerminal {
250 request_id: "r".into(),
251 status: ResponseTerminalStatus::Completed,
252 }),
253 None,
254 ] {
255 let input = Input::Peer(PeerInput {
256 directed_interaction_id: None,
257 objective_id: None,
258 system_prompts: Vec::new(),
259 injected_context: Vec::new(),
260 sender_taint: None,
261 header: make_header(),
262 convention,
263 content: "hi".into(),
264 payload: None,
265 handling_mode: None,
266 });
267 assert!(
268 validate_peer_handling_mode(&input).is_ok(),
269 "should accept peer without handling_mode"
270 );
271 }
272 }
273
274 #[test]
275 fn non_peer_input_always_accepted() {
276 let input = Input::Prompt(PromptInput {
277 injected_context: Vec::new(),
278 header: InputHeader {
279 id: InputId::new(),
280 timestamp: Utc::now(),
281 source: InputOrigin::Operator,
282 durability: InputDurability::Durable,
283 visibility: InputVisibility::default(),
284 idempotency_key: None,
285 supersession_key: None,
286 correlation_id: None,
287 },
288 content: "hi".into(),
289 typed_turn_appends: Vec::new(),
290 turn_metadata: None,
291 });
292 assert!(validate_peer_handling_mode(&input).is_ok());
293 }
294}