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_history(&self, loop_id: &str) -> Result<Map<String, Value>> {
267 self.ensure_rpc_connected().await?;
268 self.rpc_client.loop_history_fetch(loop_id).await
269 }
270
271 pub async fn iter_turn_chunks(&self, max_wait: Option<Duration>) -> Result<Vec<TurnChunk>> {
275 let _guard = self.read_lock.lock().await;
276 self.iter_turn_chunks_locked(max_wait).await
277 }
278
279 async fn iter_turn_chunks_locked(&self, max_wait: Option<Duration>) -> Result<Vec<TurnChunk>> {
280 *self.last_turn_end_state.lock().await = String::new();
281 *self.last_turn_error_message.lock().await = String::new();
282 *self.last_turn_cancel_seen.lock().await = false;
283 *self.turn_event_stats.lock().await = TurnEventStats::new();
284
285 let mut out = Vec::new();
286 let mut query_started = false;
287 let mut expected_loop_id = self.loop_id().await;
288 let mut stream_payload_seen = false;
289 let mut turn_progress_seen = false;
290 let mut cancel_seen = false;
291 let absolute_deadline = max_wait.map(|d| tokio::time::Instant::now() + d);
292
293 let _ = self.client.peel_stale_pending_control_events().await;
294
295 loop {
296 if let Some(deadline) = absolute_deadline {
297 if tokio::time::Instant::now() > deadline {
298 let err = format!(
299 "turn timed out after {:?} (loop={})",
300 max_wait.unwrap_or_default(),
301 expected_loop_id
302 );
303 *self.last_turn_error_message.lock().await = err.clone();
304 return Err(Error::msg(err));
305 }
306 }
307
308 let ev = self
309 .client
310 .read_event_with_timeout(Duration::from_millis(250))
311 .await?;
312 let Some(ev) = ev else {
313 if query_started && !self.client.is_connection_alive() {
314 *self.last_turn_end_state.lock().await = "connection_lost".into();
315 return Err(Error::msg("daemon connection lost"));
316 }
317 continue;
319 };
320
321 let mut frame = ev;
322 let mut event_type = frame
323 .get("type")
324 .and_then(|v| v.as_str())
325 .unwrap_or("")
326 .to_string();
327 if event_type == "next" {
328 frame = unwrap_next_frame(&frame);
329 event_type = frame
330 .get("type")
331 .and_then(|v| v.as_str())
332 .unwrap_or("")
333 .to_string();
334 }
335
336 let event_loop_id = frame
337 .get("loop_id")
338 .and_then(|v| v.as_str())
339 .unwrap_or("")
340 .to_string();
341 if !expected_loop_id.is_empty()
342 && !event_loop_id.is_empty()
343 && event_loop_id != expected_loop_id
344 {
345 continue;
346 }
347
348 if event_type == "error" {
349 let msg = frame
350 .get("error")
351 .and_then(|e| e.get("message"))
352 .and_then(|m| m.as_str())
353 .or_else(|| frame.get("message").and_then(|m| m.as_str()))
354 .unwrap_or("daemon error")
355 .to_string();
356 *self.last_turn_error_message.lock().await = msg.clone();
357 return Err(Error::msg(msg));
358 }
359
360 if event_type == "status" {
361 if let Some(lid) = frame.get("loop_id").and_then(|v| v.as_str()) {
362 if !lid.is_empty() {
363 *self.loop_id.lock().await = lid.to_string();
364 expected_loop_id = lid.to_string();
365 }
366 }
367 let state = frame.get("state").and_then(|v| v.as_str()).unwrap_or("");
368 match state {
369 "running" => query_started = true,
370 "stopped" if query_started => {
371 *self.last_turn_end_state.lock().await = state.into();
372 self.drain_after_idle(&expected_loop_id, &mut out).await;
373 return Ok(out);
374 }
375 "idle" if query_started => {
376 if !stream_payload_seen && !cancel_seen {
377 continue;
378 }
379 *self.last_turn_end_state.lock().await = state.into();
380 self.drain_after_idle(&expected_loop_id, &mut out).await;
381 return Ok(out);
382 }
383 _ => {}
384 }
385 continue;
386 }
387
388 if event_type == "command_response" {
389 let content = frame.get("content").and_then(|v| v.as_str()).unwrap_or("");
390 if content.contains("Cancellation requested") {
391 cancel_seen = true;
392 *self.last_turn_cancel_seen.lock().await = true;
393 }
394 continue;
395 }
396
397 if event_type != "event" {
398 continue;
399 }
400
401 let data = frame.get("data").cloned().unwrap_or(Value::Null);
402 let namespace = frame
403 .get("namespace")
404 .cloned()
405 .unwrap_or(Value::Array(vec![]));
406 let mode = frame
407 .get("mode")
408 .and_then(|v| v.as_str())
409 .unwrap_or("")
410 .to_string();
411
412 let ns_slice: Vec<Value> = match &namespace {
413 Value::Array(a) => a.clone(),
414 _ => vec![],
415 };
416 if (self.early_drop_fn)(&ns_slice, &mode, &data) {
417 self.turn_event_stats.lock().await.filtered_early += 1;
418 continue;
419 }
420
421 if mode == "custom"
422 && is_turn_end_custom_data(&data)
423 && (!query_started || !turn_progress_seen)
424 {
425 continue;
426 }
427
428 stream_payload_seen = true;
429 if is_turn_progress_chunk(&mode, &data) {
430 turn_progress_seen = true;
431 }
432
433 out.push(TurnChunk {
434 namespace,
435 mode: mode.clone(),
436 data: data.clone(),
437 });
438
439 if mode == "custom" && is_turn_end_custom_data(&data) {
440 let custom_type = data.get("type").and_then(|v| v.as_str()).unwrap_or("");
441 *self.last_turn_end_state.lock().await = if custom_type == STREAM_END {
442 "stream_end".into()
443 } else {
444 "completed".into()
445 };
446 self.drain_after_idle(&expected_loop_id, &mut out).await;
447 return Ok(out);
448 }
449 }
450 }
451
452 async fn drain_after_idle(&self, expected_loop_id: &str, out: &mut Vec<TurnChunk>) {
453 let deadline = tokio::time::Instant::now() + self.opts.post_idle_drain;
454 while tokio::time::Instant::now() < deadline {
455 let ev = match self
456 .client
457 .read_event_with_timeout(Duration::from_millis(250))
458 .await
459 {
460 Ok(Some(e)) => e,
461 _ => return,
462 };
463 let mut frame = ev;
464 let mut event_type = frame
465 .get("type")
466 .and_then(|v| v.as_str())
467 .unwrap_or("")
468 .to_string();
469 if event_type == "next" {
470 frame = unwrap_next_frame(&frame);
471 event_type = frame
472 .get("type")
473 .and_then(|v| v.as_str())
474 .unwrap_or("")
475 .to_string();
476 }
477 let event_loop_id = frame.get("loop_id").and_then(|v| v.as_str()).unwrap_or("");
478 if !expected_loop_id.is_empty()
479 && !event_loop_id.is_empty()
480 && event_loop_id != expected_loop_id
481 {
482 continue;
483 }
484 if event_type == "error" {
485 return;
486 }
487 if event_type == "status" {
488 if let Some(lid) = frame.get("loop_id").and_then(|v| v.as_str()) {
489 if !lid.is_empty() {
490 *self.loop_id.lock().await = lid.to_string();
491 }
492 }
493 continue;
494 }
495 if event_type != "event" {
496 continue;
497 }
498 let data = frame.get("data").cloned().unwrap_or(Value::Null);
499 let namespace = frame
500 .get("namespace")
501 .cloned()
502 .unwrap_or(Value::Array(vec![]));
503 let mode = frame
504 .get("mode")
505 .and_then(|v| v.as_str())
506 .unwrap_or("")
507 .to_string();
508 let ns_slice: Vec<Value> = match &namespace {
509 Value::Array(a) => a.clone(),
510 _ => vec![],
511 };
512 if (self.early_drop_fn)(&ns_slice, &mode, &data) {
513 self.turn_event_stats.lock().await.filtered_early += 1;
514 continue;
515 }
516 self.turn_event_stats.lock().await.post_idle_drained += 1;
517 out.push(TurnChunk {
518 namespace,
519 mode,
520 data,
521 });
522 }
523 }
524}