1use serde::{Deserialize, Deserializer, Serialize};
8use serde_json::json;
9
10pub const WAIT_OPERATIONS_TOOL_NAME: &str = "wait_operations";
11pub const INSPECT_OPERATIONS_TOOL_NAME: &str = "inspect_operations";
12pub const STOP_OPERATIONS_TOOL_NAME: &str = "stop_operations";
13pub const SEND_OPERATION_INPUT_TOOL_NAME: &str = "send_operation_input";
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum AsyncOperationKind {
18 Ability,
19 Delegation,
20 SubAgent,
21 Shell,
22 Media,
23}
24
25impl AsyncOperationKind {
26 pub fn as_str(self) -> &'static str {
27 match self {
28 Self::Ability => "ability",
29 Self::Delegation => "delegation",
30 Self::SubAgent => "sub_agent",
31 Self::Shell => "shell",
32 Self::Media => "media",
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum AsyncOperationStatus {
40 Running,
41 WaitingForInput,
42 Completed,
43 Failed,
44 Stopped,
45}
46
47impl AsyncOperationStatus {
48 pub fn as_str(self) -> &'static str {
49 match self {
50 Self::Running => "running",
51 Self::WaitingForInput => "waiting_for_input",
52 Self::Completed => "completed",
53 Self::Failed => "failed",
54 Self::Stopped => "stopped",
55 }
56 }
57
58 pub fn can_receive_input(self) -> bool {
59 matches!(self, Self::Running | Self::WaitingForInput)
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum AsyncOperationSignalKind {
66 Started,
67 Progress,
68 NeedsInput,
69 Completed,
70 Failed,
71 Stopped,
72}
73
74impl AsyncOperationSignalKind {
75 pub fn as_str(self) -> &'static str {
76 match self {
77 Self::Started => "started",
78 Self::Progress => "progress",
79 Self::NeedsInput => "needs_input",
80 Self::Completed => "completed",
81 Self::Failed => "failed",
82 Self::Stopped => "stopped",
83 }
84 }
85}
86
87#[derive(Debug, Clone, Deserialize)]
88pub struct InspectOperationsArgs {
89 #[serde(default)]
90 pub operations: Vec<String>,
91 #[serde(default)]
92 pub kind: Option<AsyncOperationKind>,
93 #[serde(default)]
94 pub include_transcript: bool,
95 #[serde(
96 default = "default_inspect_limit",
97 deserialize_with = "deserialize_usize_from_json_number"
98 )]
99 pub limit: usize,
100}
101
102#[derive(Debug, Clone, Deserialize)]
103pub struct StopOperationsArgs {
104 #[serde(default)]
105 pub operations: Vec<String>,
106 #[serde(default)]
107 pub kind: Option<AsyncOperationKind>,
108 pub reason: Option<String>,
109}
110
111#[derive(Debug, Clone, Deserialize)]
112pub struct WaitOperationsArgs {
113 #[serde(
114 default = "default_wait_seconds",
115 deserialize_with = "deserialize_u64_from_json_number"
116 )]
117 pub seconds: u64,
118 #[serde(default)]
119 pub kind: Option<AsyncOperationKind>,
120 pub reason: Option<String>,
121}
122
123#[derive(Debug, Clone, Deserialize)]
124pub struct SendOperationInputArgs {
125 #[serde(default)]
126 pub operations: Vec<String>,
127 pub message: String,
128}
129
130pub fn inspect_operations_parameters_schema() -> serde_json::Value {
131 json!({
132 "type": "object",
133 "properties": {
134 "operations": {"type": "array", "items": {"type": "string"}},
135 "kind": operation_kind_schema(),
136 "include_transcript": {"type": "boolean"},
137 "limit": {"type": "integer", "minimum": 1, "maximum": 50}
138 },
139 "additionalProperties": false
140 })
141}
142
143pub fn stop_operations_parameters_schema() -> serde_json::Value {
144 json!({
145 "type": "object",
146 "properties": {
147 "operations": {"type": "array", "items": {"type": "string"}},
148 "kind": operation_kind_schema(),
149 "reason": {"type": "string"}
150 },
151 "additionalProperties": false
152 })
153}
154
155pub fn wait_operations_parameters_schema() -> serde_json::Value {
156 json!({
157 "type": "object",
158 "properties": {
159 "seconds": {"type": "integer", "minimum": 1, "maximum": 30},
160 "kind": operation_kind_schema(),
161 "reason": {"type": "string"}
162 },
163 "additionalProperties": false
164 })
165}
166
167pub fn send_operation_input_parameters_schema() -> serde_json::Value {
168 json!({
169 "type": "object",
170 "properties": {
171 "operations": {"type": "array", "items": {"type": "string"}},
172 "message": {"type": "string"}
173 },
174 "required": ["operations", "message"],
175 "additionalProperties": false
176 })
177}
178
179pub fn operation_kind_schema() -> serde_json::Value {
180 json!({
181 "type": "string",
182 "enum": ["ability", "delegation", "sub_agent", "shell", "media"]
183 })
184}
185
186fn default_inspect_limit() -> usize {
187 30
188}
189
190pub fn deserialize_usize_from_json_number<'de, D>(deserializer: D) -> Result<usize, D::Error>
191where
192 D: Deserializer<'de>,
193{
194 let value = serde_json::Value::deserialize(deserializer)?;
195 match value {
196 serde_json::Value::Number(number) => {
197 if let Some(raw) = number.as_u64() {
198 usize::try_from(raw).map_err(serde::de::Error::custom)
199 } else if let Some(raw) = number.as_f64() {
200 if raw.is_finite() && raw.fract() == 0.0 && raw >= 0.0 {
201 usize::try_from(raw as u64).map_err(serde::de::Error::custom)
202 } else {
203 Err(serde::de::Error::custom(
204 "expected a non-negative whole number",
205 ))
206 }
207 } else {
208 Err(serde::de::Error::custom(
209 "expected a non-negative whole number",
210 ))
211 }
212 }
213 other => Err(serde::de::Error::custom(format!(
214 "expected a non-negative whole number, got {other}"
215 ))),
216 }
217}
218
219pub fn deserialize_u64_from_json_number<'de, D>(deserializer: D) -> Result<u64, D::Error>
220where
221 D: Deserializer<'de>,
222{
223 let value = serde_json::Value::deserialize(deserializer)?;
224 match value {
225 serde_json::Value::Number(number) => {
226 if let Some(raw) = number.as_u64() {
227 Ok(raw)
228 } else if let Some(raw) = number.as_f64() {
229 if raw.is_finite() && raw.fract() == 0.0 && raw >= 0.0 {
230 Ok(raw as u64)
231 } else {
232 Err(serde::de::Error::custom(
233 "expected a non-negative whole number",
234 ))
235 }
236 } else {
237 Err(serde::de::Error::custom(
238 "expected a non-negative whole number",
239 ))
240 }
241 }
242 other => Err(serde::de::Error::custom(format!(
243 "expected a non-negative whole number, got {other}"
244 ))),
245 }
246}
247
248fn default_wait_seconds() -> u64 {
249 10
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn async_operation_kind_uses_wire_names() {
258 assert_eq!(AsyncOperationKind::Ability.as_str(), "ability");
259 assert_eq!(AsyncOperationKind::Delegation.as_str(), "delegation");
260 assert_eq!(AsyncOperationKind::SubAgent.as_str(), "sub_agent");
261 assert_eq!(AsyncOperationKind::Shell.as_str(), "shell");
262 assert_eq!(AsyncOperationKind::Media.as_str(), "media");
263 }
264
265 #[test]
266 fn wait_args_deserialize_with_defaults() {
267 let args: WaitOperationsArgs = serde_json::from_value(json!({})).unwrap();
268
269 assert_eq!(args.seconds, 10);
270 assert_eq!(args.kind, None);
271 }
272
273 #[test]
274 fn wait_args_accept_whole_float_seconds_from_model_args() {
275 let args: WaitOperationsArgs = serde_json::from_value(json!({
276 "kind": "ability",
277 "seconds": 30.0
278 }))
279 .unwrap();
280
281 assert_eq!(args.seconds, 30);
282 assert_eq!(args.kind, Some(AsyncOperationKind::Ability));
283 }
284
285 #[test]
286 fn wait_args_reject_fractional_seconds() {
287 let err = serde_json::from_value::<WaitOperationsArgs>(json!({
288 "seconds": 5.5
289 }))
290 .unwrap_err();
291
292 assert!(err.to_string().contains("whole number"));
293 }
294
295 #[test]
296 fn inspect_args_accept_whole_float_limit_from_model_args() {
297 let args: InspectOperationsArgs = serde_json::from_value(json!({
298 "operations": ["ability_build_agent_2"],
299 "include_transcript": true,
300 "limit": 5.0
301 }))
302 .unwrap();
303
304 assert_eq!(args.limit, 5);
305 }
306
307 #[test]
308 fn inspect_args_reject_fractional_limit() {
309 let err = serde_json::from_value::<InspectOperationsArgs>(json!({
310 "limit": 5.5
311 }))
312 .unwrap_err();
313
314 assert!(err.to_string().contains("whole number"));
315 }
316}