use crate::CodingAgentTools;
use crate::just;
use crate::types::AgentLocation;
use crate::types::AgentOutput;
use crate::types::AgentType;
use crate::types::Depth;
use crate::types::GlobOutput;
use crate::types::GrepOutput;
use crate::types::LsOutput;
use crate::types::OutputMode;
use crate::types::Show;
use crate::types::SortOrder;
use agentic_config::types::CliToolsConfig;
use agentic_config::types::SubagentsConfig;
use agentic_tools_core::Tool;
use agentic_tools_core::ToolContext;
use agentic_tools_core::ToolError;
use agentic_tools_core::ToolRegistry;
use futures::future::BoxFuture;
use schemars::JsonSchema;
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct LsInput {
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub depth: Option<Depth>,
#[serde(default)]
pub show: Option<Show>,
#[serde(default)]
pub ignore: Option<Vec<String>>,
#[serde(default)]
pub hidden: Option<bool>,
}
#[derive(Clone)]
pub struct LsTool {
tools: Arc<CodingAgentTools>,
}
impl LsTool {
pub fn new(tools: Arc<CodingAgentTools>) -> Self {
Self { tools }
}
}
impl Tool for LsTool {
type Input = LsInput;
type Output = LsOutput;
const NAME: &'static str = "cli_ls";
const DESCRIPTION: &'static str = "List files and directories. Depth: 0=header only, 1=children (default), 2-10=tree. Filter with show='files'|'dirs'|'all'. Gitignore-aware. For shallow queries, call with same params again for next page.";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let tools = Arc::clone(&self.tools);
Box::pin(async move {
tools
.ls(
input.path,
input.depth,
input.show,
input.ignore,
input.hidden,
)
.await
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct AskAgentInput {
#[serde(default)]
pub agent_type: Option<AgentType>,
#[serde(default)]
pub location: Option<AgentLocation>,
pub query: String,
}
#[derive(Clone)]
pub struct AskAgentTool {
tools: Arc<CodingAgentTools>,
}
impl AskAgentTool {
pub fn new(tools: Arc<CodingAgentTools>) -> Self {
Self { tools }
}
}
impl Tool for AskAgentTool {
type Input = AskAgentInput;
type Output = AgentOutput;
const NAME: &'static str = "ask_agent";
const DESCRIPTION: &'static str = "Spawn a Claude subagent for discovery or deep analysis. Returns a single text response; no side effects.
Agent types:
- locator (haiku): Finds WHERE things are. Fast, shallow discovery via cli_grep/cli_glob/cli_ls. Returns file paths grouped by purpose. Cannot read file contents deeply.
- analyzer (sonnet): Explains HOW things work. Reads files, traces data flow, provides technical analysis. Must cite file:line for all claims.
Locations:
- codebase: Current repository. Paths are repo-relative.
- thoughts: Active branch documents (research/plans/artifacts). Uses thoughts_list_documents for discovery.
- references: Cloned reference repos. Paths start with references/{org}/{repo}/.
- web: Internet search. Returns URLs with quotes and source attribution.
When to use:
- Use locator when you need to find files/resources but don't yet know where they are.
- Use analyzer when you need to understand implementation details or extract specific information with citations.
- Use thoughts/references locations when the answer likely exists in existing documentation or external examples.
- Use web when you need external documentation, API references, or information not in the codebase.
When NOT to use:
- If you already know the file path, use Read directly.
- If you need a simple directory listing, use cli_ls.
- For pattern matching in known locations, use cli_grep or cli_glob.
- This tool cannot write files or make changes.
Usage notes:
- Provide clear, specific queries. The subagent is stateless and receives no prior context.
- Locator returns locations only; use analyzer or Read for content.
- Multiple ask_agent calls can run in parallel for independent queries.";
fn call(
&self,
input: Self::Input,
ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let tools = Arc::clone(&self.tools);
let ctx = ctx.clone();
Box::pin(async move {
tools
.ask_agent(input.agent_type, input.location, input.query, &ctx)
.await
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct SearchGrepInput {
pub pattern: String,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub mode: Option<OutputMode>,
#[serde(default)]
pub globs: Option<Vec<String>>,
#[serde(default)]
pub ignore: Option<Vec<String>>,
#[serde(default)]
pub include_hidden: Option<bool>,
#[serde(default)]
pub case_insensitive: Option<bool>,
#[serde(default)]
pub multiline: Option<bool>,
#[serde(default)]
pub line_numbers: Option<bool>,
#[serde(default)]
pub context: Option<u32>,
#[serde(default)]
pub context_before: Option<u32>,
#[serde(default)]
pub context_after: Option<u32>,
#[serde(default)]
pub include_binary: Option<bool>,
#[serde(default)]
pub head_limit: Option<usize>,
#[serde(default)]
pub offset: Option<usize>,
}
#[derive(Clone)]
pub struct SearchGrepTool {
tools: Arc<CodingAgentTools>,
}
impl SearchGrepTool {
pub fn new(tools: Arc<CodingAgentTools>) -> Self {
Self { tools }
}
}
impl Tool for SearchGrepTool {
type Input = SearchGrepInput;
type Output = GrepOutput;
const NAME: &'static str = "cli_grep";
const DESCRIPTION: &'static str = "Regex-based search. Modes: files (default), content, count. Stateless pagination via head_limit+offset.";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let tools = Arc::clone(&self.tools);
Box::pin(async move {
tools
.search_grep(
input.pattern,
input.path,
input.mode,
input.globs,
input.ignore,
input.include_hidden,
input.case_insensitive,
input.multiline,
input.line_numbers,
input.context,
input.context_before,
input.context_after,
input.include_binary,
input.head_limit,
input.offset,
)
.await
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct SearchGlobInput {
pub pattern: String,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub ignore: Option<Vec<String>>,
#[serde(default)]
pub include_hidden: Option<bool>,
#[serde(default)]
pub sort: Option<SortOrder>,
#[serde(default)]
pub head_limit: Option<usize>,
#[serde(default)]
pub offset: Option<usize>,
}
#[derive(Clone)]
pub struct SearchGlobTool {
tools: Arc<CodingAgentTools>,
}
impl SearchGlobTool {
pub fn new(tools: Arc<CodingAgentTools>) -> Self {
Self { tools }
}
}
impl Tool for SearchGlobTool {
type Input = SearchGlobInput;
type Output = GlobOutput;
const NAME: &'static str = "cli_glob";
const DESCRIPTION: &'static str = "Glob-based path match. Sorting by name (default) or mtime (newest first). Stateless pagination via head_limit+offset.";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let tools = Arc::clone(&self.tools);
Box::pin(async move {
tools
.search_glob(
input.pattern,
input.path,
input.ignore,
input.include_hidden,
input.sort,
input.head_limit,
input.offset,
)
.await
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct JustSearchInput {
#[serde(default)]
pub query: Option<String>,
#[serde(default)]
pub dir: Option<String>,
}
#[derive(Clone)]
pub struct JustSearchTool {
tools: Arc<CodingAgentTools>,
}
impl JustSearchTool {
pub fn new(tools: Arc<CodingAgentTools>) -> Self {
Self { tools }
}
}
impl Tool for JustSearchTool {
type Input = JustSearchInput;
type Output = just::SearchOutput;
const NAME: &'static str = "cli_just_search";
const DESCRIPTION: &'static str = "Search justfile recipes by name or docs. Optional dir filter. Same params => next page. Page size: 10.";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let tools = Arc::clone(&self.tools);
Box::pin(async move { tools.just_search(input.query, input.dir).await })
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct JustExecuteInput {
pub recipe: String,
#[serde(default)]
pub dir: Option<String>,
#[serde(default)]
pub args: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Clone)]
pub struct JustExecuteTool {
tools: Arc<CodingAgentTools>,
}
impl JustExecuteTool {
pub fn new(tools: Arc<CodingAgentTools>) -> Self {
Self { tools }
}
}
impl Tool for JustExecuteTool {
type Input = JustExecuteInput;
type Output = just::ExecuteOutput;
const NAME: &'static str = "cli_just_execute";
const DESCRIPTION: &'static str = "Execute a just recipe. Defaults to root justfile if no dir specified. Only disambiguate if recipe not in root.";
fn call(
&self,
input: Self::Input,
ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let tools = Arc::clone(&self.tools);
let ctx = ctx.clone();
Box::pin(async move {
tools
.just_execute(input.recipe, input.dir, input.args, &ctx)
.await
})
}
}
pub fn build_registry(subagents: SubagentsConfig, cli_tools: CliToolsConfig) -> ToolRegistry {
let tools = Arc::new(CodingAgentTools::with_config(subagents, cli_tools));
ToolRegistry::builder()
.register::<LsTool, ()>(LsTool::new(Arc::clone(&tools)))
.register::<AskAgentTool, ()>(AskAgentTool::new(Arc::clone(&tools)))
.register::<SearchGrepTool, ()>(SearchGrepTool::new(Arc::clone(&tools)))
.register::<SearchGlobTool, ()>(SearchGlobTool::new(Arc::clone(&tools)))
.register::<JustSearchTool, ()>(JustSearchTool::new(Arc::clone(&tools)))
.register::<JustExecuteTool, ()>(JustExecuteTool::new(tools))
.finish()
}