Files
penguin-harness/packages/docs/content/tools.zh.md
T

13 KiB
Raw Blame History

title, description
title description
工具与审批 极简内置工具集的设计与执行契约、Environment 统一收尾规则,以及逐调用审批与 Trace 审计。

设计取向

PenguinHarness 刻意维持一个极小的内置工具集:文件的精确读取与编辑交给专门的文件工具(read_file / edit_file / write_file)——带行号的输出与精确字符串替换比拼 sed 命令更可靠;Shell(exec_command)仍是通用兜底接口,负责运行程序、搜索、装依赖等其余一切。保留下来的每个工具都对得起它占用的 schema Token。

执行契约

所有内置工具实现同一个 BuiltinTool 接口(packages/core/src/environment/tools/types.ts):

interface BuiltinTool {
  name: string;
  definition: ToolDefinitionConfig;
  execute(
    args: Record<string, unknown>,
    ctx: ToolExecutionContext,
  ): AsyncGenerator<OmniMessage, ToolResult | void>;
}

interface ToolExecutionContext {
  workspaceDir: string;
  toolCallId: string;
  signal?: AbortSignal;
  approve?: ApproveFn; // 供需要派生子 Session 的工具转发(审批继承)
}

interface ToolResult {
  stopReason?: StopReason; // 工具自报终态(优先级最低,见下)
  note?: string; // 追加在截断范围之外的终止标记(如退出码)
  images?: string[]; // data URL 图像,附加在文本输出之后
}

工具本身只需 yield 增量的 partial_tool_call_output,收尾由 Environment 集中处理:

  • 流式分帧(start / stop)与 tool_call_id 贯穿;
  • 超时归并;输出超过 maxOutputLength(默认 16000 字符)时截断,保留开头;
  • stop_reason 按优先级归并:用户中断 > 超时 > 工具抛错 > 工具自报;
  • 输出永不为空:没有任何输出时补 [no output];
  • note(如退出码)与图像附加在截断范围之外,长输出被截断时终止标记不会丢失。

工具与 Environment 从不向引擎抛异常:错误一律折叠为 tool_call_output 消息,交给模型阅读并调整下一步。消息结构见 OmniMessage 协议。

配置字段

每个工具由一条 ToolDefinitionConfig 描述:

字段 说明
name 工具名,对应模型产出的 tool_call.name
description 提供给模型的工具说明
parameters 参数 JSON Schema
permission "r" 只读 / "rw" 读写
forModel "vision" / "text-only":按 Session 模型类别装配;缺省对所有模型可用
timeoutMs 单次调用超时(ms),默认 120000;<=0 关闭
maxOutputLength 输出长度上限(字符);<=0 关闭
call_description 条目级开关:控制 parameters 中声明的 description 调用参数(开启时为必填);缺省保留,false 时装配阶段将其连同 required 项从 schema 滤除

内置工具

共 9 个内置工具(装配入口 packages/core/src/environment/tools/registry.ts):

工具 权限 超时(ms) 用途
exec_command rw 120000 在 Workspace 内以 bash -lc 运行命令,流式返回 stdout/stderr
input_command rw 130000 按 process_id 驱动运行中的命令:写 stdin、发 Ctrl-C、轮询输出
read_file r 30000 按 cat -n 风格带行号读取文本文件,以 offset/limit 分页
edit_file rw 30000 对既有文件做精确字符串替换,回显校验片段
write_file rw 30000 新建或整体覆写文件,按需创建父目录
run_subagent rw 600000 把自包含子任务委派给同 Workspace 的子 Agent
input_subagent rw 600000 轮询后台 Subagent,或在其空闲时追加后续 Prompt
read_image r 60000 读取图片并作为图像内容返回(vision 模型)
describe_image r 90000 由 vision_model 代读图片并返回文字回答(text-only 模型)

注意:既有 Agent 已落盘的 tools.builtin 列表按原样冻结(设置页只能编辑行、不能增行):本工具集之前创建的 Agent 不会自动获得文件工具——需手工编辑该 Agent 的 system_config.yaml,把新条目补进去(可从 packages/core/src/state/default-config.ts 的默认定义复制)。

调用描述

