diff --git a/libs/agent/chat/service.rs b/libs/agent/chat/service.rs index 7fcc8b7..d78f3af 100644 --- a/libs/agent/chat/service.rs +++ b/libs/agent/chat/service.rs @@ -61,6 +61,14 @@ impl ChatService { self } + /// Returns all registered tools as OpenAI tool definitions. + pub fn tools(&self) -> Vec { + self.tool_registry + .as_ref() + .map(|r| r.to_openai_tools()) + .unwrap_or_default() + } + #[allow(deprecated)] pub async fn process(&self, request: AiRequest) -> Result { let tools: Vec = request.tools.clone().unwrap_or_default(); diff --git a/libs/agent/lib.rs b/libs/agent/lib.rs index 2efc8bf..fdb8292 100644 --- a/libs/agent/lib.rs +++ b/libs/agent/lib.rs @@ -26,7 +26,7 @@ pub use react::{ Hook, HookAction, NoopHook, ReactAgent, ReactConfig, ReactStep, ToolCallAction, TracingHook, }; pub use tool::{ - ToolCall, ToolCallResult, ToolContext, ToolDefinition, ToolError, ToolExecutor, ToolParam, + ToolCall, ToolCallResult, ToolContext, ToolDefinition, ToolError, ToolExecutor, ToolHandler, ToolParam, ToolRegistry, ToolResult, ToolSchema, }; diff --git a/libs/agent/tool/registry.rs b/libs/agent/tool/registry.rs index ffcf0fe..27a783e 100644 --- a/libs/agent/tool/registry.rs +++ b/libs/agent/tool/registry.rs @@ -25,6 +25,19 @@ type InnerHandlerFn = dyn Fn( pub struct ToolHandler(std::sync::Arc); impl ToolHandler { + /// Creates a new handler from an async closure. + /// The closure should return `Result` (as used by git_tools), + /// which is converted to `Result`. + pub fn new(f: F) -> Self + where + F: Fn(ToolContext, serde_json::Value) -> std::pin::Pin> + Send>> + + Send + + Sync + + 'static, + { + Self(std::sync::Arc::new(f)) + } + pub async fn execute( &self, ctx: ToolContext, diff --git a/libs/room/src/service.rs b/libs/room/src/service.rs index d928224..cd0def1 100644 --- a/libs/room/src/service.rs +++ b/libs/room/src/service.rs @@ -898,8 +898,8 @@ impl RoomService { frequency_penalty: 0.0, presence_penalty: 0.0, think: ai_config.think, - tools: None, - max_tool_depth: 0, + tools: Some(chat_service.tools()), + max_tool_depth: 3, }; let use_streaming = ai_config.stream; diff --git a/libs/service/git_tools/branch.rs b/libs/service/git_tools/branch.rs index 2b7518a..919e6e4 100644 --- a/libs/service/git_tools/branch.rs +++ b/libs/service/git_tools/branch.rs @@ -1,7 +1,8 @@ //! Git branch tools. use super::ctx::GitToolCtx; -use agent::ToolRegistry; +use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; +use std::collections::HashMap; async fn git_branch_list_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { let p: serde_json::Map = serde_json::from_value(args).map_err(|e| e.to_string())?; @@ -73,21 +74,91 @@ async fn git_branch_diff_exec(ctx: GitToolCtx, args: serde_json::Value) -> Resul Ok(serde_json::json!({ "ahead": diff.ahead, "behind": diff.behind, "diverged": diff.diverged })) } -macro_rules! register_fn { - ($registry:expr, $name:expr, $exec:expr) => { - let handler_fn = move |ctx: agent::ToolContext, args: serde_json::Value| async move { - let gctx = super::ctx::GitToolCtx::new(ctx); - $exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }; - $registry.register_fn($name, handler_fn); - }; -} - pub fn register_git_tools(registry: &mut ToolRegistry) { - register_fn!(registry, "git_branch_list", git_branch_list_exec); - register_fn!(registry, "git_branch_info", git_branch_info_exec); - register_fn!(registry, "git_branches_merged", git_branches_merged_exec); - register_fn!(registry, "git_branch_diff", git_branch_diff_exec); + let mut p = HashMap::from([ + ("project_name".into(), ToolParam { + name: "project_name".into(), param_type: "string".into(), + description: Some("Project name (slug)".into()), + required: true, properties: None, items: None, + }), + ("repo_name".into(), ToolParam { + name: "repo_name".into(), param_type: "string".into(), + description: Some("Repository name".into()), + required: true, properties: None, items: None, + }), + ]); + + // git_branch_list + p.insert("remote_only".into(), ToolParam { + name: "remote_only".into(), param_type: "boolean".into(), + description: Some("If true, list only remote-tracking branches".into()), + required: false, properties: None, items: None, + }); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(std::mem::take(&mut p)), required: Some(vec!["project_name".into(), "repo_name".into()]) }; + p = HashMap::from([ + ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), + ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), + ]); + registry.register( + ToolDefinition::new("git_branch_list").description("List all local or remote branches with their current HEAD commit.").parameters(schema), + ToolHandler::new(move |ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_branch_list_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_branch_info + p.insert("name".into(), ToolParam { + name: "name".into(), param_type: "string".into(), + description: Some("Branch name (e.g. main, feature/my-branch)".into()), + required: true, properties: None, items: None, + }); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(std::mem::take(&mut p)), required: Some(vec!["project_name".into(), "repo_name".into(), "name".into()]) }; + registry.register( + ToolDefinition::new("git_branch_info").description("Get detailed information about a specific branch including ahead/behind status vs upstream.").parameters(schema), + ToolHandler::new(move |ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_branch_info_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_branches_merged + p = HashMap::from([ + ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), + ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), + ("branch".into(), ToolParam { name: "branch".into(), param_type: "string".into(), description: Some("Branch to check (source)".into()), required: true, properties: None, items: None }), + ("into".into(), ToolParam { name: "into".into(), param_type: "string".into(), description: Some("Branch to check against (target, default: main)".into()), required: false, properties: None, items: None }), + ]); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "branch".into()]) }; + registry.register( + ToolDefinition::new("git_branches_merged").description("Check whether a branch has been merged into another branch, and find the merge base.").parameters(schema), + ToolHandler::new(move |ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_branches_merged_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_branch_diff + let p = HashMap::from([ + ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), + ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), + ("local".into(), ToolParam { name: "local".into(), param_type: "string".into(), description: Some("Local branch name".into()), required: true, properties: None, items: None }), + ("remote".into(), ToolParam { name: "remote".into(), param_type: "string".into(), description: Some("Remote branch name (defaults to local)".into()), required: false, properties: None, items: None }), + ]); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "local".into()]) }; + registry.register( + ToolDefinition::new("git_branch_diff").description("Compare a local branch against its remote counterpart to see how many commits ahead/behind they are.").parameters(schema), + ToolHandler::new(move |ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_branch_diff_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); } \ No newline at end of file diff --git a/libs/service/git_tools/commit.rs b/libs/service/git_tools/commit.rs index 866be0c..19f2657 100644 --- a/libs/service/git_tools/commit.rs +++ b/libs/service/git_tools/commit.rs @@ -1,8 +1,9 @@ //! Git commit-related tools. use super::ctx::GitToolCtx; -use agent::ToolRegistry; +use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; use chrono::TimeZone; +use std::collections::HashMap; // --- Execution functions for each tool --- @@ -206,25 +207,159 @@ async fn git_reflog_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { - let handler_fn = move |ctx: agent::ToolContext, args: serde_json::Value| async move { - let gctx = super::ctx::GitToolCtx::new(ctx); - $exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }; - $registry.register_fn($name, handler_fn); - }; +/// Common required params used across all git tools. +fn common_params() -> HashMap { + HashMap::from([ + ("project_name".into(), ToolParam { + name: "project_name".into(), + param_type: "string".into(), + description: Some("Project name (slug)".into()), + required: true, + properties: None, + items: None, + }), + ("repo_name".into(), ToolParam { + name: "repo_name".into(), + param_type: "string".into(), + description: Some("Repository name".into()), + required: true, + properties: None, + items: None, + }), + ]) } pub fn register_git_tools(registry: &mut ToolRegistry) { - register_fn!(registry, "git_log", git_log_exec); - register_fn!(registry, "git_show", git_show_exec); - register_fn!(registry, "git_search_commits", git_search_commits_exec); - register_fn!(registry, "git_commit_info", git_commit_info_exec); - register_fn!(registry, "git_graph", git_graph_exec); - register_fn!(registry, "git_reflog", git_reflog_exec); + // git_log + let mut p = common_params(); + p.insert("rev".into(), ToolParam { + name: "rev".into(), param_type: "string".into(), + description: Some("Revision/range specifier (branch name, commit hash, etc.)".into()), + required: false, properties: None, items: None, + }); + p.insert("limit".into(), ToolParam { + name: "limit".into(), param_type: "integer".into(), + description: Some("Maximum number of commits to return".into()), + required: false, properties: None, items: None, + }); + p.insert("skip".into(), ToolParam { + name: "skip".into(), param_type: "integer".into(), + description: Some("Number of commits to skip".into()), + required: false, properties: None, items: None, + }); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) }; + registry.register( + ToolDefinition::new("git_log").description("List commits in a repository, optionally filtered by revision range.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_log_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_show + let mut p = common_params(); + p.insert("rev".into(), ToolParam { + name: "rev".into(), param_type: "string".into(), + description: Some("Revision to show (commit hash, branch, tag)".into()), + required: true, properties: None, items: None, + }); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "rev".into()]) }; + registry.register( + ToolDefinition::new("git_show").description("Show detailed commit information including message, author, refs, and diff stats.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_show_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_search_commits + let mut p = common_params(); + p.insert("query".into(), ToolParam { + name: "query".into(), param_type: "string".into(), + description: Some("Keyword to search in commit messages".into()), + required: true, properties: None, items: None, + }); + p.insert("limit".into(), ToolParam { + name: "limit".into(), param_type: "integer".into(), + description: Some("Maximum results to return".into()), + required: false, properties: None, items: None, + }); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "query".into()]) }; + registry.register( + ToolDefinition::new("git_search_commits").description("Search commit messages for a keyword and return matching commits.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_search_commits_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_commit_info + let mut p = common_params(); + p.insert("rev".into(), ToolParam { + name: "rev".into(), param_type: "string".into(), + description: Some("Revision to look up (full or short commit hash, branch, tag)".into()), + required: true, properties: None, items: None, + }); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "rev".into()]) }; + registry.register( + ToolDefinition::new("git_commit_info").description("Get detailed metadata for a specific commit (author, committer, parents, tree)." ).parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_commit_info_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_graph + let mut p = common_params(); + p.insert("rev".into(), ToolParam { + name: "rev".into(), param_type: "string".into(), + description: Some("Starting revision (default: HEAD)".into()), + required: false, properties: None, items: None, + }); + p.insert("limit".into(), ToolParam { + name: "limit".into(), param_type: "integer".into(), + description: Some("Maximum number of commits (default: 20)".into()), + required: false, properties: None, items: None, + }); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) }; + registry.register( + ToolDefinition::new("git_graph").description("Show an ASCII commit graph with branch lanes and refs.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_graph_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_reflog + let mut p = common_params(); + p.insert("ref_name".into(), ToolParam { + name: "ref_name".into(), param_type: "string".into(), + description: Some("Reference name (e.g. refs/heads/main). Defaults to all refs.".into()), + required: false, properties: None, items: None, + }); + p.insert("limit".into(), ToolParam { + name: "limit".into(), param_type: "integer".into(), + description: Some("Maximum number of entries (default: 50)".into()), + required: false, properties: None, items: None, + }); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) }; + registry.register( + ToolDefinition::new("git_reflog").description("Show the reference log (reflog) recording when branch tips and refs were updated.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_reflog_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); } diff --git a/libs/service/git_tools/diff.rs b/libs/service/git_tools/diff.rs index c47e4fe..ca89da1 100644 --- a/libs/service/git_tools/diff.rs +++ b/libs/service/git_tools/diff.rs @@ -1,7 +1,8 @@ //! Git diff and blame tools. use super::ctx::GitToolCtx; -use agent::ToolRegistry; +use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; +use std::collections::HashMap; async fn git_diff_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { let p: serde_json::Map = serde_json::from_value(args).map_err(|e| e.to_string())?; @@ -128,20 +129,61 @@ async fn git_blame_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { - let handler_fn = move |ctx: agent::ToolContext, args: serde_json::Value| async move { - let gctx = super::ctx::GitToolCtx::new(ctx); - $exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }; - $registry.register_fn($name, handler_fn); - }; -} - pub fn register_git_tools(registry: &mut ToolRegistry) { - register_fn!(registry, "git_diff", git_diff_exec); - register_fn!(registry, "git_diff_stats", git_diff_stats_exec); - register_fn!(registry, "git_blame", git_blame_exec); + // git_diff + let p = HashMap::from([ + ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), + ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), + ("base".into(), ToolParam { name: "base".into(), param_type: "string".into(), description: Some("Base revision (commit hash or branch). Defaults to HEAD.".into()), required: false, properties: None, items: None }), + ("head".into(), ToolParam { name: "head".into(), param_type: "string".into(), description: Some("Head revision to diff against base. Requires base to be set.".into()), required: false, properties: None, items: None }), + ("paths".into(), ToolParam { name: "paths".into(), param_type: "array".into(), description: Some("Filter diff to specific file paths".into()), required: false, properties: None, items: Some(Box::new(ToolParam { name: "".into(), param_type: "string".into(), description: None, required: false, properties: None, items: None })) }), + ]); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) }; + registry.register( + ToolDefinition::new("git_diff").description("Show file changes between two commits, or between a commit and the working directory.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_diff_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_diff_stats + let p = HashMap::from([ + ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), + ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), + ("base".into(), ToolParam { name: "base".into(), param_type: "string".into(), description: Some("Base revision".into()), required: true, properties: None, items: None }), + ("head".into(), ToolParam { name: "head".into(), param_type: "string".into(), description: Some("Head revision".into()), required: true, properties: None, items: None }), + ]); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "base".into(), "head".into()]) }; + registry.register( + ToolDefinition::new("git_diff_stats").description("Get aggregated diff statistics (files changed, insertions, deletions) between two revisions.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_diff_stats_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_blame + let p = HashMap::from([ + ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), + ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), + ("path".into(), ToolParam { name: "path".into(), param_type: "string".into(), description: Some("File path to blame".into()), required: true, properties: None, items: None }), + ("rev".into(), ToolParam { name: "rev".into(), param_type: "string".into(), description: Some("Revision to blame from (default: HEAD)".into()), required: false, properties: None, items: None }), + ("from_line".into(), ToolParam { name: "from_line".into(), param_type: "integer".into(), description: Some("Start line number for blame range".into()), required: false, properties: None, items: None }), + ("to_line".into(), ToolParam { name: "to_line".into(), param_type: "integer".into(), description: Some("End line number for blame range".into()), required: false, properties: None, items: None }), + ]); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "path".into()]) }; + registry.register( + ToolDefinition::new("git_blame").description("Show what revision and author last modified each line of a file (git blame).").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_blame_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); } \ No newline at end of file diff --git a/libs/service/git_tools/tag.rs b/libs/service/git_tools/tag.rs index a6331f7..48c1be9 100644 --- a/libs/service/git_tools/tag.rs +++ b/libs/service/git_tools/tag.rs @@ -1,7 +1,8 @@ //! Git tag tools. use super::ctx::GitToolCtx; -use agent::ToolRegistry; +use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; +use std::collections::HashMap; async fn git_tag_list_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { let p: serde_json::Map = serde_json::from_value(args).map_err(|e| e.to_string())?; @@ -55,19 +56,38 @@ async fn git_tag_info_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { - let handler_fn = move |ctx: agent::ToolContext, args: serde_json::Value| async move { - let gctx = super::ctx::GitToolCtx::new(ctx); - $exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }; - $registry.register_fn($name, handler_fn); - }; -} - pub fn register_git_tools(registry: &mut ToolRegistry) { - register_fn!(registry, "git_tag_list", git_tag_list_exec); - register_fn!(registry, "git_tag_info", git_tag_info_exec); + // git_tag_list + let p = HashMap::from([ + ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), + ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), + ("pattern".into(), ToolParam { name: "pattern".into(), param_type: "string".into(), description: Some("Filter tags by name pattern (supports * wildcard)".into()), required: false, properties: None, items: None }), + ]); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) }; + registry.register( + ToolDefinition::new("git_tag_list").description("List all tags in a repository, optionally filtered by a name pattern.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_tag_list_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_tag_info + let p = HashMap::from([ + ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), + ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), + ("name".into(), ToolParam { name: "name".into(), param_type: "string".into(), description: Some("Tag name to look up".into()), required: true, properties: None, items: None }), + ]); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "name".into()]) }; + registry.register( + ToolDefinition::new("git_tag_info").description("Get detailed information about a specific tag including target commit, annotation, and tagger.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_tag_info_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); } \ No newline at end of file diff --git a/libs/service/git_tools/tree.rs b/libs/service/git_tools/tree.rs index b474d7b..64c77bf 100644 --- a/libs/service/git_tools/tree.rs +++ b/libs/service/git_tools/tree.rs @@ -1,8 +1,9 @@ //! Git tree and file tools. use super::ctx::GitToolCtx; -use agent::ToolRegistry; +use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; use base64::Engine; +use std::collections::HashMap; async fn git_file_content_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result { let p: serde_json::Map = serde_json::from_value(args).map_err(|e| e.to_string())?; @@ -107,20 +108,58 @@ fn flatten_commit(c: &git::commit::types::CommitMeta) -> serde_json::Value { }) } -macro_rules! register_fn { - ($registry:expr, $name:expr, $exec:expr) => { - let handler_fn = move |ctx: agent::ToolContext, args: serde_json::Value| async move { - let gctx = super::ctx::GitToolCtx::new(ctx); - $exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }; - $registry.register_fn($name, handler_fn); - }; -} - pub fn register_git_tools(registry: &mut ToolRegistry) { - register_fn!(registry, "git_file_content", git_file_content_exec); - register_fn!(registry, "git_tree_ls", git_tree_ls_exec); - register_fn!(registry, "git_file_history", git_file_history_exec); + // git_file_content + let p = HashMap::from([ + ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), + ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), + ("path".into(), ToolParam { name: "path".into(), param_type: "string".into(), description: Some("File path within the repository".into()), required: true, properties: None, items: None }), + ("rev".into(), ToolParam { name: "rev".into(), param_type: "string".into(), description: Some("Revision to read file from (default: HEAD)".into()), required: false, properties: None, items: None }), + ]); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "path".into()]) }; + registry.register( + ToolDefinition::new("git_file_content").description("Read the full content of a file at a given revision. Handles both text and binary files.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_file_content_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_tree_ls + let p = HashMap::from([ + ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), + ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), + ("path".into(), ToolParam { name: "path".into(), param_type: "string".into(), description: Some("Directory path to list (root if omitted)".into()), required: false, properties: None, items: None }), + ("rev".into(), ToolParam { name: "rev".into(), param_type: "string".into(), description: Some("Revision to list tree from (default: HEAD)".into()), required: false, properties: None, items: None }), + ]); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) }; + registry.register( + ToolDefinition::new("git_tree_ls").description("List the contents of a directory (tree) at a given revision, showing files and subdirectories.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_tree_ls_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); + + // git_file_history + let p = HashMap::from([ + ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), + ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), + ("path".into(), ToolParam { name: "path".into(), param_type: "string".into(), description: Some("File path to trace history for".into()), required: true, properties: None, items: None }), + ("limit".into(), ToolParam { name: "limit".into(), param_type: "integer".into(), description: Some("Maximum number of commits to return (default: 20)".into()), required: false, properties: None, items: None }), + ]); + let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "path".into()]) }; + registry.register( + ToolDefinition::new("git_file_history").description("Show the commit history for a specific file, listing all commits that modified it.").parameters(schema), + ToolHandler::new(|ctx, args| { + let gctx = super::ctx::GitToolCtx::new(ctx); + Box::pin(async move { + git_file_history_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) + }) + }), + ); } \ No newline at end of file