首页 / OpenHarness 源码课 / OH02: Tool System

OH02: Tool System OpenHarness · 对标 S02

S02 同一命题:模型只能通过结构化工具碰磁盘、Shell、网络;Harness 负责 schema → 校验 →(权限)→ 执行 → 归一化结果

OpenHarness 用 Pydantic BaseModel 描述入参、BaseTool 统一接口、ToolRegistry 聚合为 API tools 列表;权限门在 OH01 提到的 _execute_tool_call 中实现,本讲只标出挂钩点。

工具链路(与 S02 导图一致):Schema → 权限门 → Execute → 回到 Loop。

🎯 问题(与 S02 一致)

没有工具定义,Loop 只能空转;需要让模型可发现工具(name + description + JSON Schema)、可校验调用参数、可执行并返回字符串化结果供下一轮推理。

🔍 核心抽象:base.py

ToolResult:统一 output: stris_error,供序列化为 API 的 tool_result 块。ToolExecutionContext:至少含 cwd,以及 metadata(如 ask_user_prompt、registry 引用)供工具内部使用。

Python
# tools/base.py(结构节选)

class BaseTool(ABC):
    name: str
    description: str
    input_model: type[BaseModel]

    @abstractmethod
    async def execute(self, arguments: BaseModel, context: ToolExecutionContext) -> ToolResult: ...

    def is_read_only(self, arguments: BaseModel) -> bool:
        return False  # 子类可覆写,供权限层区分只读/写

    def to_api_schema(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "description": self.description,
            "input_schema": self.input_model.model_json_schema(),
        }

class ToolRegistry:
    def register(self, tool: BaseTool) -> None: ...
    def get(self, name: str) -> BaseTool | None: ...
    def to_api_schema(self) -> list[dict[str, Any]]: ...

对应 S02 的 ToolDefinputSchemainput_model.model_json_schema();没有把 hasPermission 挂在每个 tool 上,而是外置到循环里的 PermissionChecker(下一讲 OH03 可对齐 S03)。

🔍 实例:FileReadTool(只读)

独立 FileReadToolInput(BaseModel) 描述字段与约束;is_read_only 恒为 True,权限层可默认放行只读路径规则。execute 内解析路径、拒绝二进制、按行号切片输出——与课内 Read 工具职责等价、实现细节不同(如工具名 read_file)。

🔍 实例:BashTool(副作用)

通过 create_shell_subprocess 跑命令,合并 stdout/stderr,超时与返回码映射为 ToolResult.is_error;输出过长截断,避免撑爆上下文。默认非只读,走更严的权限策略。

🔍 注册:create_default_tool_registry

tools/__init__.py 中集中 registry.register(...) 数十个内置工具;若传入 mcp_manager,再挂上 MCP 资源与 McpToolAdapter——与 S07 同题,OH07 可细讲。

🔗 与 OH01 的衔接

run_query 在拿到模型 tool_use 后:tool.input_model.model_validate(tool_input)permission_checker.evaluate(..., is_read_only=tool.is_read_only(parsed_input))await tool.execute(parsed_input, ToolExecutionContext(cwd=..., metadata=...))。工具层不负责 Hooks 顺序;Pre/Post 在 _execute_tool_call 外层包裹(对齐 S10 话题)。

⚖️ 与课内叙事对照

维度 课内 S02 OpenHarness
入参 schema zod Pydantic BaseModel
权限 可选 hasPermission per tool 统一 PermissionChecker + is_read_only 提示
UI 渲染 可选 React 渲染 tool 消息 TUI 侧消费 StreamEvent,不在 BaseTool 上绑 UI
注册 课内常隐含在引擎配置 显式 ToolRegistry + create_default_tool_registry

🤔 思考题

  1. 为何把 JSON Schema 生成交给 model_json_schema() 而不是手写?模型侧看到的最小契约是什么?
  2. is_read_only 返回假阳性(只读工具标成 false)会导致什么治理问题?
  3. 新增一个内置工具时,最少要改哪几处才能进入默认 registry 并被 API 发现?

📎 延伸阅读

OH01–12 目录 · OH01 · OH03 · S02 · D02