Skip to main content

mentra/agent/
steering.rs

1use std::{
2    collections::VecDeque,
3    sync::{Arc, Mutex, MutexGuard},
4};
5
6use crate::{ContentBlock, Message, error::RuntimeError, runtime::RunOptions};
7
8use super::Agent;
9
10/// Controls how many queued entries are injected at one eligible boundary.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum QueueMode {
13    /// Drain every currently queued entry into one model round.
14    All,
15    /// Drain exactly one entry per eligible boundary.
16    #[default]
17    OneAtATime,
18}
19
20#[derive(Default)]
21struct SteeringQueues {
22    steer: VecDeque<Vec<ContentBlock>>,
23    follow_up: VecDeque<Vec<ContentBlock>>,
24    steer_mode: QueueMode,
25    follow_up_mode: QueueMode,
26}
27
28/// Cloneable, agent-scoped handle for live steering and deferred follow-ups.
29///
30/// Obtain this handle before calling [`Agent::run`](crate::Agent::run). `steer`
31/// entries are eligible at either committed round boundary; `follow_up`
32/// entries are eligible only when a tool-free assistant response would
33/// otherwise stop the run. Queues are in-memory and survive sequential runs of
34/// this agent, but are never shared with another agent on the same runtime.
35#[derive(Clone, Default)]
36pub struct SteeringHandle {
37    queues: Arc<Mutex<SteeringQueues>>,
38}
39
40impl SteeringHandle {
41    pub(crate) fn new() -> Self {
42        Self::default()
43    }
44
45    /// Enqueues context for the next eligible round boundary.
46    pub fn steer(&self, content: impl Into<Vec<ContentBlock>>) {
47        let content = content.into();
48        if !content.is_empty() {
49            self.lock().steer.push_back(content);
50        }
51    }
52
53    /// Enqueues context used only when a run would otherwise stop.
54    pub fn follow_up(&self, content: impl Into<Vec<ContentBlock>>) {
55        let content = content.into();
56        if !content.is_empty() {
57            self.lock().follow_up.push_back(content);
58        }
59    }
60
61    /// Removes steering entries that have not yet been injected.
62    pub fn clear_steer(&self) {
63        self.lock().steer.clear();
64    }
65
66    /// Removes follow-up entries that have not yet been injected.
67    pub fn clear_follow_up(&self) {
68        self.lock().follow_up.clear();
69    }
70
71    /// Returns whether either queue contains an entry awaiting injection.
72    pub fn has_pending(&self) -> bool {
73        let queues = self.lock();
74        !queues.steer.is_empty() || !queues.follow_up.is_empty()
75    }
76
77    /// Sets the steering drain mode for subsequent boundaries.
78    pub fn set_steer_mode(&self, mode: QueueMode) {
79        self.lock().steer_mode = mode;
80    }
81
82    /// Sets the follow-up drain mode for subsequent would-stop boundaries.
83    pub fn set_follow_up_mode(&self, mode: QueueMode) {
84        self.lock().follow_up_mode = mode;
85    }
86
87    pub(crate) fn has_steer(&self) -> bool {
88        !self.lock().steer.is_empty()
89    }
90
91    pub(crate) fn has_follow_up(&self) -> bool {
92        !self.lock().follow_up.is_empty()
93    }
94
95    fn drain_steer(&self) -> Vec<Vec<ContentBlock>> {
96        let mut queues = self.lock();
97        let mode = queues.steer_mode;
98        drain(&mut queues.steer, mode)
99    }
100
101    fn drain_follow_up(&self) -> Vec<Vec<ContentBlock>> {
102        let mut queues = self.lock();
103        let mode = queues.follow_up_mode;
104        drain(&mut queues.follow_up, mode)
105    }
106
107    fn prepend_steer(&self, entries: Vec<Vec<ContentBlock>>) {
108        prepend(&mut self.lock().steer, entries);
109    }
110
111    fn prepend_follow_up(&self, entries: Vec<Vec<ContentBlock>>) {
112        prepend(&mut self.lock().follow_up, entries);
113    }
114
115    fn lock(&self) -> MutexGuard<'_, SteeringQueues> {
116        self.queues
117            .lock()
118            .unwrap_or_else(|poisoned| poisoned.into_inner())
119    }
120}
121
122impl Agent {
123    /// Returns an agent-scoped handle suitable for use while `run(&mut self)`
124    /// holds the mutable agent borrow.
125    pub fn steering_handle(&self) -> SteeringHandle {
126        self.steering.clone()
127    }
128
129    /// Idle convenience for enqueueing a steer on this agent.
130    pub fn steer(&self, content: impl Into<Vec<ContentBlock>>) {
131        self.steering.steer(content);
132    }
133
134    /// Idle convenience for enqueueing a would-stop follow-up on this agent.
135    pub fn follow_up(&self, content: impl Into<Vec<ContentBlock>>) {
136        self.steering.follow_up(content);
137    }
138
139    /// Starts an idle run from the next queued steer.
140    ///
141    /// This is the only automatic consumption point for a steer while no run is
142    /// active. Follow-ups remain reserved for a running turn's would-stop
143    /// boundary. A failed run prepends the consumed entry back onto the queue.
144    pub async fn run_queued(&mut self, options: RunOptions) -> Result<Message, RuntimeError> {
145        let Some(content) = self.drain_steer() else {
146            return Err(RuntimeError::OperationDenied(
147                "no queued steering input is available".to_string(),
148            ));
149        };
150
151        let result = self.run(content, options).await;
152        if result.is_err() {
153            // `run` normally requeues on its rollback path. This second call is
154            // intentionally idempotent and covers errors raised before the run
155            // checkpoint is established.
156            self.requeue_inflight_steering();
157        }
158        result
159    }
160
161    pub(super) fn has_pending_steer(&self) -> bool {
162        self.steering.has_steer()
163    }
164
165    pub(super) fn has_pending_follow_up(&self) -> bool {
166        self.steering.has_follow_up()
167    }
168
169    pub(super) fn drain_steer(&mut self) -> Option<Vec<ContentBlock>> {
170        let entries = self.steering.drain_steer();
171        if entries.is_empty() {
172            return None;
173        }
174        let content = flatten(&entries);
175        self.inflight_steer.extend(entries);
176        Some(content)
177    }
178
179    pub(super) fn drain_follow_up(&mut self) -> Option<Vec<ContentBlock>> {
180        let entries = self.steering.drain_follow_up();
181        if entries.is_empty() {
182            return None;
183        }
184        let content = flatten(&entries);
185        self.inflight_follow_up.extend(entries);
186        Some(content)
187    }
188
189    pub(super) fn clear_inflight_steering(&mut self) {
190        self.inflight_steer.clear();
191        self.inflight_follow_up.clear();
192    }
193
194    pub(super) fn requeue_inflight_steering(&mut self) {
195        self.steering
196            .prepend_steer(std::mem::take(&mut self.inflight_steer));
197        self.steering
198            .prepend_follow_up(std::mem::take(&mut self.inflight_follow_up));
199    }
200}
201
202fn drain(queue: &mut VecDeque<Vec<ContentBlock>>, mode: QueueMode) -> Vec<Vec<ContentBlock>> {
203    match mode {
204        QueueMode::All => queue.drain(..).collect(),
205        QueueMode::OneAtATime => queue.pop_front().into_iter().collect(),
206    }
207}
208
209fn prepend(queue: &mut VecDeque<Vec<ContentBlock>>, entries: Vec<Vec<ContentBlock>>) {
210    for entry in entries.into_iter().rev() {
211        queue.push_front(entry);
212    }
213}
214
215fn flatten(entries: &[Vec<ContentBlock>]) -> Vec<ContentBlock> {
216    entries.iter().flatten().cloned().collect()
217}