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