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}