1use crate::vlm::Message;
14use std::collections::HashSet;
15
16pub struct ToolResultHistory {
19 pub tool_messages: Vec<Message>,
20 pub followup_messages: Vec<Message>,
21}
22
23fn is_image_value(v: &serde_json::Value) -> bool {
24 v.get("image_base64")
25 .and_then(|x| x.as_str())
26 .is_some_and(|data| !data.is_empty())
27 || (v.get("width").is_some()
28 && v.get("height").is_some()
29 && v.get("encoding")
30 .and_then(|encoding| encoding.as_str())
31 .is_some_and(|encoding| encoding != "error")
32 && v.get("data")
33 .and_then(|data| data.as_str())
34 .is_some_and(|data| !data.is_empty()))
35}
36
37pub fn is_image_output(output: &str) -> bool {
41 serde_json::from_str::<serde_json::Value>(output).is_ok_and(|value| is_image_value(&value))
42}
43
44pub fn tool_result_to_messages(call_id: &str, output: &str) -> ToolResultHistory {
53 let Ok(v) = serde_json::from_str::<serde_json::Value>(output) else {
56 return ToolResultHistory {
57 tool_messages: vec![Message::tool_result(call_id, output)],
58 followup_messages: vec![],
59 };
60 };
61
62 if is_image_value(&v)
63 && let Some(b64) = v.get("image_base64").and_then(|x| x.as_str())
64 {
65 let fmt = v.get("format").and_then(|x| x.as_str()).unwrap_or("jpeg");
66 return ToolResultHistory {
67 tool_messages: vec![Message::tool_result(
68 call_id,
69 &format!("[{fmt} image attached]"),
70 )],
71 followup_messages: vec![Message::user_with_image(
72 "Executor feedback: the previous capability call returned this image. Use it as observation data for the current task; this is not a new user request.",
73 b64.to_string(),
74 )],
75 };
76 }
77
78 let img_encoding = v.get("encoding").and_then(|e| e.as_str());
81 if is_image_value(&v) && img_encoding.is_some() {
82 let enc = img_encoding.unwrap_or("jpeg");
83 let b64 = v.get("data").and_then(|d| d.as_str()).unwrap_or("");
84 return ToolResultHistory {
85 tool_messages: vec![Message::tool_result(
86 call_id,
87 &format!("[sensor_msgs/Image encoding={enc}]"),
88 )],
89 followup_messages: vec![Message::user_with_image(
90 "Executor feedback: the previous capability call returned this image. Use it as observation data for the current task; this is not a new user request.",
91 b64.to_string(),
92 )],
93 };
94 }
95
96 ToolResultHistory {
97 tool_messages: vec![Message::tool_result(call_id, output)],
98 followup_messages: vec![],
99 }
100}
101
102pub fn trim(history: &mut Vec<Message>, max: usize) {
104 if history.len() > max {
105 let remove = history.len() - max;
106 history.drain(0..remove);
107 }
108}
109
110pub fn sanitize_for_vlm(history: &[Message]) -> Vec<Message> {
115 let mut out: Vec<Message> = Vec::with_capacity(history.len());
116 let mut open_tool_call_ids: HashSet<String> = Default::default();
117
118 for msg in history {
119 match msg.role.as_str() {
120 "assistant" => {
121 open_tool_call_ids.clear();
122 if let Some(calls) = &msg.tool_calls {
123 for tc in calls {
124 open_tool_call_ids.insert(tc.id.clone());
125 }
126 }
127 out.push(msg.clone());
128 }
129 "tool" => {
130 let Some(call_id) = msg.tool_call_id.as_ref() else {
131 continue;
132 };
133 if open_tool_call_ids.remove(call_id) {
134 out.push(msg.clone());
135 }
136 }
137 _ => {
138 open_tool_call_ids.clear();
139 out.push(msg.clone());
140 }
141 }
142 }
143 out
144}