1use serde::{Deserialize, Serialize};
4
5use crate::tool::ToolProvenance;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "snake_case")]
10pub enum SystemCacheType {
11 Ephemeral,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17pub struct SystemCacheMarker {
18 pub offset: usize,
20 pub length: usize,
22 pub cache_type: SystemCacheType,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28pub struct ToolCall {
29 pub id: String,
31 pub name: String,
33 pub input: serde_json::Value,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct MessageToolResult {
40 pub tool_use_id: String,
42 pub content: String,
44 pub is_error: bool,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50#[serde(tag = "role", rename_all = "snake_case")]
51pub enum Message {
52 System {
54 content: String,
56 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 cache_markers: Vec<SystemCacheMarker>,
59 },
60 Context {
62 content: String,
64 },
65 User {
67 content: String,
69 },
70 Assistant {
72 content: String,
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
76 tool_calls: Vec<ToolCall>,
77 },
78 Tool {
80 result: MessageToolResult,
82 },
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
87#[serde(rename_all = "snake_case")]
88pub enum StopReason {
89 EndTurn,
91 ToolUse,
93 MaxTokens,
95}
96
97#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
99pub struct Usage {
100 pub input_tokens: u32,
102 pub output_tokens: u32,
104 #[serde(default)]
106 pub cache_read_tokens: u32,
107 #[serde(default)]
109 pub cache_write_tokens: u32,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
114#[serde(tag = "type", rename_all = "snake_case")]
115#[non_exhaustive]
116pub enum AgentEvent {
117 TurnStart,
119 TextDelta {
121 delta: String,
123 },
124 ToolCallStarted {
126 name: String,
128 },
129 ToolCall {
131 call: ToolCall,
133 provenance: ToolProvenance,
135 summary_fields: Vec<String>,
137 },
138 ToolResult {
140 result: MessageToolResult,
142 },
143 TurnEnd {
145 stop_reason: StopReason,
147 usage: Usage,
149 },
150 Error {
152 message: String,
154 },
155}
156
157#[cfg(test)]
158#[allow(warnings)]
159#[allow(warnings)]
160#[allow(warnings)]
161#[allow(warnings)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn message_roundtrip_user() {
167 let msg = Message::User {
168 content: "Hello, world!".into(),
169 };
170 let json = serde_json::to_string(&msg).unwrap();
171 let decoded: Message = serde_json::from_str(&json).unwrap();
172 assert_eq!(msg, decoded);
173 }
174
175 #[test]
176 fn message_roundtrip_assistant() {
177 let msg = Message::Assistant {
178 content: "I can help with that.".into(),
179 tool_calls: vec![ToolCall {
180 id: "call_1".into(),
181 name: "read_file".into(),
182 input: serde_json::json!({"path": "src/main.rs"}),
183 }],
184 };
185 let json = serde_json::to_string(&msg).unwrap();
186 let decoded: Message = serde_json::from_str(&json).unwrap();
187 assert_eq!(msg, decoded);
188 }
189
190 #[test]
191 fn message_roundtrip_tool() {
192 let msg = Message::Tool {
193 result: MessageToolResult {
194 tool_use_id: "call_1".into(),
195 content: "fn main() {}".into(),
196 is_error: false,
197 },
198 };
199 let json = serde_json::to_string(&msg).unwrap();
200 let decoded: Message = serde_json::from_str(&json).unwrap();
201 assert_eq!(msg, decoded);
202 }
203
204 #[test]
205 fn event_roundtrip() {
206 let events = vec![
207 AgentEvent::TurnStart,
208 AgentEvent::TextDelta {
209 delta: "Hello".into(),
210 },
211 AgentEvent::ToolCall {
212 call: ToolCall {
213 id: "c1".into(),
214 name: "bash".into(),
215 input: serde_json::json!({"command": "ls"}),
216 },
217 provenance: ToolProvenance::Native,
218 summary_fields: vec![],
219 },
220 AgentEvent::ToolResult {
221 result: MessageToolResult {
222 tool_use_id: "c1".into(),
223 content: "file.rs".into(),
224 is_error: false,
225 },
226 },
227 AgentEvent::TurnEnd {
228 stop_reason: StopReason::EndTurn,
229 usage: Usage {
230 input_tokens: 100,
231 output_tokens: 50,
232 cache_read_tokens: 80,
233 cache_write_tokens: 20,
234 },
235 },
236 AgentEvent::Error {
237 message: "something failed".into(),
238 },
239 ];
240 for event in events {
241 let json = serde_json::to_string(&event).unwrap();
242 let decoded: AgentEvent = serde_json::from_str(&json).unwrap();
243 assert_eq!(event, decoded);
244 }
245 }
246}
247
248pub fn extract_tool_calls_from_text(text: &str) -> Vec<ToolCall> {
249 let mut calls = Vec::new();
250 let mut remaining = text;
251
252 while let Some(start) = remaining.find("```json-tool") {
253 let inner_start = start + "```json-tool".len();
254 let inner = remaining[inner_start..].trim_start();
255 let end = inner.find("```").unwrap_or(inner.len());
256 let content = inner[..end].trim();
257
258 if let Ok(obj) = serde_json::from_str::<serde_json::Value>(content)
259 && let (Some(name), Some(args)) = (obj["name"].as_str(), Some(obj["args"].clone()))
260 {
261 calls.push(ToolCall {
262 id: format!("tc_{}", calls.len()),
263 name: name.to_string(),
264 input: args,
265 });
266 }
267
268 remaining = &inner[end..];
269 if end + 3 < remaining.len() {
270 remaining = &remaining[3..];
271 } else {
272 break;
273 }
274 }
275
276 calls
277}
278
279pub fn strip_tool_syntax(text: &str) -> String {
280 let mut result = text.to_string();
281 while let Some(start) = result.find("```json-tool") {
282 let inner_start = start + "```json-tool".len();
283 let inner = &result[inner_start..];
284 let end = inner_start + inner.find("```").unwrap_or(inner.len()) + 3;
285 result.replace_range(start..end, "");
286 }
287 result.trim().to_string()
288}