1use std::sync::Arc;
4use std::time::Duration;
5
6use serde_json::{json, Map, Value};
7use tokio::sync::Mutex;
8
9use crate::client::{unwrap_next_frame, Client, SendInputOptions};
10use crate::errors::{Error, Result};
11use crate::session::{bootstrap_loop_session, connect_with_retries, BootstrapOptions};
12use crate::stream_terminal::{is_turn_end_custom_data, is_turn_progress_chunk, STREAM_END};
13
14#[derive(Debug, Clone)]
16pub struct DaemonSessionOptions {
17 pub workspace: Option<String>,
19 pub stream_delivery: String,
21 pub post_idle_drain: Duration,
23}
24
25impl Default for DaemonSessionOptions {
26 fn default() -> Self {
27 Self {
28 workspace: None,
29 stream_delivery: "adaptive".into(),
30 post_idle_drain: Duration::from_millis(500),
31 }
32 }
33}
34
35#[derive(Debug, Clone, Default)]
37pub struct SendTurnOptions {
38 pub autonomous: bool,
40 pub max_iterations: Option<u32>,
42 pub preferred_subagent: Option<String>,
44 pub model: Option<String>,
46 pub model_params: Option<Value>,
48 pub attachments: Option<Value>,
50 pub clarification_mode: Option<String>,
52 pub clarification_answer: bool,
54 pub intent_hint: Option<String>,
56}
57
58#[derive(Debug, Clone)]
60pub struct TurnChunk {
61 pub namespace: Value,
63 pub mode: String,
65 pub data: Value,
67}
68
69pub struct DaemonSession {
71 opts: DaemonSessionOptions,
72 client: Client,
73 rpc_client: Client,
74 rpc_connected: Mutex<bool>,
75 loop_id: Mutex<String>,
76 read_lock: Mutex<()>,
77 pub last_turn_end_state: Mutex<String>,
79 pub last_turn_error_message: Mutex<String>,
81}
82
83impl DaemonSession {
84 pub fn new(ws_url: impl Into<String>, opts: Option<DaemonSessionOptions>) -> Self {
86 let ws_url = ws_url.into();
87 Self {
88 client: Client::new(&ws_url),
89 rpc_client: Client::new(&ws_url),
90 rpc_connected: Mutex::new(false),
91 loop_id: Mutex::new(String::new()),
92 read_lock: Mutex::new(()),
93 last_turn_end_state: Mutex::new(String::new()),
94 last_turn_error_message: Mutex::new(String::new()),
95 opts: opts.unwrap_or_default(),
96 }
97 }
98
99 pub fn stream_client(&self) -> &Client {
101 &self.client
102 }
103
104 pub async fn loop_id(&self) -> String {
106 self.loop_id.lock().await.clone()
107 }
108
109 pub async fn connect(&self, resume_loop_id: Option<&str>) -> Result<Map<String, Value>> {
111 connect_with_retries(&self.client, 40, Duration::from_millis(250)).await?;
112 self.bootstrap_loop(resume_loop_id).await
113 }
114
115 async fn bootstrap_loop(&self, resume_loop_id: Option<&str>) -> Result<Map<String, Value>> {
116 let mut boot = BootstrapOptions::new();
117 boot.resume_loop_id = resume_loop_id.map(|s| s.to_string());
118 boot.workspace = self.opts.workspace.clone();
119 boot.stream_delivery = self.opts.stream_delivery.clone();
120 let ready = bootstrap_loop_session(&self.client, boot, None).await?;
121 if let Some(lid) = ready.get("loop_id").and_then(|v| v.as_str()) {
122 *self.loop_id.lock().await = lid.to_string();
123 }
124 Ok(ready)
125 }
126
127 pub async fn new_loop(&self) -> Result<Map<String, Value>> {
129 self.bootstrap_loop(None).await
130 }
131
132 pub async fn switch_loop(&self, loop_id: &str) -> Result<Map<String, Value>> {
134 self.bootstrap_loop(Some(loop_id)).await
135 }
136
137 pub async fn ensure_connected(&self) -> Result<()> {
139 if self.client.is_connection_alive() {
140 return Ok(());
141 }
142 connect_with_retries(&self.client, 40, Duration::from_millis(250)).await?;
143 let lid = self.loop_id().await;
144 if lid.is_empty() {
145 self.bootstrap_loop(None).await?;
146 return Ok(());
147 }
148 match self.client.reattach_and_probe(&lid).await {
149 Ok(()) => Ok(()),
150 Err(Error::StaleLoop(_)) | Err(_) => {
151 let _ = self.rpc_client.close().await;
153 *self.rpc_connected.lock().await = false;
154 self.bootstrap_loop(None).await?;
155 Ok(())
156 }
157 }
158 }
159
160 pub async fn close(&self) -> Result<()> {
162 let _ = self.client.close().await;
163 let _ = self.rpc_client.close().await;
164 *self.rpc_connected.lock().await = false;
165 Ok(())
166 }
167
168 pub async fn detach(&self) -> Result<()> {
170 self.client.notify("disconnect", Map::new()).await
171 }
172
173 pub async fn send_turn(&self, text: &str, opts: Option<SendTurnOptions>) -> Result<()> {
175 let loop_id = self.loop_id().await;
176 if loop_id.is_empty() {
177 return Err(Error::msg("no active loop session"));
178 }
179 let opts = opts.unwrap_or_default();
180 let input = SendInputOptions {
181 loop_id: Some(loop_id),
182 autonomous: opts.autonomous,
183 max_iterations: opts.max_iterations,
184 preferred_subagent: opts.preferred_subagent,
185 model: opts.model,
186 model_params: opts.model_params,
187 attachments: opts.attachments,
188 clarification_mode: opts.clarification_mode,
189 clarification_answer: opts.clarification_answer,
190 intent_hint: opts.intent_hint,
191 ..Default::default()
192 };
193 self.client.send_input(text, input).await
194 }
195
196 pub async fn cancel_active_turn(&self) -> Result<()> {
198 let mut params = Map::new();
199 params.insert("cmd".into(), json!("/cancel"));
200 self.client.notify("slash_command", params).await
201 }
202
203 async fn ensure_rpc_connected(&self) -> Result<()> {
204 let mut flag = self.rpc_connected.lock().await;
205 if *flag && self.rpc_client.is_connected() {
206 return Ok(());
207 }
208 connect_with_retries(&self.rpc_client, 5, Duration::from_millis(250)).await?;
209 *flag = true;
210 Ok(())
211 }
212
213 pub async fn list_loops(&self, limit: u32) -> Result<Map<String, Value>> {
215 self.ensure_rpc_connected().await?;
216 let lim = if limit == 0 { 20 } else { limit };
217 self.rpc_client.loop_list(lim).await
218 }
219
220 pub async fn fetch_loop_cards(&self, loop_id: &str) -> Result<Map<String, Value>> {
222 self.ensure_rpc_connected().await?;
223 self.rpc_client.loop_cards_fetch(loop_id).await
224 }
225
226 pub async fn fetch_loop_history(&self, loop_id: &str) -> Result<Map<String, Value>> {
228 self.ensure_rpc_connected().await?;
229 self.rpc_client.loop_history_fetch(loop_id).await
230 }
231
232 pub async fn iter_turn_chunks(&self, max_wait: Option<Duration>) -> Result<Vec<TurnChunk>> {
236 let _guard = self.read_lock.lock().await;
237 self.iter_turn_chunks_locked(max_wait).await
238 }
239
240 async fn iter_turn_chunks_locked(&self, max_wait: Option<Duration>) -> Result<Vec<TurnChunk>> {
241 *self.last_turn_end_state.lock().await = String::new();
242 *self.last_turn_error_message.lock().await = String::new();
243
244 let mut out = Vec::new();
245 let mut query_started = false;
246 let mut expected_loop_id = self.loop_id().await;
247 let mut stream_payload_seen = false;
248 let mut turn_progress_seen = false;
249 let mut cancel_seen = false;
250 let absolute_deadline = max_wait.map(|d| tokio::time::Instant::now() + d);
251
252 let _ = self.client.peel_stale_pending_control_events().await;
253
254 loop {
255 if let Some(deadline) = absolute_deadline {
256 if tokio::time::Instant::now() > deadline {
257 let err = format!(
258 "turn timed out after {:?} (loop={})",
259 max_wait.unwrap_or_default(),
260 expected_loop_id
261 );
262 *self.last_turn_error_message.lock().await = err.clone();
263 return Err(Error::msg(err));
264 }
265 }
266
267 let ev = self
268 .client
269 .read_event_with_timeout(Duration::from_millis(250))
270 .await?;
271 let Some(ev) = ev else {
272 if query_started && !self.client.is_connection_alive() {
273 *self.last_turn_end_state.lock().await = "connection_lost".into();
274 return Err(Error::msg("daemon connection lost"));
275 }
276 continue;
278 };
279
280 let mut frame = ev;
281 let mut event_type = frame
282 .get("type")
283 .and_then(|v| v.as_str())
284 .unwrap_or("")
285 .to_string();
286 if event_type == "next" {
287 frame = unwrap_next_frame(&frame);
288 event_type = frame
289 .get("type")
290 .and_then(|v| v.as_str())
291 .unwrap_or("")
292 .to_string();
293 }
294
295 let event_loop_id = frame
296 .get("loop_id")
297 .and_then(|v| v.as_str())
298 .unwrap_or("")
299 .to_string();
300 if !expected_loop_id.is_empty()
301 && !event_loop_id.is_empty()
302 && event_loop_id != expected_loop_id
303 {
304 continue;
305 }
306
307 if event_type == "error" {
308 let msg = frame
309 .get("error")
310 .and_then(|e| e.get("message"))
311 .and_then(|m| m.as_str())
312 .or_else(|| frame.get("message").and_then(|m| m.as_str()))
313 .unwrap_or("daemon error")
314 .to_string();
315 *self.last_turn_error_message.lock().await = msg.clone();
316 return Err(Error::msg(msg));
317 }
318
319 if event_type == "status" {
320 if let Some(lid) = frame.get("loop_id").and_then(|v| v.as_str()) {
321 if !lid.is_empty() {
322 *self.loop_id.lock().await = lid.to_string();
323 expected_loop_id = lid.to_string();
324 }
325 }
326 let state = frame.get("state").and_then(|v| v.as_str()).unwrap_or("");
327 match state {
328 "running" => query_started = true,
329 "stopped" if query_started => {
330 *self.last_turn_end_state.lock().await = state.into();
331 self.drain_after_idle(&expected_loop_id, &mut out).await;
332 return Ok(out);
333 }
334 "idle" if query_started => {
335 if !stream_payload_seen && !cancel_seen {
336 continue;
337 }
338 *self.last_turn_end_state.lock().await = state.into();
339 self.drain_after_idle(&expected_loop_id, &mut out).await;
340 return Ok(out);
341 }
342 _ => {}
343 }
344 continue;
345 }
346
347 if event_type == "command_response" {
348 let content = frame.get("content").and_then(|v| v.as_str()).unwrap_or("");
349 if content.contains("Cancellation requested") {
350 cancel_seen = true;
351 }
352 continue;
353 }
354
355 if event_type != "event" {
356 continue;
357 }
358
359 let data = frame.get("data").cloned().unwrap_or(Value::Null);
360 let namespace = frame
361 .get("namespace")
362 .cloned()
363 .unwrap_or(Value::Array(vec![]));
364 let mode = frame
365 .get("mode")
366 .and_then(|v| v.as_str())
367 .unwrap_or("")
368 .to_string();
369
370 if mode == "custom"
371 && is_turn_end_custom_data(&data)
372 && (!query_started || !turn_progress_seen)
373 {
374 continue;
375 }
376
377 stream_payload_seen = true;
378 if is_turn_progress_chunk(&mode, &data) {
379 turn_progress_seen = true;
380 }
381
382 out.push(TurnChunk {
383 namespace,
384 mode: mode.clone(),
385 data: data.clone(),
386 });
387
388 if mode == "custom" && is_turn_end_custom_data(&data) {
389 let custom_type = data.get("type").and_then(|v| v.as_str()).unwrap_or("");
390 *self.last_turn_end_state.lock().await = if custom_type == STREAM_END {
391 "stream_end".into()
392 } else {
393 "completed".into()
394 };
395 self.drain_after_idle(&expected_loop_id, &mut out).await;
396 return Ok(out);
397 }
398 }
399 }
400
401 async fn drain_after_idle(&self, expected_loop_id: &str, out: &mut Vec<TurnChunk>) {
402 let deadline = tokio::time::Instant::now() + self.opts.post_idle_drain;
403 while tokio::time::Instant::now() < deadline {
404 let ev = match self
405 .client
406 .read_event_with_timeout(Duration::from_millis(250))
407 .await
408 {
409 Ok(Some(e)) => e,
410 _ => return,
411 };
412 let mut frame = ev;
413 let mut event_type = frame
414 .get("type")
415 .and_then(|v| v.as_str())
416 .unwrap_or("")
417 .to_string();
418 if event_type == "next" {
419 frame = unwrap_next_frame(&frame);
420 event_type = frame
421 .get("type")
422 .and_then(|v| v.as_str())
423 .unwrap_or("")
424 .to_string();
425 }
426 let event_loop_id = frame.get("loop_id").and_then(|v| v.as_str()).unwrap_or("");
427 if !expected_loop_id.is_empty()
428 && !event_loop_id.is_empty()
429 && event_loop_id != expected_loop_id
430 {
431 continue;
432 }
433 if event_type == "error" {
434 return;
435 }
436 if event_type == "status" {
437 if let Some(lid) = frame.get("loop_id").and_then(|v| v.as_str()) {
438 if !lid.is_empty() {
439 *self.loop_id.lock().await = lid.to_string();
440 }
441 }
442 continue;
443 }
444 if event_type != "event" {
445 continue;
446 }
447 let data = frame.get("data").cloned().unwrap_or(Value::Null);
448 let namespace = frame
449 .get("namespace")
450 .cloned()
451 .unwrap_or(Value::Array(vec![]));
452 let mode = frame
453 .get("mode")
454 .and_then(|v| v.as_str())
455 .unwrap_or("")
456 .to_string();
457 out.push(TurnChunk {
458 namespace,
459 mode,
460 data,
461 });
462 }
463 }
464}
465
466impl DaemonSession {
467 pub fn shared(ws_url: impl Into<String>, opts: Option<DaemonSessionOptions>) -> Arc<Self> {
469 Arc::new(Self::new(ws_url, opts))
470 }
471}