use crate::storage::models::{Message, MessageRole};
pub fn system_prompt() -> &'static str {
"You are summarizing an AI-assisted coding session. \
Produce a one-sentence overview of what the session accomplished, \
followed by 2-5 bullet points covering the key technical work.\n\n\
Rules:\n\
- Keep the total summary under 300 words.\n\
- Focus on what was done and why, not how.\n\
- Do not mention tool calls or internal mechanics.\n\
- Use plain text only. No markdown headers, no special formatting.\n\
- Start bullet points with a dash (-)."
}
pub fn prepare_conversation(messages: &[Message], max_chars: usize) -> String {
if messages.is_empty() {
return String::new();
}
let formatted = format_messages(messages);
if max_chars == 0 || formatted.len() <= max_chars {
return formatted;
}
truncate_conversation(messages, max_chars)
}
fn format_messages(messages: &[Message]) -> String {
let mut parts = Vec::with_capacity(messages.len());
for msg in messages {
let text = msg.content.text();
let formatted = format_single_message(msg, &text);
if !formatted.is_empty() {
parts.push(formatted);
}
}
parts.join("\n\n")
}
fn format_single_message(msg: &Message, text: &str) -> String {
if text.is_empty() {
return String::new();
}
let header = match msg.role {
MessageRole::User => {
let ts = msg.timestamp.format("%Y-%m-%d %H:%M UTC");
format!("[User] ({ts})")
}
MessageRole::Assistant => "[Assistant]".to_string(),
MessageRole::System => {
let ts = msg.timestamp.format("%Y-%m-%d %H:%M UTC");
format!("[System] ({ts})")
}
};
format!("{header}\n{text}")
}
fn truncate_conversation(messages: &[Message], max_chars: usize) -> String {
let count = messages.len();
let head_count = ((count as f64) * 0.2).ceil() as usize;
let tail_count = ((count as f64) * 0.3).ceil() as usize;
let (head_count, tail_count) = if head_count + tail_count >= count {
return format_messages(messages);
} else {
(head_count, tail_count)
};
let omitted = count - head_count - tail_count;
let head_msgs = &messages[..head_count];
let tail_msgs = &messages[count - tail_count..];
let head_text = format_messages(head_msgs);
let marker = format!("[... {omitted} messages omitted ...]");
let tail_text = format_messages(tail_msgs);
let result = format!("{head_text}\n\n{marker}\n\n{tail_text}");
if result.len() > max_chars {
let truncated: String = result.chars().take(max_chars).collect();
truncated
} else {
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::models::{ContentBlock, MessageContent};
use chrono::{TimeZone, Utc};
use uuid::Uuid;
fn make_message(role: MessageRole, text: &str, index: i32) -> Message {
Message {
id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
parent_id: None,
index,
timestamp: Utc.with_ymd_and_hms(2024, 1, 15, 14, 30, 0).unwrap(),
role,
content: MessageContent::Text(text.to_string()),
model: None,
git_branch: None,
cwd: None,
}
}
fn make_tool_only_message(index: i32) -> Message {
Message {
id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
parent_id: None,
index,
timestamp: Utc.with_ymd_and_hms(2024, 1, 15, 14, 30, 0).unwrap(),
role: MessageRole::Assistant,
content: MessageContent::Blocks(vec![ContentBlock::ToolUse {
id: "tool_1".to_string(),
name: "Read".to_string(),
input: serde_json::json!({"file_path": "/tmp/test.rs"}),
}]),
model: None,
git_branch: None,
cwd: None,
}
}
#[test]
fn test_system_prompt_is_non_empty() {
let prompt = system_prompt();
assert!(!prompt.is_empty());
assert!(prompt.contains("summariz"));
assert!(prompt.contains("300 words"));
}
#[test]
fn test_basic_user_and_assistant_formatting() {
let messages = vec![
make_message(MessageRole::User, "Fix the login bug", 0),
make_message(MessageRole::Assistant, "I found the issue in auth.rs", 1),
];
let result = prepare_conversation(&messages, 10_000);
assert!(result.contains("[User] (2024-01-15 14:30 UTC)"));
assert!(result.contains("Fix the login bug"));
assert!(result.contains("[Assistant]"));
assert!(result.contains("I found the issue in auth.rs"));
assert!(!result.contains("[Assistant] ("));
}
#[test]
fn test_system_message_formatting() {
let messages = vec![make_message(
MessageRole::System,
"You are a coding assistant",
0,
)];
let result = prepare_conversation(&messages, 10_000);
assert!(result.contains("[System] (2024-01-15 14:30 UTC)"));
assert!(result.contains("You are a coding assistant"));
}
#[test]
fn test_empty_messages_returns_empty_string() {
let messages: Vec<Message> = vec![];
let result = prepare_conversation(&messages, 10_000);
assert!(result.is_empty());
}
#[test]
fn test_single_message_formatting() {
let messages = vec![make_message(MessageRole::User, "Hello", 0)];
let result = prepare_conversation(&messages, 10_000);
assert_eq!(result, "[User] (2024-01-15 14:30 UTC)\nHello");
}
#[test]
fn test_tool_only_messages_handled_gracefully() {
let messages = vec![
make_message(MessageRole::User, "Read that file", 0),
make_tool_only_message(1),
make_message(MessageRole::Assistant, "Done reading", 2),
];
let result = prepare_conversation(&messages, 10_000);
assert!(result.contains("Read that file"));
assert!(result.contains("Done reading"));
assert!(!result.contains("\n\n\n\n"));
}
#[test]
fn test_truncation_with_head_and_tail_strategy() {
let mut messages = Vec::new();
for i in 0..20 {
let role = if i % 2 == 0 {
MessageRole::User
} else {
MessageRole::Assistant
};
messages.push(make_message(role, &format!("Message number {i}"), i));
}
let result = prepare_conversation(&messages, 500);
assert!(result.contains("Message number 0"));
assert!(result.contains("[... 10 messages omitted ...]"));
assert!(result.contains("Message number 19"));
}
#[test]
fn test_no_truncation_when_under_limit() {
let messages = vec![
make_message(MessageRole::User, "Short", 0),
make_message(MessageRole::Assistant, "Reply", 1),
];
let result = prepare_conversation(&messages, 10_000);
assert!(!result.contains("omitted"));
}
#[test]
fn test_truncation_preserves_first_and_last_messages() {
let mut messages = Vec::new();
for i in 0..10 {
messages.push(make_message(MessageRole::User, &format!("msg-{i}"), i));
}
let result = prepare_conversation(&messages, 250);
assert!(result.contains("msg-0"));
assert!(result.contains("msg-1"));
assert!(result.contains("msg-7"));
assert!(result.contains("msg-8"));
assert!(result.contains("msg-9"));
assert!(result.contains("[... 5 messages omitted ...]"));
}
#[test]
fn test_all_tool_only_messages_produce_empty_output() {
let messages = vec![make_tool_only_message(0), make_tool_only_message(1)];
let result = prepare_conversation(&messages, 10_000);
assert!(result.is_empty());
}
#[test]
fn test_max_chars_zero_returns_full_conversation() {
let messages = vec![
make_message(MessageRole::User, "Hello", 0),
make_message(MessageRole::Assistant, "World", 1),
];
let result = prepare_conversation(&messages, 0);
assert!(result.contains("Hello"));
assert!(result.contains("World"));
}
#[test]
fn test_few_messages_no_truncation_when_head_tail_covers_all() {
let two_messages = vec![
make_message(MessageRole::User, "Alpha", 0),
make_message(MessageRole::Assistant, "Beta", 1),
];
let full = prepare_conversation(&two_messages, 10_000);
let truncated = prepare_conversation(&two_messages, 1);
assert!(!full.contains("omitted"));
assert!(truncated.len() <= full.len());
}
#[test]
fn test_small_message_set_truncation() {
let messages = vec![
make_message(
MessageRole::User,
"The very first user message in the session",
0,
),
make_message(
MessageRole::Assistant,
"A long middle response that should be omitted from output",
1,
),
make_message(
MessageRole::User,
"Another middle message that should be omitted from output",
2,
),
make_message(
MessageRole::Assistant,
"The penultimate message in the tail section",
3,
),
make_message(
MessageRole::User,
"The final message in the conversation",
4,
),
];
let full = prepare_conversation(&messages, 10_000);
let result = prepare_conversation(&messages, full.len() - 1);
assert!(result.contains("The very first user message"));
assert!(result.contains("penultimate message"));
assert!(result.contains("final message"));
assert!(result.contains("[... 2 messages omitted ...]"));
assert!(!result.contains("A long middle response"));
assert!(!result.contains("Another middle message"));
}
}