codetether_agent/provider/bedrock/
convert.rs1use crate::provider::{ContentPart, Message, Role, ToolDefinition};
42use serde_json::{Value, json};
43
44pub fn convert_messages(messages: &[Message]) -> (Vec<Value>, Vec<Value>) {
78 let mut system_parts: Vec<Value> = Vec::new();
79 let mut api_messages: Vec<Value> = Vec::new();
80
81 for msg in messages {
82 match msg.role {
83 Role::System => append_system(msg, &mut system_parts),
84 Role::User => append_user(msg, &mut api_messages),
85 Role::Assistant => append_assistant(msg, &mut api_messages),
86 Role::Tool => append_tool(msg, &mut api_messages),
87 }
88 }
89
90 repair_orphan_tool_uses(&mut api_messages);
91 (system_parts, api_messages)
92}
93
94pub fn convert_tools(tools: &[ToolDefinition]) -> Vec<Value> {
113 tools
114 .iter()
115 .map(|t| {
116 json!({
117 "toolSpec": {
118 "name": t.name,
119 "description": t.description,
120 "inputSchema": {
121 "json": t.parameters
122 }
123 }
124 })
125 })
126 .collect()
127}
128
129fn append_system(msg: &Message, system_parts: &mut Vec<Value>) {
130 let text: String = msg
131 .content
132 .iter()
133 .filter_map(|p| match p {
134 ContentPart::Text { text } => Some(text.clone()),
135 _ => None,
136 })
137 .collect::<Vec<_>>()
138 .join("\n");
139 if !text.trim().is_empty() {
140 system_parts.push(json!({"text": text}));
141 }
142}
143
144fn append_user(msg: &Message, api_messages: &mut Vec<Value>) {
145 let mut content_parts: Vec<Value> = Vec::new();
146 for part in &msg.content {
147 if let ContentPart::Text { text } = part
148 && !text.trim().is_empty()
149 {
150 content_parts.push(json!({"text": text}));
151 }
152 }
153 if content_parts.is_empty() {
154 return;
155 }
156 if let Some(last) = api_messages.last_mut()
157 && last.get("role").and_then(|r| r.as_str()) == Some("user")
158 && let Some(arr) = last.get_mut("content").and_then(|c| c.as_array_mut())
159 {
160 arr.extend(content_parts);
161 return;
162 }
163 api_messages.push(json!({
164 "role": "user",
165 "content": content_parts
166 }));
167}
168
169fn append_assistant(msg: &Message, api_messages: &mut Vec<Value>) {
170 let mut content_parts: Vec<Value> = Vec::new();
171 for part in &msg.content {
172 match part {
173 ContentPart::Text { text } => {
174 if !text.trim().is_empty() {
175 content_parts.push(json!({"text": text}));
176 }
177 }
178 ContentPart::ToolCall {
179 id,
180 name,
181 arguments,
182 ..
183 } => {
184 let input: Value =
185 serde_json::from_str(arguments).unwrap_or_else(|_| json!({"raw": arguments}));
186 content_parts.push(json!({
187 "toolUse": {
188 "toolUseId": id,
189 "name": name,
190 "input": input
191 }
192 }));
193 }
194 _ => {}
195 }
196 }
197 if content_parts.is_empty() {
199 return;
200 }
201 if let Some(last) = api_messages.last_mut()
202 && last.get("role").and_then(|r| r.as_str()) == Some("assistant")
203 && let Some(arr) = last.get_mut("content").and_then(|c| c.as_array_mut())
204 {
205 arr.extend(content_parts);
206 return;
207 }
208 api_messages.push(json!({
209 "role": "assistant",
210 "content": content_parts
211 }));
212}
213
214fn append_tool(msg: &Message, api_messages: &mut Vec<Value>) {
215 let mut content_parts: Vec<Value> = Vec::new();
216 for part in &msg.content {
217 if let ContentPart::ToolResult {
218 tool_call_id,
219 content,
220 } = part
221 {
222 let content = if content.trim().is_empty() {
223 "(empty tool result)".to_string()
224 } else {
225 content.clone()
226 };
227 content_parts.push(json!({
228 "toolResult": {
229 "toolUseId": tool_call_id,
230 "content": [{"text": content}],
231 "status": "success"
232 }
233 }));
234 }
235 }
236 if content_parts.is_empty() {
237 return;
238 }
239 if let Some(last) = api_messages.last_mut()
240 && last.get("role").and_then(|r| r.as_str()) == Some("user")
241 && let Some(arr) = last.get_mut("content").and_then(|c| c.as_array_mut())
242 {
243 arr.extend(content_parts);
244 return;
245 }
246 api_messages.push(json!({
247 "role": "user",
248 "content": content_parts
249 }));
250}
251
252fn repair_orphan_tool_uses(api_messages: &mut Vec<Value>) {
273 let mut i = 0;
274 while i < api_messages.len() {
275 if api_messages[i].get("role").and_then(Value::as_str) != Some("assistant") {
276 i += 1;
277 continue;
278 }
279 let declared_ids: Vec<String> = api_messages[i]
280 .get("content")
281 .and_then(Value::as_array)
282 .map(|arr| {
283 arr.iter()
284 .filter_map(|p| {
285 p.get("toolUse")
286 .and_then(|tu| tu.get("toolUseId"))
287 .and_then(Value::as_str)
288 .map(String::from)
289 })
290 .collect()
291 })
292 .unwrap_or_default();
293 if declared_ids.is_empty() {
294 i += 1;
295 continue;
296 }
297
298 let satisfied_ids: Vec<String> = api_messages
299 .get(i + 1)
300 .filter(|m| m.get("role").and_then(Value::as_str) == Some("user"))
301 .and_then(|m| m.get("content").and_then(Value::as_array))
302 .map(|arr| {
303 arr.iter()
304 .filter_map(|p| {
305 p.get("toolResult")
306 .and_then(|tr| tr.get("toolUseId"))
307 .and_then(Value::as_str)
308 .map(String::from)
309 })
310 .collect()
311 })
312 .unwrap_or_default();
313
314 let missing: Vec<String> = declared_ids
315 .into_iter()
316 .filter(|id| !satisfied_ids.contains(id))
317 .collect();
318 if missing.is_empty() {
319 i += 1;
320 continue;
321 }
322
323 let synthetic: Vec<Value> = missing
324 .iter()
325 .map(|id| {
326 json!({
327 "toolResult": {
328 "toolUseId": id,
329 "content": [{"text": "(tool call interrupted; no result recorded)"}],
330 "status": "error"
331 }
332 })
333 })
334 .collect();
335
336 let next_is_user = api_messages
337 .get(i + 1)
338 .and_then(|m| m.get("role").and_then(Value::as_str))
339 == Some("user");
340
341 if next_is_user {
342 if let Some(arr) = api_messages[i + 1]
343 .get_mut("content")
344 .and_then(Value::as_array_mut)
345 {
346 let mut merged = synthetic;
347 merged.extend(arr.drain(..));
348 *arr = merged;
349 }
350 } else {
351 api_messages.insert(
352 i + 1,
353 json!({
354 "role": "user",
355 "content": synthetic
356 }),
357 );
358 }
359 i += 1;
360 }
361}
362
363#[cfg(test)]
364mod repair_tests {
365 use super::*;
366
367 #[test]
368 fn synthesizes_missing_tool_result_when_assistant_is_last() {
369 let mut msgs = vec![json!({
370 "role": "assistant",
371 "content": [{"toolUse": {"toolUseId": "call_x", "name": "t", "input": {}}}]
372 })];
373 repair_orphan_tool_uses(&mut msgs);
374 assert_eq!(msgs.len(), 2);
375 assert_eq!(msgs[1]["role"], "user");
376 assert_eq!(msgs[1]["content"][0]["toolResult"]["toolUseId"], "call_x");
377 assert_eq!(msgs[1]["content"][0]["toolResult"]["status"], "error");
378 }
379
380 #[test]
381 fn prepends_missing_result_into_existing_user_turn() {
382 let mut msgs = vec![
383 json!({
384 "role": "assistant",
385 "content": [{"toolUse": {"toolUseId": "call_a", "name": "t", "input": {}}}]
386 }),
387 json!({
388 "role": "user",
389 "content": [{"text": "continue"}]
390 }),
391 ];
392 repair_orphan_tool_uses(&mut msgs);
393 assert_eq!(msgs.len(), 2);
394 assert_eq!(msgs[1]["content"][0]["toolResult"]["toolUseId"], "call_a");
395 assert_eq!(msgs[1]["content"][1]["text"], "continue");
396 }
397
398 #[test]
399 fn leaves_already_paired_tool_uses_alone() {
400 let before = vec![
401 json!({
402 "role": "assistant",
403 "content": [{"toolUse": {"toolUseId": "call_ok", "name": "t", "input": {}}}]
404 }),
405 json!({
406 "role": "user",
407 "content": [{"toolResult": {"toolUseId": "call_ok", "content": [{"text": "ok"}], "status": "success"}}]
408 }),
409 ];
410 let mut after = before.clone();
411 repair_orphan_tool_uses(&mut after);
412 assert_eq!(before, after);
413 }
414
415 #[test]
416 fn handles_multiple_missing_ids_in_one_assistant_turn() {
417 let mut msgs = vec![json!({
418 "role": "assistant",
419 "content": [
420 {"toolUse": {"toolUseId": "call_1", "name": "t", "input": {}}},
421 {"toolUse": {"toolUseId": "call_2", "name": "t", "input": {}}}
422 ]
423 })];
424 repair_orphan_tool_uses(&mut msgs);
425 assert_eq!(msgs[1]["content"].as_array().unwrap().len(), 2);
426 assert_eq!(msgs[1]["content"][0]["toolResult"]["toolUseId"], "call_1");
427 assert_eq!(msgs[1]["content"][1]["toolResult"]["toolUseId"], "call_2");
428 }
429}