use std::sync::Arc;
use std::time::Duration;
use serde_json::{json, Map, Value};
use crate::client::SendInputOptions;
use crate::errors::{Error, Result};
use crate::stream_terminal::is_turn_end_custom_data;
use super::classifier::EventClassifier;
use super::pool::{input_message_for_loop, ConnectionPool};
use super::query_gate::QueryGate;
use super::session_store::SessionStore;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TimeoutPolicy {
#[default]
Fail,
SoftComplete,
}
#[derive(Debug, Clone)]
pub struct TurnConfig {
pub query_timeout: Duration,
pub idle_timeout: Duration,
pub on_idle_timeout: TimeoutPolicy,
pub on_query_timeout: TimeoutPolicy,
pub on_stream_close: TimeoutPolicy,
}
impl Default for TurnConfig {
fn default() -> Self {
Self {
query_timeout: Duration::from_secs(30 * 60),
idle_timeout: Duration::ZERO,
on_idle_timeout: TimeoutPolicy::Fail,
on_query_timeout: TimeoutPolicy::Fail,
on_stream_close: TimeoutPolicy::Fail,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct InputOpts {
pub intent_hint: Option<String>,
pub preferred_subagent: Option<String>,
pub response_schema: Option<Value>,
pub response_schema_name: Option<String>,
pub response_schema_strict: Option<bool>,
}
pub struct TurnRunner<S: SessionStore> {
pool: Arc<ConnectionPool<S>>,
gate: QueryGate,
classifier: EventClassifier,
store: Arc<S>,
cfg: TurnConfig,
}
impl<S: SessionStore + 'static> TurnRunner<S> {
pub fn new(
pool: Arc<ConnectionPool<S>>,
gate: QueryGate,
classifier: EventClassifier,
store: Arc<S>,
cfg: Option<TurnConfig>,
) -> Self {
Self {
pool,
gate,
classifier,
store,
cfg: cfg.unwrap_or_default(),
}
}
pub async fn execute(
&self,
session_id: &str,
message: &str,
user_id: &str,
workspace_id: &str,
attachments: Option<Value>,
opts: Option<InputOpts>,
) -> Result<String> {
self.gate
.acquire(session_id)
.await
.map_err(|e| Error::msg(e.to_string()))?;
let result = self
.execute_inner(
session_id,
message,
user_id,
workspace_id,
attachments,
opts,
)
.await;
self.gate.release(session_id).await;
result
}
async fn execute_inner(
&self,
session_id: &str,
message: &str,
user_id: &str,
workspace_id: &str,
attachments: Option<Value>,
opts: Option<InputOpts>,
) -> Result<String> {
let conn = self.pool.acquire(session_id, workspace_id, user_id).await?;
let loop_id = conn.loop_id.clone();
let flat = input_message_for_loop(message, &loop_id, attachments.clone(), opts.as_ref());
let mut params = Map::new();
for (k, v) in flat {
if k == "type" {
continue;
}
params.insert(k, v);
}
let input_opts = SendInputOptions {
loop_id: Some(loop_id.clone()),
intent_hint: opts.as_ref().and_then(|o| o.intent_hint.clone()),
preferred_subagent: opts.as_ref().and_then(|o| o.preferred_subagent.clone()),
response_schema: opts.as_ref().and_then(|o| o.response_schema.clone()),
response_schema_name: opts.as_ref().and_then(|o| o.response_schema_name.clone()),
response_schema_strict: opts.as_ref().and_then(|o| o.response_schema_strict),
attachments,
..Default::default()
};
let _ = params;
conn.client.send_input(message, input_opts).await?;
self.store
.append_message(session_id, json!({"role":"user","content": message}))
.await;
let deadline = tokio::time::Instant::now() + self.cfg.query_timeout;
let mut collected = String::new();
let mut last_event = tokio::time::Instant::now();
let mut query_started = false;
let mut payload_seen = false;
loop {
if tokio::time::Instant::now() > deadline {
self.pool.release(session_id).await;
return match self.cfg.on_query_timeout {
TimeoutPolicy::SoftComplete => Ok(collected),
TimeoutPolicy::Fail => Err(Error::msg("query timeout")),
};
}
if !self.cfg.idle_timeout.is_zero() && last_event.elapsed() > self.cfg.idle_timeout {
self.pool.release(session_id).await;
return match self.cfg.on_idle_timeout {
TimeoutPolicy::SoftComplete => Ok(collected),
TimeoutPolicy::Fail => Err(Error::msg("idle timeout")),
};
}
let wait = if self.cfg.idle_timeout.is_zero() {
Duration::from_millis(500)
} else {
self.cfg.idle_timeout.min(Duration::from_millis(500))
};
let ev = conn.client.read_event_with_timeout(wait).await?;
let Some(ev) = ev else {
if !conn.client.is_connection_alive() {
self.pool.release(session_id).await;
return match self.cfg.on_stream_close {
TimeoutPolicy::SoftComplete => Ok(collected),
TimeoutPolicy::Fail => Err(Error::msg("stream closed")),
};
}
continue;
};
last_event = tokio::time::Instant::now();
let frame = if ev.get("type").and_then(|v| v.as_str()) == Some("next") {
crate::client::unwrap_next_frame(&ev)
} else {
ev
};
let event_type = frame.get("type").and_then(|v| v.as_str()).unwrap_or("");
if event_type == "status" {
let state = frame.get("state").and_then(|v| v.as_str()).unwrap_or("");
match state {
"running" => query_started = true,
"stopped" if query_started => break,
"idle" if query_started && (payload_seen || !collected.is_empty()) => break,
"idle" if query_started && self.classifier.treat_status_idle_as_complete => {
break;
}
_ => {}
}
continue;
}
if event_type != "event" {
continue;
}
let mode = frame.get("mode").and_then(|v| v.as_str()).unwrap_or("");
let data = frame.get("data").cloned().unwrap_or(Value::Null);
payload_seen = true;
if let Some(text) = self.classifier.extract_text(mode, &data) {
collected.push_str(&text);
}
if mode == "custom" && is_turn_end_custom_data(&data) {
break;
}
}
if !collected.is_empty() {
self.store
.append_message(session_id, json!({"role":"assistant","content": collected}))
.await;
}
self.pool.release(session_id).await;
Ok(collected)
}
}