llama_cpp_v3_agent_sdk/
conversation.rs1use crate::tool::{ToolCall, ToolResult};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Message {
7 pub role: Role,
8 pub content: String,
9 #[serde(skip_serializing_if = "Vec::is_empty", default)]
12 pub tool_calls: Vec<ToolCall>,
13 #[serde(skip_serializing_if = "Option::is_none", default)]
15 pub tool_result: Option<ToolCallWithResult>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19#[serde(rename_all = "lowercase")]
20pub enum Role {
21 System,
22 User,
23 Assistant,
24 Tool,
25}
26
27impl Role {
28 pub fn as_str(&self) -> &str {
29 match self {
30 Role::System => "system",
31 Role::User => "user",
32 Role::Assistant => "assistant",
33 Role::Tool => "tool",
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ToolCallWithResult {
40 pub call: ToolCall,
41 pub result: ToolResult,
42}
43
44pub struct Conversation {
46 messages: Vec<Message>,
47}
48
49impl Conversation {
50 pub fn new() -> Self {
51 Self {
52 messages: Vec::new(),
53 }
54 }
55
56 pub fn with_system_prompt(system_prompt: &str) -> Self {
58 let mut conv = Self::new();
59 conv.add_system(system_prompt);
60 conv
61 }
62
63 pub fn add_system(&mut self, content: &str) {
65 self.messages.push(Message {
66 role: Role::System,
67 content: content.to_string(),
68 tool_calls: vec![],
69 tool_result: None,
70 });
71 }
72
73 pub fn add_user(&mut self, content: &str) {
75 self.messages.push(Message {
76 role: Role::User,
77 content: content.to_string(),
78 tool_calls: vec![],
79 tool_result: None,
80 });
81 }
82
83 pub fn add_assistant(&mut self, content: &str, tool_calls: Vec<ToolCall>) {
85 self.messages.push(Message {
86 role: Role::Assistant,
87 content: content.to_string(),
88 tool_calls,
89 tool_result: None,
90 });
91 }
92
93 pub fn add_tool_result(&mut self, call: ToolCall, result: ToolResult) {
95 let content = if result.success {
96 format!("[Tool: {}] {}", call.name, result.output)
97 } else {
98 format!("[Tool: {} ERROR] {}", call.name, result.output)
99 };
100 self.messages.push(Message {
101 role: Role::Tool,
102 content,
103 tool_calls: vec![],
104 tool_result: Some(ToolCallWithResult { call, result }),
105 });
106 }
107
108 pub fn to_chat_messages(&self) -> Vec<llama_cpp_v3::ChatMessage> {
110 self.messages
111 .iter()
112 .map(|m| llama_cpp_v3::ChatMessage {
113 role: m.role.as_str().to_string(),
114 content: m.content.clone(),
115 })
116 .collect()
117 }
118
119 pub fn messages(&self) -> &[Message] {
121 &self.messages
122 }
123
124 pub fn clear(&mut self) {
126 self.messages.clear();
127 }
128
129 pub fn len(&self) -> usize {
131 self.messages.len()
132 }
133
134 pub fn is_empty(&self) -> bool {
135 self.messages.is_empty()
136 }
137
138 pub fn compact(&mut self, summary: &str, keep_recent: usize) {
143 if self.messages.len() <= keep_recent + 1 {
144 return; }
146
147 let system_msg = if !self.messages.is_empty() && self.messages[0].role == Role::System {
148 Some(self.messages[0].clone())
149 } else {
150 None
151 };
152
153 let total = self.messages.len();
154 let start = if system_msg.is_some() { 1 } else { 0 };
155 let keep_from = if total > keep_recent {
156 total - keep_recent
157 } else {
158 start
159 };
160
161 let keep_from = self.find_safe_cut_point(keep_from);
163
164 let recent: Vec<Message> = self.messages[keep_from..].to_vec();
165
166 self.messages.clear();
167
168 if let Some(sys) = system_msg {
169 self.messages.push(sys);
170 }
171
172 self.messages.push(Message {
174 role: Role::System,
175 content: format!("[Conversation Summary]\n{}", summary),
176 tool_calls: vec![],
177 tool_result: None,
178 });
179
180 self.messages.extend(recent);
181 }
182
183 pub fn find_safe_cut_point(&self, target_idx: usize) -> usize {
189 let start = if !self.messages.is_empty() && self.messages[0].role == Role::System {
190 1
191 } else {
192 0
193 };
194
195 if target_idx <= start {
196 return start;
197 }
198
199 let mut idx = target_idx.min(self.messages.len());
200
201 while idx > start {
204 let msg = &self.messages[idx.saturating_sub(1)];
205 if msg.role == Role::Tool {
208 idx -= 1;
209 } else if msg.role == Role::Assistant && !msg.tool_calls.is_empty() {
210 idx -= 1;
213 if idx <= start {
214 break;
215 }
216 } else {
217 break;
218 }
219 }
220
221 idx.max(start)
222 }
223
224 pub fn serialize_range(&self, from: usize, to: usize) -> String {
227 let mut lines = Vec::new();
228 for msg in &self.messages[from..to] {
229 let role = match msg.role {
230 Role::System => "System",
231 Role::User => "User",
232 Role::Assistant => "Assistant",
233 Role::Tool => "Tool",
234 };
235 lines.push(format!("[{}]: {}", role, msg.content));
236 }
237 lines.join("\n\n")
238 }
239
240 pub fn compactable_count(&self, keep_recent: usize) -> usize {
243 let start = if !self.messages.is_empty() && self.messages[0].role == Role::System {
244 1
245 } else {
246 0
247 };
248 let total = self.messages.len();
249 if total <= keep_recent + start {
250 0
251 } else {
252 total - keep_recent - start
253 }
254 }
255}
256
257impl Default for Conversation {
258 fn default() -> Self {
259 Self::new()
260 }
261}