Skip to main content

cookie_agent_engine/runtime/
runs.rs

1use std::collections::HashSet;
2
3use cookie_agent_protocol::{
4    EventOrigin, RunCancelResult, RunId, RunRecallSteerResult, RunStartParams, RunStartResult,
5    RunSteerResult, RunToolStdinParams, RunToolStdinResult, SessionId, SessionStatus,
6};
7
8use super::{
9    ActiveRun, Engine, EngineError, Event, SessionCommand, UserInputInterception,
10    helpers::safe_error,
11};
12
13impl Engine {
14    pub async fn start_run(
15        &self,
16        params: RunStartParams,
17        origin: EventOrigin,
18    ) -> Result<RunStartResult, EngineError> {
19        let session = params.session_id;
20        self.request(session, |reply| SessionCommand::Start {
21            params,
22            origin,
23            admission: None,
24            reply,
25        })
26        .await
27    }
28
29    /// Synchronous setup/CLI wrapper. Do not call from a Tokio runtime.
30    pub fn start_run_blocking(
31        &self,
32        params: RunStartParams,
33        origin: EventOrigin,
34    ) -> Result<RunStartResult, EngineError> {
35        let session = params.session_id;
36        self.request_blocking(session, |reply| SessionCommand::Start {
37            params,
38            origin,
39            admission: None,
40            reply,
41        })
42    }
43
44    pub async fn steer(
45        &self,
46        run_id: RunId,
47        input: String,
48        origin: EventOrigin,
49    ) -> Result<RunSteerResult, EngineError> {
50        let active = self
51            .inner
52            .sessions
53            .active
54            .lock()
55            .unwrap_or_else(|poisoned| poisoned.into_inner())
56            .get(&run_id)
57            .cloned()
58            .ok_or(EngineError::MissingRun(run_id))?;
59        match self.intercept_user_input(active.session, input).await? {
60            UserInputInterception::Accepted {
61                input,
62                original_input,
63            } => {
64                self.request(active.session, |reply| SessionCommand::Steer {
65                    run: run_id,
66                    origin,
67                    input,
68                    original_input,
69                    reply,
70                })
71                .await
72            }
73            UserInputInterception::Handled { reason } => Ok(RunSteerResult {
74                accepted: false,
75                handled_reason: Some(reason),
76            }),
77        }
78    }
79
80    /// Synchronous setup/CLI wrapper. Do not call from a Tokio runtime.
81    pub fn steer_blocking(
82        &self,
83        run_id: RunId,
84        input: String,
85        origin: EventOrigin,
86    ) -> Result<RunSteerResult, EngineError> {
87        tokio::runtime::Builder::new_current_thread()
88            .enable_all()
89            .build()
90            .map_err(|error| {
91                EngineError::Mcp(format!("create blocking steering runtime: {error}"))
92            })?
93            .block_on(self.steer(run_id, input, origin))
94    }
95
96    pub async fn recall_steer(&self, run_id: RunId) -> Result<RunRecallSteerResult, EngineError> {
97        let active = self
98            .inner
99            .sessions
100            .active
101            .lock()
102            .unwrap_or_else(|poisoned| poisoned.into_inner())
103            .get(&run_id)
104            .cloned()
105            .ok_or(EngineError::MissingRun(run_id))?;
106        self.request(active.session, |reply| SessionCommand::RecallSteer {
107            run: run_id,
108            reply,
109        })
110        .await
111    }
112
113    pub async fn cancel_run(&self, run_id: RunId) -> Result<RunCancelResult, EngineError> {
114        let active = self
115            .inner
116            .sessions
117            .active
118            .lock()
119            .unwrap_or_else(|poisoned| poisoned.into_inner())
120            .get(&run_id)
121            .cloned()
122            .ok_or(EngineError::MissingRun(run_id))?;
123        let result = self
124            .request(active.session, |reply| SessionCommand::Cancel {
125                run: run_id,
126                reply,
127            })
128            .await?;
129        let inflight_runs: Vec<_> = {
130            let mut inflight = self
131                .inner
132                .delegation
133                .inflight
134                .lock()
135                .unwrap_or_else(|poisoned| poisoned.into_inner());
136            inflight
137                .values_mut()
138                .flat_map(|entries| entries.values_mut())
139                .filter(|delegate| delegate.parent_run_id == run_id)
140                .filter_map(|delegate| {
141                    delegate.cancelled = true;
142                    delegate.child_run_id
143                })
144                .collect()
145        };
146        let delegation_events = self.inner.delegation_events.clone();
147        let children = self
148            .spawn_admission_blocking(move || Ok::<_, EngineError>(delegation_events.entries()))
149            .await?;
150        let mut pending = vec![run_id];
151        pending.extend(inflight_runs);
152        let mut visited = HashSet::new();
153        while let Some(parent_run_id) = pending.pop() {
154            if !visited.insert(parent_run_id) {
155                continue;
156            }
157            let inflight_children: Vec<_> = {
158                let mut inflight = self
159                    .inner
160                    .delegation
161                    .inflight
162                    .lock()
163                    .unwrap_or_else(|poisoned| poisoned.into_inner());
164                inflight
165                    .values_mut()
166                    .flat_map(|entries| entries.values_mut())
167                    .filter(|delegate| delegate.parent_run_id == parent_run_id)
168                    .filter_map(|delegate| {
169                        delegate.cancelled = true;
170                        delegate.child_run_id
171                    })
172                    .collect()
173            };
174            pending.extend(inflight_children);
175            for child_run_id in children
176                .iter()
177                .filter(|entry| entry.reservation.parent_run_id == parent_run_id)
178                .filter_map(|entry| entry.child_run_id)
179            {
180                pending.push(child_run_id);
181                if child_run_id == run_id {
182                    continue;
183                }
184                let child_active = {
185                    self.inner
186                        .sessions
187                        .active
188                        .lock()
189                        .unwrap_or_else(|poisoned| poisoned.into_inner())
190                        .get(&child_run_id)
191                        .cloned()
192                };
193                if let Some(child_active) = child_active {
194                    child_active.cancellation.cancel();
195                    let _ = self
196                        .request(child_active.session, |reply| SessionCommand::Cancel {
197                            run: child_run_id,
198                            reply,
199                        })
200                        .await;
201                }
202            }
203        }
204        Ok(result)
205    }
206
207    /// Cancels an active run and commits its terminal event under a per-run
208    /// gate. The run loop observes the same gate, so concurrent cancellation
209    /// paths cannot append two `RunCancelled` records.
210    pub(super) fn cancel_run_durably(
211        &self,
212        run_id: RunId,
213        reason: Option<String>,
214    ) -> Result<bool, EngineError> {
215        let active = self
216            .inner
217            .sessions
218            .active
219            .lock()
220            .unwrap_or_else(|poisoned| poisoned.into_inner())
221            .get(&run_id)
222            .cloned();
223        let Some(active) = active else {
224            let session = self
225                .inner
226                .store
227                .all()
228                .into_iter()
229                .find(|session| session.runs.contains_key(&run_id))
230                .ok_or(EngineError::MissingRun(run_id))?;
231            let mut committed = false;
232            return self.commit_run_cancelled_with_retry(
233                session.meta.session_id,
234                run_id,
235                reason,
236                &mut committed,
237            );
238        };
239        active.cancellation.cancel();
240        active
241            .stdin
242            .lock()
243            .unwrap_or_else(|poisoned| poisoned.into_inner())
244            .clear();
245        let mut committed = active
246            .cancelled_committed
247            .lock()
248            .unwrap_or_else(|poisoned| poisoned.into_inner());
249        self.commit_run_cancelled_with_retry(active.session, run_id, reason, &mut committed)
250    }
251
252    pub(super) fn append_run_cancelled_once(
253        &self,
254        active: &ActiveRun,
255        run_id: RunId,
256        reason: Option<String>,
257    ) -> Result<bool, EngineError> {
258        let mut committed = active
259            .cancelled_committed
260            .lock()
261            .unwrap_or_else(|poisoned| poisoned.into_inner());
262        self.commit_run_cancelled_with_retry(active.session, run_id, reason, &mut committed)
263    }
264
265    pub(super) fn commit_run_cancelled_with_retry(
266        &self,
267        session: SessionId,
268        run_id: RunId,
269        reason: Option<String>,
270        committed: &mut bool,
271    ) -> Result<bool, EngineError> {
272        let mut last_error = None;
273        for _ in 0..3 {
274            match self.commit_run_cancelled_once(session, run_id, reason.clone(), committed) {
275                Ok(result) => return Ok(result),
276                Err(error) => last_error = Some(error),
277            }
278        }
279        Err(last_error.expect("cancellation retry attempts are nonempty"))
280    }
281
282    pub(super) fn commit_run_cancelled_once(
283        &self,
284        session: SessionId,
285        run_id: RunId,
286        reason: Option<String>,
287        committed: &mut bool,
288    ) -> Result<bool, EngineError> {
289        if *committed {
290            return Ok(false);
291        }
292        // `append_direct` can append to the log before a projection/cache
293        // refresh fails. The event log is authoritative in that window.
294        if self.run_cancelled_recorded(session, run_id)? {
295            *committed = true;
296            return Ok(false);
297        }
298        if self
299            .inner
300            .store
301            .get(session)?
302            .runs
303            .get(&run_id)
304            .is_none_or(|run| run.status != SessionStatus::Running)
305        {
306            return Ok(false);
307        }
308        match self.append_direct(
309            session,
310            Some(run_id),
311            super::event_origin("engine:model-loop"),
312            Event::RunCancelled {
313                reason: reason.as_deref().map(safe_error),
314            },
315        ) {
316            Ok(()) => {
317                *committed = true;
318                Ok(true)
319            }
320            Err(error) => {
321                if self.run_cancelled_recorded(session, run_id)? {
322                    *committed = true;
323                    Ok(true)
324                } else {
325                    Err(error)
326                }
327            }
328        }
329    }
330
331    pub(super) fn run_cancelled_recorded(
332        &self,
333        session: SessionId,
334        run_id: RunId,
335    ) -> Result<bool, EngineError> {
336        Ok(self
337            .inner
338            .store
339            .get(session)?
340            .log
341            .event_snapshot()
342            .iter()
343            .any(|event| {
344                event.run_id == Some(run_id) && matches!(event.payload, Event::RunCancelled { .. })
345            }))
346    }
347
348    pub async fn tool_stdin(
349        &self,
350        params: RunToolStdinParams,
351    ) -> Result<RunToolStdinResult, EngineError> {
352        let active = self
353            .inner
354            .sessions
355            .active
356            .lock()
357            .unwrap_or_else(|poisoned| poisoned.into_inner())
358            .get(&params.run_id)
359            .cloned()
360            .ok_or(EngineError::MissingRun(params.run_id))?;
361        self.request(active.session, |reply| SessionCommand::Stdin {
362            params,
363            reply,
364        })
365        .await
366    }
367}