diff --git a/lib/components/fabro-agent/src/profiles/kimi.rs b/lib/components/fabro-agent/src/profiles/kimi.rs index a02d28ef4..ef6a86dbd 100644 --- a/lib/components/fabro-agent/src/profiles/kimi.rs +++ b/lib/components/fabro-agent/src/profiles/kimi.rs @@ -37,6 +37,76 @@ files that have not been read, and the call will fail. Prefer Edit for any incre change: Write replaces the entire file, so using it to make a small edit discards \ everything you did not restate."; +const BASH_DESCRIPTION: &str = "Execute a bash command. Use this for shell semantics — pipes, \ +env, processes, git, package managers, build and test runners. + +Translate these to a dedicated tool instead: +- `cat` / `head` / `tail` on a known path → Read +- `sed` / `awk` for an in-place edit → Edit +- `echo > file` / heredoc → Write +- `find` or recursive `ls` to locate files by name → Glob (plain `ls ` is fine) +- `grep` / `rg` to search file contents → Grep + +The dedicated tools cap their output, so they keep large raw dumps out of the conversation. That \ +is why they are worth reaching for whenever one fits. + +Output: stdout and stderr are combined and returned as a string, truncated if very long. + +Guidelines: +- Each call runs in a fresh bash process in the working directory. Environment variables and `cd` \ +do NOT persist between calls — use absolute paths, or `cd && ` within one call. +- Do not run interactive commands, or commands that never exit. +- A long-running command needs a raised `timeout_ms`, not a retry. The default is \ +{default_timeout_ms}ms and the maximum is {max_timeout_ms}ms. Retrying a command that timed out \ +once will simply time out again. +- Chain genuinely dependent steps with `&&`. Issue independent read-only commands as separate \ +parallel calls in one response rather than chaining them, so their output stays separate. +- Quote paths containing spaces. +- Avoid `..` to reach outside the working directory, and do not modify files outside it unless \ +explicitly asked. +- Never run commands requiring superuser privileges unless explicitly asked."; + +const READ_DESCRIPTION: &str = "Read a text file from the workspace. + +Reading a file is also what clears it for writing: Edit and Write refuse a file that has not been \ +read in this session. + +- If you have a concrete path, call Read directly. Do not Glob or `ls` first to check that it \ +exists — a missing path returns an error you can handle. +- When you need several files, emit multiple Read calls in one response rather than one per turn. +- Returns `\\t` per line. Drop the number and tab when you take text for an \ +Edit `old_string`. +- `limit` defaults to 2000 lines. Page a larger file with `offset` (1-based first line) and \ +`limit`. +- Use Bash or an MCP tool for binary formats; this tool reads text."; + +const GREP_DESCRIPTION: &str = "Search file contents with a regular expression. + +Use Grep when you are looking for unknown content or an unknown location. If you already know the \ +path, use Read instead. Prefer this over running `grep` or `rg` through Bash: it caps its output, \ +so it will not flood the conversation. + +- Backed by ripgrep when available and POSIX `grep` otherwise, so keep patterns portable across \ +both rather than relying on ripgrep-only syntax. +- `path` chooses the search root, `glob_filter` limits which files are searched, \ +`case_insensitive` folds case, and `max_results` caps the output."; + +const GLOB_DESCRIPTION: &str = "Find files by name using a glob pattern, most recently modified \ +first. + +Use this instead of `find` or recursive `ls` through Bash. Prefer patterns with a literal anchor \ +— an extension or a subdirectory — over bare wildcards. + +Good patterns: +- `*.rs` — an extension at any depth below the search root +- `src/*.rs` — directly inside `src/`, not recursive +- `src/**/*.rs` — recursive walk under a subdirectory +- `{src,tests}/**/*.rs` — brace expansion works + +Avoid recursing into dependency or build output (`node_modules/**`, `target/**`): those produce \ +thousands of matches and waste context. Narrow to a specific subpath instead. Results are files, \ +so to locate a directory, glob for something inside it."; + pub struct KimiProfile { base: BaseProfile, } @@ -61,6 +131,23 @@ impl KimiProfile { registry.register(make_edit_file_tool()); registry.redescribe(NativeTool::EditFile, EDIT_FILE_DESCRIPTION); registry.redescribe(NativeTool::WriteFile, WRITE_FILE_DESCRIPTION); + registry.redescribe(NativeTool::ReadFile, READ_DESCRIPTION); + registry.redescribe(NativeTool::Grep, GREP_DESCRIPTION); + registry.redescribe(NativeTool::Glob, GLOB_DESCRIPTION); + // The Bash description quotes the timeouts this profile actually + // enforces, so the two cannot drift. + registry.redescribe( + NativeTool::Shell, + BASH_DESCRIPTION + .replace( + "{default_timeout_ms}", + &options.default_command_timeout_ms.to_string(), + ) + .replace( + "{max_timeout_ms}", + &options.max_command_timeout_ms.to_string(), + ), + ); // Kimi Code drives todos with one replace-whole-list call. The // Anthropic task tools model the opposite interaction -- incremental @@ -283,6 +370,34 @@ mod tests { ); } assert!(describe("Edit").contains("never reconstruct it from memory")); + + // Bash steers shell usage toward the dedicated tools, under the names + // this profile actually exposes. + let bash = describe("Bash"); + for expected in ["→ Read", "→ Edit", "→ Write", "→ Glob", "→ Grep"] { + assert!(bash.contains(expected), "Bash should map {expected}"); + } + // Timeouts are interpolated from the options this profile enforces, so + // the description cannot drift from behavior. + let options = NativeToolOptions::for_profile(AgentProfileKind::Kimi); + assert!( + bash.contains(&options.default_command_timeout_ms.to_string()), + "Bash should quote the real default timeout" + ); + assert!( + !bash.contains("{default_timeout_ms}"), + "placeholder left unrendered" + ); + // Fabro has no background shell; promising one would be a lie. + assert!(!bash.contains("run_in_background"), "{bash}"); + assert!(!bash.to_lowercase().contains("background task"), "{bash}"); + + // Read explains that reading is what clears a file for writing. + assert!(describe("Read").contains("refuse a file that has not been read")); + // Grep must not promise ripgrep syntax: fabro falls back to POSIX grep. + let grep = describe("Grep"); + assert!(grep.contains("POSIX"), "{grep}"); + assert!(describe("Glob").contains("most recently modified")); // The shared description is untouched for other profiles. assert!(!describe("Read").contains("refuses writes")); }