Skip to main content

soothe_client/
command_client.rs

1//! Ephemeral command clients for jobs / cron / autopilot one-shots.
2
3use std::time::Duration;
4
5use serde_json::{json, Map, Value};
6
7use crate::client::Client;
8use crate::errors::Result;
9use crate::session::connect_with_retries;
10
11/// Async one-shot RPC client (connect → RPC → close).
12#[derive(Debug, Clone)]
13pub struct AsyncCommandClient {
14    ws_url: String,
15    timeout: Duration,
16}
17
18impl AsyncCommandClient {
19    /// Create against `ws_url` with default 30s timeout.
20    pub fn new(ws_url: impl Into<String>) -> Self {
21        Self {
22            ws_url: ws_url.into(),
23            timeout: Duration::from_secs(30),
24        }
25    }
26
27    /// Override timeout.
28    pub fn with_timeout(mut self, timeout: Duration) -> Self {
29        self.timeout = timeout;
30        self
31    }
32
33    async fn with_client<F, Fut>(&self, f: F) -> Result<Map<String, Value>>
34    where
35        F: FnOnce(Client) -> Fut,
36        Fut: std::future::Future<Output = Result<Map<String, Value>>>,
37    {
38        let client = Client::new(&self.ws_url);
39        connect_with_retries(&client, 5, Duration::from_millis(250)).await?;
40        let result = f(client.clone()).await;
41        let _ = client.close().await;
42        result
43    }
44
45    async fn rpc(&self, method: &str, params: Map<String, Value>) -> Result<Map<String, Value>> {
46        let timeout = self.timeout;
47        self.with_client(|c| async move { c.request(method, params, timeout).await })
48            .await
49    }
50
51    /// Generic request.
52    pub async fn request(
53        &self,
54        method: &str,
55        params: Map<String, Value>,
56    ) -> Result<Map<String, Value>> {
57        self.rpc(method, params).await
58    }
59
60    /// `job_create`.
61    pub async fn job_create(
62        &self,
63        goal: &str,
64        workspace: Option<&str>,
65    ) -> Result<Map<String, Value>> {
66        let mut params = Map::new();
67        params.insert("goal".into(), json!(goal));
68        if let Some(ws) = workspace {
69            params.insert("workspace".into(), json!(ws));
70        }
71        self.rpc("job_create", params).await
72    }
73
74    /// `job_status`.
75    pub async fn job_status(&self, job_id: &str) -> Result<Map<String, Value>> {
76        let mut params = Map::new();
77        params.insert("job_id".into(), json!(job_id));
78        self.rpc("job_status", params).await
79    }
80
81    /// `job_pause`.
82    pub async fn job_pause(&self, job_id: &str) -> Result<Map<String, Value>> {
83        let mut params = Map::new();
84        params.insert("job_id".into(), json!(job_id));
85        self.rpc("job_pause", params).await
86    }
87
88    /// `job_resume`.
89    pub async fn job_resume(&self, job_id: &str) -> Result<Map<String, Value>> {
90        let mut params = Map::new();
91        params.insert("job_id".into(), json!(job_id));
92        self.rpc("job_resume", params).await
93    }
94
95    /// `job_cancel`.
96    pub async fn job_cancel(&self, job_id: &str) -> Result<Map<String, Value>> {
97        let mut params = Map::new();
98        params.insert("job_id".into(), json!(job_id));
99        self.rpc("job_cancel", params).await
100    }
101
102    /// `job_dag`.
103    pub async fn job_dag(&self, job_id: &str) -> Result<Map<String, Value>> {
104        let mut params = Map::new();
105        params.insert("job_id".into(), json!(job_id));
106        self.rpc("job_dag", params).await
107    }
108
109    /// `job_guidance`.
110    pub async fn job_guidance(
111        &self,
112        job_id: &str,
113        content: &str,
114        goal_id: Option<&str>,
115    ) -> Result<Map<String, Value>> {
116        let mut params = Map::new();
117        params.insert("job_id".into(), json!(job_id));
118        params.insert("content".into(), json!(content));
119        if let Some(g) = goal_id {
120            params.insert("goal_id".into(), json!(g));
121        }
122        self.rpc("job_guidance", params).await
123    }
124
125    /// `autopilot_status`.
126    pub async fn autopilot_status(&self) -> Result<Map<String, Value>> {
127        self.rpc("autopilot_status", Map::new()).await
128    }
129
130    /// `autopilot_submit`.
131    pub async fn autopilot_submit(
132        &self,
133        description: &str,
134        priority: i32,
135        workspace: Option<&str>,
136    ) -> Result<Map<String, Value>> {
137        let mut params = Map::new();
138        params.insert("description".into(), json!(description));
139        params.insert("priority".into(), json!(priority));
140        if let Some(ws) = workspace {
141            params.insert("workspace".into(), json!(ws));
142        }
143        self.rpc("autopilot_submit", params).await
144    }
145
146    /// `autopilot_list_goals`.
147    pub async fn autopilot_list_goals(&self) -> Result<Map<String, Value>> {
148        self.rpc("autopilot_list_goals", Map::new()).await
149    }
150
151    /// `autopilot_get_goal`.
152    pub async fn autopilot_get_goal(&self, goal_id: &str) -> Result<Map<String, Value>> {
153        let mut params = Map::new();
154        params.insert("goal_id".into(), json!(goal_id));
155        self.rpc("autopilot_get_goal", params).await
156    }
157
158    /// `autopilot_cancel_goal`.
159    pub async fn autopilot_cancel_goal(&self, goal_id: &str) -> Result<Map<String, Value>> {
160        let mut params = Map::new();
161        params.insert("goal_id".into(), json!(goal_id));
162        self.rpc("autopilot_cancel_goal", params).await
163    }
164
165    /// `autopilot_cancel_all`.
166    pub async fn autopilot_cancel_all(&self) -> Result<Map<String, Value>> {
167        self.rpc("autopilot_cancel_all", Map::new()).await
168    }
169
170    /// `autopilot_wake`.
171    pub async fn autopilot_wake(&self) -> Result<Map<String, Value>> {
172        self.rpc("autopilot_wake", Map::new()).await
173    }
174
175    /// `autopilot_dream`.
176    pub async fn autopilot_dream(&self) -> Result<Map<String, Value>> {
177        self.rpc("autopilot_dream", Map::new()).await
178    }
179
180    /// `autopilot_resume`.
181    pub async fn autopilot_resume(&self, goal_id: &str) -> Result<Map<String, Value>> {
182        let mut params = Map::new();
183        params.insert("goal_id".into(), json!(goal_id));
184        self.rpc("autopilot_resume", params).await
185    }
186
187    /// `autopilot_list_jobs`.
188    pub async fn autopilot_list_jobs(&self) -> Result<Map<String, Value>> {
189        self.rpc("autopilot_list_jobs", Map::new()).await
190    }
191
192    /// `autopilot_get_job`.
193    pub async fn autopilot_get_job(&self, job_id: &str) -> Result<Map<String, Value>> {
194        let mut params = Map::new();
195        params.insert("job_id".into(), json!(job_id));
196        self.rpc("autopilot_get_job", params).await
197    }
198
199    /// `cron_add` (normalized to `{"job": {...}}` when possible).
200    pub async fn cron_add(&self, text: &str, priority: Option<i32>) -> Result<Map<String, Value>> {
201        let mut params = Map::new();
202        params.insert("text".into(), json!(text));
203        if let Some(p) = priority {
204            params.insert("priority".into(), json!(p));
205        }
206        let result = self.rpc("cron_add", params).await?;
207        Ok(normalize_cron_add(result))
208    }
209
210    /// `cron_list`.
211    pub async fn cron_list(&self, status: Option<&str>) -> Result<Map<String, Value>> {
212        let mut params = Map::new();
213        if let Some(s) = status {
214            params.insert("status".into(), json!(s));
215        }
216        self.rpc("cron_list", params).await
217    }
218
219    /// `cron_show`.
220    pub async fn cron_show(&self, job_id: &str) -> Result<Map<String, Value>> {
221        let mut params = Map::new();
222        params.insert("job_id".into(), json!(job_id));
223        let result = self.rpc("cron_show", params).await?;
224        Ok(normalize_cron_show(result))
225    }
226
227    /// `cron_cancel`.
228    pub async fn cron_cancel(&self, job_id: &str) -> Result<Map<String, Value>> {
229        let mut params = Map::new();
230        params.insert("job_id".into(), json!(job_id));
231        self.rpc("cron_cancel", params).await
232    }
233
234    /// `memory_stats`.
235    pub async fn memory_stats(&self, mode: &str) -> Result<Map<String, Value>> {
236        let mut params = Map::new();
237        params.insert("mode".into(), json!(mode));
238        self.rpc("memory_stats", params).await
239    }
240}
241
242/// Sync wrapper around [`AsyncCommandClient`] for scripts.
243#[derive(Debug, Clone)]
244pub struct CommandClient {
245    inner: AsyncCommandClient,
246}
247
248impl CommandClient {
249    /// Create sync command client.
250    pub fn new(ws_url: impl Into<String>) -> Self {
251        Self {
252            inner: AsyncCommandClient::new(ws_url),
253        }
254    }
255
256    /// Override timeout.
257    pub fn with_timeout(mut self, timeout: Duration) -> Self {
258        self.inner = self.inner.with_timeout(timeout);
259        self
260    }
261
262    fn block<F, T>(&self, f: F) -> Result<T>
263    where
264        F: std::future::Future<Output = Result<T>>,
265    {
266        match tokio::runtime::Handle::try_current() {
267            Ok(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
268            Err(_) => {
269                let rt = tokio::runtime::Builder::new_current_thread()
270                    .enable_all()
271                    .build()
272                    .map_err(|e| crate::errors::Error::msg(e.to_string()))?;
273                rt.block_on(f)
274            }
275        }
276    }
277
278    /// `job_create`.
279    pub fn job_create(&self, goal: &str, workspace: Option<&str>) -> Result<Map<String, Value>> {
280        self.block(self.inner.job_create(goal, workspace))
281    }
282
283    /// `job_status`.
284    pub fn job_status(&self, job_id: &str) -> Result<Map<String, Value>> {
285        self.block(self.inner.job_status(job_id))
286    }
287
288    /// `job_pause`.
289    pub fn job_pause(&self, job_id: &str) -> Result<Map<String, Value>> {
290        self.block(self.inner.job_pause(job_id))
291    }
292
293    /// `job_resume`.
294    pub fn job_resume(&self, job_id: &str) -> Result<Map<String, Value>> {
295        self.block(self.inner.job_resume(job_id))
296    }
297
298    /// `job_cancel`.
299    pub fn job_cancel(&self, job_id: &str) -> Result<Map<String, Value>> {
300        self.block(self.inner.job_cancel(job_id))
301    }
302
303    /// `job_dag`.
304    pub fn job_dag(&self, job_id: &str) -> Result<Map<String, Value>> {
305        self.block(self.inner.job_dag(job_id))
306    }
307
308    /// `job_guidance`.
309    pub fn job_guidance(
310        &self,
311        job_id: &str,
312        content: &str,
313        goal_id: Option<&str>,
314    ) -> Result<Map<String, Value>> {
315        self.block(self.inner.job_guidance(job_id, content, goal_id))
316    }
317
318    /// `autopilot_status`.
319    pub fn autopilot_status(&self) -> Result<Map<String, Value>> {
320        self.block(self.inner.autopilot_status())
321    }
322
323    /// `autopilot_submit`.
324    pub fn autopilot_submit(
325        &self,
326        description: &str,
327        priority: i32,
328        workspace: Option<&str>,
329    ) -> Result<Map<String, Value>> {
330        self.block(
331            self.inner
332                .autopilot_submit(description, priority, workspace),
333        )
334    }
335
336    /// `autopilot_list_goals`.
337    pub fn autopilot_list_goals(&self) -> Result<Map<String, Value>> {
338        self.block(self.inner.autopilot_list_goals())
339    }
340
341    /// `autopilot_get_goal`.
342    pub fn autopilot_get_goal(&self, goal_id: &str) -> Result<Map<String, Value>> {
343        self.block(self.inner.autopilot_get_goal(goal_id))
344    }
345
346    /// `autopilot_cancel_goal`.
347    pub fn autopilot_cancel_goal(&self, goal_id: &str) -> Result<Map<String, Value>> {
348        self.block(self.inner.autopilot_cancel_goal(goal_id))
349    }
350
351    /// `autopilot_cancel_all`.
352    pub fn autopilot_cancel_all(&self) -> Result<Map<String, Value>> {
353        self.block(self.inner.autopilot_cancel_all())
354    }
355
356    /// `autopilot_wake`.
357    pub fn autopilot_wake(&self) -> Result<Map<String, Value>> {
358        self.block(self.inner.autopilot_wake())
359    }
360
361    /// `autopilot_dream`.
362    pub fn autopilot_dream(&self) -> Result<Map<String, Value>> {
363        self.block(self.inner.autopilot_dream())
364    }
365
366    /// `autopilot_resume`.
367    pub fn autopilot_resume(&self, goal_id: &str) -> Result<Map<String, Value>> {
368        self.block(self.inner.autopilot_resume(goal_id))
369    }
370
371    /// `autopilot_list_jobs`.
372    pub fn autopilot_list_jobs(&self) -> Result<Map<String, Value>> {
373        self.block(self.inner.autopilot_list_jobs())
374    }
375
376    /// `autopilot_get_job`.
377    pub fn autopilot_get_job(&self, job_id: &str) -> Result<Map<String, Value>> {
378        self.block(self.inner.autopilot_get_job(job_id))
379    }
380
381    /// `cron_add`.
382    pub fn cron_add(&self, text: &str, priority: Option<i32>) -> Result<Map<String, Value>> {
383        self.block(self.inner.cron_add(text, priority))
384    }
385
386    /// `cron_list`.
387    pub fn cron_list(&self, status: Option<&str>) -> Result<Map<String, Value>> {
388        self.block(self.inner.cron_list(status))
389    }
390
391    /// `cron_show`.
392    pub fn cron_show(&self, job_id: &str) -> Result<Map<String, Value>> {
393        self.block(self.inner.cron_show(job_id))
394    }
395
396    /// `cron_cancel`.
397    pub fn cron_cancel(&self, job_id: &str) -> Result<Map<String, Value>> {
398        self.block(self.inner.cron_cancel(job_id))
399    }
400
401    /// `memory_stats`.
402    pub fn memory_stats(&self, mode: &str) -> Result<Map<String, Value>> {
403        self.block(self.inner.memory_stats(mode))
404    }
405
406    /// Generic request.
407    pub fn request(&self, method: &str, params: Map<String, Value>) -> Result<Map<String, Value>> {
408        self.block(self.inner.request(method, params))
409    }
410}
411
412fn normalize_cron_add(result: Map<String, Value>) -> Map<String, Value> {
413    if result.contains_key("job") {
414        return result;
415    }
416    let job_id = result.get("job_id").or_else(|| result.get("id")).cloned();
417    if let Some(id) = job_id {
418        let mut job = result.clone();
419        job.insert("id".into(), id);
420        job.remove("job_id");
421        let mut out = Map::new();
422        out.insert("job".into(), Value::Object(job));
423        if result.get("duplicate").and_then(|v| v.as_bool()) == Some(true) {
424            out.insert("duplicate".into(), json!(true));
425        }
426        return out;
427    }
428    result
429}
430
431fn normalize_cron_show(result: Map<String, Value>) -> Map<String, Value> {
432    if result.contains_key("job") {
433        return result;
434    }
435    let job_id = result.get("job_id").or_else(|| result.get("id")).cloned();
436    let Some(id) = job_id else {
437        let mut out = Map::new();
438        out.insert("job".into(), Value::Null);
439        return out;
440    };
441    let mut job = result.clone();
442    job.insert("id".into(), id);
443    job.remove("job_id");
444    let mut out = Map::new();
445    out.insert("job".into(), Value::Object(job));
446    out
447}