命令 / Subagent 类工具(exec_command、input_command、run_subagent、input_subagent)带 description 参数:由模型写一句"本次调用在做什么",CLI 与 Web 在调用运行期间展示给用户。该参数作为普通的 description 属性直接写在各条目的 parameters 中(工具 schema 完全存于可编辑配置),并且是必填的——提供该参数的工具每次调用都会带上它,前端据 schema 即可确定这次调用的展示形态,无需在参数流式过程中猜测;同时要求模型最先输出它。整个参数由条目级 call_description 字段控制——缺省保留,写 call_description: false 时装配阶段将该属性连同其 required 项一起从 schema 中滤除(仅内存内,不改写 YAML)。文件工具不带此参数——其 file_path 参数本身已说明用途。

命令会话

exec_command 先在前台等待;命令超过 yield_time_ms 仍未结束时转入后台,返回已有输出和一个 process_id,之后用 input_command 驱动:

exec_command(cmd)
  ├─ 前台窗口(yield_time_ms,默认 60000)内结束 ──► 完整输出 + 退出码
  └─ 未结束 ──► 转入后台,返回已有输出 + process_id
                     │
    input_command(process_id[, chars]) ──► 写 stdin / 发 Ctrl-C / 轮询
                     └─ 循环驱动,直至命令退出

两个工具的参数(明确键名):

// exec_command
{
  cmd: string;             // 必填:要执行的 shell 命令
  workdir?: string;        // 工作目录;缺省为 Workspace 根,相对路径按其解析
  yield_time_ms?: number;  // 前台等待时长;默认 60000,最小 250,上限受工具超时约束
  description: string;     // 开关开启时必填:一句话说明,最先输出,调用运行期间展示给用户
}

// input_command
{
  process_id: string;      // 必填:exec_command 返回的命令会话 id
  chars?: string;          // 写入 stdin 的字符;单独发送 "\u0003" 传递 Ctrl-C;缺省仅轮询
  yield_time_ms?: number;  // 等待时长;有写入默认 250,空轮询默认 5000
  description: string;     // 开关开启时必填
}

POSIX 上 Ctrl-C 向会话进程组发送 SIGINT,中断前台命令。Windows 无法向管道子进程投递控制台信号,Ctrl-C 因此退化为整棵命令会话进程树的强杀(taskkill /t /f)——前台命令及其启动的所有子进程一并终止,而不是仅中断前台命令。

文件工具

read_file / edit_file / write_file 与 Shell 工具一样以用户完整权限运行:相对路径按 Workspace 解析,也接受绝对路径。三者均为非流式(一次性输出最终结果),从不抛异常——失败以解释性文本收尾,stop_reason 为 failed。

// read_file — cat -n 风格输出(行号、制表符、内容);超长单行会被截断,
// 含 NUL 字节的二进制内容被拒绝并提示改用 Shell / 图像工具。
{
  file_path: string;       // 必填:绝对路径,或相对 Workspace 的路径
  offset?: number;         // 起始行号(1 起);默认 1
  limit?: number;          // 最多返回的行数;默认 2000——未读完时尾部注记提示续读
}

// edit_file — 文件必须已存在;old_string 必须恰好出现一次(或设 replace_all);
// 成功时回显 "Replaced N occurrence(s)" 及改动区域的 git 风格 unified diff
// (每个替换点一个 hunk,相邻替换点合并;replace_all 大量命中时截断为少量 hunk
// 并附 "…and N more replacements" 注记)。
{
  file_path: string;       // 必填
  old_string: string;      // 必填:要替换的原文,须与文件内容(含空白/缩进)完全一致
  new_string: string;      // 必填:替换文本,须与 old_string 不同
  replace_all?: boolean;   // 替换全部出现处;默认 false
}

// write_file — 按需创建父目录;报告 "Created" 或 "Overwrote" 及行数/字节数。
// 覆写时还会附上与旧内容的小型 unified diff;改动过大时改为一行 +X/−Y 摘要。
{
  file_path: string;       // 必填
  content: string;         // 必填:完整文件内容;空字符串创建空文件
}

Subagent

run_subagent 把一段能一次说清的子任务交给子 Agent 执行,同样是两段式:前台窗口(默认 300000ms)过后转入后台并返回 subagent_id,由 input_subagent 轮询或追加 Prompt;子 Agent 的待审批项会在轮询等待期间浮出。

