Skip to main content

vtcode_core/tools/
invocation.rs

1//! Unified tool invocation tracking
2//!
3//! Provides a unique `ToolInvocationId` that flows through the entire tool execution
4//! pipeline, enabling correlation of logs, metrics, and state across different tracking
5//! mechanisms (execution_context, execution_tracker, tool_ledger, execution_history).
6
7use 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/// Unique identifier for a tool invocation.
19///
20/// UUID-based for global uniqueness across sessions and processes.
21/// Implements Display for logging and correlation.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct ToolInvocationId(Uuid);
25
26impl ToolInvocationId {
27    /// Create a new unique invocation ID.
28    #[inline]
29    pub fn new() -> Self {
30        Self(Uuid::new_v4())
31    }
32
33    /// Create from an existing UUID.
34    #[inline]
35    pub fn from_uuid(uuid: Uuid) -> Self {
36        Self(uuid)
37    }
38
39    /// Parse from a string representation.
40    pub fn parse(s: &str) -> Result<Self, uuid::Error> {
41        Uuid::parse_str(s).map(Self)
42    }
43
44    /// Get the underlying UUID.
45    #[inline]
46    pub fn as_uuid(&self) -> &Uuid {
47        &self.0
48    }
49
50    /// Convert to a hyphenated string (standard UUID format).
51    #[inline]
52    pub fn to_string_hyphenated(&self) -> String {
53        self.0.hyphenated().to_string()
54    }
55
56    /// Convert to a short 8-character prefix for compact logging.
57    #[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/// Complete context for a single tool invocation.
82///
83/// Tracks all metadata needed for correlation, retry handling,
84/// and hierarchical execution (nested child calls).
85#[derive(Debug, Clone)]
86pub struct ToolInvocation {
87    /// Unique identifier for this invocation
88    pub id: ToolInvocationId,
89    /// Name of the tool being invoked
90    pub tool_name: CompactStr,
91    /// Arguments passed to the tool
92    pub args: Value,
93    /// Session identifier for grouping related invocations
94    pub session_id: CompactStr,
95    /// Attempt number (1-based, incremented on retry)
96    pub attempt: u32,
97    /// Parent invocation ID for nested child calls
98    pub parent_id: Option<ToolInvocationId>,
99    /// Timestamp when invocation was created
100    pub created_at: Instant,
101}
102
103impl ToolInvocation {
104    /// Create a new tool invocation with generated ID.
105    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    /// Create a retry of this invocation with incremented attempt.
118    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    /// Create a child invocation for nested calls.
131    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    /// Get elapsed time since creation.
144    #[inline]
145    pub fn elapsed(&self) -> std::time::Duration {
146        self.created_at.elapsed()
147    }
148
149    /// Check if this is a retry attempt.
150    #[inline]
151    pub fn is_retry(&self) -> bool {
152        self.attempt > 1
153    }
154
155    /// Check if this is a nested/child invocation.
156    #[inline]
157    pub fn is_nested(&self) -> bool {
158        self.parent_id.is_some()
159    }
160}
161
162/// Builder for ergonomic ToolInvocation construction.
163#[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    /// Start building a new invocation.
175    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    /// Set the tool arguments.
187    pub fn args(mut self, args: Value) -> Self {
188        self.args = args;
189        self
190    }
191
192    /// Set the session ID.
193    pub fn session_id(mut self, session_id: impl Into<CompactStr>) -> Self {
194        self.session_id = session_id.into();
195        self
196    }
197
198    /// Set the attempt number.
199    pub fn attempt(mut self, attempt: u32) -> Self {
200        self.attempt = attempt.max(1);
201        self
202    }
203
204    /// Set the parent invocation ID.
205    pub fn parent_id(mut self, parent_id: ToolInvocationId) -> Self {
206        self.parent_id = Some(parent_id);
207        self
208    }
209
210    /// Set a specific invocation ID (for reconstruction).
211    pub fn id(mut self, id: ToolInvocationId) -> Self {
212        self.id = Some(id);
213        self
214    }
215
216    /// Build the ToolInvocation.
217    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); // UUID hyphenated format
240        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}