Skip to main content

codetether_agent/session/context/
helpers.rs

1//! Shared types and tiny helpers used across the context derivation modules.
2
3use crate::provider::Message;
4
5/// The per-step LLM context, derived from an append-only chat history.
6///
7/// This is the object the prompt loop should hand to the provider — *not*
8/// [`Session::messages`](crate::session::Session::messages) directly.
9///
10/// # Fields
11///
12/// * `messages` — The message list to include in the completion request.
13/// * `origin_len` — Length of the source history at the moment of derivation.
14/// * `compressed` — Whether compression fired during this derivation.
15///
16/// # Examples
17///
18/// ```rust
19/// use codetether_agent::session::context::DerivedContext;
20///
21/// let derived = DerivedContext {
22///     messages: Vec::new(),
23///     origin_len: 0,
24///     compressed: false,
25/// };
26/// assert_eq!(derived.origin_len, 0);
27/// assert!(!derived.compressed);
28/// ```
29#[derive(Debug, Clone)]
30pub struct DerivedContext {
31    /// Messages to send to the provider this turn.
32    pub messages: Vec<Message>,
33    /// `session.messages.len()` at the moment of derivation.
34    pub origin_len: usize,
35    /// `true` when any compression / truncation pass rewrote the clone.
36    pub compressed: bool,
37}
38
39/// Compare two message counts and return whether compression fired.
40///
41/// Separated out so the comparison is testable without a full provider
42/// round-trip. Any count change is treated as evidence that compression
43/// fired.
44pub(super) fn messages_len_changed(before: usize, after: &[Message]) -> bool {
45    before != after.len()
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use crate::provider::{ContentPart, Role};
52
53    #[test]
54    fn derived_context_record_round_trips() {
55        let ctx = DerivedContext {
56            messages: vec![Message {
57                role: Role::User,
58                content: vec![ContentPart::Text {
59                    text: "hi".to_string(),
60                }],
61            }],
62            origin_len: 1,
63            compressed: false,
64        };
65        let cloned = ctx.clone();
66        assert_eq!(ctx.origin_len, cloned.origin_len);
67        assert_eq!(ctx.compressed, cloned.compressed);
68        assert_eq!(ctx.messages.len(), cloned.messages.len());
69    }
70
71    #[test]
72    fn messages_len_changed_detects_shrink_and_noop() {
73        let empty: Vec<Message> = Vec::new();
74        assert!(!messages_len_changed(0, &empty));
75
76        let one = vec![Message {
77            role: Role::User,
78            content: vec![ContentPart::Text {
79                text: "x".to_string(),
80            }],
81        }];
82        assert!(messages_len_changed(5, &one));
83        assert!(!messages_len_changed(1, &one));
84    }
85}