crabmate 0.5.0

Rust AI agent: OpenAI-compatible chat/completions, function calling, HTTP serve, ops CLI
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
//! 工具定义与执行:时间、计算(bc)、有限 Linux 命令
//!
//! 每个子模块对应一类工具,便于扩展新工具。

#[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};

/// 工具顶层分类(用于 `build_tools_filtered`、文档与后续按场景裁剪工具列表)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolCategory {
    /// 基础工具:时间/计算/天气、联网搜索与受控 HTTP、日程与提醒等(不依赖「在仓库里写代码」)。
    Basic,
    /// 开发工具:工作区文件、Git、Cargo/前端构建与测试、Lint、补丁、符号搜索、工作流等。
    Development,
}

pub struct ToolContext<'a> {
    /// 主 Agent / `tool_context_for*` 路径填充;工作流节点等自建上下文可为 `None`(部分工具将报错)。
    pub cfg: Option<&'a AgentConfig>,
    /// 代码语义检索;主 Agent 路径由 `crabmate-internal` 注入。
    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,
    /// `run_command` / 部分子进程工具的墙上时钟上限(秒);`python_snippet_run` 等复用。
    pub command_timeout_secs: u64,
    /// 单轮 `run_agent_turn` 内 `read_file` 缓存;`None` 表示关闭。
    pub read_file_turn_cache: Option<&'a crate::cm_tools::read_file_turn_cache::ReadFileTurnCache>,
    /// 本会话工作区变更集(按 `long_term_memory_scope_id`);`None` 时不记录。
    pub workspace_changelist: Option<&'a Arc<WorkspaceChangelist>>,
    /// `cargo_test` / `npm run test` / 部分 `run_command cargo test` 的进程内输出缓存。
    pub test_result_cache_enabled: bool,
    pub test_result_cache_max_entries: usize,
    /// 长期记忆工具;主 Agent 路径由 `crabmate-internal` 注入。
    pub long_term_memory_host: Option<&'a dyn LongTermMemoryToolHost>,
}

/// 由 [`AgentConfig`] 与当前工作目录、命令白名单构造工具上下文(供 `run_tool` 使用)。
/// 与内置文件工具相同的路径规则:将相对路径解析为工作区内绝对路径(供变更集等跨模块只读)。
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,
    }
}

/// 在 [`tool_context_for`] 基础上挂载单轮 `read_file` 缓存、会话变更集与可选记忆宿主。
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()
}

/// name → tool_specs() 数组下标索引,首次访问时构建,O(1) 查找。
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> {
    /// `None` 或 `Some(&[])`:不按顶层分类过滤(`Basic` 与 `Development` 均保留)。
    pub categories: Option<&'a [ToolCategory]>,
    /// `None` 或 `Some(&[])`:不按标签过滤。`Some(non-empty)`:仅保留 **Development** 工具中
    /// [`dev_tag::tags_for_tool_name`] 与列表 **有交集** 者;`Basic` 仍只受 `categories` 约束。
    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))
}

/// 构建传给 API 的工具列表(表驱动注册)。
pub fn build_tools() -> Vec<Tool> {
    build_tools_with_options(ToolsBuildOptions::default())
}

/// 构建传给 API 的工具列表:可按顶层分类过滤([`ToolCategory::Basic`] / [`ToolCategory::Development`])。
pub fn build_tools_filtered(allowed: Option<&[ToolCategory]>) -> Vec<Tool> {
    build_tools_with_options(ToolsBuildOptions {
        categories: allowed,
        dev_tags: None,
    })
}

/// 同时支持顶层分类与 Development 子域标签过滤(见 [`ToolsBuildOptions`])。
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)())
}

/// 内置工具 `name` 的 parameters JSON Schema(供工作流等静态校验复用)。
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;

/// 执行本地工具并返回结果字符串。
/// `ToolContext` 聚合 `run_command`、`get_weather`、`web_search` 等工具所需的配置项。
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),
    }
}

/// `run_command` 与 `cargo_*` / `rust_test_one` / `rust_rustc` 走显式 [`ToolError`];其余工具仍经 [`run_tool`] + [`crate::cm_tools::tool_result::parse_legacy_output`]。
#[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)
}

/// 与 [`run_tool`] 相同,但失败时返回 [`crate::cm_tools::tool_result::ToolError`](含 **分类 / 错误码 / retryable**)。
///
/// **`run_command`**、**`cargo_*` / `rust_test_one` / `rust_rustc`**、**`read_file`**、**`search_in_files`** 在 [`run_tool_dispatch`] 中经 `*_try` 返回显式 [`ToolError`];其余工具仍由 [`crate::cm_tools::tool_result::parse_legacy_output`] 从正文推断。
#[allow(dead_code, clippy::result_large_err)] // 供编排与单测显式 `Result` 分支;主路径现经 [`run_tool_dispatch`] + [`run_tool_result`]
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),
    }
}

/// 判断本次 run_command 是否为“成功的编译命令”(常见 C/C++ 构建工具且退出码为 0)
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)
}

/// 为前端生成简短的工具调用摘要,便于在 Chat 面板中展示
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)
        }
    }
}

/// 使用预解析的 `serde_json::Value` 生成摘要,避免重复 JSON 解析。
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),
    }
}

/// 仅 **`cargo test`**:重置少数进程级可变 `static`(`run_command` 秒级限流、**`test_result_cache`** LRU)。
/// 集成测试可在夹具开头调用 crate 根 **[`crate::cm_tools::reset_process_tool_globals_for_tests`]**。
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;