From 44ef697aac9a85f6812604f63c210f06b1264bc0 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Thu, 21 May 2026 14:28:39 +0800 Subject: [PATCH] docs(tools): clarify coding tool guidance --- nanobot/agent/tools/apply_patch.py | 11 ++++--- nanobot/agent/tools/exec_session.py | 14 ++++---- nanobot/agent/tools/filesystem.py | 21 ++++++++---- nanobot/agent/tools/search.py | 9 ++++-- nanobot/agent/tools/shell.py | 6 ++-- tests/tools/test_tool_descriptions.py | 46 +++++++++++++++++++++++++++ 6 files changed, 85 insertions(+), 22 deletions(-) create mode 100644 tests/tools/test_tool_descriptions.py diff --git a/nanobot/agent/tools/apply_patch.py b/nanobot/agent/tools/apply_patch.py index 57d60f9b8..c4dbf9f9f 100644 --- a/nanobot/agent/tools/apply_patch.py +++ b/nanobot/agent/tools/apply_patch.py @@ -298,11 +298,14 @@ class ApplyPatchTool(_FsTool): @property def description(self) -> str: return ( - "Apply a structured patch for code edits. The patch must include " + "Default tool for code edits. Apply a structured patch with " "*** Begin Patch and *** End Patch. Supports Add File, Update File, " - "Delete File, and Move to. Paths must be relative. Prefer this for " - "multi-file coding changes; use edit_file for small exact replacements. " - "Set dry_run=true to validate and preview the change without writing files." + "Delete File, and Move to across one or more files. Use this for " + "multi-file changes, structural edits, generated code, or any edit " + "where a reviewable patch is clearer than an exact replacement. " + "Paths must be relative. Set dry_run=true to validate and preview " + "the change summary without writing files. Use edit_file only for " + "small exact replacements copied from read_file." ) async def execute(self, patch: str, dry_run: bool = False, **kwargs: Any) -> str: diff --git a/nanobot/agent/tools/exec_session.py b/nanobot/agent/tools/exec_session.py index c9ca0a3d0..4dadb2d36 100644 --- a/nanobot/agent/tools/exec_session.py +++ b/nanobot/agent/tools/exec_session.py @@ -424,11 +424,12 @@ class WriteStdinTool(Tool): @property def description(self) -> str: return ( - "Write text to a running exec session and return recent output. " - "Use chars='' to poll without writing. Set close_stdin=true to send EOF, " - "or terminate=true to stop the session. Use wait_for to keep polling " - "until expected output appears. Sessions finish automatically when " - "their process exits." + "Interact with a running exec session created by exec with " + "yield_time_ms. Use chars='' to poll without writing, chars to send " + "stdin, close_stdin=true to send EOF, or terminate=true to stop the " + "process. Use wait_for with wait_timeout_ms for dev servers, test " + "watchers, and prompts where you need to wait for expected output. " + "Do not use this to start new commands; start them with exec." ) async def execute( @@ -561,7 +562,8 @@ class ListExecSessionsTool(Tool): return ( "List active long-running exec sessions, including session_id, cwd, " "elapsed time, idle time, remaining timeout, and command preview. " - "Use this to recover a session_id before polling with write_stdin." + "Use this to recover a session_id after context shifts before " + "polling, writing stdin, or terminating with write_stdin." ) @property diff --git a/nanobot/agent/tools/filesystem.py b/nanobot/agent/tools/filesystem.py index 728ff9317..fa63e5f66 100644 --- a/nanobot/agent/tools/filesystem.py +++ b/nanobot/agent/tools/filesystem.py @@ -158,6 +158,9 @@ class ReadFileTool(_FsTool): "Text output format: LINE_NUM|CONTENT. " "Images return visual content for analysis. " "Supports PDF, DOCX, XLSX, PPTX documents. " + "Use find_files/list_dir first when the path is uncertain. " + "Read the relevant range before editing so replacements or patches " + "are based on current content. " "Use offset and limit for large text files. " "Use force=true to re-read content even if unchanged. " "Reads exceeding ~128K chars are truncated." @@ -384,9 +387,10 @@ class WriteFileTool(_FsTool): @property def description(self) -> str: return ( - "Write content to a file. Overwrites if the file already exists; " - "creates parent directories as needed. " - "For partial edits, prefer edit_file instead." + "Create a new file or intentionally replace an entire file with " + "the provided content. Overwrites existing files and creates parent " + "directories as needed. For code changes or partial edits, prefer " + "apply_patch; use edit_file only for small exact replacements." ) async def execute(self, path: str | None = None, content: str | None = None, **kwargs: Any) -> str: @@ -711,10 +715,13 @@ class EditFileTool(_FsTool): @property def description(self) -> str: return ( - "Edit a file by replacing old_text with new_text. " - "Tolerates minor whitespace/indentation differences and curly/straight quote mismatches. " - "If old_text matches multiple times, you must provide more context " - "or set occurrence/line_hint/replace_all. Shows a diff of the closest match on failure." + "Perform a small, exact replacement in one file by replacing " + "old_text with new_text. Use this for narrow text substitutions " + "with old_text copied from read_file. For multi-file, structural, " + "or generated code edits, prefer apply_patch. If old_text matches " + "multiple times, provide more context or set occurrence, line_hint, " + "replace_all, and expected_replacements. Shows closest-match " + "diagnostics on failure." ) @staticmethod diff --git a/nanobot/agent/tools/search.py b/nanobot/agent/tools/search.py index 52c18f16b..0febb122c 100644 --- a/nanobot/agent/tools/search.py +++ b/nanobot/agent/tools/search.py @@ -130,8 +130,10 @@ class FindFilesTool(_SearchTool): def description(self) -> str: return ( "Find files by path fragment, glob, or file type. " - "Use this before read_file when you need to locate files. " - "Returns workspace-relative paths and skips common dependency/build directories." + "Use this before read_file when you need to locate files, and " + "prefer it over shell find/ls for ordinary workspace discovery. " + "Returns workspace-relative paths and skips common dependency/build " + "directories." ) @property @@ -289,7 +291,8 @@ class GrepTool(_SearchTool): return ( "Search file contents with a regex pattern. " "Default output_mode is files_with_matches (file paths only); " - "use content mode for matching lines with context. " + "use content mode for matching lines with context. Prefer this " + "over shell grep for ordinary workspace searches. " "Skips binary and files >2 MB. Supports glob/type filtering." ) diff --git a/nanobot/agent/tools/shell.py b/nanobot/agent/tools/shell.py index 47d3e9065..090dcc716 100644 --- a/nanobot/agent/tools/shell.py +++ b/nanobot/agent/tools/shell.py @@ -211,8 +211,10 @@ class ExecTool(Tool): def description(self) -> str: return ( "Execute a shell command and return its output. " - "Prefer read_file/write_file/edit_file over cat/echo/sed, " - "and grep/glob over shell find/grep. " + "Use this for tests, builds, package commands, git commands, and " + "other process execution. Prefer read_file/find_files/grep for " + "inspection and apply_patch/write_file/edit_file for file changes " + "instead of cat, shell find/grep, echo, or sed. " "Use -y or --yes flags to avoid interactive prompts. " "For long-running or interactive commands, pass yield_time_ms; " "if the command keeps running, exec returns a session_id that can " diff --git a/tests/tools/test_tool_descriptions.py b/tests/tools/test_tool_descriptions.py new file mode 100644 index 000000000..bb7665e4e --- /dev/null +++ b/tests/tools/test_tool_descriptions.py @@ -0,0 +1,46 @@ +from nanobot.agent.tools.apply_patch import ApplyPatchTool +from nanobot.agent.tools.exec_session import ListExecSessionsTool, WriteStdinTool +from nanobot.agent.tools.filesystem import EditFileTool, ReadFileTool, WriteFileTool +from nanobot.agent.tools.search import FindFilesTool, GrepTool +from nanobot.agent.tools.shell import ExecTool + + +def test_coding_tool_descriptions_steer_editing_priority() -> None: + apply_patch = ApplyPatchTool().description.lower() + edit_file = EditFileTool().description.lower() + write_file = WriteFileTool().description.lower() + + assert "default tool for code edits" in apply_patch + assert "multi-file" in apply_patch + assert "dry_run=true" in apply_patch + assert "edit_file only for small exact replacements" in apply_patch + + assert "small, exact replacement" in edit_file + assert "copied from read_file" in edit_file + assert "prefer apply_patch" in edit_file + + assert "replace an entire file" in write_file + assert "prefer apply_patch" in write_file + + +def test_coding_tool_descriptions_steer_discovery_and_shell_usage() -> None: + read_file = ReadFileTool().description.lower() + find_files = FindFilesTool().description.lower() + grep = GrepTool().description.lower() + exec_tool = ExecTool().description.lower() + write_stdin = WriteStdinTool().description.lower() + list_sessions = ListExecSessionsTool().description.lower() + + assert "find_files/list_dir first" in read_file + assert "before editing" in read_file + assert "prefer it over shell find/ls" in find_files + assert "prefer this over shell grep" in grep + + assert "tests, builds" in exec_tool + assert "prefer read_file/find_files/grep" in exec_tool + assert "apply_patch/write_file/edit_file" in exec_tool + assert "yield_time_ms" in exec_tool + + assert "do not use this to start new commands" in write_stdin + assert "wait_for" in write_stdin + assert "recover a session_id" in list_sessions