Skip to main content

git_iris/agents/tools/
common.rs

1//! Common utilities for agent tools
2//!
3//! This module provides shared functionality used across multiple tools:
4//! - Schema generation for OpenAI-compatible tool definitions
5//! - Error type macros
6//! - Repository initialization helpers
7
8use std::future::Future;
9use std::path::{Path, PathBuf};
10
11use serde_json::{Map, Value};
12
13use crate::git::GitRepo;
14
15tokio::task_local! {
16    static ACTIVE_REPO_ROOT: PathBuf;
17}
18
19/// Generate a JSON schema for tool parameters that's `OpenAI`-compatible.
20/// `OpenAI` tool schemas require the `required` array to list every property.
21///
22/// # Panics
23///
24/// Panics if the generated schema cannot be serialized to JSON.
25#[must_use]
26pub fn parameters_schema<T: schemars::JsonSchema>() -> Value {
27    use schemars::schema_for;
28
29    let schema = schema_for!(T);
30    let mut value = serde_json::to_value(schema).expect("tool schema should serialize");
31    enforce_required_properties(&mut value);
32    value
33}
34
35/// Ensure all properties are listed in the `required` array.
36/// This is needed for `OpenAI` tool compatibility.
37fn enforce_required_properties(value: &mut Value) {
38    let Some(obj) = value.as_object_mut() else {
39        return;
40    };
41
42    let props_entry = obj
43        .entry("properties")
44        .or_insert_with(|| Value::Object(Map::new()));
45    let props_obj = props_entry.as_object().expect("properties must be object");
46    let required_keys: Vec<Value> = props_obj.keys().cloned().map(Value::String).collect();
47
48    obj.insert("required".to_string(), Value::Array(required_keys));
49}
50
51/// Get the current repository from the working directory.
52/// This is a common operation used by most tools.
53///
54/// # Errors
55///
56/// Returns an error when the active repository root cannot be resolved.
57pub fn get_current_repo() -> anyhow::Result<GitRepo> {
58    let repo_root = current_repo_root()?;
59    GitRepo::new(&repo_root)
60}
61
62/// Run an async operation with a repo root bound to the current task.
63pub async fn with_active_repo_root<F, T>(repo_path: &Path, future: F) -> T
64where
65    F: Future<Output = T>,
66{
67    ACTIVE_REPO_ROOT
68        .scope(repo_path.to_path_buf(), future)
69        .await
70}
71
72/// Get the repo root bound to the current task, falling back to the current directory.
73///
74/// # Errors
75///
76/// Returns an error when the current directory cannot be resolved to a Git repository.
77pub fn current_repo_root() -> anyhow::Result<PathBuf> {
78    if let Ok(repo_root) = ACTIVE_REPO_ROOT.try_with(Clone::clone) {
79        return Ok(repo_root);
80    }
81
82    let current_dir = std::env::current_dir()?;
83    let repo = GitRepo::new(&current_dir)?;
84    Ok(repo.repo_path().clone())
85}
86
87/// Macro to define a tool error type with standard From implementations.
88///
89/// This creates a newtype wrapper around String that implements:
90/// - `Debug`, `thiserror::Error`
91/// - `From<anyhow::Error>`
92/// - `From<std::io::Error>`
93///
94/// # Example
95/// ```ignore
96/// define_tool_error!(GitError);
97/// // Creates: pub struct GitError(String);
98/// // With Display showing: "GitError: {message}"
99/// ```
100#[macro_export]
101macro_rules! define_tool_error {
102    ($name:ident) => {
103        #[derive(Debug)]
104        pub struct $name(pub String);
105
106        impl std::fmt::Display for $name {
107            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108                write!(f, "{}", self.0)
109            }
110        }
111
112        impl std::error::Error for $name {}
113
114        impl From<anyhow::Error> for $name {
115            fn from(err: anyhow::Error) -> Self {
116                $name(err.to_string())
117            }
118        }
119
120        impl From<std::io::Error> for $name {
121            fn from(err: std::io::Error) -> Self {
122                $name(err.to_string())
123            }
124        }
125    };
126}
127
128// Re-export the macro at the module level
129pub use define_tool_error;