1use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::fmt;
10use std::time::Instant;
11use uuid::Uuid;
12
13use crate::types::CompactStr;
14
15#[cfg(test)]
16use crate::config::constants::tools;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct ToolInvocationId(Uuid);
25
26impl ToolInvocationId {
27 #[inline]
29 pub fn new() -> Self {
30 Self(Uuid::new_v4())
31 }
32
33 #[inline]
35 pub fn from_uuid(uuid: Uuid) -> Self {
36 Self(uuid)
37 }
38
39 pub fn parse(s: &str) -> Result<Self, uuid::Error> {
41 Uuid::parse_str(s).map(Self)
42 }
43
44 #[inline]
46 pub fn as_uuid(&self) -> &Uuid {
47 &self.0
48 }
49
50 #[inline]
52 pub fn to_string_hyphenated(&self) -> String {
53 self.0.hyphenated().to_string()
54 }
55
56 #[inline]
58 pub fn short(&self) -> String {
59 self.0.hyphenated().to_string()[..8].to_string()
60 }
61}
62
63impl Default for ToolInvocationId {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl fmt::Display for ToolInvocationId {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(f, "{}", self.0.hyphenated())
72 }
73}
74
75impl From<Uuid> for ToolInvocationId {
76 fn from(uuid: Uuid) -> Self {
77 Self(uuid)
78 }
79}
80
81#[derive(Debug, Clone)]
86pub struct ToolInvocation {
87 pub id: ToolInvocationId,
89 pub tool_name: CompactStr,
91 pub args: Value,
93 pub session_id: CompactStr,
95 pub attempt: u32,
97 pub parent_id: Option<ToolInvocationId>,
99 pub created_at: Instant,
101}
102
103impl ToolInvocation {
104 pub fn new(tool_name: impl Into<CompactStr>, args: Value, session_id: impl Into<CompactStr>) -> Self {
106 Self {
107 id: ToolInvocationId::new(),
108 tool_name: tool_name.into(),
109 args,
110 session_id: session_id.into(),
111 attempt: 1,
112 parent_id: None,
113 created_at: Instant::now(),
114 }
115 }
116
117 pub fn retry(&self) -> Self {
119 Self {
120 id: ToolInvocationId::new(),
121 tool_name: self.tool_name.clone(),
122 args: self.args.clone(),
123 session_id: self.session_id.clone(),
124 attempt: self.attempt + 1,
125 parent_id: self.parent_id,
126 created_at: Instant::now(),
127 }
128 }
129
130 pub fn child(&self, tool_name: impl Into<CompactStr>, args: Value) -> Self {
132 Self {
133 id: ToolInvocationId::new(),
134 tool_name: tool_name.into(),
135 args,
136 session_id: self.session_id.clone(),
137 attempt: 1,
138 parent_id: Some(self.id),
139 created_at: Instant::now(),
140 }
141 }
142
143 #[inline]
145 pub fn elapsed(&self) -> std::time::Duration {
146 self.created_at.elapsed()
147 }
148
149 #[inline]
151 pub fn is_retry(&self) -> bool {
152 self.attempt > 1
153 }
154
155 #[inline]
157 pub fn is_nested(&self) -> bool {
158 self.parent_id.is_some()
159 }
160}
161
162#[derive(Debug, Clone)]
164pub struct InvocationBuilder {
165 tool_name: CompactStr,
166 args: Value,
167 session_id: CompactStr,
168 attempt: u32,
169 parent_id: Option<ToolInvocationId>,
170 id: Option<ToolInvocationId>,
171}
172
173impl InvocationBuilder {
174 pub fn new(tool_name: impl Into<CompactStr>) -> Self {
176 Self {
177 tool_name: tool_name.into(),
178 args: Value::Null,
179 session_id: CompactStr::default(),
180 attempt: 1,
181 parent_id: None,
182 id: None,
183 }
184 }
185
186 pub fn args(mut self, args: Value) -> Self {
188 self.args = args;
189 self
190 }
191
192 pub fn session_id(mut self, session_id: impl Into<CompactStr>) -> Self {
194 self.session_id = session_id.into();
195 self
196 }
197
198 pub fn attempt(mut self, attempt: u32) -> Self {
200 self.attempt = attempt.max(1);
201 self
202 }
203
204 pub fn parent_id(mut self, parent_id: ToolInvocationId) -> Self {
206 self.parent_id = Some(parent_id);
207 self
208 }
209
210 pub fn id(mut self, id: ToolInvocationId) -> Self {
212 self.id = Some(id);
213 self
214 }
215
216 pub fn build(self) -> ToolInvocation {
218 ToolInvocation {
219 id: self.id.unwrap_or_default(),
220 tool_name: self.tool_name,
221 args: self.args,
222 session_id: self.session_id,
223 attempt: self.attempt,
224 parent_id: self.parent_id,
225 created_at: Instant::now(),
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use serde_json::json;
234
235 #[test]
236 fn test_invocation_id_display() {
237 let id = ToolInvocationId::new();
238 let display = id.to_string();
239 assert_eq!(display.len(), 36); assert!(display.contains('-'));
241 }
242
243 #[test]
244 fn test_invocation_id_short() {
245 let id = ToolInvocationId::new();
246 let short = id.short();
247 assert_eq!(short.len(), 8);
248 }
249
250 #[test]
251 fn test_invocation_id_parse() {
252 let id = ToolInvocationId::new();
253 let s = id.to_string();
254 let parsed = ToolInvocationId::parse(&s).unwrap();
255 assert_eq!(id, parsed);
256 }
257
258 #[test]
259 fn test_invocation_creation() {
260 let inv = ToolInvocation::new("read_file", json!({"path": "/tmp/test"}), "session-123");
261 assert_eq!(inv.tool_name, "read_file");
262 assert_eq!(inv.session_id, "session-123");
263 assert_eq!(inv.attempt, 1);
264 assert!(inv.parent_id.is_none());
265 }
266
267 #[test]
268 fn test_invocation_retry() {
269 let inv = ToolInvocation::new(tools::GREP_FILE, json!({"pattern": "TODO"}), "session-456");
270 let retry = inv.retry();
271
272 assert_ne!(inv.id, retry.id);
273 assert_eq!(retry.attempt, 2);
274 assert_eq!(retry.tool_name, inv.tool_name);
275 assert_eq!(retry.args, inv.args);
276 }
277
278 #[test]
279 fn test_invocation_child() {
280 let parent = ToolInvocation::new("task_tracker", json!({}), "session-789");
281 let child = parent.child("read_file", json!({"path": "/src/main.rs"}));
282
283 assert_eq!(child.parent_id, Some(parent.id));
284 assert_eq!(child.session_id, parent.session_id);
285 assert_eq!(child.attempt, 1);
286 }
287
288 #[test]
289 fn test_builder() {
290 let inv = InvocationBuilder::new("write_file")
291 .args(json!({"path": "/out.txt", "content": "hello"}))
292 .session_id("builder-session")
293 .attempt(3)
294 .build();
295
296 assert_eq!(inv.tool_name, "write_file");
297 assert_eq!(inv.session_id, "builder-session");
298 assert_eq!(inv.attempt, 3);
299 }
300
301 #[test]
302 fn test_builder_with_parent() {
303 let parent_id = ToolInvocationId::new();
304 let inv = InvocationBuilder::new("nested_tool")
305 .session_id("test")
306 .parent_id(parent_id)
307 .build();
308
309 assert_eq!(inv.parent_id, Some(parent_id));
310 assert!(inv.is_nested());
311 }
312
313 #[test]
314 fn test_serde_roundtrip() {
315 let id = ToolInvocationId::new();
316 let json = serde_json::to_string(&id).unwrap();
317 let parsed: ToolInvocationId = serde_json::from_str(&json).unwrap();
318 assert_eq!(id, parsed);
319 }
320}