1use std::sync::Arc;
4use std::time::Duration;
5
6use serde_json::{json, Map, Value};
7
8use crate::client::SendInputOptions;
9use crate::errors::{Error, Result};
10
11use super::classifier::EventClassifier;
12use super::pool::{input_message_for_loop, ConnectionPool};
13use super::query_gate::QueryGate;
14use super::session_store::SessionStore;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum TimeoutPolicy {
19 #[default]
21 Fail,
22 SoftComplete,
24}
25
26#[derive(Debug, Clone)]
28pub struct TurnConfig {
29 pub query_timeout: Duration,
31 pub idle_timeout: Duration,
33 pub on_idle_timeout: TimeoutPolicy,
35 pub on_query_timeout: TimeoutPolicy,
37 pub on_stream_close: TimeoutPolicy,
39}
40
41impl Default for TurnConfig {
42 fn default() -> Self {
43 Self {
44 query_timeout: Duration::from_secs(30 * 60),
45 idle_timeout: Duration::ZERO,
46 on_idle_timeout: TimeoutPolicy::Fail,
47 on_query_timeout: TimeoutPolicy::Fail,
48 on_stream_close: TimeoutPolicy::Fail,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Default)]
55pub struct InputOpts {
56 pub intent_hint: Option<String>,
58 pub preferred_subagent: Option<String>,
60 pub response_schema: Option<Value>,
62 pub response_schema_name: Option<String>,
64 pub response_schema_strict: Option<bool>,
66}
67
68pub struct TurnRunner<S: SessionStore> {
70 pool: Arc<ConnectionPool<S>>,
71 gate: QueryGate,
72 classifier: EventClassifier,
73 store: Arc<S>,
74 cfg: TurnConfig,
75}
76
77impl<S: SessionStore + 'static> TurnRunner<S> {
78 pub fn new(
80 pool: Arc<ConnectionPool<S>>,
81 gate: QueryGate,
82 classifier: EventClassifier,
83 store: Arc<S>,
84 cfg: Option<TurnConfig>,
85 ) -> Self {
86 Self {
87 pool,
88 gate,
89 classifier,
90 store,
91 cfg: cfg.unwrap_or_default(),
92 }
93 }
94
95 pub async fn execute(
97 &self,
98 session_id: &str,
99 message: &str,
100 user_id: &str,
101 workspace_id: &str,
102 attachments: Option<Value>,
103 opts: Option<InputOpts>,
104 ) -> Result<String> {
105 self.gate
106 .acquire(session_id)
107 .await
108 .map_err(|e| Error::msg(e.to_string()))?;
109 let result = self
110 .execute_inner(
111 session_id,
112 message,
113 user_id,
114 workspace_id,
115 attachments,
116 opts,
117 )
118 .await;
119 self.gate.release(session_id).await;
120 result
121 }
122
123 async fn execute_inner(
124 &self,
125 session_id: &str,
126 message: &str,
127 user_id: &str,
128 workspace_id: &str,
129 attachments: Option<Value>,
130 opts: Option<InputOpts>,
131 ) -> Result<String> {
132 let conn = self.pool.acquire(session_id, workspace_id, user_id).await?;
133 let loop_id = conn.loop_id.clone();
134 let flat = input_message_for_loop(message, &loop_id, attachments.clone(), opts.as_ref());
135 let mut params = Map::new();
137 for (k, v) in flat {
138 if k == "type" {
139 continue;
140 }
141 params.insert(k, v);
142 }
143 let input_opts = SendInputOptions {
144 loop_id: Some(loop_id.clone()),
145 intent_hint: opts.as_ref().and_then(|o| o.intent_hint.clone()),
146 preferred_subagent: opts.as_ref().and_then(|o| o.preferred_subagent.clone()),
147 response_schema: opts.as_ref().and_then(|o| o.response_schema.clone()),
148 response_schema_name: opts.as_ref().and_then(|o| o.response_schema_name.clone()),
149 response_schema_strict: opts.as_ref().and_then(|o| o.response_schema_strict),
150 attachments,
151 ..Default::default()
152 };
153 let _ = params;
155 conn.client.send_input(message, input_opts).await?;
156
157 self.store
158 .append_message(session_id, json!({"role":"user","content": message}))
159 .await;
160
161 let deadline = tokio::time::Instant::now() + self.cfg.query_timeout;
162 let mut collected = String::new();
163 let mut last_event = tokio::time::Instant::now();
164 let mut boundary = super::turn_boundary::TurnBoundary::default();
165
166 loop {
167 if tokio::time::Instant::now() > deadline {
168 self.pool.release(session_id).await;
169 return match self.cfg.on_query_timeout {
170 TimeoutPolicy::SoftComplete => Ok(collected),
171 TimeoutPolicy::Fail => Err(Error::msg("query timeout")),
172 };
173 }
174 if !self.cfg.idle_timeout.is_zero() && last_event.elapsed() > self.cfg.idle_timeout {
175 self.pool.release(session_id).await;
176 return match self.cfg.on_idle_timeout {
177 TimeoutPolicy::SoftComplete => Ok(collected),
178 TimeoutPolicy::Fail => Err(Error::msg("idle timeout")),
179 };
180 }
181
182 let wait = if self.cfg.idle_timeout.is_zero() {
183 Duration::from_millis(500)
184 } else {
185 self.cfg.idle_timeout.min(Duration::from_millis(500))
186 };
187 let ev = conn.client.read_event_with_timeout(wait).await?;
188 let Some(ev) = ev else {
189 if !conn.client.is_connection_alive() {
190 self.pool.release(session_id).await;
191 return match self.cfg.on_stream_close {
192 TimeoutPolicy::SoftComplete => Ok(collected),
193 TimeoutPolicy::Fail => Err(Error::msg("stream closed")),
194 };
195 }
196 continue;
197 };
198 last_event = tokio::time::Instant::now();
199
200 let frame = if ev.get("type").and_then(|v| v.as_str()) == Some("next") {
201 crate::client::unwrap_next_frame(&ev)
202 } else {
203 ev
204 };
205 let event_type = frame.get("type").and_then(|v| v.as_str()).unwrap_or("");
206 if event_type == "status" {
207 let state = frame.get("state").and_then(|v| v.as_str()).unwrap_or("");
208 if boundary.feed_status(state).is_some() {
209 break;
210 }
211 continue;
212 }
213 if event_type != "event" {
214 continue;
215 }
216 let mode = frame.get("mode").and_then(|v| v.as_str()).unwrap_or("");
217 let data = frame.get("data").cloned().unwrap_or(Value::Null);
218 if let Some(text) = self.classifier.extract_text(mode, &data) {
219 collected.push_str(&text);
220 }
221 if boundary.feed_event(mode, &data).is_some() {
223 break;
224 }
225 }
226
227 if !collected.is_empty() {
228 self.store
229 .append_message(session_id, json!({"role":"assistant","content": collected}))
230 .await;
231 }
232 self.pool.release(session_id).await;
233 Ok(collected)
234 }
235}