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}