Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 77 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,21 @@ docs(README): add contributing guide
## How to Add a New Tool

All tools are located in the `src/workspace/tools/` directory and inherit from
the `BaseTool` base class.
the `BaseTool` base class. All tool methods must return a `ToolResult` object
(located in `src/models/tools/tool_result.py`).

### ToolResult Model

`ToolResult` is the unified tool execution result wrapper:

| Field | Description |
| :------------ | :---------------------------------------------- |
| `success` | Whether execution succeeded (`bool`) |
| `func_name` | Name of the tool method (`str`) |
| `func_kwargs` | Dictionary of invocation parameters (`dict`) |
| `data` | Return data on success (`Any`) |
| `error` | Error message on failure (`str\|None`) |
| `response` | Auto-generated XML string (for LLM consumption) |

### Step Overview

Expand All @@ -217,6 +231,7 @@ the `BaseTool` base class.
2. **Inherit from BaseTool**:

```python
from src.models.tools.tool_result import ToolResult
from src.workspace.tools.base_tool import BaseTool
from src.workspace.workspace import Workspace

Expand All @@ -231,21 +246,31 @@ the `BaseTool` base class.
)
self.func = self.your_method
self.params = BaseTool.extract_params(self.your_method)
self.param_descriptions = {
"param1": "Parameter description",
"param2": "Parameter description"
}

@BaseTool.handle_tool_exceptions
def your_method(self, param1: str, param2: int = 0) -> str:
def your_method(self, param1: str, param2: int = 0) -> ToolResult:
"""
Tool description -- will be generated as LLM-readable documentation.

Parameters
----------
param1: Parameter description
param2: Parameter description (with default value)
"""
# Path operations must be validated through PathValidator
path = self.workspace.path_validator.validate(param1)
# ... tool logic ...
return f'{path}x{param2}'

# Note: Return ToolResult, not raw data
# On success, use make_success_response
return self.make_success_response(
kwargs=locals().copy(),
data=f'{path}x{param2}'
)

# On failure, use make_failed_response
# return self.make_failed_response(
# kwargs=locals().copy(),
# error="Specific error description"
# )
```

3. **Special handling for write operations**: If the tool involves writing
Expand All @@ -254,17 +279,59 @@ the `BaseTool` base class.
`self._validate_mtime(path)`
- Generate a diff and record a `PENDING_AUDIT` snapshot instead of writing
directly to disk
- Still return via
`self.make_success_response(kwargs=locals().copy(), data=...)` or
`self.make_failed_response(kwargs=locals().copy(), error=...)`
- Refer to the implementations of `WriteTool` and `EditTool`

4. **Register the tool**: Import and instantiate your tool in the `register()`
method of `src/core/tool_registry.py`:

```python
def register(self, workspace: Workspace) -> None:
from src.workspace.tools.your_tool import YourTool

self._workspace = workspace

for cls in (
# ... other existing tool classes ...
YourTool,
):
try:
tool = cls(workspace)
if tool.func is None or tool.params is None:
warnings.warn(f"Tool {tool.name} has no registered function callback and parameters", stacklevel=2)
continue
self._tools[tool.name] = tool
self._set_tool_category(tool)
except ValueError:
pass
```

5. **Add tests**: Create corresponding test files under
`tests/workspace/tools/`. At minimum, cover:
- Normal execution paths
- Normal execution paths (assert `result.success is True`, check
`result.data`)
- Failure scenarios (assert `result.success is False`, check `result.error`)
- Parameter validation (e.g., empty parameters, invalid values)
- Path security (e.g., out-of-bounds access)

Test example:

```python
def test_your_tool_success(workspace):
tool = YourTool(workspace)
result = tool.your_method(param1="valid_path", param2=42)
assert result.success is True
assert result.data is not None

def test_your_tool_failure(workspace):
tool = YourTool(workspace)
result = tool.your_method(param1="../outside_path", param2=42)
assert result.success is False
assert "WorkspaceBoundaryError" in result.error
```

---

## Testing Requirements
Expand Down
87 changes: 77 additions & 10 deletions CONTRIBUTING_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,22 @@ docs(README): 添加贡献指南 / add contributing guide

## 如何添加新工具

所有工具位于 `src/workspace/tools/` 目录,继承 `BaseTool` 基类.
所有工具位于 `src/workspace/tools/` 目录,继承 `BaseTool`
基类. 所有工具方法必须返回 `ToolResult` 对象(位于
`src/models/tools/tool_result.py`).

### ToolResult 模型

`ToolResult` 是统一的工具执行结果包装器:

| 字段 | 说明 |
| :------------ | :--------------------------------------- |
| `success` | 执行是否成功 (`bool`) |
| `func_name` | 工具方法名 (`str`) |
| `func_kwargs` | 调用参数字典 (`dict`) |
| `data` | 成功时的返回数据 (`Any`) |
| `error` | 失败时的错误消息 (`str\|None`) |
| `response` | 自动生成的 XML 格式字符串(用于 LLM 消费) |

