feat(i18n): externalize tool descriptions for translation (#3912)

Add a locale-aware tool description system that loads translations from
TOML files in tool_descriptions/. This enables non-English users to see
tool descriptions in their language.

- Add src/i18n.rs module with ToolDescriptions loader, locale detection
  (ZEROCLAW_LOCALE, LANG, LC_ALL env vars), and English fallback chain
- Add locale config field to Config struct for explicit locale override
- Create tool_descriptions/en.toml with all 47 tool descriptions
- Create tool_descriptions/zh-CN.toml with Chinese translations
- Integrate with ToolsSection::build() and build_tool_instructions()
  to resolve descriptions from locale files before hardcoded fallback
- Add PromptContext.tool_descriptions field for prompt-time resolution
- Add AgentBuilder.tool_descriptions() setter for Agent construction
- Include tool_descriptions/ in Cargo.toml package include list
- Add 8 unit tests covering locale loading, fallback chains, env
  detection, and config override

Closes #3901
This commit is contained in:
Argenis 2026-03-18 17:01:39 -04:00 committed by Roman Tataurov
parent 3430f9bf1a
commit 81d99f513c
No known key found for this signature in database
GPG Key ID: 70A51EF3185C334B
12 changed files with 522 additions and 8 deletions

View File

@ -31,6 +31,7 @@ include = [
"/LICENSE*",
"/README.md",
"/web/dist/**/*",
"/tool_descriptions/**/*",
]
[dependencies]

View File

@ -4,6 +4,7 @@ use crate::agent::dispatcher::{
use crate::agent::memory_loader::{DefaultMemoryLoader, MemoryLoader};
use crate::agent::prompt::{PromptContext, SystemPromptBuilder};
use crate::config::Config;
use crate::i18n::ToolDescriptions;
use crate::memory::{self, Memory, MemoryCategory};
use crate::observability::{self, Observer, ObserverEvent};
use crate::providers::{self, ChatMessage, ChatRequest, ConversationMessage, Provider};
@ -40,6 +41,7 @@ pub struct Agent {
route_model_by_hint: HashMap<String, String>,
allowed_tools: Option<Vec<String>>,
response_cache: Option<Arc<crate::memory::response_cache::ResponseCache>>,
tool_descriptions: Option<ToolDescriptions>,
}
pub struct AgentBuilder {
@ -64,6 +66,7 @@ pub struct AgentBuilder {
route_model_by_hint: Option<HashMap<String, String>>,
allowed_tools: Option<Vec<String>>,
response_cache: Option<Arc<crate::memory::response_cache::ResponseCache>>,
tool_descriptions: Option<ToolDescriptions>,
}
impl AgentBuilder {
@ -90,6 +93,7 @@ impl AgentBuilder {
route_model_by_hint: None,
allowed_tools: None,
response_cache: None,
tool_descriptions: None,
}
}
@ -207,6 +211,11 @@ impl AgentBuilder {
self
}
pub fn tool_descriptions(mut self, tool_descriptions: Option<ToolDescriptions>) -> Self {
self.tool_descriptions = tool_descriptions;
self
}
pub fn build(self) -> Result<Agent> {
let mut tools = self
.tools
@ -257,6 +266,7 @@ impl AgentBuilder {
route_model_by_hint: self.route_model_by_hint.unwrap_or_default(),
allowed_tools: allowed,
response_cache: self.response_cache,
tool_descriptions: self.tool_descriptions,
})
}
}
@ -456,6 +466,7 @@ impl Agent {
skills_prompt_mode: self.skills_prompt_mode,
identity_config: Some(&self.identity_config),
dispatcher_instructions: &instructions,
tool_descriptions: self.tool_descriptions.as_ref(),
};
self.prompt_builder.build(&ctx)
}

View File

