Skip to main content

forge_sdk/client/
control_send.rs

1//! Outbound `control_request` typed wrappers - the user-facing
2//! commands (`interrupt`, `set_permission_mode`, `set_model`,
3//! `mcp_reconnect`, `mcp_toggle`, `stop_task`, `mcp_status`,
4//! `get_context_usage`) plus the `_raw` escape hatches
5//! for `mcp_status` and `get_context_usage`.
6//!
7//! All methods take `&self` and route through
8//! [`Client::send_control`] (which lives in `client.rs`); the reader
9//! task routes the matching `control_response` back via the
10//! `pending_controls` map. Concurrent calls are safe - the writer
11//! mpsc serialises onto stdin in arrival order, and each
12//! `request_id` gets its own oneshot waiter.
13
14use crate::Error;
15use crate::client::Client;
16
17impl Client {
18    /// Ask the CLI to interrupt the current turn (cancel in-flight tool
19    /// calls and return control to the SDK).
20    ///
21    /// # Errors
22    ///
23    /// See the outbound control error cases.
24    pub async fn interrupt(&self) -> Result<(), Error> {
25        self.send_control("interrupt", serde_json::json!({})).await?;
26        Ok(())
27    }
28
29    /// Drop one prompt still sitting in the CLI's queue, by the uuid it was
30    /// sent under.
31    ///
32    /// Answers whether the CLI cancelled it. `false` means it was not in the
33    /// queue - already dequeued for execution, or never enqueued - and the
34    /// prompt is on its way to the model regardless; a malformed or missing
35    /// answer reads the same way (false), which fails closed: the prompt stays
36    /// queued rather than a caller believing it was dropped.
37    ///
38    /// # Errors
39    ///
40    /// See the outbound control error cases.
41    pub async fn cancel_queued_message(&self, message_uuid: &str) -> Result<bool, Error> {
42        let response = self
43            .send_control(
44                "cancel_async_message",
45                serde_json::json!({ "message_uuid": message_uuid }),
46            )
47            .await?;
48        Ok(response.get("cancelled").and_then(serde_json::Value::as_bool).unwrap_or(false))
49    }
50
51    /// Switch the permission mode mid-session.
52    ///
53    /// # Errors
54    ///
55    /// See the outbound control error cases.
56    pub async fn set_permission_mode(
57        &self,
58        mode: crate::options::PermissionMode,
59    ) -> Result<(), Error> {
60        self.send_control("set_permission_mode", serde_json::json!({"mode": mode.as_cli_arg()}))
61            .await?;
62        Ok(())
63    }
64
65    /// Switch the model mid-session. Pass `Some("claude-sonnet-4-6")` or
66    /// similar to pick a specific model; `None` reverts to the CLI
67    /// default. Wire shape: `{"subtype": "set_model", "model": <string or null>}`.
68    ///
69    /// # Errors
70    ///
71    /// See the outbound control error cases.
72    pub async fn set_model(&self, model: Option<&str>) -> Result<(), Error> {
73        self.send_control("set_model", serde_json::json!({"model": model})).await?;
74        Ok(())
75    }
76
77    /// Reconnect a named MCP server (asks the CLI to drop + re-establish
78    /// its connection to the named server). Wire shape uses camelCase
79    /// `serverName`.
80    ///
81    /// # Errors
82    ///
83    /// See the outbound control error cases.
84    pub async fn mcp_reconnect(&self, server_name: &str) -> Result<(), Error> {
85        self.send_control("mcp_reconnect", serde_json::json!({"serverName": server_name})).await?;
86        Ok(())
87    }
88
89    /// Toggle a named MCP server on/off. Wire shape uses camelCase
90    /// `serverName`.
91    ///
92    /// # Errors
93    ///
94    /// See the outbound control error cases.
95    pub async fn mcp_toggle(&self, server_name: &str, enabled: bool) -> Result<(), Error> {
96        self.send_control(
97            "mcp_toggle",
98            serde_json::json!({"serverName": server_name, "enabled": enabled}),
99        )
100        .await?;
101        Ok(())
102    }
103
104    /// Replace the CLI's dynamically managed MCP server set. Sdk servers
105    /// are entries of `{"type":"sdk","name":<n>}`; the CLI re-handshakes
106    /// everything it adds, so this is the one lever that revives an sdk
107    /// server whose boot-time handshake was abandoned (the CLI reports
108    /// such a server `connected` with zero tools and never re-runs the
109    /// handshake itself).
110    ///
111    /// # Errors
112    ///
113    /// See the outbound control error cases, plus [`Error::MessageParse`]
114    /// when the CLI payload doesn't match
115    /// [`McpSetServersResponse`](forge_primitives::McpSetServersResponse).
116    pub async fn mcp_set_servers(
117        &self,
118        servers: serde_json::Value,
119    ) -> Result<forge_primitives::McpSetServersResponse, Error> {
120        let raw =
121            self.send_control("mcp_set_servers", serde_json::json!({ "servers": servers })).await?;
122        serde_json::from_value(raw)
123            .map_err(|e| Error::message_parse(format!("mcp_set_servers: {e}")))
124    }
125
126    /// Kill an in-flight sub-agent task by its `task_id` (from the
127    /// `TaskStarted` system message).
128    ///
129    /// # Errors
130    ///
131    /// See the outbound control error cases.
132    pub async fn stop_task(&self, task_id: &str) -> Result<(), Error> {
133        self.send_control("stop_task", serde_json::json!({"task_id": task_id})).await?;
134        Ok(())
135    }
136
137    /// Query MCP server status. Returns the typed response.
138    ///
139    /// # Errors
140    ///
141    /// See the outbound control error cases, plus [`Error::MessageParse`]
142    /// when the CLI payload doesn't match
143    /// [`McpStatusResponse`](forge_primitives::McpStatusResponse).
144    pub async fn mcp_status(&self) -> Result<forge_primitives::McpStatusResponse, Error> {
145        let raw = self.send_control("mcp_status", serde_json::json!({})).await?;
146        serde_json::from_value(raw).map_err(|e| Error::message_parse(format!("mcp_status: {e}")))
147    }
148
149    /// Query current context usage (tokens consumed vs. budget).
150    /// Returns the typed response.
151    ///
152    /// # Errors
153    ///
154    /// See the outbound control error cases, plus [`Error::MessageParse`]
155    /// when the CLI payload doesn't match
156    /// [`ContextUsageResponse`](forge_primitives::ContextUsageResponse).
157    pub async fn get_context_usage(&self) -> Result<forge_primitives::ContextUsageResponse, Error> {
158        let raw = self.send_control("get_context_usage", serde_json::json!({})).await?;
159        serde_json::from_value(raw)
160            .map_err(|e| Error::message_parse(format!("get_context_usage: {e}")))
161    }
162
163    /// Ask the CLI to reload session plugins (slash commands, agents,
164    /// MCP servers). Wire shape: `{"subtype": "reload_plugins"}`. The
165    /// CLI replies with the refreshed plugin inventory; returns the
166    /// raw JSON for the agent layer to forward.
167    ///
168    /// # Errors
169    ///
170    /// See the outbound control error cases.
171    pub async fn reload_plugins(&self) -> Result<serde_json::Value, Error> {
172        self.send_control("reload_plugins", serde_json::json!({})).await
173    }
174}