1use 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
23fn 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}