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}