Skip to main content

git_internal/internal/object/
tool.rs

1//! AI Tool Invocation Definition
2//!
3//! A `ToolInvocation` records a specific action taken by an agent, such as reading a file,
4//! running a command, or querying a search engine.
5//!
6//! # Purpose
7//!
8//! - **Audit Trail**: Allows reconstructing exactly what the agent did.
9//! - **Cost Tracking**: Can be used to calculate token/resource usage.
10//! - **Debugging**: Helps understand why an agent made a particular decision.
11//!
12//! # Fields
13//!
14//! - `tool_name`: The identifier of the tool (e.g., "read_file").
15//! - `args`: JSON arguments passed to the tool.
16//! - `io_footprint`: Files read/written during the operation (for dependency tracking).
17//! - `status`: Whether the tool call succeeded or failed.
18
19use std::fmt;
20
21use serde::{Deserialize, Serialize};
22use uuid::Uuid;
23
24use crate::{
25    errors::GitError,
26    hash::ObjectHash,
27    internal::object::{
28        ObjectTrait,
29        types::{ActorRef, ArtifactRef, Header, ObjectType},
30    },
31};
32
33/// Tool invocation status.
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35#[serde(rename_all = "snake_case")]
36pub enum ToolStatus {
37    /// Tool executed successfully.
38    Ok,
39    /// Tool execution failed (returned error).
40    Error,
41}
42
43impl ToolStatus {
44    pub fn as_str(&self) -> &'static str {
45        match self {
46            ToolStatus::Ok => "ok",
47            ToolStatus::Error => "error",
48        }
49    }
50}
51
52impl fmt::Display for ToolStatus {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        write!(f, "{}", self.as_str())
55    }
56}
57
58/// IO footprint of a tool invocation.
59/// Tracks reads and writes for auditability.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct IoFootprint {
62    #[serde(default)]
63    pub paths_read: Vec<String>,
64    #[serde(default)]
65    pub paths_written: Vec<String>,
66}
67
68/// Tool invocation record.
69/// Records a single tool call within a run.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ToolInvocation {
72    #[serde(flatten)]
73    header: Header,
74    run_id: Uuid,
75    tool_name: String,
76    io_footprint: Option<IoFootprint>,
77    #[serde(default)]
78    args: serde_json::Value,
79    status: ToolStatus,
80    result_summary: Option<String>,
81    #[serde(default)]
82    artifacts: Vec<ArtifactRef>,
83}
84
85impl ToolInvocation {
86    pub fn new(
87        repo_id: Uuid,
88        created_by: ActorRef,
89        run_id: Uuid,
90        tool_name: impl Into<String>,
91    ) -> Result<Self, String> {
92        Ok(Self {
93            header: Header::new(ObjectType::ToolInvocation, repo_id, created_by)?,
94            run_id,
95            tool_name: tool_name.into(),
96            io_footprint: None,
97            args: serde_json::Value::Null,
98            status: ToolStatus::Ok,
99            result_summary: None,
100            artifacts: Vec::new(),
101        })
102    }
103
104    pub fn header(&self) -> &Header {
105        &self.header
106    }
107
108    pub fn run_id(&self) -> Uuid {
109        self.run_id
110    }
111
112    pub fn tool_name(&self) -> &str {
113        &self.tool_name
114    }
115
116    pub fn io_footprint(&self) -> Option<&IoFootprint> {
117        self.io_footprint.as_ref()
118    }
119
120    pub fn args(&self) -> &serde_json::Value {
121        &self.args
122    }
123
124    pub fn status(&self) -> &ToolStatus {
125        &self.status
126    }
127
128    pub fn result_summary(&self) -> Option<&str> {
129        self.result_summary.as_deref()
130    }
131
132    pub fn artifacts(&self) -> &[ArtifactRef] {
133        &self.artifacts
134    }
135
136    pub fn set_io_footprint(&mut self, io_footprint: Option<IoFootprint>) {
137        self.io_footprint = io_footprint;
138    }
139
140    pub fn set_args(&mut self, args: serde_json::Value) {
141        self.args = args;
142    }
143
144    pub fn set_status(&mut self, status: ToolStatus) {
145        self.status = status;
146    }
147
148    pub fn set_result_summary(&mut self, result_summary: Option<String>) {
149        self.result_summary = result_summary;
150    }
151
152    pub fn add_artifact(&mut self, artifact: ArtifactRef) {
153        self.artifacts.push(artifact);
154    }
155}
156
157impl fmt::Display for ToolInvocation {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        write!(f, "ToolInvocation: {}", self.header.object_id())
160    }
161}
162
163impl ObjectTrait for ToolInvocation {
164    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
165    where
166        Self: Sized,
167    {
168        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
169    }
170
171    fn get_type(&self) -> ObjectType {
172        ObjectType::ToolInvocation
173    }
174
175    fn get_size(&self) -> usize {
176        serde_json::to_vec(self).map(|v| v.len()).unwrap_or(0)
177    }
178
179    fn to_data(&self) -> Result<Vec<u8>, GitError> {
180        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_tool_invocation_io_footprint() {
190        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
191        let actor = ActorRef::human("jackie").expect("actor");
192        let run_id = Uuid::from_u128(0x1);
193
194        let mut tool_inv =
195            ToolInvocation::new(repo_id, actor, run_id, "read_file").expect("tool_invocation");
196
197        let footprint = IoFootprint {
198            paths_read: vec!["src/main.rs".to_string()],
199            paths_written: vec![],
200        };
201
202        tool_inv.set_io_footprint(Some(footprint));
203
204        assert_eq!(tool_inv.tool_name(), "read_file");
205        assert!(tool_inv.io_footprint().is_some());
206        assert_eq!(
207            tool_inv.io_footprint().unwrap().paths_read[0],
208            "src/main.rs"
209        );
210    }
211
212    #[test]
213    fn test_tool_invocation_fields() {
214        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
215        let actor = ActorRef::human("jackie").expect("actor");
216        let run_id = Uuid::from_u128(0x1);
217
218        let mut tool_inv =
219            ToolInvocation::new(repo_id, actor, run_id, "apply_patch").expect("tool_invocation");
220        tool_inv.set_status(ToolStatus::Error);
221        tool_inv.set_args(serde_json::json!({"path": "src/lib.rs"}));
222        tool_inv.set_result_summary(Some("failed".to_string()));
223        tool_inv.add_artifact(ArtifactRef::new("local", "artifact-key").expect("artifact"));
224
225        assert_eq!(tool_inv.status(), &ToolStatus::Error);
226        assert_eq!(tool_inv.artifacts().len(), 1);
227        assert_eq!(tool_inv.args()["path"], "src/lib.rs");
228    }
229}