### 步骤概述

Expand All @@ -201,6 +216,7 @@ docs(README): 添加贡献指南 / add contributing guide
2. **继承 BaseTool**:

```python
from src.models.tools.tool_result import ToolResult
from src.workspace.tools.base_tool import BaseTool
from src.workspace.workspace import Workspace

Expand All @@ -215,36 +231,87 @@ docs(README): 添加贡献指南 / add contributing guide
)
self.func = self.your_method
self.params = BaseTool.extract_params(self.your_method)
self.param_descriptions = {
"param1": "参数说明",
"param2": "参数说明"
}

@BaseTool.handle_tool_exceptions
def your_method(self, param1: str, param2: int = 0) -> str:
def your_method(self, param1: str, param2: int = 0) -> ToolResult:
"""
工具描述 -- 会生成为 LLM 可读的文档.

Parameters
----------
param1: 参数说明
param2: 参数说明(带默认值)
"""
# 路径操作必须通过 PathValidator 验证
path = self.workspace.path_validator.validate(param1)
# ... 工具逻辑 ...
return f'{path}x{param2}'

# 注意: 返回 ToolResult 而非原始数据
# 成功时使用 make_success_response
return self.make_success_response(
kwargs=locals().copy(),
data=f'{path}x{param2}'
)

# 失败时使用 make_failed_response
# return self.make_failed_response(
# kwargs=locals().copy(),
# error="具体的错误描述"
# )
```

3. **写入操作的特殊处理**: 如果工具涉及写入(`write_permission=True`),需要:
- 通过 `self._validate_mtime(path)` 检查文件是否被外部修改
- 生成 diff 并记录 `PENDING_AUDIT` 快照,而非直接写入磁盘
- 返回格式仍然使用
`self.make_success_response(kwargs=locals().copy(), data=...)` 或
`self.make_failed_response(kwargs=locals().copy(), error=...)`
- 参考 `WriteTool` 和 `EditTool` 的实现

4. **注册工具**: 在 `src/core/tool_registry.py` 的 `register()`
方法中导入并实例化你的工具:

```python
def register(self, workspace: Workspace) -> None:
from src.workspace.tools.your_tool import YourTool

self._workspace = workspace

for cls in (
# ... 其他已有的工具类 ...
YourTool,
):
try:
tool = cls(workspace)
if tool.func is None or tool.params is None:
warnings.warn(f"工具{tool.name}没有注册功能回调和参数", stacklevel=2)
continue
self._tools[tool.name] = tool
self._set_tool_category(tool)
except ValueError:
pass
```

5. **补充测试**: 在 `tests/workspace/tools/` 下创建对应的测试文件. 至少覆盖:
- 正常执行路径
- 正常执行路径(断言 `result.success is True`, 检查 `result.data`)
- 失败场景(断言 `result.success is False`, 检查 `result.error`)
- 参数验证(如空参数、非法值)
- 路径安全(如越界访问)

测试示例:

```python
def test_your_tool_success(workspace):
tool = YourTool(workspace)
result = tool.your_method(param1="valid_path", param2=42)
assert result.success is True
assert result.data is not None

def test_your_tool_failure(workspace):
tool = YourTool(workspace)
result = tool.your_method(param1="../outside_path", param2=42)
assert result.success is False
assert "WorkspaceBoundaryError" in result.error
```

---

## 测试要求
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ LLM chat interfaces. Paste LLM-generated tool calls (in XML format), review and
audit dangerous operations, and manage sessions with full history tracking --
all running locally on your machine.

> **Version**: 0.4.1 | **Python**: >=3.14
> **Version**: 0.5.0 | **Python**: >=3.14

---

Expand Down Expand Up @@ -125,8 +125,7 @@ ManualAid registers 12 tools for LLM use via XML function calls:
| -------------- | ------------------------------------------------ |
| `ls` | List directory contents |
| `glob` | Find files by glob pattern |
| `read` | Read file contents (with optional line limit) |
| `read_lines` | Read specific line range from a file |
| `read` | Read file contents with optional line range |
| `stat` | Get file/directory metadata (size, mtime, lines) |
| `exact_search` | Exact string search with case/whole-word options |
| `regex_search` | Regex search with context display |
Expand Down
5 changes: 2 additions & 3 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

ManualAid 提供了一个基于 Textual 的 TUI 控制台,在剪贴板和 LLM 聊天界面之间架起桥梁. 粘贴 LLM 生成的工具调用(XML 格式),审查和审计危险操作,并通过完整的历史追踪管理会话 -- 一切都在本地运行.

> **版本**: 0.4.1 | **Python**: >=3.14
> **版本**: 0.5.0 | **Python**: >=3.14

---

