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}