use oxi_ai::Message;
#[derive(Debug, Clone)]
pub struct AppendOnlyContext {
messages: Vec<Message>,
pending_tool_results: Vec<Message>,
}
impl AppendOnlyContext {
pub fn new(messages: Vec<Message>) -> Self {
Self {
messages,
pending_tool_results: Vec::new(),
}
}
pub fn empty() -> Self {
Self::new(Vec::new())
}
pub fn append(&mut self, msg: Message) {
self.messages.push(msg);
}
pub fn queue_tool_result(&mut self, msg: Message) {
self.pending_tool_results.push(msg);
}
pub fn finalize_turn(&mut self) {
self.messages.append(&mut self.pending_tool_results);
}
pub fn replace_history(&mut self, new_history: Vec<Message>) {
self.messages = new_history;
self.pending_tool_results.clear();
}
pub fn build_messages(&self) -> Vec<Message> {
let mut all = self.messages.clone();
all.extend(self.pending_tool_results.iter().cloned());
all
}
pub fn history(&self) -> &[Message] {
&self.messages
}
pub fn pending_results(&self) -> &[Message] {
&self.pending_tool_results
}
pub fn len(&self) -> usize {
self.messages.len() + self.pending_tool_results.len()
}
pub fn is_empty(&self) -> bool {
self.messages.is_empty() && self.pending_tool_results.is_empty()
}
pub fn sync_from(&mut self, external: &[Message]) -> usize {
if external.len() <= self.messages.len() {
return 0;
}
let new_count = external.len() - self.messages.len();
for msg in &external[self.messages.len()..] {
self.messages.push(msg.clone());
}
new_count
}
pub fn into_messages(self) -> Vec<Message> {
let mut all = self.messages;
all.extend(self.pending_tool_results);
all
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxi_ai::{ContentBlock, TextContent};
#[test]
fn test_empty_context() {
let ctx = AppendOnlyContext::empty();
assert!(ctx.is_empty());
assert_eq!(ctx.len(), 0);
}
#[test]
fn test_append_and_build() {
let mut ctx = AppendOnlyContext::empty();
ctx.append(Message::user("Hello"));
ctx.append(Message::user("World"));
assert_eq!(ctx.len(), 2);
assert!(!ctx.is_empty());
let built = ctx.build_messages();
assert_eq!(built.len(), 2);
}
#[test]
fn test_tool_result_queue() {
let mut ctx = AppendOnlyContext::empty();
ctx.append(Message::user("Turn 1"));
ctx.queue_tool_result(Message::ToolResult(oxi_ai::ToolResultMessage::new(
"call-1",
"echo",
vec![ContentBlock::Text(TextContent::new("Result of tool"))],
)));
let built = ctx.build_messages();
assert_eq!(built.len(), 2);
ctx.finalize_turn();
assert_eq!(ctx.len(), 2);
assert!(ctx.pending_results().is_empty());
}
#[test]
fn test_sync_from_appends_only_new() {
let mut ctx = AppendOnlyContext::empty();
ctx.append(Message::user("A"));
ctx.append(Message::user("B"));
let external = vec![Message::user("A"), Message::user("B"), Message::user("C")];
let count = ctx.sync_from(&external);
assert_eq!(count, 1); assert_eq!(ctx.len(), 3);
let count2 = ctx.sync_from(&external);
assert_eq!(count2, 0);
}
#[test]
fn test_into_messages_flattens_all() {
let mut ctx = AppendOnlyContext::empty();
ctx.append(Message::user("A"));
ctx.queue_tool_result(Message::ToolResult(oxi_ai::ToolResultMessage::new(
"call-1",
"echo",
vec![ContentBlock::Text(TextContent::new("result"))],
)));
let all = ctx.into_messages();
assert_eq!(all.len(), 2);
}
#[test]
fn test_prefix_stability() {
let mut ctx = AppendOnlyContext::empty();
ctx.append(Message::user("Turn 1 prompt"));
ctx.queue_tool_result(Message::ToolResult(oxi_ai::ToolResultMessage::new(
"call-1",
"echo",
vec![ContentBlock::Text(TextContent::new("tool result"))],
)));
let before = ctx.build_messages();
assert_eq!(before.len(), 2);
ctx.finalize_turn();
ctx.append(Message::user("Turn 2 prompt"));
let after = ctx.build_messages();
assert_eq!(after.len(), 3);
assert!(after.len() > before.len());
}
}