Expand Down Expand Up @@ -112,8 +112,7 @@ ManualAid 注册了 12 个工具供 LLM 通过 XML 函数调用使用:
| -------------- | ----------------------------------------- |
| `ls` | 列出目录内容 |
| `glob` | 通过 glob 模式查找文件 |
| `read` | 读取文件内容(可选行数限制) |
| `read_lines` | 读取文件中指定范围的行 |
| `read` | 读取文件内容,支持指定行范围 |
| `stat` | 获取文件/目录元数据(大小、修改时间、行数) |
| `exact_search` | 精确字符串搜索,支持大小写/全词匹配 |
| `regex_search` | 正则表达式搜索,支持上下文显示 |
Expand Down
76 changes: 76 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,81 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.0] - 2026-05-05

### Added

- **Structured Tool Result**: Introduced `ToolResult` data class as the unified
return type for all tools, replacing inconsistent string and list responses.
The class includes `success`, `data`, `error`, and `response` attributes with
built-in result compression and standardized XML formatting. All tools now
return `ToolResult` objects, enabling consistent upstream error handling
([#133, #142](https://github.com/SunYanbox/ManualAid/issues/133)).
- **File Pattern Filtering for `exact_search`**: Added `file_pattern` parameter
to `exact_search` (default `"*"`), matching the existing `regex_search`
behavior. Allows filtering search scope by file extension or glob pattern
([#134, #140](https://github.com/SunYanbox/ManualAid/issues/134)).
- **Auto-Categorization of Tools**: Tools are now automatically classified as
read-only or write based on the `write_permission` attribute, eliminating
manual category registration and reducing maintenance overhead
([#135, #139](https://github.com/SunYanbox/ManualAid/issues/135)).
- **Range Reading in `read` Tool**: The `read` tool now supports precise line
range reading via `start`, `end` (supports negative indexing), and `context`
parameters, replacing the coarse `max_lines` approach. Display header now
shows the actual line range read (`[Lines start-end / total_lines]`)
([#119, #128](https://github.com/SunYanbox/ManualAid/issues/119)).
- **Parameter Descriptions**: Introduced `param_descriptions` dictionary in
`BaseTool` allowing each tool to provide human-readable parameter
descriptions. Parameter documentation format changed from inline XML to
Markdown list items (`- **name** (type, required/optional): description`)
([#127, #128](https://github.com/SunYanbox/ManualAid/issues/127)).
- **File Size Limit**: Added configurable max file size limit
(`MAX_READ_FILE_SIZE`, default 10MB) to the `read` tool to prevent
out-of-memory errors when reading large files
([#130, #141](https://github.com/SunYanbox/ManualAid/issues/130)).

### Changed

- **Tool Path Parameter Unification**: Renamed path parameters across all tools
to a consistent `path` name — `file_path` (read, write, edit) and
`folder_path` (ls, glob) are now uniformly `path`. This reduces LLM confusion
and injection token length
([#127, #128](https://github.com/SunYanbox/ManualAid/issues/127)).
- **Tool Injection Optimization**: Removed redundant docstring `Parameters`
sections from tool functions, shortened tool descriptions, and streamlined
parameter documentation format. Combined with parameter unification, these
changes significantly reduce system prompt injection length, lowering LLM
hallucination risk
([#127, #128](https://github.com/SunYanbox/ManualAid/issues/127)).
- **Symbol Search Performance**: Replaced per-pattern file traversal with a
single-pass multi-pattern search via `search_content_multi_pattern` API,
eliminating N× I/O overhead. Results are now parsed as structured `list[dict]`
instead of regex-parsing formatted text, fixing the "format-then-parse"
anti-pattern
([#132, #137](https://github.com/SunYanbox/ManualAid/issues/132)).
- **Exception Handling Consolidation**: The `handle_tool_exceptions` decorator
now uniformly wraps all exceptions into `ToolResult(success=False, error=...)`
objects. Removed `ToolErrorResponse` dependency; error messages are now
formatted as `ClassName: Message`
([#133, #142](https://github.com/SunYanbox/ManualAid/issues/133)).

### Fixed

- **`limit` Semantics in Search Tools**: Corrected the `limit` parameter in both
`exact_search` and `regex_search` to count individual match results rather
than files scanned, aligning behavior with user expectations
([#134, #140](https://github.com/SunYanbox/ManualAid/issues/134)).
- **Redundant Warnings in Input Parser**: Removed stale `warnings.warn` calls
and the unused `import warnings` dependency from the input parser
([#138](https://github.com/SunYanbox/ManualAid/issues/138)).

### Removed

- **`read_lines` Tool**: Merged into the enhanced `read` tool with range-reading
support. All `read_lines` functionality is now accessible via `read` with
`start`/`end`/`context` parameters
([#119, #128](https://github.com/SunYanbox/ManualAid/issues/119)).

## [0.4.1] - 2026-05-04

### Added
Expand Down Expand Up @@ -176,6 +251,7 @@ and this project adheres to

_Initial release features and history._

[0.5.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.5.0
[0.4.1]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.4.1
[0.4.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.4.0
[0.3.0]: https://github.com/SunYanbox/ManualAid/releases/tag/v0.3.0
Expand Down
Loading