agent_core/core/
host_note.rs1use serde_json::Value;
12
13pub const CANCEL_PHRASES: &[&str] = &[
17 "Canceled by user",
18 "Canceled (session cost cap reached)",
19 "Canceled (Synaps restarted)",
20 "Canceled (session stopped by the host)",
21 "Canceled (session driver revoked)",
22 "Canceled",
23];
24
25pub const UNRECOGNISED_TOOL: &str = "(an unrecognised tool)";
27
28pub fn is_plausible_tool_name(name: &str) -> bool {
34 (1..=128).contains(&name.len())
35 && name.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b':' | b'-'))
36}
37
38pub fn tool_in_flight_note_text(phrase: &str, tools: &[String]) -> String {
43 let names = tools
44 .iter()
45 .map(|t| if is_plausible_tool_name(t) { format!("`{t}`") } else { UNRECOGNISED_TOOL.into() })
46 .collect::<Vec<_>>()
47 .join(", ");
48 let (call, was, it, its) = if tools.len() == 1 {
49 ("tool call", "was", "It", "its")
50 } else {
51 ("tool calls", "were", "They", "their")
52 };
53 format!(
54 "[{phrase} while the {call} {names} {was} running. {it} may have already started and \
55 partially applied {its} effects.]"
56 )
57}
58
59pub fn tool_in_flight_note_label(text: &str) -> Option<&str> {
66 if text.contains(['\n', '\r']) {
67 return None;
68 }
69 for phrase in CANCEL_PHRASES {
70 for (call, was) in [("tool call", "was"), ("tool calls", "were")] {
71 let Some(rest) = text.strip_prefix(&format!("[{phrase} while the {call} ")) else {
72 continue;
73 };
74 let Some(end) = rest.find(&format!(" {was} running. ")) else {
75 continue;
76 };
77 let tools: Option<Vec<String>> = rest[..end]
79 .split(", ")
80 .map(|item| match item.strip_prefix('`').and_then(|i| i.strip_suffix('`')) {
81 Some(name) if is_plausible_tool_name(name) => Some(name.to_string()),
82 None if item == UNRECOGNISED_TOOL => Some(String::new()),
84 _ => None,
85 })
86 .collect();
87 if tools.is_some_and(|tools| tool_in_flight_note_text(phrase, &tools) == text) {
88 return Some(&text[1..text.len() - 1]);
89 }
90 }
91 }
92 None
93}
94
95pub fn is_tool_in_flight_note(message: &Value) -> bool {
98 message["role"] == "user"
99 && message["content"]
100 .as_str()
101 .is_some_and(|text| tool_in_flight_note_label(text).is_some())
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use serde_json::json;
108
109 #[test]
110 fn generated_notes_round_trip_and_nothing_else_matches() {
111 for phrase in CANCEL_PHRASES {
112 for tools in [vec!["bash".to_string()], vec!["read".into(), "edit".into(), "bash".into()]] {
113 let note = tool_in_flight_note_text(phrase, &tools);
114 assert_eq!(tool_in_flight_note_label(¬e), Some(¬e[1..note.len() - 1]));
115 assert!(is_tool_in_flight_note(&json!({"role": "user", "content": note})));
116 }
117 }
118 let note = tool_in_flight_note_text("Canceled by user", &["bash".into()]);
119 for text in [
120 note.replace("running.", "running.\n"),
121 note.replace("`bash`", "`bash` `x`"),
122 note.replace("`bash`", "`bash"),
123 format!(" {note}"),
124 "[]".into(),
125 "``".into(),
126 ] {
127 assert_eq!(tool_in_flight_note_label(&text), None, "{text:?}");
128 }
129 assert!(!is_tool_in_flight_note(&json!({"role": "assistant", "content": note})));
130 }
131
132 #[test]
137 fn implausible_tool_names_become_the_fallback_and_stay_recognised() {
138 for bad in ["a`b".to_string(), "a\nb".into(), "a".repeat(200), String::new(), "rm -rf /".into()] {
139 let note = tool_in_flight_note_text("Canceled by user", std::slice::from_ref(&bad));
140 assert_eq!(
141 note,
142 "[Canceled by user while the tool call (an unrecognised tool) was running. It \
143 may have already started and partially applied its effects.]",
144 "{bad:?}"
145 );
146 assert!(tool_in_flight_note_label(¬e).is_some(), "{bad:?}");
147 }
148 let mixed = tool_in_flight_note_text("Canceled", &["read".into(), "x`y".into()]);
149 assert_eq!(
150 mixed,
151 "[Canceled while the tool calls `read`, (an unrecognised tool) were running. They \
152 may have already started and partially applied their effects.]"
153 );
154 assert!(tool_in_flight_note_label(&mixed).is_some());
155 let kept = tool_in_flight_note_text("Canceled", &["mcp__fs.read:v2-x".into(), "a".repeat(128)]);
157 assert!(kept.contains("`mcp__fs.read:v2-x`") && kept.contains(&format!("`{}`", "a".repeat(128))));
158 assert!(tool_in_flight_note_label(&kept).is_some());
159 let forged = "[Canceled while the tool call `a b` was running. It may have already started \
161 and partially applied its effects.]";
162 assert_eq!(tool_in_flight_note_label(forged), None);
163 }
164}