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(
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    /// Create a retry of this invocation with incremented attempt.
122    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    /// Create a child invocation for nested calls.
135    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    /// Get elapsed time since creation.
148    #[inline]
149    pub fn elapsed(&self) -> std::time::Duration {
150        self.created_at.elapsed()
151    }
152
153    /// Check if this is a retry attempt.
154    #[inline]
155    pub fn is_retry(&self) -> bool {
156        self.attempt > 1
157    }
158
159    /// Check if this is a nested/child invocation.
160    #[inline]
161    pub fn is_nested(&self) -> bool {
162        self.parent_id.is_some()
163    }
164}
165
166/// Builder for ergonomic ToolInvocation construction.
167#[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    /// Start building a new invocation.
179    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    /// Set the tool arguments.
191    pub fn args(mut self, args: Value) -> Self {
192        self.args = args;
193        self
194    }
195
196    /// Set the session ID.
197    pub fn session_id(mut self, session_id: impl Into<CompactStr>) -> Self {
198        self.session_id = session_id.into();
199        self
200    }
201
202    /// Set the attempt number.
203    pub fn attempt(mut self, attempt: u32) -> Self {
204        self.attempt = attempt.max(1);
205        self
206    }
207
208    /// Set the parent invocation ID.
209    pub fn parent_id(mut self, parent_id: ToolInvocationId) -> Self {
210        self.parent_id = Some(parent_id);
211        self
212    }
213
214    /// Set a specific invocation ID (for reconstruction).
215    pub fn id(mut self, id: ToolInvocationId) -> Self {
216        self.id = Some(id);
217        self
218    }
219
220    /// Build the ToolInvocation.
221    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); // UUID hyphenated format
244        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}