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
14use super::chunk_filter::should_drop_stream_chunk_early;
15use super::observability::TurnEventStats;
16
17pub const DEFAULT_POST_IDLE_DRAIN: Duration = Duration::from_millis(500);
19
20pub type EarlyDropFn = Arc<dyn Fn(&[Value], &str, &Value) -> bool + Send + Sync>;
22
23#[derive(Clone)]
25pub struct DaemonSessionOptions {
26 pub workspace: Option<String>,
28 pub stream_delivery: String,
30 pub post_idle_drain: Duration,
32 pub early_drop_fn: Option<EarlyDropFn>,
34}
35
36impl Default for DaemonSessionOptions {
37 fn default() -> Self {
38 Self {
39 workspace: None,
40 stream_delivery: "adaptive".into(),
41 post_idle_drain: DEFAULT_POST_IDLE_DRAIN,
42 early_drop_fn: None,
43 }
44 }
45}
46
47impl std::fmt::Debug for DaemonSessionOptions {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("DaemonSessionOptions")
50 .field("workspace", &self.workspace)
51 .field("stream_delivery", &self.stream_delivery)
52 .field("post_idle_drain", &self.post_idle_drain)
53 .field(
54 "early_drop_fn",
55 &self.early_drop_fn.as_ref().map(|_| "<fn>"),
56 )
57 .finish()
58 }
59}
60
61#[derive(Debug, Clone, Default)]
63pub struct SendTurnOptions {
64 pub autonomous: bool,
66 pub max_iterations: Option<u32>,
68 pub preferred_subagent: Option<String>,
70 pub model: Option<String>,
72 pub model_params: Option<Value>,
74 pub attachments: Option<Value>,
76 pub clarification_mode: Option<String>,
78 pub clarification_answer: bool,
80 pub intent_hint: Option<String>,
82}
83
84#[derive(Debug, Clone)]
86pub struct TurnChunk {
87 pub namespace: Value,
89 pub mode: String,
91 pub data: Value,
93}
94
95pub struct DaemonSession {
97 opts: DaemonSessionOptions,
98 client: Client,
99 rpc_client: Client,
100 rpc_connected: Mutex<bool>,
101 loop_id: Mutex<String>,
102 read_lock: Mutex<()>,
103 early_drop_fn: EarlyDropFn,
104 pub turn_event_stats: Mutex<TurnEventStats>,
106 pub last_turn_end_state: Mutex<String>,
108 pub last_turn_cancel_seen: Mutex<bool>,
110 pub last_turn_error_message: Mutex<String>,
112}
113
114impl DaemonSession {
115 pub fn new(ws_url: impl Into<String>, opts: Option<DaemonSessionOptions>) -> Self {
117 let ws_url = ws_url.into();
118 let opts = opts.unwrap_or_default();
119 let early_drop_fn = opts.early_drop_fn.clone().unwrap_or_else(|| {
120 Arc::new(|ns: &[Value], mode: &str, data: &Value| {
121 should_drop_stream_chunk_early(ns, mode, data)
122 })
123 });
124 Self {
125 client: Client::new(&ws_url),
126 rpc_client: Client::new(&ws_url),
127 rpc_connected: Mutex::new(false),
128 loop_id: Mutex::new(String::new()),
129 read_lock: Mutex::new(()),
130 early_drop_fn,
131 turn_event_stats: Mutex::new(TurnEventStats::new()),
132 last_turn_end_state: Mutex::new(String::new()),
133 last_turn_cancel_seen: Mutex::new(false),
134 last_turn_error_message: Mutex::new(String::new()),
135 opts,
136 }
137 }
138
139 pub fn stream_client(&self) -> &Client {
141 &self.client
142 }
143
144 pub fn rpc_client(&self) -> &Client {
146 &self.rpc_client
147 }
148
149 pub async fn loop_id(&self) -> String {
151 self.loop_id.lock().await.clone()
152 }
153
154 pub async fn connect(&self, resume_loop_id: Option<&str>) -> Result<Map<String, Value>> {
156 connect_with_retries(&self.client, 40, Duration::from_millis(250)).await?;
157 self.bootstrap_loop(resume_loop_id).await
158 }
159
160 async fn bootstrap_loop(&self, resume_loop_id: Option<&str>) -> Result<Map<String, Value>> {
161 let mut boot = BootstrapOptions::new();
162 boot.resume_loop_id = resume_loop_id.map(|s| s.to_string());
163 boot.workspace = self.opts.workspace.clone();
164 boot.stream_delivery = self.opts.stream_delivery.clone();
165 let ready = bootstrap_loop_session(&self.client, boot, None).await?;
166 if let Some(lid) = ready.get("loop_id").and_then(|v| v.as_str()) {
167 *self.loop_id.lock().await = lid.to_string();
168 }
169 Ok(ready)
170 }
171
172 pub async fn new_loop(&self) -> Result<Map<String, Value>> {
174 self.bootstrap_loop(None).await
175 }
176
177 pub async fn switch_loop(&self, loop_id: &str) -> Result<Map<String, Value>> {
179 self.bootstrap_loop(Some(loop_id)).await
180 }
181
182 pub async fn ensure_connected(&self) -> Result<()> {
184 if self.client.is_connection_alive() {
185 return Ok(());
186 }
187 connect_with_retries(&self.client, 40, Duration::from_millis(250)).await?;
188 let lid = self.loop_id().await;
189 if lid.is_empty() {
190 self.bootstrap_loop(None).await?;
191 return Ok(());
192 }
193 match self.client.reattach_and_probe(&lid).await {
194 Ok(()) => Ok(()),
195 Err(Error::StaleLoop(_)) | Err(_) => {
196 let _ = self.rpc_client.close().await;
198 *self.rpc_connected.lock().await = false;
199 self.bootstrap_loop(None).await?;
200 Ok(())
201 }
202 }
203 }
204
205 pub async fn close(&self) -> Result<()> {
207 let _ = self.client.close().await;
208 let _ = self.rpc_client.close().await;
209 *self.rpc_connected.lock().await = false;
210 Ok(())
211 }
212
213 pub async fn detach(&self) -> Result<()> {
215 self.client.notify("disconnect", Map::new()).await
216 }
217
218 pub async fn send_turn(&self, text: &str, opts: Option<SendTurnOptions>) -> Result<()> {
220 let loop_id = self.loop_id().await;
221 if loop_id.is_empty() {
222 return Err(Error::msg("no active loop session"));
223 }
224 let opts = opts.unwrap_or_default();
225 let input = SendInputOptions {
226 loop_id: Some(loop_id),
227 autonomous: opts.autonomous,
228 max_iterations: opts.max_iterations,
229 preferred_subagent: opts.preferred_subagent,
230 model: opts.model,
231 model_params: opts.model_params,
232 attachments: opts.attachments,
233 clarification_mode: opts.clarification_mode,
234 clarification_answer: opts.clarification_answer,
235 intent_hint: opts.intent_hint,
236 ..Default::default()
237 };
238 self.client.send_input(text, input).await
239 }
240
241 pub async fn cancel_active_turn(&self) -> Result<()> {
243 let mut params = Map::new();
244 params.insert("cmd".into(), json!("/cancel"));
245 self.client.notify("slash_command", params).await
246 }
247
248 async fn ensure_rpc_connected(&self) -> Result<()> {
249 let mut flag = self.rpc_connected.lock().await;
250 if *flag && self.rpc_client.is_connected() {
251 return Ok(());
252 }
253 connect_with_retries(&self.rpc_client, 5, Duration::from_millis(250)).await?;
254 *flag = true;
255 Ok(())
256 }
257
258 pub async fn list_loops(&self, limit: u32) -> Result<Map<String, Value>> {
260 self.ensure_rpc_connected().await?;
261 let lim = if limit == 0 { 20 } else { limit };
262 self.rpc_client.loop_list(lim).await
263 }
264
265 pub async fn fetch_loop_cards(&self, loop_id: &str) -> Result<Map<String, Value>> {
267 self.ensure_rpc_connected().await?;
268 self.rpc_client.loop_cards_fetch(loop_id).await
269 }
270
271 pub async fn fetch_loop_history(&self, loop_id: &str) -> Result<Map<String, Value>> {
273 self.ensure_rpc_connected().await?;
274 self.rpc_client.loop_history_fetch(loop_id).await
275 }
276
277 pub async fn iter_turn_chunks(&self, max_wait: Option<Duration>) -> Result<Vec<TurnChunk>> {
281 let _guard = self.read_lock.lock().await;
282 self.iter_turn_chunks_locked(max_wait).await
283 }
284
285 async fn iter_turn_chunks_locked(&self, max_wait: Option<Duration>) -> Result<Vec<TurnChunk>> {
286 *self.last_turn_end_state.lock().await = String::new();
287 *self.last_turn_error_message.lock().await = String::new();
288 *self.last_turn_cancel_seen.lock().await = false;
289 *self.turn_event_stats.lock().await = TurnEventStats::new();
290
291 let mut out = Vec::new();
292 let mut query_started = false;
293 let mut expected_loop_id = self.loop_id().await;
294 let mut stream_payload_seen = false;
295 let mut turn_progress_seen = false;
296 let mut cancel_seen = false;
297 let absolute_deadline = max_wait.map(|d| tokio::time::Instant::now() + d);
298
299 let _ = self.client.peel_stale_pending_control_events().await;
300
301 loop {
302 if let Some(deadline) = absolute_deadline {
303 if tokio::time::Instant::now() > deadline {
304 let err = format!(
305 "turn timed out after {:?} (loop={})",
306 max_wait.unwrap_or_default(),
307 expected_loop_id
308 );
309 *self.last_turn_error_message.lock().await = err.clone();
310 return Err(Error::msg(err));
311 }
312 }
313
314 let ev = self
315 .client
316 .read_event_with_timeout(Duration::from_millis(250))
317 .await?;
318 let Some(ev) = ev else {
319 if query_started && !self.client.is_connection_alive() {
320 *self.last_turn_end_state.lock().await = "connection_lost".into();
321 return Err(Error::msg("daemon connection lost"));
322 }
323 continue;
325 };
326
327 let mut frame = ev;
328 let mut event_type = frame
329 .get("type")
330 .and_then(|v| v.as_str())
331 .unwrap_or("")
332 .to_string();
333 if event_type == "next" {
334 frame = unwrap_next_frame(&frame);
335 event_type = frame
336 .get("type")
337 .and_then(|v| v.as_str())
338 .unwrap_or("")
339 .to_string();
340 }
341
342 let event_loop_id = frame
343 .get("loop_id")
344 .and_then(|v| v.as_str())
345 .unwrap_or("")
346 .to_string();
347 if !expected_loop_id.is_empty()
348 && !event_loop_id.is_empty()
349 && event_loop_id != expected_loop_id
350 {
351 continue;
352 }
353
354 if event_type == "error" {
355 let msg = frame
356 .get("error")
357 .and_then(|e| e.get("message"))
358 .and_then(|m| m.as_str())
359 .or_else(|| frame.get("message").and_then(|m| m.as_str()))
360 .unwrap_or("daemon error")
361 .to_string();
362 *self.last_turn_error_message.lock().await = msg.clone();
363 return Err(Error::msg(msg));
364 }
365
366 if event_type == "status" {
367 if let Some(lid) = frame.get("loop_id").and_then(|v| v.as_str()) {
368 if !lid.is_empty() {
369 *self.loop_id.lock().await = lid.to_string();
370 expected_loop_id = lid.to_string();
371 }
372 }
373 let state = frame.get("state").and_then(|v| v.as_str()).unwrap_or("");
374 match state {
375 "running" => query_started = true,
376 "stopped" if query_started => {
377 *self.last_turn_end_state.lock().await = state.into();
378 self.drain_after_idle(&expected_loop_id, &mut out).await;
379 return Ok(out);
380 }
381 "idle" if query_started => {
382 if !stream_payload_seen && !cancel_seen {
383 continue;
384 }
385 *self.last_turn_end_state.lock().await = state.into();
386 self.drain_after_idle(&expected_loop_id, &mut out).await;
387 return Ok(out);
388 }
389 _ => {}
390 }
391 continue;
392 }
393
394 if event_type == "command_response" {
395 let content = frame.get("content").and_then(|v| v.as_str()).unwrap_or("");
396 if content.contains("Cancellation requested") {
397 cancel_seen = true;
398 *self.last_turn_cancel_seen.lock().await = true;
399 }
400 continue;
401 }
402
403 if event_type != "event" {
404 continue;
405 }
406
407 let data = frame.get("data").cloned().unwrap_or(Value::Null);
408 let namespace = frame
409 .get("namespace")
410 .cloned()
411 .unwrap_or(Value::Array(vec![]));
412 let mode = frame
413 .get("mode")
414 .and_then(|v| v.as_str())
415 .unwrap_or("")
416 .to_string();
417
418 let ns_slice: Vec<Value> = match &namespace {
419 Value::Array(a) => a.clone(),
420 _ => vec![],
421 };
422 if (self.early_drop_fn)(&ns_slice, &mode, &data) {
423 self.turn_event_stats.lock().await.filtered_early += 1;
424 continue;
425 }
426
427 if mode == "custom"
428 && is_turn_end_custom_data(&data)
429 && (!query_started || !turn_progress_seen)
430 {
431 continue;
432 }
433
434 stream_payload_seen = true;
435 if is_turn_progress_chunk(&mode, &data) {
436 turn_progress_seen = true;
437 }
438
439 out.push(TurnChunk {
440 namespace,
441 mode: mode.clone(),
442 data: data.clone(),
443 });
444
445 if mode == "custom" && is_turn_end_custom_data(&data) {
446 let custom_type = data.get("type").and_then(|v| v.as_str()).unwrap_or("");
447 *self.last_turn_end_state.lock().await = if custom_type == STREAM_END {
448 "stream_end".into()
449 } else {
450 "completed".into()
451 };
452 self.drain_after_idle(&expected_loop_id, &mut out).await;
453 return Ok(out);
454 }
455 }
456 }
457
458 async fn drain_after_idle(&self, expected_loop_id: &str, out: &mut Vec<TurnChunk>) {
459 let deadline = tokio::time::Instant::now() + self.opts.post_idle_drain;
460 while tokio::time::Instant::now() < deadline {
461 let ev = match self
462 .client
463 .read_event_with_timeout(Duration::from_millis(250))
464 .await
465 {
466 Ok(Some(e)) => e,
467 _ => return,
468 };
469 let mut frame = ev;
470 let mut event_type = frame
471 .get("type")
472 .and_then(|v| v.as_str())
473 .unwrap_or("")
474 .to_string();
475 if event_type == "next" {
476 frame = unwrap_next_frame(&frame);
477 event_type = frame
478 .get("type")
479 .and_then(|v| v.as_str())
480 .unwrap_or("")
481 .to_string();
482 }
483 let event_loop_id = frame.get("loop_id").and_then(|v| v.as_str()).unwrap_or("");
484 if !expected_loop_id.is_empty()
485 && !event_loop_id.is_empty()
486 && event_loop_id != expected_loop_id
487 {
488 continue;
489 }
490 if event_type == "error" {
491 return;
492 }
493 if event_type == "status" {
494 if let Some(lid) = frame.get("loop_id").and_then(|v| v.as_str()) {
495 if !lid.is_empty() {
496 *self.loop_id.lock().await = lid.to_string();
497 }
498 }
499 continue;
500 }
501 if event_type != "event" {
502 continue;
503 }
504 let data = frame.get("data").cloned().unwrap_or(Value::Null);
505 let namespace = frame
506 .get("namespace")
507 .cloned()
508 .unwrap_or(Value::Array(vec![]));
509 let mode = frame
510 .get("mode")
511 .and_then(|v| v.as_str())
512 .unwrap_or("")
513 .to_string();
514 let ns_slice: Vec<Value> = match &namespace {
515 Value::Array(a) => a.clone(),
516 _ => vec![],
517 };
518 if (self.early_drop_fn)(&ns_slice, &mode, &data) {
519 self.turn_event_stats.lock().await.filtered_early += 1;
520 continue;
521 }
522 self.turn_event_stats.lock().await.post_idle_drained += 1;
523 out.push(TurnChunk {
524 namespace,
525 mode,
526 data,
527 });
528 }
529 }
530}