1use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use awaken_runtime::RunActivation;
11use awaken_server_contract::contract::lifecycle::RunStatus;
12use awaken_server_contract::contract::mailbox::MailboxInterrupt;
13use awaken_server_contract::contract::message::Message;
14use awaken_server_contract::contract::storage::{
15 RunQuery, RunRecord, RunWaitingState, StorageError,
16};
17use awaken_server_contract::contract::suspension::ToolCallResume;
18
19use crate::app::RunModuleState;
20use crate::mailbox::{Mailbox, MailboxError, MailboxSubmitResult};
21use std::sync::Arc;
22
23#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
25#[serde(rename_all = "snake_case")]
26pub enum InputMode {
27 #[default]
29 Queue,
30 InterruptThenQueue,
32 ResumeOpenRun,
34}
35
36#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
38#[serde(rename_all = "snake_case")]
39pub enum InterruptMode {
40 #[default]
42 Graceful,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47pub struct ActiveRun {
48 pub thread_id: String,
49 pub run_id: String,
50 pub agent_id: String,
51 pub status: RunStatus,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub termination_reason: Option<awaken_server_contract::contract::lifecycle::TerminationReason>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub dispatch_id: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub session_id: Option<String>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub waiting: Option<RunWaitingState>,
60}
61
62impl From<RunRecord> for ActiveRun {
63 fn from(run: RunRecord) -> Self {
64 Self {
65 thread_id: run.thread_id,
66 run_id: run.run_id,
67 agent_id: run.agent_id,
68 status: run.status,
69 termination_reason: run.termination_reason,
70 dispatch_id: run.dispatch_id,
71 session_id: run.session_id,
72 waiting: run.waiting,
73 }
74 }
75}
76
77#[derive(Debug, Error)]
79pub enum RunControlError {
80 #[error("thread not found: {0}")]
81 ThreadNotFound(String),
82 #[error("run not found: {0}")]
83 RunNotFound(String),
84 #[error("decision target not found: {0}")]
85 DecisionTargetNotFound(String),
86 #[error("storage error: {0}")]
87 Store(#[from] StorageError),
88 #[error("mailbox error: {0}")]
89 Mailbox(#[from] MailboxError),
90}
91
92#[derive(Clone)]
94pub struct RunControlService {
95 state: RunModuleState,
96}
97
98impl RunControlService {
99 pub fn new(state: RunModuleState) -> Self {
100 Self { state }
101 }
102
103 fn mailbox(&self) -> Arc<Mailbox> {
104 self.state.mailbox()
105 }
106
107 pub async fn get_active_run(
109 &self,
110 thread_id: &str,
111 ) -> Result<Option<ActiveRun>, RunControlError> {
112 if let Some(thread) = self.state.store().load_thread(thread_id).await? {
113 if let Some(active) = self
114 .load_projected_run(
115 thread_id,
116 thread.active_run_id.as_deref(),
117 &[RunStatus::Running],
118 )
119 .await?
120 {
121 return Ok(Some(active));
122 }
123
124 if let Some(open) = self
125 .load_projected_run(
126 thread_id,
127 thread.open_run_id.as_deref(),
128 &[RunStatus::Running, RunStatus::Waiting],
129 )
130 .await?
131 {
132 return Ok(Some(open));
133 }
134 }
135
136 self.scan_active_run(thread_id).await
137 }
138
139 async fn load_projected_run(
140 &self,
141 thread_id: &str,
142 run_id: Option<&str>,
143 allowed_statuses: &[RunStatus],
144 ) -> Result<Option<ActiveRun>, RunControlError> {
145 let Some(run_id) = run_id else {
146 return Ok(None);
147 };
148 let Some(run) = self.state.store().load_run(run_id).await? else {
149 return Ok(None);
150 };
151 if run.thread_id == thread_id && allowed_statuses.contains(&run.status) {
152 Ok(Some(ActiveRun::from(run)))
153 } else {
154 Ok(None)
155 }
156 }
157
158 async fn scan_active_run(&self, thread_id: &str) -> Result<Option<ActiveRun>, RunControlError> {
159 let mut candidates = Vec::new();
160 for status in [RunStatus::Running, RunStatus::Waiting] {
161 let page = self
162 .state
163 .store()
164 .list_runs(&RunQuery {
165 offset: 0,
166 limit: 200,
167 thread_id: Some(thread_id.to_string()),
168 status: Some(status),
169 id_prefix: None,
170 })
171 .await?;
172 candidates.extend(page.items);
173 }
174
175 Ok(candidates
176 .into_iter()
177 .max_by_key(|run| run.updated_at)
178 .map(ActiveRun::from))
179 }
180
181 pub async fn decide(
187 &self,
188 id: &str,
189 tool_call_id: String,
190 resume: ToolCallResume,
191 ) -> Result<(), RunControlError> {
192 let mailbox = self.mailbox();
193 if mailbox
194 .send_decision_live(
195 &self.state.scoped_id(id),
196 tool_call_id.clone(),
197 resume.clone(),
198 )
199 .await?
200 {
201 Ok(())
202 } else {
203 self.enqueue_durable_decision(id, tool_call_id, resume)
204 .await
205 }
206 }
207
208 async fn enqueue_durable_decision(
209 &self,
210 id: &str,
211 tool_call_id: String,
212 resume: ToolCallResume,
213 ) -> Result<(), RunControlError> {
214 let run = if let Some(run) = self.state.store().load_run(id).await? {
215 run
216 } else if let Some(active) = self.get_active_run(id).await? {
217 self.state
218 .store()
219 .load_run(&active.run_id)
220 .await?
221 .ok_or_else(|| RunControlError::RunNotFound(active.run_id.clone()))?
222 } else {
223 return Err(RunControlError::DecisionTargetNotFound(id.to_string()));
224 };
225
226 if run.status != RunStatus::Waiting
227 || !run.is_resumable_waiting()
228 || !waiting_contains_ticket(&run, &tool_call_id)
229 {
230 return Err(RunControlError::DecisionTargetNotFound(id.to_string()));
231 }
232
233 let request = RunActivation::new(run.thread_id.clone(), Vec::new())
234 .with_agent_id(run.agent_id.clone())
235 .with_continue_run_id(run.run_id.clone())
236 .with_decisions(vec![(tool_call_id.clone(), resume.clone())]);
237 let mailbox = self.mailbox();
238 mailbox
239 .submit_background(self.state.scope_activation(request))
240 .await?;
241 mailbox
242 .record_mailbox_decision_received_for_run(
243 &run,
244 &tool_call_id,
245 &resume,
246 "durable_dispatch",
247 )
248 .await;
249 Ok(())
250 }
251
252 pub async fn cancel_run(&self, id: &str) -> Result<(), RunControlError> {
254 if self.mailbox().cancel(&self.state.scoped_id(id)).await? {
255 Ok(())
256 } else {
257 Err(RunControlError::RunNotFound(id.to_string()))
258 }
259 }
260
261 pub async fn interrupt_thread(
263 &self,
264 thread_id: &str,
265 _mode: InterruptMode,
266 ) -> Result<MailboxInterrupt, RunControlError> {
267 let interrupted = self
268 .mailbox()
269 .interrupt(&self.state.scoped_id(thread_id))
270 .await?;
271 if interrupted.active_dispatch.is_some() || interrupted.superseded_count > 0 {
272 Ok(self.state.unscope_interrupt(interrupted))
273 } else {
274 Err(RunControlError::ThreadNotFound(thread_id.to_string()))
275 }
276 }
277
278 pub async fn inject_user_input(
280 &self,
281 thread_id: &str,
282 agent_id: Option<String>,
283 messages: Vec<Message>,
284 mode: InputMode,
285 ) -> Result<MailboxSubmitResult, RunControlError> {
286 let thread = self
287 .state
288 .store()
289 .load_thread(thread_id)
290 .await?
291 .ok_or_else(|| RunControlError::ThreadNotFound(thread_id.to_string()))?;
292
293 if mode == InputMode::InterruptThenQueue {
294 let _ = self
295 .mailbox()
296 .interrupt(&self.state.scoped_id(thread_id))
297 .await
298 .map_err(RunControlError::Mailbox)?;
299 }
300
301 let mut request = RunActivation::new(thread_id.to_string(), messages);
302 if mode == InputMode::ResumeOpenRun {
303 let run = self
304 .load_open_waiting_run(thread_id, thread.open_run_id.as_deref())
305 .await?;
306 request = request
307 .with_agent_id(run.agent_id)
308 .with_continue_run_id(run.run_id);
309 } else if let Some(agent_id) = agent_id {
310 request = request.with_agent_id(agent_id);
311 }
312
313 self.mailbox()
314 .submit_background(self.state.scope_activation(request))
315 .await
316 .map(|result| self.state.unscope_submit_result(result))
317 .map_err(RunControlError::Mailbox)
318 }
319
320 pub async fn inject_user_input_live_then_queue(
322 &self,
323 thread_id: &str,
324 agent_id: Option<String>,
325 messages: Vec<Message>,
326 ) -> Result<MailboxSubmitResult, RunControlError> {
327 let _thread = self
328 .state
329 .store()
330 .load_thread(thread_id)
331 .await?
332 .ok_or_else(|| RunControlError::ThreadNotFound(thread_id.to_string()))?;
333
334 let mut request = RunActivation::new(thread_id.to_string(), messages);
335 if let Some(agent_id) = agent_id {
336 request = request.with_agent_id(agent_id);
337 }
338
339 self.mailbox()
340 .submit_live_then_queue(self.state.scope_activation(request), None)
341 .await
342 .map(|result| self.state.unscope_submit_result(result))
343 .map_err(RunControlError::Mailbox)
344 }
345
346 pub async fn inject_run_input(
348 &self,
349 run_id: &str,
350 messages: Vec<Message>,
351 mode: InputMode,
352 ) -> Result<MailboxSubmitResult, RunControlError> {
353 let run = self
354 .state
355 .store()
356 .load_run(run_id)
357 .await?
358 .ok_or_else(|| RunControlError::RunNotFound(run_id.to_string()))?;
359
360 if mode == InputMode::InterruptThenQueue {
361 return self
362 .inject_user_input(&run.thread_id, Some(run.agent_id), messages, mode)
363 .await;
364 }
365
366 if run.status == RunStatus::Waiting && run.is_resumable_waiting() {
367 let request = RunActivation::new(run.thread_id.clone(), messages)
368 .with_agent_id(run.agent_id)
369 .with_continue_run_id(run.run_id);
370 return self
371 .mailbox()
372 .submit_background(self.state.scope_activation(request))
373 .await
374 .map(|result| self.state.unscope_submit_result(result))
375 .map_err(RunControlError::Mailbox);
376 }
377
378 self.inject_user_input(&run.thread_id, Some(run.agent_id), messages, mode)
379 .await
380 }
381
382 pub async fn inject_run_input_live_then_queue(
384 &self,
385 run_id: &str,
386 messages: Vec<Message>,
387 ) -> Result<MailboxSubmitResult, RunControlError> {
388 let run = self
389 .state
390 .store()
391 .load_run(run_id)
392 .await?
393 .ok_or_else(|| RunControlError::RunNotFound(run_id.to_string()))?;
394
395 let request =
396 RunActivation::new(run.thread_id.clone(), messages).with_agent_id(run.agent_id.clone());
397 self.mailbox()
398 .submit_live_then_queue(
399 self.state.scope_activation(request),
400 Some(&self.state.scoped_id(&run.run_id)),
401 )
402 .await
403 .map(|result| self.state.unscope_submit_result(result))
404 .map_err(RunControlError::Mailbox)
405 }
406
407 async fn load_open_waiting_run(
408 &self,
409 thread_id: &str,
410 open_run_id: Option<&str>,
411 ) -> Result<RunRecord, RunControlError> {
412 let Some(open_run_id) = open_run_id else {
413 return Err(RunControlError::RunNotFound(format!(
414 "open run for thread {thread_id}"
415 )));
416 };
417 let run = self
418 .state
419 .store()
420 .load_run(open_run_id)
421 .await?
422 .ok_or_else(|| RunControlError::RunNotFound(open_run_id.to_string()))?;
423 if run.thread_id != thread_id
424 || run.status != RunStatus::Waiting
425 || !run.is_resumable_waiting()
426 {
427 return Err(RunControlError::RunNotFound(open_run_id.to_string()));
428 }
429 Ok(run)
430 }
431}
432
433fn waiting_contains_ticket(run: &RunRecord, target: &str) -> bool {
434 let Some(waiting) = run.waiting.as_ref() else {
435 return false;
436 };
437 waiting.ticket_ids.iter().any(|id| id == target)
438 || waiting
439 .tickets
440 .iter()
441 .any(|ticket| ticket.ticket_id == target || ticket.tool_call_id == target)
442}