Skip to main content

agentyk_core/
input.rs

1//! Steering — input that arrives while a turn is already running.
2//!
3//! A turn can take minutes. In that time the person who started it learns
4//! things: the approach is wrong, the file was already fixed, stop after this
5//! test. Without somewhere to put that, their only options are to wait or to
6//! cancel and start over, losing everything the turn has done.
7//!
8//! An [`InputQueue`] is that somewhere. A caller holds a clone, pushes a
9//! message whenever it likes, and the executor drains it **between reasoning
10//! steps** — see [`InputQueue::drain`] for why that boundary and not any
11//! other. Drained messages are recorded as ordinary `input.message` events, so
12//! they enter history exactly like the message that opened the turn and a
13//! replay reconstructs the same conversation.
14//!
15//! Deliberately std-only (a `Mutex<VecDeque>`, no channel crate) so it lives
16//! in `agentyk-core` and works the same in any host.
17//!
18//! ```
19//! use agentyk_core::{input::InputQueue, message::Message};
20//!
21//! let queue = InputQueue::new();
22//! let steering = queue.clone();
23//! steering.push(Message::user("actually, skip the tests"));
24//! assert_eq!(queue.drain().len(), 1);
25//! assert!(queue.drain().is_empty(), "draining consumes");
26//! ```
27
28use std::collections::VecDeque;
29use std::sync::{Arc, Mutex};
30
31use crate::message::Message;
32
33/// Messages waiting to join a turn already in progress. Cheap to clone;
34/// every clone shares one queue.
35#[derive(Clone, Default)]
36pub struct InputQueue(Arc<Mutex<VecDeque<Message>>>);
37
38impl InputQueue {
39    /// An empty queue.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Add a message for the running turn to pick up at its next reasoning
45    /// step. Never blocks and never fails: a queue with no turn draining it
46    /// simply holds what it was given, and the next turn takes it.
47    pub fn push(&self, message: Message) {
48        self.0
49            .lock()
50            .unwrap_or_else(|error| error.into_inner())
51            .push_back(message);
52    }
53
54    /// Whether anything is waiting. Cheaper than draining for a host that
55    /// only wants to know.
56    pub fn is_empty(&self) -> bool {
57        self.0
58            .lock()
59            .unwrap_or_else(|error| error.into_inner())
60            .is_empty()
61    }
62
63    /// Take everything queued, in the order it was pushed.
64    ///
65    /// Executors call this **only when the next action is a reasoning step**,
66    /// never mid-batch. That is a protocol constraint, not a preference:
67    /// providers require every tool call to be answered by its result, so a
68    /// user message wedged between a `tool_use` and its `tool_result` makes
69    /// the exchange invalid. Steering therefore lands at the next model call —
70    /// which is also the first moment the model could act on it anyway.
71    pub fn drain(&self) -> Vec<Message> {
72        self.0
73            .lock()
74            .unwrap_or_else(|error| error.into_inner())
75            .drain(..)
76            .collect()
77    }
78}
79
80impl std::fmt::Debug for InputQueue {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("InputQueue")
83            .field("queued", &self.0.lock().map(|q| q.len()).unwrap_or(0))
84            .finish()
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn clones_share_one_queue() {
94        let queue = InputQueue::new();
95        let handle = queue.clone();
96        assert!(queue.is_empty());
97
98        handle.push(Message::user("first"));
99        handle.push(Message::user("second"));
100        assert!(!queue.is_empty());
101
102        let drained = queue.drain();
103        assert_eq!(drained.len(), 2);
104        assert_eq!(drained[0].text(), "first");
105        assert_eq!(drained[1].text(), "second");
106        assert!(handle.is_empty(), "draining one clone drains them all");
107    }
108
109    #[test]
110    fn a_queue_nobody_is_draining_keeps_its_messages() {
111        let queue = InputQueue::new();
112        queue.push(Message::user("held"));
113        assert_eq!(queue.drain().len(), 1);
114    }
115}