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(
106 tool_name: impl Into<CompactStr>,
107 args: Value,
108 session_id: impl Into<CompactStr>,
109 ) -> Self {
110 Self {
111 id: ToolInvocationId::new(),
112 tool_name: tool_name.into(),
113 args,
114 session_id: session_id.into(),
115 attempt: 1,
116 parent_id: None,
117 created_at: Instant::now(),
118 }
119 }
120
121 pub fn retry(&self) -> Self {
123 Self {
124 id: ToolInvocationId::new(),
125 tool_name: self.tool_name.clone(),
126 args: self.args.clone(),
127 session_id: self.session_id.clone(),
128 attempt: self.attempt + 1,
129 parent_id: self.parent_id,
130 created_at: Instant::now(),
131 }
132 }
133
134 pub fn child(&self, tool_name: impl Into<CompactStr>, args: Value) -> Self {
136 Self {
137 id: ToolInvocationId::new(),
138 tool_name: tool_name.into(),
139 args,
140 session_id: self.session_id.clone(),
141 attempt: 1,
142 parent_id: Some(self.id),
143 created_at: Instant::now(),
144 }
145 }
146
147 #[inline]
149 pub fn elapsed(&self) -> std::time::Duration {
150 self.created_at.elapsed()
151 }
152
153 #[inline]
155 pub fn is_retry(&self) -> bool {
156 self.attempt > 1
157 }
158
159 #[inline]
161 pub fn is_nested(&self) -> bool {
162 self.parent_id.is_some()
163 }
164}
165
166#[derive(Debug, Clone)]
168pub struct InvocationBuilder {
169 tool_name: CompactStr,
170 args: Value,
171 session_id: CompactStr,
172 attempt: u32,
173 parent_id: Option<ToolInvocationId>,
174 id: Option<ToolInvocationId>,
175}
176
177impl InvocationBuilder {
178 pub fn new(tool_name: impl Into<CompactStr>) -> Self {
180 Self {
181 tool_name: tool_name.into(),
182 args: Value::Null,
183 session_id: CompactStr::default(),
184 attempt: 1,
185 parent_id: None,
186 id: None,
187 }
188 }
189
190 pub fn args(mut self, args: Value) -> Self {
192 self.args = args;
193 self
194 }
195
196 pub fn session_id(mut self, session_id: impl Into<CompactStr>) -> Self {
198 self.session_id = session_id.into();
199 self
200 }
201
202 pub fn attempt(mut self, attempt: u32) -> Self {
204 self.attempt = attempt.max(1);
205 self
206 }
207
208 pub fn parent_id(mut self, parent_id: ToolInvocationId) -> Self {
210 self.parent_id = Some(parent_id);
211 self
212 }
213
214 pub fn id(mut self, id: ToolInvocationId) -> Self {
216 self.id = Some(id);
217 self
218 }
219
220 pub fn build(self) -> ToolInvocation {
222 ToolInvocation {
223 id: self.id.unwrap_or_default(),
224 tool_name: self.tool_name,
225 args: self.args,
226 session_id: self.session_id,
227 attempt: self.attempt,
228 parent_id: self.parent_id,
229 created_at: Instant::now(),
230 }
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use serde_json::json;
238
239 #[test]
240 fn test_invocation_id_display() {
241 let id = ToolInvocationId::new();
242 let display = id.to_string();
243 assert_eq!(display.len(), 36); assert!(display.contains('-'));
245 }
246
247 #[test]
248 fn test_invocation_id_short() {
249 let id = ToolInvocationId::new();
250 let short = id.short();
251 assert_eq!(short.len(), 8);
252 }
253
254 #[test]
255 fn test_invocation_id_parse() {
256 let id = ToolInvocationId::new();
257 let s = id.to_string();
258 let parsed = ToolInvocationId::parse(&s).unwrap();
259 assert_eq!(id, parsed);
260 }
261
262 #[test]
263 fn test_invocation_creation() {
264 let inv = ToolInvocation::new("read_file", json!({"path": "/tmp/test"}), "session-123");
265 assert_eq!(inv.tool_name, "read_file");
266 assert_eq!(inv.session_id, "session-123");
267 assert_eq!(inv.attempt, 1);
268 assert!(inv.parent_id.is_none());
269 }
270
271 #[test]
272 fn test_invocation_retry() {
273 let inv = ToolInvocation::new(tools::GREP_FILE, json!({"pattern": "TODO"}), "session-456");
274 let retry = inv.retry();
275
276 assert_ne!(inv.id, retry.id);
277 assert_eq!(retry.attempt, 2);
278 assert_eq!(retry.tool_name, inv.tool_name);
279 assert_eq!(retry.args, inv.args);
280 }
281
282 #[test]
283 fn test_invocation_child() {
284 let parent = ToolInvocation::new("task_tracker", json!({}), "session-789");
285 let child = parent.child("read_file", json!({"path": "/src/main.rs"}));
286
287 assert_eq!(child.parent_id, Some(parent.id));
288 assert_eq!(child.session_id, parent.session_id);
289 assert_eq!(child.attempt, 1);
290 }
291
292 #[test]
293 fn test_builder() {
294 let inv = InvocationBuilder::new("write_file")
295 .args(json!({"path": "/out.txt", "content": "hello"}))
296 .session_id("builder-session")
297 .attempt(3)
298 .build();
299
300 assert_eq!(inv.tool_name, "write_file");
301 assert_eq!(inv.session_id, "builder-session");
302 assert_eq!(inv.attempt, 3);
303 }
304
305 #[test]
306 fn test_builder_with_parent() {
307 let parent_id = ToolInvocationId::new();
308 let inv = InvocationBuilder::new("nested_tool")
309 .session_id("test")
310 .parent_id(parent_id)
311 .build();
312
313 assert_eq!(inv.parent_id, Some(parent_id));
314 assert!(inv.is_nested());
315 }
316
317 #[test]
318 fn test_serde_roundtrip() {
319 let id = ToolInvocationId::new();
320 let json = serde_json::to_string(&id).unwrap();
321 let parsed: ToolInvocationId = serde_json::from_str(&json).unwrap();
322 assert_eq!(id, parsed);
323 }
324}