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