#[cfg(feature = "archive-tools")]
mod archive;
mod calc;
mod call_graph_sketch;
mod cargo_tools;
mod ci_tools;
mod code_metrics;
mod code_nav;
mod command;
pub use command::{
PreparedRunCommand, RunCommandError, RunCommandTestCacheOpts,
argv_has_shell_operators, argv_needs_posix_shell_wrap, argv_needs_shell_expansion,
join_run_command_argv_tail, join_run_command_shell_script, peel_cd_prefix_argv_for_shell_policy,
posix_shell_on_allowlist,
prepare_run_command_for_pty_spawn, run_checked, run_checked_wait,
scan_run_command_unsafe_args_json,
};
mod command_line_prepare;
pub use command_line_prepare::split_command_prefix_if_embedded;
mod container_tools;
mod contract_map;
mod date_calc;
mod debug_tools;
mod diagnostics;
mod docs_health_sweep;
pub use diagnostics::capture_trimmed;
pub use parse_args::parse_args_json;
pub use parse_args::parse_args_typed;
mod env_var_check;
mod error_playbook;
mod exec;
pub use exec::{
resolve_workspace_executable, run_command_invocation_targets_workspace_script_or_executable,
};
mod file;
pub use file::canonical_workspace_root;
pub use file::list_tree;
pub(crate) use file::resolve_for_read_open;
mod format;
mod frontend_tools;
mod git;
mod github_cli;
mod go_tools;
mod grep;
mod grep_try;
#[cfg(feature = "web-fetch-tools")]
pub mod http_fetch;
mod json_format;
mod jvm_tools;
mod lint;
mod markdown_links;
mod nodejs_tools;
pub mod output_util;
mod package_query;
mod parse_args;
mod patch;
mod precommit_tools;
mod process_tools;
mod python_tools;
mod quality_tools;
mod regex_test;
mod release_docs;
mod repo_overview;
mod rust_ide;
mod schedule;
mod schema_check;
mod security_tools;
mod source_analysis_tools;
mod spell_astgrep_tools;
#[cfg(feature = "csv-tools")]
mod structured_data;
pub mod structured_preview;
mod symbol;
#[cfg(feature = "csv-tools")]
mod table_text;
pub mod terminal_session;
mod test_result_cache;
#[cfg(feature = "diff-tools")]
mod text_diff;
mod text_transform;
mod time;
mod todo_scan;
#[cfg(feature = "schema-tools")]
mod tool_args_validate;
mod tool_json_schema;
mod tool_param_types;
mod tool_params;
mod tool_specs_registry;
mod tool_summary;
mod tool_summary_args;
#[cfg(feature = "unit-tools")]
mod unit_convert;
mod weather;
mod web_search;
mod workspace_image_chat_hint;
mod write_sse_preview;
pub mod dev_tag;
use std::sync::Arc;
use crate::cm_tools::memory_tool_host::{CodebaseSemanticToolHost, LongTermMemoryToolHost};
use crate::cm_tools::tool_result::{ToolError, ToolResult};
use crate::cm_tools::workspace::changelist::WorkspaceChangelist;
use crate::cm_config::{AgentConfig, ExposeSecret};
use crate::cm_types::{FunctionDef, Tool};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolCategory {
Basic,
Development,
}
pub struct ToolContext<'a> {
pub cfg: Option<&'a AgentConfig>,
pub codebase_semantic_host: Option<&'a dyn CodebaseSemanticToolHost>,
pub command_max_output_len: usize,
pub weather_timeout_secs: u64,
pub allowed_commands: &'a [String],
pub working_dir: &'a std::path::Path,
pub web_search_timeout_secs: u64,
pub web_search_provider: crate::cm_config::WebSearchProvider,
pub web_search_api_key: &'a str,
pub web_search_max_results: u32,
pub http_fetch_allowed_prefixes: &'a [String],
pub http_fetch_timeout_secs: u64,
pub http_fetch_max_response_bytes: usize,
pub command_timeout_secs: u64,
pub read_file_turn_cache: Option<&'a crate::cm_tools::read_file_turn_cache::ReadFileTurnCache>,
pub workspace_changelist: Option<&'a Arc<WorkspaceChangelist>>,
pub test_result_cache_enabled: bool,
pub test_result_cache_max_entries: usize,
pub long_term_memory_host: Option<&'a dyn LongTermMemoryToolHost>,
}
pub use crate::cm_tools::workspace::path::WorkspacePathError;
pub fn resolve_workspace_path_for_read(
working_dir: &std::path::Path,
rel: &str,
) -> Result<std::path::PathBuf, WorkspacePathError> {
file::resolve_for_read(working_dir, rel)
}
pub fn tool_context_for<'a>(
cfg: &'a AgentConfig,
allowed_commands: &'a [String],
working_dir: &'a std::path::Path,
) -> ToolContext<'a> {
ToolContext {
cfg: Some(cfg),
codebase_semantic_host: None,
command_max_output_len: cfg.command_exec.command_max_output_len,
weather_timeout_secs: cfg.weather_tool.weather_timeout_secs,
allowed_commands,
working_dir,
web_search_timeout_secs: cfg.web_search.web_search_timeout_secs,
web_search_provider: cfg.web_search.web_search_provider,
web_search_api_key: cfg.web_search.web_search_api_key.expose_secret(),
web_search_max_results: cfg.web_search.web_search_max_results,
http_fetch_allowed_prefixes: cfg.http_fetch.http_fetch_allowed_prefixes.as_slice(),
http_fetch_timeout_secs: cfg.http_fetch.http_fetch_timeout_secs,
http_fetch_max_response_bytes: cfg.http_fetch.http_fetch_max_response_bytes,
command_timeout_secs: cfg.command_exec.command_timeout_secs,
read_file_turn_cache: None,
workspace_changelist: None,
test_result_cache_enabled: cfg.chat_queues_cache.test_result_cache_enabled,
test_result_cache_max_entries: cfg.chat_queues_cache.test_result_cache_max_entries,
long_term_memory_host: None,
}
}
pub fn tool_context_for_with_read_cache_and_memory<'a>(
cfg: &'a AgentConfig,
allowed_commands: &'a [String],
working_dir: &'a std::path::Path,
read_file_turn_cache: Option<&'a crate::cm_tools::read_file_turn_cache::ReadFileTurnCache>,
workspace_changelist: Option<&'a Arc<WorkspaceChangelist>>,
codebase_semantic_host: Option<&'a dyn CodebaseSemanticToolHost>,
long_term_memory_host: Option<&'a dyn LongTermMemoryToolHost>,
) -> ToolContext<'a> {
ToolContext {
read_file_turn_cache,
workspace_changelist,
codebase_semantic_host,
long_term_memory_host,
..tool_context_for(cfg, allowed_commands, working_dir)
}
}
mod runners;
mod runners_gh;
pub use runners::*;
pub use runners_gh::*;
fn tool_specs() -> &'static [ToolSpec] {
tool_specs_registry::tool_specs()
}
fn tool_spec_index() -> &'static std::collections::HashMap<&'static str, usize> {
use std::sync::LazyLock;
static INDEX: LazyLock<std::collections::HashMap<&'static str, usize>> = LazyLock::new(|| {
tool_specs()
.iter()
.enumerate()
.map(|(i, s)| (s.name, i))
.collect()
});
&INDEX
}
fn find_spec(name: &str) -> Option<&'static ToolSpec> {
tool_spec_index().get(name).map(|&idx| &tool_specs()[idx])
}
#[derive(Clone, Copy, Default)]
pub struct ToolsBuildOptions<'a> {
pub categories: Option<&'a [ToolCategory]>,
pub dev_tags: Option<&'a [&'a str]>,
}
fn tool_passes_filters(spec: &ToolSpec, opts: ToolsBuildOptions<'_>) -> bool {
if !cfg!(feature = "fastembed") && tool_spec_requires_fastembed(spec.name) {
return false;
}
let cats = opts.categories.unwrap_or(&[]);
if !cats.is_empty() && !cats.contains(&spec.category) {
return false;
}
let Some(wanted) = opts.dev_tags.and_then(|t| (!t.is_empty()).then_some(t)) else {
return true;
};
if spec.category != ToolCategory::Development {
return true;
}
dev_tag::tags_for_tool_name(spec.name)
.iter()
.any(|tag| wanted.contains(tag))
}
pub fn build_tools() -> Vec<Tool> {
build_tools_with_options(ToolsBuildOptions::default())
}
pub fn build_tools_filtered(allowed: Option<&[ToolCategory]>) -> Vec<Tool> {
build_tools_with_options(ToolsBuildOptions {
categories: allowed,
dev_tags: None,
})
}
pub fn build_tools_with_options(opts: ToolsBuildOptions<'_>) -> Vec<Tool> {
tool_specs()
.iter()
.filter(|s| tool_passes_filters(s, opts))
.map(|s| Tool {
typ: TOOL_TYPE_FUNCTION.to_string(),
function: FunctionDef {
name: s.name.to_string(),
description: s.description.to_string(),
parameters: cached_params(s),
},
})
.collect()
}
const TOOL_TYPE_FUNCTION: &str = "function";
fn cached_params(spec: &ToolSpec) -> serde_json::Value {
use std::sync::LazyLock;
static CACHE: LazyLock<std::collections::HashMap<&'static str, serde_json::Value>> =
LazyLock::new(|| {
tool_specs()
.iter()
.map(|s| (s.name, (s.parameters)()))
.collect()
});
CACHE
.get(spec.name)
.cloned()
.unwrap_or_else(|| (spec.parameters)())
}
pub fn cached_params_for_tool_name(name: &str) -> Option<serde_json::Value> {
find_spec(name).map(cached_params)
}
pub use schema_check::workflow_tool_args_satisfy_required;
pub fn run_tool(name: &str, args_json: &str, ctx: &ToolContext<'_>) -> String {
if !cfg!(feature = "fastembed") && tool_spec_requires_fastembed(name) {
return "错误:codebase_semantic_search 需要 `fastembed` Cargo feature;当前构建未启用。"
.to_string();
}
match find_spec(name) {
Some(spec) => {
let args_eff =
match tool_args_validate::effective_builtin_tool_args_json(name, args_json) {
Ok(c) => c,
Err(e) => return format!("错误:{e}"),
};
if let Some(Err(e)) =
tool_args_validate::validate_parsed_str_for_builtin(name, args_eff.as_ref())
{
return format!("错误:{e}");
}
(spec.runner)(args_eff.as_ref(), ctx)
}
None => format!("未知工具:{}", name),
}
}
#[allow(clippy::result_large_err)]
fn run_tool_dispatch(
name: &str,
args_json: &str,
ctx: &ToolContext<'_>,
) -> Result<(String, crate::cm_tools::tool_result::ParsedLegacyOutput), ToolError> {
if !cfg!(feature = "fastembed") && tool_spec_requires_fastembed(name) {
return Err(ToolError::invalid_args(
"codebase_semantic_search 需要 `fastembed` Cargo feature;当前构建未启用。".to_string(),
));
}
if find_spec(name).is_none() {
return Err(ToolError::unknown_tool(name));
}
let args_eff = tool_args_validate::effective_builtin_tool_args_json(name, args_json)
.map_err(ToolError::invalid_args)?;
let args_ref = args_eff.as_ref();
if let Some(Err(e)) = tool_args_validate::validate_parsed_str_for_builtin(name, args_ref) {
return Err(ToolError::invalid_args(e));
}
match name {
"terminal_session" => Err(ToolError::invalid_args(
"terminal_session 须由服务端异步调度执行。".to_string(),
)),
"run_command" => {
let test_cache =
ctx.test_result_cache_enabled
.then_some(command::RunCommandTestCacheOpts {
enabled: true,
max_entries: ctx.test_result_cache_max_entries,
workspace_root: ctx.working_dir,
});
let wait = crate::cm_tools::subprocess_session::SubprocessWaitCtl::with_wall_secs(
ctx.command_timeout_secs,
);
match command::run_try_wait(
args_ref,
ctx.command_max_output_len,
ctx.allowed_commands,
ctx.working_dir,
test_cache,
false,
&wait,
) {
Ok(output) => {
let parsed = crate::cm_tools::tool_result::parse_legacy_output(name, &output);
if parsed.ok {
let output =
workspace_image_chat_hint::append_if_needed(args_ref, output);
Ok((output, parsed))
} else {
Err(ToolError::from_parsed_legacy(name, &parsed, output))
}
}
Err(e) => Err(e),
}
}
"cargo_check" => {
cargo_tools::cargo_check_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"cargo_test" => cargo_tools::cargo_test_try(
args_ref,
ctx.working_dir,
ctx.command_max_output_len,
Some(ctx),
)
.map(|output| finish_dispatch_parsed(name, args_ref, output)),
"cargo_clippy" => {
cargo_tools::cargo_clippy_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"cargo_metadata" => {
cargo_tools::cargo_metadata_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"cargo_tree" => {
cargo_tools::cargo_tree_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"cargo_clean" => {
cargo_tools::cargo_clean_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"cargo_doc" => {
cargo_tools::cargo_doc_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"cargo_nextest" => {
cargo_tools::cargo_nextest_try(
args_ref,
ctx.working_dir,
ctx.command_max_output_len,
Some(ctx.command_timeout_secs),
)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"cargo_outdated" => {
cargo_tools::cargo_outdated_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"cargo_machete" => {
cargo_tools::cargo_machete_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"cargo_udeps" => {
cargo_tools::cargo_udeps_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"cargo_publish_dry_run" => cargo_tools::cargo_publish_dry_run_try(
args_ref,
ctx.working_dir,
ctx.command_max_output_len,
)
.map(|output| finish_dispatch_parsed(name, args_ref, output)),
"cargo_fix" => {
cargo_tools::cargo_fix_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"cargo_run" => {
cargo_tools::cargo_run_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"rust_test_one" => cargo_tools::rust_test_one_try(
args_ref,
ctx.working_dir,
ctx.command_max_output_len,
Some(ctx),
)
.map(|output| finish_dispatch_parsed(name, args_ref, output)),
"rust_rustc" => {
cargo_tools::rust_rustc_try(args_ref, ctx.working_dir, ctx.command_max_output_len)
.map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"read_file" => {
read_file_try_dispatch(args_ref, ctx).map(|output| finish_dispatch_parsed(name, args_ref, output))
}
"search_in_files" => grep_try::search_in_files_try(args_ref, ctx.working_dir)
.map(|output| finish_dispatch_parsed(name, args_ref, output)),
_ => {
let output = run_tool(name, args_ref, ctx);
let parsed = crate::cm_tools::tool_result::parse_legacy_output(name, &output);
if parsed.ok {
let output = workspace_image_chat_hint::append_if_needed(args_ref, output);
Ok((output, parsed))
} else {
Err(ToolError::from_parsed_legacy(name, &parsed, output))
}
}
}
}
fn finish_dispatch_parsed(
name: &str,
args_json: &str,
output: String,
) -> (String, crate::cm_tools::tool_result::ParsedLegacyOutput) {
let output = workspace_image_chat_hint::append_if_needed(args_json, output);
let parsed = crate::cm_tools::tool_result::parse_legacy_output(name, &output);
(output, parsed)
}
#[allow(dead_code, clippy::result_large_err)] pub fn run_tool_try(
name: &str,
args_json: &str,
ctx: &ToolContext<'_>,
) -> Result<String, crate::cm_tools::tool_result::ToolError> {
run_tool_dispatch(name, args_json, ctx).map(|(output, _)| output)
}
pub fn run_tool_result(name: &str, args_json: &str, ctx: &ToolContext<'_>) -> ToolResult {
match run_tool_dispatch(name, args_json, ctx) {
Ok((output, parsed)) => ToolResult::from_parsed(output, parsed),
Err(e) => ToolResult::from_parsed(e.message, e.legacy_parsed),
}
}
pub fn is_compile_command_success(args_json: &str, result: &str) -> bool {
let v: serde_json::Value = match serde_json::from_str(args_json) {
Ok(v) => v,
Err(_) => return false,
};
let cmd = v
.get("command")
.and_then(|c| c.as_str())
.map(|s| s.trim().to_lowercase());
let is_compile_cmd = cmd.as_deref().is_some_and(|c| {
matches!(
c,
"gcc" | "g++" | "clang" | "clang++" | "make" | "cmake" | "ninja"
)
});
if !is_compile_cmd {
return false;
}
let parsed = crate::cm_tools::tool_result::parse_legacy_output("run_command", result);
parsed.ok && parsed.exit_code == Some(0)
}
pub fn summarize_tool_call(name: &str, args_json: &str) -> Option<String> {
let spec = find_spec(name)?;
match &spec.summary {
ToolSummaryKind::None => None,
ToolSummaryKind::Static(s) => Some((*s).to_string()),
ToolSummaryKind::Dynamic(f) => {
let v: serde_json::Value = serde_json::from_str(args_json).ok()?;
f(&v)
}
}
}
pub fn summarize_tool_call_parsed(name: &str, args_parsed: &serde_json::Value) -> Option<String> {
let spec = find_spec(name)?;
match &spec.summary {
ToolSummaryKind::None => None,
ToolSummaryKind::Static(s) => Some((*s).to_string()),
ToolSummaryKind::Dynamic(f) => f(args_parsed),
}
}
pub fn reset_process_tool_globals_for_tests() {
command::reset_run_command_rate_limit_for_tests();
test_result_cache::reset_test_result_cache_for_tests();
}
pub use github_cli::web_api;
#[cfg(test)]
#[path = "mod/tests.rs"]
mod tests;