Skip to main content

soothe_client/
send_methods.rs

1//! Fire-and-forget send variants (Go `send_methods.go` parity).
2//!
3//! Each `send_*` method emits the protocol-1 envelope and returns immediately
4//! without waiting for a response. Callers that need the response should use
5//! the blocking convenience methods on [`crate::Client`] (e.g. `loop_get`,
6//! `job_status`, `autopilot_status`).
7
8use serde_json::{json, Map, Value};
9
10use crate::client::Client;
11use crate::errors::Result;
12use crate::protocol::{new_disconnect, new_request_with_id, new_subscribe};
13
14/// Return the first non-empty request id from the variadic args, or empty
15/// (signalling the caller to generate one) when none is provided.
16fn opt_request_id(request_id: &[&str]) -> String {
17    request_id
18        .iter()
19        .copied()
20        .find(|s| !s.is_empty())
21        .unwrap_or_default()
22        .to_string()
23}
24
25impl Client {
26    /// Fire `slash_command` notification (Go `SendCommand` parity).
27    pub async fn send_command(&self, cmd: &str) -> Result<()> {
28        let mut params = Map::new();
29        params.insert("cmd".into(), json!(cmd));
30        self.notify("slash_command", params).await
31    }
32
33    /// Fire `disconnect` notification (Go `SendDetach` parity).
34    pub async fn send_detach(&self) -> Result<()> {
35        self.send_envelope(new_disconnect()).await
36    }
37
38    /// Send `daemon_status` request envelope (Go `SendDaemonStatus` parity).
39    pub async fn send_daemon_status(&self, request_id: &[&str]) -> Result<()> {
40        let rid = opt_request_id(request_id);
41        self.send_envelope(new_request_with_id("daemon_status", Map::new(), rid))
42            .await
43    }
44
45    /// Send `daemon_shutdown` request envelope (Go `SendDaemonShutdown` parity).
46    pub async fn send_daemon_shutdown(&self, request_id: &[&str]) -> Result<()> {
47        let rid = opt_request_id(request_id);
48        self.send_envelope(new_request_with_id("daemon_shutdown", Map::new(), rid))
49            .await
50    }
51
52    /// Send `config_get` request envelope (Go `SendConfigGet` parity).
53    pub async fn send_config_get(&self, section: &str, request_id: &[&str]) -> Result<()> {
54        let rid = opt_request_id(request_id);
55        let mut params = Map::new();
56        params.insert("section".into(), json!(section));
57        self.send_envelope(new_request_with_id("config_get", params, rid))
58            .await
59    }
60
61    /// Send `config_reload` request envelope (Go `SendConfigReload` parity).
62    pub async fn send_config_reload(&self, request_id: &[&str]) -> Result<()> {
63        let rid = opt_request_id(request_id);
64        self.send_envelope(new_request_with_id("config_reload", Map::new(), rid))
65            .await
66    }
67
68    /// Send `skills_list` request envelope (Go `SendSkillsList` parity).
69    pub async fn send_skills_list(&self, request_id: &[&str]) -> Result<()> {
70        let rid = opt_request_id(request_id);
71        self.send_envelope(new_request_with_id("skills_list", Map::new(), rid))
72            .await
73    }
74
75    /// Send `models_list` request envelope (Go `SendModelsList` parity).
76    pub async fn send_models_list(&self, request_id: &[&str]) -> Result<()> {
77        let rid = opt_request_id(request_id);
78        self.send_envelope(new_request_with_id("models_list", Map::new(), rid))
79            .await
80    }
81
82    /// Send `invoke_skill` request envelope (Go `SendInvokeSkill` parity).
83    pub async fn send_invoke_skill(
84        &self,
85        skill: &str,
86        args: &str,
87        request_id: &[&str],
88    ) -> Result<()> {
89        let rid = opt_request_id(request_id);
90        let mut params = Map::new();
91        params.insert("skill".into(), json!(skill));
92        if !args.is_empty() {
93            params.insert("args".into(), json!(args));
94        }
95        self.send_envelope(new_request_with_id("invoke_skill", params, rid))
96            .await
97    }
98
99    /// Send `mcp_status` request envelope (Go `SendMCPStatus` parity).
100    pub async fn send_mcp_status(&self, request_id: &[&str]) -> Result<()> {
101        let rid = opt_request_id(request_id);
102        self.send_envelope(new_request_with_id("mcp_status", Map::new(), rid))
103            .await
104    }
105
106    /// Send `loop_list` request envelope (Go `SendLoopList` parity).
107    pub async fn send_loop_list(
108        &self,
109        filter: Option<Map<String, Value>>,
110        limit: u32,
111        request_id: &[&str],
112    ) -> Result<()> {
113        let rid = opt_request_id(request_id);
114        let mut params = Map::new();
115        if let Some(f) = filter {
116            params.insert("filter".into(), Value::Object(f));
117        }
118        if limit > 0 {
119            params.insert("limit".into(), json!(limit));
120        }
121        self.send_envelope(new_request_with_id("loop_list", params, rid))
122            .await
123    }
124
125    /// Send `loop_get` request envelope (Go `SendLoopGet` parity).
126    pub async fn send_loop_get(
127        &self,
128        loop_id: &str,
129        verbose: bool,
130        request_id: &[&str],
131    ) -> Result<()> {
132        let rid = opt_request_id(request_id);
133        let mut params = Map::new();
134        params.insert("loop_id".into(), json!(loop_id));
135        if verbose {
136            params.insert("verbose".into(), json!(true));
137        }
138        self.send_envelope(new_request_with_id("loop_get", params, rid))
139            .await
140    }
141
142    /// Send `loop_tree` request envelope (Go `SendLoopTree` parity).
143    pub async fn send_loop_tree(
144        &self,
145        loop_id: &str,
146        format: &str,
147        request_id: &[&str],
148    ) -> Result<()> {
149        let rid = opt_request_id(request_id);
150        let mut params = Map::new();
151        params.insert("loop_id".into(), json!(loop_id));
152        if !format.is_empty() {
153            params.insert("format".into(), json!(format));
154        }
155        self.send_envelope(new_request_with_id("loop_tree", params, rid))
156            .await
157    }
158
159    /// Send `loop_prune` request envelope (Go `SendLoopPrune` parity).
160    pub async fn send_loop_prune(
161        &self,
162        loop_id: &str,
163        keep_latest: u32,
164        request_id: &[&str],
165    ) -> Result<()> {
166        let rid = opt_request_id(request_id);
167        let mut params = Map::new();
168        params.insert("loop_id".into(), json!(loop_id));
169        if keep_latest > 0 {
170            params.insert("keep_latest".into(), json!(keep_latest));
171        }
172        self.send_envelope(new_request_with_id("loop_prune", params, rid))
173            .await
174    }
175
176    /// Send `loop_delete` request envelope (Go `SendLoopDelete` parity).
177    pub async fn send_loop_delete(&self, loop_id: &str, request_id: &[&str]) -> Result<()> {
178        let rid = opt_request_id(request_id);
179        let mut params = Map::new();
180        params.insert("loop_id".into(), json!(loop_id));
181        self.send_envelope(new_request_with_id("loop_delete", params, rid))
182            .await
183    }
184
185    /// Send `loop_reattach` request envelope (Go `SendLoopReattach` parity).
186    pub async fn send_loop_reattach(&self, loop_id: &str, request_id: &[&str]) -> Result<()> {
187        let rid = opt_request_id(request_id);
188        let mut params = Map::new();
189        params.insert("loop_id".into(), json!(loop_id));
190        self.send_envelope(new_request_with_id("loop_reattach", params, rid))
191            .await
192    }
193
194    /// Send `loop_events` subscribe envelope (Go `SendLoopSubscribe` parity).
195    ///
196    /// Pass empty `wire_tier` / `stream_delivery` to omit those fields.
197    pub async fn send_loop_subscribe(
198        &self,
199        loop_id: &str,
200        wire_tier: &str,
201        stream_delivery: &str,
202        request_id: &[&str],
203    ) -> Result<()> {
204        let rid = opt_request_id(request_id);
205        let mut params = Map::new();
206        params.insert("loop_id".into(), json!(loop_id));
207        if !wire_tier.is_empty() {
208            params.insert("wire_tier".into(), json!(wire_tier));
209        }
210        if !stream_delivery.is_empty() {
211            params.insert("stream_delivery".into(), json!(stream_delivery));
212        }
213        let env = if rid.is_empty() {
214            new_subscribe("loop_events", params)
215        } else {
216            // Explicit id: build subscribe with that id for caller correlation.
217            let mut e = new_subscribe("loop_events", params);
218            e.id = Some(rid);
219            e
220        };
221        self.send_envelope(env).await
222    }
223
224    /// Send unsubscribe by subscription id (Go `SendLoopDetach` parity).
225    ///
226    /// When `request_id` is empty, `loop_id` is used as the subscription id.
227    pub async fn send_loop_detach(&self, loop_id: &str, request_id: &[&str]) -> Result<()> {
228        let rid = opt_request_id(request_id);
229        let id = if rid.is_empty() {
230            loop_id.to_string()
231        } else {
232            rid
233        };
234        self.unsubscribe(&id).await
235    }
236
237    /// Send `loop_new` request envelope (Go `SendLoopNew` parity).
238    pub async fn send_loop_new(
239        &self,
240        client_workspace: &str,
241        user_id: &str,
242        client_workspace_id: &str,
243        is_ephemeral: bool,
244        request_id: &[&str],
245    ) -> Result<()> {
246        let rid = opt_request_id(request_id);
247        let mut params = Map::new();
248        if !client_workspace.is_empty() {
249            params.insert("client_workspace".into(), json!(client_workspace));
250        }
251        if !user_id.is_empty() {
252            params.insert("user_id".into(), json!(user_id));
253        }
254        if !client_workspace_id.is_empty() {
255            params.insert("client_workspace_id".into(), json!(client_workspace_id));
256        }
257        if is_ephemeral {
258            params.insert("is_ephemeral".into(), json!(true));
259        }
260        self.send_envelope(new_request_with_id("loop_new", params, rid))
261            .await
262    }
263
264    /// Send `loop_input` notification (Go `SendLoopInput` parity).
265    pub async fn send_loop_input(&self, loop_id: &str, content: &str) -> Result<()> {
266        let mut params = Map::new();
267        params.insert("loop_id".into(), json!(loop_id));
268        params.insert("content".into(), json!(content));
269        self.notify("loop_input", params).await
270    }
271
272    /// Send `loop_messages` request envelope (Go `SendLoopMessages` parity).
273    pub async fn send_loop_messages(
274        &self,
275        loop_id: &str,
276        limit: u32,
277        offset: u32,
278        include_events: bool,
279        request_id: &[&str],
280    ) -> Result<()> {
281        let rid = opt_request_id(request_id);
282        let mut params = Map::new();
283        params.insert("loop_id".into(), json!(loop_id));
284        if limit > 0 {
285            params.insert("limit".into(), json!(limit));
286        }
287        if offset > 0 {
288            params.insert("offset".into(), json!(offset));
289        }
290        if include_events {
291            params.insert("include_events".into(), json!(true));
292        }
293        self.send_envelope(new_request_with_id("loop_messages", params, rid))
294            .await
295    }
296
297    /// Send `loop_state_get` request envelope (Go `SendLoopStateGet` parity).
298    pub async fn send_loop_state_get(&self, loop_id: &str, request_id: &[&str]) -> Result<()> {
299        let rid = opt_request_id(request_id);
300        let mut params = Map::new();
301        params.insert("loop_id".into(), json!(loop_id));
302        self.send_envelope(new_request_with_id("loop_state_get", params, rid))
303            .await
304    }
305
306    /// Send `loop_state_update` request envelope (Go `SendLoopStateUpdate` parity).
307    pub async fn send_loop_state_update(
308        &self,
309        loop_id: &str,
310        values: Map<String, Value>,
311        as_node: &str,
312        request_id: &[&str],
313    ) -> Result<()> {
314        let rid = opt_request_id(request_id);
315        let mut params = Map::new();
316        params.insert("loop_id".into(), json!(loop_id));
317        params.insert("values".into(), Value::Object(values));
318        if !as_node.is_empty() {
319            params.insert("as_node".into(), json!(as_node));
320        }
321        self.send_envelope(new_request_with_id("loop_state_update", params, rid))
322            .await
323    }
324
325    /// Send `loop_cards_fetch` request envelope (Go `SendLoopCardsFetch` parity).
326    pub async fn send_loop_cards_fetch(&self, loop_id: &str, request_id: &[&str]) -> Result<()> {
327        let rid = opt_request_id(request_id);
328        let mut params = Map::new();
329        params.insert("loop_id".into(), json!(loop_id));
330        self.send_envelope(new_request_with_id("loop_cards_fetch", params, rid))
331            .await
332    }
333
334    /// Send `loop_history_fetch` request envelope (Go `SendLoopHistoryFetch` parity).
335    pub async fn send_loop_history_fetch(&self, loop_id: &str, request_id: &[&str]) -> Result<()> {
336        let rid = opt_request_id(request_id);
337        let mut params = Map::new();
338        params.insert("loop_id".into(), json!(loop_id));
339        self.send_envelope(new_request_with_id("loop_history_fetch", params, rid))
340            .await
341    }
342
343    /// Send `auth` request envelope (Go `SendAuth` parity).
344    pub async fn send_auth(
345        &self,
346        access_key: &str,
347        secret_key: &str,
348        request_id: &[&str],
349    ) -> Result<()> {
350        let rid = opt_request_id(request_id);
351        let mut params = Map::new();
352        params.insert("access_key".into(), json!(access_key));
353        params.insert("secret_key".into(), json!(secret_key));
354        self.send_envelope(new_request_with_id("auth", params, rid))
355            .await
356    }
357
358    /// Send `auth_refresh` request envelope (Go `SendAuthRefresh` parity).
359    pub async fn send_auth_refresh(&self, refresh_token: &str, request_id: &[&str]) -> Result<()> {
360        let rid = opt_request_id(request_id);
361        let mut params = Map::new();
362        params.insert("refresh_token".into(), json!(refresh_token));
363        self.send_envelope(new_request_with_id("auth_refresh", params, rid))
364            .await
365    }
366
367    /// Send `cron_add` request envelope (Go `SendCronAdd` parity).
368    pub async fn send_cron_add(
369        &self,
370        text: &str,
371        priority: i32,
372        request_id: &[&str],
373    ) -> Result<()> {
374        let rid = opt_request_id(request_id);
375        let mut params = Map::new();
376        params.insert("text".into(), json!(text));
377        if priority > 0 {
378            params.insert("priority".into(), json!(priority));
379        }
380        self.send_envelope(new_request_with_id("cron_add", params, rid))
381            .await
382    }
383
384    /// Send `cron_list` request envelope (Go `SendCronList` parity).
385    pub async fn send_cron_list(&self, status: &str, request_id: &[&str]) -> Result<()> {
386        let rid = opt_request_id(request_id);
387        let mut params = Map::new();
388        if !status.is_empty() {
389            params.insert("status".into(), json!(status));
390        }
391        self.send_envelope(new_request_with_id("cron_list", params, rid))
392            .await
393    }
394
395    /// Send `cron_show` request envelope (Go `SendCronShow` parity).
396    pub async fn send_cron_show(&self, job_id: &str, request_id: &[&str]) -> Result<()> {
397        let rid = opt_request_id(request_id);
398        let mut params = Map::new();
399        params.insert("job_id".into(), json!(job_id));
400        self.send_envelope(new_request_with_id("cron_show", params, rid))
401            .await
402    }
403
404    /// Send `cron_cancel` request envelope (Go `SendCronCancel` parity).
405    pub async fn send_cron_cancel(&self, job_id: &str, request_id: &[&str]) -> Result<()> {
406        let rid = opt_request_id(request_id);
407        let mut params = Map::new();
408        params.insert("job_id".into(), json!(job_id));
409        self.send_envelope(new_request_with_id("cron_cancel", params, rid))
410            .await
411    }
412
413    /// Fire-and-forget `job_create` (Go `SendJobCreate` parity).
414    pub async fn send_job_create(
415        &self,
416        goal: &str,
417        workspace: Option<&str>,
418        request_id: &[&str],
419    ) -> Result<()> {
420        if goal.is_empty() {
421            return Err(crate::errors::Error::msg("goal is required"));
422        }
423        let rid = opt_request_id(request_id);
424        let mut params = Map::new();
425        params.insert("goal".into(), json!(goal));
426        if let Some(ws) = workspace {
427            if !ws.is_empty() {
428                params.insert("workspace".into(), json!(ws));
429            }
430        }
431        self.send_envelope(new_request_with_id("job_create", params, rid))
432            .await
433    }
434
435    /// Fire-and-forget `job_status` (Go `SendJobStatus` parity).
436    pub async fn send_job_status(&self, job_id: &str, request_id: &[&str]) -> Result<()> {
437        if job_id.is_empty() {
438            return Err(crate::errors::Error::msg("job_id is required"));
439        }
440        let rid = opt_request_id(request_id);
441        let mut params = Map::new();
442        params.insert("job_id".into(), json!(job_id));
443        self.send_envelope(new_request_with_id("job_status", params, rid))
444            .await
445    }
446
447    /// Fire-and-forget `job_pause` (Go `SendJobPause` parity).
448    pub async fn send_job_pause(&self, job_id: &str, request_id: &[&str]) -> Result<()> {
449        if job_id.is_empty() {
450            return Err(crate::errors::Error::msg("job_id is required"));
451        }
452        let rid = opt_request_id(request_id);
453        let mut params = Map::new();
454        params.insert("job_id".into(), json!(job_id));
455        self.send_envelope(new_request_with_id("job_pause", params, rid))
456            .await
457    }
458
459    /// Fire-and-forget `job_resume` (Go `SendJobResume` parity).
460    pub async fn send_job_resume(&self, job_id: &str, request_id: &[&str]) -> Result<()> {
461        if job_id.is_empty() {
462            return Err(crate::errors::Error::msg("job_id is required"));
463        }
464        let rid = opt_request_id(request_id);
465        let mut params = Map::new();
466        params.insert("job_id".into(), json!(job_id));
467        self.send_envelope(new_request_with_id("job_resume", params, rid))
468            .await
469    }
470
471    /// Fire-and-forget `job_cancel` (Go `SendJobCancel` parity).
472    pub async fn send_job_cancel(&self, job_id: &str, request_id: &[&str]) -> Result<()> {
473        if job_id.is_empty() {
474            return Err(crate::errors::Error::msg("job_id is required"));
475        }
476        let rid = opt_request_id(request_id);
477        let mut params = Map::new();
478        params.insert("job_id".into(), json!(job_id));
479        self.send_envelope(new_request_with_id("job_cancel", params, rid))
480            .await
481    }
482
483    /// Fire-and-forget `job_dag` (Go `SendJobDag` parity).
484    pub async fn send_job_dag(&self, job_id: &str, request_id: &[&str]) -> Result<()> {
485        if job_id.is_empty() {
486            return Err(crate::errors::Error::msg("job_id is required"));
487        }
488        let rid = opt_request_id(request_id);
489        let mut params = Map::new();
490        params.insert("job_id".into(), json!(job_id));
491        self.send_envelope(new_request_with_id("job_dag", params, rid))
492            .await
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn opt_request_id_picks_first_non_empty() {
502        assert_eq!(opt_request_id(&[]), "");
503        assert_eq!(opt_request_id(&[""]), "");
504        assert_eq!(opt_request_id(&["abc"]), "abc");
505        assert_eq!(opt_request_id(&["", "def"]), "def");
506    }
507}