Skip to main content

opencode_ctl/tools/
session.rs

1//! Basic `session_*` tools: list, create, get, interrupt, delete.
2
3use std::path::Path;
4
5use serde_json::{json, Map, Value};
6
7use crate::clock::Clock;
8use crate::compact::{compact_form_field, compact_permission};
9use crate::engine::Engine;
10use crate::error::{ErrorCode, ToolError, ToolResult};
11use crate::host::Host;
12use crate::settings::PermissionPolicy;
13use crate::transport::Transport;
14
15use super::{
16    SessionCreateArgs, SessionDeleteArgs, SessionGetArgs, SessionInterruptArgs, SessionListArgs,
17};
18
19fn field(v: &Value, ptr: &str) -> Value {
20    v.pointer(ptr).cloned().unwrap_or(Value::Null)
21}
22
23/// `provider/model[#variant]` -> `Model.Ref`; the provider ends at the first `/`, the variant starts at the first `#`.
24fn parse_model(raw: &str) -> Result<Value, ToolError> {
25    let bad = || {
26        ToolError::new(
27            ErrorCode::InvalidArgument,
28            format!("model '{raw}' is not provider/model[#variant]"),
29        )
30    };
31    let (provider, rest) = raw.split_once('/').ok_or_else(bad)?;
32    let (id, variant) = match rest.split_once('#') {
33        Some((id, v)) => (id, Some(v)),
34        None => (rest, None),
35    };
36    if provider.is_empty() || id.is_empty() || variant == Some("") {
37        return Err(bad());
38    }
39    let mut m = Map::new();
40    m.insert("providerID".into(), json!(provider));
41    m.insert("id".into(), json!(id));
42    if let Some(v) = variant {
43        m.insert("variant".into(), json!(v));
44    }
45    Ok(Value::Object(m))
46}
47
48fn compact_form(raw: &Value) -> Value {
49    let mut o = Map::new();
50    for k in ["id", "title", "metadata"] {
51        if let Some(v) = raw.get(k) {
52            o.insert(k.to_string(), v.clone());
53        }
54    }
55    if let Some(fields) = raw.get("fields").and_then(Value::as_array) {
56        o.insert(
57            "fields".into(),
58            Value::Array(fields.iter().map(compact_form_field).collect()),
59        );
60    }
61    Value::Object(o)
62}
63
64impl<T: Transport, C: Clock> Engine<T, C> {
65    pub async fn session_list(&self, a: SessionListArgs) -> ToolResult {
66        let mut params = Vec::new();
67        if !a.all.unwrap_or(false) {
68            let dir = self.resolve_directory(a.directory.as_deref().map(Path::new));
69            params.push(("directory".to_string(), dir.to_string_lossy().to_string()));
70        }
71        if let Some(s) = a.search {
72            params.push(("search".into(), s));
73        }
74        params.push((
75            "limit".into(),
76            a.limit.unwrap_or(20).clamp(1, 200).to_string(),
77        ));
78        if let Some(c) = a.cursor {
79            params.push(("cursor".into(), c));
80        }
81        let (data, cursor, _) = self.call_json("session.list", params, None).await?;
82        let sessions: Vec<Value> = data
83            .as_array()
84            .map(|a| {
85                a.iter()
86                    .map(|s| {
87                        json!({
88                            "id": field(s, "/id"),
89                            "title": field(s, "/title"),
90                            "directory": field(s, "/location/directory"),
91                            "outcome": field(s, "/outcome"),
92                            "updated": field(s, "/time/updated"),
93                            "idle": field(s, "/time/idle"),
94                            "tokens": field(s, "/tokens"),
95                            "cost": field(s, "/cost"),
96                        })
97                    })
98                    .collect()
99            })
100            .unwrap_or_default();
101        let next = cursor.as_ref().map_or(Value::Null, |c| field(c, "/next"));
102        Ok(json!({"sessions": sessions, "next_cursor": next}))
103    }
104
105    pub async fn session_create(&self, a: SessionCreateArgs) -> ToolResult {
106        let locked = self.settings.permissions_locked;
107        if locked && a.agent.is_some() {
108            return Err(ToolError::new(
109                ErrorCode::PermissionsLocked,
110                "permissions are locked: a per-call agent is refused",
111            ));
112        }
113        if locked && a.permissions.is_some() {
114            return Err(ToolError::new(
115                ErrorCode::PermissionsLocked,
116                "permissions are locked: per-call permissions are refused",
117            ));
118        }
119        let directory = self.resolve_directory(a.directory.as_deref().map(Path::new));
120        let mut body = Map::new();
121        if let Some(t) = a.title {
122            body.insert("title".into(), json!(t));
123        }
124        if let Some(p) = a.parent_id {
125            body.insert("parentID".into(), json!(p));
126        }
127        if let Some(agent) = a.agent.or_else(|| self.settings.default_agent.clone()) {
128            body.insert("agent".into(), json!(agent));
129        }
130        if let Some(model) = a.model.or_else(|| self.settings.default_model.clone()) {
131            body.insert("model".into(), parse_model(&model)?);
132        }
133        body.insert(
134            "location".into(),
135            json!({"directory": directory.to_string_lossy()}),
136        );
137        let effect = match self.settings.permission_policy {
138            PermissionPolicy::Inherit => None,
139            PermissionPolicy::Allow => Some("allow"),
140            PermissionPolicy::Ask => Some("ask"),
141            PermissionPolicy::Deny => Some("deny"),
142        };
143        match (a.permissions, effect) {
144            (Some(rules), _) => {
145                body.insert("permissions".into(), Value::Array(rules));
146            }
147            (None, Some(e)) => {
148                body.insert(
149                    "permissions".into(),
150                    json!([{"action": "*", "resource": "*", "effect": e}]),
151                );
152            }
153            (None, None) => {}
154        }
155        let sent = Value::Object(body);
156        let (data, _, _) = self
157            .call_json("session.create", Vec::new(), Some(sent.clone()))
158            .await?;
159        let pick = |ptr: &str, fallback: &str| {
160            data.pointer(ptr)
161                .cloned()
162                .or_else(|| sent.pointer(fallback).cloned())
163                .unwrap_or(Value::Null)
164        };
165        if self.host == Host::Opencode {
166            if let Some(new_id) = data.get("id").and_then(Value::as_str) {
167                self.record_created(&directory, new_id).await?;
168            }
169        }
170        Ok(json!({
171            "session_id": field(&data, "/id"),
172            "directory": pick("/location/directory", "/location/directory"),
173            "agent": pick("/agent", "/agent"),
174            "model": pick("/model", "/model"),
175        }))
176    }
177
178    pub async fn session_get(&self, a: SessionGetArgs) -> ToolResult {
179        let id = self.resolve_session(a.session_id.as_deref())?;
180        let p = || vec![("sessionID".to_string(), id.clone())];
181        let (s, _, _) = self.call_json("session.get", p(), None).await?;
182        let (perms, _, _) = self.call_json("session.permission.list", p(), None).await?;
183        let (forms, _, _) = self.call_json("session.form.list", p(), None).await?;
184        let list = |v: &Value| v.as_array().cloned().unwrap_or_default();
185        let mut out = Map::new();
186        out.insert("id".into(), field(&s, "/id"));
187        out.insert("title".into(), field(&s, "/title"));
188        out.insert("directory".into(), field(&s, "/location/directory"));
189        out.insert("outcome".into(), field(&s, "/outcome"));
190        for k in ["agent", "model"] {
191            if let Some(v) = s.get(k) {
192                out.insert(k.into(), v.clone());
193            }
194        }
195        out.insert("tokens".into(), field(&s, "/tokens"));
196        out.insert("cost".into(), field(&s, "/cost"));
197        out.insert("time".into(), field(&s, "/time"));
198        out.insert(
199            "pending_permissions".into(),
200            Value::Array(list(&perms).iter().map(compact_permission).collect()),
201        );
202        out.insert(
203            "pending_forms".into(),
204            Value::Array(list(&forms).iter().map(compact_form).collect()),
205        );
206        Ok(Value::Object(out))
207    }
208
209    pub async fn session_interrupt(&self, a: SessionInterruptArgs) -> ToolResult {
210        let id = self.resolve_session(a.session_id.as_deref())?;
211        self.guard_created(&id)?;
212        let params = vec![
213            ("sessionID".to_string(), id),
214            ("resume".to_string(), a.resume.unwrap_or(false).to_string()),
215        ];
216        let (data, _, _) = self.call_json("session.interrupt", params, None).await?;
217        Ok(
218            json!({"interrupted": data.get("interrupted").and_then(Value::as_bool).unwrap_or(false)}),
219        )
220    }
221
222    pub async fn session_delete(&self, a: SessionDeleteArgs) -> ToolResult {
223        if !a.confirm {
224            return Err(ToolError::new(
225                ErrorCode::ConfirmRequired,
226                "session_delete needs confirm: true",
227            ));
228        }
229        let id = self.resolve_session(Some(&a.session_id))?;
230        self.guard_created(&id)?;
231        self.call_json("session.remove", vec![("sessionID".to_string(), id)], None)
232            .await?;
233        Ok(json!({"deleted": true}))
234    }
235}