use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt;
use std::time::Instant;
use uuid::Uuid;
#[cfg(test)]
use crate::config::constants::tools;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ToolInvocationId(Uuid);
impl ToolInvocationId {
#[inline]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
#[inline]
pub fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
pub fn parse(s: &str) -> Result<Self, uuid::Error> {
Uuid::parse_str(s).map(Self)
}
#[inline]
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
#[inline]
pub fn to_string_hyphenated(&self) -> String {
self.0.hyphenated().to_string()
}
#[inline]
pub fn short(&self) -> String {
self.0.hyphenated().to_string()[..8].to_string()
}
}
impl Default for ToolInvocationId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for ToolInvocationId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.hyphenated())
}
}
impl From<Uuid> for ToolInvocationId {
fn from(uuid: Uuid) -> Self {
Self(uuid)
}
}
#[derive(Debug, Clone)]
pub struct ToolInvocation {
pub id: ToolInvocationId,
pub tool_name: String,
pub args: Value,
pub session_id: String,
pub attempt: u32,
pub parent_id: Option<ToolInvocationId>,
pub created_at: Instant,
}
impl ToolInvocation {
pub fn new(tool_name: impl Into<String>, args: Value, session_id: impl Into<String>) -> Self {
Self {
id: ToolInvocationId::new(),
tool_name: tool_name.into(),
args,
session_id: session_id.into(),
attempt: 1,
parent_id: None,
created_at: Instant::now(),
}
}
pub fn retry(&self) -> Self {
Self {
id: ToolInvocationId::new(),
tool_name: self.tool_name.clone(),
args: self.args.clone(),
session_id: self.session_id.clone(),
attempt: self.attempt + 1,
parent_id: self.parent_id,
created_at: Instant::now(),
}
}
pub fn child(&self, tool_name: impl Into<String>, args: Value) -> Self {
Self {
id: ToolInvocationId::new(),
tool_name: tool_name.into(),
args,
session_id: self.session_id.clone(),
attempt: 1,
parent_id: Some(self.id),
created_at: Instant::now(),
}
}
#[inline]
pub fn elapsed(&self) -> std::time::Duration {
self.created_at.elapsed()
}
#[inline]
pub fn is_retry(&self) -> bool {
self.attempt > 1
}
#[inline]
pub fn is_nested(&self) -> bool {
self.parent_id.is_some()
}
}
#[derive(Debug, Clone)]
pub struct InvocationBuilder {
tool_name: String,
args: Value,
session_id: String,
attempt: u32,
parent_id: Option<ToolInvocationId>,
id: Option<ToolInvocationId>,
}
impl InvocationBuilder {
pub fn new(tool_name: impl Into<String>) -> Self {
Self {
tool_name: tool_name.into(),
args: Value::Null,
session_id: String::new(),
attempt: 1,
parent_id: None,
id: None,
}
}
pub fn args(mut self, args: Value) -> Self {
self.args = args;
self
}
pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
self.session_id = session_id.into();
self
}
pub fn attempt(mut self, attempt: u32) -> Self {
self.attempt = attempt.max(1);
self
}
pub fn parent_id(mut self, parent_id: ToolInvocationId) -> Self {
self.parent_id = Some(parent_id);
self
}
pub fn id(mut self, id: ToolInvocationId) -> Self {
self.id = Some(id);
self
}
pub fn build(self) -> ToolInvocation {
ToolInvocation {
id: self.id.unwrap_or_default(),
tool_name: self.tool_name,
args: self.args,
session_id: self.session_id,
attempt: self.attempt,
parent_id: self.parent_id,
created_at: Instant::now(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_invocation_id_display() {
let id = ToolInvocationId::new();
let display = id.to_string();
assert_eq!(display.len(), 36); assert!(display.contains('-'));
}
#[test]
fn test_invocation_id_short() {
let id = ToolInvocationId::new();
let short = id.short();
assert_eq!(short.len(), 8);
}
#[test]
fn test_invocation_id_parse() {
let id = ToolInvocationId::new();
let s = id.to_string();
let parsed = ToolInvocationId::parse(&s).unwrap();
assert_eq!(id, parsed);
}
#[test]
fn test_invocation_creation() {
let inv = ToolInvocation::new("read_file", json!({"path": "/tmp/test"}), "session-123");
assert_eq!(inv.tool_name, "read_file");
assert_eq!(inv.session_id, "session-123");
assert_eq!(inv.attempt, 1);
assert!(inv.parent_id.is_none());
}
#[test]
fn test_invocation_retry() {
let inv = ToolInvocation::new(tools::GREP_FILE, json!({"pattern": "TODO"}), "session-456");
let retry = inv.retry();
assert_ne!(inv.id, retry.id);
assert_eq!(retry.attempt, 2);
assert_eq!(retry.tool_name, inv.tool_name);
assert_eq!(retry.args, inv.args);
}
#[test]
fn test_invocation_child() {
let parent = ToolInvocation::new("task_tracker", json!({}), "session-789");
let child = parent.child("read_file", json!({"path": "/src/main.rs"}));
assert_eq!(child.parent_id, Some(parent.id));
assert_eq!(child.session_id, parent.session_id);
assert_eq!(child.attempt, 1);
}
#[test]
fn test_builder() {
let inv = InvocationBuilder::new("write_file")
.args(json!({"path": "/out.txt", "content": "hello"}))
.session_id("builder-session")
.attempt(3)
.build();
assert_eq!(inv.tool_name, "write_file");
assert_eq!(inv.session_id, "builder-session");
assert_eq!(inv.attempt, 3);
}
#[test]
fn test_builder_with_parent() {
let parent_id = ToolInvocationId::new();
let inv = InvocationBuilder::new("nested_tool")
.session_id("test")
.parent_id(parent_id)
.build();
assert_eq!(inv.parent_id, Some(parent_id));
assert!(inv.is_nested());
}
#[test]
fn test_serde_roundtrip() {
let id = ToolInvocationId::new();
let json = serde_json::to_string(&id).unwrap();
let parsed: ToolInvocationId = serde_json::from_str(&json).unwrap();
assert_eq!(id, parsed);
}
}