@ -1,5 +1,6 @@
use crate::approval::{ApprovalManager, ApprovalRequest, ApprovalResponse};
use crate::config::Config;
use crate::i18n::ToolDescriptions;
use crate::memory::{self, Memory, MemoryCategory};
use crate::multimodal;
use crate::observability::{self, runtime_trace, Observer, ObserverEvent};
@ -3078,7 +3079,10 @@ pub(crate) async fn run_tool_call_loop(
/// Build the tool instruction block for the system prompt so the LLM knows
/// how to invoke tools.
pub(crate) fn build_tool_instructions(tools_registry: &[Box<dyn Tool>]) -> String {
pub(crate) fn build_tool_instructions(
tools_registry: &[Box<dyn Tool>],
tool_descriptions: Option<&ToolDescriptions>,
) -> String {
let mut instructions = String::new();
instructions.push_str("\n## Tool Use Protocol\n\n");
instructions.push_str("To use a tool, wrap a JSON object in <tool_call></tool_call> tags:\n\n");
@ -3094,11 +3098,14 @@ pub(crate) fn build_tool_instructions(tools_registry: &[Box<dyn Tool>]) -> Strin
instructions.push_str("### Available Tools\n\n");
for tool in tools_registry {
let desc = tool_descriptions
.and_then(|td| td.get(tool.name()))
.unwrap_or_else(|| tool.description());
let _ = writeln!(
instructions,
"**{}**: {}\nParameters: `{}`\n",
tool.name(),
tool.description(),
desc,
tool.parameters_schema()
);
}
@ -3324,6 +3331,16 @@ pub async fn run(
.map(|b| b.board.clone())
.collect();
// ── Load locale-aware tool descriptions ────────────────────────
let i18n_locale = config
.locale
.as_deref()
.filter(|s| !s.is_empty())
.map(ToString::to_string)
.unwrap_or_else(crate::i18n::detect_locale);
let i18n_search_dirs = crate::i18n::default_search_dirs(&config.workspace_dir);
let i18n_descs = crate::i18n::ToolDescriptions::load(&i18n_locale, &i18n_search_dirs);
// ── Build system prompt from workspace MD files (OpenClaw framework) ──
let skills = crate::skills::load_skills_with_config(&config.workspace_dir, &config);
let mut tool_descs: Vec<(&str, &str)> = vec![
@ -3453,7 +3470,7 @@ pub async fn run(
// Append structured tool-use instructions with schemas (only for non-native providers)
if !native_tools {
system_prompt.push_str(&build_tool_instructions(&tools_registry));
system_prompt.push_str(&build_tool_instructions(&tools_registry, Some(&i18n_descs)));
}
// Append deferred MCP tool names so the LLM knows what is available
@ -3989,6 +4006,16 @@ pub async fn process_message(
.map(|b| b.board.clone())
.collect();
// ── Load locale-aware tool descriptions ────────────────────────
let i18n_locale = config
.locale
.as_deref()
.filter(|s| !s.is_empty())
.map(ToString::to_string)
.unwrap_or_else(crate::i18n::detect_locale);
let i18n_search_dirs = crate::i18n::default_search_dirs(&config.workspace_dir);
let i18n_descs = crate::i18n::ToolDescriptions::load(&i18n_locale, &i18n_search_dirs);
let skills = crate::skills::load_skills_with_config(&config.workspace_dir, &config);
let mut tool_descs: Vec<(&str, &str)> = vec![
("shell", "Execute terminal commands."),
@ -4054,7 +4081,7 @@ pub async fn process_message(
config.skills.prompt_injection_mode,
);
if !native_tools {
system_prompt.push_str(&build_tool_instructions(&tools_registry));
system_prompt.push_str(&build_tool_instructions(&tools_registry, Some(&i18n_descs)));
}
if !deferred_section.is_empty() {
system_prompt.push('\n');
@ -5764,7 +5791,7 @@ Tail"#;
std::path::Path::new("/tmp"),
));
let tools = tools::default_tools(security);
let instructions = build_tool_instructions(&tools);
let instructions = build_tool_instructions(&tools, None);
assert!(instructions.contains("## Tool Use Protocol"));
assert!(instructions.contains("<tool_call>"));

View File

@ -1,4 +1,5 @@
use crate::config::IdentityConfig;
use crate::i18n::ToolDescriptions;
use crate::identity;
use crate::skills::Skill;
use crate::tools::Tool;
@ -17,6 +18,9 @@ pub struct PromptContext<'a> {
pub skills_prompt_mode: crate::config::SkillsPromptInjectionMode,
pub identity_config: Option<&'a IdentityConfig>,
pub dispatcher_instructions: &'a str,
/// Locale-aware tool descriptions. When present, tool descriptions in
/// prompts are resolved from the locale file instead of hardcoded values.
pub tool_descriptions: Option<&'a ToolDescriptions>,
}
pub trait PromptSection: Send + Sync {
@ -124,11 +128,15 @@ impl PromptSection for ToolsSection {
fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
let mut out = String::from("## Tools\n\n");
for tool in ctx.tools {
let desc = ctx
.tool_descriptions
.and_then(|td: &ToolDescriptions| td.get(tool.name()))
.unwrap_or_else(|| tool.description());
let _ = writeln!(
out,
"- **{}**: {}\n Parameters: `{}`",
tool.name(),
tool.description(),
desc,
tool.parameters_schema()
);
}
@ -317,6 +325,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
identity_config: Some(&identity_config),
dispatcher_instructions: "",
tool_descriptions: None,
};
let section = IdentitySection;
@ -345,6 +354,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
identity_config: None,
dispatcher_instructions: "instr",
tool_descriptions: None,
};
let prompt = SystemPromptBuilder::with_defaults().build(&ctx).unwrap();
assert!(prompt.contains("## Tools"));
@ -380,6 +390,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
identity_config: None,
dispatcher_instructions: "",
tool_descriptions: None,
};
let output = SkillsSection.build(&ctx).unwrap();
@ -418,6 +429,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Compact,
identity_config: None,
dispatcher_instructions: "",
tool_descriptions: None,
};
let output = SkillsSection.build(&ctx).unwrap();
@ -439,6 +451,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
identity_config: None,
dispatcher_instructions: "instr",
tool_descriptions: None,
};
let rendered = DateTimeSection.build(&ctx).unwrap();
@ -477,6 +490,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
identity_config: None,
dispatcher_instructions: "",
tool_descriptions: None,
};
let prompt = SystemPromptBuilder::with_defaults().build(&ctx).unwrap();

View File

@ -3939,6 +3939,16 @@ pub async fn start_channels(config: Config) -> Result<()> {
let skills = crate::skills::load_skills_with_config(&workspace, &config);
// ── Load locale-aware tool descriptions ────────────────────────
let i18n_locale = config
.locale
.as_deref()
.filter(|s| !s.is_empty())
.map(ToString::to_string)
.unwrap_or_else(crate::i18n::detect_locale);
let i18n_search_dirs = crate::i18n::default_search_dirs(&workspace);
let i18n_descs = crate::i18n::ToolDescriptions::load(&i18n_locale, &i18n_search_dirs);
// Collect tool descriptions for the prompt
let mut tool_descs: Vec<(&str, &str)> = vec![
(
@ -4018,7 +4028,10 @@ pub async fn start_channels(config: Config) -> Result<()> {
config.skills.prompt_injection_mode,
);
if !native_tools {
system_prompt.push_str(&build_tool_instructions(tools_registry.as_ref()));
system_prompt.push_str(&build_tool_instructions(
tools_registry.as_ref(),
Some(&i18n_descs),
));
}
// Append deferred MCP tool names so the LLM knows what is available
@ -6768,7 +6781,7 @@ BTC is currently around $65,000 based on latest tool output."#
"build_system_prompt should not emit protocol block directly"
);
prompt.push_str(&build_tool_instructions(&[]));
prompt.push_str(&build_tool_instructions(&[], None));
assert_eq!(
prompt.matches("## Tool Use Protocol").count(),

View File

@ -339,6 +339,17 @@ pub struct Config {
/// Plugin system configuration (`[plugins]`).
#[serde(default)]
pub plugins: PluginsConfig,
/// Locale for tool descriptions (e.g. `"en"`, `"zh-CN"`).
///
/// When set, tool descriptions shown in system prompts are loaded from
/// `tool_descriptions/<locale>.toml`. Falls back to English, then to
/// hardcoded descriptions.
///
/// If omitted or empty, the locale is auto-detected from `ZEROCLAW_LOCALE`,
/// `LANG`, or `LC_ALL` environment variables (defaulting to `"en"`).
#[serde(default)]
pub locale: Option<String>,
}
/// Multi-client workspace isolation configuration.
@ -5996,6 +6007,7 @@ impl Default for Config {
knowledge: KnowledgeConfig::default(),
linkedin: LinkedInConfig::default(),
plugins: PluginsConfig::default(),
locale: None,
}
}
}
@ -8433,6 +8445,7 @@ default_temperature = 0.7
knowledge: KnowledgeConfig::default(),
linkedin: LinkedInConfig::default(),
plugins: PluginsConfig::default(),
locale: None,
};
let toml_str = toml::to_string_pretty(&config).unwrap();
@ -8766,6 +8779,7 @@ tool_dispatcher = "xml"
knowledge: KnowledgeConfig::default(),
linkedin: LinkedInConfig::default(),
plugins: PluginsConfig::default(),
locale: None,
};
config.save().await.unwrap();

311
src/i18n.rs Normal file
View File

@ -0,0 +1,311 @@
//! Internationalization support for tool descriptions.
//!
//! Loads tool descriptions from TOML locale files in `tool_descriptions/`.
//! Falls back to English when a locale file or specific key is missing,
//! and ultimately falls back to the hardcoded `tool.description()` value
//! if no file-based description exists.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::debug;
/// Container for locale-specific tool descriptions loaded from TOML files.
#[derive(Debug, Clone)]
pub struct ToolDescriptions {
/// Descriptions from the requested locale (may be empty if file missing).
locale_descriptions: HashMap<String, String>,
/// English fallback descriptions (always loaded when locale != "en").
english_fallback: HashMap<String, String>,
/// The resolved locale tag (e.g. "en", "zh-CN").
locale: String,
}
/// TOML structure: `[tools]` table mapping tool name -> description string.
#[derive(Debug, serde::Deserialize)]
struct DescriptionFile {
#[serde(default)]
tools: HashMap<String, String>,
}
impl ToolDescriptions {
/// Load descriptions for the given locale.
///
/// `search_dirs` lists directories to probe for `tool_descriptions/<locale>.toml`.
/// The first directory containing a matching file wins.
///
/// Resolution:
/// 1. Look up tool name in the locale file.
/// 2. If missing (or locale file absent), look up in `en.toml`.
/// 3. If still missing, callers fall back to `tool.description()`.
pub fn load(locale: &str, search_dirs: &[PathBuf]) -> Self {
let locale_descriptions = load_locale_file(locale, search_dirs);
let english_fallback = if locale == "en" {
HashMap::new()
} else {
load_locale_file("en", search_dirs)
};
debug!(
locale = locale,
locale_keys = locale_descriptions.len(),
english_keys = english_fallback.len(),
"tool descriptions loaded"
);
Self {
locale_descriptions,
english_fallback,
locale: locale.to_string(),
}
}
/// Get the description for a tool by name.
///
/// Returns `Some(description)` if found in the locale file or English fallback.
/// Returns `None` if neither file contains the key (caller should use hardcoded).
pub fn get(&self, tool_name: &str) -> Option<&str> {
self.locale_descriptions
.get(tool_name)
.or_else(|| self.english_fallback.get(tool_name))
.map(String::as_str)
}
/// The resolved locale tag.
pub fn locale(&self) -> &str {
&self.locale
}
/// Create an empty instance that always returns `None` (hardcoded fallback).
pub fn empty() -> Self {
Self {
locale_descriptions: HashMap::new(),
english_fallback: HashMap::new(),
locale: "en".to_string(),
}
}
}
/// Detect the user's preferred locale from environment variables.
///
/// Checks `ZEROCLAW_LOCALE`, then `LANG`, then `LC_ALL`.
/// Returns "en" if none are set or parseable.
pub fn detect_locale() -> String {
if let Ok(val) = std::env::var("ZEROCLAW_LOCALE") {
let val = val.trim().to_string();
if !val.is_empty() {
return normalize_locale(&val);
}
}
for var in &["LANG", "LC_ALL"] {
if let Ok(val) = std::env::var(var) {
let locale = normalize_locale(&val);
if locale != "C" && locale != "POSIX" && !locale.is_empty() {
return locale;
}
}
}
"en".to_string()
}
/// Normalize a raw locale string (e.g. "zh_CN.UTF-8") to a tag we use
/// for file lookup (e.g. "zh-CN").
fn normalize_locale(raw: &str) -> String {
// Strip encoding suffix (.UTF-8, .utf8, etc.)
let base = raw.split('.').next().unwrap_or(raw);
// Replace underscores with hyphens for BCP-47-ish consistency
base.replace('_', "-")
}
/// Build the default set of search directories for locale files.
///
/// 1. The workspace directory itself (for project-local overrides).
/// 2. The binary's parent directory (for installed distributions).
/// 3. The compile-time `CARGO_MANIFEST_DIR` as a final fallback during dev.
pub fn default_search_dirs(workspace_dir: &Path) -> Vec<PathBuf> {
let mut dirs = vec![workspace_dir.to_path_buf()];
if let Ok(exe) = std::env::current_exe() {
if let Some(parent) = exe.parent() {
dirs.push(parent.to_path_buf());
}
}
// During development, also check the project root (where Cargo.toml lives).
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
if !dirs.contains(&manifest_dir) {
dirs.push(manifest_dir);
}
dirs
}
/// Try to load and parse a locale TOML file from the first matching search dir.
fn load_locale_file(locale: &str, search_dirs: &[PathBuf]) -> HashMap<String, String> {
let filename = format!("tool_descriptions/{locale}.toml");
for dir in search_dirs {
let path = dir.join(&filename);
match std::fs::read_to_string(&path) {
Ok(contents) => match toml::from_str::<DescriptionFile>(&contents) {
Ok(parsed) => {
debug!(path = %path.display(), keys = parsed.tools.len(), "loaded locale file");
return parsed.tools;
}
Err(e) => {
debug!(path = %path.display(), error = %e, "failed to parse locale file");
}
},
Err(_) => {
// File not found in this directory, try next.
}
}
}
debug!(
locale = locale,
"no locale file found in any search directory"
);
HashMap::new()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
/// Helper: create a temp dir with a `tool_descriptions/<locale>.toml` file.
fn write_locale_file(dir: &Path, locale: &str, content: &str) {
let td = dir.join("tool_descriptions");
fs::create_dir_all(&td).unwrap();
fs::write(td.join(format!("{locale}.toml")), content).unwrap();
}
#[test]
fn load_english_descriptions() {
let tmp = tempfile::tempdir().unwrap();
write_locale_file(
tmp.path(),
"en",
r#"[tools]
shell = "Execute a shell command"
file_read = "Read file contents"
"#,
);
let descs = ToolDescriptions::load("en", &[tmp.path().to_path_buf()]);
assert_eq!(descs.get("shell"), Some("Execute a shell command"));
assert_eq!(descs.get("file_read"), Some("Read file contents"));
assert_eq!(descs.get("nonexistent"), None);
assert_eq!(descs.locale(), "en");
}
#[test]
fn fallback_to_english_when_locale_key_missing() {
let tmp = tempfile::tempdir().unwrap();
write_locale_file(
tmp.path(),
"en",
r#"[tools]
shell = "Execute a shell command"
file_read = "Read file contents"
"#,
);
write_locale_file(
tmp.path(),
"zh-CN",
r#"[tools]
shell = "在工作区目录中执行 shell 命令"
"#,
);
let descs = ToolDescriptions::load("zh-CN", &[tmp.path().to_path_buf()]);
// Translated key returns Chinese.
assert_eq!(descs.get("shell"), Some("在工作区目录中执行 shell 命令"));
// Missing key falls back to English.
assert_eq!(descs.get("file_read"), Some("Read file contents"));
assert_eq!(descs.locale(), "zh-CN");
}
#[test]
fn fallback_when_locale_file_missing() {
let tmp = tempfile::tempdir().unwrap();
write_locale_file(
tmp.path(),
"en",
r#"[tools]
shell = "Execute a shell command"
"#,
);
// Request a locale that has no file.
let descs = ToolDescriptions::load("fr", &[tmp.path().to_path_buf()]);
// Falls back to English.
assert_eq!(descs.get("shell"), Some("Execute a shell command"));
assert_eq!(descs.locale(), "fr");
}
#[test]
fn fallback_when_no_files_exist() {
let tmp = tempfile::tempdir().unwrap();
let descs = ToolDescriptions::load("en", &[tmp.path().to_path_buf()]);
assert_eq!(descs.get("shell"), None);
}
#[test]
fn empty_always_returns_none() {
let descs = ToolDescriptions::empty();
assert_eq!(descs.get("shell"), None);
assert_eq!(descs.locale(), "en");
}
#[test]
fn detect_locale_from_env() {
// Save and restore env.
let saved = std::env::var("ZEROCLAW_LOCALE").ok();
let saved_lang = std::env::var("LANG").ok();
std::env::set_var("ZEROCLAW_LOCALE", "ja-JP");
assert_eq!(detect_locale(), "ja-JP");
std::env::remove_var("ZEROCLAW_LOCALE");
std::env::set_var("LANG", "zh_CN.UTF-8");
assert_eq!(detect_locale(), "zh-CN");
// Restore.
match saved {
Some(v) => std::env::set_var("ZEROCLAW_LOCALE", v),
None => std::env::remove_var("ZEROCLAW_LOCALE"),
}
match saved_lang {
Some(v) => std::env::set_var("LANG", v),
None => std::env::remove_var("LANG"),
}
}
#[test]
fn normalize_locale_strips_encoding() {
assert_eq!(normalize_locale("en_US.UTF-8"), "en-US");
assert_eq!(normalize_locale("zh_CN.utf8"), "zh-CN");
assert_eq!(normalize_locale("fr"), "fr");
assert_eq!(normalize_locale("pt_BR"), "pt-BR");
}
#[test]
fn config_locale_overrides_env() {
// This tests the precedence logic: if config provides a locale,
// it should be used instead of detect_locale().
// The actual override happens at the call site in prompt.rs / loop_.rs,
// so here we just verify ToolDescriptions works with an explicit locale.
let tmp = tempfile::tempdir().unwrap();
write_locale_file(
tmp.path(),
"de",
r#"[tools]
shell = "Einen Shell-Befehl im Arbeitsverzeichnis ausführen"
"#,
);
let descs = ToolDescriptions::load("de", &[tmp.path().to_path_buf()]);
assert_eq!(
descs.get("shell"),
Some("Einen Shell-Befehl im Arbeitsverzeichnis ausführen")
);
}
}

View File

@ -54,6 +54,7 @@ pub(crate) mod hardware;
pub(crate) mod health;
pub(crate) mod heartbeat;
pub mod hooks;
pub mod i18n;
pub(crate) mod identity;
pub(crate) mod integrations;
pub mod memory;

View File

@ -89,6 +89,7 @@ mod hardware;
mod health;
mod heartbeat;
mod hooks;
mod i18n;
mod identity;
mod integrations;
mod memory;

View File

@ -193,6 +193,7 @@ pub async fn run_wizard(force: bool) -> Result<Config> {
knowledge: crate::config::KnowledgeConfig::default(),
linkedin: crate::config::LinkedInConfig::default(),
plugins: crate::config::PluginsConfig::default(),
locale: None,
};
println!(
@ -567,6 +568,7 @@ async fn run_quick_setup_with_home(
knowledge: crate::config::KnowledgeConfig::default(),
linkedin: crate::config::LinkedInConfig::default(),
plugins: crate::config::PluginsConfig::default(),
locale: None,
};
config.save().await?;

59
tool_descriptions/en.toml Normal file
View File

@ -0,0 +1,59 @@
# English tool descriptions (default locale)
#
# Each key under [tools] matches the tool's name() return value.
# Values are the human-readable descriptions shown in system prompts.
[tools]
backup = "Create, list, verify, and restore workspace backups"
browser = "Web/browser automation with pluggable backends (agent-browser, rust-native, computer_use). Supports DOM actions plus optional OS-level actions (mouse_move, mouse_click, mouse_drag, key_type, key_press, screen_capture) through a computer-use sidecar. Use 'snapshot' to map interactive elements to refs (@e1, @e2). Enforces browser.allowed_domains for open actions."
browser_delegate = "Delegate browser-based tasks to a browser-capable CLI for interacting with web applications like Teams, Outlook, Jira, Confluence"
browser_open = "Open an approved HTTPS URL in the system browser. Security constraints: allowlist-only domains, no local/private hosts, no scraping."
cloud_ops = "Cloud transformation advisory tool. Analyzes IaC plans, assesses migration paths, reviews costs, and checks architecture against Well-Architected Framework pillars. Read-only: does not create or modify cloud resources."
cloud_patterns = "Cloud pattern library. Given a workload description, suggests applicable cloud-native architectural patterns (containerization, serverless, database modernization, etc.)."
composio = "Execute actions on 1000+ apps via Composio (Gmail, Notion, GitHub, Slack, etc.). Use action='list' to see available actions (includes parameter names). action='execute' with action_name/tool_slug and params to run an action. If you are unsure of the exact params, pass 'text' instead with a natural-language description of what you want (Composio will resolve the correct parameters via NLP). action='list_accounts' or action='connected_accounts' to list OAuth-connected accounts. action='connect' with app/auth_config_id to get OAuth URL. connected_account_id is auto-resolved when omitted."
content_search = "Search file contents by regex pattern within the workspace. Supports ripgrep (rg) with grep fallback. Output modes: 'content' (matching lines with context), 'files_with_matches' (file paths only), 'count' (match counts per file). Example: pattern='fn main', include='*.rs', output_mode='content'."
cron_add = """Create a scheduled cron job (shell or agent) with cron/at/every schedules. Use job_type='agent' with a prompt to run the AI agent on schedule. To deliver output to a channel (Discord, Telegram, Slack, Mattermost, Matrix), set delivery={"mode":"announce","channel":"discord","to":"<channel_id_or_chat_id>"}. This is the preferred tool for sending scheduled/delayed messages to users via channels."""
cron_list = "List all scheduled cron jobs"
cron_remove = "Remove a cron job by id"
cron_run = "Force-run a cron job immediately and record run history"
cron_runs = "List recent run history for a cron job"
cron_update = "Patch an existing cron job (schedule, command, prompt, enabled, delivery, model, etc.)"
data_management = "Workspace data retention, purge, and storage statistics"
delegate = "Delegate a subtask to a specialized agent. Use when: a task benefits from a different model (e.g. fast summarization, deep reasoning, code generation). The sub-agent runs a single prompt by default; with agentic=true it can iterate with a filtered tool-call loop."
file_edit = "Edit a file by replacing an exact string match with new content"
file_read = "Read file contents with line numbers. Supports partial reading via offset and limit. Extracts text from PDF; other binary files are read with lossy UTF-8 conversion."
file_write = "Write contents to a file in the workspace"
git_operations = "Perform structured Git operations (status, diff, log, branch, commit, add, checkout, stash). Provides parsed JSON output and integrates with security policy for autonomy controls."
glob_search = "Search for files matching a glob pattern within the workspace. Returns a sorted list of matching file paths relative to the workspace root. Examples: '**/*.rs' (all Rust files), 'src/**/mod.rs' (all mod.rs in src)."
google_workspace = "Interact with Google Workspace services (Drive, Gmail, Calendar, Sheets, Docs, etc.) via the gws CLI. Requires gws to be installed and authenticated."
hardware_board_info = "Return full board info (chip, architecture, memory map) for connected hardware. Use when: user asks for 'board info', 'what board do I have', 'connected hardware', 'chip info', 'what hardware', or 'memory map'."
hardware_memory_map = "Return the memory map (flash and RAM address ranges) for connected hardware. Use when: user asks for 'upper and lower memory addresses', 'memory map', 'address space', or 'readable addresses'. Returns flash/RAM ranges from datasheets."
hardware_memory_read = "Read actual memory/register values from Nucleo via USB. Use when: user asks to 'read register values', 'read memory at address', 'dump memory', 'lower memory 0-126', or 'give address and value'. Returns hex dump. Requires Nucleo connected via USB and probe feature. Params: address (hex, e.g. 0x20000000 for RAM start), length (bytes, default 128)."
http_request = "Make HTTP requests to external APIs. Supports GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS methods. Security constraints: allowlist-only domains, no local/private hosts, configurable timeout and response size limits."
image_info = "Read image file metadata (format, dimensions, size) and optionally return base64-encoded data."
knowledge = "Manage a knowledge graph of architecture decisions, solution patterns, lessons learned, and experts. Actions: capture, search, relate, suggest, expert_find, lessons_extract, graph_stats."
linkedin = "Manage LinkedIn: create posts, list your posts, comment, react, delete posts, view engagement, get profile info, and read the configured content strategy. Requires LINKEDIN_* credentials in .env file."
memory_forget = "Remove a memory by key. Use to delete outdated facts or sensitive data. Returns whether the memory was found and removed."
memory_recall = "Search long-term memory for relevant facts, preferences, or context. Returns scored results ranked by relevance."
memory_store = "Store a fact, preference, or note in long-term memory. Use category 'core' for permanent facts, 'daily' for session notes, 'conversation' for chat context, or a custom category name."
microsoft365 = "Microsoft 365 integration: manage Outlook mail, Teams messages, Calendar events, OneDrive files, and SharePoint search via Microsoft Graph API"
model_routing_config = "Manage default model settings, scenario-based provider/model routes, classification rules, and delegate sub-agent profiles"
notion = "Interact with Notion: query databases, read/create/update pages, and search the workspace."
pdf_read = "Extract plain text from a PDF file in the workspace. Returns all readable text. Image-only or encrypted PDFs return an empty result. Requires the 'rag-pdf' build feature."
project_intel = "Project delivery intelligence: generate status reports, detect risks, draft client updates, summarize sprints, and estimate effort. Read-only analysis tool."
proxy_config = "Manage ZeroClaw proxy settings (scope: environment | zeroclaw | services), including runtime and process env application"
pushover = "Send a Pushover notification to your device. Requires PUSHOVER_TOKEN and PUSHOVER_USER_KEY in .env file."
schedule = """Manage scheduled shell-only tasks. Actions: create/add/once/list/get/cancel/remove/pause/resume. WARNING: This tool creates shell jobs whose output is only logged, NOT delivered to any channel. To send a scheduled message to Discord/Telegram/Slack/Matrix, use the cron_add tool with job_type='agent' and a delivery config like {"mode":"announce","channel":"discord","to":"<channel_id>"}."""
screenshot = "Capture a screenshot of the current screen. Returns the file path and base64-encoded PNG data."
security_ops = "Security operations tool for managed cybersecurity services. Actions: triage_alert (classify/prioritize alerts), run_playbook (execute incident response steps), parse_vulnerability (parse scan results), generate_report (create security posture reports), list_playbooks (list available playbooks), alert_stats (summarize alert metrics)."
shell = "Execute a shell command in the workspace directory"
sop_advance = "Report the result of the current SOP step and advance to the next step. Provide the run_id, whether the step succeeded or failed, and a brief output summary."
sop_approve = "Approve a pending SOP step that is waiting for operator approval. Returns the step instruction to execute. Use sop_status to see which runs are waiting."
sop_execute = "Manually trigger a Standard Operating Procedure (SOP) by name. Returns the run ID and first step instruction. Use sop_list to see available SOPs."
sop_list = "List all loaded Standard Operating Procedures (SOPs) with their triggers, priority, step count, and active run count. Optionally filter by name or priority."
sop_status = "Query SOP execution status. Provide run_id for a specific run, or sop_name to list runs for that SOP. With no arguments, shows all active runs."
swarm = "Orchestrate a swarm of agents to collaboratively handle a task. Supports sequential (pipeline), parallel (fan-out/fan-in), and router (LLM-selected) strategies."
tool_search = """Fetch full schema definitions for deferred MCP tools so they can be called. Use "select:name1,name2" for exact match or keywords to search."""
web_fetch = "Fetch a web page and return its content as clean plain text. HTML pages are automatically converted to readable text. JSON and plain text responses are returned as-is. Only GET requests; follows redirects. Security: allowlist-only domains, no local/private hosts."
web_search_tool = "Search the web for information. Returns relevant search results with titles, URLs, and descriptions. Use this to find current information, news, or research topics."
workspace = "Manage multi-client workspaces. Subcommands: list, switch, create, info, export. Each workspace provides isolated memory, audit, secrets, and tool restrictions."

View File

@ -0,0 +1,60 @@
# 中文工具描述 (简体中文)
#
# [tools] 下的每个键对应工具的 name() 返回值。
# 值是显示在系统提示中的人类可读描述。
# 缺少的键将回退到英文 (en.toml) 描述。
[tools]
backup = "创建、列出、验证和恢复工作区备份"
browser = "基于可插拔后端agent-browser、rust-native、computer_use的网页/浏览器自动化。支持 DOM 操作以及通过 computer-use 辅助工具进行的可选系统级操作mouse_move、mouse_click、mouse_drag、key_type、key_press、screen_capture。使用 'snapshot' 将交互元素映射到引用(@e1、@e2。对 open 操作强制执行 browser.allowed_domains。"
browser_delegate = "将基于浏览器的任务委派给具有浏览器功能的 CLI用于与 Teams、Outlook、Jira、Confluence 等 Web 应用交互"
browser_open = "在系统浏览器中打开经批准的 HTTPS URL。安全约束仅允许列表域名禁止本地/私有主机,禁止抓取。"
cloud_ops = "云转型咨询工具。分析 IaC 计划、评估迁移路径、审查成本,并根据良好架构框架支柱检查架构。只读:不创建或修改云资源。"
cloud_patterns = "云模式库。根据工作负载描述,建议适用的云原生架构模式(容器化、无服务器、数据库现代化等)。"
composio = "通过 Composio 在 1000 多个应用上执行操作Gmail、Notion、GitHub、Slack 等)。使用 action='list' 查看可用操作(包含参数名称)。使用 action='execute' 配合 action_name/tool_slug 和 params 运行操作。如果不确定具体参数,可传入 'text' 并用自然语言描述需求Composio 将通过 NLP 解析正确参数)。使用 action='list_accounts' 或 action='connected_accounts' 列出 OAuth 已连接账户。使用 action='connect' 配合 app/auth_config_id 获取 OAuth URL。省略时自动解析 connected_account_id。"
content_search = "在工作区内按正则表达式搜索文件内容。支持 ripgrep (rg),可回退到 grep。输出模式'content'(带上下文的匹配行)、'files_with_matches'(仅文件路径)、'count'(每个文件的匹配计数)。"
cron_add = "创建带有 cron/at/every 计划的定时任务shell 或 agent。使用 job_type='agent' 配合 prompt 按计划运行 AI 代理。要将输出发送到频道Discord、Telegram、Slack、Mattermost、Matrix请设置 delivery 配置。这是通过频道向用户发送定时/延迟消息的首选工具。"
cron_list = "列出所有已计划的 cron 任务"
cron_remove = "按 ID 删除 cron 任务"
cron_run = "立即强制运行 cron 任务并记录运行历史"
cron_runs = "列出 cron 任务的最近运行历史"
cron_update = "修改现有 cron 任务(计划、命令、提示、启用状态、投递配置、模型等)"
data_management = "工作区数据保留、清理和存储统计"
delegate = "将子任务委派给专用代理。适用场景:任务受益于不同模型(如快速摘要、深度推理、代码生成)。子代理默认运行单个提示;设置 agentic=true 后可通过过滤的工具调用循环进行迭代。"
file_edit = "通过替换精确匹配的字符串来编辑文件"
file_read = "读取带行号的文件内容。支持通过 offset 和 limit 进行部分读取。可从 PDF 提取文本;其他二进制文件使用有损 UTF-8 转换读取。"
file_write = "将内容写入工作区中的文件"
git_operations = "执行结构化的 Git 操作status、diff、log、branch、commit、add、checkout、stash。提供解析后的 JSON 输出,并与安全策略集成以实现自主控制。"
glob_search = "在工作区内搜索匹配 glob 模式的文件。返回相对于工作区根目录的排序文件路径列表。示例:'**/*.rs'(所有 Rust 文件)、'src/**/mod.rs'src 中所有 mod.rs。"
google_workspace = "与 Google Workspace 服务Drive、Gmail、Calendar、Sheets、Docs 等)交互。通过 gws CLI 操作,需要 gws 已安装并认证。"
hardware_board_info = "返回已连接硬件的完整板卡信息(芯片、架构、内存映射)。适用场景:用户询问板卡信息、连接的硬件、芯片信息等。"
hardware_memory_map = "返回已连接硬件的内存映射Flash 和 RAM 地址范围)。适用场景:用户询问内存地址、地址空间或可读地址。返回数据手册中的 Flash/RAM 范围。"
hardware_memory_read = "通过 USB 从 Nucleo 读取实际内存/寄存器值。适用场景:用户要求读取寄存器值、读取内存地址、转储内存等。返回十六进制转储。需要 Nucleo 通过 USB 连接并启用 probe 功能。"
http_request = "向外部 API 发送 HTTP 请求。支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 方法。安全约束:仅允许列表域名,禁止本地/私有主机,可配置超时和响应大小限制。"
image_info = "读取图片文件元数据(格式、尺寸、大小),可选返回 base64 编码数据。"
knowledge = "管理架构决策、解决方案模式、经验教训和专家的知识图谱。操作capture、search、relate、suggest、expert_find、lessons_extract、graph_stats。"
linkedin = "管理 LinkedIn创建帖子、列出帖子、评论、点赞、删除帖子、查看互动数据、获取个人资料信息以及阅读配置的内容策略。需要在 .env 文件中配置 LINKEDIN_* 凭据。"
memory_forget = "按键删除记忆。用于删除过时事实或敏感数据。返回记忆是否被找到并删除。"
memory_recall = "在长期记忆中搜索相关事实、偏好或上下文。返回按相关性排名的评分结果。"
memory_store = "在长期记忆中存储事实、偏好或笔记。使用类别 'core' 存储永久事实,'daily' 存储会话笔记,'conversation' 存储聊天上下文,或使用自定义类别名称。"
microsoft365 = "Microsoft 365 集成:通过 Microsoft Graph API 管理 Outlook 邮件、Teams 消息、日历事件、OneDrive 文件和 SharePoint 搜索"
model_routing_config = "管理默认模型设置、基于场景的提供商/模型路由、分类规则和委派子代理配置"
notion = "与 Notion 交互:查询数据库、读取/创建/更新页面、搜索工作区。"
pdf_read = "从工作区中的 PDF 文件提取纯文本。返回所有可读文本。仅图片或加密的 PDF 返回空结果。需要 'rag-pdf' 构建功能。"
project_intel = "项目交付智能:生成状态报告、检测风险、起草客户更新、总结冲刺、估算工作量。只读分析工具。"
proxy_config = "管理 ZeroClaw 代理设置范围environment | zeroclaw | services包括运行时和进程环境应用"
pushover = "向设备发送 Pushover 通知。需要在 .env 文件中配置 PUSHOVER_TOKEN 和 PUSHOVER_USER_KEY。"
schedule = "管理仅限 shell 的定时任务。操作create/add/once/list/get/cancel/remove/pause/resume。警告此工具创建的 shell 任务输出仅记录日志,不会发送到任何频道。要向 Discord/Telegram/Slack/Matrix 发送定时消息,请使用 cron_add 工具。"
screenshot = "捕获当前屏幕截图。返回文件路径和 base64 编码的 PNG 数据。"
security_ops = "托管网络安全服务的安全运营工具。操作triage_alert分类/优先级排序警报、run_playbook执行事件响应步骤、parse_vulnerability解析扫描结果、generate_report创建安全态势报告、list_playbooks列出可用剧本、alert_stats汇总警报指标。"
shell = "在工作区目录中执行 shell 命令"
sop_advance = "报告当前 SOP 步骤的结果并前进到下一步。提供 run_id、步骤是否成功或失败以及简短的输出摘要。"
sop_approve = "批准等待操作员批准的待处理 SOP 步骤。返回要执行的步骤指令。使用 sop_status 查看哪些运行正在等待。"
sop_execute = "按名称手动触发标准操作程序 (SOP)。返回运行 ID 和第一步指令。使用 sop_list 查看可用 SOP。"
sop_list = "列出所有已加载的标准操作程序 (SOP),包括触发器、优先级、步骤数和活跃运行数。可按名称或优先级筛选。"
sop_status = "查询 SOP 执行状态。提供 run_id 查看特定运行,或提供 sop_name 列出该 SOP 的所有运行。无参数时显示所有活跃运行。"
swarm = "编排代理群以协作处理任务。支持顺序(管道)、并行(扇出/扇入和路由器LLM 选择)策略。"
tool_search = "获取延迟 MCP 工具的完整 schema 定义以便调用。使用 \"select:name1,name2\" 精确匹配或关键词搜索。"
web_fetch = "获取网页并以纯文本形式返回内容。HTML 页面自动转换为可读文本。JSON 和纯文本响应按原样返回。仅 GET 请求;跟随重定向。安全:仅允许列表域名,禁止本地/私有主机。"
web_search_tool = "搜索网络获取信息。返回包含标题、URL 和描述的相关搜索结果。用于查找当前信息、新闻或研究主题。"
workspace = "管理多客户端工作区。子命令list、switch、create、info、export。每个工作区提供隔离的记忆、审计、密钥和工具限制。"