Skip to main content

graph_flow/
context.rs

1//! Context and state management for workflows.
2//!
3//! This module provides thread-safe state management across workflow tasks,
4//! including regular data storage and specialized chat history management.
5//!
6//! All `Context` methods are synchronous: the underlying storage is an
7//! in-memory map guarded by short critical sections, so there is nothing to
8//! await. Methods can be called freely from both async tasks and sync code
9//! (e.g. edge-condition closures).
10//!
11//! # Examples
12//!
13//! ## Basic Context Usage
14//!
15//! ```rust
16//! use graph_flow::Context;
17//!
18//! # fn main() -> graph_flow::Result<()> {
19//! let context = Context::new();
20//!
21//! // Store different types of data
22//! context.set("user_id", 12345)?;
23//! context.set("name", "Alice".to_string())?;
24//! context.set("active", true)?;
25//!
26//! // Retrieve data with type safety
27//! let user_id: Option<i32> = context.get("user_id");
28//! let name: Option<String> = context.get("name");
29//! let active: Option<bool> = context.get("active");
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! ## Chat History Management
35//!
36//! ```rust
37//! use graph_flow::Context;
38//!
39//! let context = Context::new();
40//!
41//! // Add messages to chat history
42//! context.add_user_message("Hello, assistant!".to_string());
43//! context.add_assistant_message("Hello! How can I help you?".to_string());
44//! context.add_system_message("User session started".to_string());
45//!
46//! // Get chat history
47//! let history = context.get_chat_history();
48//! let all_messages = context.get_all_messages();
49//! let last_5 = context.get_last_messages(5);
50//!
51//! // Check history status
52//! let count = context.chat_history_len();
53//! let is_empty = context.is_chat_history_empty();
54//! ```
55//!
56//! ## Context with Message Limits
57//!
58//! ```rust
59//! use graph_flow::Context;
60//!
61//! // Create context with maximum 100 messages
62//! let context = Context::with_max_chat_messages(100);
63//!
64//! // Messages will be automatically pruned when limit is exceeded
65//! for i in 0..150 {
66//!     context.add_user_message(format!("Message {}", i));
67//! }
68//!
69//! // Only the last 100 messages are kept
70//! assert_eq!(context.chat_history_len(), 100);
71//! ```
72//!
73//! ## LLM Integration (with `rig` feature)
74//!
75//! ```rust
76//! # #[cfg(feature = "rig")]
77//! # {
78//! use graph_flow::Context;
79//!
80//! let context = Context::new();
81//!
82//! context.add_user_message("What is the capital of France?".to_string());
83//! context.add_assistant_message("The capital of France is Paris.".to_string());
84//!
85//! // Get messages in rig format for LLM calls
86//! let rig_messages = context.get_rig_messages();
87//! let recent_messages = context.get_last_rig_messages(10);
88//!
89//! // Use with rig's completion API
90//! // let response = agent.chat(&user_input, rig_messages).await?;
91//! # }
92//! ```
93
94use chrono::{DateTime, Utc};
95use dashmap::DashMap;
96use serde::{Deserialize, Serialize};
97use serde_json::Value;
98use std::collections::VecDeque;
99use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
100
101use crate::error::GraphError;
102
103#[cfg(feature = "rig")]
104use rig::completion::Message;
105
106/// Represents the role of a message in a conversation.
107///
108/// Used in chat history to distinguish between different types of messages.
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
110pub enum MessageRole {
111    /// Message from a user/human
112    User,
113    /// Message from an assistant/AI
114    Assistant,
115    /// System message (instructions, status updates, etc.)
116    System,
117}
118
119/// A serializable message that can be converted to/from rig::completion::Message.
120///
121/// This struct provides a unified message format that can be stored, serialized,
122/// and optionally converted to other formats like rig's Message type.
123///
124/// # Examples
125///
126/// ```rust
127/// use graph_flow::{SerializableMessage, MessageRole};
128///
129/// // Create different types of messages
130/// let user_msg = SerializableMessage::user("Hello!".to_string());
131/// let assistant_msg = SerializableMessage::assistant("Hi there!".to_string());
132/// let system_msg = SerializableMessage::system("Session started".to_string());
133///
134/// // Access message properties
135/// assert_eq!(user_msg.role, MessageRole::User);
136/// assert_eq!(user_msg.content, "Hello!");
137/// ```
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct SerializableMessage {
140    /// The role of the message sender
141    pub role: MessageRole,
142    /// The content of the message
143    pub content: String,
144    /// When the message was created
145    pub timestamp: DateTime<Utc>,
146}
147
148impl SerializableMessage {
149    /// Create a new message with the specified role and content.
150    ///
151    /// The timestamp is automatically set to the current UTC time.
152    pub fn new(role: MessageRole, content: String) -> Self {
153        Self {
154            role,
155            content,
156            timestamp: Utc::now(),
157        }
158    }
159
160    /// Create a new user message.
161    pub fn user(content: String) -> Self {
162        Self::new(MessageRole::User, content)
163    }
164
165    /// Create a new assistant message.
166    pub fn assistant(content: String) -> Self {
167        Self::new(MessageRole::Assistant, content)
168    }
169
170    /// Create a new system message.
171    pub fn system(content: String) -> Self {
172        Self::new(MessageRole::System, content)
173    }
174}
175
176/// Container for managing chat history with serialization support.
177///
178/// Messages are stored in a ring buffer: when the configured limit is
179/// exceeded, the oldest message is dropped in O(1).
180///
181/// # Examples
182///
183/// ```rust
184/// use graph_flow::ChatHistory;
185///
186/// let mut history = ChatHistory::new();
187/// history.add_user_message("Hello".to_string());
188/// history.add_assistant_message("Hi there!".to_string());
189///
190/// assert_eq!(history.len(), 2);
191/// assert!(!history.is_empty());
192///
193/// let contents: Vec<&str> = history.messages().map(|m| m.content.as_str()).collect();
194/// assert_eq!(contents, ["Hello", "Hi there!"]);
195/// ```
196#[derive(Debug, Clone, Serialize, Deserialize, Default)]
197pub struct ChatHistory {
198    messages: VecDeque<SerializableMessage>,
199    max_messages: Option<usize>,
200}
201
202impl ChatHistory {
203    /// Create a new empty chat history with a default limit of 1000 messages.
204    pub fn new() -> Self {
205        Self {
206            messages: VecDeque::new(),
207            max_messages: Some(1000), // Default limit to prevent unbounded growth
208        }
209    }
210
211    /// Create a new chat history with a maximum message limit.
212    ///
213    /// When the limit is exceeded, older messages are automatically removed.
214    ///
215    /// # Examples
216    ///
217    /// ```rust
218    /// use graph_flow::ChatHistory;
219    ///
220    /// let mut history = ChatHistory::with_max_messages(10);
221    ///
222    /// for i in 0..15 {
223    ///     history.add_user_message(format!("Message {}", i));
224    /// }
225    ///
226    /// // Only the last 10 are kept
227    /// assert_eq!(history.len(), 10);
228    /// ```
229    pub fn with_max_messages(max: usize) -> Self {
230        Self {
231            messages: VecDeque::new(),
232            max_messages: Some(max),
233        }
234    }
235
236    /// Add a user message to the chat history.
237    pub fn add_user_message(&mut self, content: String) {
238        self.add_message(SerializableMessage::user(content));
239    }
240
241    /// Add an assistant message to the chat history.
242    pub fn add_assistant_message(&mut self, content: String) {
243        self.add_message(SerializableMessage::assistant(content));
244    }
245
246    /// Add a system message to the chat history.
247    pub fn add_system_message(&mut self, content: String) {
248        self.add_message(SerializableMessage::system(content));
249    }
250
251    /// Add a message to the chat history, respecting max_messages limit.
252    fn add_message(&mut self, message: SerializableMessage) {
253        self.messages.push_back(message);
254
255        if let Some(max) = self.max_messages {
256            while self.messages.len() > max {
257                self.messages.pop_front();
258            }
259        }
260    }
261
262    /// Clear all messages from the chat history.
263    pub fn clear(&mut self) {
264        self.messages.clear();
265    }
266
267    /// Get the number of messages in the chat history.
268    pub fn len(&self) -> usize {
269        self.messages.len()
270    }
271
272    /// Check if the chat history is empty.
273    pub fn is_empty(&self) -> bool {
274        self.messages.is_empty()
275    }
276
277    /// Iterate over all messages, oldest first.
278    pub fn messages(&self) -> impl Iterator<Item = &SerializableMessage> + '_ {
279        self.messages.iter()
280    }
281
282    /// Iterate over the last N messages, oldest first.
283    ///
284    /// If N is greater than the total number of messages, all messages are returned.
285    ///
286    /// # Examples
287    ///
288    /// ```rust
289    /// use graph_flow::ChatHistory;
290    ///
291    /// let mut history = ChatHistory::new();
292    /// history.add_user_message("Message 1".to_string());
293    /// history.add_user_message("Message 2".to_string());
294    /// history.add_user_message("Message 3".to_string());
295    ///
296    /// let last_two: Vec<&str> = history.last_messages(2).map(|m| m.content.as_str()).collect();
297    /// assert_eq!(last_two, ["Message 2", "Message 3"]);
298    /// ```
299    pub fn last_messages(&self, n: usize) -> impl Iterator<Item = &SerializableMessage> + '_ {
300        self.messages
301            .iter()
302            .skip(self.messages.len().saturating_sub(n))
303    }
304}
305
306/// Helper struct for serializing/deserializing Context
307#[derive(Serialize, Deserialize)]
308struct ContextData {
309    data: std::collections::HashMap<String, Value>,
310    chat_history: ChatHistory,
311}
312
313/// Context for sharing data between tasks in a graph execution.
314///
315/// Provides thread-safe storage for workflow state and dedicated chat history
316/// management. The context is shared across all tasks in a workflow execution
317/// (cloning a `Context` is cheap and yields a handle to the same state).
318///
319/// All methods are synchronous - see the [module docs](self) for rationale.
320///
321/// # Examples
322///
323/// ```rust
324/// use graph_flow::Context;
325///
326/// # fn main() -> graph_flow::Result<()> {
327/// let context = Context::new();
328///
329/// // Typed key-value state
330/// context.set("user_id", 12345)?;
331/// let user_id: Option<i32> = context.get("user_id");
332///
333/// // Chat history
334/// context.add_user_message("Hello".to_string());
335/// let history = context.get_chat_history();
336/// # Ok(())
337/// # }
338/// ```
339#[derive(Clone, Debug)]
340pub struct Context {
341    data: Arc<DashMap<String, Value>>,
342    chat_history: Arc<RwLock<ChatHistory>>,
343}
344
345impl Context {
346    /// Create a new empty context.
347    pub fn new() -> Self {
348        Self {
349            data: Arc::new(DashMap::new()),
350            chat_history: Arc::new(RwLock::new(ChatHistory::new())),
351        }
352    }
353
354    /// Create a new context with a maximum chat history size.
355    ///
356    /// When the chat history exceeds this size, older messages are automatically removed.
357    pub fn with_max_chat_messages(max: usize) -> Self {
358        Self {
359            data: Arc::new(DashMap::new()),
360            chat_history: Arc::new(RwLock::new(ChatHistory::with_max_messages(max))),
361        }
362    }
363
364    /// Acquire the chat history read lock, recovering from poisoning.
365    ///
366    /// A poisoned lock means a task panicked while holding it; the history
367    /// itself is still valid (messages are appended atomically), so we recover
368    /// the guard rather than silently returning empty data.
369    fn history_read(&self) -> RwLockReadGuard<'_, ChatHistory> {
370        self.chat_history
371            .read()
372            .unwrap_or_else(|poisoned| poisoned.into_inner())
373    }
374
375    /// Acquire the chat history write lock, recovering from poisoning (see
376    /// [`Context::history_read`]).
377    fn history_write(&self) -> RwLockWriteGuard<'_, ChatHistory> {
378        self.chat_history
379            .write()
380            .unwrap_or_else(|poisoned| poisoned.into_inner())
381    }
382
383    // Regular context methods
384
385    /// Set a value in the context.
386    ///
387    /// Returns `Err(GraphError::ContextError)` if the value cannot be
388    /// serialized to JSON (e.g. a map with non-string keys).
389    ///
390    /// # Examples
391    ///
392    /// ```rust
393    /// use graph_flow::Context;
394    /// use serde::{Serialize, Deserialize};
395    ///
396    /// #[derive(Serialize, Deserialize)]
397    /// struct UserData {
398    ///     id: u32,
399    ///     name: String,
400    /// }
401    ///
402    /// # fn main() -> graph_flow::Result<()> {
403    /// let context = Context::new();
404    ///
405    /// // Store primitive types
406    /// context.set("count", 42)?;
407    /// context.set("name", "Alice".to_string())?;
408    ///
409    /// // Store complex types
410    /// let user = UserData { id: 1, name: "Bob".to_string() };
411    /// context.set("user", user)?;
412    /// # Ok(())
413    /// # }
414    /// ```
415    pub fn set(&self, key: impl Into<String>, value: impl serde::Serialize) -> crate::error::Result<()> {
416        let key = key.into();
417        let value = serde_json::to_value(value).map_err(|e| {
418            GraphError::ContextError(format!("Failed to serialize value for key '{key}': {e}"))
419        })?;
420        self.data.insert(key, value);
421        Ok(())
422    }
423
424    /// Get a value from the context.
425    ///
426    /// Returns `None` if the key doesn't exist or if the stored value cannot
427    /// be deserialized to the requested type (a warning is logged for the
428    /// latter, since it usually indicates a type mismatch bug).
429    ///
430    /// # Examples
431    ///
432    /// ```rust
433    /// use graph_flow::Context;
434    ///
435    /// # fn main() -> graph_flow::Result<()> {
436    /// let context = Context::new();
437    /// context.set("count", 42)?;
438    ///
439    /// let count: Option<i32> = context.get("count");
440    /// assert_eq!(count, Some(42));
441    ///
442    /// let missing: Option<String> = context.get("missing");
443    /// assert_eq!(missing, None);
444    /// # Ok(())
445    /// # }
446    /// ```
447    pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
448        let entry = self.data.get(key)?;
449        // Deserialize straight from a reference to avoid cloning the stored
450        // Value (which can be large, e.g. documents or chat transcripts).
451        match T::deserialize(entry.value()) {
452            Ok(value) => Some(value),
453            Err(e) => {
454                tracing::warn!(
455                    key = %key,
456                    error = %e,
457                    "Context value exists but failed to deserialize to the requested type"
458                );
459                None
460            }
461        }
462    }
463
464    /// Remove a value from the context.
465    ///
466    /// Returns the removed value if it existed.
467    pub fn remove(&self, key: &str) -> Option<Value> {
468        self.data.remove(key).map(|(_, v)| v)
469    }
470
471    /// Clear all regular context data (does not affect chat history).
472    pub fn clear(&self) {
473        self.data.clear();
474    }
475
476    // Chat history methods
477
478    /// Add a user message to the chat history.
479    pub fn add_user_message(&self, content: String) {
480        self.history_write().add_user_message(content);
481    }
482
483    /// Add an assistant message to the chat history.
484    pub fn add_assistant_message(&self, content: String) {
485        self.history_write().add_assistant_message(content);
486    }
487
488    /// Add a system message to the chat history.
489    pub fn add_system_message(&self, content: String) {
490        self.history_write().add_system_message(content);
491    }
492
493    /// Get a clone of the current chat history.
494    pub fn get_chat_history(&self) -> ChatHistory {
495        self.history_read().clone()
496    }
497
498    /// Clear the chat history.
499    pub fn clear_chat_history(&self) {
500        self.history_write().clear();
501    }
502
503    /// Get the number of messages in the chat history.
504    pub fn chat_history_len(&self) -> usize {
505        self.history_read().len()
506    }
507
508    /// Check if the chat history is empty.
509    pub fn is_chat_history_empty(&self) -> bool {
510        self.history_read().is_empty()
511    }
512
513    /// Get the last N messages from chat history, oldest first.
514    pub fn get_last_messages(&self, n: usize) -> Vec<SerializableMessage> {
515        self.history_read().last_messages(n).cloned().collect()
516    }
517
518    /// Get all messages from chat history, oldest first.
519    pub fn get_all_messages(&self) -> Vec<SerializableMessage> {
520        self.history_read().messages().cloned().collect()
521    }
522
523    // Rig integration methods (only available when rig feature is enabled)
524
525    #[cfg(feature = "rig")]
526    /// Get all chat history messages converted to rig::completion::Message format.
527    ///
528    /// This method is only available when the "rig" feature is enabled.
529    pub fn get_rig_messages(&self) -> Vec<Message> {
530        self.get_all_messages().into_iter().map(Message::from).collect()
531    }
532
533    #[cfg(feature = "rig")]
534    /// Get the last N messages converted to rig::completion::Message format.
535    ///
536    /// This method is only available when the "rig" feature is enabled.
537    pub fn get_last_rig_messages(&self, n: usize) -> Vec<Message> {
538        self.get_last_messages(n).into_iter().map(Message::from).collect()
539    }
540}
541
542#[cfg(feature = "rig")]
543impl From<SerializableMessage> for Message {
544    /// Convert a [`SerializableMessage`] into a `rig::completion::Message`,
545    /// moving the content instead of cloning it.
546    ///
547    /// Only available when the "rig" feature is enabled.
548    fn from(msg: SerializableMessage) -> Self {
549        match msg.role {
550            MessageRole::User => Message::user(msg.content),
551            MessageRole::Assistant => Message::assistant(msg.content),
552            MessageRole::System => Message::system(msg.content),
553        }
554    }
555}
556
557impl Default for Context {
558    fn default() -> Self {
559        Self::new()
560    }
561}
562
563// Serialization support for Context
564impl Serialize for Context {
565    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
566    where
567        S: serde::Serializer,
568    {
569        // Convert DashMap to HashMap for serialization
570        let data: std::collections::HashMap<String, Value> = self
571            .data
572            .iter()
573            .map(|entry| (entry.key().clone(), entry.value().clone()))
574            .collect();
575
576        let chat_history = self.history_read().clone();
577
578        let context_data = ContextData { data, chat_history };
579        context_data.serialize(serializer)
580    }
581}
582
583impl<'de> Deserialize<'de> for Context {
584    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
585    where
586        D: serde::Deserializer<'de>,
587    {
588        let context_data = ContextData::deserialize(deserializer)?;
589
590        let data = Arc::new(DashMap::new());
591        for (key, value) in context_data.data {
592            data.insert(key, value);
593        }
594
595        let chat_history = Arc::new(RwLock::new(context_data.chat_history));
596
597        Ok(Context { data, chat_history })
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604
605    #[test]
606    fn test_basic_context_operations() {
607        let context = Context::new();
608
609        context.set("key", "value").unwrap();
610        let value: Option<String> = context.get("key");
611        assert_eq!(value, Some("value".to_string()));
612    }
613
614    #[test]
615    fn test_set_unserializable_value_errors() {
616        use std::collections::HashMap;
617        let context = Context::new();
618
619        // Maps with non-string keys cannot be represented in JSON
620        let bad: HashMap<Vec<u8>, String> = HashMap::from([(vec![1u8], "x".to_string())]);
621        assert!(context.set("bad", bad).is_err());
622    }
623
624    #[test]
625    fn test_chat_history_operations() {
626        let context = Context::new();
627
628        assert!(context.is_chat_history_empty());
629        assert_eq!(context.chat_history_len(), 0);
630
631        context.add_user_message("Hello".to_string());
632        context.add_assistant_message("Hi there!".to_string());
633
634        assert!(!context.is_chat_history_empty());
635        assert_eq!(context.chat_history_len(), 2);
636
637        let history = context.get_chat_history();
638        assert_eq!(history.len(), 2);
639        let messages: Vec<_> = history.messages().collect();
640        assert_eq!(messages[0].content, "Hello");
641        assert_eq!(messages[0].role, MessageRole::User);
642        assert_eq!(messages[1].content, "Hi there!");
643        assert_eq!(messages[1].role, MessageRole::Assistant);
644    }
645
646    #[test]
647    fn test_chat_history_max_messages() {
648        let context = Context::with_max_chat_messages(2);
649
650        context.add_user_message("Message 1".to_string());
651        context.add_assistant_message("Response 1".to_string());
652        context.add_user_message("Message 2".to_string());
653
654        let history = context.get_chat_history();
655        assert_eq!(history.len(), 2);
656        let messages: Vec<_> = history.messages().collect();
657        assert_eq!(messages[0].content, "Response 1");
658        assert_eq!(messages[1].content, "Message 2");
659    }
660
661    #[test]
662    fn test_last_messages() {
663        let context = Context::new();
664
665        context.add_user_message("Message 1".to_string());
666        context.add_assistant_message("Response 1".to_string());
667        context.add_user_message("Message 2".to_string());
668        context.add_assistant_message("Response 2".to_string());
669
670        let last_two = context.get_last_messages(2);
671        assert_eq!(last_two.len(), 2);
672        assert_eq!(last_two[0].content, "Message 2");
673        assert_eq!(last_two[1].content, "Response 2");
674    }
675
676    #[test]
677    fn test_context_serialization() {
678        let context = Context::new();
679        context.set("key", "value").unwrap();
680        context.add_user_message("test message".to_string());
681
682        let serialized = serde_json::to_string(&context).unwrap();
683        let deserialized: Context = serde_json::from_str(&serialized).unwrap();
684
685        let value: Option<String> = deserialized.get("key");
686        assert_eq!(value, Some("value".to_string()));
687
688        assert_eq!(deserialized.chat_history_len(), 1);
689        let history = deserialized.get_chat_history();
690        let first = history.messages().next().unwrap();
691        assert_eq!(first.content, "test message");
692        assert_eq!(first.role, MessageRole::User);
693    }
694
695    #[test]
696    fn test_serializable_message() {
697        let msg = SerializableMessage::user("test content".to_string());
698        assert_eq!(msg.role, MessageRole::User);
699        assert_eq!(msg.content, "test content");
700
701        let serialized = serde_json::to_string(&msg).unwrap();
702        let deserialized: SerializableMessage = serde_json::from_str(&serialized).unwrap();
703
704        assert_eq!(msg.role, deserialized.role);
705        assert_eq!(msg.content, deserialized.content);
706    }
707
708    #[test]
709    fn test_chat_history_serialization() {
710        let mut history = ChatHistory::new();
711        history.add_user_message("Hello".to_string());
712        history.add_assistant_message("Hi!".to_string());
713
714        let serialized = serde_json::to_string(&history).unwrap();
715        let deserialized: ChatHistory = serde_json::from_str(&serialized).unwrap();
716
717        assert_eq!(deserialized.len(), 2);
718        let messages: Vec<_> = deserialized.messages().collect();
719        assert_eq!(messages[0].content, "Hello");
720        assert_eq!(messages[1].content, "Hi!");
721    }
722
723    #[test]
724    fn test_chat_history_json_compat_with_0_5() {
725        // 0.5 serialized `messages` as a JSON array from a Vec; the VecDeque
726        // representation must remain interchangeable.
727        let json = r#"{"messages":[{"role":"User","content":"hi","timestamp":"2026-01-01T00:00:00Z"}],"max_messages":1000}"#;
728        let history: ChatHistory = serde_json::from_str(json).unwrap();
729        assert_eq!(history.len(), 1);
730    }
731
732    #[cfg(feature = "rig")]
733    #[test]
734    fn test_rig_integration() {
735        let context = Context::new();
736
737        context.add_user_message("Hello".to_string());
738        context.add_assistant_message("Hi there!".to_string());
739        context.add_system_message("System message".to_string());
740
741        let rig_messages = context.get_rig_messages();
742        assert_eq!(rig_messages.len(), 3);
743
744        // Verify each message maps to the correct rig variant.
745        assert!(
746            matches!(&rig_messages[0], Message::User { .. }),
747            "Expected Message::User, got: {:?}",
748            rig_messages[0]
749        );
750        assert!(
751            matches!(&rig_messages[1], Message::Assistant { .. }),
752            "Expected Message::Assistant, got: {:?}",
753            rig_messages[1]
754        );
755        assert!(
756            matches!(&rig_messages[2], Message::System { content } if content == "System message"),
757            "Expected Message::System with correct content, got: {:?}",
758            rig_messages[2]
759        );
760
761        // Verify get_last_rig_messages tail ordering when the tail includes a system message.
762        let last_two = context.get_last_rig_messages(2);
763        assert_eq!(last_two.len(), 2);
764        assert!(
765            matches!(&last_two[0], Message::Assistant { .. }),
766            "Expected Message::Assistant as second-to-last, got: {:?}",
767            last_two[0]
768        );
769        assert!(
770            matches!(&last_two[1], Message::System { .. }),
771            "Expected Message::System as last, got: {:?}",
772            last_two[1]
773        );
774    }
775}