Skip to main content

git_internal/internal/object/
tool.rs

1//! AI Tool Invocation Definition
2//!
3//! A `ToolInvocation` records a single action taken by an agent during
4//! a [`Run`](super::run::Run) — reading a file, running a shell
5//! command, calling an API, etc. It is the finest-grained unit of
6//! agent activity, capturing *what* was done, *with which arguments*,
7//! and *what happened*.
8//!
9//! # Position in Lifecycle
10//!
11//! ```text
12//! Run ──patchsets──▶ [PatchSet₀, ...]
13//!  │
14//!  ├── evidence ──▶ [Evidence₀, ...]
15//!  │
16//!  └── tool invocations ──▶ [ToolInvocation₀, ToolInvocation₁, ...]
17//!                                  │
18//!                                  └── io_footprint (paths read/written)
19//! ```
20//!
21//! ToolInvocations are produced **throughout** a Run, one per tool
22//! call. They form a chronological log of every action the agent
23//! performed. Unlike Evidence (which validates a PatchSet) or
24//! Decision (which concludes a Run), ToolInvocations are low-level
25//! operational records.
26//!
27//! # Purpose
28//!
29//! - **Audit Trail**: Allows reconstructing exactly what the agent did
30//!   step by step, including arguments and results.
31//! - **Dependency Tracking**: `io_footprint` records which files were
32//!   read or written, enabling incremental re-runs and cache
33//!   invalidation.
34//! - **Debugging**: When a Run produces unexpected results, reviewing
35//!   the ToolInvocation sequence reveals the agent's reasoning path.
36
37use std::fmt;
38
39use serde::{Deserialize, Serialize};
40use uuid::Uuid;
41
42use crate::{
43    errors::GitError,
44    hash::ObjectHash,
45    internal::object::{
46        ObjectTrait,
47        types::{ActorRef, ArtifactRef, Header, ObjectType},
48    },
49};
50
51/// Tool invocation status.
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "snake_case")]
54pub enum ToolStatus {
55    /// Tool executed successfully.
56    Ok,
57    /// Tool execution failed (returned error).
58    Error,
59}
60
61impl ToolStatus {
62    pub fn as_str(&self) -> &'static str {
63        match self {
64            ToolStatus::Ok => "ok",
65            ToolStatus::Error => "error",
66        }
67    }
68}
69
70impl fmt::Display for ToolStatus {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "{}", self.as_str())
73    }
74}
75
76/// File-level I/O footprint of a tool invocation.
77///
78/// Records which files were read and written during the tool call.
79/// Used for dependency tracking (which inputs influenced which
80/// outputs) and for cache invalidation on incremental re-runs.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct IoFootprint {
83    /// Paths the tool read during execution (e.g. source files,
84    /// config files). Relative to the repository root.
85    #[serde(default, skip_serializing_if = "Vec::is_empty")]
86    pub paths_read: Vec<String>,
87    /// Paths the tool wrote or modified (e.g. generated files,
88    /// patch output). Relative to the repository root.
89    #[serde(default, skip_serializing_if = "Vec::is_empty")]
90    pub paths_written: Vec<String>,
91}
92
93/// Record of a single tool call made by an agent during a Run.
94///
95/// One ToolInvocation per tool call. The chronological sequence of
96/// ToolInvocations within a Run forms the agent's action log. See
97/// module documentation for lifecycle position.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct ToolInvocation {
100    /// Common header (object ID, type, timestamps, creator, etc.).
101    #[serde(flatten)]
102    header: Header,
103    /// The [`Run`](super::run::Run) during which this tool was called.
104    ///
105    /// Every ToolInvocation belongs to exactly one Run. The Run does
106    /// not store a back-reference; the full invocation log is
107    /// reconstructed by querying all ToolInvocations with the same
108    /// `run_id`, ordered by `created_at`.
109    run_id: Uuid,
110    /// Identifier of the tool that was called (e.g. "read_file",
111    /// "bash", "search_code").
112    ///
113    /// This is the tool's registered name in the agent's tool
114    /// catalogue, not a human-readable label.
115    tool_name: String,
116    /// Files read and written during this tool call.
117    ///
118    /// `None` when the tool has no file-system side effects (e.g. a
119    /// pure computation or API call). See [`IoFootprint`] for details.
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    io_footprint: Option<IoFootprint>,
122    /// Arguments passed to the tool, as a JSON value.
123    ///
124    /// The schema depends on the tool. For example, `read_file` might
125    /// have `{"path": "src/main.rs"}`, while `bash` might have
126    /// `{"command": "cargo test"}`. `Null` when the tool takes no
127    /// arguments.
128    #[serde(default)]
129    args: serde_json::Value,
130    /// Whether the tool call succeeded or failed.
131    ///
132    /// `Ok` means the tool returned a normal result; `Error` means it
133    /// returned an error. The orchestrator may use this to decide
134    /// whether to retry or abort the Run.
135    status: ToolStatus,
136    /// Short human-readable summary of the tool's output.
137    ///
138    /// For `read_file`: might be the file size or first few lines.
139    /// For `bash`: might be the last line of stdout. For failed calls:
140    /// the error message. `None` if no summary was captured. For full
141    /// output, see `artifacts`.
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    result_summary: Option<String>,
144    /// References to full output files in object storage.
145    ///
146    /// May include stdout/stderr logs, generated files, screenshots,
147    /// etc. Each [`ArtifactRef`] points to one stored file. Empty
148    /// when the tool produced no persistent output.
149    #[serde(default, skip_serializing_if = "Vec::is_empty")]
150    artifacts: Vec<ArtifactRef>,
151}
152
153impl ToolInvocation {
154    pub fn new(
155        created_by: ActorRef,
156        run_id: Uuid,
157        tool_name: impl Into<String>,
158    ) -> Result<Self, String> {
159        Ok(Self {
160            header: Header::new(ObjectType::ToolInvocation, created_by)?,
161            run_id,
162            tool_name: tool_name.into(),
163            io_footprint: None,
164            args: serde_json::Value::Null,
165            status: ToolStatus::Ok,
166            result_summary: None,
167            artifacts: Vec::new(),
168        })
169    }
170
171    pub fn header(&self) -> &Header {
172        &self.header
173    }
174
175    pub fn run_id(&self) -> Uuid {
176        self.run_id
177    }
178
179    pub fn tool_name(&self) -> &str {
180        &self.tool_name
181    }
182
183    pub fn io_footprint(&self) -> Option<&IoFootprint> {
184        self.io_footprint.as_ref()
185    }
186
187    pub fn args(&self) -> &serde_json::Value {
188        &self.args
189    }
190
191    pub fn status(&self) -> &ToolStatus {
192        &self.status
193    }
194
195    pub fn result_summary(&self) -> Option<&str> {
196        self.result_summary.as_deref()
197    }
198
199    pub fn artifacts(&self) -> &[ArtifactRef] {
200        &self.artifacts
201    }
202
203    pub fn set_io_footprint(&mut self, io_footprint: Option<IoFootprint>) {
204        self.io_footprint = io_footprint;
205    }
206
207    pub fn set_args(&mut self, args: serde_json::Value) {
208        self.args = args;
209    }
210
211    pub fn set_status(&mut self, status: ToolStatus) {
212        self.status = status;
213    }
214
215    pub fn set_result_summary(&mut self, result_summary: Option<String>) {
216        self.result_summary = result_summary;
217    }
218
219    pub fn add_artifact(&mut self, artifact: ArtifactRef) {
220        self.artifacts.push(artifact);
221    }
222}
223
224impl fmt::Display for ToolInvocation {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        write!(f, "ToolInvocation: {}", self.header.object_id())
227    }
228}
229
230impl ObjectTrait for ToolInvocation {
231    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
232    where
233        Self: Sized,
234    {
235        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
236    }
237
238    fn get_type(&self) -> ObjectType {
239        ObjectType::ToolInvocation
240    }
241
242    fn get_size(&self) -> usize {
243        match serde_json::to_vec(self) {
244            Ok(v) => v.len(),
245            Err(e) => {
246                tracing::warn!("failed to compute ToolInvocation size: {}", e);
247                0
248            }
249        }
250    }
251
252    fn to_data(&self) -> Result<Vec<u8>, GitError> {
253        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_tool_invocation_io_footprint() {
263        let actor = ActorRef::human("jackie").expect("actor");
264        let run_id = Uuid::from_u128(0x1);
265
266        let mut tool_inv =
267            ToolInvocation::new(actor, run_id, "read_file").expect("tool_invocation");
268
269        let footprint = IoFootprint {
270            paths_read: vec!["src/main.rs".to_string()],
271            paths_written: vec![],
272        };
273
274        tool_inv.set_io_footprint(Some(footprint));
275
276        assert_eq!(tool_inv.tool_name(), "read_file");
277        assert!(tool_inv.io_footprint().is_some());
278        assert_eq!(
279            tool_inv.io_footprint().unwrap().paths_read[0],
280            "src/main.rs"
281        );
282    }
283
284    #[test]
285    fn test_tool_invocation_fields() {
286        let actor = ActorRef::human("jackie").expect("actor");
287        let run_id = Uuid::from_u128(0x1);
288
289        let mut tool_inv =
290            ToolInvocation::new(actor, run_id, "apply_patch").expect("tool_invocation");
291        tool_inv.set_status(ToolStatus::Error);
292        tool_inv.set_args(serde_json::json!({"path": "src/lib.rs"}));
293        tool_inv.set_result_summary(Some("failed".to_string()));
294        tool_inv.add_artifact(ArtifactRef::new("local", "artifact-key").expect("artifact"));
295
296        assert_eq!(tool_inv.status(), &ToolStatus::Error);
297        assert_eq!(tool_inv.artifacts().len(), 1);
298        assert_eq!(tool_inv.args()["path"], "src/lib.rs");
299    }
300}