oxicode_agent/agent_loop/
append_only.rs1use oxicode_ai::Message;
15
16#[derive(Debug, Clone)]
22pub struct AppendOnlyContext {
23 messages: Vec<Message>,
25 pending_tool_results: Vec<Message>,
27}
28
29impl AppendOnlyContext {
30 pub fn new(messages: Vec<Message>) -> Self {
32 Self {
33 messages,
34 pending_tool_results: Vec::new(),
35 }
36 }
37
38 pub fn empty() -> Self {
40 Self::new(Vec::new())
41 }
42
43 pub fn append(&mut self, msg: Message) {
45 self.messages.push(msg);
46 }
47
48 pub fn queue_tool_result(&mut self, msg: Message) {
50 self.pending_tool_results.push(msg);
51 }
52
53 pub fn finalize_turn(&mut self) {
56 self.messages.append(&mut self.pending_tool_results);
57 }
58
59 pub fn replace_history(&mut self, new_history: Vec<Message>) {
62 self.messages = new_history;
63 self.pending_tool_results.clear();
64 }
65
66 pub fn build_messages(&self) -> Vec<Message> {
68 let mut all = self.messages.clone();
69 all.extend(self.pending_tool_results.iter().cloned());
70 all
71 }
72
73 pub fn history(&self) -> &[Message] {
75 &self.messages
76 }
77
78 pub fn pending_results(&self) -> &[Message] {
80 &self.pending_tool_results
81 }
82
83 pub fn len(&self) -> usize {
85 self.messages.len() + self.pending_tool_results.len()
86 }
87
88 pub fn is_empty(&self) -> bool {
90 self.messages.is_empty() && self.pending_tool_results.is_empty()
91 }
92
93 pub fn sync_from(&mut self, external: &[Message]) -> usize {
101 if external.len() <= self.messages.len() {
102 return 0;
103 }
104 let new_count = external.len() - self.messages.len();
105 for msg in &external[self.messages.len()..] {
106 self.messages.push(msg.clone());
107 }
108 new_count
109 }
110
111 pub fn into_messages(self) -> Vec<Message> {
113 let mut all = self.messages;
114 all.extend(self.pending_tool_results);
115 all
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use oxicode_ai::{ContentBlock, TextContent};
123
124 #[test]
125 fn test_empty_context() {
126 let ctx = AppendOnlyContext::empty();
127 assert!(ctx.is_empty());
128 assert_eq!(ctx.len(), 0);
129 }
130
131 #[test]
132 fn test_append_and_build() {
133 let mut ctx = AppendOnlyContext::empty();
134 ctx.append(Message::user("Hello"));
135 ctx.append(Message::user("World"));
136
137 assert_eq!(ctx.len(), 2);
138 assert!(!ctx.is_empty());
139
140 let built = ctx.build_messages();
141 assert_eq!(built.len(), 2);
142 }
143
144 #[test]
145 fn test_tool_result_queue() {
146 let mut ctx = AppendOnlyContext::empty();
147 ctx.append(Message::user("Turn 1"));
148
149 ctx.queue_tool_result(Message::ToolResult(oxicode_ai::ToolResultMessage::new(
150 "call-1",
151 "echo",
152 vec![ContentBlock::Text(TextContent::new("Result of tool"))],
153 )));
154
155 let built = ctx.build_messages();
157 assert_eq!(built.len(), 2);
158
159 ctx.finalize_turn();
161 assert_eq!(ctx.len(), 2);
162 assert!(ctx.pending_results().is_empty());
163 }
164
165 #[test]
166 fn test_sync_from_appends_only_new() {
167 let mut ctx = AppendOnlyContext::empty();
168 ctx.append(Message::user("A"));
169 ctx.append(Message::user("B"));
170
171 let external = vec![Message::user("A"), Message::user("B"), Message::user("C")];
173
174 let count = ctx.sync_from(&external);
175 assert_eq!(count, 1); assert_eq!(ctx.len(), 3);
177
178 let count2 = ctx.sync_from(&external);
180 assert_eq!(count2, 0);
181 }
182
183 #[test]
184 fn test_into_messages_flattens_all() {
185 let mut ctx = AppendOnlyContext::empty();
186 ctx.append(Message::user("A"));
187 ctx.queue_tool_result(Message::ToolResult(oxicode_ai::ToolResultMessage::new(
188 "call-1",
189 "echo",
190 vec![ContentBlock::Text(TextContent::new("result"))],
191 )));
192
193 let all = ctx.into_messages();
194 assert_eq!(all.len(), 2);
195 }
196
197 #[test]
198 fn test_prefix_stability() {
199 let mut ctx = AppendOnlyContext::empty();
200 ctx.append(Message::user("Turn 1 prompt"));
201
202 ctx.queue_tool_result(Message::ToolResult(oxicode_ai::ToolResultMessage::new(
204 "call-1",
205 "echo",
206 vec![ContentBlock::Text(TextContent::new("tool result"))],
207 )));
208
209 let before = ctx.build_messages();
210 assert_eq!(before.len(), 2);
211
212 ctx.finalize_turn();
214 ctx.append(Message::user("Turn 2 prompt"));
215
216 let after = ctx.build_messages();
217 assert_eq!(after.len(), 3);
219 assert!(after.len() > before.len());
221 }
222}