1use acp_utils::notifications::{
2 SubAgentEvent, SubAgentToolCallUpdate, SubAgentToolError, SubAgentToolRequest, SubAgentToolResult,
3};
4use llm::{ToolCallError, ToolCallRequest, ToolCallResult};
5use mcp_utils::display_meta::ToolResultMeta;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub enum AgentMessage {
11 Text {
12 message_id: String,
13 chunk: String,
14 is_complete: bool,
15 model_name: String,
16 },
17
18 Thought {
19 message_id: String,
20 chunk: String,
21 is_complete: bool,
22 model_name: String,
23 },
24
25 ToolCall {
26 request: ToolCallRequest,
27 model_name: String,
28 },
29
30 ToolCallUpdate {
31 tool_call_id: String,
32 chunk: String,
33 model_name: String,
34 },
35
36 ToolProgress {
37 request: ToolCallRequest,
38 progress: f64,
39 total: Option<f64>,
40 message: Option<String>,
41 },
42
43 ToolResult {
44 result: ToolCallResult,
45 result_meta: Option<ToolResultMeta>,
46 model_name: String,
47 },
48
49 ToolError {
50 error: ToolCallError,
51 model_name: String,
52 },
53
54 Error {
55 message: String,
56 },
57
58 Cancelled {
59 message: String,
60 },
61
62 ContextCompactionStarted {
64 message_count: usize,
65 },
66
67 ContextCompactionResult {
69 summary: String,
70 messages_removed: usize,
71 },
72
73 ContextUsageUpdate {
75 usage_ratio: Option<f64>,
77 context_limit: Option<u32>,
79 input_tokens: u32,
81 output_tokens: u32,
83 cache_read_tokens: Option<u32>,
85 cache_creation_tokens: Option<u32>,
87 reasoning_tokens: Option<u32>,
89 total_input_tokens: u64,
91 total_output_tokens: u64,
93 total_cache_read_tokens: u64,
95 total_cache_creation_tokens: u64,
97 total_reasoning_tokens: u64,
99 },
100
101 AutoContinue {
103 attempt: u32,
105 max_attempts: u32,
107 },
108
109 Retrying {
111 attempt: u32,
113 max_attempts: u32,
115 delay_ms: u64,
117 error: String,
119 },
120
121 ModelSwitched {
123 previous: String,
124 new: String,
125 },
126
127 ContextCleared,
129
130 Done,
131}
132
133impl From<&AgentMessage> for SubAgentEvent {
134 fn from(msg: &AgentMessage) -> Self {
135 match msg {
136 AgentMessage::ToolCall { request, .. } => SubAgentEvent::ToolCall {
137 request: SubAgentToolRequest {
138 id: request.id.clone(),
139 name: request.name.clone(),
140 arguments: request.arguments.clone(),
141 },
142 },
143 AgentMessage::ToolCallUpdate { tool_call_id, chunk, .. } => SubAgentEvent::ToolCallUpdate {
144 update: SubAgentToolCallUpdate { id: tool_call_id.clone(), chunk: chunk.clone() },
145 },
146 AgentMessage::ToolResult { result, result_meta, .. } => SubAgentEvent::ToolResult {
147 result: SubAgentToolResult {
148 id: result.id.clone(),
149 name: result.name.clone(),
150 result_meta: result_meta.clone(),
151 },
152 },
153 AgentMessage::ToolError { error, .. } => {
154 SubAgentEvent::ToolError { error: SubAgentToolError { id: error.id.clone(), name: error.name.clone() } }
155 }
156 AgentMessage::Done => SubAgentEvent::Done,
157 _ => SubAgentEvent::Other,
158 }
159 }
160}
161
162impl AgentMessage {
163 pub fn text(message_id: &str, chunk: &str, is_complete: bool, model_name: &str) -> Self {
164 AgentMessage::Text {
165 message_id: message_id.to_string(),
166 chunk: chunk.to_string(),
167 is_complete,
168 model_name: model_name.to_string(),
169 }
170 }
171
172 pub fn thought(message_id: &str, chunk: &str, is_complete: bool, model_name: &str) -> Self {
173 AgentMessage::Thought {
174 message_id: message_id.to_string(),
175 chunk: chunk.to_string(),
176 is_complete,
177 model_name: model_name.to_string(),
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::AgentMessage;
185 use acp_utils::notifications::SubAgentEvent;
186 use llm::ToolCallResult;
187 use mcp_utils::display_meta::ToolDisplayMeta;
188
189 #[test]
190 fn test_model_switched_serde_roundtrip() {
191 let msg = AgentMessage::ModelSwitched {
192 previous: "anthropic:claude-3.5-sonnet".to_string(),
193 new: "ollama:llama3.2".to_string(),
194 };
195 let json = serde_json::to_string(&msg).unwrap();
196 let parsed: AgentMessage = serde_json::from_str(&json).unwrap();
197 assert_eq!(parsed, msg);
198 }
199
200 #[test]
201 fn test_thought_serde_roundtrip() {
202 let msg = AgentMessage::Thought {
203 message_id: "msg_1".to_string(),
204 chunk: "thinking".to_string(),
205 is_complete: false,
206 model_name: "test-model".to_string(),
207 };
208 let json = serde_json::to_string(&msg).unwrap();
209 let parsed: AgentMessage = serde_json::from_str(&json).unwrap();
210 assert_eq!(parsed, msg);
211 }
212
213 #[test]
214 fn test_thought_complete_serde_roundtrip() {
215 let msg = AgentMessage::Thought {
216 message_id: "msg_1".to_string(),
217 chunk: "full reasoning".to_string(),
218 is_complete: true,
219 model_name: "test-model".to_string(),
220 };
221 let json = serde_json::to_string(&msg).unwrap();
222 let parsed: AgentMessage = serde_json::from_str(&json).unwrap();
223 assert_eq!(parsed, msg);
224 }
225
226 #[test]
227 fn test_tool_result_serializes_result_meta() {
228 let msg = AgentMessage::ToolResult {
229 result: ToolCallResult {
230 id: "call_1".to_string(),
231 name: "coding__read_file".to_string(),
232 arguments: r#"{"filePath":"Cargo.toml"}"#.to_string(),
233 result: "ok".to_string(),
234 },
235 result_meta: Some(ToolDisplayMeta::new("Read file", "Cargo.toml, 156 lines").into()),
236 model_name: "test-model".to_string(),
237 };
238
239 let json = serde_json::to_value(&msg).unwrap();
240 let tool_result = &json["ToolResult"];
241 assert_eq!(tool_result["result_meta"]["display"]["title"], "Read file");
242 assert_eq!(tool_result["result_meta"]["display"]["value"], "Cargo.toml, 156 lines");
243
244 let parsed: AgentMessage = serde_json::from_value(json).unwrap();
245 assert_eq!(parsed, msg);
246 }
247
248 #[test]
249 fn test_sub_agent_tool_result_includes_display_fields() {
250 let msg = AgentMessage::ToolResult {
251 result: ToolCallResult {
252 id: "call_1".to_string(),
253 name: "coding__read_file".to_string(),
254 arguments: r#"{"filePath":"Cargo.toml"}"#.to_string(),
255 result: "ok".to_string(),
256 },
257 result_meta: Some(ToolDisplayMeta::new("Read file", "Cargo.toml, 156 lines").into()),
258 model_name: "test-model".to_string(),
259 };
260
261 let event: SubAgentEvent = (&msg).into();
262 match event {
263 SubAgentEvent::ToolResult { result } => {
264 assert_eq!(result.id, "call_1");
265 assert_eq!(result.name, "coding__read_file");
266 let result_meta = result.result_meta.expect("result_meta should be present");
267 assert_eq!(result_meta.display.title, "Read file");
268 assert_eq!(result_meta.display.value, "Cargo.toml, 156 lines");
269 }
270 other => panic!("Expected ToolResult, got {other:?}"),
271 }
272 }
273
274 #[test]
275 fn test_sub_agent_tool_call_update_includes_updated_fields() {
276 let msg = AgentMessage::ToolCallUpdate {
277 tool_call_id: "call_1".to_string(),
278 chunk: r#"{"filePath":"Cargo.toml"}"#.to_string(),
279 model_name: "test-model".to_string(),
280 };
281
282 let event: SubAgentEvent = (&msg).into();
283 match event {
284 SubAgentEvent::ToolCallUpdate { update } => {
285 assert_eq!(update.id, "call_1");
286 assert_eq!(update.chunk, r#"{"filePath":"Cargo.toml"}"#);
287 }
288 other => panic!("Expected ToolCallUpdate, got {other:?}"),
289 }
290 }
291}