opendev_context/
validated_list.rs1use std::collections::HashSet;
15use std::sync::Mutex;
16
17use tracing::warn;
18
19use crate::compaction::ApiMessage;
20
21pub const SYNTHETIC_TOOL_RESULT: &str =
23 "Error: Tool execution result was lost. The tool may have been interrupted or crashed.";
24
25pub struct ValidatedMessageList {
30 messages: Vec<ApiMessage>,
31 pending_tool_ids: Mutex<HashSet<String>>,
32 strict: bool,
33}
34
35impl ValidatedMessageList {
36 pub fn new(initial: Option<Vec<ApiMessage>>, strict: bool) -> Self {
41 let messages = initial.unwrap_or_default();
42 let mut list = Self {
43 messages,
44 pending_tool_ids: Mutex::new(HashSet::new()),
45 strict,
46 };
47 list.rebuild_pending_state();
48 list
49 }
50
51 pub fn messages(&self) -> &[ApiMessage] {
53 &self.messages
54 }
55
56 pub fn into_inner(self) -> Vec<ApiMessage> {
58 self.messages
59 }
60
61 pub fn len(&self) -> usize {
62 self.messages.len()
63 }
64
65 pub fn is_empty(&self) -> bool {
66 self.messages.is_empty()
67 }
68
69 pub fn pending_tool_ids(&self) -> HashSet<String> {
71 self.pending_tool_ids.lock().unwrap().clone()
72 }
73
74 pub fn has_pending_tools(&self) -> bool {
76 !self.pending_tool_ids.lock().unwrap().is_empty()
77 }
78
79 pub fn add_user(&mut self, content: &str) {
81 self.auto_complete_pending("add_user");
82 let mut msg = ApiMessage::new();
83 msg.insert(
84 "role".to_string(),
85 serde_json::Value::String("user".to_string()),
86 );
87 msg.insert(
88 "content".to_string(),
89 serde_json::Value::String(content.to_string()),
90 );
91 self.messages.push(msg);
92 }
93
94 pub fn add_assistant(
96 &mut self,
97 content: Option<&str>,
98 tool_calls: Option<Vec<serde_json::Value>>,
99 ) {
100 self.auto_complete_pending("add_assistant");
101 let mut msg = ApiMessage::new();
102 msg.insert(
103 "role".to_string(),
104 serde_json::Value::String("assistant".to_string()),
105 );
106 msg.insert(
107 "content".to_string(),
108 serde_json::Value::String(content.unwrap_or("").to_string()),
109 );
110 if let Some(tcs) = tool_calls {
111 let mut pending = self.pending_tool_ids.lock().unwrap();
112 for tc in &tcs {
113 if let Some(id) = tc.get("id").and_then(|v| v.as_str())
114 && !id.is_empty()
115 {
116 pending.insert(id.to_string());
117 }
118 }
119 msg.insert("tool_calls".to_string(), serde_json::Value::Array(tcs));
120 }
121 self.messages.push(msg);
122 }
123
124 pub fn add_tool_result(&mut self, tool_call_id: &str, content: &str) -> Result<(), String> {
126 let mut pending = self.pending_tool_ids.lock().unwrap();
127 if !pending.contains(tool_call_id) {
128 let detail = format!("Orphaned tool result for id={tool_call_id}");
129 if self.strict {
130 return Err(detail);
131 }
132 warn!(
133 "ValidatedMessageList: {} (permissive mode, accepting)",
134 detail
135 );
136 } else {
137 pending.remove(tool_call_id);
138 }
139 drop(pending);
140
141 let mut msg = ApiMessage::new();
142 msg.insert(
143 "role".to_string(),
144 serde_json::Value::String("tool".to_string()),
145 );
146 msg.insert(
147 "tool_call_id".to_string(),
148 serde_json::Value::String(tool_call_id.to_string()),
149 );
150 msg.insert(
151 "content".to_string(),
152 serde_json::Value::String(content.to_string()),
153 );
154 self.messages.push(msg);
155 Ok(())
156 }
157
158 pub fn add_tool_results_batch(
160 &mut self,
161 tool_calls: &[serde_json::Value],
162 results_by_id: &std::collections::HashMap<String, String>,
163 ) {
164 let mut pending = self.pending_tool_ids.lock().unwrap();
165 for tc in tool_calls {
166 let tc_id = tc.get("id").and_then(|v| v.as_str()).unwrap_or("");
167 if tc_id.is_empty() {
168 continue;
169 }
170 let content = if let Some(result) = results_by_id.get(tc_id) {
171 result.clone()
172 } else {
173 let tool_name = tc
174 .get("function")
175 .and_then(|f| f.get("name"))
176 .and_then(|n| n.as_str())
177 .unwrap_or("unknown");
178 warn!(
179 "ValidatedMessageList: Missing result for {} (id={}), inserting synthetic error",
180 tool_name, tc_id
181 );
182 SYNTHETIC_TOOL_RESULT.to_string()
183 };
184 pending.remove(tc_id);
185
186 let mut msg = ApiMessage::new();
187 msg.insert(
188 "role".to_string(),
189 serde_json::Value::String("tool".to_string()),
190 );
191 msg.insert(
192 "tool_call_id".to_string(),
193 serde_json::Value::String(tc_id.to_string()),
194 );
195 msg.insert("content".to_string(), serde_json::Value::String(content));
196 drop(pending);
197 self.messages.push(msg);
198 pending = self.pending_tool_ids.lock().unwrap();
199 }
200 }
201
202 pub fn replace_all(&mut self, messages: Vec<ApiMessage>) {
204 self.messages = messages;
205 self.rebuild_pending_state();
206 }
207
208 fn rebuild_pending_state(&mut self) {
210 let mut expected: HashSet<String> = HashSet::new();
211 for msg in &self.messages {
212 let role = msg.get("role").and_then(|v| v.as_str()).unwrap_or("");
213 if role == "assistant" {
214 if let Some(tcs) = msg.get("tool_calls").and_then(|v| v.as_array()) {
215 for tc in tcs {
216 if let Some(id) = tc.get("id").and_then(|v| v.as_str())
217 && !id.is_empty()
218 {
219 expected.insert(id.to_string());
220 }
221 }
222 }
223 } else if role == "tool"
224 && let Some(id) = msg.get("tool_call_id").and_then(|v| v.as_str())
225 {
226 expected.remove(id);
227 }
228 }
229 *self.pending_tool_ids.lock().unwrap() = expected;
230 }
231
232 fn auto_complete_pending(&mut self, source: &str) {
234 let mut pending = self.pending_tool_ids.lock().unwrap();
235 if pending.is_empty() {
236 return;
237 }
238 warn!(
239 "ValidatedMessageList: Auto-completing {} pending tool results before {}: {:?}",
240 pending.len(),
241 source,
242 pending,
243 );
244 let ids: Vec<String> = pending.drain().collect();
245 drop(pending);
246
247 for tc_id in ids {
248 let mut msg = ApiMessage::new();
249 msg.insert(
250 "role".to_string(),
251 serde_json::Value::String("tool".to_string()),
252 );
253 msg.insert("tool_call_id".to_string(), serde_json::Value::String(tc_id));
254 msg.insert(
255 "content".to_string(),
256 serde_json::Value::String(SYNTHETIC_TOOL_RESULT.to_string()),
257 );
258 self.messages.push(msg);
259 }
260 }
261}
262
263#[cfg(test)]
264#[path = "validated_list_tests.rs"]
265mod tests;