1use oino_ai::{AgentMessage, ImageContent, UserContent, UserContentField, UserMessage};
10use serde::{Deserialize, Serialize};
11
12use super::prompt::{InputSource, PromptOptions};
13use super::{AgentSession, SessionOpError, lock};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub enum QueueKind {
19 Steering,
21 FollowUp,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(tag = "op", rename_all = "camelCase")]
28pub enum QueueEdit {
29 Remove {
31 queue: QueueKind,
33 index: usize,
35 },
36 Edit {
38 queue: QueueKind,
40 index: usize,
42 text: String,
44 },
45 Move {
47 queue: QueueKind,
49 from: usize,
51 to: usize,
53 },
54 SendNow {
57 queue: QueueKind,
59 index: usize,
61 },
62}
63
64fn user_text(message: &AgentMessage) -> Option<String> {
65 match message {
66 AgentMessage::User(user) => Some(user.content.text("")),
67 _ => None,
68 }
69}
70
71fn user_images(message: &AgentMessage) -> Vec<ImageContent> {
72 match message {
73 AgentMessage::User(UserMessage {
74 content: UserContentField::Blocks(blocks),
75 ..
76 }) => blocks
77 .iter()
78 .filter_map(|block| match block {
79 UserContent::Image(image) => Some(image.clone()),
80 UserContent::Text(_) => None,
81 })
82 .collect(),
83 _ => Vec::new(),
84 }
85}
86
87fn match_texts(texts: &[String], messages: &[AgentMessage]) -> Vec<Option<usize>> {
89 let mut claimed = vec![false; messages.len()];
90 texts
91 .iter()
92 .map(|text| {
93 let found = messages.iter().enumerate().position(|(i, message)| {
94 !claimed[i] && user_text(message).as_deref() == Some(text.as_str())
95 });
96 if let Some(i) = found {
97 claimed[i] = true;
98 }
99 found
100 })
101 .collect()
102}
103
104fn out_of_range(index: usize, len: usize) -> SessionOpError {
105 SessionOpError::Failed(format!("Queue index {index} out of range ({len} queued)"))
106}
107
108impl AgentSession {
109 pub async fn edit_queue(&self, edit: QueueEdit) -> Result<(), SessionOpError> {
116 if let QueueEdit::SendNow { queue, index } = edit {
117 return self.send_queued_now(queue, index).await;
118 }
119 let edit = match edit {
120 QueueEdit::Edit { queue, index, text } => QueueEdit::Edit {
121 queue,
122 index,
123 text: self.expand_queued_text(&text),
124 },
125 other => other,
126 };
127 self.apply_queue_edit(&edit)?;
128 self.emit_queue_update();
129 Ok(())
130 }
131
132 fn expand_queued_text(&self, text: &str) -> String {
133 let resources = self
134 .inner
135 .resources
136 .read()
137 .unwrap_or_else(std::sync::PoisonError::into_inner);
138 oino_resources::templates::expand_prompt_template(text, resources.prompts())
139 .unwrap_or_else(|| text.to_owned())
140 }
141
142 fn apply_queue_edit(&self, edit: &QueueEdit) -> Result<(), SessionOpError> {
143 let queue = match edit {
144 QueueEdit::Remove { queue, .. }
145 | QueueEdit::Edit { queue, .. }
146 | QueueEdit::Move { queue, .. }
147 | QueueEdit::SendNow { queue, .. } => *queue,
148 };
149 let steering = queue == QueueKind::Steering;
150 let mut book = lock(&self.inner.book);
151 let texts = if steering {
152 &mut book.steering
153 } else {
154 &mut book.follow_up
155 };
156 self.inner.agent.update_queue(steering, |messages| {
157 let matches = match_texts(texts, messages);
158 match edit {
159 QueueEdit::Remove { index, .. } | QueueEdit::SendNow { index, .. } => {
160 if *index >= texts.len() {
161 return Err(out_of_range(*index, texts.len()));
162 }
163 texts.remove(*index);
164 if let Some(at) = matches[*index] {
165 messages.remove(at);
166 }
167 }
168 QueueEdit::Edit { index, text, .. } => {
169 if *index >= texts.len() {
170 return Err(out_of_range(*index, texts.len()));
171 }
172 texts[*index].clone_from(text);
173 if let Some(at) = matches[*index] {
174 let images = user_images(&messages[at]);
175 let mut content = vec![UserContent::text(text.clone())];
176 content.extend(images.into_iter().map(UserContent::Image));
177 if let AgentMessage::User(user) = &mut messages[at] {
178 user.content = UserContentField::Blocks(content);
179 }
180 }
181 }
182 QueueEdit::Move { from, to, .. } => {
183 let len = texts.len();
184 if *from >= len || *to >= len {
185 return Err(out_of_range((*from).max(*to), len));
186 }
187 let item = texts.remove(*from);
188 texts.insert(*to, item);
189 let mut slots: Vec<usize> = matches.iter().flatten().copied().collect();
190 if slots.len() == len {
191 let mut ordered: Vec<AgentMessage> = matches
192 .iter()
193 .flatten()
194 .map(|&i| messages[i].clone())
195 .collect();
196 let moved = ordered.remove(*from);
197 ordered.insert(*to, moved);
198 slots.sort_unstable();
199 for (slot, message) in slots.into_iter().zip(ordered) {
200 messages[slot] = message;
201 }
202 }
203 }
204 }
205 Ok(())
206 })
207 }
208
209 async fn send_queued_now(&self, queue: QueueKind, index: usize) -> Result<(), SessionOpError> {
210 let steering = queue == QueueKind::Steering;
211 let taken = {
212 let book = lock(&self.inner.book);
213 let texts = if steering {
214 &book.steering
215 } else {
216 &book.follow_up
217 };
218 let text = texts
219 .get(index)
220 .cloned()
221 .ok_or_else(|| out_of_range(index, texts.len()))?;
222 let message = self.inner.agent.update_queue(steering, |messages| {
223 match_texts(texts, messages)[index].map(|at| messages[at].clone())
224 });
225 (text, message)
226 };
227 let (text, message) = taken;
228 self.apply_queue_edit(&QueueEdit::Remove { queue, index })?;
229 if self.is_streaming() {
230 let message = message.unwrap_or_else(|| {
231 AgentMessage::User(UserMessage {
232 content: UserContentField::Blocks(vec![UserContent::text(text.clone())]),
233 timestamp: oino_agent::now_ms(),
234 })
235 });
236 lock(&self.inner.book).steering.insert(0, text);
237 self.inner
238 .agent
239 .update_queue(true, |messages| messages.insert(0, message));
240 self.emit_queue_update();
241 return Ok(());
242 }
243 self.emit_queue_update();
244 let images = message.as_ref().map(user_images).unwrap_or_default();
245 self.prompt(
246 text,
247 PromptOptions {
248 images,
249 source: InputSource::Rpc,
250 ..PromptOptions::default()
251 },
252 )
253 .await
254 .map(drop)
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 fn user(text: &str) -> AgentMessage {
263 AgentMessage::User(UserMessage {
264 content: UserContentField::Blocks(vec![UserContent::text(text.to_owned())]),
265 timestamp: 0,
266 })
267 }
268
269 #[test]
270 fn texts_match_user_messages_in_order() {
271 let messages = vec![user("a"), user("b"), user("a")];
272 let texts = vec!["a".to_owned(), "a".to_owned(), "c".to_owned()];
273 assert_eq!(match_texts(&texts, &messages), vec![Some(0), Some(2), None]);
274 }
275
276 #[test]
277 fn edits_round_trip_as_json() {
278 let edit: QueueEdit = serde_json::from_value(
279 serde_json::json!({"op": "move", "queue": "followUp", "from": 1, "to": 0}),
280 )
281 .unwrap();
282 assert_eq!(
283 edit,
284 QueueEdit::Move {
285 queue: QueueKind::FollowUp,
286 from: 1,
287 to: 0
288 }
289 );
290 }
291}