// run_subagent
{
  prompt: string;          // 必填:完整的子任务(含全部上下文与期望的最终产出)
  agent_id?: string;       // 子 Agent;缺省复用当前 Agent
  model_id?: string;       // 子 Session 模型,须与 provider 成对给出;两者都缺省时继承父 Session 的模型
  provider?: string;       // model_id 所属的 provider 组;给出 model_id 时必填
  yield_time_ms?: number;  // 前台等待时长;默认 300000
  description: string;     // 开关开启时必填
}

// input_subagent
{
  subagent_id: string;     // 必填:run_subagent 返回的后台 Subagent id
  prompt?: string;         // 追加任务,仅在子 Session 空闲时接受;缺省仅轮询
  yield_time_ms?: number;  // 等待时长;有追加默认 300000,空轮询默认 10000
  description: string;     // 开关开启时必填
}
  • 深度上限为 1:Subagent 不能再派生 Subagent。
  • 子 Session 跟随父 Session:模型(除非以 model_id/provider 显式指定)、thinking level 与 Workspace 均继承父级,而非 Project 默认值。
  • 子 Session 继承父 Agent 的审批回调,审批模式随父生效。
  • 子 Session 拥有独立 Trace,父 Trace 以 subagent 指针事件链接;子消息带 origin 标记回流到父级消息流。见 Session 与 Trace。

图像工具

read_image 与 describe_image 互斥,按 Session 模型的 vision 标记二选一装配。两者都接受 http(s) URL 或 Workspace 路径,支持 png/jpeg/gif/webp,不超过 5MB。text-only 模型走 describe_image:图片连同提问转交 Project 配置的 vision_model,其文字回答即工具输出。见 模型与 Provider。

// read_image(vision 模型)
{
  source: string;          // 必填:http(s) URL,或 Workspace 内的文件路径
}

// describe_image(text-only 模型)
{
  source: string;          // 必填:同上
  prompt?: string;         // 要对图片提出的问题;缺省为详细描述
}

后台会话上限

会话类型 上限 淘汰策略
命令会话 64 满时优先淘汰已退出者,否则对空闲会话按 LRU 淘汰
Subagent 会话 8 只淘汰已完成者;运行中的从不淘汰,无空位则拒绝派生

审批

每个完整的 tool_call 触发且只触发一次审批决策:

type ApproveFn = (toolCall: OmniMessage<ToolCallPayload>) => Promise<"allow" | "deny">;
使用面 行为
SDK 每次 session.run 传入 approve 回调;未注入时引擎默认全部拒绝(保守策略,避免无人值守下误放行)
CLI --approve 四种模式:allow-all(默认)/ deny-all / read-only / always-ask;read-only 自动放行 permission: "r" 的工具,其余转人工
Web / Server 同样四种模式,按 Session 设置;每次决策前从数据库重读,改模式立即生效;人工决策经 API 送达

deny 会合成一条 aborted 的 tool_call_output(内容为 Tool call denied by user.),模型据此调整策略。每次决策都以 approval_decision 事件写入 Trace,构成完整的审计记录。审批发生在 Agent 运行循环 的工具执行阶段。

自定义与 MCP

system_config.yaml 的 tools.builtin 数组以 ToolDefinitionConfig 同构条目声明工具集。注意语义是整体替换而非合并:整段省略时使用完整默认工具集;一旦写出,默认列表即被替换,要保留的每个工具都必须携带完整定义(含 parameters JSON Schema——工具的参数 schema 完全来自配置)。tools.mcpServers 承载 MCP Server 配置(name + config)——具体 MCP 工具的枚举由后续适配层接管,当前仅保留配置位。见 配置参考。

tools:
  # 写出 builtin 即整体替换默认工具集(此例刻意只保留一个最小工具集)。
  builtin:
    - name: exec_command
      description: Run a shell command in the workspace.
      permission: rw
      # 可选的条目级开关:false 时从 schema 滤除 parameters.properties 里声明的
      # description 调用参数(缺省保留)。
      call_description: false
      timeoutMs: 120000
      maxOutputLength: 16000
      # parameters: 必须携带完整 JSON Schema(默认定义见
      # packages/core/src/state/default-config.ts),此处从略。
  mcpServers: []