agent_engine/engine/
stream.rs1pub fn repair_history_after_failure(
12 messages: &mut Vec<crate::SharedMessage>,
13 turn_baseline: usize,
14) {
15 fn trailing_is_valid(msg: &serde_json::Value) -> bool {
16 match msg["role"].as_str() {
17 Some("user") => true,
20 Some("assistant") => match &msg["content"] {
21 serde_json::Value::String(text) => !text.is_empty(),
22 serde_json::Value::Array(blocks) => {
23 !blocks.is_empty()
24 && !blocks
25 .iter()
26 .any(|b| b["type"].as_str() == Some("tool_use"))
27 }
28 _ => false,
29 },
30 _ => false,
31 }
32 }
33
34 while messages.len() > turn_baseline {
35 match messages.last() {
36 Some(last) if trailing_is_valid(last) => break,
37 _ => {
38 messages.pop();
39 }
40 }
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47 use serde_json::json;
48 use std::sync::Arc;
49
50 fn msg(v: serde_json::Value) -> crate::SharedMessage {
51 Arc::new(v)
52 }
53
54 #[test]
57 fn error_repair_keeps_preexisting_trailing_user_message() {
58 let mut messages = vec![msg(json!({"role": "user", "content": "hello"}))];
59 repair_history_after_failure(&mut messages, 1);
60 assert_eq!(
61 messages.len(),
62 1,
63 "pre-existing trailing user message must survive turn failure"
64 );
65 assert_eq!(messages[0]["content"], "hello");
66 }
67
68 #[test]
71 fn error_repair_keeps_partial_assistant_output_and_tool_results() {
72 let mut messages = vec![
73 msg(json!({"role": "user", "content": "do the thing"})),
74 msg(json!({"role": "assistant", "content": [
75 {"type": "text", "text": "working on it"},
76 {"type": "tool_use", "id": "tu_1", "name": "bash", "input": {}},
77 ]})),
78 msg(json!({"role": "user", "content": [
79 {"type": "tool_result", "tool_use_id": "tu_1", "content": "ok"},
80 ]})),
81 msg(json!({"role": "assistant", "content": [
82 {"type": "text", "text": "partial answer before the failure"},
83 ]})),
84 ];
85 repair_history_after_failure(&mut messages, 1);
86 assert_eq!(
87 messages.len(),
88 4,
89 "valid partial assistant output and completed tool results must survive"
90 );
91 assert_eq!(
92 messages[3]["content"][0]["text"],
93 "partial answer before the failure"
94 );
95 }
96
97 #[test]
100 fn error_repair_drops_turn_appended_unmatched_tool_use() {
101 let mut messages = vec![
102 msg(json!({"role": "user", "content": "do the thing"})),
103 msg(json!({"role": "assistant", "content": [
104 {"type": "tool_use", "id": "tu_1", "name": "bash", "input": {}},
105 ]})),
106 ];
107 repair_history_after_failure(&mut messages, 1);
108 assert_eq!(
109 messages.len(),
110 1,
111 "unmatched trailing tool_use appended by the turn must be removed"
112 );
113 assert_eq!(messages[0]["content"], "do the thing");
114 }
115}